fdc-shared-kernel 0.0.97__tar.gz → 0.0.99__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 (104) hide show
  1. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/PKG-INFO +1 -1
  2. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/PKG-INFO +1 -1
  3. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/SOURCES.txt +6 -1
  4. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/pyproject.toml +1 -1
  5. fdc_shared_kernel-0.0.99/shared_kernel/dataclasses/event_executor.py +41 -0
  6. fdc_shared_kernel-0.0.99/shared_kernel/event_executor/event_executor.py +251 -0
  7. fdc_shared_kernel-0.0.99/shared_kernel/event_executor/job_executor.py +306 -0
  8. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/event_executor/utils.py +1 -11
  9. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/logger/__init__.py +1 -1
  10. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/aws_databus.py +16 -21
  11. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/utils/aws_utility.py +1 -1
  12. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/utils/event_messages.py +15 -14
  13. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/status_tracker/status_tracker.py +19 -19
  14. fdc_shared_kernel-0.0.99/shared_kernel/tests/utils/__init__.py +0 -0
  15. fdc_shared_kernel-0.0.99/shared_kernel/utils/thread_debug_utils.py +43 -0
  16. fdc_shared_kernel-0.0.99/tests/test_job_executor.py +150 -0
  17. fdc_shared_kernel-0.0.97/shared_kernel/event_executor/event_executor.py +0 -481
  18. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/README.md +0 -0
  19. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/README_pypi.md +0 -0
  20. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/dependency_links.txt +0 -0
  21. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/requires.txt +0 -0
  22. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/top_level.txt +0 -0
  23. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/requirements.txt +0 -0
  24. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/setup.cfg +0 -0
  25. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/__init__.py +0 -0
  26. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/async_task_executor/__init__.py +0 -0
  27. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/async_task_executor/async_task_executor.py +0 -0
  28. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/__init__.py +0 -0
  29. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/jwt_helper.py +0 -0
  30. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/token_handler.py +0 -0
  31. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/workbook_permission_handler.py +0 -0
  32. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/config/__init__.py +0 -0
  33. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/database/__init__.py +0 -0
  34. {fdc_shared_kernel-0.0.97/shared_kernel/datatype_mappings → fdc_shared_kernel-0.0.99/shared_kernel/dataclasses}/__init__.py +0 -0
  35. {fdc_shared_kernel-0.0.97/shared_kernel/models → fdc_shared_kernel-0.0.99/shared_kernel/datatype_mappings}/__init__.py +0 -0
  36. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/__init__.py +0 -0
  37. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/csv.py +0 -0
  38. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/db2.py +0 -0
  39. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/jira.py +0 -0
  40. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/mssql.py +0 -0
  41. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/mysql.py +0 -0
  42. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/oracle.py +0 -0
  43. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/postgres.py +0 -0
  44. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/redshift.py +0 -0
  45. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/salesforce.py +0 -0
  46. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/__init__.py +0 -0
  47. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/postgres.py +0 -0
  48. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/redshift.py +0 -0
  49. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/__init__.py +0 -0
  50. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/async_task_executor.py +0 -0
  51. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/status_tracker.py +0 -0
  52. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/event_executor/__init__.py +0 -0
  53. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/__init__.py +0 -0
  54. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/configuration_exceptions.py +0 -0
  55. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/custom_exceptions.py +0 -0
  56. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/data_validation_exceptions.py +0 -0
  57. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/http_exceptions.py +0 -0
  58. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/infrastructure_exceptions.py +0 -0
  59. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/operational_exceptions.py +0 -0
  60. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/security_exceptions.py +0 -0
  61. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/__init__.py +0 -0
  62. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/httpx_http_client.py +0 -0
  63. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/request_http_client.py +0 -0
  64. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/__init__.py +0 -0
  65. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/databus.py +0 -0
  66. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/http.py +0 -0
  67. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/keyvault.py +0 -0
  68. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/__init__.py +0 -0
  69. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/http_databus.py +0 -0
  70. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_databus.py +0 -0
  71. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_publisher.py +0 -0
  72. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_test.py +0 -0
  73. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/metrics/__init__.py +0 -0
  74. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/metrics/status_tracker.py +0 -0
  75. {fdc_shared_kernel-0.0.97/shared_kernel/tests → fdc_shared_kernel-0.0.99/shared_kernel/models}/__init__.py +0 -0
  76. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/registries/__init__.py +0 -0
  77. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/registries/service_event_registry.py +0 -0
  78. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/__init__.py +0 -0
  79. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/__init__.py +0 -0
  80. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/aws_secret_manager.py +0 -0
  81. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/azure_keyvault.py +0 -0
  82. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/status_tracker/__init__.py +0 -0
  83. {fdc_shared_kernel-0.0.97/shared_kernel/tests/config → fdc_shared_kernel-0.0.99/shared_kernel/tests}/__init__.py +0 -0
  84. {fdc_shared_kernel-0.0.97/shared_kernel/tests/logger → fdc_shared_kernel-0.0.99/shared_kernel/tests/config}/__init__.py +0 -0
  85. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/config/test_config.py +0 -0
  86. {fdc_shared_kernel-0.0.97/shared_kernel/tests/messaging → fdc_shared_kernel-0.0.99/shared_kernel/tests/logger}/__init__.py +0 -0
  87. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/logger/test_logger.py +0 -0
  88. {fdc_shared_kernel-0.0.97/shared_kernel/tests/utils → fdc_shared_kernel-0.0.99/shared_kernel/tests/messaging}/__init__.py +0 -0
  89. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_aws_databus.py +0 -0
  90. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_event_executor.py +0 -0
  91. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_nats_interface.py +0 -0
  92. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_data_validators.py +0 -0
  93. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_date_format_utils.py +0 -0
  94. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_string_utils.py +0 -0
  95. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/__init__.py +0 -0
  96. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/data_validators_utils.py +0 -0
  97. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/date_format_utils.py +0 -0
  98. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/string_utils.py +0 -0
  99. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/template_renderer.py +0 -0
  100. /fdc_shared_kernel-0.0.97/shared_kernel/utils/thread_local_util.py → /fdc_shared_kernel-0.0.99/shared_kernel/utils/thread_local_storage.py +0 -0
  101. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/__init__.py +0 -0
  102. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/messaging/__init__.py +0 -0
  103. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/messaging/utils/__init__.py +0 -0
  104. {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/messaging/utils/test_aws_utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fdc_shared_kernel
3
- Version: 0.0.97
3
+ Version: 0.0.99
4
4
  Summary: Shared library for microservice
5
5
  Author-email: Shikhil S <shikhil.s@dbizsolution.com>, Ahammed Akdham N <ahammedakdham.n@dbizsolution.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fdc_shared_kernel
3
- Version: 0.0.97
3
+ Version: 0.0.99
4
4
  Summary: Shared library for microservice
5
5
  Author-email: Shikhil S <shikhil.s@dbizsolution.com>, Ahammed Akdham N <ahammedakdham.n@dbizsolution.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -16,6 +16,8 @@ shared_kernel/auth/token_handler.py
16
16
  shared_kernel/auth/workbook_permission_handler.py
17
17
  shared_kernel/config/__init__.py
18
18
  shared_kernel/database/__init__.py
19
+ shared_kernel/dataclasses/__init__.py
20
+ shared_kernel/dataclasses/event_executor.py
19
21
  shared_kernel/datatype_mappings/__init__.py
20
22
  shared_kernel/datatype_mappings/connectors_to_system/__init__.py
21
23
  shared_kernel/datatype_mappings/connectors_to_system/csv.py
@@ -35,6 +37,7 @@ shared_kernel/enums/async_task_executor.py
35
37
  shared_kernel/enums/status_tracker.py
36
38
  shared_kernel/event_executor/__init__.py
37
39
  shared_kernel/event_executor/event_executor.py
40
+ shared_kernel/event_executor/job_executor.py
38
41
  shared_kernel/event_executor/utils.py
39
42
  shared_kernel/exceptions/__init__.py
40
43
  shared_kernel/exceptions/configuration_exceptions.py
@@ -89,8 +92,10 @@ shared_kernel/utils/data_validators_utils.py
89
92
  shared_kernel/utils/date_format_utils.py
90
93
  shared_kernel/utils/string_utils.py
91
94
  shared_kernel/utils/template_renderer.py
92
- shared_kernel/utils/thread_local_util.py
95
+ shared_kernel/utils/thread_debug_utils.py
96
+ shared_kernel/utils/thread_local_storage.py
93
97
  tests/__init__.py
98
+ tests/test_job_executor.py
94
99
  tests/messaging/__init__.py
95
100
  tests/messaging/utils/__init__.py
96
101
  tests/messaging/utils/test_aws_utility.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fdc_shared_kernel"
7
- version = "0.0.97"
7
+ version = "0.0.99"
8
8
  requires-python = ">=3.7"
9
9
  readme = "README_pypi.md"
10
10
  description = "Shared library for microservice"
@@ -0,0 +1,41 @@
1
+ """
2
+ File: event_executor.py
3
+ Author: Akdham
4
+ Description: Dataclasses for event executor
5
+ Date: 2025-05-07
6
+ """
7
+ from concurrent.futures import Future
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Callable, List
10
+
11
+ from shared_kernel.messaging.utils.event_messages import EventMessage
12
+
13
+ @dataclass
14
+ class EventStats:
15
+ """Statistics for an event type"""
16
+ successful_events: int = 0
17
+ failed_events: int = 0
18
+
19
+ @property
20
+ def total_events(self) -> int:
21
+ return self.successful_events + self.failed_events
22
+
23
+
24
+ @dataclass
25
+ class ActiveJob:
26
+ """Represents a job currently being processed by the executor."""
27
+
28
+ execution_future: Future
29
+ event_msg_object: EventMessage
30
+
31
+
32
+ @dataclass
33
+ class EventContext:
34
+ """Stores metadata and runtime statistics for an event."""
35
+
36
+ schema: dict
37
+ description: str
38
+ callback: Callable[[Any], None]
39
+ total_workers: int
40
+ event_stats: EventStats
41
+ active_jobs: List[ActiveJob] = field(default_factory=list)
@@ -0,0 +1,251 @@
1
+ import threading
2
+ from concurrent.futures import Future
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from shared_kernel.config import Config
6
+ from shared_kernel.dataclasses.event_executor import ActiveJob, EventContext, EventStats
7
+ from shared_kernel.event_executor.job_executor import JobExecutor
8
+ from shared_kernel.event_executor.utils import EventConcurrencyManager
9
+ from shared_kernel.interfaces import DataBus
10
+ from shared_kernel.logger import Logger
11
+ from shared_kernel.status_tracker import StatusTracker
12
+ from shared_kernel.utils.thread_local_storage import ThreadLocalStorage
13
+ from shared_kernel.messaging.utils.event_messages import EventMessage
14
+
15
+
16
+ app_config = Config()
17
+ logger = Logger(app_config.get("APP_NAME"))
18
+
19
+ thread_local_storage = ThreadLocalStorage()
20
+
21
+
22
+ class EventExecutor:
23
+ _singleton_instance = None
24
+ _singleton_lock = threading.Lock()
25
+
26
+ def __new__(self, *args, **kwargs):
27
+ with self._singleton_lock:
28
+ if self._singleton_instance is None:
29
+ instance = super().__new__(self)
30
+ instance._is_initialized = False
31
+ self._singleton_instance = instance
32
+ return self._singleton_instance
33
+
34
+ def __init__(
35
+ self,
36
+ databus: Optional[DataBus] = None,
37
+ status_tracker: Optional[StatusTracker] = None,
38
+ ):
39
+ """Initialize the EventExecutor singleton.
40
+
41
+ Args:
42
+ databus (Optional[DataBus]): The DataBus instance to use for publishing messages.
43
+ status_tracker (Optional[StatusTracker]): The StatusTracker instance to use for tracking status.
44
+ """
45
+ with self._singleton_lock:
46
+ if self._is_initialized:
47
+ return
48
+
49
+ # checked only during initial initialization - that is during app startup
50
+ if databus is None or status_tracker is None:
51
+ raise ValueError(
52
+ "DataBus and StatusTracker must be provided for initial initialization"
53
+ )
54
+
55
+ # initialize core components
56
+ self.databus = databus
57
+ self.status_tracker = status_tracker
58
+ self.job_executor = JobExecutor(status_tracker, databus)
59
+ self._event_listener_threads: Dict[str, threading.Thread] = {}
60
+ self._event_concurrency_manager = EventConcurrencyManager()
61
+ self._shutdown_flag = threading.Event()
62
+ self._event_catalog: Dict[str, EventContext] = {}
63
+
64
+ self._is_initialized = True
65
+ logger.info("EventExecutor singleton initialized.")
66
+
67
+ def get_stats(self, event_name: str) -> EventStats:
68
+ """Return statistics for a specific event."""
69
+ if event_name in self._event_catalog:
70
+ return self._event_catalog[event_name].event_stats
71
+ return EventStats()
72
+
73
+ def _on_job_execution_complete(
74
+ self,
75
+ execution_future: Future,
76
+ event_name: str,
77
+ event_semaphore: threading.Semaphore,
78
+ active_job: ActiveJob,
79
+ ) -> None:
80
+ """
81
+ Callback executed after a job completes. Updates counters and releases semaphore.
82
+
83
+ Args:
84
+ execution_future (Future): the completed future
85
+ event_name (str): name of the event
86
+ event_semaphore (Semaphore): semaphore controlling concurrency
87
+ active_job (ActiveJob): the job object containing the future and payload
88
+ """
89
+ try:
90
+ if active_job in self._event_catalog[event_name].active_jobs:
91
+ self._event_catalog[event_name].active_jobs.remove(active_job)
92
+
93
+ try:
94
+ execution_future.result() # will raise if the job failed
95
+ self._event_catalog[event_name].event_stats.successful_events += 1
96
+ except Exception:
97
+ self._event_catalog[event_name].event_stats.failed_events += 1
98
+ finally:
99
+ # always release the semaphore, even if there's an exception
100
+ event_semaphore.release()
101
+
102
+ def _listen_events(self, event_name: str) -> None:
103
+ """Listen to the async event source and dispatch jobs."""
104
+ logger.info(f"Starting event listener for [{event_name}].")
105
+
106
+ event_semaphore = self._event_concurrency_manager.get_event_semaphore(
107
+ event_name=event_name
108
+ )
109
+ event_threadpool_executor = (
110
+ self._event_concurrency_manager.get_event_threadpool_executor(
111
+ event_name=event_name
112
+ )
113
+ )
114
+ event_context = self._event_catalog[event_name]
115
+
116
+ while not self._shutdown_flag.is_set():
117
+ is_semaphore_acquired = False
118
+ try:
119
+ is_semaphore_acquired = event_semaphore.acquire(timeout=0.5)
120
+ if is_semaphore_acquired:
121
+ # get message from the databus
122
+ event_msg_object: EventMessage = self.databus.get_message(event_name)
123
+ if event_msg_object:
124
+ logger.info(
125
+ f"Received message for event {event_name}: {event_msg_object.raw_message}"
126
+ )
127
+
128
+ # submit job to executor
129
+ execution_future = event_threadpool_executor.submit(
130
+ self.job_executor.submit_job,
131
+ event_context.callback,
132
+ event_msg_object,
133
+ )
134
+
135
+ # track active job
136
+ active_job = ActiveJob(
137
+ execution_future=execution_future, event_msg_object=event_msg_object
138
+ )
139
+ self._event_catalog[event_name].active_jobs.append(active_job)
140
+
141
+ # assign callback to future
142
+ execution_future.add_done_callback(
143
+ lambda fut, event_name=event_name, event_semaphore=event_semaphore, active_job=active_job: self._on_job_execution_complete(
144
+ fut, event_name, event_semaphore, active_job
145
+ )
146
+ )
147
+
148
+ # we don't release the semaphore here because it will be released in _task_done_callback
149
+ is_semaphore_acquired = False
150
+ else:
151
+ # no message, release semaphore to permit
152
+ event_semaphore.release()
153
+ is_semaphore_acquired = False
154
+ else:
155
+ # if we couldn't acquire the semaphore, wait for 0.1 seconds
156
+ self._shutdown_flag.wait(0.1)
157
+ except Exception as e:
158
+ logger.error(f"Error in event listener for {event_name}: {str(e)}")
159
+ # TODO: implement retry logic or circuit breaker pattern here - needs to do some research on this
160
+ finally:
161
+ # make sure semaphore is released if we encountered an exception after acquiring it
162
+ if is_semaphore_acquired:
163
+ event_semaphore.release()
164
+
165
+ logger.info(f"Event listener for {event_name} has been stopped.")
166
+
167
+ def get_event_catalog(self) -> dict:
168
+ """Return the event catalog."""
169
+ return self._event_catalog
170
+
171
+ def get_databus_instance(self) -> DataBus:
172
+ """Return the databus instance"""
173
+ return self.databus
174
+
175
+ def register_event(
176
+ self,
177
+ event_name: str,
178
+ event_schema: dict,
179
+ event_description: str,
180
+ callback: Callable[[Any], None],
181
+ max_concurrency: int,
182
+ ) -> dict:
183
+ """Register an event and start listening for messages."""
184
+ with self._singleton_lock: # protect against concurrent register_event calls
185
+ if event_name in self._event_listener_threads:
186
+ raise ValueError(f"Event {event_name} is already registered")
187
+
188
+ # register with async bus and set up concurrency controls
189
+ self.databus.subscribe_async_event(event_name, None)
190
+ self._event_concurrency_manager.set_event_concurrency(
191
+ event_name=event_name, max_concurrency=max_concurrency
192
+ )
193
+
194
+ # initialize with default EventStats
195
+ self._event_catalog[event_name] = EventContext(
196
+ schema=event_schema,
197
+ description=event_description,
198
+ callback=callback,
199
+ total_workers=max_concurrency,
200
+ event_stats=EventStats(),
201
+ )
202
+
203
+ # launch listener thread
204
+ long_running_event_listener_thread = threading.Thread(
205
+ target=self._listen_events,
206
+ args=(event_name,),
207
+ name=f"EventListener-{event_name}",
208
+ daemon=True,
209
+ )
210
+ self._event_listener_threads[event_name] = long_running_event_listener_thread
211
+ long_running_event_listener_thread.start()
212
+ logger.info(f"Event {event_name} registered and listener started.")
213
+
214
+ return True
215
+
216
+ def shutdown(self) -> None:
217
+ """Shut down all running threads and cleanup resources."""
218
+ self._shutdown_flag.set()
219
+
220
+ for event_name, thread in self._event_listener_threads.items():
221
+ logger.info(f"Shutting down thread for event {event_name}")
222
+ thread.join(timeout=30) # Don't wait forever
223
+
224
+ if thread.is_alive():
225
+ logger.warning(
226
+ f"Thread for event {event_name} did not terminate gracefully"
227
+ )
228
+
229
+ # wait for all running jobs to finish with timeout
230
+ for event_name, catalog in self._event_catalog.items():
231
+ for job in catalog.active_jobs:
232
+ try:
233
+ # Set a timeout to avoid hanging
234
+ job.execution_future.result(timeout=10)
235
+ except Exception as e:
236
+ logger.error(
237
+ f"Error during shutdown of task for event {event_name}: {str(e)}"
238
+ )
239
+
240
+ # shut down executors
241
+ for (
242
+ event_name,
243
+ executor,
244
+ ) in self._event_concurrency_manager.event_threadpool_executors.items():
245
+ logger.info(f"Shutting down executor for event {event_name}")
246
+ executor.shutdown(wait=True)
247
+
248
+ self._event_listener_threads.clear()
249
+ self._event_concurrency_manager.event_threadpool_executors.clear()
250
+ self._event_concurrency_manager.event_semaphores.clear()
251
+ logger.info("EventExecutor shutdown complete.")
@@ -0,0 +1,306 @@
1
+ """
2
+ Filename: job_executor.py
3
+ Author: Akdham
4
+ Description: Handles the execution of individual jobs/tasks triggered by events.
5
+ Date: 2025-05-07
6
+ """
7
+ import json
8
+ from typing import Any, Callable, Dict, Optional
9
+
10
+ from shared_kernel.config import Config
11
+ from shared_kernel.enums import TaskStatus
12
+ from shared_kernel.interfaces import DataBus
13
+ from shared_kernel.logger import Logger
14
+ from shared_kernel.status_tracker import StatusTracker
15
+ from shared_kernel.messaging.utils.event_messages import AWSEventMessage, EventMessage
16
+ from shared_kernel.utils.thread_local_storage import ThreadLocalStorage
17
+
18
+ app_config = Config()
19
+ logger = Logger(app_config.get("APP_NAME"))
20
+
21
+ thread_local_storage = ThreadLocalStorage()
22
+
23
+
24
+ class JobExecutor:
25
+ """
26
+ Handles the execution of individual jobs/tasks triggered by events.
27
+ Responsible for the actual business logic execution and status tracking as well as error handling.
28
+ """
29
+
30
+ def __init__(self, status_tracker: StatusTracker, databus: DataBus):
31
+ """
32
+ Initialize the job executor.
33
+
34
+ Args:
35
+ status_tracker: Status tracker to track status of events' jobs
36
+ databus: DataBus
37
+ """
38
+ self.status_tracker = status_tracker
39
+ self.databus = databus
40
+
41
+ def _setup_thread_context(self, event_msg: EventMessage) -> None:
42
+ """
43
+ Set up thread-local storage with event metadata.
44
+
45
+ Args:
46
+ event_msg: Event message containing metadata
47
+ """
48
+ context = {
49
+ "trace_id": event_msg.event_meta.trace_id,
50
+ "span_id": event_msg.event_meta.span_id,
51
+ "event_name": event_msg.event_name,
52
+ "event_payload": json.dumps(event_msg.event_payload),
53
+ "event_meta": json.dumps(event_msg.event_meta.to_dict()),
54
+ }
55
+
56
+ # add optional fields if they exist
57
+ if hasattr(event_msg.event_meta, "org_id"):
58
+ context["org_id"] = event_msg.event_meta.org_id
59
+
60
+ if hasattr(event_msg.event_meta, "trigger"):
61
+ context["trigger"] = event_msg.event_meta.trigger
62
+
63
+ if hasattr(event_msg.event_meta, "parent_span_id"):
64
+ context["parent_span_id"] = event_msg.event_meta.parent_span_id
65
+
66
+ thread_local_storage.set_all(context)
67
+ logger.info("Event received", type="distributed_trace")
68
+
69
+ def _get_task_tracking_id(self, task_details: Dict[str, Any]) -> Optional[Dict[str, Any]]:
70
+ """
71
+ Extract tracking id from task details if available.
72
+
73
+ Args:
74
+ task_details: Task details dictionary
75
+
76
+ Returns:
77
+ tracking id or None
78
+ """
79
+ task = task_details.get("task_details") if task_details else None
80
+ if task and task.get("tracking_id"):
81
+ return json.loads(task["tracking_id"])
82
+ return None
83
+
84
+ def _is_duplicate_task(self, task_details: Optional[dict]) -> bool:
85
+ return bool(task_details and task_details.get("is_duplicate", False))
86
+
87
+ def _handle_new_task(self, event_msg: EventMessage):
88
+ """
89
+ Create a new task and set event meta and message receipt handle.
90
+
91
+ Args:
92
+ event_msg: Event message
93
+ """
94
+ logger.info(
95
+ f"[JobExecutor] Creating new task:\n"
96
+ f" event_name: {event_msg.event_name}\n"
97
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
98
+ f" span_id: {event_msg.event_meta.span_id}"
99
+ )
100
+ self.status_tracker.create_task(event_msg=event_msg, status=TaskStatus.PROCESSING.value)
101
+ self.status_tracker.set_event_meta_and_message_receipt_handle(event_msg)
102
+
103
+ def _update_queued_task(self, event_msg: EventMessage):
104
+ """
105
+ Update a queued task and set event meta and message receipt handle.
106
+
107
+ Args:
108
+ event_msg: Event message
109
+ """
110
+ logger.info(
111
+ f"[JobExecutor] Updating queued task:\n"
112
+ f" event_name: {event_msg.event_name}\n"
113
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
114
+ f" span_id: {event_msg.event_meta.span_id}"
115
+ )
116
+ self.status_tracker.set_event_meta_and_message_receipt_handle(event_msg)
117
+ self.status_tracker.update_task(event_msg=event_msg, status=TaskStatus.PROCESSING.value)
118
+
119
+ def _handle_dlq(self, event_msg: EventMessage):
120
+ """
121
+ Publish event to DLQ.
122
+
123
+ Args:
124
+ event_msg: Event message
125
+ """
126
+ logger.warning(
127
+ f"[JobExecutor] Publishing event to DLQ:\n"
128
+ f" event_name: {event_msg.event_name}\n"
129
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
130
+ f" span_id: {event_msg.event_meta.span_id}"
131
+ )
132
+ self.databus.publish_event("DLQ", {
133
+ "event_name": event_msg.event_name,
134
+ "event_payload": event_msg.event_payload,
135
+ "event_meta": event_msg.event_meta.to_dict(),
136
+ })
137
+
138
+ def _check_and_update_task_status(self, event_msg: AWSEventMessage) -> tuple[bool, Optional[Dict[str, Any]]]:
139
+ """
140
+ Determines task state, returns whether it's a duplicate and tracking ID if needed.
141
+
142
+ Args:
143
+ event_msg: Event message
144
+
145
+ Returns:
146
+ tuple: (is_duplicate, tracking_id)
147
+ """
148
+ is_duplicate_task = False
149
+ tracking_id = None
150
+
151
+ logger.info(
152
+ f"[JobExecutor] Checking task status:\n"
153
+ f" event_name: {event_msg.event_name}\n"
154
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
155
+ f" span_id: {event_msg.event_meta.span_id}"
156
+ )
157
+
158
+ task_details: dict = self.status_tracker.get_task(task_details=event_msg)
159
+
160
+ # check if the task is marked as a duplicate
161
+ if self._is_duplicate_task(task_details):
162
+ logger.info(
163
+ f"[JobExecutor] Duplicate task already in progress:\n"
164
+ f" event_name: {event_msg.event_name}\n"
165
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
166
+ f" span_id: {event_msg.event_meta.span_id}"
167
+ )
168
+ is_duplicate_task = True
169
+
170
+ task = task_details.get("task_details") if task_details else None
171
+
172
+ # task does not exist -> this is a new/fresh task
173
+ if not task:
174
+ logger.info(
175
+ f"[JobExecutor] No existing task found, treating as new:\n"
176
+ f" event_name: {event_msg.event_name}\n"
177
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
178
+ f" span_id: {event_msg.event_meta.span_id}"
179
+ )
180
+ self._handle_new_task(event_msg)
181
+
182
+ # task exists and is in QUEUED state
183
+ # -> first execution attempt, possibly re-queued from restart mechanism
184
+ # update in to PROCESSING state and get tracking id
185
+ elif task["status"] == TaskStatus.QUEUED.value:
186
+ logger.info(
187
+ f"[JobExecutor] Task is queued, updating to processing:\n"
188
+ f" event_name: {event_msg.event_name}\n"
189
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
190
+ f" span_id: {event_msg.event_meta.span_id}"
191
+ )
192
+ self._update_queued_task(event_msg)
193
+ tracking_id = self._get_task_tracking_id(task_details)
194
+
195
+ # task exists and is in PROCESSING state -> execution started earlier but was interrupted
196
+ # so just get the tracking id
197
+ elif task["status"] == TaskStatus.PROCESSING.value:
198
+ logger.info(
199
+ f"[JobExecutor] Task is already in processing:\n"
200
+ f" event_name: {event_msg.event_name}\n"
201
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
202
+ f" span_id: {event_msg.event_meta.span_id}"
203
+ )
204
+ tracking_id = self._get_task_tracking_id(task_details)
205
+
206
+ return is_duplicate_task, tracking_id
207
+
208
+ def _handle_failure(self, event_msg: EventMessage, exception: Exception):
209
+ """
210
+ Handles failure reporting, tracking, and DLQ publishing.
211
+
212
+ Args:
213
+ event_msg: The event being processed
214
+ exception: The exception that occurred
215
+ """
216
+ logger.error(
217
+ f"[JobExecutor] Error processing event:\n"
218
+ f" event_name: {event_msg.event_name}\n"
219
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
220
+ f" span_id: {event_msg.event_meta.span_id}\n"
221
+ f" error: {str(exception)}"
222
+ )
223
+ event_msg.event_meta.failure_reason = str(exception)
224
+
225
+ self.status_tracker.mark_task_as_failure(
226
+ span_id=event_msg.event_meta.span_id,
227
+ trace_id=event_msg.event_meta.trace_id,
228
+ task=event_msg.event_name,
229
+ failure_reason=str(exception),
230
+ task_id=event_msg.event_meta.job_id,
231
+ )
232
+
233
+ # NOTE: for dead letter queue we are simply publishing the
234
+ # failed event to the databus as a DLQ event.
235
+ self._handle_dlq(event_msg)
236
+
237
+ def submit_job(self, callback: Callable, event_msg: AWSEventMessage) -> None:
238
+ """
239
+ Combined method to process message and handle status updates and cleanup.
240
+
241
+ Args:
242
+ callback: Callback function to invoke
243
+ event_msg: Event message to process
244
+ """
245
+ is_success = False
246
+ try:
247
+ logger.info(
248
+ f"[JobExecutor] Initiating event handling:\n"
249
+ f" event_name: {event_msg.event_name}\n"
250
+ f" event_payload: {event_msg.raw_message}\n"
251
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
252
+ f" span_id: {event_msg.event_meta.span_id}"
253
+ )
254
+
255
+ # start event timing
256
+ event_msg.event_meta.start_event_timer()
257
+ logger.info(
258
+ f"[JobExecutor] Event timer started:\n"
259
+ f" event_name: {event_msg.event_name}\n"
260
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
261
+ f" span_id: {event_msg.event_meta.span_id}"
262
+ )
263
+
264
+ # thread-local storage
265
+ self._setup_thread_context(event_msg)
266
+
267
+ is_duplicate_task, tracking_id = self._check_and_update_task_status(event_msg=event_msg)
268
+
269
+ if is_duplicate_task:
270
+ logger.info(
271
+ f"[JobExecutor] Skipping execution for duplicate task:\n"
272
+ f" event_name: {event_msg.event_name}\n"
273
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
274
+ f" span_id: {event_msg.event_meta.span_id}"
275
+ )
276
+ return
277
+
278
+ # pass the actual message to the callback rather than the event message object
279
+ callback(event_msg.raw_message, tracking_id)
280
+ is_success = True
281
+
282
+ except Exception as e:
283
+ self._handle_failure(event_msg, e)
284
+
285
+ finally:
286
+ event_msg.event_meta.end_event_timer()
287
+ self.status_tracker.set_event_meta_and_message_receipt_handle(event_msg)
288
+
289
+ if is_success:
290
+ logger.info(
291
+ f"[JobExecutor] Event processed successfully:\n"
292
+ f" event_name: {event_msg.event_name}\n"
293
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
294
+ f" span_id: {event_msg.event_meta.span_id}"
295
+ )
296
+ self.status_tracker.update_task(event_msg=event_msg, status=TaskStatus.COMPLETED.value)
297
+ else:
298
+ logger.error(
299
+ f"[JobExecutor] Event processing failed:\n"
300
+ f" event_name: {event_msg.event_name}\n"
301
+ f" trace_id: {event_msg.event_meta.trace_id}\n"
302
+ f" span_id: {event_msg.event_meta.span_id}"
303
+ )
304
+
305
+ thread_local_storage.clear()
306
+ self.databus.delete_message(event_msg)
@@ -25,14 +25,4 @@ class EventConcurrencyManager:
25
25
 
26
26
  def get_event_semaphore(self, event_name: str):
27
27
  semaphore = self.event_semaphores.get(event_name)
28
- return semaphore
29
-
30
- @dataclass
31
- class EventStats:
32
- """Statistics for an event type"""
33
- successful_events: int = 0
34
- failed_events: int = 0
35
-
36
- @property
37
- def total_events(self) -> int:
38
- return self.successful_events + self.failed_events
28
+ return semaphore
@@ -2,7 +2,7 @@ import logging
2
2
  import os
3
3
  import json
4
4
  from shared_kernel.config import Config
5
- from shared_kernel.utils.thread_local_util import ThreadLocalStorage
5
+ from shared_kernel.utils.thread_local_storage import ThreadLocalStorage
6
6
  class JSONFormatter(logging.Formatter):
7
7
  """
8
8
  Custom JSON formatter to structure log records as JSON.