rrq 0.9.8__py3-none-macosx_11_0_arm64.whl → 0.9.9__py3-none-macosx_11_0_arm64.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.
- rrq/__init__.py +10 -10
- rrq/bin/README.md +4 -3
- rrq/bin/librrq_producer.dylib +0 -0
- rrq/bin/rrq +0 -0
- rrq/client.py +140 -181
- rrq/exc.py +0 -6
- rrq/integrations/otel.py +36 -302
- rrq/job.py +10 -1
- rrq/producer_ffi.py +329 -0
- rrq/protocol.py +11 -11
- rrq/registry.py +6 -6
- rrq/{executor.py → runner.py} +27 -41
- rrq/runner_runtime.py +462 -0
- rrq/runner_settings.py +17 -0
- rrq/telemetry.py +15 -121
- {rrq-0.9.8.dist-info → rrq-0.9.9.dist-info}/METADATA +85 -56
- rrq-0.9.9.dist-info/RECORD +22 -0
- rrq-0.9.9.dist-info/entry_points.txt +3 -0
- rrq/config.py +0 -177
- rrq/constants.py +0 -54
- rrq/executor_runtime.py +0 -389
- rrq/executor_settings.py +0 -23
- rrq/integrations/ddtrace.py +0 -634
- rrq/integrations/logfire.py +0 -23
- rrq/settings.py +0 -169
- rrq/store.py +0 -1009
- rrq-0.9.8.dist-info/RECORD +0 -26
- rrq-0.9.8.dist-info/entry_points.txt +0 -3
- {rrq-0.9.8.dist-info → rrq-0.9.9.dist-info}/WHEEL +0 -0
- {rrq-0.9.8.dist-info → rrq-0.9.9.dist-info}/licenses/LICENSE +0 -0
rrq/__init__.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
from .client import RRQClient
|
|
2
|
-
from .
|
|
2
|
+
from .job import JobResult, JobStatus
|
|
3
|
+
from .runner import (
|
|
3
4
|
ExecutionContext,
|
|
4
5
|
ExecutionError,
|
|
5
6
|
ExecutionOutcome,
|
|
6
7
|
ExecutionRequest,
|
|
7
|
-
|
|
8
|
+
PythonRunner,
|
|
8
9
|
)
|
|
9
|
-
from .
|
|
10
|
-
from .registry import
|
|
11
|
-
from .settings import ExecutorConfig, RRQSettings
|
|
10
|
+
from .runner_settings import PythonRunnerSettings
|
|
11
|
+
from .registry import Registry
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"RRQClient",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
15
|
+
"Registry",
|
|
16
|
+
"JobResult",
|
|
17
|
+
"JobStatus",
|
|
18
|
+
"PythonRunnerSettings",
|
|
19
19
|
"ExecutionContext",
|
|
20
20
|
"ExecutionError",
|
|
21
21
|
"ExecutionRequest",
|
|
22
22
|
"ExecutionOutcome",
|
|
23
|
-
"
|
|
23
|
+
"PythonRunner",
|
|
24
24
|
]
|
rrq/bin/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
This directory is reserved for the Rust `rrq` binary.
|
|
1
|
+
This directory is reserved for the Rust `rrq` binary and shared libraries.
|
|
2
2
|
|
|
3
|
-
Packaging note: build pipelines should place the compiled `rrq` executable
|
|
4
|
-
before producing a wheel so the Python
|
|
3
|
+
Packaging note: build pipelines should place the compiled `rrq` executable and
|
|
4
|
+
the `rrq_producer` shared library here before producing a wheel so the Python
|
|
5
|
+
wrapper can locate them.
|
|
Binary file
|
rrq/bin/rrq
CHANGED
|
Binary file
|
rrq/client.py
CHANGED
|
@@ -1,196 +1,155 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""RRQ client for enqueuing jobs via the Rust producer FFI."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import uuid
|
|
5
|
-
from datetime import timezone, datetime, timedelta
|
|
6
|
-
from typing import Any, Optional
|
|
3
|
+
from __future__ import annotations
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from .telemetry import get_telemetry
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
from .job import JobResult, JobStatus
|
|
10
|
+
from .producer_ffi import (
|
|
11
|
+
JobResultModel,
|
|
12
|
+
JobStatusResponseModel,
|
|
13
|
+
RustProducer,
|
|
14
|
+
RustProducerError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _normalize_datetime(value: datetime) -> str:
|
|
19
|
+
if value.tzinfo is None:
|
|
20
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
21
|
+
else:
|
|
22
|
+
value = value.astimezone(timezone.utc)
|
|
23
|
+
return value.isoformat()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _to_job_result(payload: JobResultModel) -> JobResult:
|
|
27
|
+
try:
|
|
28
|
+
status = JobStatus(payload.status)
|
|
29
|
+
except ValueError:
|
|
30
|
+
status = JobStatus.UNKNOWN
|
|
31
|
+
return JobResult(
|
|
32
|
+
status=status,
|
|
33
|
+
result=payload.result,
|
|
34
|
+
last_error=payload.last_error,
|
|
35
|
+
)
|
|
14
36
|
|
|
15
37
|
|
|
16
38
|
class RRQClient:
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
a JobStore instance across multiple components.
|
|
30
|
-
"""
|
|
31
|
-
self.settings = settings
|
|
32
|
-
# If job_store is not provided, create one. This allows for flexibility:
|
|
33
|
-
# - External management of JobStore (e.g., passed from an application context)
|
|
34
|
-
# - Client creates its own if used standalone.
|
|
35
|
-
if job_store:
|
|
36
|
-
self.job_store = job_store
|
|
37
|
-
self._created_store_internally = False
|
|
39
|
+
"""Thin async wrapper around the Rust producer FFI."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
config: dict[str, Any] | None = None,
|
|
45
|
+
config_path: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
if config is not None and config_path is not None:
|
|
48
|
+
raise ValueError("Provide either config or config_path, not both.")
|
|
49
|
+
if config is not None:
|
|
50
|
+
self._producer = RustProducer.from_config(config)
|
|
38
51
|
else:
|
|
39
|
-
self.
|
|
40
|
-
self._created_store_internally = True
|
|
52
|
+
self._producer = RustProducer.from_toml(config_path)
|
|
41
53
|
|
|
42
54
|
async def close(self) -> None:
|
|
43
|
-
|
|
44
|
-
if self._created_store_internally:
|
|
45
|
-
await self.job_store.aclose()
|
|
55
|
+
self._producer.close()
|
|
46
56
|
|
|
47
57
|
async def enqueue(
|
|
48
58
|
self,
|
|
49
59
|
function_name: str,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
function_name=function_name,
|
|
140
|
-
job_args=list(args),
|
|
141
|
-
job_kwargs=kwargs,
|
|
142
|
-
enqueue_time=enqueue_time_utc,
|
|
143
|
-
status=JobStatus.PENDING,
|
|
144
|
-
current_retries=0,
|
|
145
|
-
max_retries=(
|
|
146
|
-
_max_retries
|
|
147
|
-
if _max_retries is not None
|
|
148
|
-
else self.settings.default_max_retries
|
|
149
|
-
),
|
|
150
|
-
job_timeout_seconds=(
|
|
151
|
-
_job_timeout_seconds
|
|
152
|
-
if _job_timeout_seconds is not None
|
|
153
|
-
else self.settings.default_job_timeout_seconds
|
|
154
|
-
),
|
|
155
|
-
result_ttl_seconds=(
|
|
156
|
-
_result_ttl_seconds
|
|
157
|
-
if _result_ttl_seconds is not None
|
|
158
|
-
else self.settings.default_result_ttl_seconds
|
|
159
|
-
),
|
|
160
|
-
job_unique_key=_unique_key,
|
|
161
|
-
queue_name=queue_name_to_use, # Store the target queue name
|
|
162
|
-
trace_context=trace_context,
|
|
60
|
+
options: dict[str, Any] | None = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
options = dict(options) if options else {}
|
|
63
|
+
args = list(options.pop("args", []))
|
|
64
|
+
kwargs = dict(options.pop("kwargs", {}))
|
|
65
|
+
|
|
66
|
+
defer_until = options.get("defer_until")
|
|
67
|
+
if isinstance(defer_until, datetime):
|
|
68
|
+
options["defer_until"] = _normalize_datetime(defer_until)
|
|
69
|
+
|
|
70
|
+
request = {
|
|
71
|
+
"function_name": function_name,
|
|
72
|
+
"args": args,
|
|
73
|
+
"kwargs": kwargs,
|
|
74
|
+
"options": options,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
response = await asyncio.to_thread(self._producer.enqueue, request)
|
|
79
|
+
except RustProducerError as exc:
|
|
80
|
+
raise ValueError(str(exc)) from exc
|
|
81
|
+
|
|
82
|
+
job_id = response.get("job_id")
|
|
83
|
+
if not job_id:
|
|
84
|
+
raise ValueError("Producer did not return a job_id")
|
|
85
|
+
return job_id
|
|
86
|
+
|
|
87
|
+
async def enqueue_with_unique_key(
|
|
88
|
+
self,
|
|
89
|
+
function_name: str,
|
|
90
|
+
unique_key: str,
|
|
91
|
+
options: dict[str, Any] | None = None,
|
|
92
|
+
) -> str:
|
|
93
|
+
merged = dict(options or {})
|
|
94
|
+
merged["unique_key"] = unique_key
|
|
95
|
+
return await self.enqueue(function_name, merged)
|
|
96
|
+
|
|
97
|
+
async def enqueue_with_rate_limit(
|
|
98
|
+
self,
|
|
99
|
+
function_name: str,
|
|
100
|
+
options: dict[str, Any],
|
|
101
|
+
) -> str | None:
|
|
102
|
+
merged = dict(options)
|
|
103
|
+
request = {
|
|
104
|
+
"function_name": function_name,
|
|
105
|
+
"args": list(merged.pop("args", [])),
|
|
106
|
+
"kwargs": dict(merged.pop("kwargs", {})),
|
|
107
|
+
"options": merged,
|
|
108
|
+
"mode": "rate_limit",
|
|
109
|
+
}
|
|
110
|
+
try:
|
|
111
|
+
response = await asyncio.to_thread(self._producer.enqueue, request)
|
|
112
|
+
except RustProducerError as exc:
|
|
113
|
+
raise ValueError(str(exc)) from exc
|
|
114
|
+
return response.get("job_id")
|
|
115
|
+
|
|
116
|
+
async def enqueue_with_debounce(
|
|
117
|
+
self,
|
|
118
|
+
function_name: str,
|
|
119
|
+
options: dict[str, Any],
|
|
120
|
+
) -> str:
|
|
121
|
+
merged = dict(options)
|
|
122
|
+
request = {
|
|
123
|
+
"function_name": function_name,
|
|
124
|
+
"args": list(merged.pop("args", [])),
|
|
125
|
+
"kwargs": dict(merged.pop("kwargs", {})),
|
|
126
|
+
"options": merged,
|
|
127
|
+
"mode": "debounce",
|
|
128
|
+
}
|
|
129
|
+
try:
|
|
130
|
+
response = await asyncio.to_thread(self._producer.enqueue, request)
|
|
131
|
+
except RustProducerError as exc:
|
|
132
|
+
raise ValueError(str(exc)) from exc
|
|
133
|
+
job_id = response.get("job_id")
|
|
134
|
+
if not job_id:
|
|
135
|
+
raise ValueError("Producer did not return a job_id")
|
|
136
|
+
return job_id
|
|
137
|
+
|
|
138
|
+
async def enqueue_deferred(
|
|
139
|
+
self,
|
|
140
|
+
function_name: str,
|
|
141
|
+
options: dict[str, Any],
|
|
142
|
+
) -> str:
|
|
143
|
+
return await self.enqueue(function_name, options)
|
|
144
|
+
|
|
145
|
+
async def get_job_status(self, job_id: str) -> JobResult | None:
|
|
146
|
+
try:
|
|
147
|
+
response: JobStatusResponseModel = await asyncio.to_thread(
|
|
148
|
+
self._producer.get_job_status, job_id
|
|
163
149
|
)
|
|
150
|
+
except RustProducerError as exc:
|
|
151
|
+
raise ValueError(str(exc)) from exc
|
|
164
152
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# Ensure score_dt is timezone-aware (timezone.utc) if it's naive from user input
|
|
170
|
-
if score_dt.tzinfo is None:
|
|
171
|
-
score_dt = score_dt.replace(tzinfo=timezone.utc)
|
|
172
|
-
elif score_dt.tzinfo != timezone.utc:
|
|
173
|
-
# Convert to timezone.utc if it's aware but not timezone.utc
|
|
174
|
-
score_dt = score_dt.astimezone(timezone.utc)
|
|
175
|
-
|
|
176
|
-
score_timestamp_ms = int(score_dt.timestamp() * 1000)
|
|
177
|
-
# Record when the job is next scheduled to run (for deferred execution)
|
|
178
|
-
job.next_scheduled_run_time = score_dt
|
|
179
|
-
|
|
180
|
-
# Save the full job definition and add to queue (ensure unique lock is released on error)
|
|
181
|
-
try:
|
|
182
|
-
await self.job_store.save_job_definition(job)
|
|
183
|
-
await self.job_store.add_job_to_queue(
|
|
184
|
-
queue_name_to_use,
|
|
185
|
-
job.id,
|
|
186
|
-
float(score_timestamp_ms),
|
|
187
|
-
)
|
|
188
|
-
except Exception:
|
|
189
|
-
if unique_acquired and _unique_key is not None:
|
|
190
|
-
await self.job_store.release_unique_job_lock(_unique_key)
|
|
191
|
-
raise
|
|
192
|
-
|
|
193
|
-
logger.debug(
|
|
194
|
-
f"Enqueued job {job.id} ('{job.function_name}') to queue '{queue_name_to_use}' with score {score_timestamp_ms}"
|
|
195
|
-
)
|
|
196
|
-
return job
|
|
153
|
+
if not response.found or response.job is None:
|
|
154
|
+
return None
|
|
155
|
+
return _to_job_result(response.job)
|
rrq/exc.py
CHANGED
|
@@ -37,12 +37,6 @@ class HandlerNotFound(RRQError):
|
|
|
37
37
|
pass
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class JobNotFound(RRQError):
|
|
41
|
-
"""Exception raised when a job definition cannot be found in the JobStore."""
|
|
42
|
-
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
|
|
46
40
|
class MaxRetriesExceeded(Exception):
|
|
47
41
|
"""Raised when a job fails after reaching its maximum retry limit."""
|
|
48
42
|
|