dbos 1.1.0a2__py3-none-any.whl → 1.1.0a4__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.
- dbos/_context.py +6 -0
- dbos/_error.py +14 -0
- dbos/_sys_db.py +87 -18
- {dbos-1.1.0a2.dist-info → dbos-1.1.0a4.dist-info}/METADATA +1 -1
- {dbos-1.1.0a2.dist-info → dbos-1.1.0a4.dist-info}/RECORD +8 -8
- {dbos-1.1.0a2.dist-info → dbos-1.1.0a4.dist-info}/WHEEL +0 -0
- {dbos-1.1.0a2.dist-info → dbos-1.1.0a4.dist-info}/entry_points.txt +0 -0
- {dbos-1.1.0a2.dist-info → dbos-1.1.0a4.dist-info}/licenses/LICENSE +0 -0
dbos/_context.py
CHANGED
@@ -392,6 +392,7 @@ class SetWorkflowTimeout:
|
|
392
392
|
else None
|
393
393
|
)
|
394
394
|
self.saved_workflow_timeout: Optional[int] = None
|
395
|
+
self.saved_workflow_deadline_epoch_ms: Optional[int] = None
|
395
396
|
|
396
397
|
def __enter__(self) -> SetWorkflowTimeout:
|
397
398
|
# Code to create a basic context
|
@@ -402,6 +403,8 @@ class SetWorkflowTimeout:
|
|
402
403
|
ctx = assert_current_dbos_context()
|
403
404
|
self.saved_workflow_timeout = ctx.workflow_timeout_ms
|
404
405
|
ctx.workflow_timeout_ms = self.workflow_timeout_ms
|
406
|
+
self.saved_workflow_deadline_epoch_ms = ctx.workflow_deadline_epoch_ms
|
407
|
+
ctx.workflow_deadline_epoch_ms = None
|
405
408
|
return self
|
406
409
|
|
407
410
|
def __exit__(
|
@@ -411,6 +414,9 @@ class SetWorkflowTimeout:
|
|
411
414
|
traceback: Optional[TracebackType],
|
412
415
|
) -> Literal[False]:
|
413
416
|
assert_current_dbos_context().workflow_timeout_ms = self.saved_workflow_timeout
|
417
|
+
assert_current_dbos_context().workflow_deadline_epoch_ms = (
|
418
|
+
self.saved_workflow_deadline_epoch_ms
|
419
|
+
)
|
414
420
|
# Code to clean up the basic context if we created it
|
415
421
|
if self.created_ctx:
|
416
422
|
_clear_local_dbos_context()
|
dbos/_error.py
CHANGED
@@ -62,6 +62,7 @@ class DBOSErrorCode(Enum):
|
|
62
62
|
WorkflowCancelled = 10
|
63
63
|
UnexpectedStep = 11
|
64
64
|
QueueDeduplicated = 12
|
65
|
+
AwaitedWorkflowCancelled = 13
|
65
66
|
ConflictingRegistrationError = 25
|
66
67
|
|
67
68
|
|
@@ -206,6 +207,19 @@ class DBOSQueueDeduplicatedError(DBOSException):
|
|
206
207
|
)
|
207
208
|
|
208
209
|
|
210
|
+
class DBOSAwaitedWorkflowCancelledError(DBOSException):
|
211
|
+
def __init__(self, workflow_id: str):
|
212
|
+
self.workflow_id = workflow_id
|
213
|
+
super().__init__(
|
214
|
+
f"Awaited workflow {workflow_id} was cancelled",
|
215
|
+
dbos_error_code=DBOSErrorCode.AwaitedWorkflowCancelled.value,
|
216
|
+
)
|
217
|
+
|
218
|
+
def __reduce__(self) -> Any:
|
219
|
+
# Tell jsonpickle how to reconstruct this object
|
220
|
+
return (self.__class__, (self.workflow_id,))
|
221
|
+
|
222
|
+
|
209
223
|
#######################################
|
210
224
|
## BaseException
|
211
225
|
#######################################
|
dbos/_sys_db.py
CHANGED
@@ -32,6 +32,7 @@ from dbos._utils import INTERNAL_QUEUE_NAME
|
|
32
32
|
from . import _serialization
|
33
33
|
from ._context import get_local_dbos_context
|
34
34
|
from ._error import (
|
35
|
+
DBOSAwaitedWorkflowCancelledError,
|
35
36
|
DBOSConflictingWorkflowError,
|
36
37
|
DBOSDeadLetterQueueError,
|
37
38
|
DBOSNonExistentWorkflowError,
|
@@ -96,6 +97,10 @@ class WorkflowStatus:
|
|
96
97
|
executor_id: Optional[str]
|
97
98
|
# The application version on which this workflow was started
|
98
99
|
app_version: Optional[str]
|
100
|
+
# The start-to-close timeout of the workflow in ms
|
101
|
+
workflow_timeout_ms: Optional[int]
|
102
|
+
# The deadline of a workflow, computed by adding its timeout to its start time.
|
103
|
+
workflow_deadline_epoch_ms: Optional[int]
|
99
104
|
|
100
105
|
# INTERNAL FIELDS
|
101
106
|
|
@@ -222,6 +227,47 @@ class StepInfo(TypedDict):
|
|
222
227
|
_dbos_null_topic = "__null__topic__"
|
223
228
|
|
224
229
|
|
230
|
+
class ConditionCount(TypedDict):
|
231
|
+
condition: threading.Condition
|
232
|
+
count: int
|
233
|
+
|
234
|
+
|
235
|
+
class ThreadSafeConditionDict:
|
236
|
+
def __init__(self) -> None:
|
237
|
+
self._dict: Dict[str, ConditionCount] = {}
|
238
|
+
self._lock = threading.Lock()
|
239
|
+
|
240
|
+
def get(self, key: str) -> Optional[threading.Condition]:
|
241
|
+
with self._lock:
|
242
|
+
if key not in self._dict:
|
243
|
+
# Key does not exist, return None
|
244
|
+
return None
|
245
|
+
return self._dict[key]["condition"]
|
246
|
+
|
247
|
+
def set(
|
248
|
+
self, key: str, value: threading.Condition
|
249
|
+
) -> tuple[bool, threading.Condition]:
|
250
|
+
with self._lock:
|
251
|
+
if key in self._dict:
|
252
|
+
# Key already exists, do not overwrite. Increment the wait count.
|
253
|
+
cc = self._dict[key]
|
254
|
+
cc["count"] += 1
|
255
|
+
return False, cc["condition"]
|
256
|
+
self._dict[key] = ConditionCount(condition=value, count=1)
|
257
|
+
return True, value
|
258
|
+
|
259
|
+
def pop(self, key: str) -> None:
|
260
|
+
with self._lock:
|
261
|
+
if key in self._dict:
|
262
|
+
cc = self._dict[key]
|
263
|
+
cc["count"] -= 1
|
264
|
+
if cc["count"] == 0:
|
265
|
+
# No more threads waiting on this condition, remove it
|
266
|
+
del self._dict[key]
|
267
|
+
else:
|
268
|
+
dbos_logger.warning(f"Key {key} not found in condition dictionary.")
|
269
|
+
|
270
|
+
|
225
271
|
class SystemDatabase:
|
226
272
|
|
227
273
|
def __init__(
|
@@ -248,8 +294,8 @@ class SystemDatabase:
|
|
248
294
|
self._engine_kwargs = engine_kwargs
|
249
295
|
|
250
296
|
self.notification_conn: Optional[psycopg.connection.Connection] = None
|
251
|
-
self.notifications_map
|
252
|
-
self.workflow_events_map
|
297
|
+
self.notifications_map = ThreadSafeConditionDict()
|
298
|
+
self.workflow_events_map = ThreadSafeConditionDict()
|
253
299
|
|
254
300
|
# Now we can run background processes
|
255
301
|
self._run_background_processes = True
|
@@ -720,9 +766,9 @@ class SystemDatabase:
|
|
720
766
|
error = row[2]
|
721
767
|
raise _serialization.deserialize_exception(error)
|
722
768
|
elif status == WorkflowStatusString.CANCELLED.value:
|
723
|
-
# Raise
|
769
|
+
# Raise AwaitedWorkflowCancelledError here, not the cancellation exception
|
724
770
|
# because the awaiting workflow is not being cancelled.
|
725
|
-
raise
|
771
|
+
raise DBOSAwaitedWorkflowCancelledError(workflow_id)
|
726
772
|
else:
|
727
773
|
pass # CB: I guess we're assuming the WF will show up eventually.
|
728
774
|
time.sleep(1)
|
@@ -796,6 +842,8 @@ class SystemDatabase:
|
|
796
842
|
SystemSchema.workflow_inputs.c.inputs,
|
797
843
|
SystemSchema.workflow_status.c.output,
|
798
844
|
SystemSchema.workflow_status.c.error,
|
845
|
+
SystemSchema.workflow_status.c.workflow_deadline_epoch_ms,
|
846
|
+
SystemSchema.workflow_status.c.workflow_timeout_ms,
|
799
847
|
).join(
|
800
848
|
SystemSchema.workflow_inputs,
|
801
849
|
SystemSchema.workflow_status.c.workflow_uuid
|
@@ -877,6 +925,8 @@ class SystemDatabase:
|
|
877
925
|
info.input = inputs
|
878
926
|
info.output = output
|
879
927
|
info.error = exception
|
928
|
+
info.workflow_deadline_epoch_ms = row[18]
|
929
|
+
info.workflow_timeout_ms = row[19]
|
880
930
|
|
881
931
|
infos.append(info)
|
882
932
|
return infos
|
@@ -906,6 +956,8 @@ class SystemDatabase:
|
|
906
956
|
SystemSchema.workflow_inputs.c.inputs,
|
907
957
|
SystemSchema.workflow_status.c.output,
|
908
958
|
SystemSchema.workflow_status.c.error,
|
959
|
+
SystemSchema.workflow_status.c.workflow_deadline_epoch_ms,
|
960
|
+
SystemSchema.workflow_status.c.workflow_timeout_ms,
|
909
961
|
).select_from(
|
910
962
|
SystemSchema.workflow_queue.join(
|
911
963
|
SystemSchema.workflow_status,
|
@@ -983,6 +1035,8 @@ class SystemDatabase:
|
|
983
1035
|
info.input = inputs
|
984
1036
|
info.output = output
|
985
1037
|
info.error = exception
|
1038
|
+
info.workflow_deadline_epoch_ms = row[18]
|
1039
|
+
info.workflow_timeout_ms = row[19]
|
986
1040
|
|
987
1041
|
infos.append(info)
|
988
1042
|
|
@@ -1288,7 +1342,12 @@ class SystemDatabase:
|
|
1288
1342
|
condition = threading.Condition()
|
1289
1343
|
# Must acquire first before adding to the map. Otherwise, the notification listener may notify it before the condition is acquired and waited.
|
1290
1344
|
condition.acquire()
|
1291
|
-
self.notifications_map
|
1345
|
+
success, _ = self.notifications_map.set(payload, condition)
|
1346
|
+
if not success:
|
1347
|
+
# This should not happen, but if it does, it means the workflow is executed concurrently.
|
1348
|
+
condition.release()
|
1349
|
+
self.notifications_map.pop(payload)
|
1350
|
+
raise DBOSWorkflowConflictIDError(workflow_uuid)
|
1292
1351
|
|
1293
1352
|
# Check if the key is already in the database. If not, wait for the notification.
|
1294
1353
|
init_recv: Sequence[Any]
|
@@ -1381,11 +1440,11 @@ class SystemDatabase:
|
|
1381
1440
|
f"Received notification on channel: {channel}, payload: {notify.payload}"
|
1382
1441
|
)
|
1383
1442
|
if channel == "dbos_notifications_channel":
|
1384
|
-
if
|
1385
|
-
notify.payload
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1443
|
+
if notify.payload:
|
1444
|
+
condition = self.notifications_map.get(notify.payload)
|
1445
|
+
if condition is None:
|
1446
|
+
# No condition found for this payload
|
1447
|
+
continue
|
1389
1448
|
condition.acquire()
|
1390
1449
|
condition.notify_all()
|
1391
1450
|
condition.release()
|
@@ -1393,11 +1452,11 @@ class SystemDatabase:
|
|
1393
1452
|
f"Signaled notifications condition for {notify.payload}"
|
1394
1453
|
)
|
1395
1454
|
elif channel == "dbos_workflow_events_channel":
|
1396
|
-
if
|
1397
|
-
notify.payload
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1455
|
+
if notify.payload:
|
1456
|
+
condition = self.workflow_events_map.get(notify.payload)
|
1457
|
+
if condition is None:
|
1458
|
+
# No condition found for this payload
|
1459
|
+
continue
|
1401
1460
|
condition.acquire()
|
1402
1461
|
condition.notify_all()
|
1403
1462
|
condition.release()
|
@@ -1535,8 +1594,13 @@ class SystemDatabase:
|
|
1535
1594
|
|
1536
1595
|
payload = f"{target_uuid}::{key}"
|
1537
1596
|
condition = threading.Condition()
|
1538
|
-
self.workflow_events_map[payload] = condition
|
1539
1597
|
condition.acquire()
|
1598
|
+
success, existing_condition = self.workflow_events_map.set(payload, condition)
|
1599
|
+
if not success:
|
1600
|
+
# Wait on the existing condition
|
1601
|
+
condition.release()
|
1602
|
+
condition = existing_condition
|
1603
|
+
condition.acquire()
|
1540
1604
|
|
1541
1605
|
# Check if the key is already in the database. If not, wait for the notification.
|
1542
1606
|
init_recv: Sequence[Any]
|
@@ -1776,8 +1840,13 @@ class SystemDatabase:
|
|
1776
1840
|
# If a timeout is set, set the deadline on dequeue
|
1777
1841
|
workflow_deadline_epoch_ms=sa.case(
|
1778
1842
|
(
|
1779
|
-
|
1780
|
-
|
1843
|
+
sa.and_(
|
1844
|
+
SystemSchema.workflow_status.c.workflow_timeout_ms.isnot(
|
1845
|
+
None
|
1846
|
+
),
|
1847
|
+
SystemSchema.workflow_status.c.workflow_deadline_epoch_ms.is_(
|
1848
|
+
None
|
1849
|
+
),
|
1781
1850
|
),
|
1782
1851
|
sa.func.extract("epoch", sa.func.now()) * 1000
|
1783
1852
|
+ SystemSchema.workflow_status.c.workflow_timeout_ms,
|
@@ -1,7 +1,7 @@
|
|
1
|
-
dbos-1.1.
|
2
|
-
dbos-1.1.
|
3
|
-
dbos-1.1.
|
4
|
-
dbos-1.1.
|
1
|
+
dbos-1.1.0a4.dist-info/METADATA,sha256=U7kpwMutGlWwRdkgX-Zt_y9g_6hPYwZ5NFyMbTW4S48,13267
|
2
|
+
dbos-1.1.0a4.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
|
3
|
+
dbos-1.1.0a4.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
|
4
|
+
dbos-1.1.0a4.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
5
5
|
dbos/__init__.py,sha256=NssPCubaBxdiKarOWa-wViz1hdJSkmBGcpLX_gQ4NeA,891
|
6
6
|
dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
|
7
7
|
dbos/_admin_server.py,sha256=A_28_nJ1nBBYDmCxtklJR9O2v14JRMtD1rAo_D4y8Kc,9764
|
@@ -10,14 +10,14 @@ dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
|
|
10
10
|
dbos/_client.py,sha256=-nK2GjS9D0qnD2DkRDs7gKxNECwYlsvW6hFCjADlnv0,14186
|
11
11
|
dbos/_conductor/conductor.py,sha256=o0IaZjwnZ2TOyHeP2H4iSX6UnXLXQ4uODvWAKD9hHMs,21703
|
12
12
|
dbos/_conductor/protocol.py,sha256=wgOFZxmS81bv0WCB9dAyg0s6QzldpzVKQDoSPeaX0Ws,6967
|
13
|
-
dbos/_context.py,sha256=
|
13
|
+
dbos/_context.py,sha256=5ajoWAmToAfzzmMLylnJZoL4Ny9rBwZWuG05sXadMIA,24798
|
14
14
|
dbos/_core.py,sha256=UDpSgRA9m_YuViNXR9tVgNFLC-zxKZPxjlkj2a-Kj00,48317
|
15
15
|
dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
|
16
16
|
dbos/_dbos.py,sha256=f5s9cVgsiMkAkpvctLHE6sjVAEuC-eFEpRddYBIKxiA,46430
|
17
17
|
dbos/_dbos_config.py,sha256=IufNrIC-M2xSNTXyT_KXlEdfB3j03pPLv_nE0fEq4_U,20955
|
18
18
|
dbos/_debug.py,sha256=MNlQVZ6TscGCRQeEEL0VE8Uignvr6dPeDDDefS3xgIE,1823
|
19
19
|
dbos/_docker_pg_helper.py,sha256=tLJXWqZ4S-ExcaPnxg_i6cVxL6ZxrYlZjaGsklY-s2I,6115
|
20
|
-
dbos/_error.py,sha256=
|
20
|
+
dbos/_error.py,sha256=q0OQJZTbR8FFHV9hEpAGpz9oWBT5L509zUhmyff7FJw,8500
|
21
21
|
dbos/_event_loop.py,sha256=NmaLbEQFfEK36S_0KhVD39YdYrGce3qSKCTJ-5RqKQ0,2136
|
22
22
|
dbos/_fastapi.py,sha256=m4SL3H9P-NBQ_ZrbFxAWMOqNyIi3HGEn2ODR7xAK038,3118
|
23
23
|
dbos/_flask.py,sha256=Npnakt-a3W5OykONFRkDRnumaDhTQmA0NPdUCGRYKXE,1652
|
@@ -47,7 +47,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
dbos/_schemas/application_database.py,sha256=SypAS9l9EsaBHFn9FR8jmnqt01M74d9AF1AMa4m2hhI,1040
|
48
48
|
dbos/_schemas/system_database.py,sha256=3Z0L72bOgHnusK1hBaETWU9RfiLBP0QnS-fdu41i0yY,5835
|
49
49
|
dbos/_serialization.py,sha256=bWuwhXSQcGmiazvhJHA5gwhrRWxtmFmcCFQSDJnqqkU,3666
|
50
|
-
dbos/_sys_db.py,sha256=
|
50
|
+
dbos/_sys_db.py,sha256=gVa5arMBT8rKHkycPS8HyRzfvQdQRxYqIclw0Fcp6CM,84240
|
51
51
|
dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
|
52
52
|
dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
53
53
|
dbos/_templates/dbos-db-starter/__package/main.py.dbos,sha256=aQnBPSSQpkB8ERfhf7gB7P9tsU6OPKhZscfeh0yiaD8,2702
|
@@ -67,4 +67,4 @@ dbos/cli/cli.py,sha256=HinoCGrAUTiSeq7AAoCFfhdiE0uDw7vLMuDMN1_YTLI,20705
|
|
67
67
|
dbos/dbos-config.schema.json,sha256=CjaspeYmOkx6Ip_pcxtmfXJTn_YGdSx_0pcPBF7KZmo,6060
|
68
68
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
69
69
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
70
|
-
dbos-1.1.
|
70
|
+
dbos-1.1.0a4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|