dbos 0.20.0a9__tar.gz → 0.21.0__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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- {dbos-0.20.0a9 → dbos-0.21.0}/PKG-INFO +1 -1
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_context.py +0 -30
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_core.py +2 -2
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_dbos.py +8 -6
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_dbos_config.py +17 -13
- dbos-0.21.0/dbos/_recovery.py +70 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_sys_db.py +212 -72
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_workflow_commands.py +51 -39
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/cli/cli.py +87 -19
- {dbos-0.20.0a9 → dbos-0.21.0}/pyproject.toml +1 -1
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_admin_server.py +38 -33
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_dbos.py +53 -91
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_failures.py +22 -45
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_package.py +46 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_queue.py +292 -0
- dbos-0.21.0/tests/test_sqlalchemy.py +113 -0
- dbos-0.21.0/tests/test_workflow_cmds.py +289 -0
- dbos-0.20.0a9/dbos/_recovery.py +0 -53
- dbos-0.20.0a9/tests/test_workflow_cmds.py +0 -216
- {dbos-0.20.0a9 → dbos-0.21.0}/LICENSE +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/README.md +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_admin_server.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_app_db.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_classproperty.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_croniter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_db_wizard.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_error.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_fastapi.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_flask.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_kafka.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_kafka_message.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_logger.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/env.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_outcome.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_registrations.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_request.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_roles.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_scheduler.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_serialization.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/_tracer.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/cli/_github_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/cli/_template_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/dbos/py.typed +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/atexit_no_launch.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/classdefs.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/conftest.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/more_classdefs.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/queuedworkflow.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_async.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_classdecorators.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_concurrency.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_config.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_croniter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_fastapi.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_flask.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_kafka.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_outcome.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_scheduler.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_schema_migration.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_singleton.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/tests/test_spans.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0}/version/__init__.py +0 -0
|
@@ -63,7 +63,6 @@ class DBOSContext:
|
|
|
63
63
|
self.parent_workflow_fid: int = -1
|
|
64
64
|
self.workflow_id: str = ""
|
|
65
65
|
self.function_id: int = -1
|
|
66
|
-
self.in_recovery: bool = False
|
|
67
66
|
|
|
68
67
|
self.curr_step_function_id: int = -1
|
|
69
68
|
self.curr_tx_function_id: int = -1
|
|
@@ -82,7 +81,6 @@ class DBOSContext:
|
|
|
82
81
|
rv.is_within_set_workflow_id_block = self.is_within_set_workflow_id_block
|
|
83
82
|
rv.parent_workflow_id = self.workflow_id
|
|
84
83
|
rv.parent_workflow_fid = self.function_id
|
|
85
|
-
rv.in_recovery = self.in_recovery
|
|
86
84
|
rv.authenticated_user = self.authenticated_user
|
|
87
85
|
rv.authenticated_roles = (
|
|
88
86
|
self.authenticated_roles[:]
|
|
@@ -335,34 +333,6 @@ class SetWorkflowID:
|
|
|
335
333
|
return False # Did not handle
|
|
336
334
|
|
|
337
335
|
|
|
338
|
-
class SetWorkflowRecovery:
|
|
339
|
-
def __init__(self) -> None:
|
|
340
|
-
self.created_ctx = False
|
|
341
|
-
|
|
342
|
-
def __enter__(self) -> SetWorkflowRecovery:
|
|
343
|
-
# Code to create a basic context
|
|
344
|
-
ctx = get_local_dbos_context()
|
|
345
|
-
if ctx is None:
|
|
346
|
-
self.created_ctx = True
|
|
347
|
-
_set_local_dbos_context(DBOSContext())
|
|
348
|
-
assert_current_dbos_context().in_recovery = True
|
|
349
|
-
|
|
350
|
-
return self
|
|
351
|
-
|
|
352
|
-
def __exit__(
|
|
353
|
-
self,
|
|
354
|
-
exc_type: Optional[Type[BaseException]],
|
|
355
|
-
exc_value: Optional[BaseException],
|
|
356
|
-
traceback: Optional[TracebackType],
|
|
357
|
-
) -> Literal[False]:
|
|
358
|
-
assert assert_current_dbos_context().in_recovery == True
|
|
359
|
-
assert_current_dbos_context().in_recovery = False
|
|
360
|
-
# Code to clean up the basic context if we created it
|
|
361
|
-
if self.created_ctx:
|
|
362
|
-
_clear_local_dbos_context()
|
|
363
|
-
return False # Did not handle
|
|
364
|
-
|
|
365
|
-
|
|
366
336
|
class EnterDBOSWorkflow(AbstractContextManager[DBOSContext, Literal[False]]):
|
|
367
337
|
def __init__(self, attributes: TracedAttributes) -> None:
|
|
368
338
|
self.created_ctx = False
|
|
@@ -185,8 +185,8 @@ def _init_workflow(
|
|
|
185
185
|
# Synchronously record the status and inputs for workflows and single-step workflows
|
|
186
186
|
# We also have to do this for single-step workflows because of the foreign key constraint on the operation outputs table
|
|
187
187
|
# TODO: Make this transactional (and with the queue step below)
|
|
188
|
-
wf_status = dbos._sys_db.
|
|
189
|
-
status,
|
|
188
|
+
wf_status = dbos._sys_db.insert_workflow_status(
|
|
189
|
+
status, max_recovery_attempts=max_recovery_attempts
|
|
190
190
|
)
|
|
191
191
|
# TODO: Modify the inputs if they were changed by `update_workflow_inputs`
|
|
192
192
|
dbos._sys_db.update_workflow_inputs(wfid, _serialization.serialize_args(inputs))
|
|
@@ -56,7 +56,7 @@ from ._registrations import (
|
|
|
56
56
|
)
|
|
57
57
|
from ._roles import default_required_roles, required_roles
|
|
58
58
|
from ._scheduler import ScheduledWorkflow, scheduled
|
|
59
|
-
from ._sys_db import
|
|
59
|
+
from ._sys_db import reset_system_database
|
|
60
60
|
from ._tracer import dbos_tracer
|
|
61
61
|
|
|
62
62
|
if TYPE_CHECKING:
|
|
@@ -613,6 +613,7 @@ class DBOS:
|
|
|
613
613
|
workflow_id=workflow_id,
|
|
614
614
|
status=stat["status"],
|
|
615
615
|
name=stat["name"],
|
|
616
|
+
executor_id=stat["executor_id"],
|
|
616
617
|
recovery_attempts=stat["recovery_attempts"],
|
|
617
618
|
class_name=stat["class_name"],
|
|
618
619
|
config_name=stat["config_name"],
|
|
@@ -800,14 +801,13 @@ class DBOS:
|
|
|
800
801
|
@classmethod
|
|
801
802
|
def cancel_workflow(cls, workflow_id: str) -> None:
|
|
802
803
|
"""Cancel a workflow by ID."""
|
|
803
|
-
_get_dbos_instance()._sys_db.
|
|
804
|
-
workflow_id, WorkflowStatusString.CANCELLED, False
|
|
805
|
-
)
|
|
804
|
+
_get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
|
|
806
805
|
|
|
807
806
|
@classmethod
|
|
808
|
-
def resume_workflow(cls, workflow_id: str) ->
|
|
807
|
+
def resume_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
809
808
|
"""Resume a workflow by ID."""
|
|
810
|
-
|
|
809
|
+
_get_dbos_instance()._sys_db.resume_workflow(workflow_id)
|
|
810
|
+
return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
|
|
811
811
|
|
|
812
812
|
@classproperty
|
|
813
813
|
def logger(cls) -> Logger:
|
|
@@ -910,6 +910,7 @@ class WorkflowStatus:
|
|
|
910
910
|
workflow_id(str): The ID of the workflow execution
|
|
911
911
|
status(str): The status of the execution, from `WorkflowStatusString`
|
|
912
912
|
name(str): The workflow function name
|
|
913
|
+
executor_id(str): The ID of the executor running the workflow
|
|
913
914
|
class_name(str): For member functions, the name of the class containing the workflow function
|
|
914
915
|
config_name(str): For instance member functions, the name of the class instance for the execution
|
|
915
916
|
queue_name(str): For workflows that are or were queued, the queue name
|
|
@@ -923,6 +924,7 @@ class WorkflowStatus:
|
|
|
923
924
|
workflow_id: str
|
|
924
925
|
status: str
|
|
925
926
|
name: str
|
|
927
|
+
executor_id: Optional[str]
|
|
926
928
|
class_name: Optional[str]
|
|
927
929
|
config_name: Optional[str]
|
|
928
930
|
queue_name: Optional[str]
|
|
@@ -123,7 +123,10 @@ def get_dbos_database_url(config_file_path: str = DBOS_CONFIG_PATH) -> str:
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def load_config(
|
|
126
|
-
config_file_path: str = DBOS_CONFIG_PATH,
|
|
126
|
+
config_file_path: str = DBOS_CONFIG_PATH,
|
|
127
|
+
*,
|
|
128
|
+
use_db_wizard: bool = True,
|
|
129
|
+
silent: bool = False,
|
|
127
130
|
) -> ConfigFile:
|
|
128
131
|
"""
|
|
129
132
|
Load the DBOS `ConfigFile` from the specified path (typically `dbos-config.yaml`).
|
|
@@ -188,18 +191,19 @@ def load_config(
|
|
|
188
191
|
# Load the DB connection file. Use its values for missing fields from dbos-config.yaml. Use defaults otherwise.
|
|
189
192
|
data = cast(ConfigFile, data)
|
|
190
193
|
db_connection = load_db_connection()
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
if not silent:
|
|
195
|
+
if data["database"].get("hostname"):
|
|
196
|
+
print(
|
|
197
|
+
"[bold blue]Loading database connection parameters from dbos-config.yaml[/bold blue]"
|
|
198
|
+
)
|
|
199
|
+
elif db_connection.get("hostname"):
|
|
200
|
+
print(
|
|
201
|
+
"[bold blue]Loading database connection parameters from .dbos/db_connection[/bold blue]"
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
print(
|
|
205
|
+
"[bold blue]Using default database connection parameters (localhost)[/bold blue]"
|
|
206
|
+
)
|
|
203
207
|
|
|
204
208
|
data["database"]["hostname"] = (
|
|
205
209
|
data["database"].get("hostname") or db_connection.get("hostname") or "localhost"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import TYPE_CHECKING, Any, List
|
|
6
|
+
|
|
7
|
+
from ._core import execute_workflow_by_id
|
|
8
|
+
from ._error import DBOSWorkflowFunctionNotFoundError
|
|
9
|
+
from ._sys_db import GetPendingWorkflowsOutput
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._dbos import DBOS, WorkflowHandle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def startup_recovery_thread(
|
|
16
|
+
dbos: "DBOS", pending_workflows: List[GetPendingWorkflowsOutput]
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Attempt to recover local pending workflows on startup using a background thread."""
|
|
19
|
+
stop_event = threading.Event()
|
|
20
|
+
dbos.stop_events.append(stop_event)
|
|
21
|
+
while not stop_event.is_set() and len(pending_workflows) > 0:
|
|
22
|
+
try:
|
|
23
|
+
for pending_workflow in list(pending_workflows):
|
|
24
|
+
if (
|
|
25
|
+
pending_workflow.queue_name
|
|
26
|
+
and pending_workflow.queue_name != "_dbos_internal_queue"
|
|
27
|
+
):
|
|
28
|
+
dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
29
|
+
continue
|
|
30
|
+
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
31
|
+
pending_workflows.remove(pending_workflow)
|
|
32
|
+
except DBOSWorkflowFunctionNotFoundError:
|
|
33
|
+
time.sleep(1)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
dbos.logger.error(
|
|
36
|
+
f"Exception encountered when recovering workflows: {traceback.format_exc()}"
|
|
37
|
+
)
|
|
38
|
+
raise e
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def recover_pending_workflows(
|
|
42
|
+
dbos: "DBOS", executor_ids: List[str] = ["local"]
|
|
43
|
+
) -> List["WorkflowHandle[Any]"]:
|
|
44
|
+
workflow_handles: List["WorkflowHandle[Any]"] = []
|
|
45
|
+
for executor_id in executor_ids:
|
|
46
|
+
if executor_id == "local" and os.environ.get("DBOS__VMID"):
|
|
47
|
+
dbos.logger.debug(
|
|
48
|
+
f"Skip local recovery because it's running in a VM: {os.environ.get('DBOS__VMID')}"
|
|
49
|
+
)
|
|
50
|
+
dbos.logger.debug(f"Recovering pending workflows for executor: {executor_id}")
|
|
51
|
+
pending_workflows = dbos._sys_db.get_pending_workflows(executor_id)
|
|
52
|
+
for pending_workflow in pending_workflows:
|
|
53
|
+
if (
|
|
54
|
+
pending_workflow.queue_name
|
|
55
|
+
and pending_workflow.queue_name != "_dbos_internal_queue"
|
|
56
|
+
):
|
|
57
|
+
try:
|
|
58
|
+
dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
59
|
+
workflow_handles.append(
|
|
60
|
+
dbos.retrieve_workflow(pending_workflow.workflow_uuid)
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
dbos.logger.error(e)
|
|
64
|
+
else:
|
|
65
|
+
workflow_handles.append(
|
|
66
|
+
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
dbos.logger.info("Recovered pending workflows")
|
|
70
|
+
return workflow_handles
|
|
@@ -126,11 +126,26 @@ class GetWorkflowsInput:
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
class GetQueuedWorkflowsInput(TypedDict):
|
|
130
|
+
queue_name: Optional[str]
|
|
131
|
+
status: Optional[str]
|
|
132
|
+
start_time: Optional[str] # Timestamp in ISO 8601 format
|
|
133
|
+
end_time: Optional[str] # Timestamp in ISO 8601 format
|
|
134
|
+
limit: Optional[int] # Return up to this many workflows IDs.
|
|
135
|
+
name: Optional[str] # The name of the workflow function
|
|
136
|
+
|
|
137
|
+
|
|
129
138
|
class GetWorkflowsOutput:
|
|
130
139
|
def __init__(self, workflow_uuids: List[str]):
|
|
131
140
|
self.workflow_uuids = workflow_uuids
|
|
132
141
|
|
|
133
142
|
|
|
143
|
+
class GetPendingWorkflowsOutput:
|
|
144
|
+
def __init__(self, *, workflow_uuid: str, queue_name: Optional[str] = None):
|
|
145
|
+
self.workflow_uuid: str = workflow_uuid
|
|
146
|
+
self.queue_name: Optional[str] = queue_name
|
|
147
|
+
|
|
148
|
+
|
|
134
149
|
class WorkflowInformation(TypedDict, total=False):
|
|
135
150
|
workflow_uuid: str
|
|
136
151
|
status: WorkflowStatuses # The status of the workflow.
|
|
@@ -243,66 +258,50 @@ class SystemDatabase:
|
|
|
243
258
|
dbos_logger.debug("Waiting for system buffers to be exported")
|
|
244
259
|
time.sleep(1)
|
|
245
260
|
|
|
246
|
-
def
|
|
261
|
+
def insert_workflow_status(
|
|
247
262
|
self,
|
|
248
263
|
status: WorkflowStatusInternal,
|
|
249
|
-
replace: bool = True,
|
|
250
|
-
in_recovery: bool = False,
|
|
251
264
|
*,
|
|
252
|
-
conn: Optional[sa.Connection] = None,
|
|
253
265
|
max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
|
254
266
|
) -> WorkflowStatuses:
|
|
255
267
|
wf_status: WorkflowStatuses = status["status"]
|
|
256
268
|
|
|
257
|
-
cmd =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
set_=dict(
|
|
278
|
-
status=status["status"],
|
|
279
|
-
output=status["output"],
|
|
280
|
-
error=status["error"],
|
|
281
|
-
),
|
|
282
|
-
)
|
|
283
|
-
elif in_recovery:
|
|
284
|
-
cmd = cmd.on_conflict_do_update(
|
|
285
|
-
index_elements=["workflow_uuid"],
|
|
286
|
-
set_=dict(
|
|
287
|
-
recovery_attempts=SystemSchema.workflow_status.c.recovery_attempts
|
|
288
|
-
+ 1,
|
|
269
|
+
cmd = (
|
|
270
|
+
pg.insert(SystemSchema.workflow_status)
|
|
271
|
+
.values(
|
|
272
|
+
workflow_uuid=status["workflow_uuid"],
|
|
273
|
+
status=status["status"],
|
|
274
|
+
name=status["name"],
|
|
275
|
+
class_name=status["class_name"],
|
|
276
|
+
config_name=status["config_name"],
|
|
277
|
+
output=status["output"],
|
|
278
|
+
error=status["error"],
|
|
279
|
+
executor_id=status["executor_id"],
|
|
280
|
+
application_version=status["app_version"],
|
|
281
|
+
application_id=status["app_id"],
|
|
282
|
+
request=status["request"],
|
|
283
|
+
authenticated_user=status["authenticated_user"],
|
|
284
|
+
authenticated_roles=status["authenticated_roles"],
|
|
285
|
+
assumed_role=status["assumed_role"],
|
|
286
|
+
queue_name=status["queue_name"],
|
|
287
|
+
recovery_attempts=(
|
|
288
|
+
1 if wf_status != WorkflowStatusString.ENQUEUED.value else 0
|
|
289
289
|
),
|
|
290
290
|
)
|
|
291
|
-
|
|
292
|
-
# A blank update so that we can return the existing status
|
|
293
|
-
cmd = cmd.on_conflict_do_update(
|
|
291
|
+
.on_conflict_do_update(
|
|
294
292
|
index_elements=["workflow_uuid"],
|
|
295
293
|
set_=dict(
|
|
296
|
-
recovery_attempts=
|
|
294
|
+
recovery_attempts=(
|
|
295
|
+
SystemSchema.workflow_status.c.recovery_attempts + 1
|
|
296
|
+
),
|
|
297
297
|
),
|
|
298
298
|
)
|
|
299
|
+
)
|
|
300
|
+
|
|
299
301
|
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
|
|
300
302
|
|
|
301
|
-
|
|
302
|
-
results =
|
|
303
|
-
else:
|
|
304
|
-
with self.engine.begin() as c:
|
|
305
|
-
results = c.execute(cmd)
|
|
303
|
+
with self.engine.begin() as c:
|
|
304
|
+
results = c.execute(cmd)
|
|
306
305
|
|
|
307
306
|
row = results.fetchone()
|
|
308
307
|
if row is not None:
|
|
@@ -325,7 +324,9 @@ class SystemDatabase:
|
|
|
325
324
|
if err_msg is not None:
|
|
326
325
|
raise DBOSConflictingWorkflowError(status["workflow_uuid"], err_msg)
|
|
327
326
|
|
|
328
|
-
|
|
327
|
+
# Every time we start executing a workflow (and thus attempt to insert its status), we increment `recovery_attempts` by 1.
|
|
328
|
+
# When this number becomes equal to `maxRetries + 1`, we mark the workflow as `RETRIES_EXCEEDED`.
|
|
329
|
+
if recovery_attempts > max_recovery_attempts + 1:
|
|
329
330
|
with self.engine.begin() as c:
|
|
330
331
|
c.execute(
|
|
331
332
|
sa.delete(SystemSchema.workflow_queue).where(
|
|
@@ -352,38 +353,107 @@ class SystemDatabase:
|
|
|
352
353
|
status["workflow_uuid"], max_recovery_attempts
|
|
353
354
|
)
|
|
354
355
|
|
|
355
|
-
|
|
356
|
+
return wf_status
|
|
357
|
+
|
|
358
|
+
def update_workflow_status(
|
|
359
|
+
self,
|
|
360
|
+
status: WorkflowStatusInternal,
|
|
361
|
+
*,
|
|
362
|
+
conn: Optional[sa.Connection] = None,
|
|
363
|
+
) -> None:
|
|
364
|
+
wf_status: WorkflowStatuses = status["status"]
|
|
365
|
+
|
|
366
|
+
cmd = (
|
|
367
|
+
pg.insert(SystemSchema.workflow_status)
|
|
368
|
+
.values(
|
|
369
|
+
workflow_uuid=status["workflow_uuid"],
|
|
370
|
+
status=status["status"],
|
|
371
|
+
name=status["name"],
|
|
372
|
+
class_name=status["class_name"],
|
|
373
|
+
config_name=status["config_name"],
|
|
374
|
+
output=status["output"],
|
|
375
|
+
error=status["error"],
|
|
376
|
+
executor_id=status["executor_id"],
|
|
377
|
+
application_version=status["app_version"],
|
|
378
|
+
application_id=status["app_id"],
|
|
379
|
+
request=status["request"],
|
|
380
|
+
authenticated_user=status["authenticated_user"],
|
|
381
|
+
authenticated_roles=status["authenticated_roles"],
|
|
382
|
+
assumed_role=status["assumed_role"],
|
|
383
|
+
queue_name=status["queue_name"],
|
|
384
|
+
recovery_attempts=(
|
|
385
|
+
1 if wf_status != WorkflowStatusString.ENQUEUED.value else 0
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
.on_conflict_do_update(
|
|
389
|
+
index_elements=["workflow_uuid"],
|
|
390
|
+
set_=dict(
|
|
391
|
+
status=status["status"],
|
|
392
|
+
output=status["output"],
|
|
393
|
+
error=status["error"],
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if conn is not None:
|
|
399
|
+
conn.execute(cmd)
|
|
400
|
+
else:
|
|
401
|
+
with self.engine.begin() as c:
|
|
402
|
+
c.execute(cmd)
|
|
403
|
+
|
|
404
|
+
# If this is a single-transaction workflow, record that its status has been exported
|
|
356
405
|
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
357
406
|
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
358
407
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def set_workflow_status(
|
|
408
|
+
def cancel_workflow(
|
|
362
409
|
self,
|
|
363
|
-
|
|
364
|
-
status: WorkflowStatusString,
|
|
365
|
-
reset_recovery_attempts: bool,
|
|
410
|
+
workflow_id: str,
|
|
366
411
|
) -> None:
|
|
367
412
|
with self.engine.begin() as c:
|
|
368
|
-
|
|
413
|
+
# Remove the workflow from the queues table so it does not block the table
|
|
414
|
+
c.execute(
|
|
415
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
416
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
# Set the workflow's status to CANCELLED
|
|
420
|
+
c.execute(
|
|
369
421
|
sa.update(SystemSchema.workflow_status)
|
|
370
|
-
.where(SystemSchema.workflow_status.c.workflow_uuid ==
|
|
422
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
371
423
|
.values(
|
|
372
|
-
status=
|
|
424
|
+
status=WorkflowStatusString.CANCELLED.value,
|
|
373
425
|
)
|
|
374
426
|
)
|
|
375
|
-
c.execute(stmt)
|
|
376
427
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
428
|
+
def resume_workflow(
|
|
429
|
+
self,
|
|
430
|
+
workflow_id: str,
|
|
431
|
+
) -> None:
|
|
432
|
+
with self.engine.begin() as c:
|
|
433
|
+
# Check the status of the workflow. If it is complete, do nothing.
|
|
434
|
+
row = c.execute(
|
|
435
|
+
sa.select(
|
|
436
|
+
SystemSchema.workflow_status.c.status,
|
|
437
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
438
|
+
).fetchone()
|
|
439
|
+
if (
|
|
440
|
+
row is None
|
|
441
|
+
or row[0] == WorkflowStatusString.SUCCESS.value
|
|
442
|
+
or row[0] == WorkflowStatusString.ERROR.value
|
|
443
|
+
):
|
|
444
|
+
return
|
|
445
|
+
# Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
|
|
446
|
+
c.execute(
|
|
447
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
448
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
385
449
|
)
|
|
386
|
-
|
|
450
|
+
)
|
|
451
|
+
# Set the workflow's status to PENDING and clear its recovery attempts.
|
|
452
|
+
c.execute(
|
|
453
|
+
sa.update(SystemSchema.workflow_status)
|
|
454
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
455
|
+
.values(status=WorkflowStatusString.PENDING.value, recovery_attempts=0)
|
|
456
|
+
)
|
|
387
457
|
|
|
388
458
|
def get_workflow_status(
|
|
389
459
|
self, workflow_uuid: str
|
|
@@ -401,6 +471,7 @@ class SystemDatabase:
|
|
|
401
471
|
SystemSchema.workflow_status.c.authenticated_roles,
|
|
402
472
|
SystemSchema.workflow_status.c.assumed_role,
|
|
403
473
|
SystemSchema.workflow_status.c.queue_name,
|
|
474
|
+
SystemSchema.workflow_status.c.executor_id,
|
|
404
475
|
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
|
|
405
476
|
).fetchone()
|
|
406
477
|
if row is None:
|
|
@@ -415,7 +486,7 @@ class SystemDatabase:
|
|
|
415
486
|
"error": None,
|
|
416
487
|
"app_id": None,
|
|
417
488
|
"app_version": None,
|
|
418
|
-
"executor_id":
|
|
489
|
+
"executor_id": row[10],
|
|
419
490
|
"request": row[2],
|
|
420
491
|
"recovery_attempts": row[3],
|
|
421
492
|
"authenticated_user": row[6],
|
|
@@ -601,9 +672,8 @@ class SystemDatabase:
|
|
|
601
672
|
|
|
602
673
|
def get_workflows(self, input: GetWorkflowsInput) -> GetWorkflowsOutput:
|
|
603
674
|
query = sa.select(SystemSchema.workflow_status.c.workflow_uuid).order_by(
|
|
604
|
-
SystemSchema.workflow_status.c.created_at.
|
|
675
|
+
SystemSchema.workflow_status.c.created_at.asc()
|
|
605
676
|
)
|
|
606
|
-
|
|
607
677
|
if input.name:
|
|
608
678
|
query = query.where(SystemSchema.workflow_status.c.name == input.name)
|
|
609
679
|
if input.authenticated_user:
|
|
@@ -637,16 +707,73 @@ class SystemDatabase:
|
|
|
637
707
|
|
|
638
708
|
return GetWorkflowsOutput(workflow_uuids)
|
|
639
709
|
|
|
640
|
-
def
|
|
710
|
+
def get_queued_workflows(
|
|
711
|
+
self, input: GetQueuedWorkflowsInput
|
|
712
|
+
) -> GetWorkflowsOutput:
|
|
713
|
+
|
|
714
|
+
query = (
|
|
715
|
+
sa.select(SystemSchema.workflow_queue.c.workflow_uuid)
|
|
716
|
+
.join(
|
|
717
|
+
SystemSchema.workflow_status,
|
|
718
|
+
SystemSchema.workflow_queue.c.workflow_uuid
|
|
719
|
+
== SystemSchema.workflow_status.c.workflow_uuid,
|
|
720
|
+
)
|
|
721
|
+
.order_by(SystemSchema.workflow_status.c.created_at.asc())
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
if input.get("name"):
|
|
725
|
+
query = query.where(SystemSchema.workflow_status.c.name == input["name"])
|
|
726
|
+
|
|
727
|
+
if input.get("queue_name"):
|
|
728
|
+
query = query.where(
|
|
729
|
+
SystemSchema.workflow_queue.c.queue_name == input["queue_name"]
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if input.get("status"):
|
|
733
|
+
query = query.where(
|
|
734
|
+
SystemSchema.workflow_status.c.status == input["status"]
|
|
735
|
+
)
|
|
736
|
+
if "start_time" in input and input["start_time"] is not None:
|
|
737
|
+
query = query.where(
|
|
738
|
+
SystemSchema.workflow_status.c.created_at
|
|
739
|
+
>= datetime.datetime.fromisoformat(input["start_time"]).timestamp()
|
|
740
|
+
* 1000
|
|
741
|
+
)
|
|
742
|
+
if "end_time" in input and input["end_time"] is not None:
|
|
743
|
+
query = query.where(
|
|
744
|
+
SystemSchema.workflow_status.c.created_at
|
|
745
|
+
<= datetime.datetime.fromisoformat(input["end_time"]).timestamp() * 1000
|
|
746
|
+
)
|
|
747
|
+
if input.get("limit"):
|
|
748
|
+
query = query.limit(input["limit"])
|
|
749
|
+
|
|
750
|
+
with self.engine.begin() as c:
|
|
751
|
+
rows = c.execute(query)
|
|
752
|
+
workflow_uuids = [row[0] for row in rows]
|
|
753
|
+
|
|
754
|
+
return GetWorkflowsOutput(workflow_uuids)
|
|
755
|
+
|
|
756
|
+
def get_pending_workflows(
|
|
757
|
+
self, executor_id: str
|
|
758
|
+
) -> list[GetPendingWorkflowsOutput]:
|
|
641
759
|
with self.engine.begin() as c:
|
|
642
760
|
rows = c.execute(
|
|
643
|
-
sa.select(
|
|
761
|
+
sa.select(
|
|
762
|
+
SystemSchema.workflow_status.c.workflow_uuid,
|
|
763
|
+
SystemSchema.workflow_status.c.queue_name,
|
|
764
|
+
).where(
|
|
644
765
|
SystemSchema.workflow_status.c.status
|
|
645
766
|
== WorkflowStatusString.PENDING.value,
|
|
646
767
|
SystemSchema.workflow_status.c.executor_id == executor_id,
|
|
647
768
|
)
|
|
648
769
|
).fetchall()
|
|
649
|
-
return [
|
|
770
|
+
return [
|
|
771
|
+
GetPendingWorkflowsOutput(
|
|
772
|
+
workflow_uuid=row.workflow_uuid,
|
|
773
|
+
queue_name=row.queue_name,
|
|
774
|
+
)
|
|
775
|
+
for row in rows
|
|
776
|
+
]
|
|
650
777
|
|
|
651
778
|
def record_operation_result(
|
|
652
779
|
self, result: OperationResultInternal, conn: Optional[sa.Connection] = None
|
|
@@ -1266,6 +1393,19 @@ class SystemDatabase:
|
|
|
1266
1393
|
.values(completed_at_epoch_ms=int(time.time() * 1000))
|
|
1267
1394
|
)
|
|
1268
1395
|
|
|
1396
|
+
def clear_queue_assignment(self, workflow_id: str) -> None:
|
|
1397
|
+
with self.engine.begin() as c:
|
|
1398
|
+
c.execute(
|
|
1399
|
+
sa.update(SystemSchema.workflow_queue)
|
|
1400
|
+
.where(SystemSchema.workflow_queue.c.workflow_uuid == workflow_id)
|
|
1401
|
+
.values(executor_id=None, started_at_epoch_ms=None)
|
|
1402
|
+
)
|
|
1403
|
+
c.execute(
|
|
1404
|
+
sa.update(SystemSchema.workflow_status)
|
|
1405
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
1406
|
+
.values(executor_id=None, status=WorkflowStatusString.ENQUEUED.value)
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1269
1409
|
|
|
1270
1410
|
def reset_system_database(config: ConfigFile) -> None:
|
|
1271
1411
|
sysdb_name = (
|