dbos 0.26.0a6__tar.gz → 0.26.0a8__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.
Files changed (104) hide show
  1. {dbos-0.26.0a6 → dbos-0.26.0a8}/PKG-INFO +1 -1
  2. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_app_db.py +14 -5
  3. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_conductor/conductor.py +1 -1
  4. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_core.py +12 -20
  5. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_dbos.py +67 -67
  6. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_debug.py +1 -1
  7. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_error.py +13 -0
  8. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_sys_db.py +120 -55
  9. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_workflow_commands.py +3 -0
  10. {dbos-0.26.0a6 → dbos-0.26.0a8}/pyproject.toml +1 -1
  11. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/queuedworkflow.py +1 -1
  12. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_classdecorators.py +8 -8
  13. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_client.py +2 -2
  14. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_dbos.py +65 -8
  15. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_debug.py +2 -2
  16. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_failures.py +81 -38
  17. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_fastapi.py +1 -1
  18. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_flask.py +1 -1
  19. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_queue.py +8 -6
  20. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_scheduler.py +1 -1
  21. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_workflow_introspection.py +34 -0
  22. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_workflow_management.py +2 -0
  23. {dbos-0.26.0a6 → dbos-0.26.0a8}/LICENSE +0 -0
  24. {dbos-0.26.0a6 → dbos-0.26.0a8}/README.md +0 -0
  25. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/__init__.py +0 -0
  26. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/__main__.py +0 -0
  27. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_admin_server.py +0 -0
  28. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_classproperty.py +0 -0
  29. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_client.py +0 -0
  30. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/authentication.py +0 -0
  31. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
  32. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/databases.py +0 -0
  33. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_conductor/protocol.py +0 -0
  34. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_context.py +0 -0
  35. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_croniter.py +0 -0
  36. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_db_wizard.py +0 -0
  37. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_dbos_config.py +0 -0
  38. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_fastapi.py +0 -0
  39. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_flask.py +0 -0
  40. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_kafka.py +0 -0
  41. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_kafka_message.py +0 -0
  42. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_logger.py +0 -0
  43. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/env.py +0 -0
  44. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/script.py.mako +0 -0
  45. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  46. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  47. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  48. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  49. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  50. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  51. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  52. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  53. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_outcome.py +0 -0
  54. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_queue.py +0 -0
  55. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_recovery.py +0 -0
  56. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_registrations.py +0 -0
  57. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_request.py +0 -0
  58. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_roles.py +0 -0
  59. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_scheduler.py +0 -0
  60. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/__init__.py +0 -0
  61. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/application_database.py +0 -0
  62. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/system_database.py +0 -0
  63. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_serialization.py +0 -0
  64. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
  65. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  66. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  67. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  68. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  69. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  70. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  71. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  72. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  73. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  74. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_tracer.py +0 -0
  75. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_utils.py +0 -0
  76. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/_github_init.py +0 -0
  77. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/_template_init.py +0 -0
  78. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/cli.py +0 -0
  79. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/dbos-config.schema.json +0 -0
  80. {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/py.typed +0 -0
  81. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/__init__.py +0 -0
  82. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/atexit_no_ctor.py +0 -0
  83. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/atexit_no_launch.py +0 -0
  84. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/classdefs.py +0 -0
  85. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/client_collateral.py +0 -0
  86. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/client_worker.py +0 -0
  87. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/conftest.py +0 -0
  88. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/more_classdefs.py +0 -0
  89. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_admin_server.py +0 -0
  90. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_async.py +0 -0
  91. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_concurrency.py +0 -0
  92. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_config.py +0 -0
  93. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_croniter.py +0 -0
  94. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_dbwizard.py +0 -0
  95. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_docker_secrets.py +0 -0
  96. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_fastapi_roles.py +0 -0
  97. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_kafka.py +0 -0
  98. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_outcome.py +0 -0
  99. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_package.py +0 -0
  100. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_schema_migration.py +0 -0
  101. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_singleton.py +0 -0
  102. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_spans.py +0 -0
  103. {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_sqlalchemy.py +0 -0
  104. {dbos-0.26.0a6 → dbos-0.26.0a8}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a6
3
+ Version: 0.26.0a8
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker
8
8
 
9
9
  from . import _serialization
10
10
  from ._dbos_config import ConfigFile, DatabaseConfig
11
- from ._error import DBOSWorkflowConflictIDError
11
+ from ._error import DBOSUnexpectedStepError, DBOSWorkflowConflictIDError
12
12
  from ._schemas.application_database import ApplicationSchema
13
13
  from ._sys_db import StepInfo
14
14
 
@@ -171,22 +171,31 @@ class ApplicationDatabase:
171
171
 
172
172
  @staticmethod
173
173
  def check_transaction_execution(
174
- session: Session, workflow_uuid: str, function_id: int
174
+ session: Session, workflow_id: str, function_id: int, function_name: str
175
175
  ) -> Optional[RecordedResult]:
176
176
  rows = session.execute(
177
177
  sa.select(
178
178
  ApplicationSchema.transaction_outputs.c.output,
179
179
  ApplicationSchema.transaction_outputs.c.error,
180
+ ApplicationSchema.transaction_outputs.c.function_name,
180
181
  ).where(
181
- ApplicationSchema.transaction_outputs.c.workflow_uuid == workflow_uuid,
182
+ ApplicationSchema.transaction_outputs.c.workflow_uuid == workflow_id,
182
183
  ApplicationSchema.transaction_outputs.c.function_id == function_id,
183
184
  )
184
185
  ).all()
185
186
  if len(rows) == 0:
186
187
  return None
188
+ output, error, recorded_function_name = rows[0][0], rows[0][1], rows[0][2]
189
+ if function_name != recorded_function_name:
190
+ raise DBOSUnexpectedStepError(
191
+ workflow_id=workflow_id,
192
+ step_id=function_id,
193
+ expected_name=function_name,
194
+ recorded_name=recorded_function_name,
195
+ )
187
196
  result: RecordedResult = {
188
- "output": rows[0][0],
189
- "error": rows[0][1],
197
+ "output": output,
198
+ "error": error,
190
199
  }
191
200
  return result
192
201
 
@@ -67,7 +67,7 @@ class ConductorWebsocket(threading.Thread):
67
67
  recovery_message = p.RecoveryRequest.from_json(message)
68
68
  success = True
69
69
  try:
70
- self.dbos.recover_pending_workflows(
70
+ self.dbos._recover_pending_workflows(
71
71
  recovery_message.executor_ids
72
72
  )
73
73
  except Exception as e:
@@ -52,6 +52,7 @@ from ._error import (
52
52
  DBOSMaxStepRetriesExceeded,
53
53
  DBOSNonExistentWorkflowError,
54
54
  DBOSRecoveryError,
55
+ DBOSUnexpectedStepError,
55
56
  DBOSWorkflowCancelledError,
56
57
  DBOSWorkflowConflictIDError,
57
58
  DBOSWorkflowFunctionNotFoundError,
@@ -783,7 +784,7 @@ def decorate_transaction(
783
784
  ) -> Callable[[F], F]:
784
785
  def decorator(func: F) -> F:
785
786
 
786
- transactionName = func.__qualname__
787
+ transaction_name = func.__qualname__
787
788
 
788
789
  def invoke_tx(*args: Any, **kwargs: Any) -> Any:
789
790
  if dbosreg.dbos is None:
@@ -791,13 +792,14 @@ def decorate_transaction(
791
792
  f"Function {func.__name__} invoked before DBOS initialized"
792
793
  )
793
794
 
795
+ dbos = dbosreg.dbos
794
796
  ctx = assert_current_dbos_context()
795
- if dbosreg.is_workflow_cancelled(ctx.workflow_id):
797
+ status = dbos._sys_db.get_workflow_status(ctx.workflow_id)
798
+ if status and status["status"] == WorkflowStatusString.CANCELLED.value:
796
799
  raise DBOSWorkflowCancelledError(
797
800
  f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {func.__name__}."
798
801
  )
799
802
 
800
- dbos = dbosreg.dbos
801
803
  with dbos._app_db.sessionmaker() as session:
802
804
  attributes: TracedAttributes = {
803
805
  "name": func.__name__,
@@ -813,18 +815,12 @@ def decorate_transaction(
813
815
  "txn_snapshot": "", # TODO: add actual snapshot
814
816
  "executor_id": None,
815
817
  "txn_id": None,
816
- "function_name": transactionName,
818
+ "function_name": transaction_name,
817
819
  }
818
820
  retry_wait_seconds = 0.001
819
821
  backoff_factor = 1.5
820
822
  max_retry_wait_seconds = 2.0
821
823
  while True:
822
-
823
- if dbosreg.is_workflow_cancelled(ctx.workflow_id):
824
- raise DBOSWorkflowCancelledError(
825
- f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {func.__name__}."
826
- )
827
-
828
824
  has_recorded_error = False
829
825
  txn_error: Optional[Exception] = None
830
826
  try:
@@ -841,6 +837,7 @@ def decorate_transaction(
841
837
  session,
842
838
  ctx.workflow_id,
843
839
  ctx.function_id,
840
+ transaction_name,
844
841
  )
845
842
  )
846
843
  if dbos.debug_mode and recorded_output is None:
@@ -904,6 +901,8 @@ def decorate_transaction(
904
901
  )
905
902
  txn_error = invalid_request_error
906
903
  raise
904
+ except DBOSUnexpectedStepError:
905
+ raise
907
906
  except Exception as error:
908
907
  txn_error = error
909
908
  raise
@@ -969,7 +968,7 @@ def decorate_step(
969
968
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
970
969
  def decorator(func: Callable[P, R]) -> Callable[P, R]:
971
970
 
972
- stepName = func.__qualname__
971
+ step_name = func.__qualname__
973
972
 
974
973
  def invoke_step(*args: Any, **kwargs: Any) -> Any:
975
974
  if dbosreg.dbos is None:
@@ -983,13 +982,6 @@ def decorate_step(
983
982
  "operationType": OperationType.STEP.value,
984
983
  }
985
984
 
986
- # Check if the workflow is cancelled
987
- ctx = assert_current_dbos_context()
988
- if dbosreg.is_workflow_cancelled(ctx.workflow_id):
989
- raise DBOSWorkflowCancelledError(
990
- f"Workflow {ctx.workflow_id} is cancelled. Aborting step {func.__name__}."
991
- )
992
-
993
985
  attempts = max_attempts if retries_allowed else 1
994
986
  max_retry_interval_seconds: float = 3600 # 1 Hour
995
987
 
@@ -1017,7 +1009,7 @@ def decorate_step(
1017
1009
  step_output: OperationResultInternal = {
1018
1010
  "workflow_uuid": ctx.workflow_id,
1019
1011
  "function_id": ctx.function_id,
1020
- "function_name": stepName,
1012
+ "function_name": step_name,
1021
1013
  "output": None,
1022
1014
  "error": None,
1023
1015
  }
@@ -1035,7 +1027,7 @@ def decorate_step(
1035
1027
  def check_existing_result() -> Union[NoResult, R]:
1036
1028
  ctx = assert_current_dbos_context()
1037
1029
  recorded_output = dbos._sys_db.check_operation_execution(
1038
- ctx.workflow_id, ctx.function_id
1030
+ ctx.workflow_id, ctx.function_id, step_name
1039
1031
  )
1040
1032
  if dbos.debug_mode and recorded_output is None:
1041
1033
  raise DBOSException("Step output not found in debug mode")
@@ -166,12 +166,17 @@ class DBOSRegistry:
166
166
  self.pollers: list[RegisteredJob] = []
167
167
  self.dbos: Optional[DBOS] = None
168
168
  self.config: Optional[ConfigFile] = None
169
- self.workflow_cancelled_map: dict[str, bool] = {}
170
169
 
171
170
  def register_wf_function(self, name: str, wrapped_func: F, functype: str) -> None:
172
171
  if name in self.function_type_map:
173
172
  if self.function_type_map[name] != functype:
174
173
  raise DBOSConflictingRegistrationError(name)
174
+ if name != TEMP_SEND_WF_NAME:
175
+ # Remove the `<temp>` prefix from the function name to avoid confusion
176
+ truncated_name = name.replace("<temp>.", "")
177
+ dbos_logger.warning(
178
+ f"Duplicate registration of function '{truncated_name}'. A function named '{truncated_name}' has already been registered with DBOS. All functions registered with DBOS must have unique names."
179
+ )
175
180
  self.function_type_map[name] = functype
176
181
  self.workflow_info_map[name] = wrapped_func
177
182
 
@@ -209,15 +214,6 @@ class DBOSRegistry:
209
214
  else:
210
215
  self.instance_info_map[fn] = inst
211
216
 
212
- def cancel_workflow(self, workflow_id: str) -> None:
213
- self.workflow_cancelled_map[workflow_id] = True
214
-
215
- def is_workflow_cancelled(self, workflow_id: str) -> bool:
216
- return self.workflow_cancelled_map.get(workflow_id, False)
217
-
218
- def clear_workflow_cancelled(self, workflow_id: str) -> None:
219
- self.workflow_cancelled_map.pop(workflow_id, None)
220
-
221
217
  def compute_app_version(self) -> str:
222
218
  """
223
219
  An application's version is computed from a hash of the source of its workflows.
@@ -734,32 +730,11 @@ class DBOS:
734
730
  @classmethod
735
731
  def get_workflow_status(cls, workflow_id: str) -> Optional[WorkflowStatus]:
736
732
  """Return the status of a workflow execution."""
737
- sys_db = _get_dbos_instance()._sys_db
738
- ctx = get_local_dbos_context()
739
- if ctx and ctx.is_within_workflow():
740
- ctx.function_id += 1
741
- res = sys_db.check_operation_execution(ctx.workflow_id, ctx.function_id)
742
- if res is not None:
743
- if res["output"]:
744
- resstat: WorkflowStatus = _serialization.deserialize(res["output"])
745
- return resstat
746
- else:
747
- raise DBOSException(
748
- "Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
749
- )
750
- stat = get_workflow(_get_dbos_instance()._sys_db, workflow_id, True)
751
-
752
- if ctx and ctx.is_within_workflow():
753
- sys_db.record_operation_result(
754
- {
755
- "workflow_uuid": ctx.workflow_id,
756
- "function_id": ctx.function_id,
757
- "function_name": "DBOS.getStatus",
758
- "output": _serialization.serialize(stat),
759
- "error": None,
760
- }
761
- )
762
- return stat
733
+
734
+ def fn() -> Optional[WorkflowStatus]:
735
+ return get_workflow(_get_dbos_instance()._sys_db, workflow_id, True)
736
+
737
+ return _get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.getStatus")
763
738
 
764
739
  @classmethod
765
740
  async def get_workflow_status_async(
@@ -935,12 +910,12 @@ class DBOS:
935
910
  )
936
911
 
937
912
  @classmethod
938
- def execute_workflow_id(cls, workflow_id: str) -> WorkflowHandle[Any]:
913
+ def _execute_workflow_id(cls, workflow_id: str) -> WorkflowHandle[Any]:
939
914
  """Execute a workflow by ID (for recovery)."""
940
915
  return execute_workflow_by_id(_get_dbos_instance(), workflow_id)
941
916
 
942
917
  @classmethod
943
- def recover_pending_workflows(
918
+ def _recover_pending_workflows(
944
919
  cls, executor_ids: List[str] = ["local"]
945
920
  ) -> List[WorkflowHandle[Any]]:
946
921
  """Find all PENDING workflows and execute them."""
@@ -949,22 +924,37 @@ class DBOS:
949
924
  @classmethod
950
925
  def cancel_workflow(cls, workflow_id: str) -> None:
951
926
  """Cancel a workflow by ID."""
952
- dbos_logger.info(f"Cancelling workflow: {workflow_id}")
953
- _get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
954
- _get_or_create_dbos_registry().cancel_workflow(workflow_id)
927
+
928
+ def fn() -> None:
929
+ dbos_logger.info(f"Cancelling workflow: {workflow_id}")
930
+ _get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
931
+
932
+ return _get_dbos_instance()._sys_db.call_function_as_step(
933
+ fn, "DBOS.cancelWorkflow"
934
+ )
955
935
 
956
936
  @classmethod
957
937
  def resume_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
958
938
  """Resume a workflow by ID."""
959
- dbos_logger.info(f"Resuming workflow: {workflow_id}")
960
- _get_dbos_instance()._sys_db.resume_workflow(workflow_id)
961
- _get_or_create_dbos_registry().clear_workflow_cancelled(workflow_id)
939
+
940
+ def fn() -> None:
941
+ dbos_logger.info(f"Resuming workflow: {workflow_id}")
942
+ _get_dbos_instance()._sys_db.resume_workflow(workflow_id)
943
+
944
+ _get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.resumeWorkflow")
962
945
  return cls.retrieve_workflow(workflow_id)
963
946
 
964
947
  @classmethod
965
948
  def restart_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
966
949
  """Restart a workflow with a new workflow ID"""
967
- forked_workflow_id = _get_dbos_instance()._sys_db.fork_workflow(workflow_id)
950
+
951
+ def fn() -> str:
952
+ dbos_logger.info(f"Restarting workflow: {workflow_id}")
953
+ return _get_dbos_instance()._sys_db.fork_workflow(workflow_id)
954
+
955
+ forked_workflow_id = _get_dbos_instance()._sys_db.call_function_as_step(
956
+ fn, "DBOS.restartWorkflow"
957
+ )
968
958
  return cls.retrieve_workflow(forked_workflow_id)
969
959
 
970
960
  @classmethod
@@ -982,18 +972,23 @@ class DBOS:
982
972
  offset: Optional[int] = None,
983
973
  sort_desc: bool = False,
984
974
  ) -> List[WorkflowStatus]:
985
- return list_workflows(
986
- _get_dbos_instance()._sys_db,
987
- workflow_ids=workflow_ids,
988
- status=status,
989
- start_time=start_time,
990
- end_time=end_time,
991
- name=name,
992
- app_version=app_version,
993
- user=user,
994
- limit=limit,
995
- offset=offset,
996
- sort_desc=sort_desc,
975
+ def fn() -> List[WorkflowStatus]:
976
+ return list_workflows(
977
+ _get_dbos_instance()._sys_db,
978
+ workflow_ids=workflow_ids,
979
+ status=status,
980
+ start_time=start_time,
981
+ end_time=end_time,
982
+ name=name,
983
+ app_version=app_version,
984
+ user=user,
985
+ limit=limit,
986
+ offset=offset,
987
+ sort_desc=sort_desc,
988
+ )
989
+
990
+ return _get_dbos_instance()._sys_db.call_function_as_step(
991
+ fn, "DBOS.listWorkflows"
997
992
  )
998
993
 
999
994
  @classmethod
@@ -1009,16 +1004,21 @@ class DBOS:
1009
1004
  offset: Optional[int] = None,
1010
1005
  sort_desc: bool = False,
1011
1006
  ) -> List[WorkflowStatus]:
1012
- return list_queued_workflows(
1013
- _get_dbos_instance()._sys_db,
1014
- queue_name=queue_name,
1015
- status=status,
1016
- start_time=start_time,
1017
- end_time=end_time,
1018
- name=name,
1019
- limit=limit,
1020
- offset=offset,
1021
- sort_desc=sort_desc,
1007
+ def fn() -> List[WorkflowStatus]:
1008
+ return list_queued_workflows(
1009
+ _get_dbos_instance()._sys_db,
1010
+ queue_name=queue_name,
1011
+ status=status,
1012
+ start_time=start_time,
1013
+ end_time=end_time,
1014
+ name=name,
1015
+ limit=limit,
1016
+ offset=offset,
1017
+ sort_desc=sort_desc,
1018
+ )
1019
+
1020
+ return _get_dbos_instance()._sys_db.call_function_as_step(
1021
+ fn, "DBOS.listQueuedWorkflows"
1022
1022
  )
1023
1023
 
1024
1024
  @classproperty
@@ -28,7 +28,7 @@ def debug_workflow(workflow_id: str, entrypoint: Union[str, PythonModule]) -> No
28
28
 
29
29
  DBOS.logger.info(f"Debugging workflow {workflow_id}...")
30
30
  DBOS.launch(debug_mode=True)
31
- handle = DBOS.execute_workflow_id(workflow_id)
31
+ handle = DBOS._execute_workflow_id(workflow_id)
32
32
  handle.get_result()
33
33
  DBOS.logger.info("Workflow Debugging complete. Exiting process.")
34
34
 
@@ -37,6 +37,7 @@ class DBOSErrorCode(Enum):
37
37
  NotAuthorized = 8
38
38
  ConflictingWorkflowError = 9
39
39
  WorkflowCancelled = 10
40
+ UnexpectedStep = 11
40
41
  ConflictingRegistrationError = 25
41
42
 
42
43
 
@@ -155,3 +156,15 @@ class DBOSConflictingRegistrationError(DBOSException):
155
156
  f"Operation (Name: {name}) is already registered with a conflicting function type",
156
157
  dbos_error_code=DBOSErrorCode.ConflictingRegistrationError.value,
157
158
  )
159
+
160
+
161
+ class DBOSUnexpectedStepError(DBOSException):
162
+ """Exception raised when a step has an unexpected recorded name."""
163
+
164
+ def __init__(
165
+ self, workflow_id: str, step_id: int, expected_name: str, recorded_name: str
166
+ ) -> None:
167
+ super().__init__(
168
+ f"During execution of workflow {workflow_id} step {step_id}, function {recorded_name} was recorded when {expected_name} was expected. Check that your workflow is deterministic.",
169
+ dbos_error_code=DBOSErrorCode.UnexpectedStep.value,
170
+ )