atlan-application-sdk 0.1.1rc35__py3-none-any.whl → 0.1.1rc36__py3-none-any.whl
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.
- application_sdk/activities/lock_management.py +110 -0
- application_sdk/clients/redis.py +443 -0
- application_sdk/clients/temporal.py +31 -187
- application_sdk/common/error_codes.py +24 -3
- application_sdk/constants.py +18 -1
- application_sdk/decorators/__init__.py +0 -0
- application_sdk/decorators/locks.py +42 -0
- application_sdk/handlers/base.py +18 -1
- application_sdk/interceptors/__init__.py +0 -0
- application_sdk/interceptors/events.py +193 -0
- application_sdk/interceptors/lock.py +139 -0
- application_sdk/version.py +1 -1
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/METADATA +4 -2
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/RECORD +17 -10
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-0.1.1rc35.dist-info → atlan_application_sdk-0.1.1rc36.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import uuid
|
|
3
3
|
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
-
from datetime import timedelta
|
|
5
4
|
from typing import Any, Dict, Optional, Sequence, Type
|
|
6
5
|
|
|
7
6
|
from temporalio import activity, workflow
|
|
8
7
|
from temporalio.client import Client, WorkflowExecutionStatus, WorkflowFailureError
|
|
9
|
-
from temporalio.common import RetryPolicy
|
|
10
8
|
from temporalio.types import CallableType, ClassType
|
|
11
|
-
from temporalio.worker import
|
|
12
|
-
ActivityInboundInterceptor,
|
|
13
|
-
ExecuteActivityInput,
|
|
14
|
-
ExecuteWorkflowInput,
|
|
15
|
-
Interceptor,
|
|
16
|
-
Worker,
|
|
17
|
-
WorkflowInboundInterceptor,
|
|
18
|
-
WorkflowInterceptorClassInput,
|
|
19
|
-
)
|
|
9
|
+
from temporalio.worker import Worker
|
|
20
10
|
from temporalio.worker.workflow_sandbox import (
|
|
21
11
|
SandboxedWorkflowRunner,
|
|
22
12
|
SandboxRestrictions,
|
|
@@ -28,6 +18,7 @@ from application_sdk.constants import (
|
|
|
28
18
|
APPLICATION_NAME,
|
|
29
19
|
DEPLOYMENT_NAME,
|
|
30
20
|
DEPLOYMENT_NAME_KEY,
|
|
21
|
+
IS_LOCKING_DISABLED,
|
|
31
22
|
MAX_CONCURRENT_ACTIVITIES,
|
|
32
23
|
WORKFLOW_HOST,
|
|
33
24
|
WORKFLOW_MAX_TIMEOUT_HOURS,
|
|
@@ -35,15 +26,9 @@ from application_sdk.constants import (
|
|
|
35
26
|
WORKFLOW_PORT,
|
|
36
27
|
WORKFLOW_TLS_ENABLED_KEY,
|
|
37
28
|
)
|
|
38
|
-
from application_sdk.events
|
|
39
|
-
|
|
40
|
-
Event,
|
|
41
|
-
EventMetadata,
|
|
42
|
-
EventTypes,
|
|
43
|
-
WorkflowStates,
|
|
44
|
-
)
|
|
29
|
+
from application_sdk.interceptors.events import EventInterceptor, publish_event
|
|
30
|
+
from application_sdk.interceptors.lock import RedisLockInterceptor
|
|
45
31
|
from application_sdk.observability.logger_adaptor import get_logger
|
|
46
|
-
from application_sdk.services.eventstore import EventStore
|
|
47
32
|
from application_sdk.services.secretstore import SecretStore
|
|
48
33
|
from application_sdk.services.statestore import StateStore, StateType
|
|
49
34
|
from application_sdk.workflows import WorkflowInterface
|
|
@@ -55,170 +40,6 @@ TEMPORAL_NOT_FOUND_FAILURE = (
|
|
|
55
40
|
)
|
|
56
41
|
|
|
57
42
|
|
|
58
|
-
# Activity for publishing events (runs outside sandbox)
|
|
59
|
-
@activity.defn
|
|
60
|
-
async def publish_event(event_data: dict) -> None:
|
|
61
|
-
"""Activity to publish events outside the workflow sandbox.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
event_data (dict): Event data to publish containing event_type, event_name,
|
|
65
|
-
metadata, and data fields.
|
|
66
|
-
"""
|
|
67
|
-
try:
|
|
68
|
-
event = Event(**event_data)
|
|
69
|
-
await EventStore.publish_event(event)
|
|
70
|
-
activity.logger.info(f"Published event: {event_data.get('event_name','')}")
|
|
71
|
-
except Exception as e:
|
|
72
|
-
activity.logger.error(f"Failed to publish event: {e}")
|
|
73
|
-
raise
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class EventActivityInboundInterceptor(ActivityInboundInterceptor):
|
|
77
|
-
"""Interceptor for tracking activity execution events.
|
|
78
|
-
|
|
79
|
-
This interceptor captures the start and end of activity executions,
|
|
80
|
-
creating events that can be used for monitoring and tracking.
|
|
81
|
-
Activities run outside the sandbox so they can directly call EventStore.
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
async def execute_activity(self, input: ExecuteActivityInput) -> Any:
|
|
85
|
-
"""Execute an activity with event tracking.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
input (ExecuteActivityInput): The activity execution input.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Any: The result of the activity execution.
|
|
92
|
-
"""
|
|
93
|
-
# Extract activity information for tracking
|
|
94
|
-
|
|
95
|
-
start_event = Event(
|
|
96
|
-
event_type=EventTypes.APPLICATION_EVENT.value,
|
|
97
|
-
event_name=ApplicationEventNames.ACTIVITY_START.value,
|
|
98
|
-
data={},
|
|
99
|
-
)
|
|
100
|
-
await EventStore.publish_event(start_event)
|
|
101
|
-
|
|
102
|
-
output = None
|
|
103
|
-
try:
|
|
104
|
-
output = await super().execute_activity(input)
|
|
105
|
-
except Exception:
|
|
106
|
-
raise
|
|
107
|
-
finally:
|
|
108
|
-
end_event = Event(
|
|
109
|
-
event_type=EventTypes.APPLICATION_EVENT.value,
|
|
110
|
-
event_name=ApplicationEventNames.ACTIVITY_END.value,
|
|
111
|
-
data={},
|
|
112
|
-
)
|
|
113
|
-
await EventStore.publish_event(end_event)
|
|
114
|
-
|
|
115
|
-
return output
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class EventWorkflowInboundInterceptor(WorkflowInboundInterceptor):
|
|
119
|
-
"""Interceptor for tracking workflow execution events.
|
|
120
|
-
|
|
121
|
-
This interceptor captures the start and end of workflow executions,
|
|
122
|
-
creating events that can be used for monitoring and tracking.
|
|
123
|
-
Uses activities to publish events to avoid sandbox restrictions.
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
|
|
127
|
-
"""Execute a workflow with event tracking.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
input (ExecuteWorkflowInput): The workflow execution input.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
Any: The result of the workflow execution.
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
# Publish workflow start event via activity
|
|
137
|
-
try:
|
|
138
|
-
await workflow.execute_activity(
|
|
139
|
-
publish_event,
|
|
140
|
-
{
|
|
141
|
-
"metadata": EventMetadata(
|
|
142
|
-
workflow_state=WorkflowStates.RUNNING.value
|
|
143
|
-
),
|
|
144
|
-
"event_type": EventTypes.APPLICATION_EVENT.value,
|
|
145
|
-
"event_name": ApplicationEventNames.WORKFLOW_START.value,
|
|
146
|
-
"data": {},
|
|
147
|
-
},
|
|
148
|
-
schedule_to_close_timeout=timedelta(seconds=30),
|
|
149
|
-
retry_policy=RetryPolicy(maximum_attempts=3),
|
|
150
|
-
)
|
|
151
|
-
except Exception as e:
|
|
152
|
-
workflow.logger.warning(f"Failed to publish workflow start event: {e}")
|
|
153
|
-
# Don't fail the workflow if event publishing fails
|
|
154
|
-
|
|
155
|
-
output = None
|
|
156
|
-
workflow_state = WorkflowStates.FAILED.value # Default to failed
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
output = await super().execute_workflow(input)
|
|
160
|
-
workflow_state = (
|
|
161
|
-
WorkflowStates.COMPLETED.value
|
|
162
|
-
) # Update to completed on success
|
|
163
|
-
except Exception:
|
|
164
|
-
workflow_state = WorkflowStates.FAILED.value # Keep as failed
|
|
165
|
-
raise
|
|
166
|
-
finally:
|
|
167
|
-
# Always publish workflow end event
|
|
168
|
-
try:
|
|
169
|
-
await workflow.execute_activity(
|
|
170
|
-
publish_event,
|
|
171
|
-
{
|
|
172
|
-
"metadata": EventMetadata(workflow_state=workflow_state),
|
|
173
|
-
"event_type": EventTypes.APPLICATION_EVENT.value,
|
|
174
|
-
"event_name": ApplicationEventNames.WORKFLOW_END.value,
|
|
175
|
-
"data": {},
|
|
176
|
-
},
|
|
177
|
-
schedule_to_close_timeout=timedelta(seconds=30),
|
|
178
|
-
retry_policy=RetryPolicy(maximum_attempts=3),
|
|
179
|
-
)
|
|
180
|
-
except Exception as publish_error:
|
|
181
|
-
workflow.logger.warning(
|
|
182
|
-
f"Failed to publish workflow end event: {publish_error}"
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
return output
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class EventInterceptor(Interceptor):
|
|
189
|
-
"""Temporal interceptor for event tracking.
|
|
190
|
-
|
|
191
|
-
This interceptor provides event tracking capabilities for both
|
|
192
|
-
workflow and activity executions.
|
|
193
|
-
"""
|
|
194
|
-
|
|
195
|
-
def intercept_activity(
|
|
196
|
-
self, next: ActivityInboundInterceptor
|
|
197
|
-
) -> ActivityInboundInterceptor:
|
|
198
|
-
"""Intercept activity executions.
|
|
199
|
-
|
|
200
|
-
Args:
|
|
201
|
-
next (ActivityInboundInterceptor): The next interceptor in the chain.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
ActivityInboundInterceptor: The activity interceptor.
|
|
205
|
-
"""
|
|
206
|
-
return EventActivityInboundInterceptor(super().intercept_activity(next))
|
|
207
|
-
|
|
208
|
-
def workflow_interceptor_class(
|
|
209
|
-
self, input: WorkflowInterceptorClassInput
|
|
210
|
-
) -> Optional[Type[WorkflowInboundInterceptor]]:
|
|
211
|
-
"""Get the workflow interceptor class.
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
input (WorkflowInterceptorClassInput): The interceptor input.
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
Optional[Type[WorkflowInboundInterceptor]]: The workflow interceptor class.
|
|
218
|
-
"""
|
|
219
|
-
return EventWorkflowInboundInterceptor
|
|
220
|
-
|
|
221
|
-
|
|
222
43
|
class TemporalWorkflowClient(WorkflowClient):
|
|
223
44
|
"""Temporal-specific implementation of WorkflowClient with simple token refresh.
|
|
224
45
|
|
|
@@ -537,14 +358,34 @@ class TemporalWorkflowClient(WorkflowClient):
|
|
|
537
358
|
f"Started token refresh loop with dynamic interval (initial: {self._token_refresh_interval}s)"
|
|
538
359
|
)
|
|
539
360
|
|
|
540
|
-
#
|
|
541
|
-
|
|
361
|
+
# Start with provided activities and add system activities
|
|
362
|
+
final_activities = list(activities) + [publish_event]
|
|
363
|
+
|
|
364
|
+
# Add lock management activities if needed
|
|
365
|
+
if not IS_LOCKING_DISABLED:
|
|
366
|
+
from application_sdk.activities.lock_management import (
|
|
367
|
+
acquire_distributed_lock,
|
|
368
|
+
release_distributed_lock,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
final_activities.extend(
|
|
372
|
+
[
|
|
373
|
+
acquire_distributed_lock,
|
|
374
|
+
release_distributed_lock,
|
|
375
|
+
]
|
|
376
|
+
)
|
|
377
|
+
logger.info(
|
|
378
|
+
"Auto-registered lock management activities for @needs_lock decorated activities"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Create activities lookup dict for interceptors
|
|
382
|
+
activities_dict = {getattr(a, "__name__", str(a)): a for a in final_activities}
|
|
542
383
|
|
|
543
384
|
return Worker(
|
|
544
385
|
self.client,
|
|
545
386
|
task_queue=self.worker_task_queue,
|
|
546
387
|
workflows=workflow_classes,
|
|
547
|
-
activities=
|
|
388
|
+
activities=final_activities,
|
|
548
389
|
workflow_runner=SandboxedWorkflowRunner(
|
|
549
390
|
restrictions=SandboxRestrictions.default.with_passthrough_modules(
|
|
550
391
|
*passthrough_modules
|
|
@@ -552,7 +393,10 @@ class TemporalWorkflowClient(WorkflowClient):
|
|
|
552
393
|
),
|
|
553
394
|
max_concurrent_activities=max_concurrent_activities,
|
|
554
395
|
activity_executor=activity_executor,
|
|
555
|
-
interceptors=[
|
|
396
|
+
interceptors=[
|
|
397
|
+
EventInterceptor(),
|
|
398
|
+
RedisLockInterceptor(activities_dict),
|
|
399
|
+
],
|
|
556
400
|
)
|
|
557
401
|
|
|
558
402
|
async def get_workflow_run_status(
|
|
@@ -81,6 +81,18 @@ class ClientError(AtlanError):
|
|
|
81
81
|
AUTH_CONFIG_ERROR = ErrorCode(
|
|
82
82
|
ErrorComponent.CLIENT, "400", "00", "Authentication configuration error"
|
|
83
83
|
)
|
|
84
|
+
REDIS_CONNECTION_ERROR = ErrorCode(
|
|
85
|
+
ErrorComponent.CLIENT, "503", "00", "Redis connection failed"
|
|
86
|
+
)
|
|
87
|
+
REDIS_TIMEOUT_ERROR = ErrorCode(
|
|
88
|
+
ErrorComponent.CLIENT, "408", "00", "Redis operation timeout"
|
|
89
|
+
)
|
|
90
|
+
REDIS_AUTH_ERROR = ErrorCode(
|
|
91
|
+
ErrorComponent.CLIENT, "401", "05", "Redis authentication failed"
|
|
92
|
+
)
|
|
93
|
+
REDIS_PROTOCOL_ERROR = ErrorCode(
|
|
94
|
+
ErrorComponent.CLIENT, "502", "00", "Redis protocol error"
|
|
95
|
+
)
|
|
84
96
|
|
|
85
97
|
|
|
86
98
|
class ApiError(AtlanError):
|
|
@@ -174,7 +186,7 @@ class IOError(AtlanError):
|
|
|
174
186
|
INPUT_PROCESSING_ERROR = ErrorCode(
|
|
175
187
|
ErrorComponent.IO, "500", "01", "Input processing error"
|
|
176
188
|
)
|
|
177
|
-
SQL_QUERY_ERROR = ErrorCode(ErrorComponent.IO, "400", "
|
|
189
|
+
SQL_QUERY_ERROR = ErrorCode(ErrorComponent.IO, "400", "01", "SQL query error")
|
|
178
190
|
SQL_QUERY_BATCH_ERROR = ErrorCode(
|
|
179
191
|
ErrorComponent.IO, "500", "02", "SQL query batch error"
|
|
180
192
|
)
|
|
@@ -268,10 +280,10 @@ class CommonError(AtlanError):
|
|
|
268
280
|
ErrorComponent.COMMON, "400", "01", "Query preparation error"
|
|
269
281
|
)
|
|
270
282
|
FILTER_PREPARATION_ERROR = ErrorCode(
|
|
271
|
-
ErrorComponent.COMMON, "400", "
|
|
283
|
+
ErrorComponent.COMMON, "400", "02", "Filter preparation error"
|
|
272
284
|
)
|
|
273
285
|
CREDENTIALS_PARSE_ERROR = ErrorCode(
|
|
274
|
-
ErrorComponent.COMMON, "400", "
|
|
286
|
+
ErrorComponent.COMMON, "400", "03", "Credentials parse error"
|
|
275
287
|
)
|
|
276
288
|
CREDENTIALS_RESOLUTION_ERROR = ErrorCode(
|
|
277
289
|
ErrorComponent.COMMON, "401", "03", "Credentials resolution error"
|
|
@@ -367,3 +379,12 @@ class ActivityError(AtlanError):
|
|
|
367
379
|
ATLAN_UPLOAD_ERROR = ErrorCode(
|
|
368
380
|
ErrorComponent.ACTIVITY, "500", "08", "Atlan upload error"
|
|
369
381
|
)
|
|
382
|
+
LOCK_ACQUISITION_ERROR = ErrorCode(
|
|
383
|
+
ErrorComponent.ACTIVITY, "503", "01", "Distributed lock acquisition error"
|
|
384
|
+
)
|
|
385
|
+
LOCK_RELEASE_ERROR = ErrorCode(
|
|
386
|
+
ErrorComponent.ACTIVITY, "500", "09", "Distributed lock release error"
|
|
387
|
+
)
|
|
388
|
+
LOCK_TIMEOUT_ERROR = ErrorCode(
|
|
389
|
+
ErrorComponent.ACTIVITY, "408", "00", "Lock acquisition timeout"
|
|
390
|
+
)
|
application_sdk/constants.py
CHANGED
|
@@ -146,7 +146,6 @@ DEPLOYMENT_SECRET_STORE_NAME = os.getenv(
|
|
|
146
146
|
"DEPLOYMENT_SECRET_STORE_NAME", "deployment-secret-store"
|
|
147
147
|
)
|
|
148
148
|
|
|
149
|
-
|
|
150
149
|
# Logger Constants
|
|
151
150
|
#: Log level for the application (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
152
151
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
@@ -230,3 +229,21 @@ ATLAN_BASE_URL = os.getenv("ATLAN_BASE_URL")
|
|
|
230
229
|
ATLAN_API_KEY = os.getenv("ATLAN_API_KEY")
|
|
231
230
|
ATLAN_CLIENT_ID = os.getenv("CLIENT_ID")
|
|
232
231
|
ATLAN_CLIENT_SECRET = os.getenv("CLIENT_SECRET")
|
|
232
|
+
# Lock Configuration
|
|
233
|
+
LOCK_METADATA_KEY = "__lock_metadata__"
|
|
234
|
+
|
|
235
|
+
# Redis Lock Configuration
|
|
236
|
+
#: Redis host for direct connection (when not using Sentinel)
|
|
237
|
+
REDIS_HOST = os.getenv("REDIS_HOST", "")
|
|
238
|
+
#: Redis port for direct connection (when not using Sentinel)
|
|
239
|
+
REDIS_PORT = os.getenv("REDIS_PORT", "")
|
|
240
|
+
#: Redis password (required for authenticated Redis instances)
|
|
241
|
+
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
|
|
242
|
+
#: Redis Sentinel service name (default: mymaster)
|
|
243
|
+
REDIS_SENTINEL_SERVICE_NAME = os.getenv("REDIS_SENTINEL_SERVICE_NAME", "mymaster")
|
|
244
|
+
#: Redis Sentinel hosts (comma-separated host:port pairs)
|
|
245
|
+
REDIS_SENTINEL_HOSTS = os.getenv("REDIS_SENTINEL_HOSTS", "")
|
|
246
|
+
#: Whether to enable strict locking
|
|
247
|
+
IS_LOCKING_DISABLED = os.getenv("IS_LOCKING_DISABLED", "true").lower() == "true"
|
|
248
|
+
#: Retry interval for lock acquisition
|
|
249
|
+
LOCK_RETRY_INTERVAL = int(os.getenv("LOCK_RETRY_INTERVAL", "5"))
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Callable, Optional
|
|
2
|
+
|
|
3
|
+
from application_sdk.constants import LOCK_METADATA_KEY
|
|
4
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
5
|
+
|
|
6
|
+
logger = get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def needs_lock(max_locks: int = 5, lock_name: Optional[str] = None):
|
|
10
|
+
"""Decorator to mark activities that require distributed locking.
|
|
11
|
+
|
|
12
|
+
This decorator attaches lock configuration directly to the activity
|
|
13
|
+
definition that will be used by the workflow interceptor to acquire
|
|
14
|
+
locks before executing activities.
|
|
15
|
+
|
|
16
|
+
Note:
|
|
17
|
+
Activities decorated with ``needs_lock`` must be called with
|
|
18
|
+
``schedule_to_close_timeout`` to ensure proper lock TTL calculation
|
|
19
|
+
that covers retries.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
max_locks (int): Maximum number of concurrent locks allowed.
|
|
23
|
+
lock_name (str | None): Optional custom name for the lock (defaults to activity name).
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
WorkflowError: If activity is called without ``schedule_to_close_timeout``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
30
|
+
# Store lock metadata directly on the function object
|
|
31
|
+
metadata = {
|
|
32
|
+
"is_needs_lock": True,
|
|
33
|
+
"max_locks": max_locks,
|
|
34
|
+
"lock_name": lock_name or func.__name__,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Attach metadata to the function
|
|
38
|
+
setattr(func, LOCK_METADATA_KEY, metadata)
|
|
39
|
+
|
|
40
|
+
return func
|
|
41
|
+
|
|
42
|
+
return decorator
|
application_sdk/handlers/base.py
CHANGED
|
@@ -44,7 +44,24 @@ class BaseHandler(HandlerInterface):
|
|
|
44
44
|
|
|
45
45
|
# The following methods are inherited from HandlerInterface and should be implemented
|
|
46
46
|
# by subclasses to handle calls from their respective FastAPI endpoints:
|
|
47
|
-
#
|
|
48
47
|
# - test_auth(**kwargs) -> bool: Called by /workflow/v1/auth endpoint
|
|
49
48
|
# - preflight_check(**kwargs) -> Any: Called by /workflow/v1/check endpoint
|
|
50
49
|
# - fetch_metadata(**kwargs) -> Any: Called by /workflow/v1/metadata endpoint
|
|
50
|
+
|
|
51
|
+
async def test_auth(self, **kwargs: Any) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Test the authentication of the handler.
|
|
54
|
+
"""
|
|
55
|
+
raise NotImplementedError("test_auth is not implemented")
|
|
56
|
+
|
|
57
|
+
async def preflight_check(self, **kwargs: Any) -> Any:
|
|
58
|
+
"""
|
|
59
|
+
Check the preflight of the handler.
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError("preflight_check is not implemented")
|
|
62
|
+
|
|
63
|
+
async def fetch_metadata(self, **kwargs: Any) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Fetch the metadata of the handler.
|
|
66
|
+
"""
|
|
67
|
+
raise NotImplementedError("fetch_metadata is not implemented")
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from typing import Any, Optional, Type
|
|
3
|
+
|
|
4
|
+
from temporalio import activity, workflow
|
|
5
|
+
from temporalio.common import RetryPolicy
|
|
6
|
+
from temporalio.worker import (
|
|
7
|
+
ActivityInboundInterceptor,
|
|
8
|
+
ExecuteActivityInput,
|
|
9
|
+
ExecuteWorkflowInput,
|
|
10
|
+
Interceptor,
|
|
11
|
+
WorkflowInboundInterceptor,
|
|
12
|
+
WorkflowInterceptorClassInput,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from application_sdk.events.models import (
|
|
16
|
+
ApplicationEventNames,
|
|
17
|
+
Event,
|
|
18
|
+
EventMetadata,
|
|
19
|
+
EventTypes,
|
|
20
|
+
WorkflowStates,
|
|
21
|
+
)
|
|
22
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
23
|
+
from application_sdk.services.eventstore import EventStore
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
TEMPORAL_NOT_FOUND_FAILURE = (
|
|
28
|
+
"type.googleapis.com/temporal.api.errordetails.v1.NotFoundFailure"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Activity for publishing events (runs outside sandbox)
|
|
33
|
+
@activity.defn
|
|
34
|
+
async def publish_event(event_data: dict) -> None:
|
|
35
|
+
"""Activity to publish events outside the workflow sandbox.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
event_data (dict): Event data to publish containing event_type, event_name,
|
|
39
|
+
metadata, and data fields.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
event = Event(**event_data)
|
|
43
|
+
await EventStore.publish_event(event)
|
|
44
|
+
activity.logger.info(f"Published event: {event_data.get('event_name','')}")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
activity.logger.error(f"Failed to publish event: {e}")
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EventActivityInboundInterceptor(ActivityInboundInterceptor):
|
|
51
|
+
"""Interceptor for tracking activity execution events.
|
|
52
|
+
|
|
53
|
+
This interceptor captures the start and end of activity executions,
|
|
54
|
+
creating events that can be used for monitoring and tracking.
|
|
55
|
+
Activities run outside the sandbox so they can directly call EventStore.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
async def execute_activity(self, input: ExecuteActivityInput) -> Any:
|
|
59
|
+
"""Execute an activity with event tracking.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
input (ExecuteActivityInput): The activity execution input.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Any: The result of the activity execution.
|
|
66
|
+
"""
|
|
67
|
+
# Extract activity information for tracking
|
|
68
|
+
|
|
69
|
+
start_event = Event(
|
|
70
|
+
event_type=EventTypes.APPLICATION_EVENT.value,
|
|
71
|
+
event_name=ApplicationEventNames.ACTIVITY_START.value,
|
|
72
|
+
data={},
|
|
73
|
+
)
|
|
74
|
+
await EventStore.publish_event(start_event)
|
|
75
|
+
|
|
76
|
+
output = None
|
|
77
|
+
try:
|
|
78
|
+
output = await super().execute_activity(input)
|
|
79
|
+
except Exception:
|
|
80
|
+
raise
|
|
81
|
+
finally:
|
|
82
|
+
end_event = Event(
|
|
83
|
+
event_type=EventTypes.APPLICATION_EVENT.value,
|
|
84
|
+
event_name=ApplicationEventNames.ACTIVITY_END.value,
|
|
85
|
+
data={},
|
|
86
|
+
)
|
|
87
|
+
await EventStore.publish_event(end_event)
|
|
88
|
+
|
|
89
|
+
return output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EventWorkflowInboundInterceptor(WorkflowInboundInterceptor):
|
|
93
|
+
"""Interceptor for tracking workflow execution events.
|
|
94
|
+
|
|
95
|
+
This interceptor captures the start and end of workflow executions,
|
|
96
|
+
creating events that can be used for monitoring and tracking.
|
|
97
|
+
Uses activities to publish events to avoid sandbox restrictions.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
|
|
101
|
+
"""Execute a workflow with event tracking.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
input (ExecuteWorkflowInput): The workflow execution input.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Any: The result of the workflow execution.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Publish workflow start event via activity
|
|
111
|
+
try:
|
|
112
|
+
await workflow.execute_activity(
|
|
113
|
+
publish_event,
|
|
114
|
+
{
|
|
115
|
+
"metadata": EventMetadata(
|
|
116
|
+
workflow_state=WorkflowStates.RUNNING.value
|
|
117
|
+
),
|
|
118
|
+
"event_type": EventTypes.APPLICATION_EVENT.value,
|
|
119
|
+
"event_name": ApplicationEventNames.WORKFLOW_START.value,
|
|
120
|
+
"data": {},
|
|
121
|
+
},
|
|
122
|
+
schedule_to_close_timeout=timedelta(seconds=30),
|
|
123
|
+
retry_policy=RetryPolicy(maximum_attempts=3),
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
workflow.logger.warning(f"Failed to publish workflow start event: {e}")
|
|
127
|
+
# Don't fail the workflow if event publishing fails
|
|
128
|
+
|
|
129
|
+
output = None
|
|
130
|
+
workflow_state = WorkflowStates.FAILED.value # Default to failed
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
output = await super().execute_workflow(input)
|
|
134
|
+
workflow_state = (
|
|
135
|
+
WorkflowStates.COMPLETED.value
|
|
136
|
+
) # Update to completed on success
|
|
137
|
+
except Exception:
|
|
138
|
+
workflow_state = WorkflowStates.FAILED.value # Keep as failed
|
|
139
|
+
raise
|
|
140
|
+
finally:
|
|
141
|
+
# Always publish workflow end event
|
|
142
|
+
try:
|
|
143
|
+
await workflow.execute_activity(
|
|
144
|
+
publish_event,
|
|
145
|
+
{
|
|
146
|
+
"metadata": EventMetadata(workflow_state=workflow_state),
|
|
147
|
+
"event_type": EventTypes.APPLICATION_EVENT.value,
|
|
148
|
+
"event_name": ApplicationEventNames.WORKFLOW_END.value,
|
|
149
|
+
"data": {},
|
|
150
|
+
},
|
|
151
|
+
schedule_to_close_timeout=timedelta(seconds=30),
|
|
152
|
+
retry_policy=RetryPolicy(maximum_attempts=3),
|
|
153
|
+
)
|
|
154
|
+
except Exception as publish_error:
|
|
155
|
+
workflow.logger.warning(
|
|
156
|
+
f"Failed to publish workflow end event: {publish_error}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return output
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class EventInterceptor(Interceptor):
|
|
163
|
+
"""Temporal interceptor for event tracking.
|
|
164
|
+
|
|
165
|
+
This interceptor provides event tracking capabilities for both
|
|
166
|
+
workflow and activity executions.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def intercept_activity(
|
|
170
|
+
self, next: ActivityInboundInterceptor
|
|
171
|
+
) -> ActivityInboundInterceptor:
|
|
172
|
+
"""Intercept activity executions.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
next (ActivityInboundInterceptor): The next interceptor in the chain.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
ActivityInboundInterceptor: The activity interceptor.
|
|
179
|
+
"""
|
|
180
|
+
return EventActivityInboundInterceptor(super().intercept_activity(next))
|
|
181
|
+
|
|
182
|
+
def workflow_interceptor_class(
|
|
183
|
+
self, input: WorkflowInterceptorClassInput
|
|
184
|
+
) -> Optional[Type[WorkflowInboundInterceptor]]:
|
|
185
|
+
"""Get the workflow interceptor class.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
input (WorkflowInterceptorClassInput): The interceptor input.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Optional[Type[WorkflowInboundInterceptor]]: The workflow interceptor class.
|
|
192
|
+
"""
|
|
193
|
+
return EventWorkflowInboundInterceptor
|