
    $j                        d Z ddlZddlZddlZddlZddlZddlZddlmZ ddl	m
Z
mZmZ ddlmZmZ ddlmZmZ ddlmZ d	d
lmZmZ d	dlmZmZ erddlmZ ddlmZ  ej         e!          Z"dZ#dZ$dZ%ddde&fdZ'e G d d                      Z(e G d d                      Z)e G d d                      Z* G d d          Z+dS )a  
Worker poller for distributed task execution.

Polls the database for pending tasks and executes them using
FOR UPDATE SKIP LOCKED for safe concurrent claiming.

Backend-agnostic: works with any DatabaseBackend implementation
(PostgreSQL via asyncpg, Oracle via oracledb, etc.).
    N)Counter)	AwaitableCallableIterable)	dataclassfield)TYPE_CHECKINGAny   )fq_table_explicit   )DeferOperationRetryTaskAt)StageHolderbind_holder)DatabaseBackend)TenantExtension   i,  i`T  siblingszIterable[Any]returnc                     g }| D ]B}|d         dk    r|d         pd                                 }|r|                    |           C|sdS t          |                              d          d         \  }}|S )a  Pick a representative error message for a parent whose children failed.

    Used when a batch_retain parent transitions to 'failed' because at least
    one child sub-batch failed. Without this, the parent gets a generic
    "One or more sub-batches failed" string and any consumer that reasons
    about errors via error_message (dashboards, alert filters, log
    aggregators) loses the actual cause -- a class of failures that all
    share the same root reason at the child level becomes indistinguishable
    at the parent level.

    Strategy: pick the most common non-empty error_message among failed
    siblings. If they all failed for the same reason (the common case), the
    parent inherits that reason verbatim. If they vary, the most-common one
    is still a useful representative. Falls back to the legacy generic
    string when no failed sibling carries an error_message at all.
    statusfailederror_message zOne or more sub-batches failedr   r   )stripappendr   most_common)r   failed_errorssmsgr   _counts         c/home/rurouni/.hermes/hermes-agent/venv/lib/python3.11/site-packages/hindsight_api/worker/poller.py_summarise_child_error_messagesr$   )   s    "  "M & &X;(""!'R..00 	&  %%% 0//!-00<<Q??BK    c                   r    e Zd ZU dZeed<   eed<   edz  ed<   ded<   eed<   eed	<   d
Ze	ed<   dZ
eed<   dS )ActiveTaskInfozTracking info for an in-flight worker task.

    Carries everything the periodic stats / stuck-task logger needs
    so it can render a useful per-task line without touching the DB.
    op_typebank_idNschemazasyncio.Task[Any]bg_task
started_atstage_holderr   last_stack_dump_thresholdr   	task_type)__name__
__module____qualname____doc__str__annotations__floatr   r.   intr/    r%   r#   r'   r'   G   s           LLLLLL$J     &'s&&&Isr%   r'   c                   H    e Zd ZU dZeed<   eeef         ed<   edz  ed<   dS )ClaimedTaskz9A task claimed from the database with its schema context.operation_id	task_dictNr*   )r0   r1   r2   r3   r4   r5   dictr
   r8   r%   r#   r:   r:   [   sH         CCCH~$Jr%   r:   c                   :    e Zd ZU dZeeef         ed<   	 eed<   dS )SlotAvailabilityzAvailable slot capacity across reserved and shared pools.

    Each operation type with a reservation has its own reserved pool.
    The shared pool (max_slots - sum of reservations) is usable by any type.
    reservedsharedN)r0   r1   r2   r3   r=   r4   r7   r5   r8   r%   r#   r?   r?   d   s>           38n9KKKHHr%   r?   c                   F   e Zd ZdZ	 	 	 	 	 dCdddedeeeef         ged         f         d	e	d
edz  ddde	deee	f         dz  fdZ
deedz           fdZdeedz           deedz           fdZdefdZdDdedefdZdee         fdZd
edz  deee	f         de	dee         fdZd
edz  deee	f         de	dee         fdZded
edz  fdZdeded
edz  fd Zd!ed
edz  ddfd"Zded#d$ded
edz  fd%Zded&d$d'ed
edz  fd(Zd)efd*Zded+efd,ZdEd)ed-edz  fd.Z de	fd/Z!d
edz  de	fd0Z"d1 Z#dFdefd3Z$d4 Z%defd5Z&defd6Z'd7eeeee	f         f         ddfd8Z(d9eee)f         d:eddfd;Z*d<ed=e)d>eddfd?Z+dGd@Z,e-defdA            Z.e-defdB            Z/dS )HWorkerPollera<  
    Polls the database for pending tasks and executes them.

    Uses FOR UPDATE SKIP LOCKED for safe distributed claiming,
    allowing multiple workers to process tasks without conflicts.

    Supports dynamic multi-tenant discovery via tenant_extension.
    Backend-agnostic via DatabaseBackend abstraction.
      N
   backendr   	worker_idexecutorpoll_interval_msr*   tenant_extensionzTenantExtension | None	max_slotsslot_reservationsc	                    || _         || _        || _        || _        || _        |ddlm}	 |rd|ini }
 |	|
          }|| _        || _        ||nddi| _	        ddl
m}  || j                   | _        t          j                    | _        t!                      | _        d| _        t          j                    | _        d	| _        d| _        i | _        i | _        d| _        dS )
a  
        Initialize the worker poller.

        Args:
            backend: Database backend (PostgreSQL, Oracle, etc.)
            worker_id: Unique identifier for this worker
            executor: Async function to execute tasks (typically MemoryEngine.execute_task)
            poll_interval_ms: Interval between polls when no tasks found (milliseconds)
            schema: Database schema for single-tenant support (deprecated, use tenant_extension)
            tenant_extension: Extension for dynamic multi-tenant discovery. If None, creates a
                            DefaultTenantExtension with the configured schema.
            max_slots: Maximum concurrent tasks per worker
            slot_reservations: Per-operation-type reserved slot counts (e.g. {"consolidation": 2,
                "retain": 3}). Reserved slots guarantee capacity for that operation type.
                Remaining slots (max_slots - sum of reservations) form a shared pool usable
                by any operation type. Defaults to {"consolidation": 2} if None.
        Nr   )DefaultTenantExtensionr*   )configconsolidation)OptionalRoutinesr           )_backend
_worker_id	_executor_poll_interval_ms_schemaextensions.builtin.tenantrN   _tenant_extension
_max_slots_slot_reservationsengine.db.optional_routinesrQ   _optional_routinesasyncioEvent	_shutdownset_current_tasks_in_flight_countLock_in_flight_lock_last_progress_log_tasks_completed_since_log_active_tasks_in_flight_by_type_next_schema_idx)selfrF   rG   rH   rI   r*   rJ   rK   rL   rN   rO   rQ   s               r#   __init__zWorkerPoller.__init__~   s   8  #!!1#JJJJJJ ,29h''rF55VDDD!1#!2!>_VWDX 	
 	CBBBBB"2"24="A"A 14 !&|~~"%*+'8:24 &'r%   r   c                 n   K   ddl m | j                                         d{V }fd|D             S )zKGet list of schemas to poll. Returns [None] for default schema (no prefix).r   )DEFAULT_DATABASE_SCHEMANc                 8    g | ]}|j         k    r|j         nd S Nr*   ).0trn   s     r#   
<listcomp>z-WorkerPoller._get_schemas.<locals>.<listcomp>   s,    [[[aAH(???T[[[r%   )rO   rn   rY   list_tenants)rk   tenantsrn   s     @r#   _get_schemaszWorkerPoller._get_schemas   sZ      444444.;;========[[[[SZ[[[[r%   schemasc                   K   | j                                         4 d{V }| j                            |d           d{V r9|                    d           d{V }d |D             cddd          d{V  S t                      }|D ]Y}t          d|          }	 |                    d| d           d{V }|r|                    |           J# t          $ r Y Vw xY w|cddd          d{V  S # 1 d{V swxY w Y   dS )a  Find which schemas have pending work.

        Prefers a server-side PL/pgSQL routine (single DB round-trip,
        ~200ms for 1400+ schemas) when ``public.schemas_with_pending_work()``
        is installed. The presence check goes through
        ``OptionalRoutines.is_installed`` which probes ``pg_proc`` once and
        caches the result, so we don't generate a server-side error on
        every poll cycle when the routine isn't installed.

        Falls back to per-schema Python EXISTS queries (~4ms each) on
        non-PostgreSQL backends or when the routine isn't installed. See
        ``hindsight_api.engine.db.optional_routines`` for the canonical
        install SQL.
        Nschemas_with_pending_workz0SELECT * FROM public.schemas_with_pending_work()c                     h | ]
}|d          S r   r8   )rr   rs     r#   	<setcomp>z4WorkerPoller._scan_active_schemas.<locals>.<setcomp>   s    +++!+++r%   async_operationszSELECT EXISTS(SELECT 1 FROM z? WHERE status = 'pending' AND task_payload IS NOT NULL LIMIT 1))
rS   acquirer]   is_installedfetchra   fq_tablefetchvaladd	Exception)rk   rx   connrowsactiver*   tablehas_works           r#   _scan_active_schemasz!WorkerPoller._scan_active_schemas   s:      =((** 	 	 	 	 	 	 	d,99$@[\\\\\\\\ ,!ZZ(Z[[[[[[[[++d+++	 	 	 	 	 	 	 	 	 	 	 	 	 	 '*eeF! 
 
 !3V<<%)]]Zu Z Z Z& &            H   +

6***    D%	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s<   AC;="C; 6CC;
C$!C;#C$$C;;
DDc                 L  K   | j         4 d{V  | j        }t          | j                  }ddd          d{V  n# 1 d{V swxY w Y   i }d}| j                                        D ]D\  }}|                    |d          }t          d||z
            ||<   |t          ||          z  }Et          | j        
                                          }t          d| j        |z
            }	t          d||z
            }
t          d|	|
z
            }t          ||          S )u  
        Calculate available slots for claiming tasks.

        Each operation type can have reserved slots (via ``slot_reservations``).
        Reserved slots guarantee capacity for that type — they cannot be used by
        other types. The remaining slots (``max_slots - sum(reservations)``) form
        a shared pool usable by any operation type on a first-come basis.

        When an operation type's in-flight count exceeds its reservation, the
        excess tasks are considered to be using shared pool slots.
        Nr   )r@   rA   )re   rc   r=   ri   r[   itemsgetmaxminsumvaluesrZ   r?   )rk   total_in_flightin_flight_snapshotreserved_availabletasks_in_reservedr(   r@   	in_flightsum_reservationsshared_pool_sizetasks_in_sharedshared_availables               r#   _get_available_slotsz!WorkerPoller._get_available_slots   s      ' 	? 	? 	? 	? 	? 	? 	? 	?"3O!%d&=!>!>	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	?
 .0!%!8!>!>!@!@ 	: 	:GX*..w::I*-aI1E*F*Fw'Xy!9!99 t6==??@@q$/4D"DEEa3D!DEEq"2_"DEE);DTUUUUs   >
AA      $@timeoutc                   K   t          j                                                    }	 | j        4 d{V  | j        dk    r	 ddd          d{V  dS 	 ddd          d{V  n# 1 d{V swxY w Y   t          j                                                    |z
  }||k    rdS t          j        d           d{V  )ax  
        Wait for all active background tasks to complete (test helper).

        This is a test-only utility that allows tests to synchronize with
        fire-and-forget background tasks without using sleep().

        Args:
            timeout: Maximum time to wait in seconds

        Returns:
            True if all tasks completed, False if timeout was reached
        TNr   Fg{Gz?)r^   get_event_looptimere   rc   sleep)rk   r   
start_timeelapseds       r#   wait_for_active_tasksz"WorkerPoller.wait_for_active_tasks	  s      +--2244

	&+                (A--                           -                                                      ,..3355
BG'!!u -%%%%%%%%%
	&s   A))
A36A3c                 t  K   |                                   d{V }t          d |j                                        D                       r|j        dk    rg S |                                  d{V }|sg S |                     |           d{V s!| j        dz   t          |          z  | _        g S t          t          |                    }fd|D             }| j        t          |          z  fd|D             fd|D             z   }g }t          |j                  |j        d}g }dt          ffd	}	d
t          t                   ddffd}
|D ]\  }} |	            s nd                                 D             }dk    rt          d          nd}|                     |||           d{V } |
|           |r|}|                    ||f           |                    |            |	            rs|rq|D ]n\  }} |	            s n^|                     |d                                 D                        d{V } |
|           |r|}|                    |           o||dz   t          |          z  | _        ndz   t          |          z  | _        |S )a  
        Claim pending tasks atomically across all tenant schemas,
        respecting per-operation-type slot reservations and shared pool limits.

        Uses FOR UPDATE SKIP LOCKED to ensure no conflicts with other workers.

        Schema iteration is round-robin to prevent one busy tenant from
        starving others. Each poll starts at ``self._next_schema_idx`` and
        wraps around the full list. First pass caps at 1 claim per pool per
        schema so every tenant with pending work gets a fair chance; a second
        pass backfills remaining slots from any schema when there's spare
        capacity. After the call, the offset advances past the last
        schema we serviced (or by 1 if nothing was claimed) so the next
        poll starts at a different position.

        Returns:
            List of ClaimedTask objects containing operation_id, task_dict, and schema
        Nc              3   "   K   | ]
}|d k    V  dS r   Nr8   rr   vs     r#   	<genexpr>z+WorkerPoller.claim_batch.<locals>.<genexpr>9  s&      >>!qAv>>>>>>r%   r   r   c                 &    g | ]\  }}|v 	||fS r8   r8   )rr   ir    active_schemass      r#   rt   z,WorkerPoller.claim_batch.<locals>.<listcomp>L  s+    PPPTQA<O<O1a&<O<O<Or%   c                 ,    g | ]}|d          k    |S r|   r8   rr   xstarts     r#   rt   z,WorkerPoller.claim_batch.<locals>.<listcomp>P  s"    >>>!1r%   c                 ,    g | ]}|d          k     |S r|   r8   r   s     r#   rt   z,WorkerPoller.claim_batch.<locals>.<listcomp>P  s+    AlAlAl_`ab_cfk_k_k!_k_k_kr%   r   c                  f    t          d                                  D                       pdk    S )Nc              3   "   K   | ]
}|d k    V  dS r   r8   r   s     r#   r   zBWorkerPoller.claim_batch.<locals>._has_capacity.<locals>.<genexpr>Y  s&      BBq1uBBBBBBr%   r   )anyr   )remaining_reservedremaining_shareds   r#   _has_capacityz/WorkerPoller.claim_batch.<locals>._has_capacityX  s9    BB&8&?&?&A&ABBBBBZFVYZFZZr%   tasksc                     | D ]C}|j                             dd          }|v r|         dk    r|xx         dz  cc<   >dz  Dd S )Noperation_typeunknownr   r   )r<   r   )r   taskr(   r   r   s      r#   _account_tasksz0WorkerPoller.claim_batch.<locals>._account_tasks[  s~     * *.,,-=yII0005G5PST5T5T&w///14////$)$$* *r%   c                 B    i | ]\  }}|d k    |t          d|          S )r   r   )r   rr   rs   r   s      r#   
<dictcomp>z,WorkerPoller.claim_batch.<locals>.<dictcomp>j  s2    ZZZdaTUXYTYTYQAq		TYTYTYr%   c                 &    i | ]\  }}|d k    ||S r|   r8   r   s      r#   r   z,WorkerPoller.claim_batch.<locals>.<dictcomp>~  s$    RRRdaAPQEEQEEEr%   )r   allr@   r   rA   rw   r   rj   lenlist	enumerater=   boolr:   r   r   _claim_batch_for_schemar   extend)rk   availabilityrx   all_indexedactive_indexedrotated	all_taskslast_serviced_idxschemas_with_workr   r   orig_idxr*   fair_reservedfair_sharedr   r   r   r   r   s                   @@@@r#   claim_batchzWorkerPoller.claim_batch#  s     ( "6688888888>>|4;;==>>>>> 	<CVZ[C[C[I))++++++++ 	I
  $88AAAAAAAA 	%)%:Q%>#g,,$ND!I 9W--..PPPP[PPP %G4>>>>n>>>AlAlAlAl^AlAlAll')	!,"788'.(,:<	[t 	[ 	[ 	[ 	[ 	[ 	[ 	[	*${"3 	* 	* 	* 	* 	* 	* 	* 	* !( 	$ 	$Hf =?? ZZ6H6N6N6P6PZZZM6F6J6J#a!1222PQK66v}kZZZZZZZZEN5!!! =$,!!(((F);<<<U#### =?? 	(0 	($5 ( ( &$} E"::RR.@.F.F.H.HRRRTd        u%%% 1(0%  '''' (%6%:c'll$JD!!%*QY#g,,$>D!r%   reserved_limitsshared_limitc           	         K   	 |                      |||           d{V S # t          $ rK}|rd| dnt          |          }t                              d| j         d| d|            g cY d}~S d}~ww xY w)zNClaim tasks from a specific schema respecting per-type and shared slot limits.N"Worker z" failed to claim tasks for schema : )_claim_batch_for_schema_innerr   r4   loggerwarningrT   )rk   r*   r   r   eschema_displays         r#   r   z$WorkerPoller._claim_batch_for_schema  s      	;;FOUabbbbbbbbb 	 	 	.4E]]]]]#f++NNNmT_mmXfmmjkmmnnnIIIIII		s   ! 
