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.
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/PKG-INFO +1 -1
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/PKG-INFO +1 -1
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/SOURCES.txt +6 -1
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/pyproject.toml +1 -1
- fdc_shared_kernel-0.0.99/shared_kernel/dataclasses/event_executor.py +41 -0
- fdc_shared_kernel-0.0.99/shared_kernel/event_executor/event_executor.py +251 -0
- fdc_shared_kernel-0.0.99/shared_kernel/event_executor/job_executor.py +306 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/event_executor/utils.py +1 -11
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/logger/__init__.py +1 -1
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/aws_databus.py +16 -21
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/utils/aws_utility.py +1 -1
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/utils/event_messages.py +15 -14
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/status_tracker/status_tracker.py +19 -19
- fdc_shared_kernel-0.0.99/shared_kernel/tests/utils/__init__.py +0 -0
- fdc_shared_kernel-0.0.99/shared_kernel/utils/thread_debug_utils.py +43 -0
- fdc_shared_kernel-0.0.99/tests/test_job_executor.py +150 -0
- fdc_shared_kernel-0.0.97/shared_kernel/event_executor/event_executor.py +0 -481
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/README.md +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/README_pypi.md +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/dependency_links.txt +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/requires.txt +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/top_level.txt +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/requirements.txt +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/setup.cfg +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/async_task_executor/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/async_task_executor/async_task_executor.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/jwt_helper.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/token_handler.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/auth/workbook_permission_handler.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/config/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/database/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/datatype_mappings → fdc_shared_kernel-0.0.99/shared_kernel/dataclasses}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/models → fdc_shared_kernel-0.0.99/shared_kernel/datatype_mappings}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/csv.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/db2.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/jira.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/mssql.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/mysql.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/oracle.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/postgres.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/redshift.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/connectors_to_system/salesforce.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/postgres.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/datatype_mappings/system_to_warehouse/redshift.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/async_task_executor.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/enums/status_tracker.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/event_executor/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/configuration_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/custom_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/data_validation_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/http_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/infrastructure_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/operational_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/exceptions/security_exceptions.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/httpx_http_client.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/http/request_http_client.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/databus.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/http.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/interfaces/keyvault.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/http_databus.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_databus.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_publisher.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/messaging/nats_test.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/metrics/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/metrics/status_tracker.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/tests → fdc_shared_kernel-0.0.99/shared_kernel/models}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/registries/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/registries/service_event_registry.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/aws_secret_manager.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/security/key_vault/azure_keyvault.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/status_tracker/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/tests/config → fdc_shared_kernel-0.0.99/shared_kernel/tests}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/tests/logger → fdc_shared_kernel-0.0.99/shared_kernel/tests/config}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/config/test_config.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/tests/messaging → fdc_shared_kernel-0.0.99/shared_kernel/tests/logger}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/logger/test_logger.py +0 -0
- {fdc_shared_kernel-0.0.97/shared_kernel/tests/utils → fdc_shared_kernel-0.0.99/shared_kernel/tests/messaging}/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_aws_databus.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_event_executor.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/messaging/test_nats_interface.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_data_validators.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_date_format_utils.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/tests/utils/test_string_utils.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/data_validators_utils.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/date_format_utils.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/string_utils.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/shared_kernel/utils/template_renderer.py +0 -0
- /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
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/messaging/__init__.py +0 -0
- {fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/tests/messaging/utils/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
{fdc_shared_kernel-0.0.97 → fdc_shared_kernel-0.0.99}/fdc_shared_kernel.egg-info/SOURCES.txt
RENAMED
|
@@ -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/
|
|
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
|
|
@@ -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.
|
|
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.
|