queueio 0.4.0__tar.gz → 0.5.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.
- {queueio-0.4.0 → queueio-0.5.0}/CHANGELOG.md +20 -1
- {queueio-0.4.0 → queueio-0.5.0}/PKG-INFO +1 -1
- {queueio-0.4.0 → queueio-0.5.0}/pyproject.toml +2 -1
- {queueio-0.4.0 → queueio-0.5.0}/queueio/__init__.py +2 -1
- {queueio-0.4.0 → queueio-0.5.0}/queueio/consumer.py +3 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/continuation.py +2 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/invocation.py +7 -15
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queueio.py +7 -2
- queueio-0.5.0/queueio/queuevar.md +102 -0
- queueio-0.5.0/queueio/queuevar.py +71 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/priority.py +3 -3
- queueio-0.5.0/queueio/samples/queuevar.py +21 -0
- queueio-0.5.0/queueio/samples/queuevar_test.py +42 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/worker.py +13 -6
- {queueio-0.4.0 → queueio-0.5.0}/.claude/settings.local.json +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/.devcontainer/devcontainer.json +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/.devcontainer/docker-compose.yml +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/.github/workflows/build.yml +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/.github/workflows/publish.yml +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/.gitignore +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/LICENSE +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/README.md +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/dill.pyi +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/invocation.md +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/logo.svg +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/principles.md +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/pyrightconfig.json +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/__main__.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/broker.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/broker_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/conftest.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/event.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/freethreading_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/gather.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/id.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/journal.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/journal_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/message.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/monitor.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pause.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/__init__.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/broker.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/broker_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/journal.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/journal_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/receiver.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/pika/threadsafe.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queue.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queue_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queueio_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queuespec.md +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queuespec.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/queuespec_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/receiver.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/registry.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/result.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/routine.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/__init__.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/basic.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/basic_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/expanded.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/expanded_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/samples/priority_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/select.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stream.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/__init__.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/broker.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/broker_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/journal.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/journal_test.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/stub/receiver.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/suspension.py +0 -0
- {queueio-0.4.0 → queueio-0.5.0}/queueio/thread.py +0 -0
|
@@ -8,6 +8,24 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
|
|
|
8
8
|
[Unreleased]
|
|
9
9
|
------------
|
|
10
10
|
|
|
11
|
+
[0.5.0] - 2026-02-08
|
|
12
|
+
--------------------
|
|
13
|
+
|
|
14
|
+
### Breaking Changes
|
|
15
|
+
|
|
16
|
+
- `invocation.priority` removed;
|
|
17
|
+
use `invocation.context[priority]` instead.
|
|
18
|
+
- `Invocation.Submitted.priority` replaced with `context`.
|
|
19
|
+
- Serialized invocations from previous versions are incompatible.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `QueueVar` for user-defined serializable context variables
|
|
24
|
+
that propagate across queue boundaries.
|
|
25
|
+
- `QueueContext` mapping to access captured context on invocations.
|
|
26
|
+
- `priority` is now a `QueueVar` instance
|
|
27
|
+
(same context manager syntax as before).
|
|
28
|
+
|
|
11
29
|
[0.4.0] - 2026-02-07
|
|
12
30
|
--------------------
|
|
13
31
|
|
|
@@ -78,7 +96,8 @@ Thank you to Nick Anderegg for allowing me to use the queueio name for this proj
|
|
|
78
96
|
- The queuespec syntax to `queue run` to consume multiple queues with shared capacity.
|
|
79
97
|
- `queueio monitor` command to monitor activity in the queueio system.
|
|
80
98
|
|
|
81
|
-
[Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.
|
|
99
|
+
[Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...HEAD
|
|
100
|
+
[0.5.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.4.0...tag/0.5.0
|
|
82
101
|
[0.4.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.3.0...tag/0.4.0
|
|
83
102
|
[0.3.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.2...tag/0.3.0
|
|
84
103
|
[0.2.2]: https://github.com/ryanhiebert/queueio/compare/tag/0.2.1...tag/0.2.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "queueio"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Python background queues with an async twist"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -52,6 +52,7 @@ register = [
|
|
|
52
52
|
"queueio.samples.basic",
|
|
53
53
|
"queueio.samples.expanded",
|
|
54
54
|
"queueio.samples.priority",
|
|
55
|
+
"queueio.samples.queuevar",
|
|
55
56
|
]
|
|
56
57
|
pika = "amqp://localhost:5672"
|
|
57
58
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
|
|
3
3
|
from .gather import gather as gather
|
|
4
|
-
from .invocation import priority as priority
|
|
5
4
|
from .pause import pause as pause
|
|
6
5
|
from .queueio import QueueIO as RealQueueIO
|
|
6
|
+
from .queueio import priority as priority
|
|
7
|
+
from .queuevar import QueueVar as QueueVar
|
|
7
8
|
from .registry import routine as routine
|
|
8
9
|
|
|
9
10
|
|
|
@@ -2,6 +2,7 @@ from collections.abc import Callable
|
|
|
2
2
|
from collections.abc import Generator
|
|
3
3
|
from collections.abc import Iterable
|
|
4
4
|
from collections.abc import Iterator
|
|
5
|
+
from contextvars import Context
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from .invocation import Invocation
|
|
@@ -41,6 +42,7 @@ class Consumer(Iterable[Invocation]):
|
|
|
41
42
|
invocation: Invocation,
|
|
42
43
|
generator: Generator,
|
|
43
44
|
suspension: Suspension | None,
|
|
45
|
+
context: Context,
|
|
44
46
|
):
|
|
45
47
|
"""Signal that the invocation has suspended."""
|
|
46
48
|
if suspension:
|
|
@@ -51,6 +53,7 @@ class Consumer(Iterable[Invocation]):
|
|
|
51
53
|
suspension=suspension,
|
|
52
54
|
invocation=invocation,
|
|
53
55
|
generator=generator,
|
|
56
|
+
context=context,
|
|
54
57
|
)
|
|
55
58
|
)
|
|
56
59
|
self.__receiver.pause(self.__invocations[invocation])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from collections.abc import Generator
|
|
3
|
+
from contextvars import Context
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from dataclasses import field
|
|
5
6
|
from typing import Any
|
|
@@ -17,6 +18,7 @@ class Continuation[T: Callable[..., Any] = Callable[..., Any]]:
|
|
|
17
18
|
invocation: Invocation
|
|
18
19
|
generator: Generator[Invocation, Any, Any]
|
|
19
20
|
result: Result[Any, BaseException]
|
|
21
|
+
context: Context
|
|
20
22
|
|
|
21
23
|
def resume(self) -> Any:
|
|
22
24
|
match self.result:
|
|
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
|
|
3
3
|
from collections.abc import Generator
|
|
4
4
|
from concurrent.futures import Future
|
|
5
5
|
from contextlib import contextmanager
|
|
6
|
+
from contextvars import Context
|
|
6
7
|
from contextvars import ContextVar
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from dataclasses import field
|
|
@@ -11,19 +12,9 @@ from typing import Self
|
|
|
11
12
|
|
|
12
13
|
from .event import Event
|
|
13
14
|
from .id import random_id
|
|
15
|
+
from .queuevar import QueueContext
|
|
14
16
|
from .suspension import Suspension
|
|
15
17
|
|
|
16
|
-
_priority = ContextVar[int]("priority", default=4)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@contextmanager
|
|
20
|
-
def priority(level: int, /):
|
|
21
|
-
token = _priority.set(level)
|
|
22
|
-
try:
|
|
23
|
-
yield
|
|
24
|
-
finally:
|
|
25
|
-
_priority.reset(token)
|
|
26
|
-
|
|
27
18
|
|
|
28
19
|
@dataclass(eq=False, kw_only=True)
|
|
29
20
|
class Invocation[R](Suspension[R]):
|
|
@@ -31,7 +22,7 @@ class Invocation[R](Suspension[R]):
|
|
|
31
22
|
routine: str
|
|
32
23
|
args: tuple[Any, ...]
|
|
33
24
|
kwargs: dict[str, Any]
|
|
34
|
-
|
|
25
|
+
context: QueueContext = field(default_factory=QueueContext.capture)
|
|
35
26
|
|
|
36
27
|
__handler = ContextVar[Callable[[Self], Future] | None](
|
|
37
28
|
"Invocation.handler", default=None
|
|
@@ -68,7 +59,7 @@ class Invocation[R](Suspension[R]):
|
|
|
68
59
|
"routine": self.routine,
|
|
69
60
|
"args": self.args,
|
|
70
61
|
"kwargs": self.kwargs,
|
|
71
|
-
"
|
|
62
|
+
"context": self.context.serialize(),
|
|
72
63
|
}
|
|
73
64
|
).encode()
|
|
74
65
|
|
|
@@ -80,7 +71,7 @@ class Invocation[R](Suspension[R]):
|
|
|
80
71
|
routine=data["routine"],
|
|
81
72
|
args=data["args"],
|
|
82
73
|
kwargs=data["kwargs"],
|
|
83
|
-
|
|
74
|
+
context=QueueContext.deserialize(data.get("context", {})),
|
|
84
75
|
)
|
|
85
76
|
|
|
86
77
|
@dataclass(eq=False, kw_only=True, repr=False)
|
|
@@ -88,7 +79,7 @@ class Invocation[R](Suspension[R]):
|
|
|
88
79
|
routine: str
|
|
89
80
|
args: tuple[Any]
|
|
90
81
|
kwargs: dict[str, Any]
|
|
91
|
-
|
|
82
|
+
context: QueueContext
|
|
92
83
|
|
|
93
84
|
@dataclass(eq=False, kw_only=True)
|
|
94
85
|
class Started(Event): ...
|
|
@@ -104,6 +95,7 @@ class Invocation[R](Suspension[R]):
|
|
|
104
95
|
suspension: Suspension = field(repr=False)
|
|
105
96
|
generator: Generator[Invocation, Any, Any] = field(repr=False)
|
|
106
97
|
invocation: Invocation = field(repr=False)
|
|
98
|
+
context: Context = field(repr=False)
|
|
107
99
|
|
|
108
100
|
@dataclass(eq=False, kw_only=True)
|
|
109
101
|
class BaseContinued(Event):
|
|
@@ -17,6 +17,7 @@ from .message import Message
|
|
|
17
17
|
from .queue import Queue
|
|
18
18
|
from .queue import ShutDown
|
|
19
19
|
from .queuespec import QueueSpec
|
|
20
|
+
from .queuevar import QueueVar
|
|
20
21
|
from .registry import ROUTINE_REGISTRY
|
|
21
22
|
from .result import Err
|
|
22
23
|
from .result import Ok
|
|
@@ -24,6 +25,8 @@ from .routine import Routine
|
|
|
24
25
|
from .stream import Stream
|
|
25
26
|
from .thread import Thread
|
|
26
27
|
|
|
28
|
+
priority: QueueVar[int] = QueueVar("priority", default=4)
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
class QueueIO:
|
|
29
32
|
__active = ContextVar[Self | None]("active", default=None)
|
|
@@ -181,12 +184,14 @@ class QueueIO:
|
|
|
181
184
|
routine=invocation.routine,
|
|
182
185
|
args=invocation.args,
|
|
183
186
|
kwargs=invocation.kwargs,
|
|
184
|
-
|
|
187
|
+
context=invocation.context,
|
|
185
188
|
)
|
|
186
189
|
)
|
|
187
190
|
queue = routine.queue
|
|
188
191
|
self.__broker.enqueue(
|
|
189
|
-
invocation.serialize(),
|
|
192
|
+
invocation.serialize(),
|
|
193
|
+
queue=queue,
|
|
194
|
+
priority=invocation.context.get(priority, priority.get()),
|
|
190
195
|
)
|
|
191
196
|
|
|
192
197
|
def consume(self, queuespec: QueueSpec, /) -> Consumer:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# QueueVar
|
|
2
|
+
|
|
3
|
+
Queue variables propagate context across queue boundaries.
|
|
4
|
+
Like Python's `contextvars.ContextVar`,
|
|
5
|
+
but values automatically serialize with invocations
|
|
6
|
+
and are restored when the worker executes them.
|
|
7
|
+
|
|
8
|
+
## Defining a QueueVar
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from queueio import QueueVar
|
|
12
|
+
|
|
13
|
+
tenant_id = QueueVar("tenant_id", default=None)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Creating a `QueueVar` registers it globally.
|
|
17
|
+
All registered queue variables are captured
|
|
18
|
+
when an `Invocation` is created,
|
|
19
|
+
and restored when the worker runs it.
|
|
20
|
+
|
|
21
|
+
## Setting and reading values
|
|
22
|
+
|
|
23
|
+
Set a value with the context manager:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
with tenant_id("acme"):
|
|
27
|
+
# tenant_id.get() returns "acme" here
|
|
28
|
+
invocation = my_routine()
|
|
29
|
+
# tenant_id is captured in invocation.context
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Read the current value with `.get()`:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
@routine(name="greet", queue="default")
|
|
36
|
+
def greet():
|
|
37
|
+
tid = tenant_id.get()
|
|
38
|
+
print(f"Hello from tenant {tid}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Propagation
|
|
42
|
+
|
|
43
|
+
When an invocation is created,
|
|
44
|
+
the current values of all registered queue variables are captured
|
|
45
|
+
into `invocation.context`.
|
|
46
|
+
The worker restores these values before executing the routine,
|
|
47
|
+
so any invocations created during execution inherit the parent's context.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
@routine(name="inner", queue="work")
|
|
51
|
+
def inner():
|
|
52
|
+
return tenant_id.get()
|
|
53
|
+
|
|
54
|
+
@routine(name="outer", queue="work")
|
|
55
|
+
async def outer():
|
|
56
|
+
with tenant_id("acme"):
|
|
57
|
+
result = await inner()
|
|
58
|
+
assert result == "acme"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Accessing context on an invocation
|
|
62
|
+
|
|
63
|
+
The `QueueVar` instance is a key,
|
|
64
|
+
and `QueueContext` behaves like a mapping:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
with tenant_id("acme"):
|
|
68
|
+
invocation = my_routine()
|
|
69
|
+
print(invocation.context[tenant_id]) # "acme"
|
|
70
|
+
print(tenant_id in invocation.context) # True
|
|
71
|
+
|
|
72
|
+
invocation = my_routine()
|
|
73
|
+
# invocation.context[tenant_id] would raise KeyError
|
|
74
|
+
print(tenant_id in invocation.context) # False
|
|
75
|
+
print(invocation.context.get(tenant_id, "unknown")) # "unknown"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Priority
|
|
79
|
+
|
|
80
|
+
Priority is a built-in queue variable:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from queueio import priority
|
|
84
|
+
|
|
85
|
+
with priority(2):
|
|
86
|
+
invocation = my_routine()
|
|
87
|
+
print(invocation.context[priority]) # 2
|
|
88
|
+
print(invocation.context.get(priority, priority.default)) # 2
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Context variables
|
|
92
|
+
|
|
93
|
+
Regular `contextvars.ContextVar` values
|
|
94
|
+
do not propagate across queue boundaries.
|
|
95
|
+
Use `QueueVar` for any context that needs to travel
|
|
96
|
+
with invocations.
|
|
97
|
+
|
|
98
|
+
## Serialization
|
|
99
|
+
|
|
100
|
+
Values must be JSON-serializable.
|
|
101
|
+
The context is included in the serialized invocation
|
|
102
|
+
and restored on deserialization.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from contextvars import Context
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from contextvars import copy_context
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_registry: dict[ContextVar, QueueVar] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QueueVar[T]:
|
|
13
|
+
def __init__(self, name: str, *, default: T):
|
|
14
|
+
self.__default = default
|
|
15
|
+
self.__var: ContextVar[T] = ContextVar(name, default=default)
|
|
16
|
+
_registry[self.__var] = self
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return self.__var.name
|
|
21
|
+
|
|
22
|
+
def get(self) -> T:
|
|
23
|
+
return self.__var.get()
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def __call__(self, value: T, /):
|
|
27
|
+
token = self.__var.set(value)
|
|
28
|
+
try:
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
self.__var.reset(token)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QueueContext:
|
|
35
|
+
def __init__(self, data: dict[str, Any]):
|
|
36
|
+
self.__data = data
|
|
37
|
+
|
|
38
|
+
def __getitem__[T](self, var: QueueVar[T]) -> T:
|
|
39
|
+
return self.__data[var.name]
|
|
40
|
+
|
|
41
|
+
def get[T, Default](self, var: QueueVar[T], default: Default = None) -> T | Default:
|
|
42
|
+
return self.__data.get(var.name, default)
|
|
43
|
+
|
|
44
|
+
def __contains__(self, var: object) -> bool:
|
|
45
|
+
if not isinstance(var, QueueVar):
|
|
46
|
+
return False
|
|
47
|
+
return var.name in self.__data
|
|
48
|
+
|
|
49
|
+
def load(self, ctx: Context) -> None:
|
|
50
|
+
def initialize():
|
|
51
|
+
for contextvar, queuevar in _registry.items():
|
|
52
|
+
if queuevar.name in self.__data:
|
|
53
|
+
contextvar.set(self.__data[queuevar.name])
|
|
54
|
+
|
|
55
|
+
ctx.run(initialize)
|
|
56
|
+
|
|
57
|
+
def serialize(self) -> dict[str, Any]:
|
|
58
|
+
return dict(self.__data)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def deserialize(cls, data: dict[str, Any]) -> QueueContext:
|
|
62
|
+
return cls(data)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def capture(cls) -> QueueContext:
|
|
66
|
+
data: dict[str, Any] = {}
|
|
67
|
+
ctx = copy_context()
|
|
68
|
+
for contextvar in ctx:
|
|
69
|
+
if contextvar in _registry:
|
|
70
|
+
data[_registry[contextvar].name] = ctx[contextvar]
|
|
71
|
+
return cls(data)
|
|
@@ -4,16 +4,16 @@ from queueio import routine
|
|
|
4
4
|
|
|
5
5
|
@routine(name="inherited_priority", queue="priority")
|
|
6
6
|
def inherited_priority():
|
|
7
|
-
return
|
|
7
|
+
return priority.get()
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@routine(name="demonstrate_priority_inheritance", queue="priority")
|
|
11
11
|
async def demonstrate_priorities():
|
|
12
12
|
invocation = inherited_priority()
|
|
13
|
-
assert
|
|
13
|
+
assert priority not in invocation.context
|
|
14
14
|
assert await invocation == 4
|
|
15
15
|
|
|
16
16
|
with priority(2):
|
|
17
17
|
invocation = inherited_priority()
|
|
18
|
-
assert invocation.priority == 2
|
|
18
|
+
assert invocation.context[priority] == 2
|
|
19
19
|
assert await invocation == 2
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from queueio import QueueVar
|
|
2
|
+
from queueio import routine
|
|
3
|
+
|
|
4
|
+
tenant_id = QueueVar[str | None]("tenant_id", default=None)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@routine(name="get_tenant", queue="queuevar")
|
|
8
|
+
def get_tenant():
|
|
9
|
+
return tenant_id.get()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@routine(name="demonstrate_queuevar", queue="queuevar")
|
|
13
|
+
async def demonstrate_queuevar():
|
|
14
|
+
invocation = get_tenant()
|
|
15
|
+
assert tenant_id not in invocation.context
|
|
16
|
+
assert await invocation is None
|
|
17
|
+
|
|
18
|
+
with tenant_id("acme"):
|
|
19
|
+
invocation = get_tenant()
|
|
20
|
+
assert invocation.context[tenant_id] == "acme"
|
|
21
|
+
assert await invocation == "acme"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from queueio.invocation import Invocation
|
|
7
|
+
from queueio.queueio import QueueIO
|
|
8
|
+
from queueio.result import Ok
|
|
9
|
+
|
|
10
|
+
from .queuevar import demonstrate_queuevar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.timeout(10)
|
|
14
|
+
def test_queuevar_propagation():
|
|
15
|
+
"""QueueVar values propagate through the full invocation chain."""
|
|
16
|
+
queueio = QueueIO()
|
|
17
|
+
|
|
18
|
+
with queueio.activate():
|
|
19
|
+
queueio.create(queue="queuevar")
|
|
20
|
+
queueio.purge(queue="queuevar")
|
|
21
|
+
events = queueio.subscribe({Invocation.Completed})
|
|
22
|
+
invocation = demonstrate_queuevar()
|
|
23
|
+
queueio.submit(invocation)
|
|
24
|
+
|
|
25
|
+
proc = subprocess.Popen(
|
|
26
|
+
[sys.executable, "-m", "queueio", "run", "queuevar=2"],
|
|
27
|
+
stdout=subprocess.PIPE,
|
|
28
|
+
stderr=subprocess.PIPE,
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
while event := events.get():
|
|
32
|
+
if event.id == invocation.id:
|
|
33
|
+
assert isinstance(event.result, Ok)
|
|
34
|
+
break
|
|
35
|
+
finally:
|
|
36
|
+
if proc.poll() is None:
|
|
37
|
+
proc.terminate()
|
|
38
|
+
try:
|
|
39
|
+
proc.wait(timeout=5)
|
|
40
|
+
except subprocess.TimeoutExpired:
|
|
41
|
+
proc.kill()
|
|
42
|
+
proc.wait()
|
|
@@ -3,11 +3,11 @@ from concurrent.futures import FIRST_COMPLETED
|
|
|
3
3
|
from concurrent.futures import Future
|
|
4
4
|
from concurrent.futures import wait
|
|
5
5
|
from contextlib import suppress
|
|
6
|
+
from contextvars import copy_context
|
|
6
7
|
from threading import Timer
|
|
7
8
|
|
|
8
9
|
from .continuation import Continuation
|
|
9
10
|
from .invocation import Invocation
|
|
10
|
-
from .invocation import priority
|
|
11
11
|
from .queue import Queue
|
|
12
12
|
from .queue import ShutDown
|
|
13
13
|
from .queueio import QueueIO
|
|
@@ -105,6 +105,7 @@ class Worker:
|
|
|
105
105
|
invocation=event.invocation,
|
|
106
106
|
generator=event.generator,
|
|
107
107
|
result=Ok(None),
|
|
108
|
+
context=event.context,
|
|
108
109
|
)
|
|
109
110
|
# Replace ``new`` before setting the result
|
|
110
111
|
# to avoid short busy wait loops.
|
|
@@ -144,6 +145,7 @@ class Worker:
|
|
|
144
145
|
invocation=continuation.invocation,
|
|
145
146
|
generator=continuation.generator,
|
|
146
147
|
result=Err(exception),
|
|
148
|
+
context=continuation.context,
|
|
147
149
|
)
|
|
148
150
|
)
|
|
149
151
|
else:
|
|
@@ -156,6 +158,7 @@ class Worker:
|
|
|
156
158
|
invocation=continuation.invocation,
|
|
157
159
|
generator=continuation.generator,
|
|
158
160
|
result=Ok(value),
|
|
161
|
+
context=continuation.context,
|
|
159
162
|
),
|
|
160
163
|
)
|
|
161
164
|
|
|
@@ -190,9 +193,10 @@ class Worker:
|
|
|
190
193
|
def __run_invocation(self, invocation: Invocation):
|
|
191
194
|
"""Process an invocation task."""
|
|
192
195
|
routine = self.__queueio.routine(invocation.routine)
|
|
196
|
+
ctx = copy_context()
|
|
197
|
+
invocation.context.load(ctx)
|
|
193
198
|
try:
|
|
194
|
-
|
|
195
|
-
result = routine.fn(*invocation.args, **invocation.kwargs)
|
|
199
|
+
result = ctx.run(routine.fn, *invocation.args, **invocation.kwargs)
|
|
196
200
|
except Exception as exception:
|
|
197
201
|
self.__consumer.error(invocation, exception)
|
|
198
202
|
else:
|
|
@@ -203,6 +207,7 @@ class Worker:
|
|
|
203
207
|
invocation=invocation,
|
|
204
208
|
generator=generator,
|
|
205
209
|
result=Ok(None),
|
|
210
|
+
context=ctx,
|
|
206
211
|
)
|
|
207
212
|
)
|
|
208
213
|
else:
|
|
@@ -213,15 +218,17 @@ class Worker:
|
|
|
213
218
|
method = continuation.resume
|
|
214
219
|
|
|
215
220
|
try:
|
|
216
|
-
|
|
217
|
-
suspension = method()
|
|
221
|
+
suspension = continuation.context.run(method)
|
|
218
222
|
except StopIteration as stop:
|
|
219
223
|
self.__consumer.succeed(continuation.invocation, stop.value)
|
|
220
224
|
except Exception as exception:
|
|
221
225
|
self.__consumer.error(continuation.invocation, exception)
|
|
222
226
|
else:
|
|
223
227
|
self.__consumer.suspend(
|
|
224
|
-
continuation.invocation,
|
|
228
|
+
continuation.invocation,
|
|
229
|
+
continuation.generator,
|
|
230
|
+
suspension,
|
|
231
|
+
continuation.context,
|
|
225
232
|
)
|
|
226
233
|
|
|
227
234
|
def stop(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|