streamlit-octostar-utils 0.5.0.dev13__tar.gz → 0.5.0.dev14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/PKG-INFO +1 -1
  2. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/pyproject.toml +1 -1
  3. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/celery.py +58 -43
  4. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/LICENSE +0 -0
  5. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/README.md +0 -0
  6. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/__init__.py +0 -0
  7. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/__init__.py +0 -0
  8. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/contents.py +0 -0
  9. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/fastapi.py +0 -0
  10. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/nifi.py +0 -0
  11. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parallelism.py +0 -0
  12. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/__init__.py +0 -0
  13. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/combine_fields.py +0 -0
  14. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/entities_parser.py +0 -0
  15. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/generics.py +0 -0
  16. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/info.py +0 -0
  17. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/linkchart_functions.py +0 -0
  18. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/matches.py +0 -0
  19. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/parameters.py +0 -0
  20. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/rules.py +0 -0
  21. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/signals.py +0 -0
  22. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/__init__.py +0 -0
  23. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/dict.py +0 -0
  24. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/filetypes.py +0 -0
  25. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/threading/__init__.py +0 -0
  26. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/threading/key_queue.py +0 -0
  27. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/timestamp.py +0 -0
  28. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/__init__.py +0 -0
  29. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/custom_recognizers.py +0 -0
  30. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/language.py +0 -0
  31. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/ner.py +0 -0
  32. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/__init__.py +0 -0
  33. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/client.py +0 -0
  34. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/context.py +0 -0
  35. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/permissions.py +0 -0
  36. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/__init__.py +0 -0
  37. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/inheritance.py +0 -0
  38. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/relationships.py +0 -0
  39. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/validation.py +0 -0
  40. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/style/__init__.py +0 -0
  41. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/style/common.py +0 -0
  42. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/__init__.py +0 -0
  43. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/async_task_manager.py +0 -0
  44. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/session_callback_manager.py +0 -0
  45. {streamlit_octostar_utils-0.5.0.dev13 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/session_state_hot_swapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamlit-octostar-utils
3
- Version: 0.5.0.dev13
3
+ Version: 0.5.0.dev14
4
4
  Summary:
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -5,7 +5,7 @@ include = '\.pyi?$'
5
5
 
6
6
  [tool.poetry]
7
7
  name = "streamlit-octostar-utils"
8
- version = "0.5.0-dev.13"
8
+ version = "0.5.0-dev.14"
9
9
  description = ""
10
10
  license = "MIT"
11
11
  authors = ["Octostar"]
@@ -13,7 +13,6 @@ import atexit
13
13
  import redis
14
14
  import uuid
15
15
  import json
16
- import hashlib
17
16
  import shutil
18
17
  import threading
19
18
  from pottery import Redlock
@@ -56,14 +55,12 @@ class CeleryQueueConfig:
56
55
  max_tasks_in_queue=None,
57
56
  max_tasks_per_child=None,
58
57
  max_memory_per_child=None,
59
- stall_timeout=1200,
60
58
  **options,
61
59
  ):
62
60
  self.n_workers = n_workers
63
61
  self.max_tasks_in_queue = max_tasks_in_queue
64
62
  self.max_tasks_per_child = max_tasks_per_child
65
63
  self.max_memory_per_child = max_memory_per_child # KiB
66
- self.stall_timeout = stall_timeout # seconds; None or 0 to disable
67
64
  self.options = options
68
65
 
69
66
 
@@ -257,9 +254,9 @@ class CeleryExecutor(object):
257
254
  self.worker_info = {}
258
255
 
259
256
  # Queue stall detection
260
- self._queue_fingerprints = {}
261
- self._queue_fingerprint_changed_at = {}
262
257
  self._queue_stalled = {}
258
+ self._last_stall_check = 0
259
+ self._stall_check_interval = 60
263
260
 
264
261
  atexit.register(self.close)
265
262
  self.set_cleanup_task()
@@ -291,6 +288,11 @@ class CeleryExecutor(object):
291
288
  def set_started_state(self, task_id, task, *args, **kwargs):
292
289
  result = AsyncResult(task_id, app=self.app)
