avtomatika 1.0b4__py3-none-any.whl → 1.0b6__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/blueprint.py +9 -11
- avtomatika/config.py +11 -0
- avtomatika/constants.py +30 -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 +170 -92
- avtomatika/executor.py +168 -148
- avtomatika/history/base.py +7 -7
- avtomatika/history/noop.py +7 -7
- avtomatika/history/postgres.py +63 -22
- avtomatika/history/sqlite.py +61 -44
- avtomatika/logging_config.py +59 -8
- avtomatika/scheduler.py +119 -0
- avtomatika/scheduler_config_loader.py +41 -0
- avtomatika/security.py +3 -5
- avtomatika/storage/__init__.py +2 -2
- avtomatika/storage/base.py +48 -23
- avtomatika/storage/memory.py +76 -46
- avtomatika/storage/redis.py +141 -60
- avtomatika/worker_config_loader.py +2 -2
- avtomatika/ws_manager.py +1 -2
- {avtomatika-1.0b4.dist-info → avtomatika-1.0b6.dist-info}/METADATA +45 -5
- avtomatika-1.0b6.dist-info/RECORD +40 -0
- avtomatika-1.0b4.dist-info/RECORD +0 -37
- {avtomatika-1.0b4.dist-info → avtomatika-1.0b6.dist-info}/WHEEL +0 -0
- {avtomatika-1.0b4.dist-info → avtomatika-1.0b6.dist-info}/licenses/LICENSE +0 -0
- {avtomatika-1.0b4.dist-info → avtomatika-1.0b6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from tomllib import load
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ScheduledJobConfig:
|
|
8
|
+
name: str
|
|
9
|
+
blueprint: str
|
|
10
|
+
input_data: dict[str, Any]
|
|
11
|
+
interval_seconds: int | None = None
|
|
12
|
+
daily_at: str | None = None
|
|
13
|
+
weekly_days: list[str] | None = None
|
|
14
|
+
monthly_dates: list[int] | None = None
|
|
15
|
+
time: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_schedules_from_file(file_path: str) -> list[ScheduledJobConfig]:
|
|
19
|
+
"""Loads scheduled job configurations from a TOML file."""
|
|
20
|
+
with open(file_path, "rb") as f:
|
|
21
|
+
data = load(f)
|
|
22
|
+
|
|
23
|
+
schedules = []
|
|
24
|
+
for name, config in data.items():
|
|
25
|
+
# Skip sections that might be metadata (though TOML structure usually implies all top-level keys are jobs)
|
|
26
|
+
if not isinstance(config, dict):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
schedules.append(
|
|
30
|
+
ScheduledJobConfig(
|
|
31
|
+
name=name,
|
|
32
|
+
blueprint=config.get("blueprint"),
|
|
33
|
+
input_data=config.get("input_data", {}),
|
|
34
|
+
interval_seconds=config.get("interval_seconds"),
|
|
35
|
+
daily_at=config.get("daily_at"),
|
|
36
|
+
weekly_days=config.get("weekly_days"),
|
|
37
|
+
monthly_dates=config.get("monthly_dates"),
|
|
38
|
+
time=config.get("time"),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return schedules
|
avtomatika/security.py
CHANGED
|
@@ -4,11 +4,9 @@ from typing import Any, Awaitable, Callable
|
|
|
4
4
|
from aiohttp import web
|
|
5
5
|
|
|
6
6
|
from .config import Config
|
|
7
|
+
from .constants import AUTH_HEADER_CLIENT, AUTH_HEADER_WORKER
|
|
7
8
|
from .storage.base import StorageBackend
|
|
8
9
|
|
|
9
|
-
AUTH_HEADER_AVTOMATIKA = "X-Avtomatika-Token"
|
|
10
|
-
AUTH_HEADER_WORKER = "X-Worker-Token"
|
|
11
|
-
|
|
12
10
|
Handler = Callable[[web.Request], Awaitable[web.Response]]
|
|
13
11
|
|
|
14
12
|
|
|
@@ -21,10 +19,10 @@ def client_auth_middleware_factory(
|
|
|
21
19
|
|
|
22
20
|
@web.middleware
|
|
23
21
|
async def middleware(request: web.Request, handler: Handler) -> web.Response:
|
|
24
|
-
token = request.headers.get(
|
|
22
|
+
token = request.headers.get(AUTH_HEADER_CLIENT)
|
|
25
23
|
if not token:
|
|
26
24
|
return web.json_response(
|
|
27
|
-
{"error": "Missing
|
|
25
|
+
{"error": f"Missing {AUTH_HEADER_CLIENT} header"},
|
|
28
26
|
status=401,
|
|
29
27
|
)
|
|
30
28
|
|
avtomatika/storage/__init__.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
from contextlib import suppress
|
|
2
2
|
|
|
3
3
|
from .base import StorageBackend
|
|
4
4
|
from .memory import MemoryStorage
|
|
5
5
|
|
|
6
6
|
__all__ = ["StorageBackend", "MemoryStorage"]
|
|
7
7
|
|
|
8
|
-
with
|
|
8
|
+
with suppress(ImportError):
|
|
9
9
|
from .redis import RedisStorage # noqa: F401
|
|
10
10
|
|
|
11
11
|
__all__.append("RedisStorage")
|
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
|
|
|
@@ -258,13 +269,27 @@ class StorageBackend(ABC):
|
|
|
258
269
|
"""Completely clears the storage. Used mainly for tests."""
|
|
259
270
|
raise NotImplementedError
|
|
260
271
|
|
|
261
|
-
@abstractmethod
|
|
262
272
|
async def get_active_worker_count(self) -> int:
|
|
263
|
-
"""
|
|
264
|
-
|
|
273
|
+
"""Returns the number of currently active workers."""
|
|
274
|
+
raise NotImplementedError
|
|
275
|
+
|
|
276
|
+
async def set_nx_ttl(self, key: str, value: str, ttl: int) -> bool:
|
|
277
|
+
"""
|
|
278
|
+
Atomically sets key to value if it does not exist.
|
|
279
|
+
Sets a TTL (in seconds) on the key.
|
|
280
|
+
Returns True if set, False if already exists.
|
|
281
|
+
Critical for distributed locking.
|
|
265
282
|
"""
|
|
266
283
|
raise NotImplementedError
|
|
267
284
|
|
|
285
|
+
async def get_str(self, key: str) -> str | None:
|
|
286
|
+
"""Gets a simple string value from storage."""
|
|
287
|
+
raise NotImplementedError
|
|
288
|
+
|
|
289
|
+
async def set_str(self, key: str, value: str, ttl: int | None = None) -> None:
|
|
290
|
+
"""Sets a simple string value in storage with optional TTL."""
|
|
291
|
+
raise NotImplementedError
|
|
292
|
+
|
|
268
293
|
@abstractmethod
|
|
269
294
|
async def acquire_lock(self, key: str, holder_id: str, ttl: int) -> bool:
|
|
270
295
|
"""
|
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,49 @@ 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
|
|
36
|
+
async def _clean_expired(self):
|
|
37
|
+
"""Helper to remove expired keys."""
|
|
38
|
+
now = monotonic()
|
|
39
|
+
|
|
40
|
+
expired_generic = [k for k, t in self._generic_key_ttls.items() if t < now]
|
|
41
|
+
for k in expired_generic:
|
|
42
|
+
self._generic_key_ttls.pop(k, None)
|
|
43
|
+
self._generic_keys.pop(k, None)
|
|
44
|
+
|
|
45
|
+
expired_workers = [k for k, t in self._worker_ttls.items() if t < now]
|
|
46
|
+
for k in expired_workers:
|
|
47
|
+
self._worker_ttls.pop(k, None)
|
|
48
|
+
self._workers.pop(k, None)
|
|
49
|
+
|
|
50
|
+
async def save_job_state(self, job_id: str, state: dict[str, Any]):
|
|
37
51
|
async with self._lock:
|
|
38
52
|
self._jobs[job_id] = state
|
|
39
53
|
|
|
40
54
|
async def update_job_state(
|
|
41
55
|
self,
|
|
42
56
|
job_id: str,
|
|
43
|
-
update_data:
|
|
44
|
-
) ->
|
|
57
|
+
update_data: dict[str, Any],
|
|
58
|
+
) -> dict[str, Any]:
|
|
45
59
|
async with self._lock:
|
|
46
60
|
if job_id not in self._jobs:
|
|
47
61
|
self._jobs[job_id] = {}
|
|
@@ -51,7 +65,7 @@ class MemoryStorage(StorageBackend):
|
|
|
51
65
|
async def register_worker(
|
|
52
66
|
self,
|
|
53
67
|
worker_id: str,
|
|
54
|
-
worker_info:
|
|
68
|
+
worker_info: dict[str, Any],
|
|
55
69
|
ttl: int,
|
|
56
70
|
) -> None:
|
|
57
71
|
"""Registers a worker and creates a task queue for it."""
|
|
@@ -66,21 +80,20 @@ class MemoryStorage(StorageBackend):
|
|
|
66
80
|
async def enqueue_task_for_worker(
|
|
67
81
|
self,
|
|
68
82
|
worker_id: str,
|
|
69
|
-
task_payload:
|
|
83
|
+
task_payload: dict[str, Any],
|
|
70
84
|
priority: float,
|
|
71
85
|
) -> None:
|
|
72
86
|
"""Puts a task on the priority queue for a worker."""
|
|
73
87
|
async with self._lock:
|
|
74
88
|
if worker_id not in self._worker_task_queues:
|
|
75
89
|
self._worker_task_queues[worker_id] = PriorityQueue()
|
|
76
|
-
# asyncio.PriorityQueue is a min-heap, so we invert the priority
|
|
77
90
|
await self._worker_task_queues[worker_id].put((-priority, task_payload))
|
|
78
91
|
|
|
79
92
|
async def dequeue_task_for_worker(
|
|
80
93
|
self,
|
|
81
94
|
worker_id: str,
|
|
82
95
|
timeout: int,
|
|
83
|
-
) ->
|
|
96
|
+
) -> dict[str, Any] | None:
|
|
84
97
|
"""Retrieves a task from the worker's priority queue with a timeout."""
|
|
85
98
|
queue = None
|
|
86
99
|
async with self._lock:
|
|
@@ -104,9 +117,9 @@ class MemoryStorage(StorageBackend):
|
|
|
104
117
|
async def update_worker_status(
|
|
105
118
|
self,
|
|
106
119
|
worker_id: str,
|
|
107
|
-
status_update:
|
|
120
|
+
status_update: dict[str, Any],
|
|
108
121
|
ttl: int,
|
|
109
|
-
) ->
|
|
122
|
+
) -> dict[str, Any] | None:
|
|
110
123
|
async with self._lock:
|
|
111
124
|
if worker_id in self._workers:
|
|
112
125
|
self._workers[worker_id].update(status_update)
|
|
@@ -117,8 +130,8 @@ class MemoryStorage(StorageBackend):
|
|
|
117
130
|
async def update_worker_data(
|
|
118
131
|
self,
|
|
119
132
|
worker_id: str,
|
|
120
|
-
update_data:
|
|
121
|
-
) ->
|
|
133
|
+
update_data: dict[str, Any],
|
|
134
|
+
) -> dict[str, Any] | None:
|
|
122
135
|
async with self._lock:
|
|
123
136
|
if worker_id in self._workers:
|
|
124
137
|
self._workers[worker_id].update(update_data)
|
|
@@ -155,13 +168,17 @@ class MemoryStorage(StorageBackend):
|
|
|
155
168
|
async def enqueue_job(self, job_id: str) -> None:
|
|
156
169
|
await self._job_queue.put(job_id)
|
|
157
170
|
|
|
158
|
-
async def dequeue_job(self) -> str | None:
|
|
171
|
+
async def dequeue_job(self) -> tuple[str, str] | None:
|
|
159
172
|
"""Waits indefinitely for a job ID from the queue and returns it.
|
|
160
|
-
|
|
173
|
+
Returns a tuple of (job_id, message_id). In MemoryStorage, message_id is dummy.
|
|
161
174
|
"""
|
|
162
175
|
job_id = await self._job_queue.get()
|
|
163
176
|
self._job_queue.task_done()
|
|
164
|
-
return job_id
|
|
177
|
+
return job_id, "memory-msg-id"
|
|
178
|
+
|
|
179
|
+
async def ack_job(self, message_id: str) -> None:
|
|
180
|
+
"""No-op for MemoryStorage as it doesn't support persistent streams."""
|
|
181
|
+
pass
|
|
165
182
|
|
|
166
183
|
async def quarantine_job(self, job_id: str) -> None:
|
|
167
184
|
async with self._lock:
|
|
@@ -187,11 +204,11 @@ class MemoryStorage(StorageBackend):
|
|
|
187
204
|
self._generic_key_ttls[key] = now + ttl
|
|
188
205
|
return self._generic_keys[key]
|
|
189
206
|
|
|
190
|
-
async def save_client_config(self, token: str, config:
|
|
207
|
+
async def save_client_config(self, token: str, config: dict[str, Any]) -> None:
|
|
191
208
|
async with self._lock:
|
|
192
209
|
self._client_configs[token] = config
|
|
193
210
|
|
|
194
|
-
async def get_client_config(self, token: str) ->
|
|
211
|
+
async def get_client_config(self, token: str) -> dict[str, Any] | None:
|
|
195
212
|
async with self._lock:
|
|
196
213
|
return self._client_configs.get(token)
|
|
197
214
|
|
|
@@ -217,7 +234,6 @@ class MemoryStorage(StorageBackend):
|
|
|
217
234
|
self._workers.clear()
|
|
218
235
|
self._worker_ttls.clear()
|
|
219
236
|
self._worker_task_queues.clear()
|
|
220
|
-
# Empty the queue
|
|
221
237
|
while not self._job_queue.empty():
|
|
222
238
|
try:
|
|
223
239
|
self._job_queue.get_nowait()
|
|
@@ -232,17 +248,38 @@ class MemoryStorage(StorageBackend):
|
|
|
232
248
|
self._locks.clear()
|
|
233
249
|
|
|
234
250
|
async def get_job_queue_length(self) -> int:
|
|
235
|
-
# No lock needed for asyncio.Queue.qsize()
|
|
236
251
|
return self._job_queue.qsize()
|
|
237
252
|
|
|
238
253
|
async def get_active_worker_count(self) -> int:
|
|
239
254
|
async with self._lock:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
255
|
+
await self._clean_expired()
|
|
256
|
+
return len(self._workers)
|
|
257
|
+
|
|
258
|
+
async def set_nx_ttl(self, key: str, value: str, ttl: int) -> bool:
|
|
259
|
+
async with self._lock:
|
|
260
|
+
await self._clean_expired()
|
|
261
|
+
if key in self._generic_keys:
|
|
262
|
+
return False
|
|
244
263
|
|
|
245
|
-
|
|
264
|
+
self._generic_keys[key] = value
|
|
265
|
+
self._generic_key_ttls[key] = monotonic() + ttl
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
async def get_str(self, key: str) -> str | None:
|
|
269
|
+
async with self._lock:
|
|
270
|
+
await self._clean_expired()
|
|
271
|
+
val = self._generic_keys.get(key)
|
|
272
|
+
return str(val) if val is not None else None
|
|
273
|
+
|
|
274
|
+
async def set_str(self, key: str, value: str, ttl: int | None = None) -> None:
|
|
275
|
+
async with self._lock:
|
|
276
|
+
self._generic_keys[key] = value
|
|
277
|
+
if ttl:
|
|
278
|
+
self._generic_key_ttls[key] = monotonic() + ttl
|
|
279
|
+
else:
|
|
280
|
+
self._generic_key_ttls.pop(key, None)
|
|
281
|
+
|
|
282
|
+
async def get_worker_info(self, worker_id: str) -> dict[str, Any] | None:
|
|
246
283
|
async with self._lock:
|
|
247
284
|
return self._workers.get(worker_id)
|
|
248
285
|
|
|
@@ -250,7 +287,7 @@ class MemoryStorage(StorageBackend):
|
|
|
250
287
|
async with self._lock:
|
|
251
288
|
self._worker_tokens[worker_id] = token
|
|
252
289
|
|
|
253
|
-
async def get_worker_token(self, worker_id: str) ->
|
|
290
|
+
async def get_worker_token(self, worker_id: str) -> str | None:
|
|
254
291
|
async with self._lock:
|
|
255
292
|
return self._worker_tokens.get(worker_id)
|
|
256
293
|
|
|
@@ -258,7 +295,7 @@ class MemoryStorage(StorageBackend):
|
|
|
258
295
|
key = f"task_cancel:{task_id}"
|
|
259
296
|
await self.increment_key_with_ttl(key, 3600)
|
|
260
297
|
|
|
261
|
-
async def get_priority_queue_stats(self, task_type: str) ->
|
|
298
|
+
async def get_priority_queue_stats(self, task_type: str) -> dict[str, Any]:
|
|
262
299
|
"""
|
|
263
300
|
Returns empty data, as `asyncio.PriorityQueue` does not
|
|
264
301
|
support introspection to get statistics.
|
|
@@ -278,14 +315,8 @@ class MemoryStorage(StorageBackend):
|
|
|
278
315
|
async with self._lock:
|
|
279
316
|
now = monotonic()
|
|
280
317
|
current_lock = self._locks.get(key)
|
|
281
|
-
|
|
282
|
-
# If lock exists and hasn't expired
|
|
283
318
|
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
319
|
return False
|
|
287
|
-
|
|
288
|
-
# Acquire lock
|
|
289
320
|
self._locks[key] = (holder_id, now + ttl)
|
|
290
321
|
return True
|
|
291
322
|
|
|
@@ -294,7 +325,6 @@ class MemoryStorage(StorageBackend):
|
|
|
294
325
|
current_lock = self._locks.get(key)
|
|
295
326
|
if current_lock:
|
|
296
327
|
owner, expiry = current_lock
|
|
297
|
-
# Only release if we are the owner
|
|
298
328
|
if owner == holder_id:
|
|
299
329
|
del self._locks[key]
|
|
300
330
|
return True
|