Ë
    §‘ˆjÂ%  ã            	       ó   — d Z ddlZddlZddlZddlZddlmZmZmZ ddlZddl	m
Z
 ddlmZmZ ddlmZ  ej                   e«      ZdZdZd	Z G d
„ de«      Zd#d„Zdedefd„Zdededefd„Zdededefd„Zdedededdfd„Zd$dedededdfd„Z d$dedededdfd„Z!defd„Z"defd„Z#dedede$fd„Z%d%dedede$fd „Z&deddfd!„Z'deddfd"„Z(y)&aú  Job state store for long-running catalogue jobs (sync, enrich, pair, build).

These jobs used to hold an HTTP connection open for minutes. This module lets a
job run in the background while a client polls `get(job_id)` for progress.

State lives in Postgres rather than Redis. Redis was the first choice -- job
state is ephemeral and a TTL is free there -- but the deployment could not reach
it reliably, and a control plane that fails whenever its own store is
unreachable is worse than one that shares a database already proven to work.
Postgres also gives two things Redis did not: a job survives a restart, and the
"one job per tenant per kind" rule becomes a unique index the database enforces
rather than a lock the application has to remember to release.
é    N)ÚdatetimeÚ	timedeltaÚtimezone)Úerrors)ÚJsonÚRealDictCursor)Úget_master_db_connectioni,  i€Q )ÚqueuedÚrunningc                   ó(   ‡ — e Zd ZdZdefˆ fd„Zˆ xZS )ÚJobAlreadyRunningzÚRaised when a tenant already has a job of this kind in flight.

    Two concurrent runs of the same kind (e.g. two `pair` jobs) would both
    rewrite the same downstream table, so `create` refuses the second one.
    Újob_idc                 ó8   •— || _         t        ‰| �	  d|› �«       y )NzJob already running: )r   ÚsuperÚ__init__)Úselfr   Ú	__class__s     €ú6/var/www/html/strategist-ai/app/services/infra/jobs.pyr   zJobAlreadyRunning.__init__)   s   ø€ ØˆŒÜ‰ÑÐ0°°Ð9Õ:ó    )Ú__name__Ú
__module__Ú__qualname__Ú__doc__Ústrr   Ú__classcell__)r   s   @r   r   r   "   s   ø„ ñð;˜s÷ ;ñ ;r   r   Úreturnc                  ó   — t        «       } 	 | j                  «       5 }|j                  d«       |j                  d«       |j                  d«       d d d «       | j                  «        	 | j                  «        y # 1 sw Y   Œ+xY w# t        $ r) | j                  «        t        j                  dd¬«       ‚ w xY w# | j                  «        w xY w)NaU  
                CREATE TABLE IF NOT EXISTS catalog_jobs (
                    job_id     TEXT PRIMARY KEY,
                    tenant_id  TEXT NOT NULL,
                    kind       TEXT NOT NULL,
                    status     TEXT NOT NULL,
                    percent    INT  NOT NULL DEFAULT 0,
                    step       TEXT NOT NULL DEFAULT '',
                    result     JSONB,
                    error      TEXT,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
                    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
                )
            z¿
                CREATE UNIQUE INDEX IF NOT EXISTS catalog_jobs_active_idx
                ON catalog_jobs (tenant_id, kind)
                WHERE status IN ('queued', 'running')
            z_CREATE INDEX IF NOT EXISTS catalog_jobs_tenant_idx ON catalog_jobs (tenant_id, created_at DESC)z#Could not ensure catalog_jobs tableT©Úexc_info)	r	   ÚcursorÚexecuteÚcommitÚ	ExceptionÚrollbackÚloggerÚerrorÚclose)ÚconnÚcurs     r   Úensure_tabler*   .   s»   € Ü#Ó%€Dð"Ø�[‰[‹]ð 	@˜cØ�K‰Kð ô ð$ �K‰Kð ô ð
 �K‰Kð?ô@÷1	@ð6 	�‰�ð 	�
‰
�÷C	@ð 	@ûô8 ò Ø�‰ŒÜ�‰Ð:ÀTˆÔJØðûð
 	�
‰
�ús.   ŒB œ4A:ÁB Á:BÁ?B Â2B8Â8B; Â;CÚrowc                 óB  — | d   }| d   }t        j                  t        j                  «      |z
  j	                  «       }|dk(  r|t
        kD  rd}| d   | d   | d   |t        | d   «      | d	   xs d