A6A A1+A61A6c                 L  K   t          d|          }| j                                        4 d{V }|                                4 d{V  | j        j                            ||| j        ||           d{V }|s&g cddd          d{V  cddd          d{V  S g }|D ]}|d         }	t          |	t                    rt          j
        |	          n|	}
|d         |
d<   t          |d                   |
d<   |d         }|r||
d<   |                    t          t          |d                   |
|	                     |cddd          d{V  cddd          d{V  S # 1 d{V swxY w Y   	 ddd          d{V  dS # 1 d{V swxY w Y   dS )
zInner implementation for claiming tasks from a specific schema.

        Delegates the SQL claiming logic to backend.ops.claim_tasks() which
        handles backend-specific differences (e.g. Oracle's ORA-02014 workaround).
        r   Ntask_payloadretry_count_retry_countr;   _operation_idr   )r;   r<   r*   )r   rS   r   transactionopsclaim_tasksrT   
isinstancer4   jsonloadsr   r:   )rk   r*   r   r   r   r   all_rowsresultrowpayloadr<   
db_op_types               r#   r   z*WorkerPoller._claim_batch_for_schema_inner  s      +V44=((**  	  	  	  	  	  	  	d''))        !%!2!>!>O# " "                      	  	  	  	  	  	  	  	  	  	  	  	  	  	 #  C!.1G7A'37O7O \
7 3 3 3U\I03M0BIn-14S5H1I1IIo. "%%5!6J! A6@	"23MM#),S-@)A)A&/#)      ?             	  	  	  	  	  	  	  	  	  	  	  	  	  	               	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	sB   F2E.?F$B%E.	F.
E8	8F;E8	<F
F Fr;   c                    K   t          d|          }| j                                        4 d{V }|                    d| d|           d{V  ddd          d{V  dS # 1 d{V swxY w Y   dS )zMark a task as completed.r   N
                UPDATE z
                SET status = 'completed', completed_at = now(), updated_at = now()
                WHERE operation_id = $1
                )r   rS   r   execute)rk   r;   r*   r   r   s        r#   _mark_completedzWorkerPoller._mark_completed  s5     +V44=((** 	 	 	 	 	 	 	d,,  
         	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   !A&&
