arcane-execution-handler 1.0.0__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.
- arcane_execution_handler-1.0.0/PKG-INFO +24 -0
- arcane_execution_handler-1.0.0/README.md +5 -0
- arcane_execution_handler-1.0.0/arcane/execution_handler/__init__.py +3 -0
- arcane_execution_handler-1.0.0/arcane/execution_handler/datastore_lgp.py +32 -0
- arcane_execution_handler-1.0.0/arcane/execution_handler/exception.py +3 -0
- arcane_execution_handler-1.0.0/arcane/execution_handler/execution_handler.py +120 -0
- arcane_execution_handler-1.0.0/arcane/execution_handler/types.py +10 -0
- arcane_execution_handler-1.0.0/pyproject.toml +25 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arcane-execution-handler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generic execution-start handling for scheduled entities (ingestion, processing, ...)
|
|
5
|
+
Author: Arcane
|
|
6
|
+
Author-email: product@wearcane.com
|
|
7
|
+
Requires-Python: >=3.11,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: arcane-datastore (>=1.1.15,<2.0.0)
|
|
14
|
+
Requires-Dist: arcane-pubsub (>=1.5.0,<2.0.0)
|
|
15
|
+
Requires-Dist: python-dateutil (>=2.7,<3.0)
|
|
16
|
+
Requires-Dist: pytz (>=2024.2,<2025.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Arcane execution-handler README
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Release history
|
|
23
|
+
To see changes, please see CHANGELOG.md
|
|
24
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import pytz
|
|
4
|
+
|
|
5
|
+
from google.cloud.datastore import Entity
|
|
6
|
+
|
|
7
|
+
from arcane.datastore import DATA_INGESTION_KIND, Client as DatastoreClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_execution_info_as_start(
|
|
11
|
+
datastore_client: DatastoreClient,
|
|
12
|
+
entity: Entity,
|
|
13
|
+
kind: str = DATA_INGESTION_KIND,
|
|
14
|
+
current_time: Optional[datetime] = None
|
|
15
|
+
):
|
|
16
|
+
"""Set an entity's execution_info to `running` on the given Datastore kind.
|
|
17
|
+
"""
|
|
18
|
+
entity_id = entity['id']
|
|
19
|
+
if not current_time:
|
|
20
|
+
current_time = datetime.now().replace(microsecond=0).astimezone(pytz.utc)
|
|
21
|
+
execution_info = {
|
|
22
|
+
**entity.get('execution_info', {}),
|
|
23
|
+
"status": "running",
|
|
24
|
+
"timestamp": current_time,
|
|
25
|
+
"errors": []
|
|
26
|
+
}
|
|
27
|
+
updated_properties = dict(
|
|
28
|
+
execution_info=datastore_client.convert_input_to_excluded_entity(
|
|
29
|
+
execution_info)
|
|
30
|
+
)
|
|
31
|
+
datastore_client.save_entity_with_transactions(
|
|
32
|
+
entity_id, updated_properties, kind)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from dateutil.parser import isoparse
|
|
5
|
+
import pytz
|
|
6
|
+
|
|
7
|
+
from google.cloud.datastore import Entity
|
|
8
|
+
|
|
9
|
+
from arcane.datastore import Client as DatastoreClient, DATA_INGESTION_KIND, DATA_PROCESSING_KIND
|
|
10
|
+
from arcane.pubsub import Client as PubSubClient
|
|
11
|
+
|
|
12
|
+
from .datastore_lgp import set_execution_info_as_start
|
|
13
|
+
from .exception import SkipExecutionError
|
|
14
|
+
from .types import ExecutionStartBody
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_delta_time_for_ingestion_type(data_ingestion_type: str) -> timedelta:
|
|
18
|
+
"""Delta time to consider a running ingestion still in progress"""
|
|
19
|
+
delta = timedelta(minutes=30)
|
|
20
|
+
# Because bigquery ingestion is expensive, we prevent overretrying
|
|
21
|
+
# Because HTTP ingestion can run during 30 minutes and can be retry 3 times
|
|
22
|
+
if data_ingestion_type == 'BIG_QUERY' or data_ingestion_type == 'HTTP':
|
|
23
|
+
delta = timedelta(minutes=90)
|
|
24
|
+
# Because SHOPIFY_API ingestion can run during several minutes and can be retry 5 times (5 bulk exec per token)
|
|
25
|
+
elif data_ingestion_type == 'SHOPIFY_API':
|
|
26
|
+
delta = timedelta(minutes=60)
|
|
27
|
+
return delta
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_delta_time_for_processing() -> timedelta:
|
|
31
|
+
"""Delta time for a running processing"""
|
|
32
|
+
return timedelta(minutes=90)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_schedule_version(entity: Entity, kind: str) -> Optional[str]:
|
|
36
|
+
if kind == DATA_PROCESSING_KIND:
|
|
37
|
+
return entity.get('schedule_version')
|
|
38
|
+
return entity['parameters']['schedule_version']
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle_start_execution(
|
|
42
|
+
body: ExecutionStartBody,
|
|
43
|
+
entity: Entity,
|
|
44
|
+
pubsub_client: PubSubClient,
|
|
45
|
+
datastore_client: DatastoreClient,
|
|
46
|
+
project_id: str,
|
|
47
|
+
pf_monitoring_topic: str,
|
|
48
|
+
delta_time: timedelta,
|
|
49
|
+
monitoring_step: str,
|
|
50
|
+
task_retry_count: int = 0,
|
|
51
|
+
*,
|
|
52
|
+
kind: str = DATA_INGESTION_KIND,
|
|
53
|
+
check_version: bool = True
|
|
54
|
+
):
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
Handle the start of an execution for an entity.
|
|
58
|
+
This function validates the schedule version, checks if the entity is already running,
|
|
59
|
+
updates the execution status, and publishes a monitoring event for product flow services.
|
|
60
|
+
Args:
|
|
61
|
+
body (dict): The request body containing monitoring_id, entity_id, and schedule_version.
|
|
62
|
+
entity (Entity): The entity object containing parameters and execution information.
|
|
63
|
+
pubsub_client (PubSubClient): Client for publishing messages to Pub/Sub topics.
|
|
64
|
+
datastore_client (DatastoreClient): Client for interacting with the datastore.
|
|
65
|
+
project_id (str): The GCP project ID.
|
|
66
|
+
pf_monitoring_topic (str): The Pub/Sub topic name for product flow monitoring.
|
|
67
|
+
delta_time (timedelta): How long a running execution is still considered in progress
|
|
68
|
+
(anti over-retry window). Callers pass get_delta_time_for_ingestion_type(type) for
|
|
69
|
+
ingestions or get_delta_time_for_processing() for processings.
|
|
70
|
+
monitoring_step (str): The step name to include in the monitoring event.
|
|
71
|
+
task_retry_count (int): The value of the X-CloudTasks-TaskRetryCount header. If > 0, a running entity is treated as a Cloud Tasks retry and execution is allowed.
|
|
72
|
+
kind (str): Datastore kind of the entity (DATA_INGESTION_KIND by default).
|
|
73
|
+
check_version (bool): When False, skip the schedule_version comparison (used for the event-driven / OFF processing path that has no schedule_version). The `running` status is still set. Defaults to True.
|
|
74
|
+
Raises:
|
|
75
|
+
SkipExecutionError: If the schedule version is outdated (only when check_version) or if a previous execution
|
|
76
|
+
is still running (less than the allowed delta time).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
monitoring_id = body["monitoring_id"]
|
|
80
|
+
entity_id = int(body["entity_id"])
|
|
81
|
+
|
|
82
|
+
schedule_version = body.get('schedule_version')
|
|
83
|
+
manual_execution = body.get('manual_execution', False)
|
|
84
|
+
if manual_execution:
|
|
85
|
+
logging.info(f'Manual execution for entity with id {entity_id}. Running without checking schedule version and execution status.')
|
|
86
|
+
else:
|
|
87
|
+
if entity['enabled'] is False:
|
|
88
|
+
logging.info(f'Entity with id {entity_id} is disabled. Skipping execution.')
|
|
89
|
+
raise SkipExecutionError('Entity is disabled')
|
|
90
|
+
|
|
91
|
+
if check_version:
|
|
92
|
+
current_schedule_version = _get_schedule_version(entity, kind)
|
|
93
|
+
if schedule_version != current_schedule_version:
|
|
94
|
+
logging.info(f'Entity with id {entity_id} has outdated schedule info. Previous version {schedule_version} and {current_schedule_version}')
|
|
95
|
+
raise SkipExecutionError(f'Schedule info is outdated. Previous version {schedule_version} and current version {current_schedule_version}')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if entity.get('execution_info') is not None and 'status' in entity['execution_info']:
|
|
99
|
+
if entity['execution_info']['status'] == 'running':
|
|
100
|
+
timestamp = entity['execution_info'].get('timestamp')
|
|
101
|
+
if timestamp and not isinstance(timestamp, datetime):
|
|
102
|
+
timestamp = isoparse(timestamp).astimezone(pytz.utc)
|
|
103
|
+
if timestamp and (datetime.now(timezone.utc) - timestamp).total_seconds() < delta_time.total_seconds():
|
|
104
|
+
if task_retry_count > 0:
|
|
105
|
+
logging.info(f'Entity with id {entity_id} is already running but this is a Cloud Tasks retry (retry_count={task_retry_count}). Allowing retry.')
|
|
106
|
+
else:
|
|
107
|
+
logging.info(f'Entity with id {entity_id} is already running since {timestamp}. Skipping execution.')
|
|
108
|
+
raise SkipExecutionError('Previous execution is still running')
|
|
109
|
+
|
|
110
|
+
set_execution_info_as_start(datastore_client, entity, kind)
|
|
111
|
+
if entity['service'] != 'datalab':
|
|
112
|
+
pubsub_client.pubsub_publish_pf_monitoring(
|
|
113
|
+
monitoring_id=monitoring_id,
|
|
114
|
+
entity_id=str(entity_id),
|
|
115
|
+
step=monitoring_step,
|
|
116
|
+
status='start',
|
|
117
|
+
project_id=project_id,
|
|
118
|
+
topic=pf_monitoring_topic
|
|
119
|
+
)
|
|
120
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any, NotRequired, Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExecutionStartBody(TypedDict):
|
|
5
|
+
"""Expected body of an execution-start event (ingestion or processing)."""
|
|
6
|
+
monitoring_id: str
|
|
7
|
+
entity_id: Any
|
|
8
|
+
schedule_version: NotRequired[Optional[str]]
|
|
9
|
+
manual_execution: NotRequired[bool]
|
|
10
|
+
execution_time: NotRequired[str]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "arcane-execution-handler"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Generic execution-start handling for scheduled entities (ingestion, processing, ...)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = ["Arcane <product@wearcane.com>"]
|
|
7
|
+
packages = [
|
|
8
|
+
{ include = "arcane" }
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = "^3.11"
|
|
13
|
+
arcane-datastore = "^1.1.15"
|
|
14
|
+
arcane-pubsub = "^1.5.0"
|
|
15
|
+
python-dateutil = "^2.7"
|
|
16
|
+
pytz = "^2024.2"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^7.0"
|
|
21
|
+
pytest-mock = "^3.10"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core>=1.0.0"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|