streamlit-octostar-utils 0.5.0.dev12__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.dev12 → streamlit_octostar_utils-0.5.0.dev14}/PKG-INFO +1 -1
  2. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/pyproject.toml +1 -1
  3. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/celery.py +62 -43
  4. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/LICENSE +0 -0
  5. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/README.md +0 -0
  6. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/__init__.py +0 -0
  7. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/__init__.py +0 -0
  8. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/contents.py +0 -0
  9. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/fastapi.py +0 -0
  10. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/nifi.py +0 -0
  11. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parallelism.py +0 -0
  12. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/__init__.py +0 -0
  13. {streamlit_octostar_utils-0.5.0.dev12 → 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.dev12 → 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.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/generics.py +0 -0
  16. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/info.py +0 -0
  17. {streamlit_octostar_utils-0.5.0.dev12 → 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.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/matches.py +0 -0
  19. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/parameters.py +0 -0
  20. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/rules.py +0 -0
  21. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/api_crafter/parser/signals.py +0 -0
  22. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/__init__.py +0 -0
  23. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/dict.py +0 -0
  24. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/filetypes.py +0 -0
  25. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/threading/__init__.py +0 -0
  26. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/threading/key_queue.py +0 -0
  27. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/core/timestamp.py +0 -0
  28. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/__init__.py +0 -0
  29. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/custom_recognizers.py +0 -0
  30. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/language.py +0 -0
  31. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/nlp/ner.py +0 -0
  32. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/__init__.py +0 -0
  33. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/client.py +0 -0
  34. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/context.py +0 -0
  35. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/octostar/permissions.py +0 -0
  36. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/__init__.py +0 -0
  37. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/inheritance.py +0 -0
  38. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/relationships.py +0 -0
  39. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/ontology/validation.py +0 -0
  40. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/style/__init__.py +0 -0
  41. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/style/common.py +0 -0
  42. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/__init__.py +0 -0
  43. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/async_task_manager.py +0 -0
  44. {streamlit_octostar_utils-0.5.0.dev12 → streamlit_octostar_utils-0.5.0.dev14}/streamlit_octostar_utils/threading/session_callback_manager.py +0 -0
  45. {streamlit_octostar_utils-0.5.0.dev12 → 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.dev12
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.12"
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,20 +309,17 @@ 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()))
314
+ if task_id:
315
+ self.redis_client.delete(f"task:queue:{task_id}")
317
316
  except Exception:
318
317
  pass
319
318
 
320
319
  def register_state_signals(self):
321
320
  celery_signals.before_task_publish.connect(self.set_awaiting_state)
322
321
  celery_signals.task_prerun.connect(self.set_started_state)
323
- celery_signals.task_postrun.connect(self.set_last_completed_time)
322
+ celery_signals.task_postrun.connect(self.cleanup_task_keys)
324
323
 
325
324
  def cleanup_task_results(in_dir, out_dir, redis_host, redis_port, task_expires, result_expires):
326
325
  logger.info("Starting cleanup of expired task results...")
@@ -419,6 +418,9 @@ class CeleryExecutor(object):
419
418
  if attempts_done == CeleryExecutor.MAX_STARTUP_CHECKS:
420
419
  raise TimeoutError("Redis not ready after a long wait!")
421
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)
422
424
  for queue, queue_config in self.queue_config.items():
423
425
  for slot in range(queue_config.n_workers):
424
426
  worker_name = f"celery@{self.name}:{queue}:{slot}"
@@ -493,7 +495,10 @@ class CeleryExecutor(object):
493
495
  while not self.stop_event.is_set():
494
496
  try:
495
497
  self._restart_dead_processes()
496
- 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()
497
502
  time.sleep(5)
498
503
  except Exception as e:
499
504
  logger.error(f"Error in worker health check: {e}")
@@ -528,44 +533,56 @@ class CeleryExecutor(object):
528
533
  logger.info(f"Restarted beat process (PID: {self.beat_process.pid})")
529
534
 
530
535
  def _check_queue_stalls(self):
531
- for queue_name, queue_config in self.queue_config.items():
532
- if not queue_config.stall_timeout:
533
- 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:
534
555
  try:
535
- queue_items = self.redis_client.lrange(queue_name, 0, -1)
536
- 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.")
537
563
  self._queue_stalled[queue_name] = False
538
- self._queue_fingerprints.pop(queue_name, None)
539
- self._queue_fingerprint_changed_at.pop(queue_name, None)
540
564
  continue
541
565
 
542
- fingerprint = hashlib.md5(b"".join(sorted(queue_items))).hexdigest()
543
- now_time = time.time()
544
- prev_fingerprint = self._queue_fingerprints.get(queue_name)
545
-
546
- if fingerprint != prev_fingerprint:
547
- self._queue_fingerprints[queue_name] = fingerprint
548
- 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.")
549
569
  self._queue_stalled[queue_name] = False
550
570
  continue
551
571
 
552
- fingerprint_age = now_time - self._queue_fingerprint_changed_at.get(queue_name, now_time)
553
-
554
- last_completed_raw = self.redis_client.get(f"queue:last_completed:{queue_name}")
555
- last_completed = float(last_completed_raw) if last_completed_raw else 0
556
- 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")
557
578
 
558
- was_stalled = self._queue_stalled.get(queue_name, False)
559
- is_stalled = (
560
- fingerprint_age >= queue_config.stall_timeout
561
- and time_since_completion >= queue_config.stall_timeout
562
- )
579
+ is_stalled = time_since_activity >= grace_period
563
580
  self._queue_stalled[queue_name] = is_stalled
564
581
 
565
582
  if is_stalled and not was_stalled:
566
583
  logger.error(
567
- f"Queue '{queue_name}' is STALLED: {len(queue_items)} task(s) stuck for "
568
- 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. "
569
586
  f"New requests will receive 503."
570
587
  )
571
588
  elif not is_stalled and was_stalled:
@@ -719,10 +736,10 @@ class CeleryExecutor(object):
719
736
  task_fn.apply_async(task_id=task_id, **options)
720
737
 
721
738
  def _store_task_queue_mapping(task_id, queue_name):
722
- self.redis_client.set(
723
- f"task:queue:{task_id}", queue_name,
724
- ex=self.app.conf.result_expires,
725
- )
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()
726
743
 
727
744
  task_id = str(uuid.uuid4())
728
745
  queue_name = self.app.conf.task_default_routing_key
@@ -830,6 +847,7 @@ class CeleryExecutor(object):
830
847
 
831
848
  def _remove_task_data(celery_app, in_folder, out_folder, task_id):
832
849
  celery_app.AsyncResult(task_id).forget()
850
+ self.redis_client.delete(f"task:queue:{task_id}")
833
851
  if os.path.isfile(os.path.join(in_folder, task_id)):
834
852
  with RedisFileLock(self.redis_client, os.path.join(self.in_folder, task_id)):
835
853
  os.remove(os.path.join(in_folder, task_id))
@@ -889,6 +907,7 @@ class CeleryExecutor(object):
889
907
 
890
908
  def _remove_task_data(celery_app, in_folder, out_folder, task_id):
891
909
  celery_app.AsyncResult(task_id).forget()
910
+ self.redis_client.delete(f"task:queue:{task_id}")
892
911
  if os.path.isfile(os.path.join(in_folder, task_id)):
893
912
  with RedisFileLock(self.redis_client, os.path.join(self.in_folder, task_id)):
894
913
  os.remove(os.path.join(in_folder, task_id))