A03A0r   c                   K   t          d|          }t          |          dk    r
|dd         n|}| j                                        4 d{V }|                                4 d{V  |                    d| d||           d{V  |                     |||           d{V  ddd          d{V  n# 1 d{V swxY w Y   ddd          d{V  dS # 1 d{V swxY w Y   dS )zQMark a task as failed with error message, then propagate to parent if applicable.r     N
                    UPDATE z
                    SET status = 'failed', error_message = $2, completed_at = now(), updated_at = now()
                    WHERE operation_id = $1
                    )r   r   rS   r   r   r   _maybe_update_parent_operation)rk   r;   r   r*   r   r   s         r#   _mark_failedzWorkerPoller._mark_failed  s     +V4403M0B0BT0I0Iete,,}=((** 	V 	V 	V 	V 	V 	V 	Vd'')) 
V 
V 
V 
V 
V 
V 
V 
Vll!  
 !!         99,PTUUUUUUUUU
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V 
V	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	Vs6   C!,?B=+C!=
C	C!
C	C!!
C+.C+child_operation_idc                 0  K   ddl }ddl}t          d|          }	 |                    d| d|                    |                     d{V }|sdS |d         pi }t          |t                    r |j        |          }|                    d          }	|	sdS |d         }
|                    d	| d
|                    |	          |
           d{V }|sdS |	                    d| d|
 |j
        d|	i                     d{V }|rt          d |D                       sdS t          d |D                       }|rB|                    d| d|                    |	          t          |                     d{V  n3|                    d| d|                    |	                     d{V  t                              d|	 d|rdnd d           dS # t"          $ r+}t                              d| d|            Y d}~dS d}~ww xY w)a?  If this operation is a child of a batch_retain, update the parent status when all siblings are done.

        Must be called within an active transaction that has already updated the child's status.
        The memory engine has an equivalent method that runs inside task execution transactions.
        This poller-level version handles the case where a task fails via an unhandled exception
        that bypasses the memory engine's own failure path (e.g. a DB constraint violation that
        rolls back the engine's transaction before it can update the parent).
        r   Nr   z%SELECT result_metadata, bank_id FROM z WHERE operation_id = $1result_metadataparent_operation_idr)   zSELECT operation_id FROM z4 WHERE operation_id = $1 AND bank_id = $2 FOR UPDATEz3
                SELECT status, error_message FROM zn
                WHERE bank_id = $1
                  AND result_metadata::jsonb @> $2::jsonb
                c              3   *   K   | ]}|d          dv V  dS )r   )	completedr   Nr8   rr   r    s     r#   r   z>WorkerPoller._maybe_update_parent_operation.<locals>.<genexpr>  s-      &`&`RSq{6M'M&`&`&`&`&`&`r%   c              3   .   K   | ]}|d          dk    V  dS )r   r   Nr8   r  s     r#   r   z>WorkerPoller._maybe_update_parent_operation.<locals>.<genexpr>"  s+      GGQx[H4GGGGGGr%   r   z
                    SET status = 'failed', error_message = $2, updated_at = now()
                    WHERE operation_id = $1
                    z
                    SET status = 'completed', updated_at = now(), completed_at = now()
                    WHERE operation_id = $1
                    z Poller updated parent operation z to r   r   z (all siblings done)z,Failed to update parent operation for child r   )r   uuidr   fetchrowUUIDr   r4   r   r   r   dumpsr   r   r   r$   r   infor   error)rk   r   r*   r   r   r  r   r   r   r   r)   
