@@ -129,6 +129,9 @@ class JobQueueService(Service):
129129
130130 name = "job_queue_service"
131131
132+ # Backend label used on OpenTelemetry metrics. Subclasses override.
133+ _backend_label : str = "memory"
134+
132135 def __init__ (self ) -> None :
133136 """Initialize the JobQueueService.
134137
@@ -142,6 +145,44 @@ def __init__(self) -> None:
142145 self ._closed = False
143146 self .ready = False
144147 self .CLEANUP_GRACE_PERIOD = 300 # 5 minutes before cleaning up marked tasks
148+ # Cached OpenTelemetry handle (resolved lazily on first emit).
149+ # _otel_resolved is True after the first lookup attempt, including failures,
150+ # so we don't retry on every bump. _otel remains None when lookup fails.
151+ self ._otel : Any = None
152+ self ._otel_resolved : bool = False
153+
154+ def _get_otel (self ) -> Any :
155+ """Best-effort lookup of the OpenTelemetry singleton. Cached after first call.
156+
157+ Returns the ``ot`` handle, or ``None`` if telemetry is unavailable. Never raises —
158+ instrumentation must not break the queue.
159+ """
160+ if self ._otel_resolved :
161+ return self ._otel
162+ self ._otel_resolved = True
163+ try :
164+ from langflow .services .deps import get_telemetry_service
165+
166+ self ._otel = get_telemetry_service ().ot
167+ except Exception : # noqa: BLE001 - telemetry must not crash the queue
168+ self ._otel = None
169+ return self ._otel
170+
171+ def _emit_otel_counter (self , metric_name : str , labels : dict [str , str ], value : float = 1.0 ) -> None :
172+ """Best-effort OpenTelemetry counter bump. Silent on failure."""
173+ ot = self ._get_otel ()
174+ if ot is None :
175+ return
176+ with contextlib .suppress (Exception ):
177+ ot .increment_counter (metric_name , labels , value )
178+
179+ def _emit_otel_up_down (self , metric_name : str , value : float , labels : dict [str , str ]) -> None :
180+ """Best-effort OpenTelemetry up-down counter delta. Silent on failure."""
181+ ot = self ._get_otel ()
182+ if ot is None :
183+ return
184+ with contextlib .suppress (Exception ):
185+ ot .up_down_counter (metric_name , value , labels )
145186
146187 def is_started (self ) -> bool :
147188 """Check if the JobQueueService has started.
@@ -241,6 +282,7 @@ def create_queue(self, job_id: str) -> tuple[asyncio.Queue, EventManager]:
241282
242283 # Register the queue without an active task.
243284 self ._queues [job_id ] = (main_queue , event_manager , None , None )
285+ self ._emit_otel_up_down ("langflow_job_queue_active_jobs" , 1 , {"backend" : self ._backend_label })
244286 logger .debug (f"Queue and event manager successfully created for job_id { job_id } " )
245287 return main_queue , event_manager
246288
@@ -436,7 +478,8 @@ async def cleanup_job(self, job_id: str) -> None:
436478
437479 await logger .adebug (f"Removed { items_cleared } items from queue for job_id { job_id } " )
438480 # Remove the job entry from the registry
439- self ._queues .pop (job_id , None )
481+ if self ._queues .pop (job_id , None ) is not None :
482+ self ._emit_otel_up_down ("langflow_job_queue_active_jobs" , - 1 , {"backend" : self ._backend_label })
440483 self ._job_owners .pop (job_id , None )
441484 self ._public_jobs .discard (job_id )
442485 await logger .adebug (f"Cleanup successful for job_id { job_id } : resources have been released." )
@@ -810,6 +853,8 @@ class RedisJobQueueService(JobQueueService):
810853 ACTIVITY_PREFIX = _ACTIVITY_PREFIX
811854 PUBLIC_JOB_PREFIX = _PUBLIC_JOB_PREFIX
812855
856+ _backend_label : str = "redis"
857+
813858 def __init__ (
814859 self ,
815860 host : str = "localhost" ,
@@ -880,6 +925,16 @@ def cross_worker_cancel_enabled(self) -> bool:
880925 """
881926 return self ._cancel_channel_enabled
882927
928+ def _bump_cancel_stat (self , key : str , value : int = 1 ) -> None :
929+ """Increment a cancel_stat counter and emit the matching OTel counter event.
930+
931+ Single point of mutation for ``self._cancel_stats`` so ``/monitor/job_queue``
932+ and Prometheus stay in lockstep. The OTel emit is best-effort and never
933+ raises (see :meth:`_emit_otel_counter` on the base class).
934+ """
935+ self ._cancel_stats [key ] += value
936+ self ._emit_otel_counter ("langflow_job_queue_cancel_events_total" , {"event_type" : key }, float (value ))
937+
883938 def _stream_key (self , job_id : str ) -> str :
884939 return f"{ self .STREAM_PREFIX } { job_id } "
885940
@@ -1208,7 +1263,7 @@ async def touch_activity(self, job_id: str) -> None:
12081263 except asyncio .CancelledError :
12091264 raise
12101265 except Exception as exc : # noqa: BLE001
1211- self ._cancel_stats [ "activity_touch_errors" ] += 1
1266+ self ._bump_cancel_stat ( "activity_touch_errors" )
12121267 await logger .adebug (f"touch_activity SET failed for { job_id } : { exc } " )
12131268
12141269 def _owner_refresh_interval_s (self ) -> float :
@@ -1248,7 +1303,7 @@ async def _check_pending_cancel_marker(self, job_id: str) -> None:
12481303 try :
12491304 if await self ._client .exists (marker_key ):
12501305 await self ._client .delete (marker_key )
1251- self ._cancel_stats [ "marker_hit" ] += 1
1306+ self ._bump_cancel_stat ( "marker_hit" )
12521307 await self ._handle_cancel (job_id , source = "marker" )
12531308 except Exception as exc : # noqa: BLE001
12541309 await logger .awarning (f"Pending cancel marker check failed for { job_id } : { exc } " )
@@ -1306,7 +1361,7 @@ async def _run_polling_watchdog(self) -> None:
13061361 try :
13071362 raw = await self ._client .get (self ._activity_key (job_id ))
13081363 except Exception as exc : # noqa: BLE001
1309- self ._cancel_stats [ "activity_get_errors" ] += 1
1364+ self ._bump_cancel_stat ( "activity_get_errors" )
13101365 await logger .adebug (f"polling watchdog: GET failed for { job_id } : { exc } " )
13111366 continue
13121367 # Default to "infinitely stale" so a missed elif below cannot
@@ -1324,7 +1379,7 @@ async def _run_polling_watchdog(self) -> None:
13241379 try :
13251380 last = float (raw .decode () if isinstance (raw , bytes ) else raw )
13261381 except (ValueError , TypeError ) as exc :
1327- self ._cancel_stats [ "activity_parse_errors" ] += 1
1382+ self ._bump_cancel_stat ( "activity_parse_errors" )
13281383 await logger .awarning (
13291384 f"polling watchdog: ignoring malformed activity value for { job_id } : { exc } "
13301385 )
@@ -1335,7 +1390,7 @@ async def _run_polling_watchdog(self) -> None:
13351390 # Stale → cancel this job. Local cancel on owned jobs skips the
13361391 # pubsub round-trip and stays correct even during a dispatcher
13371392 # reconnect window.
1338- self ._cancel_stats [ "polling_watchdog_kills" ] += 1
1393+ self ._bump_cancel_stat ( "polling_watchdog_kills" )
13391394 await logger .ainfo (f"polling watchdog: reclaiming abandoned job { job_id } (age={ age :.1f} s)" )
13401395 await self ._handle_cancel (job_id , source = "watchdog" )
13411396 with contextlib .suppress (Exception ):
@@ -1383,7 +1438,7 @@ async def _run_cancel_dispatcher(self) -> None:
13831438 job_id = channel_str [len (self .CANCEL_CHANNEL_PREFIX ) :]
13841439 await self ._handle_cancel (job_id , source = "pubsub" )
13851440 # listen() returned cleanly — treat as a soft disconnect and reconnect.
1386- self ._cancel_stats [ "dispatcher_reconnects" ] += 1
1441+ self ._bump_cancel_stat ( "dispatcher_reconnects" )
13871442 await logger .awarning (f"Cancel dispatcher pubsub.listen() ended; reconnecting in { backoff :.1f} s" )
13881443 except asyncio .CancelledError :
13891444 with contextlib .suppress (Exception ):
@@ -1393,15 +1448,15 @@ async def _run_cancel_dispatcher(self) -> None:
13931448 except (ConnectionError , TimeoutError , OSError ) as exc :
13941449 # Expected transient failure: Redis dropped the pubsub, network
13951450 # blip, broker restart. Reconnect quietly via the backoff loop.
1396- self ._cancel_stats [ "dispatcher_reconnects" ] += 1
1451+ self ._bump_cancel_stat ( "dispatcher_reconnects" )
13971452 await logger .awarning (f"Cancel dispatcher disconnect (retrying in { backoff :.1f} s): { exc !r} " )
13981453 except Exception as exc : # noqa: BLE001
13991454 # Unexpected exception — likely a bug in dispatch logic, NOT a
14001455 # Redis problem. Surface at error level with traceback so it
14011456 # reaches Sentry / log aggregation, then still reconnect so a
14021457 # one-off bug doesn't kill cross-worker cancel permanently.
1403- self ._cancel_stats [ "dispatcher_reconnects" ] += 1
1404- self ._cancel_stats [ "dispatcher_internal_errors" ] += 1
1458+ self ._bump_cancel_stat ( "dispatcher_reconnects" )
1459+ self ._bump_cancel_stat ( "dispatcher_internal_errors" )
14051460 await logger .aerror (
14061461 f"Cancel dispatcher internal error (retrying in { backoff :.1f} s): { exc !r} " ,
14071462 exc_info = True ,
@@ -1418,7 +1473,7 @@ async def _run_cancel_dispatcher(self) -> None:
14181473
14191474 async def _on_cancel_dispatcher_connection_reconnect (self , _connection : Any ) -> None :
14201475 """Record redis-py reconnects that happen inside an active PubSub."""
1421- self ._cancel_stats [ "dispatcher_reconnects" ] += 1
1476+ self ._bump_cancel_stat ( "dispatcher_reconnects" )
14221477 with contextlib .suppress (Exception ):
14231478 await logger .awarning ("Cancel dispatcher pubsub connection reconnected transparently" )
14241479
@@ -1458,10 +1513,10 @@ async def _apply_cancel(self, job_id: str, *, source: str, wait_for_cleanup: boo
14581513 """
14591514 entry = self ._queues .get (job_id )
14601515 if entry is None :
1461- self ._cancel_stats [ "dispatched_foreign" ] += 1
1516+ self ._bump_cancel_stat ( "dispatched_foreign" )
14621517 await logger .adebug (f"Cancel for { job_id } ignored on this worker (not owner); source={ source } " )
14631518 return
1464- self ._cancel_stats [ "dispatched_owned" ] += 1
1519+ self ._bump_cancel_stat ( "dispatched_owned" )
14651520 await logger .ainfo (f"Cancel applied to { job_id } (source={ source } )" )
14661521 main_queue , _ , task , _ = entry
14671522 if task is not None and not task .done ():
@@ -1569,9 +1624,9 @@ async def signal_cancel(self, job_id: str) -> int:
15691624 await self ._client .set (self ._cancel_marker_key (job_id ), "1" , ex = self ._cancel_marker_ttl )
15701625 receivers = int (await self ._client .publish (self ._cancel_channel (job_id ), "1" ))
15711626 except Exception :
1572- self ._cancel_stats [ "publish_errors" ] += 1
1627+ self ._bump_cancel_stat ( "publish_errors" )
15731628 raise
1574- self ._cancel_stats [ "published" ] += 1
1629+ self ._bump_cancel_stat ( "published" )
15751630 await logger .ainfo (f"signal_cancel: job_id={ job_id } receivers={ receivers } " )
15761631 return receivers
15771632
0 commit comments