| d   | d   xs d | d   j                  «       |j                  «       dœ
S )NÚstatusÚ
updated_atr   Úlostr   Ú	tenant_idÚkindÚpercentÚstepÚ Úresultr&   Ú
created_at)
r   r0   r1   r-   r2   r3   r5   r&   r6   r.   )r   Únowr   ÚutcÚtotal_secondsÚSTALE_AFTER_SECONDSÚintÚ	timestamp)r+   r-   r.   Úages       r   Ú_row_to_stater>   U   s¹   € Ø�‰]€FØ�\Ñ"€JÜ�<‰<œŸ™Ó%¨
Ñ2×
AÑ
AÓ
C€Cð �Ò˜sÔ%8Ò8Øˆð �h‘-Ø˜Ñ%Ø�F‘ØÜ�s˜9‘~Ó&Ø�F‘Ò!˜rØ�h‘-Ø�W‘Ò% Ø˜,Ñ'×1Ñ1Ó3Ø ×*Ñ*Ó,ñð r   r0   r1   c                 óæ  — t        «        dt        j                  «       j                  › �}t	        j
                  t        j                  «      t        t        ¬«      z
  }t        «       }	 |j                  t        ¬«      5 }|j                  d| ||f«       |j                  dt	        j
                  t        j                  «      t        t        ¬«      z
  f«       	 |j                  d|| |f«       	 d d d «       |j)                  «        	 |j-                  «        |S # t        j                   $ rw |j#                  «        |j                  t        ¬«      5 }|j                  d| |f«       |j%                  «       }d d d «       n# 1 sw Y   nxY wt'        r	|d   «      ‚|«      ‚w xY w# 1 sw Y   Œ¹xY w# t&        $ r ‚ t*        $ r |j#                  «        ‚ w xY w# |j-                  «        w xY w)	NÚjob_)Úseconds©Úcursor_factoryzè
                UPDATE catalog_jobs SET status = 'lost', updated_at = now()
                WHERE tenant_id = %s AND kind = %s
                  AND status IN ('queued', 'running')
                  AND updated_at < %s
            z.DELETE FROM catalog_jobs WHERE created_at < %sz’
                    INSERT INTO catalog_jobs (job_id, tenant_id, kind, status)
                    VALUES (%s, %s, %s, 'queued')
                zæ
                        SELECT job_id FROM catalog_jobs
                        WHERE tenant_id = %s AND kind = %s
                          AND status IN ('queued', 'running')
                        LIMIT 1
                    r   )r*   ÚuuidÚuuid4Úhexr   r7   r   r8   r   r:   r	   r    r   r!   ÚJOB_TTL_SECONDSr   ÚUniqueViolationr$   Úfetchoner   r"   r#   r'   )r0   r1   r   Ústale_beforer(   r)   ÚretryÚexistings           r   Ú_createrM   m   sÀ  € Ü„NØ”D—J‘J“L×$Ñ$Ð%Ð&€FÜ—<‘<¤§¡Ó-´	ÔBUÔ0VÑV€Lä#Ó%€Dð(Ø�[‰[¬ˆ[Ó7ð 	T¸3ð
 �K‰Kð ð
 ˜T <Ð0ô2ð �K‰KØ@Ü—‘œhŸl™lÓ+¬iÄÔ.PÑPÐRôTðTØ—‘ð ð ˜i¨Ð.õ0÷#	Tð@ 	�‰�ð 	�
