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/_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 = DEFAULT_MAX_RECOVERY_ATTEMPTS,
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[2] != status["name"]:
342
- err_msg = f"Workflow already exists with a different function name: {row[2]}, but the provided function name is: {status['name']}"
343
- elif row[3] != status["class_name"]:
344
- err_msg = f"Workflow already exists with a different class name: {row[3]}, but the provided class name is: {status['class_name']}"
345
- elif row[4] != status["config_name"]:
346
- err_msg = f"Workflow already exists with a different config name: {row[4]}, but the provided config name is: {status['config_name']}"
347
- elif row[5] != status["queue_name"]:
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[5]}, but the provided queue name is: {status['queue_name']}. The queue is not updated."
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 recovery_attempts > max_recovery_attempts + 1:
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(status=WorkflowStatusString.ENQUEUED.value, recovery_attempts=0)
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(self, input: GetWorkflowsInput) -> GetWorkflowsOutput:
706
- query = sa.select(SystemSchema.workflow_status.c.workflow_uuid)
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
- return GetWorkflowsOutput(workflow_ids)
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
- ) -> GetWorkflowsOutput:
759
-
760
- query = sa.select(SystemSchema.workflow_queue.c.workflow_uuid).join(
761
- SystemSchema.workflow_status,
762
- SystemSchema.workflow_queue.c.workflow_uuid
763
- == SystemSchema.workflow_status.c.workflow_uuid,
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
- return GetWorkflowsOutput(workflow_uuids)
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
- # Retrieve the status of the workflow. Additionally, if this step
942
- # has run before, retrieve its name, output, and error.
943
- sql = (
944
- sa.select(
945
- SystemSchema.workflow_status.c.status,
946
- SystemSchema.operation_outputs.c.output,
947
- SystemSchema.operation_outputs.c.error,
948
- SystemSchema.operation_outputs.c.function_name,
949
- )
950
- .select_from(
951
- SystemSchema.workflow_status.outerjoin(
952
- SystemSchema.operation_outputs,
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
- # If in a transaction, use the provided connection
963
- rows: Sequence[Any]
1146
+
1147
+ # Execute both queries
964
1148
  if conn is not None:
965
- rows = conn.execute(sql).all()
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
- rows = c.execute(sql).all()
969
- assert len(rows) > 0, f"Error: Workflow {workflow_id} does not exist"
970
- workflow_status, output, error, recorded_function_name = (
971
- rows[0][0],
972
- rows[0][1],
973
- rows[0][2],
974
- rows[0][3],
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
- # If there is no row for the function, return None
982
- if recorded_function_name is None:
1169
+
1170
+ # If there are no operation outputs, return None
1171
+ if not operation_output_rows:
983
1172
  return None
984
- # If the provided and recorded function name are different, throw an exception.
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 = DEFAULT_MAX_RECOVERY_ATTEMPTS,
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:
@@ -1,64 +1,18 @@
1
- import json
2
1
  import uuid
3
- from typing import Any, List, Optional
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
- output: GetWorkflowsOutput = sys_db.get_workflows(input)
92
- infos: List[WorkflowStatus] = []
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
- output: GetWorkflowsOutput = sys_db.get_queued_workflows(input)
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
- internal_status = sys_db.get_workflow_status(workflow_id)
137
- if internal_status is None:
84
+ infos: List[WorkflowStatus] = sys_db.get_workflows(input, get_request)
85
+ if not infos:
138
86
  return None
139
87
 
140
- info = WorkflowStatus()
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a19
3
+ Version: 0.26.0a22
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT