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.
- {dbos-0.26.0a6 → dbos-0.26.0a8}/PKG-INFO +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_app_db.py +14 -5
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_conductor/conductor.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_core.py +12 -20
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_dbos.py +67 -67
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_debug.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_error.py +13 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_sys_db.py +120 -55
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_workflow_commands.py +3 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/pyproject.toml +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/queuedworkflow.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_classdecorators.py +8 -8
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_client.py +2 -2
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_dbos.py +65 -8
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_debug.py +2 -2
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_failures.py +81 -38
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_fastapi.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_flask.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_queue.py +8 -6
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_scheduler.py +1 -1
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_workflow_introspection.py +34 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_workflow_management.py +2 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/LICENSE +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/README.md +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/__init__.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/__main__.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_admin_server.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_classproperty.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_client.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_conductor/protocol.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_context.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_croniter.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_db_wizard.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_dbos_config.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_fastapi.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_flask.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_kafka.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_kafka_message.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_logger.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/env.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_outcome.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_queue.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_recovery.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_registrations.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_request.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_roles.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_scheduler.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_serialization.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_tracer.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/_utils.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/_github_init.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/_template_init.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/cli/cli.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/dbos/py.typed +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/__init__.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/atexit_no_launch.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/classdefs.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/client_collateral.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/client_worker.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/conftest.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/more_classdefs.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_admin_server.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_async.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_concurrency.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_config.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_croniter.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_dbwizard.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_docker_secrets.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_kafka.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_outcome.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_package.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_schema_migration.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_singleton.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_spans.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.26.0a6 → dbos-0.26.0a8}/version/__init__.py +0 -0
@@ -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,
|
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 ==
|
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":
|
189
|
-
"error":
|
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.
|
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
|
-
|
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
|
-
|
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":
|
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
|
-
|
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":
|
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
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
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
|
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
|
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
|
-
|
953
|
-
|
954
|
-
|
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
|
-
|
960
|
-
|
961
|
-
|
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
|
-
|
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
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
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
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
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.
|
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
|
+
)
|