‰
Œà€Møô) ×)Ñ)ò 
TØ—‘”Ø—[‘[´�[Ó?ð 0À5Ø—M‘Mð #ð
 $ TÐ*ô,ð  %Ÿ~™~Ó/�H÷0÷ 0ñ 0úô (¹h¨°Ñ(:ÓSÐSÈFÓSÐSð
Tú÷+	Tð 	TûôB ò ØÜò Ø�‰ŒØðûð 	�
‰
�ús[   Á+F7 ÂAF+ÃDÃ3F7 Ä9F(Å%FÅ<	F(ÆFÆ
F(Æ(F+Æ+F4Æ0F7 Æ7$GÇG ÇG0c              ƒ   óh   ‡ ‡K  — t         j                  j                  ˆˆ fd„«      ƒ d {  –—† S 7 Œ­w)Nc                  ó   •— t        ‰‰ «      S ©N)rM   )r1   r0   s   €€r   ú<lambda>zcreate.<locals>.<lambda>¡   s   ø€ ´'¸)ÀTÓ2J€ r   ©ÚanyioÚ	to_threadÚrun_sync)r0   r1   s   ``r   ÚcreaterV       s#   ùè ø€ Ü—‘×)Ñ)Ô*JÓK×KÐKÐKúó   „'2«0¬2r   r2   r3   c                 óZ  — 	 t        «       }	 |j                  «       5 }|j                  dt        |«      || f«       ddd«       |j	                  «        |j                  «        y# 1 sw Y   Œ*xY w# |j                  «        w xY w# t        $ r t        j                  d| d¬«       Y yw xY w)a]  Best-effort progress report. Never raises.

    Deliberately synchronous: the job runs in a worker thread, so it can write
    its own progress rather than handing a coroutine back to the event loop.

    This runs alongside work that is already spending real money on model
    calls -- a database hiccup here must not abort an enrichment run.
    zÏ
                    UPDATE catalog_jobs
                    SET status = 'running', percent = %s, step = %s,
                        updated_at = now()
                    WHERE job_id = %s
                Nz!Progress update failed for job %sTr   )	r	   r    r!   r;   r"   r'   r#   r%   Úwarning)r   r2   r3   r(   r)   s        r   ÚtickrZ   ¤   s™   € ðSÜ'Ó)ˆð
	Ø—‘“ð 3 #Ø—‘ð ô
 ˜'“l D¨&Ð1ô3÷3ð �K‰KŒMà�J‰J�L÷3ð 3ûð �J‰J�LûÜò SÜ�‰Ð:¸FÈTˆÖRðSús>   ‚
B �A1 �A%¼A1 ÁB Á%A.Á*A1 Á1BÂB Â!B*Â)B*r5   r&   c           	      óŒ  — t        «       }	 |j                  «       5 }|j                  d|rdnd||�t        |«      nd || f«       d d d «       |j	                  «        |j                  «        y # 1 sw Y   Œ*xY w# t
        $ r+ |j                  «        t        j                  d| d¬«       Y ŒPw xY w# |j                  «        w xY w)Na<  
                UPDATE catalog_jobs
                SET status = %s,
                    percent = CASE WHEN %s IS NULL THEN 100 ELSE percent END,
                    result = COALESCE(%s, result),
                    error = %s,
                    updated_at = now()
                WHERE job_id = %s
            ÚfailedÚdonezCould not finish job %sTr   )
r	   r    r!   r   r"   r#   r$   r%   r&   r'   )r   r5   r&   r(   r)   s        r   Ú_finishr^   ¾   sº   € Ü#Ó%€DðØ�[‰[‹]ð 	P˜cð �K‰Kð ñ $‘(¨°Ø"(Ð"4”$�v”,¸$ÀÀvðOô	P÷	Pð 	�‰Œð
 	�
‰
�÷%	Pð 	Pûô ò GØ�‰ŒÜ�‰Ð.°ÀˆÖFðGûð 	�
‰
�ús:   ŒA: œ)A.ÁA: Á.A7Á3A: Á:1B.Â+B1 Â-B.Â.B1 Â1Cc              ƒ   ón   ‡ ‡‡K  — t         j                  j                  ˆˆ ˆfd„«      ƒ d {  –—†  y 7 Œ­w)Nc                  ó   •— t        ‰‰‰ «      S rP   )r^   )r&   r   r5   s   €€€r   rQ   zfinish.<locals>.<lambda>×   s   ø€ ¬7°6¸6À5Ó+I€ r   rR   )r   r5   r&   s   ```r   Úfinishra   Ö   s    úè ø€ Ü
�/‰/×
"Ñ
"Õ#IÓ
J×JÒJús   …(5­3®5c                 ó.  — t        «       }	 |j                  t        ¬«      5 }|j                  d| f«       |j	                  «       }d d d «       |j                  «        rt        t        |«      «      S d S # 1 sw Y   Œ1xY w# |j                  «        w xY w)NrB   z,SELECT * FROM catalog_jobs WHERE job_id = %s)r	   r    r   r!   rI   r'   r>   Údict)r   r(   r)   r+   s       r   Ú_getrd   Ú   s}   € Ü#Ó%€DðØ�[‰[¬ˆ[Ó7ð 	!¸3Ø�K‰KÐFÈÈ	ÔRØ—,‘,“.ˆC÷	!ð 	�
‰
ŒÙ'*Œ=œ˜c›Ó#Ð4°Ð4÷	!ð 	!ûð 	�
‰
�ús"   ŒB ¢$A6ÁB Á6A?Á;B ÂBc              ƒ   ód   ‡ K  — t         j                  j                  ˆ fd„«      ƒ d {  –—† S 7 Œ­w)Nc                  ó   •— t        ‰ «      S rP   )rd   ©r   s   €r   rQ   zget.<locals>.<lambda>æ   s   ø€ ´$°v³,€ r   rR   rg   s   `r   Úgetrh   å   s#   øè ø€ Ü—‘×)Ñ)Ó*>Ó?×?Ð?Ð?ús   ƒ&0©.ª0Úlimitc                 óN  — t        «       }	 |j                  t        ¬«      5 }|j                  d| |f«       |j	                  «       }d d d «       |j                  «        D �cg c]  }t        t        |«      «      ‘Œ c}S # 1 sw Y   Œ;xY w# |j                  «        w xY wc c}w )NrB   z
                SELECT * FROM catalog_jobs WHERE tenant_id = %s
                ORDER BY created_at DESC LIMIT %s
            )r	   r    r   r!   Úfetchallr'   r>   rc   )r0   ri   r(   r)   Úrowsr+   s         r   Ú_recentrm   é   s�   € Ü#Ó%€DðØ�[‰[¬ˆ[Ó7ð 	"¸3Ø�K‰Kð ð ˜UÐ#ô%ð —<‘<“>ˆD÷	"ð 	�
