avtomatika 1.0b3__py3-none-any.whl → 1.0b5__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.
- avtomatika/__init__.py +2 -2
- avtomatika/api.html +0 -11
- avtomatika/blueprint.py +9 -11
- avtomatika/config.py +7 -0
- avtomatika/context.py +18 -18
- avtomatika/data_types.py +6 -7
- avtomatika/datastore.py +2 -2
- avtomatika/dispatcher.py +20 -21
- avtomatika/engine.py +107 -68
- avtomatika/executor.py +168 -148
- avtomatika/history/base.py +7 -7
- avtomatika/history/noop.py +7 -7
- avtomatika/history/postgres.py +7 -9
- avtomatika/history/sqlite.py +7 -10
- avtomatika/logging_config.py +1 -1
- avtomatika/storage/__init__.py +2 -2
- avtomatika/storage/base.py +31 -20
- avtomatika/storage/memory.py +36 -43
- avtomatika/storage/redis.py +124 -60
- avtomatika/worker_config_loader.py +2 -2
- avtomatika/ws_manager.py +1 -2
- {avtomatika-1.0b3.dist-info → avtomatika-1.0b5.dist-info}/METADATA +44 -9
- avtomatika-1.0b5.dist-info/RECORD +37 -0
- avtomatika-1.0b3.dist-info/RECORD +0 -37
- {avtomatika-1.0b3.dist-info → avtomatika-1.0b5.dist-info}/WHEEL +0 -0
- {avtomatika-1.0b3.dist-info → avtomatika-1.0b5.dist-info}/licenses/LICENSE +0 -0
- {avtomatika-1.0b3.dist-info → avtomatika-1.0b5.dist-info}/top_level.txt +0 -0
avtomatika/storage/base.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class StorageBackend(ABC):
|
|
@@ -8,7 +8,7 @@ class StorageBackend(ABC):
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
@abstractmethod
|
|
11
|
-
async def get_job_state(self, job_id: str) ->
|
|
11
|
+
async def get_job_state(self, job_id: str) -> dict[str, Any] | None:
|
|
12
12
|
"""Get the full state of a job by its ID.
|
|
13
13
|
|
|
14
14
|
:param job_id: Unique identifier for the job.
|
|
@@ -20,8 +20,8 @@ class StorageBackend(ABC):
|
|
|
20
20
|
async def update_worker_data(
|
|
21
21
|
self,
|
|
22
22
|
worker_id: str,
|
|
23
|
-
update_data:
|
|
24
|
-
) ->
|
|
23
|
+
update_data: dict[str, Any],
|
|
24
|
+
) -> dict[str, Any] | None:
|
|
25
25
|
"""Partially update worker information without affecting its TTL.
|
|
26
26
|
Used for background processes like the reputation calculator.
|
|
27
27
|
|
|
@@ -54,9 +54,9 @@ class StorageBackend(ABC):
|
|
|
54
54
|
async def update_worker_status(
|
|
55
55
|
self,
|
|
56
56
|
worker_id: str,
|
|
57
|
-
status_update:
|
|
57
|
+
status_update: dict[str, Any],
|
|
58
58
|
ttl: int,
|
|
59
|
-
) ->
|
|
59
|
+
) -> dict[str, Any] | None:
|
|
60
60
|
"""Partially update worker information and extend its TTL.
|
|
61
61
|
Used for heartbeat messages.
|
|
62
62
|
|
|
@@ -68,7 +68,7 @@ class StorageBackend(ABC):
|
|
|
68
68
|
raise NotImplementedError
|
|
69
69
|
|
|
70
70
|
@abstractmethod
|
|
71
|
-
async def save_job_state(self, job_id: str, state:
|
|
71
|
+
async def save_job_state(self, job_id: str, state: dict[str, Any]) -> None:
|
|
72
72
|
"""Save the full state of a job.
|
|
73
73
|
|
|
74
74
|
:param job_id: Unique identifier for the job.
|
|
@@ -80,8 +80,8 @@ class StorageBackend(ABC):
|
|
|
80
80
|
async def update_job_state(
|
|
81
81
|
self,
|
|
82
82
|
job_id: str,
|
|
83
|
-
update_data:
|
|
84
|
-
) ->
|
|
83
|
+
update_data: dict[str, Any],
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
85
|
"""Partially update the state of a job.
|
|
86
86
|
|
|
87
87
|
:param job_id: Unique identifier for the job.
|
|
@@ -94,7 +94,7 @@ class StorageBackend(ABC):
|
|
|
94
94
|
async def register_worker(
|
|
95
95
|
self,
|
|
96
96
|
worker_id: str,
|
|
97
|
-
worker_info:
|
|
97
|
+
worker_info: dict[str, Any],
|
|
98
98
|
ttl: int,
|
|
99
99
|
) -> None:
|
|
100
100
|
"""Registers a new worker or updates information about an existing one.
|
|
@@ -109,7 +109,7 @@ class StorageBackend(ABC):
|
|
|
109
109
|
async def enqueue_task_for_worker(
|
|
110
110
|
self,
|
|
111
111
|
worker_id: str,
|
|
112
|
-
task_payload:
|
|
112
|
+
task_payload: dict[str, Any],
|
|
113
113
|
priority: float,
|
|
114
114
|
) -> None:
|
|
115
115
|
"""Adds a task to the priority queue for a specific worker.
|
|
@@ -125,7 +125,7 @@ class StorageBackend(ABC):
|
|
|
125
125
|
self,
|
|
126
126
|
worker_id: str,
|
|
127
127
|
timeout: int,
|
|
128
|
-
) ->
|
|
128
|
+
) -> dict[str, Any] | None:
|
|
129
129
|
"""Retrieves the highest priority task from the queue for a worker (blocking operation).
|
|
130
130
|
|
|
131
131
|
:param worker_id: The ID of the worker for whom to retrieve the task.
|
|
@@ -135,7 +135,7 @@ class StorageBackend(ABC):
|
|
|
135
135
|
raise NotImplementedError
|
|
136
136
|
|
|
137
137
|
@abstractmethod
|
|
138
|
-
async def get_available_workers(self) -> list[
|
|
138
|
+
async def get_available_workers(self) -> list[dict[str, Any]]:
|
|
139
139
|
"""Get a list of all active (not expired) workers.
|
|
140
140
|
|
|
141
141
|
:return: A list of dictionaries, where each dictionary represents information about a worker.
|
|
@@ -165,8 +165,19 @@ class StorageBackend(ABC):
|
|
|
165
165
|
raise NotImplementedError
|
|
166
166
|
|
|
167
167
|
@abstractmethod
|
|
168
|
-
async def dequeue_job(self) ->
|
|
169
|
-
"""Retrieve a job ID from the execution queue
|
|
168
|
+
async def dequeue_job(self) -> tuple[str, str] | None:
|
|
169
|
+
"""Retrieve a job ID and its message ID from the execution queue.
|
|
170
|
+
|
|
171
|
+
:return: A tuple of (job_id, message_id) or None if the timeout has expired.
|
|
172
|
+
"""
|
|
173
|
+
raise NotImplementedError
|
|
174
|
+
|
|
175
|
+
@abstractmethod
|
|
176
|
+
async def ack_job(self, message_id: str) -> None:
|
|
177
|
+
"""Acknowledge successful processing of a job from the queue.
|
|
178
|
+
|
|
179
|
+
:param message_id: The identifier of the message to acknowledge.
|
|
180
|
+
"""
|
|
170
181
|
raise NotImplementedError
|
|
171
182
|
|
|
172
183
|
@abstractmethod
|
|
@@ -196,12 +207,12 @@ class StorageBackend(ABC):
|
|
|
196
207
|
raise NotImplementedError
|
|
197
208
|
|
|
198
209
|
@abstractmethod
|
|
199
|
-
async def save_client_config(self, token: str, config:
|
|
210
|
+
async def save_client_config(self, token: str, config: dict[str, Any]) -> None:
|
|
200
211
|
"""Saves the static configuration of a client."""
|
|
201
212
|
raise NotImplementedError
|
|
202
213
|
|
|
203
214
|
@abstractmethod
|
|
204
|
-
async def get_client_config(self, token: str) ->
|
|
215
|
+
async def get_client_config(self, token: str) -> dict[str, Any] | None:
|
|
205
216
|
"""Gets the static configuration of a client."""
|
|
206
217
|
raise NotImplementedError
|
|
207
218
|
|
|
@@ -225,7 +236,7 @@ class StorageBackend(ABC):
|
|
|
225
236
|
raise NotImplementedError
|
|
226
237
|
|
|
227
238
|
@abstractmethod
|
|
228
|
-
async def get_priority_queue_stats(self, task_type: str) ->
|
|
239
|
+
async def get_priority_queue_stats(self, task_type: str) -> dict[str, Any]:
|
|
229
240
|
"""Get statistics on the priority queue for a given task type.
|
|
230
241
|
|
|
231
242
|
:param task_type: The type of task (used as part of the queue key).
|
|
@@ -244,12 +255,12 @@ class StorageBackend(ABC):
|
|
|
244
255
|
raise NotImplementedError
|
|
245
256
|
|
|
246
257
|
@abstractmethod
|
|
247
|
-
async def get_worker_token(self, worker_id: str) ->
|
|
258
|
+
async def get_worker_token(self, worker_id: str) -> str | None:
|
|
248
259
|
"""Retrieves an individual token for a specific worker."""
|
|
249
260
|
raise NotImplementedError
|
|
250
261
|
|
|
251
262
|
@abstractmethod
|
|
252
|
-
async def get_worker_info(self, worker_id: str) ->
|
|
263
|
+
async def get_worker_info(self, worker_id: str) -> dict[str, Any] | None:
|
|
253
264
|
"""Get complete information about a worker by its ID."""
|
|
254
265
|
raise NotImplementedError
|
|
255
266
|
|
avtomatika/storage/memory.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from asyncio import Lock, PriorityQueue, Queue, QueueEmpty, wait_for
|
|
2
2
|
from asyncio import TimeoutError as AsyncTimeoutError
|
|
3
3
|
from time import monotonic
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .base import StorageBackend
|
|
7
7
|
|
|
@@ -13,35 +13,35 @@ class MemoryStorage(StorageBackend):
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
def __init__(self):
|
|
16
|
-
self._jobs:
|
|
17
|
-
self._workers:
|
|
18
|
-
self._worker_ttls:
|
|
19
|
-
self._worker_task_queues:
|
|
16
|
+
self._jobs: dict[str, dict[str, Any]] = {}
|
|
17
|
+
self._workers: dict[str, dict[str, Any]] = {}
|
|
18
|
+
self._worker_ttls: dict[str, float] = {}
|
|
19
|
+
self._worker_task_queues: dict[str, PriorityQueue] = {}
|
|
20
20
|
self._job_queue = Queue()
|
|
21
|
-
self._quarantine_queue:
|
|
22
|
-
self._watched_jobs:
|
|
23
|
-
self._client_configs:
|
|
24
|
-
self._quotas:
|
|
25
|
-
self._worker_tokens:
|
|
26
|
-
self._generic_keys:
|
|
27
|
-
self._generic_key_ttls:
|
|
28
|
-
self._locks:
|
|
21
|
+
self._quarantine_queue: list[str] = []
|
|
22
|
+
self._watched_jobs: dict[str, float] = {}
|
|
23
|
+
self._client_configs: dict[str, dict[str, Any]] = {}
|
|
24
|
+
self._quotas: dict[str, int] = {}
|
|
25
|
+
self._worker_tokens: dict[str, str] = {}
|
|
26
|
+
self._generic_keys: dict[str, Any] = {}
|
|
27
|
+
self._generic_key_ttls: dict[str, float] = {}
|
|
28
|
+
self._locks: dict[str, tuple[str, float]] = {}
|
|
29
29
|
|
|
30
30
|
self._lock = Lock()
|
|
31
31
|
|
|
32
|
-
async def get_job_state(self, job_id: str) ->
|
|
32
|
+
async def get_job_state(self, job_id: str) -> dict[str, Any] | None:
|
|
33
33
|
async with self._lock:
|
|
34
34
|
return self._jobs.get(job_id)
|
|
35
35
|
|
|
36
|
-
async def save_job_state(self, job_id: str, state:
|
|
36
|
+
async def save_job_state(self, job_id: str, state: dict[str, Any]) -> None:
|
|
37
37
|
async with self._lock:
|
|
38
38
|
self._jobs[job_id] = state
|
|
39
39
|
|
|
40
40
|
async def update_job_state(
|
|
41
41
|
self,
|
|
42
42
|
job_id: str,
|
|
43
|
-
update_data:
|
|
44
|
-
) ->
|
|
43
|
+
update_data: dict[str, Any],
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
45
|
async with self._lock:
|
|
46
46
|
if job_id not in self._jobs:
|
|
47
47
|
self._jobs[job_id] = {}
|
|
@@ -51,7 +51,7 @@ class MemoryStorage(StorageBackend):
|
|
|
51
51
|
async def register_worker(
|
|
52
52
|
self,
|
|
53
53
|
worker_id: str,
|
|
54
|
-
worker_info:
|
|
54
|
+
worker_info: dict[str, Any],
|
|
55
55
|
ttl: int,
|
|
56
56
|
) -> None:
|
|
57
57
|
"""Registers a worker and creates a task queue for it."""
|
|
@@ -66,21 +66,20 @@ class MemoryStorage(StorageBackend):
|
|
|
66
66
|
async def enqueue_task_for_worker(
|
|
67
67
|
self,
|
|
68
68
|
worker_id: str,
|
|
69
|
-
task_payload:
|
|
69
|
+
task_payload: dict[str, Any],
|
|
70
70
|
priority: float,
|
|
71
71
|
) -> None:
|
|
72
72
|
"""Puts a task on the priority queue for a worker."""
|
|
73
73
|
async with self._lock:
|
|
74
74
|
if worker_id not in self._worker_task_queues:
|
|
75
75
|
self._worker_task_queues[worker_id] = PriorityQueue()
|
|
76
|
-
# asyncio.PriorityQueue is a min-heap, so we invert the priority
|
|
77
76
|
await self._worker_task_queues[worker_id].put((-priority, task_payload))
|
|
78
77
|
|
|
79
78
|
async def dequeue_task_for_worker(
|
|
80
79
|
self,
|
|
81
80
|
worker_id: str,
|
|
82
81
|
timeout: int,
|
|
83
|
-
) ->
|
|
82
|
+
) -> dict[str, Any] | None:
|
|
84
83
|
"""Retrieves a task from the worker's priority queue with a timeout."""
|
|
85
84
|
queue = None
|
|
86
85
|
async with self._lock:
|
|
@@ -104,9 +103,9 @@ class MemoryStorage(StorageBackend):
|
|
|
104
103
|
async def update_worker_status(
|
|
105
104
|
self,
|
|
106
105
|
worker_id: str,
|
|
107
|
-
status_update:
|
|
106
|
+
status_update: dict[str, Any],
|
|
108
107
|
ttl: int,
|
|
109
|
-
) ->
|
|
108
|
+
) -> dict[str, Any] | None:
|
|
110
109
|
async with self._lock:
|
|
111
110
|
if worker_id in self._workers:
|
|
112
111
|
self._workers[worker_id].update(status_update)
|
|
@@ -117,8 +116,8 @@ class MemoryStorage(StorageBackend):
|
|
|
117
116
|
async def update_worker_data(
|
|
118
117
|
self,
|
|
119
118
|
worker_id: str,
|
|
120
|
-
update_data:
|
|
121
|
-
) ->
|
|
119
|
+
update_data: dict[str, Any],
|
|
120
|
+
) -> dict[str, Any] | None:
|
|
122
121
|
async with self._lock:
|
|
123
122
|
if worker_id in self._workers:
|
|
124
123
|
self._workers[worker_id].update(update_data)
|
|
@@ -155,13 +154,17 @@ class MemoryStorage(StorageBackend):
|
|
|
155
154
|
async def enqueue_job(self, job_id: str) -> None:
|
|
156
155
|
await self._job_queue.put(job_id)
|
|
157
156
|
|
|
158
|
-
async def dequeue_job(self) -> str | None:
|
|
157
|
+
async def dequeue_job(self) -> tuple[str, str] | None:
|
|
159
158
|
"""Waits indefinitely for a job ID from the queue and returns it.
|
|
160
|
-
|
|
159
|
+
Returns a tuple of (job_id, message_id). In MemoryStorage, message_id is dummy.
|
|
161
160
|
"""
|
|
162
161
|
job_id = await self._job_queue.get()
|
|
163
162
|
self._job_queue.task_done()
|
|
164
|
-
return job_id
|
|
163
|
+
return job_id, "memory-msg-id"
|
|
164
|
+
|
|
165
|
+
async def ack_job(self, message_id: str) -> None:
|
|
166
|
+
"""No-op for MemoryStorage as it doesn't support persistent streams."""
|
|
167
|
+
pass
|
|
165
168
|
|
|
166
169
|
async def quarantine_job(self, job_id: str) -> None:
|
|
167
170
|
async with self._lock:
|
|
@@ -187,11 +190,11 @@ class MemoryStorage(StorageBackend):
|
|
|
187
190
|
self._generic_key_ttls[key] = now + ttl
|
|
188
191
|
return self._generic_keys[key]
|
|
189
192
|
|
|
190
|
-
async def save_client_config(self, token: str, config:
|
|
193
|
+
async def save_client_config(self, token: str, config: dict[str, Any]) -> None:
|
|
191
194
|
async with self._lock:
|
|
192
195
|
self._client_configs[token] = config
|
|
193
196
|
|
|
194
|
-
async def get_client_config(self, token: str) ->
|
|
197
|
+
async def get_client_config(self, token: str) -> dict[str, Any] | None:
|
|
195
198
|
async with self._lock:
|
|
196
199
|
return self._client_configs.get(token)
|
|
197
200
|
|
|
@@ -217,7 +220,6 @@ class MemoryStorage(StorageBackend):
|
|
|
217
220
|
self._workers.clear()
|
|
218
221
|
self._worker_ttls.clear()
|
|
219
222
|
self._worker_task_queues.clear()
|
|
220
|
-
# Empty the queue
|
|
221
223
|
while not self._job_queue.empty():
|
|
222
224
|
try:
|
|
223
225
|
self._job_queue.get_nowait()
|
|
@@ -232,17 +234,15 @@ class MemoryStorage(StorageBackend):
|
|
|
232
234
|
self._locks.clear()
|
|
233
235
|
|
|
234
236
|
async def get_job_queue_length(self) -> int:
|
|
235
|
-
# No lock needed for asyncio.Queue.qsize()
|
|
236
237
|
return self._job_queue.qsize()
|
|
237
238
|
|
|
238
239
|
async def get_active_worker_count(self) -> int:
|
|
239
240
|
async with self._lock:
|
|
240
241
|
now = monotonic()
|
|
241
|
-
# Create a copy of keys to avoid issues with concurrent modifications
|
|
242
242
|
worker_ids = list(self._workers.keys())
|
|
243
243
|
return sum(self._worker_ttls.get(worker_id, 0) > now for worker_id in worker_ids)
|
|
244
244
|
|
|
245
|
-
async def get_worker_info(self, worker_id: str) ->
|
|
245
|
+
async def get_worker_info(self, worker_id: str) -> dict[str, Any] | None:
|
|
246
246
|
async with self._lock:
|
|
247
247
|
return self._workers.get(worker_id)
|
|
248
248
|
|
|
@@ -250,7 +250,7 @@ class MemoryStorage(StorageBackend):
|
|
|
250
250
|
async with self._lock:
|
|
251
251
|
self._worker_tokens[worker_id] = token
|
|
252
252
|
|
|
253
|
-
async def get_worker_token(self, worker_id: str) ->
|
|
253
|
+
async def get_worker_token(self, worker_id: str) -> str | None:
|
|
254
254
|
async with self._lock:
|
|
255
255
|
return self._worker_tokens.get(worker_id)
|
|
256
256
|
|
|
@@ -258,7 +258,7 @@ class MemoryStorage(StorageBackend):
|
|
|
258
258
|
key = f"task_cancel:{task_id}"
|
|
259
259
|
await self.increment_key_with_ttl(key, 3600)
|
|
260
260
|
|
|
261
|
-
async def get_priority_queue_stats(self, task_type: str) ->
|
|
261
|
+
async def get_priority_queue_stats(self, task_type: str) -> dict[str, Any]:
|
|
262
262
|
"""
|
|
263
263
|
Returns empty data, as `asyncio.PriorityQueue` does not
|
|
264
264
|
support introspection to get statistics.
|
|
@@ -278,14 +278,8 @@ class MemoryStorage(StorageBackend):
|
|
|
278
278
|
async with self._lock:
|
|
279
279
|
now = monotonic()
|
|
280
280
|
current_lock = self._locks.get(key)
|
|
281
|
-
|
|
282
|
-
# If lock exists and hasn't expired
|
|
283
281
|
if current_lock and current_lock[1] > now:
|
|
284
|
-
# If explicitly owned by us, we can extend/re-enter (optional behavior)
|
|
285
|
-
# But for strict locking, if it's held, return False (unless it's us? let's simpler: just False if held)
|
|
286
282
|
return False
|
|
287
|
-
|
|
288
|
-
# Acquire lock
|
|
289
283
|
self._locks[key] = (holder_id, now + ttl)
|
|
290
284
|
return True
|
|
291
285
|
|
|
@@ -294,7 +288,6 @@ class MemoryStorage(StorageBackend):
|
|
|
294
288
|
current_lock = self._locks.get(key)
|
|
295
289
|
if current_lock:
|
|
296
290
|
owner, expiry = current_lock
|
|
297
|
-
# Only release if we are the owner
|
|
298
291
|
if owner == holder_id:
|
|
299
292
|
del self._locks[key]
|
|
300
293
|
return True
|