parent_rowr   
any_failedr   s                  r#   r   z+WorkerPoller._maybe_update_parent_operation  s      	+V44G	cWWWW		,--       C  !"34:O/3// >",$*_"="="1"5"56K"L"L& )nG  $}}gEggg		-..         J
   "ZZ38  
 
13FGHH       H  3&`&`W_&`&`&`#`#` GGhGGGGGJ ll!  
 II1223H==          ll!  
 II122         KKQ3F Q Q):88{Q Q Q      	c 	c 	c LLaHZaa^_aabbbbbbbbb		cs2   5G  AG  >G  AG  )B5G   
H* HHretry_atr
   c                 x  K   t          d|          }t          |          dk    r
|dd         n|}| j                                        4 d{V }|                    d| d|||           d{V  ddd          d{V  n# 1 d{V swxY w Y   t
                              d| d| d|            dS )	z4Reset task to pending with a future retry timestamp.r   r   Nr   z
                SET status = 'pending', next_retry_at = $2, worker_id = NULL, claimed_at = NULL,
                    retry_count = retry_count + 1, error_message = $3, updated_at = now()
                WHERE operation_id = $1
                Task z scheduled for retry at r   )r   r   rS   r   r   r   r   )rk   r;   r  r   r*   r   r   s          r#   _schedule_retryzWorkerPoller._schedule_retry@  s     +V4403M0B0BT0I0Iete,,}=((** 	 	 	 	 	 	 	d,,   
 
 
 
 
 
 
 
 
	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	`|``X``Q^``aaaaas   #B
BB	exec_datereasonc                 8  K   t          d|          }| j                                        4 d{V }|                    d| d||           d{V  ddd          d{V  n# 1 d{V swxY w Y   t                              d| d| d|            dS )u   Reset task to pending for re-pickup at exec_date without counting as a retry.

        Unlike `_schedule_retry`, this does not bump `retry_count` and does not
        populate `error_message` — defer is intentional backpressure, not a failure.
        r   Nr   z
                SET status = 'pending', next_retry_at = $2, worker_id = NULL, claimed_at = NULL,
                    updated_at = now()
                WHERE operation_id = $1
                r  z deferred until r   )r   rS   r   r   r   r  )rk   r;   r  r  r*   r   r   s          r#   _defer_operationzWorkerPoller._defer_operationR  s^      +V44=((** 
	 
	 
	 
	 
	 
	 
	d,,   	 	 	 	 	 	 	 	 	
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 	OLOO)OOvOOPPPPPs   "A&&
A03A0r   c           
         K   j                             dd          }j                             dd          j                             dd          }t          d|           }t          j                             |                    } j        4 d{V  t          |j        |t          j
                    ||           j        j        <    xj        d	z  c_         j                            d
          d	z    j        <   ddd          d{V  n# 1 d{V swxY w Y   |                     fd           dS )z<Execute a single task as a background job (fire-and-forget).typer   r   r)   zqueued.)stageN)r(   r)   r*   r+   r,   r-   r/   r   r   c                 ^    t          j                            j                            S rp   )r^   create_task_cleanup_taskr;   )_r   rk   r   s    r#   <lambda>z+WorkerPoller.execute_task.<locals>.<lambda>  s'    G,?@R@RSWSdft@u@u,v,v r%   )r<   r   r   r^   r  _execute_task_innerre   r'   r*   r   	monotonicrh   r;   rc   ri   add_done_callback)rk   r   r/   r)   holderr+   r   s   ``    @r#   execute_taskzWorkerPoller.execute_taskf  s     N&&vy99	++,<iHH.$$Y	::
 #8Y#8#8999
 %d&>&>tV&L&LMM ' 	i 	i 	i 	i 	i 	i 	i 	i4B&{>++##5 5 5Dt01 !!Q&!!6:6M6Q6QR`bc6d6dgh6hD#N3	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	i 	!!"v"v"v"v"v"vwwwwws   A0D!!
D+.D+r   c                 f  K   | j         4 d{V  || j        v rr| j                            |d           | xj        dz  c_        | j                            |d          }|dk    r&|dz
  | j        |<   | j        |         dk    r| j        |= 	 ddd          d{V  dS # 1 d{V swxY w Y   dS )z+Remove task from tracking after completion.Nr   r   )re   rh   poprc   ri   r   )rk   r;   r   counts       r#   r  zWorkerPoller._cleanup_task  s     ' 	D 	D 	D 	D 	D 	D 	D 	Dt111"&&|T:::%%*%%/33NAFF199>CaiD+N;.~>!CC 3NCC	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	Ds   A<B  
B*-B*r  c           
      8  K   |j                             dd          }|j                             dd          }|1t          |           d| |_        t	          j                    |_        	 |j        r
d|j         nd}t          	                    d|j
         d	| d
| | d           |j        r|j        |j         d<   |                     |j                    d{V  t          	                    d|j
         d           dS # t          $ r=}|                     |j
        |j        |j        |j                   d{V  Y d}~dS d}~wt           $ rE}|                     |j
        |j        t'          |          |j                   d{V  Y d}~dS d}~wt(          $ rw}t                              d|j
         d|            t-          j                     |                     |j
        t'          |          |j                   d{V  Y d}~dS d}~ww xY w)u  Inner task execution with retry/fail handling.

        Tasks that want to be retried raise RetryTaskAt; the poller sets next_retry_at
        and resets status to 'pending'. All other exceptions are marked as failed immediately.
        Non-retryable failures (e.g., file_convert_retain) are handled by the executor
        internally — it marks the operation as failed and returns normally.
        r  r   r)   Nz	executor.z	, schema=r   zExecuting task z (type=z, bank=)rW   r  z execution finishedz	 failed: )r<   r   r   r  r   r  
