dbos 0.26.0a19__py3-none-any.whl → 0.26.0a22__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/__init__.py +1 -2
- dbos/_client.py +18 -5
- dbos/_conductor/protocol.py +1 -2
- dbos/_context.py +62 -0
- dbos/_core.py +98 -28
- dbos/_dbos.py +4 -4
- dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +44 -0
- dbos/_registrations.py +1 -1
- dbos/_schemas/system_database.py +3 -1
- dbos/_sys_db.py +280 -71
- dbos/_workflow_commands.py +11 -102
- {dbos-0.26.0a19.dist-info → dbos-0.26.0a22.dist-info}/METADATA +1 -1
- {dbos-0.26.0a19.dist-info → dbos-0.26.0a22.dist-info}/RECORD +16 -15
- {dbos-0.26.0a19.dist-info → dbos-0.26.0a22.dist-info}/WHEEL +0 -0
- {dbos-0.26.0a19.dist-info → dbos-0.26.0a22.dist-info}/entry_points.txt +0 -0
- {dbos-0.26.0a19.dist-info → dbos-0.26.0a22.dist-info}/licenses/LICENSE +0 -0
dbos/_sys_db.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import datetime
|
2
|
+
import json
|
2
3
|
import logging
|
3
4
|
import os
|
4
5
|
import re
|
@@ -64,6 +65,50 @@ WorkflowStatuses = Literal[
|
|
64
65
|
]
|
65
66
|
|
66
67
|
|
68
|
+
class WorkflowStatus:
|
69
|
+
# The workflow ID
|
70
|
+
workflow_id: str
|
71
|
+
# The workflow status. Must be one of ENQUEUED, PENDING, SUCCESS, ERROR, CANCELLED, or RETRIES_EXCEEDED
|
72
|
+
status: str
|
73
|
+
# The name of the workflow function
|
74
|
+
name: str
|
75
|
+
# The name of the workflow's class, if any
|
76
|
+
class_name: Optional[str]
|
77
|
+
# The name with which the workflow's class instance was configured, if any
|
78
|
+
config_name: Optional[str]
|
79
|
+
# The user who ran the workflow, if specified
|
80
|
+
authenticated_user: Optional[str]
|
81
|
+
# The role with which the workflow ran, if specified
|
82
|
+
assumed_role: Optional[str]
|
83
|
+
# All roles which the authenticated user could assume
|
84
|
+
authenticated_roles: Optional[list[str]]
|
85
|
+
# The deserialized workflow input object
|
86
|
+
input: Optional[_serialization.WorkflowInputs]
|
87
|
+
# The workflow's output, if any
|
88
|
+
output: Optional[Any] = None
|
89
|
+
# The error the workflow threw, if any
|
90
|
+
error: Optional[Exception] = None
|
91
|
+
# Workflow start time, as a Unix epoch timestamp in ms
|
92
|
+
created_at: Optional[int]
|
93
|
+
# Last time the workflow status was updated, as a Unix epoch timestamp in ms
|
94
|
+
updated_at: Optional[int]
|
95
|
+
# If this workflow was enqueued, on which queue
|
96
|
+
queue_name: Optional[str]
|
97
|
+
# The executor to most recently executed this workflow
|
98
|
+
executor_id: Optional[str]
|
99
|
+
# The application version on which this workflow was started
|
100
|
+
app_version: Optional[str]
|
101
|
+
|
102
|
+
# INTERNAL FIELDS
|
103
|
+
|
104
|
+
# The ID of the application executing this workflow
|
105
|
+
app_id: Optional[str]
|
106
|
+
# The number of times this workflow's execution has been attempted
|
107
|
+
recovery_attempts: Optional[int]
|
108
|
+
# The HTTP request that triggered the workflow, if known
|
109
|
+
request: Optional[str]
|
110
|
+
|
111
|
+
|
67
112
|
class WorkflowStatusInternal(TypedDict):
|
68
113
|
workflow_uuid: str
|
69
114
|
status: WorkflowStatuses
|
@@ -83,6 +128,11 @@ class WorkflowStatusInternal(TypedDict):
|
|
83
128
|
app_version: Optional[str]
|
84
129
|
app_id: Optional[str]
|
85
130
|
recovery_attempts: Optional[int]
|
131
|
+
# The start-to-close timeout of the workflow in ms
|
132
|
+
workflow_timeout_ms: Optional[int]
|
133
|
+
# The deadline of a workflow, computed by adding its timeout to its start time.
|
134
|
+
# Deadlines propagate to children. When the deadline is reached, the workflow is cancelled.
|
135
|
+
workflow_deadline_epoch_ms: Optional[int]
|
86
136
|
|
87
137
|
|
88
138
|
class RecordedResult(TypedDict):
|
@@ -148,11 +198,6 @@ class GetQueuedWorkflowsInput(TypedDict):
|
|
148
198
|
sort_desc: Optional[bool] # Sort by created_at in DESC or ASC order
|
149
199
|
|
150
200
|
|
151
|
-
class GetWorkflowsOutput:
|
152
|
-
def __init__(self, workflow_uuids: List[str]):
|
153
|
-
self.workflow_uuids = workflow_uuids
|
154
|
-
|
155
|
-
|
156
201
|
class GetPendingWorkflowsOutput:
|
157
202
|
def __init__(self, *, workflow_uuid: str, queue_name: Optional[str] = None):
|
158
203
|
self.workflow_uuid: str = workflow_uuid
|
@@ -287,11 +332,12 @@ class SystemDatabase:
|
|
287
332
|
status: WorkflowStatusInternal,
|
288
333
|
conn: sa.Connection,
|
289
334
|
*,
|
290
|
-
max_recovery_attempts: int
|
291
|
-
) -> WorkflowStatuses:
|
335
|
+
max_recovery_attempts: Optional[int],
|
336
|
+
) -> tuple[WorkflowStatuses, Optional[int]]:
|
292
337
|
if self._debug_mode:
|
293
338
|
raise Exception("called insert_workflow_status in debug mode")
|
294
339
|
wf_status: WorkflowStatuses = status["status"]
|
340
|
+
workflow_deadline_epoch_ms: Optional[int] = status["workflow_deadline_epoch_ms"]
|
295
341
|
|
296
342
|
cmd = (
|
297
343
|
pg.insert(SystemSchema.workflow_status)
|
@@ -314,6 +360,8 @@ class SystemDatabase:
|
|
314
360
|
recovery_attempts=(
|
315
361
|
1 if wf_status != WorkflowStatusString.ENQUEUED.value else 0
|
316
362
|
),
|
363
|
+
workflow_timeout_ms=status["workflow_timeout_ms"],
|
364
|
+
workflow_deadline_epoch_ms=status["workflow_deadline_epoch_ms"],
|
317
365
|
)
|
318
366
|
.on_conflict_do_update(
|
319
367
|
index_elements=["workflow_uuid"],
|
@@ -327,7 +375,7 @@ class SystemDatabase:
|
|
327
375
|
)
|
328
376
|
)
|
329
377
|
|
330
|
-
cmd = cmd.returning(SystemSchema.workflow_status.c.recovery_attempts, SystemSchema.workflow_status.c.status, SystemSchema.workflow_status.c.name, SystemSchema.workflow_status.c.class_name, SystemSchema.workflow_status.c.config_name, SystemSchema.workflow_status.c.queue_name) # type: ignore
|
378
|
+
cmd = cmd.returning(SystemSchema.workflow_status.c.recovery_attempts, SystemSchema.workflow_status.c.status, SystemSchema.workflow_status.c.workflow_deadline_epoch_ms, SystemSchema.workflow_status.c.name, SystemSchema.workflow_status.c.class_name, SystemSchema.workflow_status.c.config_name, SystemSchema.workflow_status.c.queue_name) # type: ignore
|
331
379
|
|
332
380
|
results = conn.execute(cmd)
|
333
381
|
|
@@ -337,24 +385,29 @@ class SystemDatabase:
|
|
337
385
|
# A mismatch indicates a workflow starting with the same UUID but different functions, which would throw an exception.
|
338
386
|
recovery_attempts: int = row[0]
|
339
387
|
wf_status = row[1]
|
388
|
+
workflow_deadline_epoch_ms = row[2]
|
340
389
|
err_msg: Optional[str] = None
|
341
|
-
if row[
|
342
|
-
err_msg = f"Workflow already exists with a different function name: {row[
|
343
|
-
elif row[
|
344
|
-
err_msg = f"Workflow already exists with a different class name: {row[
|
345
|
-
elif row[
|
346
|
-
err_msg = f"Workflow already exists with a different config name: {row[
|
347
|
-
elif row[
|
390
|
+
if row[3] != status["name"]:
|
391
|
+
err_msg = f"Workflow already exists with a different function name: {row[3]}, but the provided function name is: {status['name']}"
|
392
|
+
elif row[4] != status["class_name"]:
|
393
|
+
err_msg = f"Workflow already exists with a different class name: {row[4]}, but the provided class name is: {status['class_name']}"
|
394
|
+
elif row[5] != status["config_name"]:
|
395
|
+
err_msg = f"Workflow already exists with a different config name: {row[5]}, but the provided config name is: {status['config_name']}"
|
396
|
+
elif row[6] != status["queue_name"]:
|
348
397
|
# This is a warning because a different queue name is not necessarily an error.
|
349
398
|
dbos_logger.warning(
|
350
|
-
f"Workflow already exists in queue: {row[
|
399
|
+
f"Workflow already exists in queue: {row[6]}, but the provided queue name is: {status['queue_name']}. The queue is not updated."
|
351
400
|
)
|
352
401
|
if err_msg is not None:
|
353
402
|
raise DBOSConflictingWorkflowError(status["workflow_uuid"], err_msg)
|
354
403
|
|
355
404
|
# Every time we start executing a workflow (and thus attempt to insert its status), we increment `recovery_attempts` by 1.
|
356
405
|
# When this number becomes equal to `maxRetries + 1`, we mark the workflow as `RETRIES_EXCEEDED`.
|
357
|
-
if
|
406
|
+
if (
|
407
|
+
(wf_status != "SUCCESS" and wf_status != "ERROR")
|
408
|
+
and max_recovery_attempts is not None
|
409
|
+
and recovery_attempts > max_recovery_attempts + 1
|
410
|
+
):
|
358
411
|
delete_cmd = sa.delete(SystemSchema.workflow_queue).where(
|
359
412
|
SystemSchema.workflow_queue.c.workflow_uuid
|
360
413
|
== status["workflow_uuid"]
|
@@ -383,7 +436,7 @@ class SystemDatabase:
|
|
383
436
|
status["workflow_uuid"], max_recovery_attempts
|
384
437
|
)
|
385
438
|
|
386
|
-
return wf_status
|
439
|
+
return wf_status, workflow_deadline_epoch_ms
|
387
440
|
|
388
441
|
def update_workflow_status(
|
389
442
|
self,
|
@@ -441,6 +494,18 @@ class SystemDatabase:
|
|
441
494
|
if self._debug_mode:
|
442
495
|
raise Exception("called cancel_workflow in debug mode")
|
443
496
|
with self.engine.begin() as c:
|
497
|
+
# Check the status of the workflow. If it is complete, do nothing.
|
498
|
+
row = c.execute(
|
499
|
+
sa.select(
|
500
|
+
SystemSchema.workflow_status.c.status,
|
501
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
502
|
+
).fetchone()
|
503
|
+
if (
|
504
|
+
row is None
|
505
|
+
or row[0] == WorkflowStatusString.SUCCESS.value
|
506
|
+
or row[0] == WorkflowStatusString.ERROR.value
|
507
|
+
):
|
508
|
+
return
|
444
509
|
# Remove the workflow from the queues table so it does not block the table
|
445
510
|
c.execute(
|
446
511
|
sa.delete(SystemSchema.workflow_queue).where(
|
@@ -487,11 +552,15 @@ class SystemDatabase:
|
|
487
552
|
queue_name=INTERNAL_QUEUE_NAME,
|
488
553
|
)
|
489
554
|
)
|
490
|
-
# Set the workflow's status to ENQUEUED and clear its recovery attempts.
|
555
|
+
# Set the workflow's status to ENQUEUED and clear its recovery attempts and deadline.
|
491
556
|
c.execute(
|
492
557
|
sa.update(SystemSchema.workflow_status)
|
493
558
|
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
494
|
-
.values(
|
559
|
+
.values(
|
560
|
+
status=WorkflowStatusString.ENQUEUED.value,
|
561
|
+
recovery_attempts=0,
|
562
|
+
workflow_deadline_epoch_ms=None,
|
563
|
+
)
|
495
564
|
)
|
496
565
|
|
497
566
|
def get_max_function_id(self, workflow_uuid: str) -> Optional[int]:
|
@@ -604,6 +673,8 @@ class SystemDatabase:
|
|
604
673
|
SystemSchema.workflow_status.c.updated_at,
|
605
674
|
SystemSchema.workflow_status.c.application_version,
|
606
675
|
SystemSchema.workflow_status.c.application_id,
|
676
|
+
SystemSchema.workflow_status.c.workflow_deadline_epoch_ms,
|
677
|
+
SystemSchema.workflow_status.c.workflow_timeout_ms,
|
607
678
|
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
|
608
679
|
).fetchone()
|
609
680
|
if row is None:
|
@@ -627,6 +698,8 @@ class SystemDatabase:
|
|
627
698
|
"updated_at": row[12],
|
628
699
|
"app_version": row[13],
|
629
700
|
"app_id": row[14],
|
701
|
+
"workflow_deadline_epoch_ms": row[15],
|
702
|
+
"workflow_timeout_ms": row[16],
|
630
703
|
}
|
631
704
|
return status
|
632
705
|
|
@@ -702,8 +775,37 @@ class SystemDatabase:
|
|
702
775
|
)
|
703
776
|
return inputs
|
704
777
|
|
705
|
-
def get_workflows(
|
706
|
-
|
778
|
+
def get_workflows(
|
779
|
+
self, input: GetWorkflowsInput, get_request: bool = False
|
780
|
+
) -> List[WorkflowStatus]:
|
781
|
+
"""
|
782
|
+
Retrieve a list of workflows result and inputs based on the input criteria. The result is a list of external-facing workflow status objects.
|
783
|
+
"""
|
784
|
+
query = sa.select(
|
785
|
+
SystemSchema.workflow_status.c.workflow_uuid,
|
786
|
+
SystemSchema.workflow_status.c.status,
|
787
|
+
SystemSchema.workflow_status.c.name,
|
788
|
+
SystemSchema.workflow_status.c.request,
|
789
|
+
SystemSchema.workflow_status.c.recovery_attempts,
|
790
|
+
SystemSchema.workflow_status.c.config_name,
|
791
|
+
SystemSchema.workflow_status.c.class_name,
|
792
|
+
SystemSchema.workflow_status.c.authenticated_user,
|
793
|
+
SystemSchema.workflow_status.c.authenticated_roles,
|
794
|
+
SystemSchema.workflow_status.c.assumed_role,
|
795
|
+
SystemSchema.workflow_status.c.queue_name,
|
796
|
+
SystemSchema.workflow_status.c.executor_id,
|
797
|
+
SystemSchema.workflow_status.c.created_at,
|
798
|
+
SystemSchema.workflow_status.c.updated_at,
|
799
|
+
SystemSchema.workflow_status.c.application_version,
|
800
|
+
SystemSchema.workflow_status.c.application_id,
|
801
|
+
SystemSchema.workflow_inputs.c.inputs,
|
802
|
+
SystemSchema.workflow_status.c.output,
|
803
|
+
SystemSchema.workflow_status.c.error,
|
804
|
+
).join(
|
805
|
+
SystemSchema.workflow_inputs,
|
806
|
+
SystemSchema.workflow_status.c.workflow_uuid
|
807
|
+
== SystemSchema.workflow_inputs.c.workflow_uuid,
|
808
|
+
)
|
707
809
|
if input.sort_desc:
|
708
810
|
query = query.order_by(SystemSchema.workflow_status.c.created_at.desc())
|
709
811
|
else:
|
@@ -749,18 +851,76 @@ class SystemDatabase:
|
|
749
851
|
|
750
852
|
with self.engine.begin() as c:
|
751
853
|
rows = c.execute(query)
|
752
|
-
workflow_ids = [row[0] for row in rows]
|
753
854
|
|
754
|
-
|
855
|
+
infos: List[WorkflowStatus] = []
|
856
|
+
for row in rows:
|
857
|
+
info = WorkflowStatus()
|
858
|
+
info.workflow_id = row[0]
|
859
|
+
info.status = row[1]
|
860
|
+
info.name = row[2]
|
861
|
+
info.request = row[3] if get_request else None
|
862
|
+
info.recovery_attempts = row[4]
|
863
|
+
info.config_name = row[5]
|
864
|
+
info.class_name = row[6]
|
865
|
+
info.authenticated_user = row[7]
|
866
|
+
info.authenticated_roles = (
|
867
|
+
json.loads(row[8]) if row[8] is not None else None
|
868
|
+
)
|
869
|
+
info.assumed_role = row[9]
|
870
|
+
info.queue_name = row[10]
|
871
|
+
info.executor_id = row[11]
|
872
|
+
info.created_at = row[12]
|
873
|
+
info.updated_at = row[13]
|
874
|
+
info.app_version = row[14]
|
875
|
+
info.app_id = row[15]
|
876
|
+
|
877
|
+
inputs = _serialization.deserialize_args(row[16])
|
878
|
+
if inputs is not None:
|
879
|
+
info.input = inputs
|
880
|
+
if info.status == WorkflowStatusString.SUCCESS.value:
|
881
|
+
info.output = _serialization.deserialize(row[17])
|
882
|
+
elif info.status == WorkflowStatusString.ERROR.value:
|
883
|
+
info.error = _serialization.deserialize_exception(row[18])
|
884
|
+
|
885
|
+
infos.append(info)
|
886
|
+
return infos
|
755
887
|
|
756
888
|
def get_queued_workflows(
|
757
|
-
self, input: GetQueuedWorkflowsInput
|
758
|
-
) ->
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
889
|
+
self, input: GetQueuedWorkflowsInput, get_request: bool = False
|
890
|
+
) -> List[WorkflowStatus]:
|
891
|
+
"""
|
892
|
+
Retrieve a list of queued workflows result and inputs based on the input criteria. The result is a list of external-facing workflow status objects.
|
893
|
+
"""
|
894
|
+
query = sa.select(
|
895
|
+
SystemSchema.workflow_status.c.workflow_uuid,
|
896
|
+
SystemSchema.workflow_status.c.status,
|
897
|
+
SystemSchema.workflow_status.c.name,
|
898
|
+
SystemSchema.workflow_status.c.request,
|
899
|
+
SystemSchema.workflow_status.c.recovery_attempts,
|
900
|
+
SystemSchema.workflow_status.c.config_name,
|
901
|
+
SystemSchema.workflow_status.c.class_name,
|
902
|
+
SystemSchema.workflow_status.c.authenticated_user,
|
903
|
+
SystemSchema.workflow_status.c.authenticated_roles,
|
904
|
+
SystemSchema.workflow_status.c.assumed_role,
|
905
|
+
SystemSchema.workflow_status.c.queue_name,
|
906
|
+
SystemSchema.workflow_status.c.executor_id,
|
907
|
+
SystemSchema.workflow_status.c.created_at,
|
908
|
+
SystemSchema.workflow_status.c.updated_at,
|
909
|
+
SystemSchema.workflow_status.c.application_version,
|
910
|
+
SystemSchema.workflow_status.c.application_id,
|
911
|
+
SystemSchema.workflow_inputs.c.inputs,
|
912
|
+
SystemSchema.workflow_status.c.output,
|
913
|
+
SystemSchema.workflow_status.c.error,
|
914
|
+
).select_from(
|
915
|
+
SystemSchema.workflow_queue.join(
|
916
|
+
SystemSchema.workflow_status,
|
917
|
+
SystemSchema.workflow_queue.c.workflow_uuid
|
918
|
+
== SystemSchema.workflow_status.c.workflow_uuid,
|
919
|
+
).join(
|
920
|
+
SystemSchema.workflow_inputs,
|
921
|
+
SystemSchema.workflow_queue.c.workflow_uuid
|
922
|
+
== SystemSchema.workflow_inputs.c.workflow_uuid,
|
923
|
+
)
|
764
924
|
)
|
765
925
|
if input["sort_desc"]:
|
766
926
|
query = query.order_by(SystemSchema.workflow_status.c.created_at.desc())
|
@@ -797,9 +957,40 @@ class SystemDatabase:
|
|
797
957
|
|
798
958
|
with self.engine.begin() as c:
|
799
959
|
rows = c.execute(query)
|
800
|
-
workflow_uuids = [row[0] for row in rows]
|
801
960
|
|
802
|
-
|
961
|
+
infos: List[WorkflowStatus] = []
|
962
|
+
for row in rows:
|
963
|
+
info = WorkflowStatus()
|
964
|
+
info.workflow_id = row[0]
|
965
|
+
info.status = row[1]
|
966
|
+
info.name = row[2]
|
967
|
+
info.request = row[3] if get_request else None
|
968
|
+
info.recovery_attempts = row[4]
|
969
|
+
info.config_name = row[5]
|
970
|
+
info.class_name = row[6]
|
971
|
+
info.authenticated_user = row[7]
|
972
|
+
info.authenticated_roles = (
|
973
|
+
json.loads(row[8]) if row[8] is not None else None
|
974
|
+
)
|
975
|
+
info.assumed_role = row[9]
|
976
|
+
info.queue_name = row[10]
|
977
|
+
info.executor_id = row[11]
|
978
|
+
info.created_at = row[12]
|
979
|
+
info.updated_at = row[13]
|
980
|
+
info.app_version = row[14]
|
981
|
+
info.app_id = row[15]
|
982
|
+
|
983
|
+
inputs = _serialization.deserialize_args(row[16])
|
984
|
+
if inputs is not None:
|
985
|
+
info.input = inputs
|
986
|
+
if info.status == WorkflowStatusString.SUCCESS.value:
|
987
|
+
info.output = _serialization.deserialize(row[17])
|
988
|
+
elif info.status == WorkflowStatusString.ERROR.value:
|
989
|
+
info.error = _serialization.deserialize_exception(row[18])
|
990
|
+
|
991
|
+
infos.append(info)
|
992
|
+
|
993
|
+
return infos
|
803
994
|
|
804
995
|
def get_pending_workflows(
|
805
996
|
self, executor_id: str, app_version: str
|
@@ -938,50 +1129,56 @@ class SystemDatabase:
|
|
938
1129
|
*,
|
939
1130
|
conn: Optional[sa.Connection] = None,
|
940
1131
|
) -> Optional[RecordedResult]:
|
941
|
-
#
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
.
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
SystemSchema.workflow_status.c.workflow_uuid
|
955
|
-
== SystemSchema.operation_outputs.c.workflow_uuid
|
956
|
-
)
|
957
|
-
& (SystemSchema.operation_outputs.c.function_id == function_id),
|
958
|
-
)
|
959
|
-
)
|
960
|
-
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
1132
|
+
# First query: Retrieve the workflow status
|
1133
|
+
workflow_status_sql = sa.select(
|
1134
|
+
SystemSchema.workflow_status.c.status,
|
1135
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
1136
|
+
|
1137
|
+
# Second query: Retrieve operation outputs if they exist
|
1138
|
+
operation_output_sql = sa.select(
|
1139
|
+
SystemSchema.operation_outputs.c.output,
|
1140
|
+
SystemSchema.operation_outputs.c.error,
|
1141
|
+
SystemSchema.operation_outputs.c.function_name,
|
1142
|
+
).where(
|
1143
|
+
(SystemSchema.operation_outputs.c.workflow_uuid == workflow_id)
|
1144
|
+
& (SystemSchema.operation_outputs.c.function_id == function_id)
|
961
1145
|
)
|
962
|
-
|
963
|
-
|
1146
|
+
|
1147
|
+
# Execute both queries
|
964
1148
|
if conn is not None:
|
965
|
-
|
1149
|
+
workflow_status_rows = conn.execute(workflow_status_sql).all()
|
1150
|
+
operation_output_rows = conn.execute(operation_output_sql).all()
|
966
1151
|
else:
|
967
1152
|
with self.engine.begin() as c:
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
1153
|
+
workflow_status_rows = c.execute(workflow_status_sql).all()
|
1154
|
+
operation_output_rows = c.execute(operation_output_sql).all()
|
1155
|
+
|
1156
|
+
# Check if the workflow exists
|
1157
|
+
assert (
|
1158
|
+
len(workflow_status_rows) > 0
|
1159
|
+
), f"Error: Workflow {workflow_id} does not exist"
|
1160
|
+
|
1161
|
+
# Get workflow status
|
1162
|
+
workflow_status = workflow_status_rows[0][0]
|
1163
|
+
|
976
1164
|
# If the workflow is cancelled, raise the exception
|
977
1165
|
if workflow_status == WorkflowStatusString.CANCELLED.value:
|
978
1166
|
raise DBOSWorkflowCancelledError(
|
979
1167
|
f"Workflow {workflow_id} is cancelled. Aborting function."
|
980
1168
|
)
|
981
|
-
|
982
|
-
|
1169
|
+
|
1170
|
+
# If there are no operation outputs, return None
|
1171
|
+
if not operation_output_rows:
|
983
1172
|
return None
|
984
|
-
|
1173
|
+
|
1174
|
+
# Extract operation output data
|
1175
|
+
output, error, recorded_function_name = (
|
1176
|
+
operation_output_rows[0][0],
|
1177
|
+
operation_output_rows[0][1],
|
1178
|
+
operation_output_rows[0][2],
|
1179
|
+
)
|
1180
|
+
|
1181
|
+
# If the provided and recorded function name are different, throw an exception
|
985
1182
|
if function_name != recorded_function_name:
|
986
1183
|
raise DBOSUnexpectedStepError(
|
987
1184
|
workflow_id=workflow_id,
|
@@ -989,6 +1186,7 @@ class SystemDatabase:
|
|
989
1186
|
expected_name=function_name,
|
990
1187
|
recorded_name=recorded_function_name,
|
991
1188
|
)
|
1189
|
+
|
992
1190
|
result: RecordedResult = {
|
993
1191
|
"output": output,
|
994
1192
|
"error": error,
|
@@ -1537,6 +1735,17 @@ class SystemDatabase:
|
|
1537
1735
|
status=WorkflowStatusString.PENDING.value,
|
1538
1736
|
application_version=app_version,
|
1539
1737
|
executor_id=executor_id,
|
1738
|
+
# If a timeout is set, set the deadline on dequeue
|
1739
|
+
workflow_deadline_epoch_ms=sa.case(
|
1740
|
+
(
|
1741
|
+
SystemSchema.workflow_status.c.workflow_timeout_ms.isnot(
|
1742
|
+
None
|
1743
|
+
),
|
1744
|
+
sa.func.extract("epoch", sa.func.now()) * 1000
|
1745
|
+
+ SystemSchema.workflow_status.c.workflow_timeout_ms,
|
1746
|
+
),
|
1747
|
+
else_=SystemSchema.workflow_status.c.workflow_deadline_epoch_ms,
|
1748
|
+
),
|
1540
1749
|
)
|
1541
1750
|
)
|
1542
1751
|
if res.rowcount > 0:
|
@@ -1658,13 +1867,13 @@ class SystemDatabase:
|
|
1658
1867
|
status: WorkflowStatusInternal,
|
1659
1868
|
inputs: str,
|
1660
1869
|
*,
|
1661
|
-
max_recovery_attempts: int
|
1662
|
-
) -> WorkflowStatuses:
|
1870
|
+
max_recovery_attempts: Optional[int],
|
1871
|
+
) -> tuple[WorkflowStatuses, Optional[int]]:
|
1663
1872
|
"""
|
1664
1873
|
Synchronously record the status and inputs for workflows in a single transaction
|
1665
1874
|
"""
|
1666
1875
|
with self.engine.begin() as conn:
|
1667
|
-
wf_status = self.insert_workflow_status(
|
1876
|
+
wf_status, workflow_deadline_epoch_ms = self.insert_workflow_status(
|
1668
1877
|
status, conn, max_recovery_attempts=max_recovery_attempts
|
1669
1878
|
)
|
1670
1879
|
# TODO: Modify the inputs if they were changed by `update_workflow_inputs`
|
@@ -1675,7 +1884,7 @@ class SystemDatabase:
|
|
1675
1884
|
and wf_status == WorkflowStatusString.ENQUEUED.value
|
1676
1885
|
):
|
1677
1886
|
self.enqueue(status["workflow_uuid"], status["queue_name"], conn)
|
1678
|
-
return wf_status
|
1887
|
+
return wf_status, workflow_deadline_epoch_ms
|
1679
1888
|
|
1680
1889
|
|
1681
1890
|
def reset_system_database(config: ConfigFile) -> None:
|
dbos/_workflow_commands.py
CHANGED
@@ -1,64 +1,18 @@
|
|
1
|
-
import json
|
2
1
|
import uuid
|
3
|
-
from typing import
|
2
|
+
from typing import List, Optional
|
4
3
|
|
5
4
|
from dbos._error import DBOSException
|
6
5
|
|
7
|
-
from . import _serialization
|
8
6
|
from ._app_db import ApplicationDatabase
|
9
7
|
from ._sys_db import (
|
10
8
|
GetQueuedWorkflowsInput,
|
11
9
|
GetWorkflowsInput,
|
12
|
-
GetWorkflowsOutput,
|
13
10
|
StepInfo,
|
14
11
|
SystemDatabase,
|
12
|
+
WorkflowStatus,
|
15
13
|
)
|
16
14
|
|
17
15
|
|
18
|
-
class WorkflowStatus:
|
19
|
-
# The workflow ID
|
20
|
-
workflow_id: str
|
21
|
-
# The workflow status. Must be one of ENQUEUED, PENDING, SUCCESS, ERROR, CANCELLED, or RETRIES_EXCEEDED
|
22
|
-
status: str
|
23
|
-
# The name of the workflow function
|
24
|
-
name: str
|
25
|
-
# The name of the workflow's class, if any
|
26
|
-
class_name: Optional[str]
|
27
|
-
# The name with which the workflow's class instance was configured, if any
|
28
|
-
config_name: Optional[str]
|
29
|
-
# The user who ran the workflow, if specified
|
30
|
-
authenticated_user: Optional[str]
|
31
|
-
# The role with which the workflow ran, if specified
|
32
|
-
assumed_role: Optional[str]
|
33
|
-
# All roles which the authenticated user could assume
|
34
|
-
authenticated_roles: Optional[list[str]]
|
35
|
-
# The deserialized workflow input object
|
36
|
-
input: Optional[_serialization.WorkflowInputs]
|
37
|
-
# The workflow's output, if any
|
38
|
-
output: Optional[Any] = None
|
39
|
-
# The error the workflow threw, if any
|
40
|
-
error: Optional[Exception] = None
|
41
|
-
# Workflow start time, as a Unix epoch timestamp in ms
|
42
|
-
created_at: Optional[int]
|
43
|
-
# Last time the workflow status was updated, as a Unix epoch timestamp in ms
|
44
|
-
updated_at: Optional[int]
|
45
|
-
# If this workflow was enqueued, on which queue
|
46
|
-
queue_name: Optional[str]
|
47
|
-
# The executor to most recently executed this workflow
|
48
|
-
executor_id: Optional[str]
|
49
|
-
# The application version on which this workflow was started
|
50
|
-
app_version: Optional[str]
|
51
|
-
|
52
|
-
# INTERNAL FIELDS
|
53
|
-
|
54
|
-
# The ID of the application executing this workflow
|
55
|
-
app_id: Optional[str]
|
56
|
-
# The number of times this workflow's execution has been attempted
|
57
|
-
recovery_attempts: Optional[int]
|
58
|
-
# The HTTP request that triggered the workflow, if known
|
59
|
-
request: Optional[str]
|
60
|
-
|
61
|
-
|
62
16
|
def list_workflows(
|
63
17
|
sys_db: SystemDatabase,
|
64
18
|
*,
|
@@ -88,12 +42,8 @@ def list_workflows(
|
|
88
42
|
input.sort_desc = sort_desc
|
89
43
|
input.workflow_id_prefix = workflow_id_prefix
|
90
44
|
|
91
|
-
|
92
|
-
|
93
|
-
for workflow_id in output.workflow_uuids:
|
94
|
-
info = get_workflow(sys_db, workflow_id, request) # Call the method for each ID
|
95
|
-
if info is not None:
|
96
|
-
infos.append(info)
|
45
|
+
infos: List[WorkflowStatus] = sys_db.get_workflows(input, request)
|
46
|
+
|
97
47
|
return infos
|
98
48
|
|
99
49
|
|
@@ -120,63 +70,22 @@ def list_queued_workflows(
|
|
120
70
|
"offset": offset,
|
121
71
|
"sort_desc": sort_desc,
|
122
72
|
}
|
123
|
-
|
124
|
-
infos: List[WorkflowStatus] =
|
125
|
-
for workflow_id in output.workflow_uuids:
|
126
|
-
info = get_workflow(sys_db, workflow_id, request) # Call the method for each ID
|
127
|
-
if info is not None:
|
128
|
-
infos.append(info)
|
73
|
+
|
74
|
+
infos: List[WorkflowStatus] = sys_db.get_queued_workflows(input, request)
|
129
75
|
return infos
|
130
76
|
|
131
77
|
|
132
78
|
def get_workflow(
|
133
79
|
sys_db: SystemDatabase, workflow_id: str, get_request: bool
|
134
80
|
) -> Optional[WorkflowStatus]:
|
81
|
+
input = GetWorkflowsInput()
|
82
|
+
input.workflow_ids = [workflow_id]
|
135
83
|
|
136
|
-
|
137
|
-
if
|
84
|
+
infos: List[WorkflowStatus] = sys_db.get_workflows(input, get_request)
|
85
|
+
if not infos:
|
138
86
|
return None
|
139
87
|
|
140
|
-
|
141
|
-
|
142
|
-
info.workflow_id = workflow_id
|
143
|
-
info.status = internal_status["status"]
|
144
|
-
info.name = internal_status["name"]
|
145
|
-
info.class_name = internal_status["class_name"]
|
146
|
-
info.config_name = internal_status["config_name"]
|
147
|
-
info.authenticated_user = internal_status["authenticated_user"]
|
148
|
-
info.assumed_role = internal_status["assumed_role"]
|
149
|
-
info.authenticated_roles = (
|
150
|
-
json.loads(internal_status["authenticated_roles"])
|
151
|
-
if internal_status["authenticated_roles"] is not None
|
152
|
-
else None
|
153
|
-
)
|
154
|
-
info.request = internal_status["request"]
|
155
|
-
info.created_at = internal_status["created_at"]
|
156
|
-
info.updated_at = internal_status["updated_at"]
|
157
|
-
info.queue_name = internal_status["queue_name"]
|
158
|
-
info.executor_id = internal_status["executor_id"]
|
159
|
-
info.app_version = internal_status["app_version"]
|
160
|
-
info.app_id = internal_status["app_id"]
|
161
|
-
info.recovery_attempts = internal_status["recovery_attempts"]
|
162
|
-
|
163
|
-
input_data = sys_db.get_workflow_inputs(workflow_id)
|
164
|
-
if input_data is not None:
|
165
|
-
info.input = input_data
|
166
|
-
|
167
|
-
if internal_status.get("status") == "SUCCESS":
|
168
|
-
result = sys_db.await_workflow_result(workflow_id)
|
169
|
-
info.output = result
|
170
|
-
elif internal_status.get("status") == "ERROR":
|
171
|
-
try:
|
172
|
-
sys_db.await_workflow_result(workflow_id)
|
173
|
-
except Exception as e:
|
174
|
-
info.error = e
|
175
|
-
|
176
|
-
if not get_request:
|
177
|
-
info.request = None
|
178
|
-
|
179
|
-
return info
|
88
|
+
return infos[0]
|
180
89
|
|
181
90
|
|
182
91
|
def list_workflow_steps(
|