schedex 0.1.0__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.
- schedex/__init__.py +35 -0
- schedex/context.py +40 -0
- schedex/contrib/__init__.py +0 -0
- schedex/contrib/dishka.py +53 -0
- schedex/contrib/opentelemetry.py +213 -0
- schedex/contrib/psycopg.py +114 -0
- schedex/contrib/sqlalchemy/__init__.py +16 -0
- schedex/contrib/sqlalchemy/lock.py +811 -0
- schedex/contrib/sqlalchemy/scheduler.py +130 -0
- schedex/contrib/sqlalchemy/storage.py +158 -0
- schedex/contrib/sqlalchemy/tables.py +113 -0
- schedex/eventbus.py +174 -0
- schedex/executor/__init__.py +2 -0
- schedex/executor/task_fetcher.py +87 -0
- schedex/executor/worker_pool.py +295 -0
- schedex/guard.py +210 -0
- schedex/lock.py +130 -0
- schedex/mem/__init__.py +1 -0
- schedex/mem/eventbus.py +68 -0
- schedex/metadata.py +9 -0
- schedex/py.typed +0 -0
- schedex/runtime/__init__.py +1 -0
- schedex/runtime/select.py +140 -0
- schedex/schedule/__init__.py +3 -0
- schedex/schedule/base.py +22 -0
- schedex/schedule/combined.py +20 -0
- schedex/schedule/cron.py +28 -0
- schedex/schedule/periodic.py +200 -0
- schedex/scheduler.py +286 -0
- schedex/serializer/__init__.py +1 -0
- schedex/serializer/json.py +85 -0
- schedex/serializer/serializable.py +86 -0
- schedex/storage.py +194 -0
- schedex/task.py +209 -0
- schedex-0.1.0.dist-info/METADATA +190 -0
- schedex-0.1.0.dist-info/RECORD +38 -0
- schedex-0.1.0.dist-info/WHEEL +4 -0
- schedex-0.1.0.dist-info/licenses/LICENSE +24 -0
schedex/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from . import mem
|
|
2
|
+
from .context import Context, TaskInfo
|
|
3
|
+
from .eventbus import BufferedEventSender, Event, EventKind, EventReceiver, EventSender
|
|
4
|
+
from .executor import (
|
|
5
|
+
ExecutorMiddleware,
|
|
6
|
+
ExecutorMiddlewareWrappedFunc,
|
|
7
|
+
TaskFetcher,
|
|
8
|
+
Worker,
|
|
9
|
+
WorkerPool,
|
|
10
|
+
WorkerPoolExecutor,
|
|
11
|
+
)
|
|
12
|
+
from .lock import EventManagerMixin, JobLock, Lock, LockManager, TaskLock
|
|
13
|
+
from .schedule import CombinedSchedule, PeriodicSchedule, Schedule
|
|
14
|
+
from .scheduler import (
|
|
15
|
+
Job,
|
|
16
|
+
OwnedSchedulerTransaction,
|
|
17
|
+
Scheduler,
|
|
18
|
+
SchedulerMiddleware,
|
|
19
|
+
SchedulerMiddlewareWrappedFunc,
|
|
20
|
+
SchedulerTransaction,
|
|
21
|
+
TransactionalScheduler,
|
|
22
|
+
)
|
|
23
|
+
from .serializer import Serializable
|
|
24
|
+
from .storage import (
|
|
25
|
+
JobManager,
|
|
26
|
+
JobStatus,
|
|
27
|
+
OwnedTransaction,
|
|
28
|
+
StoredJob,
|
|
29
|
+
StoredTask,
|
|
30
|
+
TaskManager,
|
|
31
|
+
TaskStatus,
|
|
32
|
+
Transaction,
|
|
33
|
+
TransactionManager,
|
|
34
|
+
)
|
|
35
|
+
from .task import BoundTask, Task, TaskRegistry
|
schedex/context.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import datetime as dt
|
|
3
|
+
|
|
4
|
+
from .metadata import Metadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dc.dataclass(frozen=True)
|
|
8
|
+
class TaskInfo:
|
|
9
|
+
"""
|
|
10
|
+
Executing task information.
|
|
11
|
+
|
|
12
|
+
:param id: task identifier
|
|
13
|
+
:param job_id: job identifier which spawned the task
|
|
14
|
+
:param sequence_number: task sequence number (unique within a job)
|
|
15
|
+
:param created_at: task creation time
|
|
16
|
+
:param attempts: number of task execution attempts
|
|
17
|
+
:param meta: task metadata
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
job_id: str
|
|
22
|
+
sequence_number: int
|
|
23
|
+
created_at: dt.datetime
|
|
24
|
+
attempts: int
|
|
25
|
+
meta: Metadata
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dc.dataclass(frozen=True)
|
|
29
|
+
class Context[StT, LkT]:
|
|
30
|
+
"""
|
|
31
|
+
Task context.
|
|
32
|
+
|
|
33
|
+
:param state: application state
|
|
34
|
+
:param task: executing task
|
|
35
|
+
:param lock: executing task lock
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
state: StT
|
|
39
|
+
task: TaskInfo
|
|
40
|
+
lock: LkT
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections import ChainMap
|
|
2
|
+
from typing import Any, Callable, Concatenate, MutableMapping, Never
|
|
3
|
+
|
|
4
|
+
import dishka
|
|
5
|
+
from dishka.integrations.base import wrap_injection
|
|
6
|
+
|
|
7
|
+
from schedex import Context, ExecutorMiddlewareWrappedFunc, Task
|
|
8
|
+
|
|
9
|
+
CONTAINER_KEY = "dishka_container"
|
|
10
|
+
|
|
11
|
+
AppState = MutableMapping[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InjectedType:
|
|
15
|
+
def __getattribute__(self, item: Any) -> Never:
|
|
16
|
+
raise AssertionError("argument is not injected")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Injected: Any = InjectedType()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def inject[StT: AppState, **P, R](
|
|
23
|
+
func: Callable[Concatenate[Context[StT, Any], P], R],
|
|
24
|
+
) -> Callable[Concatenate[Context[StT, Any], P], R]:
|
|
25
|
+
"""
|
|
26
|
+
Function decorator injecting dependencies into the function arguments.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
return wrap_injection(
|
|
30
|
+
func=func,
|
|
31
|
+
is_async=True,
|
|
32
|
+
container_getter=lambda args, kwargs: args[0].state[CONTAINER_KEY],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def request_container_middleware(
|
|
37
|
+
context: Context[AppState, Any],
|
|
38
|
+
task: Task[Context[AppState, Any], Any, Any],
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
wrapped: ExecutorMiddlewareWrappedFunc[Context[AppState, Any]],
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
A middleware that initialize request-scoped di container.
|
|
45
|
+
Works only with MutableMapping application state.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
container: dishka.AsyncContainer = context.state[CONTAINER_KEY]
|
|
49
|
+
|
|
50
|
+
async with container(scope=dishka.Scope.REQUEST) as request_container:
|
|
51
|
+
request_context = ChainMap({CONTAINER_KEY: request_container}, context.state)
|
|
52
|
+
context = Context(state=request_context, task=context.task, lock=context.lock)
|
|
53
|
+
return await wrapped(context, task)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import timeit
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import opentelemetry as otl
|
|
5
|
+
import opentelemetry.baggage
|
|
6
|
+
import opentelemetry.metrics
|
|
7
|
+
import opentelemetry.propagate
|
|
8
|
+
import opentelemetry.propagators.textmap
|
|
9
|
+
import opentelemetry.trace.propagation.tracecontext
|
|
10
|
+
from opentelemetry.util import types
|
|
11
|
+
|
|
12
|
+
from schedex import (
|
|
13
|
+
Context,
|
|
14
|
+
ExecutorMiddleware,
|
|
15
|
+
ExecutorMiddlewareWrappedFunc,
|
|
16
|
+
Job,
|
|
17
|
+
Schedule,
|
|
18
|
+
SchedulerMiddleware,
|
|
19
|
+
SchedulerMiddlewareWrappedFunc,
|
|
20
|
+
Task,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
INSTRUMENTING_MODULE_NAME = "schedex"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_job_schedule_tracing_middleware[SchT: Schedule](
|
|
27
|
+
tracer_provider: Optional[otl.trace.TracerProvider] = None,
|
|
28
|
+
propagator: Optional[otl.propagators.textmap.TextMapPropagator] = None,
|
|
29
|
+
) -> SchedulerMiddleware[SchT]:
|
|
30
|
+
"""
|
|
31
|
+
Builds a middleware for job scheduling tracing.
|
|
32
|
+
|
|
33
|
+
:param tracer_provider: tracer provider. If not passed the global one will be used.
|
|
34
|
+
:param propagator: trace propagator. If not passed the global one will be used.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
tracer_provider = tracer_provider or otl.trace.get_tracer_provider()
|
|
38
|
+
tracer = tracer_provider.get_tracer(INSTRUMENTING_MODULE_NAME)
|
|
39
|
+
|
|
40
|
+
propagator = propagator or otl.propagate.get_global_textmap()
|
|
41
|
+
|
|
42
|
+
async def trace_job_schedule(
|
|
43
|
+
job: Job[SchT],
|
|
44
|
+
/,
|
|
45
|
+
*,
|
|
46
|
+
wrapped: SchedulerMiddlewareWrappedFunc[SchT],
|
|
47
|
+
) -> Optional[str]:
|
|
48
|
+
attributes: types.Attributes = {
|
|
49
|
+
"scheduler.job.id": job.id,
|
|
50
|
+
"scheduler.job.name": job.task.name,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
with tracer.start_as_current_span(job.task.name, kind=otl.trace.SpanKind.PRODUCER, attributes=attributes):
|
|
54
|
+
propagator.inject(job.meta)
|
|
55
|
+
return await wrapped(job)
|
|
56
|
+
|
|
57
|
+
return trace_job_schedule
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_job_schedule_metrics_collecting_middleware[SchT: Schedule](
|
|
61
|
+
meter_provider: Optional[otl.metrics.MeterProvider] = None,
|
|
62
|
+
) -> SchedulerMiddleware[SchT]:
|
|
63
|
+
"""
|
|
64
|
+
Builds a middleware for job scheduling metrics collecting.
|
|
65
|
+
|
|
66
|
+
:param meter_provider: metrics provider. If not passed the global one will be used.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
meter_provider = meter_provider or otl.metrics.get_meter_provider()
|
|
70
|
+
meter = meter_provider.get_meter(INSTRUMENTING_MODULE_NAME)
|
|
71
|
+
|
|
72
|
+
publish_counter = meter.create_up_down_counter(
|
|
73
|
+
name="scheduler.submit.jobs",
|
|
74
|
+
unit="jobs",
|
|
75
|
+
description="Measures the total number of scheduled jobs",
|
|
76
|
+
)
|
|
77
|
+
duration_histogram = meter.create_histogram(
|
|
78
|
+
name="scheduler.submit.duration",
|
|
79
|
+
unit="ms",
|
|
80
|
+
description="Measures the duration of scheduling",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def collect_metrics(
|
|
84
|
+
job: Job[SchT],
|
|
85
|
+
/,
|
|
86
|
+
*,
|
|
87
|
+
wrapped: SchedulerMiddlewareWrappedFunc[SchT],
|
|
88
|
+
) -> Optional[str]:
|
|
89
|
+
attributes: types.Attributes = {
|
|
90
|
+
"scheduler.job.name": job.task.name,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
publish_counter.add(amount=1, attributes=attributes)
|
|
94
|
+
|
|
95
|
+
started_at = timeit.default_timer()
|
|
96
|
+
try:
|
|
97
|
+
return await wrapped(job)
|
|
98
|
+
finally:
|
|
99
|
+
duration_histogram.record(amount=timeit.default_timer() - started_at, attributes=attributes)
|
|
100
|
+
|
|
101
|
+
return collect_metrics
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_task_execution_tracing_middleware[CtxT: Context[Any, Any]](
|
|
105
|
+
tracer_provider: Optional[otl.trace.TracerProvider] = None,
|
|
106
|
+
propagator: Optional[otl.propagators.textmap.TextMapPropagator] = None,
|
|
107
|
+
) -> ExecutorMiddleware[CtxT]:
|
|
108
|
+
"""
|
|
109
|
+
Builds a middleware for tracing scheduler task executions.
|
|
110
|
+
|
|
111
|
+
:param tracer_provider: tracer provider. If not passed the global one will be used.
|
|
112
|
+
:param propagator: trace propagator. If not passed the global one will be used.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
tracer_provider = tracer_provider or otl.trace.get_tracer_provider()
|
|
116
|
+
tracer = tracer_provider.get_tracer(INSTRUMENTING_MODULE_NAME)
|
|
117
|
+
|
|
118
|
+
propagator = propagator or otl.propagate.get_global_textmap()
|
|
119
|
+
|
|
120
|
+
def copy_baggage(
|
|
121
|
+
src_context: otl.context.Context, dst_context: Optional[otl.context.Context] = None
|
|
122
|
+
) -> otl.context.Context:
|
|
123
|
+
dst_context = dst_context or otl.context.get_current()
|
|
124
|
+
|
|
125
|
+
src_baggage = otl.baggage.get_all(src_context)
|
|
126
|
+
for key, value in src_baggage.items():
|
|
127
|
+
dst_context = otl.baggage.set_baggage(key, value, dst_context)
|
|
128
|
+
|
|
129
|
+
return dst_context
|
|
130
|
+
|
|
131
|
+
async def trace_task_execution(
|
|
132
|
+
context: CtxT,
|
|
133
|
+
task: Task[CtxT, Any, Any],
|
|
134
|
+
/,
|
|
135
|
+
*,
|
|
136
|
+
wrapped: ExecutorMiddlewareWrappedFunc[CtxT],
|
|
137
|
+
) -> None:
|
|
138
|
+
attributes: types.Attributes = {
|
|
139
|
+
"scheduler.job.id": context.task.job_id,
|
|
140
|
+
"scheduler.task.id": context.task.id,
|
|
141
|
+
"scheduler.task.name": task.name,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
propagated_context = propagator.extract(context.task.meta)
|
|
145
|
+
linked_span = otl.trace.get_current_span(propagated_context)
|
|
146
|
+
linked_context = linked_span.get_span_context()
|
|
147
|
+
|
|
148
|
+
token = otl.context.attach(copy_baggage(propagated_context))
|
|
149
|
+
try:
|
|
150
|
+
with tracer.start_as_current_span(
|
|
151
|
+
task.name,
|
|
152
|
+
links=[otl.trace.Link(linked_context)],
|
|
153
|
+
kind=otl.trace.SpanKind.CONSUMER,
|
|
154
|
+
attributes=attributes,
|
|
155
|
+
):
|
|
156
|
+
return await wrapped(context, task)
|
|
157
|
+
finally:
|
|
158
|
+
otl.context.detach(token)
|
|
159
|
+
|
|
160
|
+
return trace_task_execution
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def build_task_execution_metrics_collecting_middleware[CtxT: Context[Any, Any]](
|
|
164
|
+
meter_provider: Optional[otl.metrics.MeterProvider] = None,
|
|
165
|
+
) -> ExecutorMiddleware[CtxT]:
|
|
166
|
+
"""
|
|
167
|
+
Builds a middleware for task execution metrics collecting.
|
|
168
|
+
|
|
169
|
+
:param meter_provider: metrics provider. If not passed the global one will be used.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
meter_provider = meter_provider or otl.metrics.get_meter_provider()
|
|
173
|
+
meter = meter_provider.get_meter(INSTRUMENTING_MODULE_NAME)
|
|
174
|
+
|
|
175
|
+
publish_counter = meter.create_up_down_counter(
|
|
176
|
+
name="scheduler.execution.tasks",
|
|
177
|
+
unit="tasks",
|
|
178
|
+
description="Measures the total number of scheduler executed tasks",
|
|
179
|
+
)
|
|
180
|
+
error_counter = meter.create_counter(
|
|
181
|
+
name="scheduler.execution.errors",
|
|
182
|
+
unit="errors",
|
|
183
|
+
description="Measures the number of scheduler task execution errors",
|
|
184
|
+
)
|
|
185
|
+
duration_histogram = meter.create_histogram(
|
|
186
|
+
name="scheduler.execution.duration",
|
|
187
|
+
unit="ms",
|
|
188
|
+
description="Measures the duration of scheduler task execution",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def collect_metrics(
|
|
192
|
+
context: CtxT,
|
|
193
|
+
task: Task[CtxT, Any, Any],
|
|
194
|
+
/,
|
|
195
|
+
*,
|
|
196
|
+
wrapped: ExecutorMiddlewareWrappedFunc[CtxT],
|
|
197
|
+
) -> None:
|
|
198
|
+
attributes: types.Attributes = {
|
|
199
|
+
"scheduler.task.name": task.name,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
publish_counter.add(amount=1, attributes=attributes)
|
|
203
|
+
|
|
204
|
+
started_at = timeit.default_timer()
|
|
205
|
+
try:
|
|
206
|
+
return await wrapped(context, task)
|
|
207
|
+
except Exception:
|
|
208
|
+
error_counter.add(amount=1, attributes=attributes)
|
|
209
|
+
raise
|
|
210
|
+
finally:
|
|
211
|
+
duration_histogram.record(amount=timeit.default_timer() - started_at, attributes=attributes)
|
|
212
|
+
|
|
213
|
+
return collect_metrics
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import contextlib as cl
|
|
3
|
+
import logging
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Self
|
|
6
|
+
|
|
7
|
+
import psycopg as ppg
|
|
8
|
+
import psycopg_pool as ppg_pool
|
|
9
|
+
from psycopg import sql
|
|
10
|
+
|
|
11
|
+
from schedex import Event, EventReceiver, EventSender
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PsycopgEventSender(EventSender):
|
|
17
|
+
"""
|
|
18
|
+
Psycopg event sender.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
pool: ppg_pool.AsyncConnectionPool,
|
|
24
|
+
channel: str = "schedex_events",
|
|
25
|
+
encoder: Callable[[bytes], str] = lambda d: base64.b64encode(d).decode(),
|
|
26
|
+
):
|
|
27
|
+
self._pool = pool
|
|
28
|
+
self._channel = channel
|
|
29
|
+
self._encoder = encoder
|
|
30
|
+
|
|
31
|
+
async def send(self, event: Event) -> None:
|
|
32
|
+
serialized = event.serialize()
|
|
33
|
+
encoded = self._encoder(serialized)
|
|
34
|
+
|
|
35
|
+
async with self._pool.connection() as conn:
|
|
36
|
+
await conn.execute("SELECT pg_notify(%s, %s)", (self._channel, encoded))
|
|
37
|
+
logger.debug("sent event: %s", event)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EventSource(AsyncIterator[Event]):
|
|
41
|
+
"""
|
|
42
|
+
Psycopg event source.
|
|
43
|
+
|
|
44
|
+
:param conn: database connection
|
|
45
|
+
:param decoder: binary data decoder
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, conn: ppg.AsyncConnection[Any], decoder: Callable[[str], bytes]):
|
|
49
|
+
self._conn = conn
|
|
50
|
+
self._notifies = conn.notifies()
|
|
51
|
+
self._decoder = decoder
|
|
52
|
+
|
|
53
|
+
async def __aenter__(self) -> Self:
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
async def __aexit__(
|
|
57
|
+
self,
|
|
58
|
+
exc_type: Optional[type[BaseException]],
|
|
59
|
+
exc_value: Optional[BaseException],
|
|
60
|
+
traceback: Optional[TracebackType],
|
|
61
|
+
) -> Optional[bool]:
|
|
62
|
+
await self._notifies.aclose()
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def __aiter__(self) -> Self:
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __anext__(self) -> Event:
|
|
69
|
+
try:
|
|
70
|
+
notification = await anext(self._notifies)
|
|
71
|
+
except StopAsyncIteration:
|
|
72
|
+
# reconfigure notifies generator if it had been stopped
|
|
73
|
+
self._notifies = self._conn.notifies()
|
|
74
|
+
notification = await anext(self._notifies)
|
|
75
|
+
|
|
76
|
+
decoded = self._decoder(notification.payload)
|
|
77
|
+
event = Event.deserialize(decoded)
|
|
78
|
+
logger.debug("received event: %s", event)
|
|
79
|
+
|
|
80
|
+
return event
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PsycopgEventReceiver(EventReceiver):
|
|
84
|
+
"""
|
|
85
|
+
Psycopg event receiver.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
pool: ppg_pool.AsyncConnectionPool,
|
|
91
|
+
channel: str = "schedex_events",
|
|
92
|
+
decoder: Callable[[str], bytes] = base64.b64decode,
|
|
93
|
+
):
|
|
94
|
+
self._pool = pool
|
|
95
|
+
self._channel = channel
|
|
96
|
+
self._decoder = decoder
|
|
97
|
+
|
|
98
|
+
@cl.asynccontextmanager
|
|
99
|
+
async def connect(self) -> AsyncGenerator[EventSource, None]:
|
|
100
|
+
logger.info("connecting to event source ...")
|
|
101
|
+
|
|
102
|
+
async with self._pool.connection() as conn:
|
|
103
|
+
await conn.set_autocommit(True)
|
|
104
|
+
|
|
105
|
+
await conn.execute(sql.SQL("LISTEN {0}").format(sql.Identifier(self._channel)))
|
|
106
|
+
try:
|
|
107
|
+
async with EventSource(conn, self._decoder) as source:
|
|
108
|
+
logger.info("event source configured")
|
|
109
|
+
yield source
|
|
110
|
+
|
|
111
|
+
finally:
|
|
112
|
+
await conn.execute(sql.SQL("UNLISTEN {0}").format(sql.Identifier(self._channel)))
|
|
113
|
+
|
|
114
|
+
logger.info("event source connection closed")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .lock import (
|
|
2
|
+
SqlAlchemyLeasingJobLock,
|
|
3
|
+
SqlAlchemyLeasingLockManager,
|
|
4
|
+
SqlAlchemyLeasingTaskLock,
|
|
5
|
+
SqlAlchemySfuJobLock,
|
|
6
|
+
SqlAlchemySfuLockManager,
|
|
7
|
+
SqlAlchemySfuTaskLock,
|
|
8
|
+
)
|
|
9
|
+
from .scheduler import SqlAlchemyScheduler, SqlAlchemyTransactionalScheduler
|
|
10
|
+
from .storage import (
|
|
11
|
+
SqlAlchemyJobManager,
|
|
12
|
+
SqlAlchemyOwnedTransaction,
|
|
13
|
+
SqlAlchemyTransaction,
|
|
14
|
+
SqlAlchemyTransactionManager,
|
|
15
|
+
)
|
|
16
|
+
from .tables import BaseModel
|