updated_atr*   r   debugr;   rU   r   r  r  r  r   r  r  r4   r   r  	traceback	print_excr   )rk   r   r  r/   r)   schema_infor   s          r#   r  z WorkerPoller._execute_task_inner  s      N&&vy99	.$$Y	::
 2y22FL $ 0 0F	L7;{J3dk333KLLn4+<nnYnnW^n`knnnooo{ 8,0Ky)..000000000LLG!2GGGHHHHH 	_ 	_ 	_''(91;RVR]^^^^^^^^^^^^^^^ 	[ 	[ 	[&&t'8!*c!ffdkZZZZZZZZZZZZZZZ 	L 	L 	LLL@!2@@Q@@AAA!!!##D$5s1vvt{KKKKKKKKKKKKKKK	Ls2   -BD 
H2EH:FH"A,HHc           
        K   |                                   d{V }d}|D ]"}	 t          d|          }|                     |           d{V }||z  }| j                                        4 d{V }|                    d| d| j                   d{V }ddd          d{V  n# 1 d{V swxY w Y   |r't          |                                d                   nd}||z  }# t          $ rJ}	|rd| dnt          |          }
t                              d| j         d	|
 d
|	            Y d}	~	d}	~	ww xY w|dk    r&t                              d| j         d| d           |S )a  
        Recover tasks that were assigned to this worker but not completed.

        This handles the case where a worker crashes while processing tasks.
        On startup, we reset any tasks stuck in 'processing' for this worker_id
        back to 'pending' so they can be picked up again.

        Also recovers batch API operations that were in-flight.

        If tenant_extension is configured, recovers across all tenant schemas.

        Returns:
            Number of tasks recovered
        Nr   r    
                        UPDATE z
                        SET status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = now()
                        WHERE status = 'processing' AND worker_id = $1 AND result_metadata->>'batch_id' IS NULL
                        r   r   z$ failed to recover tasks for schema r   z recovered z stale tasks from previous run)rw   r   _recover_batch_operationsrS   r   r   rT   r7   splitr   r4   r   r   r  )rk   rx   total_countr*   r   batch_countr   r   r"  r   r   s              r#   recover_own_taskszWorkerPoller.recover_own_tasks  s      ))++++++++ 	u 	uFu !3V<< %)$B$B6$J$JJJJJJJ{*  =0022       d#'<< %  
 $ $      F                           4:@FLLNN2.///qu$ u u u28!IVc&kksss^lsspqssttttttttu
 ??KKi$/iikiiijjjs<   AC.4&B,C.,
B6	6C.9B6	:3C..
E8?D==Ec           	      D  K   t          d|          }	 | j                                        4 d{V }|                    d| d           d{V }ddd          d{V  n# 1 d{V swxY w Y   |sdS d}|D ]Q}t	          |d                   }|d         }|d         }	t          |	t                    rt          j        |	          }	|	                    d	          }
|	                    d
d          }t          
                    d| d|
 d|            t          |t                    rt          j        |          }n|}| j                                        4 d{V }|                    d| d|           d{V  ddd          d{V  n# 1 d{V swxY w Y   |dz  }t          
                    d| d           S|S # t          $ rB}|rd| dnt	          |          }t                              d| d|            Y d}~dS d}~ww xY w)aM  
        Recover batch API operations that were in-flight when worker crashed.

        Finds operations with batch_id in metadata and re-submits them as tasks
        so polling can resume.

        Args:
            schema: Database schema to recover from

        Returns:
            Number of batch operations recovered
        r   Nza
                    SELECT operation_id, task_payload, result_metadata
                    FROM z
                    WHERE status = 'processing'
                      AND result_metadata ? 'batch_id'
                      AND task_payload IS NOT NULL
                    r   r;   r   r   batch_idbatch_provideropenaiz)Recovering batch operation: operation_id=z, batch_id=z, provider=r+  z
                        SET status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = now()
                        WHERE operation_id = $1
                        r   zBatch operation z# reset to pending for re-processingr   z.Failed to recover batch operations for schema r   )r   rS   r   r   r4   r   r   r   r   r   r  r   r   r  )rk   r*   r   r   r   	recoveredr   r;   r   r   r3  r4  r<   r   r   s                  r#   r-  z&WorkerPoller._recover_batch_operations  s      +V44;	},,.. 
 
 
 
 
 
 
$!ZZ         
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  qI #b #b"3~#677">2"%&7"8 os33 B&*j&A&AO*..z::!0!4!45Ex!P!P~~~Ya~~n|~~  
 lC00 - $
< 8 8II ,I  =0022       d,, %  
 %                                   Q	`|```aaaa 	 	 	.4E]]]]]#f++NLL_.__\]__```11111	se   G  A%G %
A//G 2A/3G :C.G (!F	G 
F%	%G (F%	))G 
H7HHc                 
  K   |                                   d{V  | j        r6d                    d | j                                        D                       nd}t	          d| j        t          | j                                                  z
            }t          	                    d| j
         d| j         d| d	| d
	           | j                                        s}	 |                                  d{V }|r<i }t                      }d}|D ]y}|j                            dd          }|j                            dd          }	|                    |d          dz   ||<   |                    |j                   |	dk    r|dz  }zd                    d |                                D                       }
