dbos 0.18.0a1__py3-none-any.whl → 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- dbos/_context.py +11 -2
- dbos/_core.py +30 -8
- dbos/_db_wizard.py +53 -12
- dbos/_dbos.py +1 -1
- dbos/_dbos_config.py +52 -7
- dbos/_error.py +11 -0
- dbos/_kafka.py +17 -1
- dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +34 -0
- dbos/_queue.py +20 -0
- dbos/_schemas/system_database.py +1 -0
- dbos/_sys_db.py +100 -47
- dbos/_templates/hello/dbos-config.yaml.dbos +0 -4
- dbos/_workflow_commands.py +172 -0
- dbos/cli.py +100 -1
- dbos/dbos-config.schema.json +2 -11
- {dbos-0.18.0a1.dist-info → dbos-0.19.0.dist-info}/METADATA +21 -16
- {dbos-0.18.0a1.dist-info → dbos-0.19.0.dist-info}/RECORD +20 -18
- {dbos-0.18.0a1.dist-info → dbos-0.19.0.dist-info}/WHEEL +0 -0
- {dbos-0.18.0a1.dist-info → dbos-0.19.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.18.0a1.dist-info → dbos-0.19.0.dist-info}/licenses/LICENSE +0 -0
dbos/_sys_db.py
CHANGED
|
@@ -13,7 +13,6 @@ from typing import (
|
|
|
13
13
|
Optional,
|
|
14
14
|
Sequence,
|
|
15
15
|
Set,
|
|
16
|
-
Tuple,
|
|
17
16
|
TypedDict,
|
|
18
17
|
cast,
|
|
19
18
|
)
|
|
@@ -23,12 +22,15 @@ import sqlalchemy as sa
|
|
|
23
22
|
import sqlalchemy.dialects.postgresql as pg
|
|
24
23
|
from alembic import command
|
|
25
24
|
from alembic.config import Config
|
|
25
|
+
from sqlalchemy import or_
|
|
26
26
|
from sqlalchemy.exc import DBAPIError
|
|
27
27
|
|
|
28
28
|
from . import _serialization
|
|
29
29
|
from ._dbos_config import ConfigFile
|
|
30
30
|
from ._error import (
|
|
31
|
+
DBOSConflictingWorkflowError,
|
|
31
32
|
DBOSDeadLetterQueueError,
|
|
33
|
+
DBOSException,
|
|
32
34
|
DBOSNonExistentWorkflowError,
|
|
33
35
|
DBOSWorkflowConflictIDError,
|
|
34
36
|
)
|
|
@@ -249,7 +251,9 @@ class SystemDatabase:
|
|
|
249
251
|
*,
|
|
250
252
|
conn: Optional[sa.Connection] = None,
|
|
251
253
|
max_recovery_attempts: int = DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
|
252
|
-
) ->
|
|
254
|
+
) -> WorkflowStatuses:
|
|
255
|
+
wf_status: WorkflowStatuses = status["status"]
|
|
256
|
+
|
|
253
257
|
cmd = pg.insert(SystemSchema.workflow_status).values(
|
|
254
258
|
workflow_uuid=status["workflow_uuid"],
|
|
255
259
|
status=status["status"],
|
|
@@ -285,49 +289,75 @@ class SystemDatabase:
|
|
|
285
289
|
),
|
|
286
290
|
)
|
|
287
291
|
else:
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
# A blank update so that we can return the existing status
|
|
293
|
+
cmd = cmd.on_conflict_do_update(
|
|
294
|
+
index_elements=["workflow_uuid"],
|
|
295
|
+
set_=dict(
|
|
296
|
+
recovery_attempts=SystemSchema.workflow_status.c.recovery_attempts
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
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
|
|
290
300
|
|
|
291
301
|
if conn is not None:
|
|
292
302
|
results = conn.execute(cmd)
|
|
293
303
|
else:
|
|
294
304
|
with self.engine.begin() as c:
|
|
295
305
|
results = c.execute(cmd)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
|
|
307
|
+
row = results.fetchone()
|
|
308
|
+
if row is not None:
|
|
309
|
+
# Check the started workflow matches the expected name, class_name, config_name, and queue_name
|
|
310
|
+
# A mismatch indicates a workflow starting with the same UUID but different functions, which would throw an exception.
|
|
311
|
+
recovery_attempts: int = row[0]
|
|
312
|
+
wf_status = row[1]
|
|
313
|
+
err_msg: Optional[str] = None
|
|
314
|
+
if row[2] != status["name"]:
|
|
315
|
+
err_msg = f"Workflow already exists with a different function name: {row[2]}, but the provided function name is: {status['name']}"
|
|
316
|
+
elif row[3] != status["class_name"]:
|
|
317
|
+
err_msg = f"Workflow already exists with a different class name: {row[3]}, but the provided class name is: {status['class_name']}"
|
|
318
|
+
elif row[4] != status["config_name"]:
|
|
319
|
+
err_msg = f"Workflow already exists with a different config name: {row[4]}, but the provided config name is: {status['config_name']}"
|
|
320
|
+
elif row[5] != status["queue_name"]:
|
|
321
|
+
# This is a warning because a different queue name is not necessarily an error.
|
|
322
|
+
dbos_logger.warning(
|
|
323
|
+
f"Workflow already exists in queue: {row[5]}, but the provided queue name is: {status['queue_name']}. The queue is not updated."
|
|
324
|
+
)
|
|
325
|
+
if err_msg is not None:
|
|
326
|
+
raise DBOSConflictingWorkflowError(status["workflow_uuid"], err_msg)
|
|
327
|
+
|
|
328
|
+
if in_recovery and recovery_attempts > max_recovery_attempts:
|
|
329
|
+
with self.engine.begin() as c:
|
|
330
|
+
c.execute(
|
|
331
|
+
sa.delete(SystemSchema.workflow_queue).where(
|
|
332
|
+
SystemSchema.workflow_queue.c.workflow_uuid
|
|
333
|
+
== status["workflow_uuid"]
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
c.execute(
|
|
337
|
+
sa.update(SystemSchema.workflow_status)
|
|
338
|
+
.where(
|
|
339
|
+
SystemSchema.workflow_status.c.workflow_uuid
|
|
340
|
+
== status["workflow_uuid"]
|
|
341
|
+
)
|
|
342
|
+
.where(
|
|
343
|
+
SystemSchema.workflow_status.c.status
|
|
344
|
+
== WorkflowStatusString.PENDING.value
|
|
307
345
|
)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
SystemSchema.workflow_status.c.workflow_uuid
|
|
312
|
-
== status["workflow_uuid"]
|
|
313
|
-
)
|
|
314
|
-
.where(
|
|
315
|
-
SystemSchema.workflow_status.c.status
|
|
316
|
-
== WorkflowStatusString.PENDING.value
|
|
317
|
-
)
|
|
318
|
-
.values(
|
|
319
|
-
status=WorkflowStatusString.RETRIES_EXCEEDED.value,
|
|
320
|
-
queue_name=None,
|
|
321
|
-
)
|
|
346
|
+
.values(
|
|
347
|
+
status=WorkflowStatusString.RETRIES_EXCEEDED.value,
|
|
348
|
+
queue_name=None,
|
|
322
349
|
)
|
|
323
|
-
raise DBOSDeadLetterQueueError(
|
|
324
|
-
status["workflow_uuid"], max_recovery_attempts
|
|
325
350
|
)
|
|
351
|
+
raise DBOSDeadLetterQueueError(
|
|
352
|
+
status["workflow_uuid"], max_recovery_attempts
|
|
353
|
+
)
|
|
326
354
|
|
|
327
355
|
# Record we have exported status for this single-transaction workflow
|
|
328
356
|
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
329
357
|
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
330
358
|
|
|
359
|
+
return wf_status
|
|
360
|
+
|
|
331
361
|
def set_workflow_status(
|
|
332
362
|
self,
|
|
333
363
|
workflow_uuid: str,
|
|
@@ -349,7 +379,7 @@ class SystemDatabase:
|
|
|
349
379
|
stmt = (
|
|
350
380
|
sa.update(SystemSchema.workflow_status)
|
|
351
381
|
.where(
|
|
352
|
-
SystemSchema.
|
|
382
|
+
SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid
|
|
353
383
|
)
|
|
354
384
|
.values(recovery_attempts=reset_recovery_attempts)
|
|
355
385
|
)
|
|
@@ -405,7 +435,10 @@ class SystemDatabase:
|
|
|
405
435
|
res["output"]
|
|
406
436
|
)
|
|
407
437
|
return resstat
|
|
408
|
-
|
|
438
|
+
else:
|
|
439
|
+
raise DBOSException(
|
|
440
|
+
"Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
|
|
441
|
+
)
|
|
409
442
|
stat = self.get_workflow_status(workflow_uuid)
|
|
410
443
|
self.record_operation_result(
|
|
411
444
|
{
|
|
@@ -528,18 +561,27 @@ class SystemDatabase:
|
|
|
528
561
|
workflow_uuid=workflow_uuid,
|
|
529
562
|
inputs=inputs,
|
|
530
563
|
)
|
|
531
|
-
.
|
|
564
|
+
.on_conflict_do_update(
|
|
565
|
+
index_elements=["workflow_uuid"],
|
|
566
|
+
set_=dict(workflow_uuid=SystemSchema.workflow_inputs.c.workflow_uuid),
|
|
567
|
+
)
|
|
568
|
+
.returning(SystemSchema.workflow_inputs.c.inputs)
|
|
532
569
|
)
|
|
533
570
|
if conn is not None:
|
|
534
|
-
conn.execute(cmd)
|
|
571
|
+
row = conn.execute(cmd).fetchone()
|
|
535
572
|
else:
|
|
536
573
|
with self.engine.begin() as c:
|
|
537
|
-
c.execute(cmd)
|
|
538
|
-
|
|
574
|
+
row = c.execute(cmd).fetchone()
|
|
575
|
+
if row is not None and row[0] != inputs:
|
|
576
|
+
dbos_logger.warning(
|
|
577
|
+
f"Workflow inputs for {workflow_uuid} changed since the first call! Use the original inputs."
|
|
578
|
+
)
|
|
579
|
+
# TODO: actually changing the input
|
|
539
580
|
if workflow_uuid in self._temp_txn_wf_ids:
|
|
540
581
|
# Clean up the single-transaction tracking sets
|
|
541
582
|
self._exported_temp_txn_wf_status.discard(workflow_uuid)
|
|
542
583
|
self._temp_txn_wf_ids.discard(workflow_uuid)
|
|
584
|
+
return
|
|
543
585
|
|
|
544
586
|
def get_workflow_inputs(
|
|
545
587
|
self, workflow_uuid: str
|
|
@@ -572,12 +614,12 @@ class SystemDatabase:
|
|
|
572
614
|
if input.start_time:
|
|
573
615
|
query = query.where(
|
|
574
616
|
SystemSchema.workflow_status.c.created_at
|
|
575
|
-
>= datetime.datetime.fromisoformat(input.start_time).timestamp()
|
|
617
|
+
>= datetime.datetime.fromisoformat(input.start_time).timestamp() * 1000
|
|
576
618
|
)
|
|
577
619
|
if input.end_time:
|
|
578
620
|
query = query.where(
|
|
579
621
|
SystemSchema.workflow_status.c.created_at
|
|
580
|
-
<= datetime.datetime.fromisoformat(input.end_time).timestamp()
|
|
622
|
+
<= datetime.datetime.fromisoformat(input.end_time).timestamp() * 1000
|
|
581
623
|
)
|
|
582
624
|
if input.status:
|
|
583
625
|
query = query.where(SystemSchema.workflow_status.c.status == input.status)
|
|
@@ -1130,27 +1172,38 @@ class SystemDatabase:
|
|
|
1130
1172
|
if num_recent_queries >= queue.limiter["limit"]:
|
|
1131
1173
|
return []
|
|
1132
1174
|
|
|
1133
|
-
#
|
|
1134
|
-
#
|
|
1135
|
-
# If there is a concurrency limit N, select only the N most recent
|
|
1175
|
+
# Dequeue functions eligible for this worker and ordered by the time at which they were enqueued.
|
|
1176
|
+
# If there is a global or local concurrency limit N, select only the N oldest enqueued
|
|
1136
1177
|
# functions, else select all of them.
|
|
1137
1178
|
query = (
|
|
1138
1179
|
sa.select(
|
|
1139
1180
|
SystemSchema.workflow_queue.c.workflow_uuid,
|
|
1140
1181
|
SystemSchema.workflow_queue.c.started_at_epoch_ms,
|
|
1182
|
+
SystemSchema.workflow_queue.c.executor_id,
|
|
1141
1183
|
)
|
|
1142
1184
|
.where(SystemSchema.workflow_queue.c.queue_name == queue.name)
|
|
1143
1185
|
.where(SystemSchema.workflow_queue.c.completed_at_epoch_ms == None)
|
|
1186
|
+
.where(
|
|
1187
|
+
# Only select functions that have not been started yet or have been started by this worker
|
|
1188
|
+
or_(
|
|
1189
|
+
SystemSchema.workflow_queue.c.executor_id == None,
|
|
1190
|
+
SystemSchema.workflow_queue.c.executor_id == executor_id,
|
|
1191
|
+
)
|
|
1192
|
+
)
|
|
1144
1193
|
.order_by(SystemSchema.workflow_queue.c.created_at_epoch_ms.asc())
|
|
1145
1194
|
)
|
|
1146
|
-
|
|
1195
|
+
# Set a dequeue limit if necessary
|
|
1196
|
+
if queue.worker_concurrency is not None:
|
|
1197
|
+
query = query.limit(queue.worker_concurrency)
|
|
1198
|
+
elif queue.concurrency is not None:
|
|
1147
1199
|
query = query.limit(queue.concurrency)
|
|
1148
1200
|
|
|
1149
|
-
# From the functions retrieved, get the workflow IDs of the functions
|
|
1150
|
-
# that have not yet been started so we can start them.
|
|
1151
1201
|
rows = c.execute(query).fetchall()
|
|
1202
|
+
|
|
1203
|
+
# Now, get the workflow IDs of functions that have not yet been started
|
|
1152
1204
|
dequeued_ids: List[str] = [row[0] for row in rows if row[1] is None]
|
|
1153
1205
|
ret_ids: list[str] = []
|
|
1206
|
+
dbos_logger.debug(f"[{queue.name}] dequeueing {len(dequeued_ids)} task(s)")
|
|
1154
1207
|
for id in dequeued_ids:
|
|
1155
1208
|
|
|
1156
1209
|
# If we have a limiter, stop starting functions when the number
|
|
@@ -1173,11 +1226,11 @@ class SystemDatabase:
|
|
|
1173
1226
|
)
|
|
1174
1227
|
)
|
|
1175
1228
|
|
|
1176
|
-
# Then give it a start time
|
|
1229
|
+
# Then give it a start time and assign the executor ID
|
|
1177
1230
|
c.execute(
|
|
1178
1231
|
SystemSchema.workflow_queue.update()
|
|
1179
1232
|
.where(SystemSchema.workflow_queue.c.workflow_uuid == id)
|
|
1180
|
-
.values(started_at_epoch_ms=start_time_ms)
|
|
1233
|
+
.values(started_at_epoch_ms=start_time_ms, executor_id=executor_id)
|
|
1181
1234
|
)
|
|
1182
1235
|
ret_ids.append(id)
|
|
1183
1236
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import Any, List, Optional, cast
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
|
|
6
|
+
from dbos import DBOS
|
|
7
|
+
|
|
8
|
+
from . import _serialization, load_config
|
|
9
|
+
from ._dbos_config import ConfigFile, _is_valid_app_name
|
|
10
|
+
from ._sys_db import (
|
|
11
|
+
GetWorkflowsInput,
|
|
12
|
+
GetWorkflowsOutput,
|
|
13
|
+
SystemDatabase,
|
|
14
|
+
WorkflowStatuses,
|
|
15
|
+
WorkflowStatusInternal,
|
|
16
|
+
WorkflowStatusString,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkflowInformation:
|
|
21
|
+
workflowUUID: str
|
|
22
|
+
status: WorkflowStatuses
|
|
23
|
+
workflowName: str
|
|
24
|
+
workflowClassName: Optional[str]
|
|
25
|
+
workflowConfigName: Optional[str]
|
|
26
|
+
input: Optional[_serialization.WorkflowInputs] # JSON (jsonpickle)
|
|
27
|
+
output: Optional[str] # JSON (jsonpickle)
|
|
28
|
+
error: Optional[str] # JSON (jsonpickle)
|
|
29
|
+
executor_id: Optional[str]
|
|
30
|
+
app_version: Optional[str]
|
|
31
|
+
app_id: Optional[str]
|
|
32
|
+
request: Optional[str] # JSON (jsonpickle)
|
|
33
|
+
recovery_attempts: Optional[int]
|
|
34
|
+
authenticated_user: Optional[str]
|
|
35
|
+
assumed_role: Optional[str]
|
|
36
|
+
authenticated_roles: Optional[str] # JSON list of roles.
|
|
37
|
+
queue_name: Optional[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _list_workflows(
|
|
41
|
+
config: ConfigFile,
|
|
42
|
+
li: int,
|
|
43
|
+
user: Optional[str],
|
|
44
|
+
starttime: Optional[str],
|
|
45
|
+
endtime: Optional[str],
|
|
46
|
+
status: Optional[str],
|
|
47
|
+
request: bool,
|
|
48
|
+
appversion: Optional[str],
|
|
49
|
+
) -> List[WorkflowInformation]:
|
|
50
|
+
|
|
51
|
+
sys_db = None
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
sys_db = SystemDatabase(config)
|
|
55
|
+
|
|
56
|
+
input = GetWorkflowsInput()
|
|
57
|
+
input.authenticated_user = user
|
|
58
|
+
input.start_time = starttime
|
|
59
|
+
input.end_time = endtime
|
|
60
|
+
if status is not None:
|
|
61
|
+
input.status = cast(WorkflowStatuses, status)
|
|
62
|
+
input.application_version = appversion
|
|
63
|
+
input.limit = li
|
|
64
|
+
|
|
65
|
+
output: GetWorkflowsOutput = sys_db.get_workflows(input)
|
|
66
|
+
|
|
67
|
+
infos: List[WorkflowInformation] = []
|
|
68
|
+
|
|
69
|
+
if output.workflow_uuids is None:
|
|
70
|
+
typer.echo("No workflows found")
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
for workflow_id in output.workflow_uuids:
|
|
74
|
+
info = _get_workflow_info(
|
|
75
|
+
sys_db, workflow_id, request
|
|
76
|
+
) # Call the method for each ID
|
|
77
|
+
|
|
78
|
+
if info is not None:
|
|
79
|
+
infos.append(info)
|
|
80
|
+
|
|
81
|
+
return infos
|
|
82
|
+
except Exception as e:
|
|
83
|
+
typer.echo(f"Error listing workflows: {e}")
|
|
84
|
+
return []
|
|
85
|
+
finally:
|
|
86
|
+
if sys_db:
|
|
87
|
+
sys_db.destroy()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_workflow(
|
|
91
|
+
config: ConfigFile, uuid: str, request: bool
|
|
92
|
+
) -> Optional[WorkflowInformation]:
|
|
93
|
+
sys_db = None
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
sys_db = SystemDatabase(config)
|
|
97
|
+
|
|
98
|
+
info = _get_workflow_info(sys_db, uuid, request)
|
|
99
|
+
return info
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
typer.echo(f"Error getting workflow: {e}")
|
|
103
|
+
return None
|
|
104
|
+
finally:
|
|
105
|
+
if sys_db:
|
|
106
|
+
sys_db.destroy()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cancel_workflow(config: ConfigFile, uuid: str) -> None:
|
|
110
|
+
# config = load_config()
|
|
111
|
+
sys_db = None
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
sys_db = SystemDatabase(config)
|
|
115
|
+
sys_db.set_workflow_status(uuid, WorkflowStatusString.CANCELLED, False)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
typer.echo(f"Failed to connect to DBOS system database: {e}")
|
|
120
|
+
return None
|
|
121
|
+
finally:
|
|
122
|
+
if sys_db:
|
|
123
|
+
sys_db.destroy()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _reattempt_workflow(uuid: str, startNewWorkflow: bool) -> None:
|
|
127
|
+
print(f"Reattempt workflow info for {uuid} not implemented")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _get_workflow_info(
|
|
132
|
+
sys_db: SystemDatabase, workflowUUID: str, getRequest: bool
|
|
133
|
+
) -> Optional[WorkflowInformation]:
|
|
134
|
+
|
|
135
|
+
info = sys_db.get_workflow_status(workflowUUID)
|
|
136
|
+
if info is None:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
winfo = WorkflowInformation()
|
|
140
|
+
|
|
141
|
+
winfo.workflowUUID = workflowUUID
|
|
142
|
+
winfo.status = info["status"]
|
|
143
|
+
winfo.workflowName = info["name"]
|
|
144
|
+
winfo.workflowClassName = info["class_name"]
|
|
145
|
+
winfo.workflowConfigName = info["config_name"]
|
|
146
|
+
winfo.executor_id = info["executor_id"]
|
|
147
|
+
winfo.app_version = info["app_version"]
|
|
148
|
+
winfo.app_id = info["app_id"]
|
|
149
|
+
winfo.recovery_attempts = info["recovery_attempts"]
|
|
150
|
+
winfo.authenticated_user = info["authenticated_user"]
|
|
151
|
+
winfo.assumed_role = info["assumed_role"]
|
|
152
|
+
winfo.authenticated_roles = info["authenticated_roles"]
|
|
153
|
+
winfo.queue_name = info["queue_name"]
|
|
154
|
+
|
|
155
|
+
# no input field
|
|
156
|
+
input_data = sys_db.get_workflow_inputs(workflowUUID)
|
|
157
|
+
if input_data is not None:
|
|
158
|
+
winfo.input = input_data
|
|
159
|
+
|
|
160
|
+
if info.get("status") == "SUCCESS":
|
|
161
|
+
result = sys_db.await_workflow_result(workflowUUID)
|
|
162
|
+
winfo.output = result
|
|
163
|
+
elif info.get("status") == "ERROR":
|
|
164
|
+
try:
|
|
165
|
+
sys_db.await_workflow_result(workflowUUID)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
winfo.error = str(e)
|
|
168
|
+
|
|
169
|
+
if not getRequest:
|
|
170
|
+
winfo.request = None
|
|
171
|
+
|
|
172
|
+
return winfo
|
dbos/cli.py
CHANGED
|
@@ -8,6 +8,7 @@ import typing
|
|
|
8
8
|
from os import path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
import jsonpickle # type: ignore
|
|
11
12
|
import sqlalchemy as sa
|
|
12
13
|
import tomlkit
|
|
13
14
|
import typer
|
|
@@ -17,12 +18,21 @@ from typing_extensions import Annotated
|
|
|
17
18
|
|
|
18
19
|
from dbos._schemas.system_database import SystemSchema
|
|
19
20
|
|
|
20
|
-
from . import load_config
|
|
21
|
+
from . import _serialization, load_config
|
|
21
22
|
from ._app_db import ApplicationDatabase
|
|
22
23
|
from ._dbos_config import _is_valid_app_name
|
|
23
24
|
from ._sys_db import SystemDatabase
|
|
25
|
+
from ._workflow_commands import (
|
|
26
|
+
_cancel_workflow,
|
|
27
|
+
_get_workflow,
|
|
28
|
+
_list_workflows,
|
|
29
|
+
_reattempt_workflow,
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
app = typer.Typer()
|
|
33
|
+
workflow = typer.Typer()
|
|
34
|
+
|
|
35
|
+
app.add_typer(workflow, name="workflow", help="Manage DBOS workflows")
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
def _on_windows() -> bool:
|
|
@@ -333,5 +343,94 @@ def reset(
|
|
|
333
343
|
sys_db.destroy()
|
|
334
344
|
|
|
335
345
|
|
|
346
|
+
@workflow.command(help="List workflows for your application")
|
|
347
|
+
def list(
|
|
348
|
+
limit: Annotated[
|
|
349
|
+
int,
|
|
350
|
+
typer.Option("--limit", "-l", help="Limit the results returned"),
|
|
351
|
+
] = 10,
|
|
352
|
+
user: Annotated[
|
|
353
|
+
typing.Optional[str],
|
|
354
|
+
typer.Option("--user", "-u", help="Retrieve workflows run by this user"),
|
|
355
|
+
] = None,
|
|
356
|
+
starttime: Annotated[
|
|
357
|
+
typing.Optional[str],
|
|
358
|
+
typer.Option(
|
|
359
|
+
"--start-time",
|
|
360
|
+
"-s",
|
|
361
|
+
help="Retrieve workflows starting after this timestamp (ISO 8601 format)",
|
|
362
|
+
),
|
|
363
|
+
] = None,
|
|
364
|
+
endtime: Annotated[
|
|
365
|
+
typing.Optional[str],
|
|
366
|
+
typer.Option(
|
|
367
|
+
"--end-time",
|
|
368
|
+
"-e",
|
|
369
|
+
help="Retrieve workflows starting before this timestamp (ISO 8601 format)",
|
|
370
|
+
),
|
|
371
|
+
] = None,
|
|
372
|
+
status: Annotated[
|
|
373
|
+
typing.Optional[str],
|
|
374
|
+
typer.Option(
|
|
375
|
+
"--status",
|
|
376
|
+
"-S",
|
|
377
|
+
help="Retrieve workflows with this status (PENDING, SUCCESS, ERROR, RETRIES_EXCEEDED, ENQUEUED, or CANCELLED)",
|
|
378
|
+
),
|
|
379
|
+
] = None,
|
|
380
|
+
appversion: Annotated[
|
|
381
|
+
typing.Optional[str],
|
|
382
|
+
typer.Option(
|
|
383
|
+
"--application-version",
|
|
384
|
+
"-v",
|
|
385
|
+
help="Retrieve workflows with this application version",
|
|
386
|
+
),
|
|
387
|
+
] = None,
|
|
388
|
+
request: Annotated[
|
|
389
|
+
bool,
|
|
390
|
+
typer.Option("--request", help="Retrieve workflow request information"),
|
|
391
|
+
] = True,
|
|
392
|
+
appdir: Annotated[
|
|
393
|
+
typing.Optional[str],
|
|
394
|
+
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
395
|
+
] = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
config = load_config()
|
|
398
|
+
workflows = _list_workflows(
|
|
399
|
+
config, limit, user, starttime, endtime, status, request, appversion
|
|
400
|
+
)
|
|
401
|
+
print(jsonpickle.encode(workflows, unpicklable=False))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@workflow.command(help="Retrieve the status of a workflow")
|
|
405
|
+
def get(
|
|
406
|
+
uuid: Annotated[str, typer.Argument()],
|
|
407
|
+
appdir: Annotated[
|
|
408
|
+
typing.Optional[str],
|
|
409
|
+
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
410
|
+
] = None,
|
|
411
|
+
request: Annotated[
|
|
412
|
+
bool,
|
|
413
|
+
typer.Option("--request", help="Retrieve workflow request information"),
|
|
414
|
+
] = True,
|
|
415
|
+
) -> None:
|
|
416
|
+
config = load_config()
|
|
417
|
+
print(jsonpickle.encode(_get_workflow(config, uuid, request), unpicklable=False))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@workflow.command(
|
|
421
|
+
help="Cancel a workflow so it is no longer automatically retried or restarted"
|
|
422
|
+
)
|
|
423
|
+
def cancel(
|
|
424
|
+
uuid: Annotated[str, typer.Argument()],
|
|
425
|
+
appdir: Annotated[
|
|
426
|
+
typing.Optional[str],
|
|
427
|
+
typer.Option("--app-dir", "-d", help="Specify the application root directory"),
|
|
428
|
+
] = None,
|
|
429
|
+
) -> None:
|
|
430
|
+
config = load_config()
|
|
431
|
+
_cancel_workflow(config, uuid)
|
|
432
|
+
print(f"Workflow {uuid} has been cancelled")
|
|
433
|
+
|
|
434
|
+
|
|
336
435
|
if __name__ == "__main__":
|
|
337
436
|
app()
|
dbos/dbos-config.schema.json
CHANGED
|
@@ -81,13 +81,7 @@
|
|
|
81
81
|
"type": "array",
|
|
82
82
|
"description": "Specify a list of user DB rollback commands to run"
|
|
83
83
|
}
|
|
84
|
-
}
|
|
85
|
-
"required": [
|
|
86
|
-
"hostname",
|
|
87
|
-
"port",
|
|
88
|
-
"username",
|
|
89
|
-
"password"
|
|
90
|
-
]
|
|
84
|
+
}
|
|
91
85
|
},
|
|
92
86
|
"telemetry": {
|
|
93
87
|
"type": "object",
|
|
@@ -181,9 +175,6 @@
|
|
|
181
175
|
"type": "string",
|
|
182
176
|
"deprecated": true
|
|
183
177
|
}
|
|
184
|
-
}
|
|
185
|
-
"required": [
|
|
186
|
-
"database"
|
|
187
|
-
]
|
|
178
|
+
}
|
|
188
179
|
}
|
|
189
180
|
|