293
290
  result.backend.store_result(task_id, result=None, state=CeleryExecutor.STARTED)
291
+ queue = task.request.delivery_info.get(
292
+ "routing_key", self.app.conf.task_default_routing_key
293
+ ) if task else None
294
+ if queue:
295
+ self.redis_client.set(f"queue:last_started:{queue}", str(time.time()))
294
296
  request_timelimit = getattr(getattr(task, "request", None), "timelimit", None) or (None, None)
295
297
  time_limit = request_timelimit[0] or getattr(task, "time_limit", None) or 0
296
298
  extended_ttl = int(time_limit) + int(self.app.conf.result_expires)
@@ -307,13 +309,8 @@ class CeleryExecutor(object):
307
309
  if self.preload_functions:
308
310
  celery_signals.worker_process_init.connect(self.preload_on_worker_init)
309
311
 
310
- def set_last_completed_time(self, sender=None, task_id=None, task=None, **kwargs):
312
+ def cleanup_task_keys(self, sender=None, task_id=None, **kwargs):
311
313
  try:
312
- queue = task.request.delivery_info.get(
313
- "routing_key", self.app.conf.task_default_routing_key
314
- ) if task else None
315
- if queue:
316
- self.redis_client.set(f"queue:last_completed:{queue}", str(time.time()))
317
314
  if task_id:
318
315
  self.redis_client.delete(f"task:queue:{task_id}")
319
316
  except Exception:
@@ -322,7 +319,7 @@ class CeleryExecutor(object):
322
319
  def register_state_signals(self):
323
320
  celery_signals.before_task_publish.connect(self.set_awaiting_state)
324
321
  celery_signals.task_prerun.connect(self.set_started_state)
325
- celery_signals.task_postrun.connect(self.set_last_completed_time)
322
+ celery_signals.task_postrun.connect(self.cleanup_task_keys)
326
323
 
327
324
  def cleanup_task_results(in_dir, out_dir, redis_host, redis_port, task_expires, result_expires):
328
325
  logger.info("Starting cleanup of expired task results...")
@@ -421,6 +418,9 @@ class CeleryExecutor(object):
421
418
  if attempts_done == CeleryExecutor.MAX_STARTUP_CHECKS:
422
419
  raise TimeoutError("Redis not ready after a long wait!")
423
420
  self.redis_client.flushall()
421
+ boot_time = str(time.time())
422
+ for queue_name in self.queue_config:
423
+ self.redis_client.set(f"queue:last_started:{queue_name}", boot_time)
424
424
  for queue, queue_config in self.queue_config.items():
425
425
  for slot in range(queue_config.n_workers):
426
426
  worker_name = f"celery@{self.name}:{queue}:{slot}"
@@ -495,7 +495,10 @@ class CeleryExecutor(object):
495
495
  while not self.stop_event.is_set():
496
496
  try:
497
497
  self._restart_dead_processes()
498
- self._check_queue_stalls()
498
+ now = time.time()
499
+ if now - self._last_stall_check >= self._stall_check_interval:
500
+ self._last_stall_check = now
501
+ self._check_queue_stalls()
499
502
  time.sleep(5)
500
503
  except Exception as e:
501
504
  logger.error(f"Error in worker health check: {e}")
@@ -530,44 +533,56 @@ class CeleryExecutor(object):
530
533
  logger.info(f"Restarted beat process (PID: {self.beat_process.pid})")
531
534
 
532
535
  def _check_queue_stalls(self):
533
- for queue_name, queue_config in self.queue_config.items():
534
- if not queue_config.stall_timeout:
535
- continue
536
+ try:
537
+ inspector = self.app.control.inspect(timeout=5)
538
+ active_data = inspector.active() or {}
539
+ except Exception as e:
540
+ logger.error(f"Failed to inspect active workers: {e}")
541
+ return
542
+
543
+ now = time.time()
544
+ grace_period = 3 * self.app.conf.worker_proc_alive_timeout
545
+
546
+ active_queues = set()
547
+ for _worker_name, tasks in active_data.items():
548
+ for task_info in (tasks or []):
549
+ queue = (task_info.get("delivery_info") or {}).get(
550
+ "routing_key", self.app.conf.task_default_routing_key
551
+ )
552
+ active_queues.add(queue)
553
+
554
+ for queue_name in self.queue_config:
536
555
  try:
537
- queue_items = self.redis_client.lrange(queue_name, 0, -1)
538
- if len(queue_items) == 0:
556
+ pending_count = self.redis_client.llen(queue_name)
557
+ was_stalled = self._queue_stalled.get(queue_name, False)
558
+
559
+ if pending_count == 0:
560
+ self.redis_client.delete(f"queue:first_enqueued:{queue_name}")
561
+ if was_stalled:
562
+ logger.info(f"Queue '{queue_name}' has recovered from stall.")
539
563
  self._queue_stalled[queue_name] = False
540
- self._queue_fingerprints.pop(queue_name, None)
541
- self._queue_fingerprint_changed_at.pop(queue_name, None)
542
564
  continue
543
565
 
544
- fingerprint = hashlib.md5(b"".join(sorted(queue_items))).hexdigest()
545
- now_time = time.time()
546
- prev_fingerprint = self._queue_fingerprints.get(queue_name)
547
-
548
- if fingerprint != prev_fingerprint:
549
- self._queue_fingerprints[queue_name] = fingerprint
550
- self._queue_fingerprint_changed_at[queue_name] = now_time
566
+ if queue_name in active_queues:
567
+ if was_stalled:
568
+ logger.info(f"Queue '{queue_name}' has recovered from stall.")
551
569
  self._queue_stalled[queue_name] = False
552
570
  continue
553
571
 
554
- fingerprint_age = now_time - self._queue_fingerprint_changed_at.get(queue_name, now_time)
555
-
556
- last_completed_raw = self.redis_client.get(f"queue:last_completed:{queue_name}")
557
- last_completed = float(last_completed_raw) if last_completed_raw else 0
558
- time_since_completion = (now_time - last_completed) if last_completed else float("inf")
572
+ last_started_raw = self.redis_client.get(f"queue:last_started:{queue_name}")
573
+ first_enqueued_raw = self.redis_client.get(f"queue:first_enqueued:{queue_name}")
574
+ last_started = float(last_started_raw) if last_started_raw else 0
575
+ first_enqueued = float(first_enqueued_raw) if first_enqueued_raw else 0
576
+ last_activity = max(last_started, first_enqueued)
577
+ time_since_activity = (now - last_activity) if last_activity else float("inf")
559
578
 
560
- was_stalled = self._queue_stalled.get(queue_name, False)
561
- is_stalled = (
562
- fingerprint_age >= queue_config.stall_timeout
563
- and time_since_completion >= queue_config.stall_timeout
564
- )
579
+ is_stalled = time_since_activity >= grace_period
565
580
  self._queue_stalled[queue_name] = is_stalled
566
581
 
567
582
  if is_stalled and not was_stalled:
568
583
  logger.error(
569
- f"Queue '{queue_name}' is STALLED: {len(queue_items)} task(s) stuck for "
570
- f"{fingerprint_age:.0f}s with no completions in {time_since_completion:.0f}s. "
584
+ f"Queue '{queue_name}' is STALLED: {pending_count} task(s) pending, "
585
+ f"no active workers, no task started in {time_since_activity:.0f}s. "
571
586
  f"New requests will receive 503."
572
587
  )
573
588
  elif not is_stalled and was_stalled:
@@ -721,10 +736,10 @@ class CeleryExecutor(object):
721
736
  task_fn.apply_async(task_id=task_id, **options)
722
737
 
723
738
  def _store_task_queue_mapping(task_id, queue_name):
724
- self.redis_client.set(
725
- f"task:queue:{task_id}", queue_name,
726
- ex=self.app.conf.result_expires,
727
- )
739
+ pipe = self.redis_client.pipeline()
740
+ pipe.set(f"task:queue:{task_id}", queue_name, ex=self.app.conf.result_expires)
741
+ pipe.set(f"queue:first_enqueued:{queue_name}", str(time.time()), nx=True)
742
+ pipe.execute()
728
743
 
729
744
  task_id = str(uuid.uuid4())
730
745
  queue_name = self.app.conf.task_default_routing_key