d                    d |D                       }t          	                    d| j
         dt'          |           d| d|
 d| d
           |D ]}|                     |           d{V  t	 t+          j        | j                                        | j        dz             d{V  n# t*          j        $ r Y nw xY w|                                  d{V  n# t*          j        $ r& t          	                    d| j
         d           Y nt8          $ r\}t                              d| j
         d|            t=          j                     t+          j         d           d{V  Y d}~nd}~ww xY w| j                                        }t          	                    d| j
         d           dS )z
        Main polling loop with fire-and-forget task execution.

        Continuously polls for pending tasks, spawns them as background tasks,
        and immediately continues polling (up to slot limits).
        N, c              3   *   K   | ]\  }}| d | V  dS )=Nr8   rr   kr   s      r#   r   z#WorkerPoller.run.<locals>.<genexpr>=  s0      MMTQjjQjjMMMMMMr%   noner   r   z" starting polling loop (max_slots=z, reservations=[z], shared_pool=r$  r  r   r   r   rP   c              3   *   K   | ]\  }}| d | V  dS ):Nr8   r;  s      r#   r   z#WorkerPoller.run.<locals>.<genexpr>W  s0      )T)TAQ****)T)T)T)T)T)Tr%   c              3   "   K   | ]
}|r|nd V  dS defaultNr8   r  s     r#   r   z#WorkerPoller.run.<locals>.<genexpr>Y  s+      +X+Xa,AAA	+X+X+X+X+X+Xr%   z	 claimed z tasks (z consolidation): z (schemas: i  )r   z polling loop cancelledz error in polling loop: z polling loop stopped)!r1  r[   joinr   r   rZ   r   r   r   r  rT   r`   is_setr   ra   r<   r   r   r*   r   r  r^   wait_forwaitrV   TimeoutError_log_progress_if_dueCancelledErrorr   r  r'  r(  r   )rk   reservations_strshared_poolr   
task_typesschemas_seenconsolidation_countr   rs   r(   	types_strschemas_strr   s                r#   runzWorkerPoller.run3  s      $$&&&&&&&&& RVQhtDIIMMT-D-J-J-L-LMMMMMMnt 	 !T_s43J3Q3Q3S3S/T/TTUUkdo k k/k k;Kk k\gk k k	
 	
 	

 .'')) 5	'4'"..00000000 13J47EEL*+' % 5 5 N..vyAA"&."4"45Ey"Q"Q(2q!(<(<q(@
1$((555"o55/14/ $		)T)TAQAQASAS)T)T)T T TI"&))+X+X<+X+X+X"X"XKKKg$/ g gCJJ g g/g gBKg gXcg g g   !& 6 6"//5555555555 !*++-- $ 6 =           +   D //1111111111)   NdoNNNOOO ' ' 'StSSPQSSTTT#%%%mA&&&&&&&&&&&&&&	'c .'')) 5	'n 	DdoDDDEEEEEsD   $EJ* >;I: 9J* :J	J* JJ* *2M	M'AL>>M      >@c                   K   t                               d| j         d           | j                                         t          j                                                    }t          j                                                    |z
  |k     r | j        4 d{V  | j	        }d | j
                                        D             }ddd          d{V  n# 1 d{V swxY w Y   |dk    r%t                               d| j         d           dS t                               d| j         d| d           |r+t          j        |d	t
          j        
           d{V \  }}nt          j        d	           d{V  t          j                                                    |z
  |k      t                               d| j         d| d           | j        4 d{V  t!          | j
                                                  D ]7\  }}|j                                        s|j                                         8	 ddd          d{V  dS # 1 d{V swxY w Y   dS )z
        Signal shutdown and wait for current tasks to complete.

        Args:
            timeout: Maximum time to wait for in-flight tasks (seconds)
        r   z initiating graceful shutdownNc                     g | ]	}|j         
S r8   )r+   rr   r  s     r#   rt   z2WorkerPoller.shutdown_graceful.<locals>.<listcomp>  s    &\&\&\t|&\&\&\r%   r   z graceful shutdown completez waiting for z in-flight tasksg      ?)r   return_whenz shutdown timeout after zs, cancelling remaining tasks)r   r  rT   r`   ra   r^   r   r   re   rc   rh   r   rF  FIRST_COMPLETEDr   r   r   r   r+   donecancel)	rk   r   r   r   active_task_objectsrX  r  r;   r  s	            r#   shutdown_gracefulzWorkerPoller.shutdown_graceful~  s      	LdoLLLMMM +--2244
$&&++--
:WDD+ ] ] ] ] ] ] ] ] 1	&\&\@R@Y@Y@[@[&\&\&\#] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] ] A~~RdoRRRSSSKK[$/[[	[[[\\\ # ) '-@#[b[r s s sssssssaamC((((((((( $&&++--
:WDD" 	ppp'pppqqq ' 	* 	* 	* 	* 	* 	* 	* 	*&*4+=+C+C+E+E&F&F * *"d|((** *L'')))*	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	*s%   +C
C%(C%$AI
I!$I!c                    K   t          j                     }|| j        z
  t          k     rdS || _        	 | j        4 d{V  | j        }t          | j                  }t          | j                  }ddd          d{V  n# 1 d{V swxY w Y   d}g }| j        	                                D ]b\  }}|
                    |d          }	t          d||	z
            }
|t          ||	          z  }|                    | d|	 d| d|
 d           ct          | j                                                  }t          d| j        |z
            }t          d||z
            }t          d||z
            }|rd                    |          nd}i }|                                D ],}|j        |j        f}|
                    |d          d	z   ||<   -d
 |	                                D             }|rd                    |dd                   nd}t)          |          dk    r|dt)          |          dz
   dz  }|                                  d{V }t)          |          }|                     |           d{V }d |                                D             }||z  }d}i }i }| j                                        4 d{V }|D ]7}t3          d|          }	 |                    d| d           d{V } n# t6          $ r g } Y nw xY w| D ]}!|!d         pd}|                    |ddddd          }"|"dxx         |!d         z  cc<   |"dxx         |!d         z  cc<   |"dxx         |!d         z  cc<   |"dxx         |!d         z  cc<   ||!d         z  }	 |                    d| d           d{V }#n# t6          $ r g }#Y nw xY w|#D ].}$|$d         pd}%|
                    |%d          |$d         z   ||%<   /9	 ddd          d{V  n# 1 d{V swxY w Y   g }&|	                                D ]*\  }%}'|%| j        k    r|&                    |% d|'            +|&rd                    |&          nd}(|                                 })|                                 }*t)          |          }+tA          d |D                       },d                    |,dd                   }-t)          |,          dk    r|-dt)          |,          dz
   dz  }-tB          "                    d | j         d!| d| j         d"| d#| d| d| d$| d%|+ d| d&|- d'|( d(|) d)|* d*|            |dk    r| #                    |           | $                    |t          j%                    +           | &                                 d{V  dS # t6          $ r(}.tB          '                    d,|.            Y d}.~.dS d}.~.ww xY w)-a  Log progress stats every PROGRESS_LOG_INTERVAL seconds.

        Emits four kinds of lines:
          * [WORKER_STATS]  - aggregate slots / pool / global pending counts
          * [WORKER_TASK]   - one line per in-flight task with age + stage
          * [STUCK_STACK]   - async stack trace for tasks past stuck thresholds
          * [DB_WAITS]      - any non-idle hindsight session waiting on a lock
        Nr   r:  /z(avail=r$  r8  r=  r   c                 0    g | ]\  \  }}}| d | d| dS )r?  (r$  r8   )rr   opbankcnts       r#   rt   z5WorkerPoller._log_progress_if_due.<locals>.<listcomp>  s9    ```"dS"44t44c444```r%   rE   z +z morec                     h | ]	}|j         
