dbos 0.25.0a3__py3-none-any.whl → 0.25.0a8__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/__init__.py +2 -1
- dbos/__main__.py +3 -0
- dbos/_admin_server.py +20 -2
- dbos/_conductor/conductor.py +1 -1
- dbos/_conductor/protocol.py +13 -7
- dbos/_context.py +48 -0
- dbos/_core.py +76 -12
- dbos/_dbos.py +112 -61
- dbos/_fastapi.py +4 -1
- dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +46 -0
- dbos/_outcome.py +6 -2
- dbos/_schemas/system_database.py +2 -0
- dbos/_sys_db.py +80 -26
- dbos/_templates/dbos-db-starter/__package/main.py +6 -11
- dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +2 -4
- dbos/_workflow_commands.py +90 -63
- dbos/cli/_template_init.py +8 -3
- dbos/cli/cli.py +17 -1
- {dbos-0.25.0a3.dist-info → dbos-0.25.0a8.dist-info}/METADATA +1 -1
- {dbos-0.25.0a3.dist-info → dbos-0.25.0a8.dist-info}/RECORD +23 -22
- {dbos-0.25.0a3.dist-info → dbos-0.25.0a8.dist-info}/WHEEL +0 -0
- {dbos-0.25.0a3.dist-info → dbos-0.25.0a8.dist-info}/entry_points.txt +0 -0
- {dbos-0.25.0a3.dist-info → dbos-0.25.0a8.dist-info}/licenses/LICENSE +0 -0
dbos/__init__.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from . import _error as error
|
|
2
2
|
from ._context import DBOSContextEnsure, DBOSContextSetAuth, SetWorkflowID
|
|
3
|
-
from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
|
|
3
|
+
from ._dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle
|
|
4
4
|
from ._dbos_config import ConfigFile, DBOSConfig, get_dbos_database_url, load_config
|
|
5
5
|
from ._kafka_message import KafkaMessage
|
|
6
6
|
from ._queue import Queue
|
|
7
7
|
from ._sys_db import GetWorkflowsInput, WorkflowStatusString
|
|
8
|
+
from ._workflow_commands import WorkflowStatus
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"ConfigFile",
|
dbos/__main__.py
CHANGED
dbos/_admin_server.py
CHANGED
|
@@ -7,6 +7,8 @@ from functools import partial
|
|
|
7
7
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
8
|
from typing import TYPE_CHECKING, Any, List, TypedDict
|
|
9
9
|
|
|
10
|
+
import jsonpickle # type: ignore
|
|
11
|
+
|
|
10
12
|
from ._logger import dbos_logger
|
|
11
13
|
from ._recovery import recover_pending_workflows
|
|
12
14
|
|
|
@@ -20,6 +22,7 @@ _workflow_queues_metadata_path = "/dbos-workflow-queues-metadata"
|
|
|
20
22
|
# /workflows/:workflow_id/cancel
|
|
21
23
|
# /workflows/:workflow_id/resume
|
|
22
24
|
# /workflows/:workflow_id/restart
|
|
25
|
+
# /workflows/:workflow_id/steps
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class AdminServer:
|
|
@@ -86,8 +89,16 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
86
89
|
self._end_headers()
|
|
87
90
|
self.wfile.write(json.dumps(queue_metadata_array).encode("utf-8"))
|
|
88
91
|
else:
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
steps_match = re.match(
|
|
93
|
+
r"^/workflows/(?P<workflow_id>[^/]+)/steps$", self.path
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if steps_match:
|
|
97
|
+
workflow_id = steps_match.group("workflow_id")
|
|
98
|
+
self._handle_steps(workflow_id)
|
|
99
|
+
else:
|
|
100
|
+
self.send_response(404)
|
|
101
|
+
self._end_headers()
|
|
91
102
|
|
|
92
103
|
def do_POST(self) -> None:
|
|
93
104
|
content_length = int(
|
|
@@ -149,6 +160,13 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
|
|
|
149
160
|
self.send_response(204)
|
|
150
161
|
self._end_headers()
|
|
151
162
|
|
|
163
|
+
def _handle_steps(self, workflow_id: str) -> None:
|
|
164
|
+
steps = self.dbos._sys_db.get_workflow_steps(workflow_id)
|
|
165
|
+
json_steps = jsonpickle.encode(steps, unpicklable=False).encode("utf-8")
|
|
166
|
+
self.send_response(200)
|
|
167
|
+
self._end_headers()
|
|
168
|
+
self.wfile.write(json_steps)
|
|
169
|
+
|
|
152
170
|
|
|
153
171
|
# Be consistent with DBOS-TS response.
|
|
154
172
|
class PerfUtilization(TypedDict):
|
dbos/_conductor/conductor.py
CHANGED
|
@@ -203,7 +203,7 @@ class ConductorWebsocket(threading.Thread):
|
|
|
203
203
|
info = get_workflow(
|
|
204
204
|
self.dbos._sys_db,
|
|
205
205
|
get_workflow_message.workflow_id,
|
|
206
|
-
|
|
206
|
+
get_request=False,
|
|
207
207
|
)
|
|
208
208
|
except Exception as e:
|
|
209
209
|
error_message = f"Exception encountered when getting workflow {get_workflow_message.workflow_id}: {traceback.format_exc()}"
|
dbos/_conductor/protocol.py
CHANGED
|
@@ -3,7 +3,7 @@ from dataclasses import asdict, dataclass
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import List, Optional, Type, TypedDict, TypeVar
|
|
5
5
|
|
|
6
|
-
from dbos._workflow_commands import
|
|
6
|
+
from dbos._workflow_commands import WorkflowStatus
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MessageType(str, Enum):
|
|
@@ -141,27 +141,33 @@ class WorkflowsOutput:
|
|
|
141
141
|
ExecutorID: Optional[str]
|
|
142
142
|
|
|
143
143
|
@classmethod
|
|
144
|
-
def from_workflow_information(cls, info:
|
|
144
|
+
def from_workflow_information(cls, info: WorkflowStatus) -> "WorkflowsOutput":
|
|
145
145
|
# Convert fields to strings as needed
|
|
146
146
|
created_at_str = str(info.created_at) if info.created_at is not None else None
|
|
147
147
|
updated_at_str = str(info.updated_at) if info.updated_at is not None else None
|
|
148
148
|
inputs_str = str(info.input) if info.input is not None else None
|
|
149
149
|
outputs_str = str(info.output) if info.output is not None else None
|
|
150
|
+
error_str = str(info.error) if info.error is not None else None
|
|
150
151
|
request_str = str(info.request) if info.request is not None else None
|
|
152
|
+
roles_str = (
|
|
153
|
+
str(info.authenticated_roles)
|
|
154
|
+
if info.authenticated_roles is not None
|
|
155
|
+
else None
|
|
156
|
+
)
|
|
151
157
|
|
|
152
158
|
return cls(
|
|
153
159
|
WorkflowUUID=info.workflow_id,
|
|
154
160
|
Status=info.status,
|
|
155
|
-
WorkflowName=info.
|
|
156
|
-
WorkflowClassName=info.
|
|
157
|
-
WorkflowConfigName=info.
|
|
161
|
+
WorkflowName=info.name,
|
|
162
|
+
WorkflowClassName=info.class_name,
|
|
163
|
+
WorkflowConfigName=info.config_name,
|
|
158
164
|
AuthenticatedUser=info.authenticated_user,
|
|
159
165
|
AssumedRole=info.assumed_role,
|
|
160
|
-
AuthenticatedRoles=
|
|
166
|
+
AuthenticatedRoles=roles_str,
|
|
161
167
|
Input=inputs_str,
|
|
162
168
|
Output=outputs_str,
|
|
163
169
|
Request=request_str,
|
|
164
|
-
Error=
|
|
170
|
+
Error=error_str,
|
|
165
171
|
CreatedAt=created_at_str,
|
|
166
172
|
UpdatedAt=updated_at_str,
|
|
167
173
|
QueueName=info.queue_name,
|
dbos/_context.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import uuid
|
|
6
6
|
from contextlib import AbstractContextManager
|
|
7
7
|
from contextvars import ContextVar
|
|
8
|
+
from dataclasses import dataclass
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from types import TracebackType
|
|
10
11
|
from typing import List, Literal, Optional, Type, TypedDict
|
|
@@ -48,6 +49,23 @@ class TracedAttributes(TypedDict, total=False):
|
|
|
48
49
|
authenticatedUserAssumedRole: Optional[str]
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@dataclass
|
|
53
|
+
class StepStatus:
|
|
54
|
+
"""
|
|
55
|
+
Status of a step execution.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
step_id: The unique ID of this step in its workflow.
|
|
59
|
+
current_attempt: For steps with automatic retries, which attempt number (zero-indexed) is currently executing.
|
|
60
|
+
max_attempts: For steps with automatic retries, the maximum number of attempts that will be made before the step fails.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
step_id: int
|
|
65
|
+
current_attempt: Optional[int]
|
|
66
|
+
max_attempts: Optional[int]
|
|
67
|
+
|
|
68
|
+
|
|
51
69
|
class DBOSContext:
|
|
52
70
|
def __init__(self) -> None:
|
|
53
71
|
self.executor_id = GlobalParams.executor_id
|
|
@@ -73,6 +91,7 @@ class DBOSContext:
|
|
|
73
91
|
self.authenticated_user: Optional[str] = None
|
|
74
92
|
self.authenticated_roles: Optional[List[str]] = None
|
|
75
93
|
self.assumed_role: Optional[str] = None
|
|
94
|
+
self.step_status: Optional[StepStatus] = None
|
|
76
95
|
|
|
77
96
|
def create_child(self) -> DBOSContext:
|
|
78
97
|
rv = DBOSContext()
|
|
@@ -92,6 +111,9 @@ class DBOSContext:
|
|
|
92
111
|
rv.assumed_role = self.assumed_role
|
|
93
112
|
return rv
|
|
94
113
|
|
|
114
|
+
def has_parent(self) -> bool:
|
|
115
|
+
return len(self.parent_workflow_id) > 0
|
|
116
|
+
|
|
95
117
|
def assign_workflow_id(self) -> str:
|
|
96
118
|
if len(self.id_assigned_for_next_workflow) > 0:
|
|
97
119
|
wfid = self.id_assigned_for_next_workflow
|
|
@@ -147,10 +169,12 @@ class DBOSContext:
|
|
|
147
169
|
attributes: TracedAttributes,
|
|
148
170
|
) -> None:
|
|
149
171
|
self.curr_step_function_id = fid
|
|
172
|
+
self.step_status = StepStatus(fid, None, None)
|
|
150
173
|
self._start_span(attributes)
|
|
151
174
|
|
|
152
175
|
def end_step(self, exc_value: Optional[BaseException]) -> None:
|
|
153
176
|
self.curr_step_function_id = -1
|
|
177
|
+
self.step_status = None
|
|
154
178
|
self._end_span(exc_value)
|
|
155
179
|
|
|
156
180
|
def start_transaction(
|
|
@@ -429,6 +453,30 @@ class EnterDBOSStep:
|
|
|
429
453
|
return False # Did not handle
|
|
430
454
|
|
|
431
455
|
|
|
456
|
+
class EnterDBOSStepRetry:
|
|
457
|
+
def __init__(self, current_attempt: int, max_attempts: int) -> None:
|
|
458
|
+
self.current_attempt = current_attempt
|
|
459
|
+
self.max_attempts = max_attempts
|
|
460
|
+
|
|
461
|
+
def __enter__(self) -> None:
|
|
462
|
+
ctx = get_local_dbos_context()
|
|
463
|
+
if ctx is not None and ctx.step_status is not None:
|
|
464
|
+
ctx.step_status.current_attempt = self.current_attempt
|
|
465
|
+
ctx.step_status.max_attempts = self.max_attempts
|
|
466
|
+
|
|
467
|
+
def __exit__(
|
|
468
|
+
self,
|
|
469
|
+
exc_type: Optional[Type[BaseException]],
|
|
470
|
+
exc_value: Optional[BaseException],
|
|
471
|
+
traceback: Optional[TracebackType],
|
|
472
|
+
) -> Literal[False]:
|
|
473
|
+
ctx = get_local_dbos_context()
|
|
474
|
+
if ctx is not None and ctx.step_status is not None:
|
|
475
|
+
ctx.step_status.current_attempt = None
|
|
476
|
+
ctx.step_status.max_attempts = None
|
|
477
|
+
return False # Did not handle
|
|
478
|
+
|
|
479
|
+
|
|
432
480
|
class EnterDBOSTransaction:
|
|
433
481
|
def __init__(self, sqls: Session, attributes: TracedAttributes) -> None:
|
|
434
482
|
self.sqls = sqls
|
dbos/_core.py
CHANGED
|
@@ -84,10 +84,10 @@ if TYPE_CHECKING:
|
|
|
84
84
|
Workflow,
|
|
85
85
|
WorkflowHandle,
|
|
86
86
|
WorkflowHandleAsync,
|
|
87
|
-
WorkflowStatus,
|
|
88
87
|
DBOSRegistry,
|
|
89
88
|
IsolationLevel,
|
|
90
89
|
)
|
|
90
|
+
from ._workflow_commands import WorkflowStatus
|
|
91
91
|
|
|
92
92
|
from sqlalchemy.exc import DBAPIError, InvalidRequestError
|
|
93
93
|
|
|
@@ -243,12 +243,15 @@ def _init_workflow(
|
|
|
243
243
|
wf_status = dbos._sys_db.insert_workflow_status(
|
|
244
244
|
status, max_recovery_attempts=max_recovery_attempts
|
|
245
245
|
)
|
|
246
|
+
|
|
246
247
|
# TODO: Modify the inputs if they were changed by `update_workflow_inputs`
|
|
247
248
|
dbos._sys_db.update_workflow_inputs(
|
|
248
249
|
wfid, _serialization.serialize_args(inputs)
|
|
249
250
|
)
|
|
251
|
+
|
|
250
252
|
else:
|
|
251
253
|
# Buffer the inputs for single-transaction workflows, but don't buffer the status
|
|
254
|
+
|
|
252
255
|
dbos._sys_db.buffer_workflow_inputs(
|
|
253
256
|
wfid, _serialization.serialize_args(inputs)
|
|
254
257
|
)
|
|
@@ -475,6 +478,15 @@ def start_workflow(
|
|
|
475
478
|
|
|
476
479
|
new_wf_id, new_wf_ctx = _get_new_wf()
|
|
477
480
|
|
|
481
|
+
ctx = new_wf_ctx
|
|
482
|
+
new_child_workflow_id = ctx.id_assigned_for_next_workflow
|
|
483
|
+
if ctx.has_parent():
|
|
484
|
+
child_workflow_id = dbos._sys_db.check_child_workflow(
|
|
485
|
+
ctx.parent_workflow_id, ctx.parent_workflow_fid
|
|
486
|
+
)
|
|
487
|
+
if child_workflow_id is not None:
|
|
488
|
+
return WorkflowHandlePolling(child_workflow_id, dbos)
|
|
489
|
+
|
|
478
490
|
status = _init_workflow(
|
|
479
491
|
dbos,
|
|
480
492
|
new_wf_ctx,
|
|
@@ -488,6 +500,13 @@ def start_workflow(
|
|
|
488
500
|
)
|
|
489
501
|
|
|
490
502
|
wf_status = status["status"]
|
|
503
|
+
if ctx.has_parent():
|
|
504
|
+
dbos._sys_db.record_child_workflow(
|
|
505
|
+
ctx.parent_workflow_id,
|
|
506
|
+
new_child_workflow_id,
|
|
507
|
+
ctx.parent_workflow_fid,
|
|
508
|
+
func.__name__,
|
|
509
|
+
)
|
|
491
510
|
|
|
492
511
|
if not execute_workflow or (
|
|
493
512
|
not dbos.debug_mode
|
|
@@ -496,9 +515,6 @@ def start_workflow(
|
|
|
496
515
|
or wf_status == WorkflowStatusString.SUCCESS.value
|
|
497
516
|
)
|
|
498
517
|
):
|
|
499
|
-
dbos.logger.debug(
|
|
500
|
-
f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
|
|
501
|
-
)
|
|
502
518
|
return WorkflowHandlePolling(new_wf_id, dbos)
|
|
503
519
|
|
|
504
520
|
future = dbos._executor.submit(
|
|
@@ -544,6 +560,17 @@ async def start_workflow_async(
|
|
|
544
560
|
|
|
545
561
|
new_wf_id, new_wf_ctx = _get_new_wf()
|
|
546
562
|
|
|
563
|
+
ctx = new_wf_ctx
|
|
564
|
+
new_child_workflow_id = ctx.id_assigned_for_next_workflow
|
|
565
|
+
if ctx.has_parent():
|
|
566
|
+
child_workflow_id = await asyncio.to_thread(
|
|
567
|
+
dbos._sys_db.check_child_workflow,
|
|
568
|
+
ctx.parent_workflow_id,
|
|
569
|
+
ctx.parent_workflow_fid,
|
|
570
|
+
)
|
|
571
|
+
if child_workflow_id is not None:
|
|
572
|
+
return WorkflowHandleAsyncPolling(child_workflow_id, dbos)
|
|
573
|
+
|
|
547
574
|
status = await asyncio.to_thread(
|
|
548
575
|
_init_workflow,
|
|
549
576
|
dbos,
|
|
@@ -557,6 +584,15 @@ async def start_workflow_async(
|
|
|
557
584
|
max_recovery_attempts=fi.max_recovery_attempts,
|
|
558
585
|
)
|
|
559
586
|
|
|
587
|
+
if ctx.has_parent():
|
|
588
|
+
await asyncio.to_thread(
|
|
589
|
+
dbos._sys_db.record_child_workflow,
|
|
590
|
+
ctx.parent_workflow_id,
|
|
591
|
+
new_child_workflow_id,
|
|
592
|
+
ctx.parent_workflow_fid,
|
|
593
|
+
func.__name__,
|
|
594
|
+
)
|
|
595
|
+
|
|
560
596
|
wf_status = status["status"]
|
|
561
597
|
|
|
562
598
|
if not execute_workflow or (
|
|
@@ -566,9 +602,6 @@ async def start_workflow_async(
|
|
|
566
602
|
or wf_status == WorkflowStatusString.SUCCESS.value
|
|
567
603
|
)
|
|
568
604
|
):
|
|
569
|
-
dbos.logger.debug(
|
|
570
|
-
f"Workflow {new_wf_id} already completed with status {wf_status}. Directly returning a workflow handle."
|
|
571
|
-
)
|
|
572
605
|
return WorkflowHandleAsyncPolling(new_wf_id, dbos)
|
|
573
606
|
|
|
574
607
|
coro = _execute_workflow_async(dbos, status, func, new_wf_ctx, *args, **kwargs)
|
|
@@ -599,6 +632,8 @@ def workflow_wrapper(
|
|
|
599
632
|
) -> Callable[P, R]:
|
|
600
633
|
func.__orig_func = func # type: ignore
|
|
601
634
|
|
|
635
|
+
funcName = func.__name__
|
|
636
|
+
|
|
602
637
|
fi = get_or_create_func_info(func)
|
|
603
638
|
fi.max_recovery_attempts = max_recovery_attempts
|
|
604
639
|
|
|
@@ -629,7 +664,24 @@ def workflow_wrapper(
|
|
|
629
664
|
wfOutcome = Outcome[R].make(functools.partial(func, *args, **kwargs))
|
|
630
665
|
|
|
631
666
|
def init_wf() -> Callable[[Callable[[], R]], R]:
|
|
667
|
+
|
|
668
|
+
def recorded_result(
|
|
669
|
+
c_wfid: str, dbos: "DBOS"
|
|
670
|
+
) -> Callable[[Callable[[], R]], R]:
|
|
671
|
+
def recorded_result_inner(func: Callable[[], R]) -> R:
|
|
672
|
+
return WorkflowHandlePolling(c_wfid, dbos).get_result()
|
|
673
|
+
|
|
674
|
+
return recorded_result_inner
|
|
675
|
+
|
|
632
676
|
ctx = assert_current_dbos_context() # Now the child ctx
|
|
677
|
+
|
|
678
|
+
if ctx.has_parent():
|
|
679
|
+
child_workflow_id = dbos._sys_db.check_child_workflow(
|
|
680
|
+
ctx.parent_workflow_id, ctx.parent_workflow_fid
|
|
681
|
+
)
|
|
682
|
+
if child_workflow_id is not None:
|
|
683
|
+
return recorded_result(child_workflow_id, dbos)
|
|
684
|
+
|
|
633
685
|
status = _init_workflow(
|
|
634
686
|
dbos,
|
|
635
687
|
ctx,
|
|
@@ -640,11 +692,20 @@ def workflow_wrapper(
|
|
|
640
692
|
temp_wf_type=get_temp_workflow_type(func),
|
|
641
693
|
max_recovery_attempts=max_recovery_attempts,
|
|
642
694
|
)
|
|
695
|
+
|
|
643
696
|
# TODO: maybe modify the parameters if they've been changed by `_init_workflow`
|
|
644
697
|
dbos.logger.debug(
|
|
645
698
|
f"Running workflow, id: {ctx.workflow_id}, name: {get_dbos_func_name(func)}"
|
|
646
699
|
)
|
|
647
700
|
|
|
701
|
+
if ctx.has_parent():
|
|
702
|
+
dbos._sys_db.record_child_workflow(
|
|
703
|
+
ctx.parent_workflow_id,
|
|
704
|
+
ctx.workflow_id,
|
|
705
|
+
ctx.parent_workflow_fid,
|
|
706
|
+
funcName,
|
|
707
|
+
)
|
|
708
|
+
|
|
648
709
|
return _get_wf_invoke_func(dbos, status)
|
|
649
710
|
|
|
650
711
|
outcome = (
|
|
@@ -853,6 +914,8 @@ def decorate_step(
|
|
|
853
914
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
854
915
|
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
855
916
|
|
|
917
|
+
stepName = func.__name__
|
|
918
|
+
|
|
856
919
|
def invoke_step(*args: Any, **kwargs: Any) -> Any:
|
|
857
920
|
if dbosreg.dbos is None:
|
|
858
921
|
raise DBOSException(
|
|
@@ -877,7 +940,7 @@ def decorate_step(
|
|
|
877
940
|
|
|
878
941
|
def on_exception(attempt: int, error: BaseException) -> float:
|
|
879
942
|
dbos.logger.warning(
|
|
880
|
-
f"Step being automatically retried. (attempt {attempt} of {attempts}). {traceback.format_exc()}"
|
|
943
|
+
f"Step being automatically retried. (attempt {attempt + 1} of {attempts}). {traceback.format_exc()}"
|
|
881
944
|
)
|
|
882
945
|
ctx = assert_current_dbos_context()
|
|
883
946
|
ctx.get_current_span().add_event(
|
|
@@ -897,19 +960,20 @@ def decorate_step(
|
|
|
897
960
|
step_output: OperationResultInternal = {
|
|
898
961
|
"workflow_uuid": ctx.workflow_id,
|
|
899
962
|
"function_id": ctx.function_id,
|
|
963
|
+
"function_name": stepName,
|
|
900
964
|
"output": None,
|
|
901
965
|
"error": None,
|
|
902
966
|
}
|
|
903
967
|
|
|
904
968
|
try:
|
|
905
969
|
output = func()
|
|
906
|
-
step_output["output"] = _serialization.serialize(output)
|
|
907
|
-
return output
|
|
908
970
|
except Exception as error:
|
|
909
971
|
step_output["error"] = _serialization.serialize_exception(error)
|
|
910
|
-
raise
|
|
911
|
-
finally:
|
|
912
972
|
dbos._sys_db.record_operation_result(step_output)
|
|
973
|
+
raise
|
|
974
|
+
step_output["output"] = _serialization.serialize(output)
|
|
975
|
+
dbos._sys_db.record_operation_result(step_output)
|
|
976
|
+
return output
|
|
913
977
|
|
|
914
978
|
def check_existing_result() -> Union[NoResult, R]:
|
|
915
979
|
ctx = assert_current_dbos_context()
|
dbos/_dbos.py
CHANGED
|
@@ -11,7 +11,6 @@ import threading
|
|
|
11
11
|
import traceback
|
|
12
12
|
import uuid
|
|
13
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
-
from dataclasses import dataclass
|
|
15
14
|
from logging import Logger
|
|
16
15
|
from typing import (
|
|
17
16
|
TYPE_CHECKING,
|
|
@@ -28,17 +27,23 @@ from typing import (
|
|
|
28
27
|
TypeVar,
|
|
29
28
|
Union,
|
|
30
29
|
cast,
|
|
31
|
-
overload,
|
|
32
30
|
)
|
|
33
31
|
|
|
34
32
|
from opentelemetry.trace import Span
|
|
35
33
|
|
|
34
|
+
from dbos import _serialization
|
|
36
35
|
from dbos._conductor.conductor import ConductorWebsocket
|
|
37
36
|
from dbos._utils import GlobalParams
|
|
37
|
+
from dbos._workflow_commands import (
|
|
38
|
+
WorkflowStatus,
|
|
39
|
+
list_queued_workflows,
|
|
40
|
+
list_workflows,
|
|
41
|
+
)
|
|
38
42
|
|
|
39
43
|
from ._classproperty import classproperty
|
|
40
44
|
from ._core import (
|
|
41
45
|
TEMP_SEND_WF_NAME,
|
|
46
|
+
WorkflowHandleAsyncPolling,
|
|
42
47
|
WorkflowHandlePolling,
|
|
43
48
|
decorate_step,
|
|
44
49
|
decorate_transaction,
|
|
@@ -85,6 +90,7 @@ from ._admin_server import AdminServer
|
|
|
85
90
|
from ._app_db import ApplicationDatabase
|
|
86
91
|
from ._context import (
|
|
87
92
|
EnterDBOSStep,
|
|
93
|
+
StepStatus,
|
|
88
94
|
TracedAttributes,
|
|
89
95
|
assert_current_dbos_context,
|
|
90
96
|
get_local_dbos_context,
|
|
@@ -107,6 +113,7 @@ from ._error import (
|
|
|
107
113
|
)
|
|
108
114
|
from ._logger import add_otlp_to_all_loggers, config_logger, dbos_logger, init_logger
|
|
109
115
|
from ._sys_db import SystemDatabase
|
|
116
|
+
from ._workflow_commands import WorkflowStatus, get_workflow
|
|
110
117
|
|
|
111
118
|
# Most DBOS functions are just any callable F, so decorators / wrappers work on F
|
|
112
119
|
# There are cases where the parameters P and return value R should be separate
|
|
@@ -728,34 +735,39 @@ class DBOS:
|
|
|
728
735
|
@classmethod
|
|
729
736
|
def get_workflow_status(cls, workflow_id: str) -> Optional[WorkflowStatus]:
|
|
730
737
|
"""Return the status of a workflow execution."""
|
|
738
|
+
sys_db = _get_dbos_instance()._sys_db
|
|
731
739
|
ctx = get_local_dbos_context()
|
|
732
740
|
if ctx and ctx.is_within_workflow():
|
|
733
741
|
ctx.function_id += 1
|
|
734
|
-
|
|
735
|
-
|
|
742
|
+
res = sys_db.check_operation_execution(ctx.workflow_id, ctx.function_id)
|
|
743
|
+
if res is not None:
|
|
744
|
+
if res["output"]:
|
|
745
|
+
resstat: WorkflowStatus = _serialization.deserialize(res["output"])
|
|
746
|
+
return resstat
|
|
747
|
+
else:
|
|
748
|
+
raise DBOSException(
|
|
749
|
+
"Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
|
|
750
|
+
)
|
|
751
|
+
stat = get_workflow(_get_dbos_instance()._sys_db, workflow_id, True)
|
|
752
|
+
|
|
753
|
+
if ctx and ctx.is_within_workflow():
|
|
754
|
+
sys_db.record_operation_result(
|
|
755
|
+
{
|
|
756
|
+
"workflow_uuid": ctx.workflow_id,
|
|
757
|
+
"function_id": ctx.function_id,
|
|
758
|
+
"function_name": "DBOS.getStatus",
|
|
759
|
+
"output": _serialization.serialize(stat),
|
|
760
|
+
"error": None,
|
|
761
|
+
}
|
|
736
762
|
)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
name=stat["name"],
|
|
746
|
-
executor_id=stat["executor_id"],
|
|
747
|
-
recovery_attempts=stat["recovery_attempts"],
|
|
748
|
-
class_name=stat["class_name"],
|
|
749
|
-
config_name=stat["config_name"],
|
|
750
|
-
queue_name=stat["queue_name"],
|
|
751
|
-
authenticated_user=stat["authenticated_user"],
|
|
752
|
-
assumed_role=stat["assumed_role"],
|
|
753
|
-
authenticated_roles=(
|
|
754
|
-
json.loads(stat["authenticated_roles"])
|
|
755
|
-
if stat["authenticated_roles"] is not None
|
|
756
|
-
else None
|
|
757
|
-
),
|
|
758
|
-
)
|
|
763
|
+
return stat
|
|
764
|
+
|
|
765
|
+
@classmethod
|
|
766
|
+
async def get_workflow_status_async(
|
|
767
|
+
cls, workflow_id: str
|
|
768
|
+
) -> Optional[WorkflowStatus]:
|
|
769
|
+
"""Return the status of a workflow execution."""
|
|
770
|
+
return await asyncio.to_thread(cls.get_workflow_status, workflow_id)
|
|
759
771
|
|
|
760
772
|
@classmethod
|
|
761
773
|
def retrieve_workflow(
|
|
@@ -769,6 +781,18 @@ class DBOS:
|
|
|
769
781
|
raise DBOSNonExistentWorkflowError(workflow_id)
|
|
770
782
|
return WorkflowHandlePolling(workflow_id, dbos)
|
|
771
783
|
|
|
784
|
+
@classmethod
|
|
785
|
+
async def retrieve_workflow_async(
|
|
786
|
+
cls, workflow_id: str, existing_workflow: bool = True
|
|
787
|
+
) -> WorkflowHandleAsync[R]:
|
|
788
|
+
"""Return a `WorkflowHandle` for a workflow execution."""
|
|
789
|
+
dbos = _get_dbos_instance()
|
|
790
|
+
if existing_workflow:
|
|
791
|
+
stat = await dbos.get_workflow_status_async(workflow_id)
|
|
792
|
+
if stat is None:
|
|
793
|
+
raise DBOSNonExistentWorkflowError(workflow_id)
|
|
794
|
+
return WorkflowHandleAsyncPolling(workflow_id, dbos)
|
|
795
|
+
|
|
772
796
|
@classmethod
|
|
773
797
|
def send(
|
|
774
798
|
cls, destination_id: str, message: Any, topic: Optional[str] = None
|
|
@@ -943,6 +967,60 @@ class DBOS:
|
|
|
943
967
|
_get_or_create_dbos_registry().clear_workflow_cancelled(workflow_id)
|
|
944
968
|
return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
|
|
945
969
|
|
|
970
|
+
@classmethod
|
|
971
|
+
def list_workflows(
|
|
972
|
+
cls,
|
|
973
|
+
*,
|
|
974
|
+
workflow_ids: Optional[List[str]] = None,
|
|
975
|
+
status: Optional[str] = None,
|
|
976
|
+
start_time: Optional[str] = None,
|
|
977
|
+
end_time: Optional[str] = None,
|
|
978
|
+
name: Optional[str] = None,
|
|
979
|
+
app_version: Optional[str] = None,
|
|
980
|
+
user: Optional[str] = None,
|
|
981
|
+
limit: Optional[int] = None,
|
|
982
|
+
offset: Optional[int] = None,
|
|
983
|
+
sort_desc: bool = False,
|
|
984
|
+
) -> List[WorkflowStatus]:
|
|
985
|
+
return list_workflows(
|
|
986
|
+
_get_dbos_instance()._sys_db,
|
|
987
|
+
workflow_ids=workflow_ids,
|
|
988
|
+
status=status,
|
|
989
|
+
start_time=start_time,
|
|
990
|
+
end_time=end_time,
|
|
991
|
+
name=name,
|
|
992
|
+
app_version=app_version,
|
|
993
|
+
user=user,
|
|
994
|
+
limit=limit,
|
|
995
|
+
offset=offset,
|
|
996
|
+
sort_desc=sort_desc,
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
@classmethod
|
|
1000
|
+
def list_queued_workflows(
|
|
1001
|
+
cls,
|
|
1002
|
+
*,
|
|
1003
|
+
queue_name: Optional[str] = None,
|
|
1004
|
+
status: Optional[str] = None,
|
|
1005
|
+
start_time: Optional[str] = None,
|
|
1006
|
+
end_time: Optional[str] = None,
|
|
1007
|
+
name: Optional[str] = None,
|
|
1008
|
+
limit: Optional[int] = None,
|
|
1009
|
+
offset: Optional[int] = None,
|
|
1010
|
+
sort_desc: bool = False,
|
|
1011
|
+
) -> List[WorkflowStatus]:
|
|
1012
|
+
return list_queued_workflows(
|
|
1013
|
+
_get_dbos_instance()._sys_db,
|
|
1014
|
+
queue_name=queue_name,
|
|
1015
|
+
status=status,
|
|
1016
|
+
start_time=start_time,
|
|
1017
|
+
end_time=end_time,
|
|
1018
|
+
name=name,
|
|
1019
|
+
limit=limit,
|
|
1020
|
+
offset=offset,
|
|
1021
|
+
sort_desc=sort_desc,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
946
1024
|
@classproperty
|
|
947
1025
|
def logger(cls) -> Logger:
|
|
948
1026
|
"""Return the DBOS `Logger` for the current context."""
|
|
@@ -990,6 +1068,14 @@ class DBOS:
|
|
|
990
1068
|
), "step_id is only available within a DBOS workflow."
|
|
991
1069
|
return ctx.function_id
|
|
992
1070
|
|
|
1071
|
+
@classproperty
|
|
1072
|
+
def step_status(cls) -> StepStatus:
|
|
1073
|
+
"""Return the status of the currently executing step."""
|
|
1074
|
+
ctx = assert_current_dbos_context()
|
|
1075
|
+
assert ctx.is_step(), "step_status is only available within a DBOS step."
|
|
1076
|
+
assert ctx.step_status is not None
|
|
1077
|
+
return ctx.step_status
|
|
1078
|
+
|
|
993
1079
|
@classproperty
|
|
994
1080
|
def parent_workflow_id(cls) -> str:
|
|
995
1081
|
"""
|
|
@@ -1044,41 +1130,6 @@ class DBOS:
|
|
|
1044
1130
|
ctx.authenticated_roles = authenticated_roles
|
|
1045
1131
|
|
|
1046
1132
|
|
|
1047
|
-
@dataclass
|
|
1048
|
-
class WorkflowStatus:
|
|
1049
|
-
"""
|
|
1050
|
-
Status of workflow execution.
|
|
1051
|
-
|
|
1052
|
-
This captures the state of a workflow execution at a point in time.
|
|
1053
|
-
|
|
1054
|
-
Attributes:
|
|
1055
|
-
workflow_id(str): The ID of the workflow execution
|
|
1056
|
-
status(str): The status of the execution, from `WorkflowStatusString`
|
|
1057
|
-
name(str): The workflow function name
|
|
1058
|
-
executor_id(str): The ID of the executor running the workflow
|
|
1059
|
-
class_name(str): For member functions, the name of the class containing the workflow function
|
|
1060
|
-
config_name(str): For instance member functions, the name of the class instance for the execution
|
|
1061
|
-
queue_name(str): For workflows that are or were queued, the queue name
|
|
1062
|
-
authenticated_user(str): The user who invoked the workflow
|
|
1063
|
-
assumed_role(str): The access role used by the user to allow access to the workflow function
|
|
1064
|
-
authenticated_roles(List[str]): List of all access roles available to the authenticated user
|
|
1065
|
-
recovery_attempts(int): Number of times the workflow has been restarted (usually by recovery)
|
|
1066
|
-
|
|
1067
|
-
"""
|
|
1068
|
-
|
|
1069
|
-
workflow_id: str
|
|
1070
|
-
status: str
|
|
1071
|
-
name: str
|
|
1072
|
-
executor_id: Optional[str]
|
|
1073
|
-
class_name: Optional[str]
|
|
1074
|
-
config_name: Optional[str]
|
|
1075
|
-
queue_name: Optional[str]
|
|
1076
|
-
authenticated_user: Optional[str]
|
|
1077
|
-
assumed_role: Optional[str]
|
|
1078
|
-
authenticated_roles: Optional[List[str]]
|
|
1079
|
-
recovery_attempts: Optional[int]
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
1133
|
class WorkflowHandle(Generic[R], Protocol):
|
|
1083
1134
|
"""
|
|
1084
1135
|
Handle to a workflow function.
|
dbos/_fastapi.py
CHANGED
|
@@ -63,7 +63,10 @@ class LifespanMiddleware:
|
|
|
63
63
|
if scope["type"] == "lifespan":
|
|
64
64
|
|
|
65
65
|
async def wrapped_send(message: MutableMapping[str, Any]) -> None:
|
|
66
|
-
if
|
|
66
|
+
if (
|
|
67
|
+
message["type"] == "lifespan.startup.complete"
|
|
68
|
+
and not self.dbos._launched
|
|
69
|
+
):
|
|
67
70
|
self.dbos._launch()
|
|
68
71
|
elif message["type"] == "lifespan.shutdown.complete":
|
|
69
72
|
self.dbos._destroy()
|