dbos 0.20.0a9__tar.gz → 0.21.0a4__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.0a4}/PKG-INFO +1 -1
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_context.py +0 -30
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_core.py +2 -2
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_dbos.py +4 -5
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_recovery.py +2 -5
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_sys_db.py +121 -66
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_workflow_commands.py +8 -26
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/cli/cli.py +4 -4
- {dbos-0.20.0a9 → dbos-0.21.0a4}/pyproject.toml +1 -1
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_admin_server.py +35 -30
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_dbos.py +50 -88
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_failures.py +21 -44
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_queue.py +188 -0
- dbos-0.21.0a4/tests/test_sqlalchemy.py +113 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_workflow_cmds.py +14 -14
- {dbos-0.20.0a9 → dbos-0.21.0a4}/LICENSE +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/README.md +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_admin_server.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_app_db.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_classproperty.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_croniter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_db_wizard.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_dbos_config.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_error.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_fastapi.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_flask.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_kafka.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_kafka_message.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_logger.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/env.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_outcome.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_queue.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_registrations.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_request.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_roles.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_scheduler.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_serialization.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/_tracer.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/cli/_github_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/cli/_template_init.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/dbos-config.schema.json +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/dbos/py.typed +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/__init__.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/atexit_no_launch.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/classdefs.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/conftest.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/more_classdefs.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/queuedworkflow.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_async.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_classdecorators.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_concurrency.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_config.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_croniter.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_fastapi.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_flask.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_kafka.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_outcome.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_package.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_scheduler.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_schema_migration.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_singleton.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/tests/test_spans.py +0 -0
- {dbos-0.20.0a9 → dbos-0.21.0a4}/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))
|
|
@@ -800,14 +800,13 @@ class DBOS:
|
|
|
800
800
|
@classmethod
|
|
801
801
|
def cancel_workflow(cls, workflow_id: str) -> None:
|
|
802
802
|
"""Cancel a workflow by ID."""
|
|
803
|
-
_get_dbos_instance()._sys_db.
|
|
804
|
-
workflow_id, WorkflowStatusString.CANCELLED, False
|
|
805
|
-
)
|
|
803
|
+
_get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
|
|
806
804
|
|
|
807
805
|
@classmethod
|
|
808
|
-
def resume_workflow(cls, workflow_id: str) ->
|
|
806
|
+
def resume_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
809
807
|
"""Resume a workflow by ID."""
|
|
810
|
-
|
|
808
|
+
_get_dbos_instance()._sys_db.resume_workflow(workflow_id)
|
|
809
|
+
return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
|
|
811
810
|
|
|
812
811
|
@classproperty
|
|
813
812
|
def logger(cls) -> Logger:
|
|
@@ -4,7 +4,6 @@ import time
|
|
|
4
4
|
import traceback
|
|
5
5
|
from typing import TYPE_CHECKING, Any, List
|
|
6
6
|
|
|
7
|
-
from ._context import SetWorkflowRecovery
|
|
8
7
|
from ._core import execute_workflow_by_id
|
|
9
8
|
from ._error import DBOSWorkflowFunctionNotFoundError
|
|
10
9
|
|
|
@@ -19,8 +18,7 @@ def startup_recovery_thread(dbos: "DBOS", workflow_ids: List[str]) -> None:
|
|
|
19
18
|
while not stop_event.is_set() and len(workflow_ids) > 0:
|
|
20
19
|
try:
|
|
21
20
|
for workflowID in list(workflow_ids):
|
|
22
|
-
|
|
23
|
-
execute_workflow_by_id(dbos, workflowID)
|
|
21
|
+
execute_workflow_by_id(dbos, workflowID)
|
|
24
22
|
workflow_ids.remove(workflowID)
|
|
25
23
|
except DBOSWorkflowFunctionNotFoundError:
|
|
26
24
|
time.sleep(1)
|
|
@@ -45,8 +43,7 @@ def recover_pending_workflows(
|
|
|
45
43
|
dbos.logger.debug(f"Pending workflows: {workflow_ids}")
|
|
46
44
|
|
|
47
45
|
for workflowID in workflow_ids:
|
|
48
|
-
|
|
49
|
-
handle = execute_workflow_by_id(dbos, workflowID)
|
|
46
|
+
handle = execute_workflow_by_id(dbos, workflowID)
|
|
50
47
|
workflow_handles.append(handle)
|
|
51
48
|
|
|
52
49
|
dbos.logger.info("Recovered pending workflows")
|
|
@@ -243,66 +243,50 @@ class SystemDatabase:
|
|
|
243
243
|
dbos_logger.debug("Waiting for system buffers to be exported")
|
|
244
244
|
time.sleep(1)
|
|
245
245
|
|
|
246
|
-
def
|
|
246
|
+
def insert_workflow_status(
|
|
247
247
|
self,
|
|
248
248
|
status: WorkflowStatusInternal,
|
|
249
|
-
replace: bool = True,
|
|
250
|
-
in_recovery: bool = False,
|
|
251
249
|
*,
|
|
252
|
-
conn: Optional[sa.Connection] = None,
|
|
253
250
|
max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
|
254
251
|
) -> WorkflowStatuses:
|
|
255
252
|
wf_status: WorkflowStatuses = status["status"]
|
|
256
253
|
|
|
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,
|
|
254
|
+
cmd = (
|
|
255
|
+
pg.insert(SystemSchema.workflow_status)
|
|
256
|
+
.values(
|
|
257
|
+
workflow_uuid=status["workflow_uuid"],
|
|
258
|
+
status=status["status"],
|
|
259
|
+
name=status["name"],
|
|
260
|
+
class_name=status["class_name"],
|
|
261
|
+
config_name=status["config_name"],
|
|
262
|
+
output=status["output"],
|
|
263
|
+
error=status["error"],
|
|
264
|
+
executor_id=status["executor_id"],
|
|
265
|
+
application_version=status["app_version"],
|
|
266
|
+
application_id=status["app_id"],
|
|
267
|
+
request=status["request"],
|
|
268
|
+
authenticated_user=status["authenticated_user"],
|
|
269
|
+
authenticated_roles=status["authenticated_roles"],
|
|
270
|
+
assumed_role=status["assumed_role"],
|
|
271
|
+
queue_name=status["queue_name"],
|
|
272
|
+
recovery_attempts=(
|
|
273
|
+
1 if wf_status != WorkflowStatusString.ENQUEUED.value else 0
|
|
289
274
|
),
|
|
290
275
|
)
|
|
291
|
-
|
|
292
|
-
# A blank update so that we can return the existing status
|
|
293
|
-
cmd = cmd.on_conflict_do_update(
|
|
276
|
+
.on_conflict_do_update(
|
|
294
277
|
index_elements=["workflow_uuid"],
|
|
295
278
|
set_=dict(
|
|
296
|
-
recovery_attempts=
|
|
279
|
+
recovery_attempts=(
|
|
280
|
+
SystemSchema.workflow_status.c.recovery_attempts + 1
|
|
281
|
+
),
|
|
297
282
|
),
|
|
298
283
|
)
|
|
284
|
+
)
|
|
285
|
+
|
|
299
286
|
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
287
|
|
|
301
|
-
|
|
302
|
-
results =
|
|
303
|
-
else:
|
|
304
|
-
with self.engine.begin() as c:
|
|
305
|
-
results = c.execute(cmd)
|
|
288
|
+
with self.engine.begin() as c:
|
|
289
|
+
results = c.execute(cmd)
|
|
306
290
|
|
|
307
291
|
row = results.fetchone()
|
|
308
292
|
if row is not None:
|
|
@@ -325,7 +309,9 @@ class SystemDatabase:
|
|
|
325
309
|
if err_msg is not None:
|
|
326
310
|
raise DBOSConflictingWorkflowError(status["workflow_uuid"], err_msg)
|
|
327
311
|
|
|
328
|
-
|
|
312
|
+
# Every time we start executing a workflow (and thus attempt to insert its status), we increment `recovery_attempts` by 1.
|
|
313
|
+
# When this number becomes equal to `maxRetries + 1`, we mark the workflow as `RETRIES_EXCEEDED`.
|
|
314
|
+
if recovery_attempts > max_recovery_attempts + 1:
|
|
329
315
|
with self.engine.begin() as c:
|
|
330
316
|
c.execute(
|
|
331
317
|
sa.delete(SystemSchema.workflow_queue).where(
|
|
@@ -352,38 +338,107 @@ class SystemDatabase:
|
|
|
352
338
|
status["workflow_uuid"], max_recovery_attempts
|
|
353
339
|
)
|
|
354
340
|
|
|
355
|
-
|
|
341
|
+
return wf_status
|
|
342
|
+
|
|
343
|
+
def update_workflow_status(
|
|
344
|
+
self,
|
|
345
|
+
status: WorkflowStatusInternal,
|
|
346
|
+
*,
|
|
347
|
+
conn: Optional[sa.Connection] = None,
|
|
348
|
+
) -> None:
|
|
349
|
+
wf_status: WorkflowStatuses = status["status"]
|
|
350
|
+
|
|
351
|
+
cmd = (
|
|
352
|
+
pg.insert(SystemSchema.workflow_status)
|
|
353
|
+
.values(
|
|
354
|
+
workflow_uuid=status["workflow_uuid"],
|
|
355
|
+
status=status["status"],
|
|
356
|
+
name=status["name"],
|
|
357
|
+
class_name=status["class_name"],
|
|
358
|
+
config_name=status["config_name"],
|
|
359
|
+
output=status["output"],
|
|
360
|
+
error=status["error"],
|
|
361
|
+
executor_id=status["executor_id"],
|
|
362
|
+
application_version=status["app_version"],
|
|
363
|
+
application_id=status["app_id"],
|
|
364
|
+
request=status["request"],
|
|
365
|
+
authenticated_user=status["authenticated_user"],
|
|
366
|
+
authenticated_roles=status["authenticated_roles"],
|
|
367
|
+
assumed_role=status["assumed_role"],
|
|
368
|
+
queue_name=status["queue_name"],
|
|
369
|
+
recovery_attempts=(
|
|
370
|
+
1 if wf_status != WorkflowStatusString.ENQUEUED.value else 0
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
.on_conflict_do_update(
|
|
374
|
+
index_elements=["workflow_uuid"],
|
|
375
|
+
set_=dict(
|
|
376
|
+
status=status["status"],
|
|
377
|
+
output=status["output"],
|
|
378
|
+
error=status["error"],
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if conn is not None:
|
|
384
|
+
conn.execute(cmd)
|
|
385
|
+
else:
|
|
386
|
+
with self.engine.begin() as c:
|
|
387
|
+
c.execute(cmd)
|
|
388
|
+
|
|
389
|
+
# If this is a single-transaction workflow, record that its status has been exported
|
|
356
390
|
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
357
391
|
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
358
392
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def set_workflow_status(
|
|
393
|
+
def cancel_workflow(
|
|
362
394
|
self,
|
|
363
|
-
|
|
364
|
-
status: WorkflowStatusString,
|
|
365
|
-
reset_recovery_attempts: bool,
|
|
395
|
+
workflow_id: str,
|
|
366
396
|
) -> None:
|
|
367
397
|
with self.engine.begin() as c:
|
|
368
|
-
|
|
398
|
+
# Remove the workflow from the queues table so it does not block the table
|
|
399
|
+
c.execute(
|
|
400
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
401
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
# Set the workflow's status to CANCELLED
|
|
405
|
+
c.execute(
|
|
369
406
|
sa.update(SystemSchema.workflow_status)
|
|
370
|
-
.where(SystemSchema.workflow_status.c.workflow_uuid ==
|
|
407
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
371
408
|
.values(
|
|
372
|
-
status=
|
|
409
|
+
status=WorkflowStatusString.CANCELLED.value,
|
|
373
410
|
)
|
|
374
411
|
)
|
|
375
|
-
c.execute(stmt)
|
|
376
412
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
413
|
+
def resume_workflow(
|
|
414
|
+
self,
|
|
415
|
+
workflow_id: str,
|
|
416
|
+
) -> None:
|
|
417
|
+
with self.engine.begin() as c:
|
|
418
|
+
# Check the status of the workflow. If it is complete, do nothing.
|
|
419
|
+
row = c.execute(
|
|
420
|
+
sa.select(
|
|
421
|
+
SystemSchema.workflow_status.c.status,
|
|
422
|
+
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
423
|
+
).fetchone()
|
|
424
|
+
if (
|
|
425
|
+
row is None
|
|
426
|
+
or row[0] == WorkflowStatusString.SUCCESS.value
|
|
427
|
+
or row[0] == WorkflowStatusString.ERROR.value
|
|
428
|
+
):
|
|
429
|
+
return
|
|
430
|
+
# Remove the workflow from the queues table so resume can safely be called on an ENQUEUED workflow
|
|
431
|
+
c.execute(
|
|
432
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
433
|
+
SystemSchema.workflow_queue.c.workflow_uuid == workflow_id
|
|
385
434
|
)
|
|
386
|
-
|
|
435
|
+
)
|
|
436
|
+
# Set the workflow's status to PENDING and clear its recovery attempts.
|
|
437
|
+
c.execute(
|
|
438
|
+
sa.update(SystemSchema.workflow_status)
|
|
439
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
440
|
+
.values(status=WorkflowStatusString.PENDING.value, recovery_attempts=0)
|
|
441
|
+
)
|
|
387
442
|
|
|
388
443
|
def get_workflow_status(
|
|
389
444
|
self, workflow_uuid: str
|
|
@@ -1,23 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
from typing import Any, List, Optional, cast
|
|
1
|
+
from typing import List, Optional, cast
|
|
5
2
|
|
|
6
3
|
import typer
|
|
7
|
-
from rich import print
|
|
8
4
|
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from . import _serialization, load_config
|
|
12
|
-
from ._core import execute_workflow_by_id
|
|
13
|
-
from ._dbos_config import ConfigFile, _is_valid_app_name
|
|
5
|
+
from . import _serialization
|
|
6
|
+
from ._dbos_config import ConfigFile
|
|
14
7
|
from ._sys_db import (
|
|
15
8
|
GetWorkflowsInput,
|
|
16
9
|
GetWorkflowsOutput,
|
|
17
10
|
SystemDatabase,
|
|
18
11
|
WorkflowStatuses,
|
|
19
|
-
WorkflowStatusInternal,
|
|
20
|
-
WorkflowStatusString,
|
|
21
12
|
)
|
|
22
13
|
|
|
23
14
|
|
|
@@ -41,7 +32,7 @@ class WorkflowInformation:
|
|
|
41
32
|
queue_name: Optional[str]
|
|
42
33
|
|
|
43
34
|
|
|
44
|
-
def
|
|
35
|
+
def list_workflows(
|
|
45
36
|
config: ConfigFile,
|
|
46
37
|
li: int,
|
|
47
38
|
user: Optional[str],
|
|
@@ -91,17 +82,13 @@ def _list_workflows(
|
|
|
91
82
|
sys_db.destroy()
|
|
92
83
|
|
|
93
84
|
|
|
94
|
-
def
|
|
85
|
+
def get_workflow(
|
|
95
86
|
config: ConfigFile, uuid: str, request: bool
|
|
96
87
|
) -> Optional[WorkflowInformation]:
|
|
97
|
-
sys_db = None
|
|
98
|
-
|
|
99
88
|
try:
|
|
100
89
|
sys_db = SystemDatabase(config)
|
|
101
|
-
|
|
102
90
|
info = _get_workflow_info(sys_db, uuid, request)
|
|
103
91
|
return info
|
|
104
|
-
|
|
105
92
|
except Exception as e:
|
|
106
93
|
typer.echo(f"Error getting workflow: {e}")
|
|
107
94
|
return None
|
|
@@ -110,18 +97,13 @@ def _get_workflow(
|
|
|
110
97
|
sys_db.destroy()
|
|
111
98
|
|
|
112
99
|
|
|
113
|
-
def
|
|
114
|
-
# config = load_config()
|
|
115
|
-
sys_db = None
|
|
116
|
-
|
|
100
|
+
def cancel_workflow(config: ConfigFile, uuid: str) -> None:
|
|
117
101
|
try:
|
|
118
102
|
sys_db = SystemDatabase(config)
|
|
119
|
-
sys_db.
|
|
120
|
-
return
|
|
121
|
-
|
|
103
|
+
sys_db.cancel_workflow(uuid)
|
|
122
104
|
except Exception as e:
|
|
123
105
|
typer.echo(f"Failed to connect to DBOS system database: {e}")
|
|
124
|
-
|
|
106
|
+
raise e
|
|
125
107
|
finally:
|
|
126
108
|
if sys_db:
|
|
127
109
|
sys_db.destroy()
|
|
@@ -19,7 +19,7 @@ from .. import load_config
|
|
|
19
19
|
from .._app_db import ApplicationDatabase
|
|
20
20
|
from .._dbos_config import _is_valid_app_name
|
|
21
21
|
from .._sys_db import SystemDatabase, reset_system_database
|
|
22
|
-
from .._workflow_commands import
|
|
22
|
+
from .._workflow_commands import cancel_workflow, get_workflow, list_workflows
|
|
23
23
|
from ..cli._github_init import create_template_from_github
|
|
24
24
|
from ._template_init import copy_template, get_project_name, get_templates_directory
|
|
25
25
|
|
|
@@ -282,7 +282,7 @@ def list(
|
|
|
282
282
|
] = None,
|
|
283
283
|
) -> None:
|
|
284
284
|
config = load_config()
|
|
285
|
-
workflows =
|
|
285
|
+
workflows = list_workflows(
|
|
286
286
|
config, limit, user, starttime, endtime, status, request, appversion
|
|
287
287
|
)
|
|
288
288
|
print(jsonpickle.encode(workflows, unpicklable=False))
|
|
@@ -301,7 +301,7 @@ def get(
|
|
|
301
301
|
] = True,
|
|
302
302
|
) -> None:
|
|
303
303
|
config = load_config()
|
|
304
|
-
print(jsonpickle.encode(
|
|
304
|
+
print(jsonpickle.encode(get_workflow(config, uuid, request), unpicklable=False))
|
|
305
305
|
|
|
306
306
|
|
|
307
307
|
@workflow.command(
|
|
@@ -315,7 +315,7 @@ def cancel(
|
|
|
315
315
|
] = None,
|
|
316
316
|
) -> None:
|
|
317
317
|
config = load_config()
|
|
318
|
-
|
|
318
|
+
cancel_workflow(config, uuid)
|
|
319
319
|
print(f"Workflow {uuid} has been cancelled")
|
|
320
320
|
|
|
321
321
|
|
|
@@ -151,54 +151,59 @@ runtimeConfig:
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def test_admin_workflow_resume(dbos: DBOS, config: ConfigFile) -> None:
|
|
154
|
+
counter: int = 0
|
|
154
155
|
|
|
155
156
|
@DBOS.workflow()
|
|
156
157
|
def simple_workflow() -> None:
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
nonlocal counter
|
|
159
|
+
counter += 1
|
|
159
160
|
|
|
160
|
-
#
|
|
161
|
+
# Run the workflow and flush its results
|
|
161
162
|
simple_workflow()
|
|
162
|
-
|
|
163
|
+
assert counter == 1
|
|
164
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
163
165
|
|
|
164
|
-
#
|
|
165
|
-
output = _workflow_commands.
|
|
166
|
+
# Verify the workflow has succeeded
|
|
167
|
+
output = _workflow_commands.list_workflows(
|
|
166
168
|
config, 10, None, None, None, None, False, None
|
|
167
169
|
)
|
|
168
170
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
169
|
-
|
|
170
171
|
assert output[0] != None, "Expected output to be not None"
|
|
171
|
-
|
|
172
172
|
wfUuid = output[0].workflowUUID
|
|
173
|
-
|
|
174
|
-
info = _workflow_commands._get_workflow(config, wfUuid, True)
|
|
173
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
175
174
|
assert info is not None, "Expected output to be not None"
|
|
176
|
-
|
|
177
175
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
178
176
|
|
|
177
|
+
# Cancel the workflow. Verify it was cancelled
|
|
179
178
|
response = requests.post(
|
|
180
179
|
f"http://localhost:3001/workflows/{wfUuid}/cancel", json=[], timeout=5
|
|
181
180
|
)
|
|
182
181
|
assert response.status_code == 204
|
|
182
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
183
|
+
assert info is not None
|
|
184
|
+
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
183
185
|
|
|
184
|
-
|
|
185
|
-
if info is not None:
|
|
186
|
-
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
187
|
-
else:
|
|
188
|
-
assert False, "Expected info to be not None"
|
|
189
|
-
|
|
186
|
+
# Resume the workflow. Verify that it succeeds again.
|
|
190
187
|
response = requests.post(
|
|
191
188
|
f"http://localhost:3001/workflows/{wfUuid}/resume", json=[], timeout=5
|
|
192
189
|
)
|
|
193
190
|
assert response.status_code == 204
|
|
191
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
192
|
+
assert counter == 2
|
|
193
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
194
|
+
assert info is not None
|
|
195
|
+
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
# Resume the workflow. Verify it does not run and status remains SUCCESS
|
|
198
|
+
response = requests.post(
|
|
199
|
+
f"http://localhost:3001/workflows/{wfUuid}/resume", json=[], timeout=5
|
|
200
|
+
)
|
|
201
|
+
assert response.status_code == 204
|
|
202
|
+
dbos._sys_db.wait_for_buffer_flush()
|
|
203
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
204
|
+
assert info is not None
|
|
205
|
+
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
206
|
+
assert counter == 2
|
|
202
207
|
|
|
203
208
|
|
|
204
209
|
def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
@@ -213,7 +218,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
213
218
|
time.sleep(1)
|
|
214
219
|
|
|
215
220
|
# get the workflow list
|
|
216
|
-
output = _workflow_commands.
|
|
221
|
+
output = _workflow_commands.list_workflows(
|
|
217
222
|
config, 10, None, None, None, None, False, None
|
|
218
223
|
)
|
|
219
224
|
assert len(output) == 1, f"Expected list length to be 1, but got {len(output)}"
|
|
@@ -222,7 +227,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
222
227
|
|
|
223
228
|
wfUuid = output[0].workflowUUID
|
|
224
229
|
|
|
225
|
-
info = _workflow_commands.
|
|
230
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
226
231
|
assert info is not None, "Expected output to be not None"
|
|
227
232
|
|
|
228
233
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
@@ -232,7 +237,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
232
237
|
)
|
|
233
238
|
assert response.status_code == 204
|
|
234
239
|
|
|
235
|
-
info = _workflow_commands.
|
|
240
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
236
241
|
if info is not None:
|
|
237
242
|
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
238
243
|
else:
|
|
@@ -245,13 +250,13 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
245
250
|
|
|
246
251
|
time.sleep(1)
|
|
247
252
|
|
|
248
|
-
info = _workflow_commands.
|
|
253
|
+
info = _workflow_commands.get_workflow(config, wfUuid, True)
|
|
249
254
|
if info is not None:
|
|
250
255
|
assert info.status == "CANCELLED", f"Expected status to be CANCELLED"
|
|
251
256
|
else:
|
|
252
257
|
assert False, "Expected info to be not None"
|
|
253
258
|
|
|
254
|
-
output = _workflow_commands.
|
|
259
|
+
output = _workflow_commands.list_workflows(
|
|
255
260
|
config, 10, None, None, None, None, False, None
|
|
256
261
|
)
|
|
257
262
|
assert len(output) == 2, f"Expected list length to be 2, but got {len(output)}"
|
|
@@ -261,7 +266,7 @@ def test_admin_workflow_restart(dbos: DBOS, config: ConfigFile) -> None:
|
|
|
261
266
|
else:
|
|
262
267
|
new_wfUuid = output[0].workflowUUID
|
|
263
268
|
|
|
264
|
-
info = _workflow_commands.
|
|
269
|
+
info = _workflow_commands.get_workflow(config, new_wfUuid, True)
|
|
265
270
|
if info is not None:
|
|
266
271
|
assert info.status == "SUCCESS", f"Expected status to be SUCCESS"
|
|
267
272
|
else:
|