S r8   rq   rU  s     r#   r~   z4WorkerPoller._log_progress_if_due.<locals>.<setcomp>  s    (W(W(W(W(W(Wr%   r   a  
                            SELECT
                                operation_type,
                                COUNT(*) AS total,
                                SUM(CASE WHEN task_payload IS NULL THEN 1 ELSE 0 END) AS payload_null,
                                SUM(CASE WHEN next_retry_at IS NOT NULL AND next_retry_at > now()
                                    THEN 1 ELSE 0 END) AS retry_blocked,
                                SUM(CASE WHEN worker_id IS NOT NULL THEN 1 ELSE 0 END) AS assigned
                            FROM z
                            WHERE status = 'pending'
                            GROUP BY operation_type
                            r   r   )totalpayload_nullretry_blockedassignedrd  re  rf  rg  zb
                            SELECT worker_id, COUNT(*) as count
                            FROM z
                            WHERE status = 'processing'
                            GROUP BY worker_id
                            rG   r"  r?  c              3   "   K   | ]
}|r|nd V  dS rA  r8   r  s     r#   r   z4WorkerPoller._log_progress_if_due.<locals>.<genexpr>+  s+      !R!RAq"7!!i!R!R!R!R!R!Rr%      z[WORKER_STATS] worker=z slots=z | reserved: [z] | shared=z) | global: pending=z
 (queried=z
 schemas: z) | others: z	 | pool: z	 | proc: z | my_active: )nowzFailed to log progress stats: )(r   rf   PROGRESS_LOG_INTERVALre   rc   r=   ri   rh   r[   r   r   r   r   r   r   r   rZ   rC  r(   r)   r   rw   r   rS   r   r   r   r   
setdefaultrT   _format_pool_stats_format_proc_statssortedr   r  _log_pending_breakdown_log_per_task_linesr  _log_db_waitsr&  )/rk   rj  r   in_flight_by_typeactive_tasksr   reserved_partsr(   r@   type_in_flighttype_availabler   r   r   r   reserved_strtask_groupsr  keyprocessing_infoprocessing_strrx   total_schema_countschemas_with_pendingschemas_with_active_tasksschemas_to_queryglobal_pendingall_worker_countspending_breakdownr   r*   r   breakdown_rowsbrbucketworker_rowswrwidother_workersrb  
others_strpool_strproc_strqueried_countqueried_listrP  r   s/                                                  r#   rH  z!WorkerPoller._log_progress_if_due  s	      ikk((+@@@F"%Y	?+ 8 8 8 8 8 8 8 8 1	$()@$A$A!#D$6778 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 !"N%)%<%B%B%D%D h h!!2!6!6w!B!B!$Q>(A!B!B!S>%B%BB!%%&f&f>&f&fH&f&fUc&f&f&fgggg"4#:#A#A#C#CDD"1do8H&HII!!Y1B%BCCO"1&6&HII8FR499^444FL 79K$++-- ? ?|T\2#.??3#:#:Q#>C  ``KL]L]L_L_```O@O[TYYss';<<<U[N?##b(("Gs?';';b'@"G"G"GG !--////////G!$W *.)B)B7)K)K#K#K#K#K#K#K  )X(WATATAVAV(W(W(W%36OON02;=},,.. 4] 4] 4] 4] 4] 4] 4]$. 3] 3]F$%7@@E,/3zz  #(     0 0 * * * * * * % , , ,)+, - 	6 	6"$%5"6"C)!2!=!=#q!VWef%g%g" " w2g;6~..."^2DD...///2o3FF///z***bn<***&"W+5
),0JJ "'     - - ' ' ' ' ' ' % ) ) )&()) ] ] o:1B1F1FsA1N1NQST[Q\1\)#..]c3]4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4] 4]l M-3355 9 9S$/))!((C#8885BN=111J ..00H..00H 011M!!R!RAQ!R!R!RRRL))L"$566K<  2%%AC$5$5$:AAAAKK	/ 	/ 	/"	/ 	/%)_	/ 	/*	/ 	/ *	/ 	/ -=	/ 	/ FV	/ 	/ $2		/ 	/
 *	/ 	/
 -?	/ 	/
 KV	/ 	/ &	/ 	/ "	/ 	/ "	/ 	/ -	/ 	/  " !!++,=>>> $$\t~7G7G$HHH $$&&&&&&&&&&& 	? 	? 	?LL=!==>>>>>>>>>	?s   W  0B0W 
BW BH:W 
P!L PLPLBP!O POPO6PW 
P##W &P#'F$W 
W?W::W?c                     	 ddl }ddl}|                    |j                  }|j        }|j        dk    r|dz  }|dz  }d|dS # t          $ r(}t                              d|            Y d}~d	S d}~ww xY w)
zVRender lightweight process memory stats. Returns 'unavailable' if introspection fails.r   Ndarwini   i   zrss_mb=.0fzProcess stats unavailable: unavailable)	resourcesys	getrusageRUSAGE_SELF	ru_maxrssplatformr   r   r&  )rk   r  r  usagerssrss_mbr   s          r#   rn  zWorkerPoller._format_proc_statsM  s    	!OOO JJJ&&x';<<E/C|x''tK(F)V)))) 	! 	! 	!LL:q::;;; =====	!s   AA 
A8A33A8c                    	 | j                                         }t          |d          r|                                nt	          t          |dg                     }t          |d          r|                                nd}t          |d          r|                                nd}t          |d          r|                                nd}t          |dd          }|$t          |d          r|	                                nd}|t          |d	d          nd}|t	          |          nd}	d
| g}
|||

                    d| d|            |3|

                    d|            |

                    d||z
              ||

                    d|            |	|

                    d|	            d                    |
          S # t          $ r(}t                              d|            Y d}~dS d}~ww xY w)zRRender connection pool stats. Returns 'unavailable' if pool can't be introspected.get_size_holdersget_idle_sizeNget_min_sizeget_max_size_queueqsize_getterszsize=zlimits=-zidle=zin_use=zfree_holders=zpending_acquires= zPool stats unavailable: r  )rS   get_poolhasattrr  r   getattrr  r  r  r  r   rC  r   r   r&  )rk   poolsizefreemin_sizemax_sizequeuefree_holdersgetterspending_acquirespartsr   s               r#   rm  zWorkerPoller._format_pool_stats_  sB   	!=))++D '.dJ&?&?g4==???SQUWaceIfIfEgEgD+24+I+IS4%%'''tD.5dN.K.KUt((***QUH.5dN.K.KUt((***QUHD(D11E -2,='%QXBYBY,=5;;===_cL:?:KgeZ666QUG/6/Bs7|||#T^^$E#(<<x<<(<<===^T^^,,,4td{44555';\;;<<<+C1ACCDDD88E??" 	! 	! 	!LL7A77888 =====	!s   GG 
HH  H	breakdownc                 h   |sdS g }t          |          D ]k}||         }|d         |d         z
  |d         z
  |d         z
  }|                    | d|d          d| d|d          d	|d          d
|d                     lt                              dd                    |                      dS )a  Emit one [PENDING_BREAKDOWN] line bucketing pending rows by claimability.

        Each bucket mirrors a predicate in the claim query:
          * payload_null   - row has no task_payload (e.g. batch_retain parent
                             whose reconciliation never fired); claim query
                             skips it forever
          * retry_blocked  - next_retry_at is still in the future
          * assigned       - worker_id already set; another worker owns it

        ``claimable`` is the residual that *should* be picked up on the next
        poll. If ``claimable > 0`` while workers report free slots, the bug is
        somewhere else (lock contention, tenant discovery, etc.) - this line
        narrows the search.
        Nrd  re  rf  rg  z: total=z claimable=z payload_null=z retry_blocked=z
 assigned=z[PENDING_BREAKDOWN] z | )ro  r   r   r  rC  )rk   r  r  r(   b	claimables         r#   rp  z#WorkerPoller._log_pending_breakdown  s     	Fi(( 	 	G'"A'
Q~%66?9KKaPZm[ILL , ,AgJ , ,9 , , !. 1, ,BCOBT, ,jM, ,   
 	>5::e+<+<>>?????r%   rt  rj  c                    |sdS t          |                                fdd          }|D ]\  }}|j        z
  }|j        }||j        nd}|
|j        z
  nd}	|t          k    rdnd}