‰
ŒØ04Ö5¨ŒMœ$˜s›)Õ$Ò5Ð5÷	"ð 	"ûð 	�
‰
�üÚ5s(   ŒB ¢%BÁB Á#B"ÂB
ÂB ÂBc              ƒ   óh   ‡ ‡K  — t         j                  j                  ˆˆ fd„«      ƒ d {  –—† S 7 Œ­w)Nc                  ó   •— t        ‰‰ «      S rP   )rm   )ri   r0   s   €€r   rQ   zrecent.<locals>.<lambda>ø   s   ø€ ´'¸)ÀUÓ2K€ r   rR   )r0   ri   s   ``r   Úrecentrp   ÷   s#   ùè ø€ Ü—‘×)Ñ)Ô*KÓL×LÐLÐLúrW   c                 óô   — t        «       }	 |j                  «       5 }|j                  d| f«       d d d «       |j                  «        |j	                  «        y # 1 sw Y   Œ*xY w# |j	                  «        w xY w)Nz-DELETE FROM catalog_jobs WHERE tenant_id = %s)r	   r    r!   r"   r'   )r0   r(   r)   s      r   Ú_clear_tenantrr   û   sb   € Ü#Ó%€DðØ�[‰[‹]ð 	W˜cØ�K‰KÐGÈ)ÈÔV÷	Wà�‰Œà�
‰
�÷		Wð 	Wûð 	�
‰
�ús!   ŒA% œA°A% ÁA"ÁA% Á%A7c              ƒ   óf   ‡ K  — t         j                  j                  ˆ fd„«      ƒ d{  –—†  y7 Œ­w)zTest-only teardown.c                  ó   •— t        ‰ «      S rP   )rr   ©r0   s   €r   rQ   zclear_tenant.<locals>.<lambda>  s   ø€ ¬=¸Ó+C€ r   NrR   ru   s   `r   Úclear_tenantrv     s    øè ø€ ä
�/‰/×
"Ñ
"Ó#CÓ
D×DÒDús   ƒ&1©/ª1)r   N)NN)é   ))r   ÚjsonÚloggingÚtimerD   r   r   r   rS   Úpsycopg2r   Úpsycopg2.extrasr   r   Úapp.services.infra.databaser	   Ú	getLoggerr   r%   r:   rG   ÚACTIVE_STATUSESr#   r   r*   rc   r>   r   rM   rV   r;   rZ   r^   ra   rd   rh   Úlistrm   rp   rr   rv   © r   r   ú<module>r‚      s’  ðñó Û Û Û ß 2Ñ 2ã Ý ß 0å @à	ˆ×	Ñ	˜8Ó	$€àÐ Ø€à'€ô	;˜	ô 	;ó$ðN�tð  ó ð00�sð 0 #ð 0¨#ó 0ðfL˜Cð L sð L¨só LðS�ð S˜sð S¨#ð S°$ó Sñ4�Cð  ð °Sð ÀDó ñ0K˜ð K dð K¸#ð KÈó Kð5�ó 5ð@�có @ð6�sð 6 3ð 6¨4ó 6ñM˜Cð M¨ð M°Tó Mð˜Sð  Tó ðE #ð E¨$ô Er   