|j        r
d	|j         nd}t          	                    d
|
 d| d|j
         d|j         d|j         | d|dd| d|	dd           |                     |||           dS )zEmit one [WORKER_TASK] line per in-flight task and dump stuck stacks.

        Sorted by age desc so the oldest (most likely stuck) tasks appear first.
        Nc                 4    | d         j         z
  | d         fS )Nr   r   )r,   )kvrj  s    r#   r  z2WorkerPoller._log_per_task_lines.<locals>.<lambda>  s    C"Q%"22BqE: r%   T)rz  reverser   rR   z	[STUCK?] r   z schema=z[WORKER_TASK] zop= type=z	 op_type= bank= age=r  s stage=z stage_age=r    )ro  r   r,   r-   r  r%  STUCK_STACK_INITIAL_THRESHOLD_Sr*   r   r  r/   r(   r)   _maybe_dump_stuck_stack)rk   rt  rj  orderedop_idr  age_sr  r  stage_age_sstuck_markerschema_parts     `         r#   rq  z WorkerPoller._log_per_task_lines  s   
  	F   ::::
 
 
 # 	= 	=KE4$/)E&F$*$6FLLIE7=7I3!222sK*/3R*R*R;;XZL6:kI2T[222rKKKO O O% O Ot~ O O<O O/3|O=HO ONO O*/O O<GNO O O   ((e<<<<	= 	=r%   r  r  r  c                 H   |t           k     rdS t           }t           }||k    r#|t          k    r|}|dz  }||k    r|t          k    ||j        k    rdS ||_        	 t          j                    }|j                            |d           |j        r|j        j        nd}t          
                    d| d|j         d|j         d	|d
d| d| d|                                            dS # t          $ r+}t                              d| d|            Y d}~dS d}~ww xY w)a9  Dump a coroutine stack for tasks that crossed a stuck threshold.

        Each task gets one dump per threshold (5min, 10min, 20min, 40min...),
        gated by `info.last_stack_dump_threshold` so logs don't flood for tasks
        that legitimately take a long time (large LLM jobs, schema-retry loops).
        Nr      )filelimitr   z[STUCK_STACK] op=r  r  r  r  zs threshold=r  
zFailed to capture stack for r   )r  STUCK_STACK_MAX_THRESHOLD_Sr.   ioStringIOr+   print_stackr-   r  r   r   r/   r)   getvaluer   r&  )	rk   r  r  r  	thresholdcrossedbufr  r   s	            r#   r  z$WorkerPoller._maybe_dump_stuck_stack  s    222F 4	15  Y2M%M%MGNI 5  Y2M%M%M d444F)0&
	F+--CL$$#R$888/3/@OD%++iENNYE Y Y Y Yt| Y YXY Y.5Y Y?DY YHKY Y      	F 	F 	FLLDDDDDEEEEEEEEE	Fs   BC, ,
D!6 DD!c                   K   | j         j        dk    rdS 	 | j                                         4 d{V }|                    d           d{V }ddd          d{V  n# 1 d{V swxY w Y   n5# t          $ r(}t
                              d|            Y d}~dS d}~ww xY w|sdS |D ][}t
                              d|d          d|d          d	|d
          d|d          d|d          d|d          d|d                    \dS )aw  Log any non-idle hindsight session that's waiting on a lock or other resource.

        Catches the case where a coroutine appears 'fine' from Python's perspective
        but is blocked on a Postgres row lock - which is exactly how the 3-phase
        retain pipeline deadlock would present.

        pg_stat_activity is PostgreSQL-specific; skip on other backends.
        
postgresqlNa  
                    SELECT
                        pid,
                        application_name,
                        wait_event_type,
                        wait_event,
                        state,
                        EXTRACT(EPOCH FROM (now() - query_start))::int AS age_s,
                        LEFT(query, 200) AS query
                    FROM pg_stat_activity
                    WHERE datname = current_database()
                      AND state IS NOT NULL
                      AND state != 'idle'
                      AND wait_event IS NOT NULL
                      AND wait_event_type NOT IN ('Activity', 'Client')
                    ORDER BY age_s DESC NULLS LAST
                    LIMIT 20
                    zDB waits query failed: z[DB_WAITS] pid=pidz app=application_namez wait=wait_event_type.
wait_eventz state=stater  r  zs query=query)rS   backend_typer   r   r   r   r&  r  )rk   r   r   r   r}   s        r#   rr  zWorkerPoller._log_db_waits  s#      =%55F	},,..       $!ZZ                                 *  	 	 	LL6166777FFFFF	
  	F 	 	AKK:!E( : :3E1F : :+,: :/0: :GHz: :z: :+,W:: :   	 	s:   A5 A#A5 #
A--A5 0A-1A5 5
B'?B""B'c                     | j         S )zGet the worker ID.)rT   rk   s    r#   rG   zWorkerPoller.worker_id  s     r%   c                 4    | j                                         S )z$Check if shutdown has been signaled.)r`   rD  r  s    r#   is_shutdownzWorkerPoller.is_shutdown  s     ~$$&&&r%   )rD   NNrE   N)r   rp   )rR  )r   N)0r0   r1   r2   r3   r4   r   r=   r
   r   r7   rl   r   rw   ra   r   r?   r   r6   r   r   r:   r   r   r   r   r   r   r  r  r  r  r   r  r1  r-  rQ  r[  rH  rn  rm  rp  r'   rq  r  rr  propertyrG   r  r8   r%   r#   rC   rC   s   s         !$!5937>' >'">' >' DcN+Yt_<=	>'
 >' d
>' 3>' >'  S>D0>' >' >' >'@\Dt$4 \ \ \ \!$sTz2B !s3QU: ! ! ! !FV,< V V V V@& &5 &D & & & &4l4#4 l l l l\
Dj
37S>
QT
	k	
 
 
 
*Dj*37S>*QT*	k	* * * *X# sTz    Vs V3 VPSVZPZ V V V V&Ucs UcTWZ^T^ Ucim Uc Uc Uc Ucnb# b bWZ bdgjndn b b b b$Q3 Q5 QRU Q_bei_i Q Q Q Q(x{ x x x xB
D 
DS 
D 
D 
D 
D!L !Lk !L;QUCU !L !L !L !LF/ / / / /bJcDj JS J J J JXIF IF IFV#* #*u #* #* #* #*Jh? h? h?T!C ! ! ! !$!!C !! !! !! !!F@S$sCx.5H0I @d @ @ @ @:=S.5H0I =PU =Z^ = = = =< FS  F  Fu  FY]  F  F  F  FD0 0 0 0d 3    X 'T ' ' ' X' ' 'r%   rC   ),r3   r^   r  r   loggingr   r'  collectionsr   collections.abcr   r   r   dataclassesr   r   typingr	   r
   engine.schemar   r   
exceptionsr   r   r  r   r   hindsight_api.engine.db.baser   hindsight_api.extensions.tenantr   	getLoggerr0   r   rk  r  r  r4   r$   r'   r:   r?   rC   r8   r%   r#   <module>r     sC     				              9 9 9 9 9 9 9 9 9 9 ( ( ( ( ( ( ( ( % % % % % % % % 9 9 9 9 9 9 3 3 3 3 3 3 3 3 + + + + + + + + @<<<<<<??????		8	$	$   #& & o #    <        &         I I I I I I I If' f' f' f' f' f' f' f' f' f'r%   