dbos 0.23.0a9__py3-none-any.whl → 0.23.0a11__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/_conductor/conductor.py +194 -0
- dbos/_conductor/protocol.py +186 -0
- dbos/_dbos.py +36 -1
- dbos/_recovery.py +13 -8
- dbos/_sys_db.py +29 -13
- dbos/_workflow_commands.py +0 -4
- dbos/cli/cli.py +4 -1
- {dbos-0.23.0a9.dist-info → dbos-0.23.0a11.dist-info}/METADATA +2 -1
- {dbos-0.23.0a9.dist-info → dbos-0.23.0a11.dist-info}/RECORD +12 -10
- {dbos-0.23.0a9.dist-info → dbos-0.23.0a11.dist-info}/WHEEL +0 -0
- {dbos-0.23.0a9.dist-info → dbos-0.23.0a11.dist-info}/entry_points.txt +0 -0
- {dbos-0.23.0a9.dist-info → dbos-0.23.0a11.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from websockets import ConnectionClosed, ConnectionClosedOK
|
|
7
|
+
from websockets.sync.client import connect
|
|
8
|
+
from websockets.sync.connection import Connection
|
|
9
|
+
|
|
10
|
+
from dbos._utils import GlobalParams
|
|
11
|
+
from dbos._workflow_commands import list_queued_workflows, list_workflows
|
|
12
|
+
|
|
13
|
+
from . import protocol as p
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from dbos import DBOS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConductorWebsocket(threading.Thread):
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, dbos: "DBOS", conductor_url: str, conductor_key: str, evt: threading.Event
|
|
23
|
+
):
|
|
24
|
+
super().__init__(daemon=True)
|
|
25
|
+
self.websocket: Optional[Connection] = None
|
|
26
|
+
self.evt = evt
|
|
27
|
+
self.dbos = dbos
|
|
28
|
+
self.app_name = dbos.config["name"]
|
|
29
|
+
self.url = (
|
|
30
|
+
conductor_url.rstrip("/") + f"/websocket/{self.app_name}/{conductor_key}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def run(self) -> None:
|
|
34
|
+
while not self.evt.is_set():
|
|
35
|
+
try:
|
|
36
|
+
with connect(self.url) as websocket:
|
|
37
|
+
self.websocket = websocket
|
|
38
|
+
while not self.evt.is_set():
|
|
39
|
+
message = websocket.recv()
|
|
40
|
+
if not isinstance(message, str):
|
|
41
|
+
self.dbos.logger.warning(
|
|
42
|
+
"Receieved unexpected non-str message"
|
|
43
|
+
)
|
|
44
|
+
continue
|
|
45
|
+
base_message = p.BaseMessage.from_json(message)
|
|
46
|
+
type = base_message.type
|
|
47
|
+
if type == p.MessageType.EXECUTOR_INFO:
|
|
48
|
+
info_response = p.ExecutorInfoResponse(
|
|
49
|
+
type=p.MessageType.EXECUTOR_INFO,
|
|
50
|
+
request_id=base_message.request_id,
|
|
51
|
+
executor_id=GlobalParams.executor_id,
|
|
52
|
+
application_version=GlobalParams.app_version,
|
|
53
|
+
)
|
|
54
|
+
websocket.send(info_response.to_json())
|
|
55
|
+
self.dbos.logger.info("Connected to DBOS conductor")
|
|
56
|
+
elif type == p.MessageType.RECOVERY:
|
|
57
|
+
recovery_message = p.RecoveryRequest.from_json(message)
|
|
58
|
+
success = True
|
|
59
|
+
try:
|
|
60
|
+
self.dbos.recover_pending_workflows(
|
|
61
|
+
recovery_message.executor_ids
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self.dbos.logger.error(
|
|
65
|
+
f"Exception encountered when recovering workflows: {traceback.format_exc()}"
|
|
66
|
+
)
|
|
67
|
+
success = False
|
|
68
|
+
recovery_response = p.RecoveryResponse(
|
|
69
|
+
type=p.MessageType.RECOVERY,
|
|
70
|
+
request_id=base_message.request_id,
|
|
71
|
+
success=success,
|
|
72
|
+
)
|
|
73
|
+
websocket.send(recovery_response.to_json())
|
|
74
|
+
elif type == p.MessageType.CANCEL:
|
|
75
|
+
cancel_message = p.CancelRequest.from_json(message)
|
|
76
|
+
success = True
|
|
77
|
+
try:
|
|
78
|
+
self.dbos.cancel_workflow(cancel_message.workflow_id)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.dbos.logger.error(
|
|
81
|
+
f"Exception encountered when cancelling workflow {cancel_message.workflow_id}: {traceback.format_exc()}"
|
|
82
|
+
)
|
|
83
|
+
success = False
|
|
84
|
+
cancel_response = p.CancelResponse(
|
|
85
|
+
type=p.MessageType.CANCEL,
|
|
86
|
+
request_id=base_message.request_id,
|
|
87
|
+
success=success,
|
|
88
|
+
)
|
|
89
|
+
websocket.send(cancel_response.to_json())
|
|
90
|
+
elif type == p.MessageType.RESUME:
|
|
91
|
+
resume_message = p.ResumeRequest.from_json(message)
|
|
92
|
+
success = True
|
|
93
|
+
try:
|
|
94
|
+
self.dbos.resume_workflow(resume_message.workflow_id)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.dbos.logger.error(
|
|
97
|
+
f"Exception encountered when resuming workflow {resume_message.workflow_id}: {traceback.format_exc()}"
|
|
98
|
+
)
|
|
99
|
+
success = False
|
|
100
|
+
resume_response = p.ResumeResponse(
|
|
101
|
+
type=p.MessageType.RESUME,
|
|
102
|
+
request_id=base_message.request_id,
|
|
103
|
+
success=success,
|
|
104
|
+
)
|
|
105
|
+
websocket.send(resume_response.to_json())
|
|
106
|
+
elif type == p.MessageType.RESTART:
|
|
107
|
+
restart_message = p.RestartRequest.from_json(message)
|
|
108
|
+
success = True
|
|
109
|
+
try:
|
|
110
|
+
self.dbos.restart_workflow(restart_message.workflow_id)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.dbos.logger.error(
|
|
113
|
+
f"Exception encountered when restarting workflow {restart_message.workflow_id}: {traceback.format_exc()}"
|
|
114
|
+
)
|
|
115
|
+
success = False
|
|
116
|
+
restart_response = p.RestartResponse(
|
|
117
|
+
type=p.MessageType.RESTART,
|
|
118
|
+
request_id=base_message.request_id,
|
|
119
|
+
success=success,
|
|
120
|
+
)
|
|
121
|
+
websocket.send(restart_response.to_json())
|
|
122
|
+
elif type == p.MessageType.LIST_WORKFLOWS:
|
|
123
|
+
list_workflows_message = p.ListWorkflowsRequest.from_json(
|
|
124
|
+
message
|
|
125
|
+
)
|
|
126
|
+
body = list_workflows_message.body
|
|
127
|
+
infos = list_workflows(
|
|
128
|
+
self.dbos._sys_db,
|
|
129
|
+
workflow_ids=body["workflow_uuids"],
|
|
130
|
+
user=body["authenticated_user"],
|
|
131
|
+
start_time=body["start_time"],
|
|
132
|
+
end_time=body["end_time"],
|
|
133
|
+
status=body["status"],
|
|
134
|
+
request=False,
|
|
135
|
+
app_version=body["application_version"],
|
|
136
|
+
name=body["workflow_name"],
|
|
137
|
+
limit=body["limit"],
|
|
138
|
+
offset=body["offset"],
|
|
139
|
+
sort_desc=body["sort_desc"],
|
|
140
|
+
)
|
|
141
|
+
list_workflows_response = p.ListWorkflowsResponse(
|
|
142
|
+
type=p.MessageType.LIST_WORKFLOWS,
|
|
143
|
+
request_id=base_message.request_id,
|
|
144
|
+
output=[
|
|
145
|
+
p.WorkflowsOutput.from_workflow_information(i)
|
|
146
|
+
for i in infos
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
websocket.send(list_workflows_response.to_json())
|
|
150
|
+
elif type == p.MessageType.LIST_QUEUED_WORKFLOWS:
|
|
151
|
+
list_queued_workflows_message = (
|
|
152
|
+
p.ListQueuedWorkflowsRequest.from_json(message)
|
|
153
|
+
)
|
|
154
|
+
q_body = list_queued_workflows_message.body
|
|
155
|
+
infos = list_queued_workflows(
|
|
156
|
+
self.dbos._sys_db,
|
|
157
|
+
start_time=q_body["start_time"],
|
|
158
|
+
end_time=q_body["end_time"],
|
|
159
|
+
status=q_body["status"],
|
|
160
|
+
request=False,
|
|
161
|
+
name=q_body["workflow_name"],
|
|
162
|
+
limit=q_body["limit"],
|
|
163
|
+
offset=q_body["offset"],
|
|
164
|
+
queue_name=q_body["queue_name"],
|
|
165
|
+
sort_desc=q_body["sort_desc"],
|
|
166
|
+
)
|
|
167
|
+
list_queued_workflows_response = (
|
|
168
|
+
p.ListQueuedWorkflowsResponse(
|
|
169
|
+
type=p.MessageType.LIST_QUEUED_WORKFLOWS,
|
|
170
|
+
request_id=base_message.request_id,
|
|
171
|
+
output=[
|
|
172
|
+
p.WorkflowsOutput.from_workflow_information(i)
|
|
173
|
+
for i in infos
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
websocket.send(list_queued_workflows_response.to_json())
|
|
178
|
+
else:
|
|
179
|
+
self.dbos.logger.warning(f"Unexpected message type: {type}")
|
|
180
|
+
except ConnectionClosedOK:
|
|
181
|
+
self.dbos.logger.info("Conductor connection terminated")
|
|
182
|
+
break
|
|
183
|
+
except ConnectionClosed as e:
|
|
184
|
+
self.dbos.logger.warning(
|
|
185
|
+
f"Connection to conductor lost. Reconnecting: {e}"
|
|
186
|
+
)
|
|
187
|
+
time.sleep(1)
|
|
188
|
+
continue
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.dbos.logger.error(
|
|
191
|
+
f"Unexpected exception in connection to conductor. Reconnecting: {e}"
|
|
192
|
+
)
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
continue
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import List, Optional, Type, TypedDict, TypeVar
|
|
5
|
+
|
|
6
|
+
from dbos._workflow_commands import WorkflowInformation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessageType(str, Enum):
|
|
10
|
+
EXECUTOR_INFO = "executor_info"
|
|
11
|
+
RECOVERY = "recovery"
|
|
12
|
+
CANCEL = "cancel"
|
|
13
|
+
LIST_WORKFLOWS = "list_workflows"
|
|
14
|
+
LIST_QUEUED_WORKFLOWS = "list_queued_workflows"
|
|
15
|
+
RESUME = "resume"
|
|
16
|
+
RESTART = "restart"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T", bound="BaseMessage")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BaseMessage:
|
|
24
|
+
type: MessageType
|
|
25
|
+
request_id: str
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_json(cls: Type[T], json_str: str) -> T:
|
|
29
|
+
"""
|
|
30
|
+
Safely load a JSON into a dataclass, loading only the
|
|
31
|
+
attributes specified in the dataclass.
|
|
32
|
+
"""
|
|
33
|
+
data = json.loads(json_str)
|
|
34
|
+
all_annotations = {}
|
|
35
|
+
for base_cls in cls.__mro__:
|
|
36
|
+
if hasattr(base_cls, "__annotations__"):
|
|
37
|
+
all_annotations.update(base_cls.__annotations__)
|
|
38
|
+
kwargs = {k: v for k, v in data.items() if k in all_annotations}
|
|
39
|
+
return cls(**kwargs)
|
|
40
|
+
|
|
41
|
+
def to_json(self) -> str:
|
|
42
|
+
dict_data = asdict(self)
|
|
43
|
+
return json.dumps(dict_data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ExecutorInfoRequest(BaseMessage):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ExecutorInfoResponse(BaseMessage):
|
|
53
|
+
executor_id: str
|
|
54
|
+
application_version: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class RecoveryRequest(BaseMessage):
|
|
59
|
+
executor_ids: List[str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class RecoveryResponse(BaseMessage):
|
|
64
|
+
success: bool
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CancelRequest(BaseMessage):
|
|
69
|
+
workflow_id: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class CancelResponse(BaseMessage):
|
|
74
|
+
success: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ResumeRequest(BaseMessage):
|
|
79
|
+
workflow_id: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ResumeResponse(BaseMessage):
|
|
84
|
+
success: bool
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class RestartRequest(BaseMessage):
|
|
89
|
+
workflow_id: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class RestartResponse(BaseMessage):
|
|
94
|
+
success: bool
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ListWorkflowsBody(TypedDict):
|
|
98
|
+
workflow_uuids: List[str]
|
|
99
|
+
workflow_name: Optional[str]
|
|
100
|
+
authenticated_user: Optional[str]
|
|
101
|
+
start_time: Optional[str]
|
|
102
|
+
end_time: Optional[str]
|
|
103
|
+
status: Optional[str]
|
|
104
|
+
application_version: Optional[str]
|
|
105
|
+
limit: Optional[int]
|
|
106
|
+
offset: Optional[int]
|
|
107
|
+
sort_desc: bool
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class WorkflowsOutput:
|
|
112
|
+
WorkflowUUID: str
|
|
113
|
+
Status: Optional[str]
|
|
114
|
+
WorkflowName: Optional[str]
|
|
115
|
+
WorkflowClassName: Optional[str]
|
|
116
|
+
WorkflowConfigName: Optional[str]
|
|
117
|
+
AuthenticatedUser: Optional[str]
|
|
118
|
+
AssumedRole: Optional[str]
|
|
119
|
+
AuthenticatedRoles: Optional[str]
|
|
120
|
+
Input: Optional[str]
|
|
121
|
+
Output: Optional[str]
|
|
122
|
+
Request: Optional[str]
|
|
123
|
+
Error: Optional[str]
|
|
124
|
+
CreatedAt: Optional[str]
|
|
125
|
+
UpdatedAt: Optional[str]
|
|
126
|
+
QueueName: Optional[str]
|
|
127
|
+
ApplicationVersion: Optional[str]
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_workflow_information(cls, info: WorkflowInformation) -> "WorkflowsOutput":
|
|
131
|
+
# Convert fields to strings as needed
|
|
132
|
+
created_at_str = str(info.created_at) if info.created_at is not None else None
|
|
133
|
+
updated_at_str = str(info.updated_at) if info.updated_at is not None else None
|
|
134
|
+
inputs_str = str(info.input) if info.input is not None else None
|
|
135
|
+
outputs_str = str(info.output) if info.output is not None else None
|
|
136
|
+
request_str = str(info.request) if info.request is not None else None
|
|
137
|
+
|
|
138
|
+
return cls(
|
|
139
|
+
WorkflowUUID=info.workflow_id,
|
|
140
|
+
Status=info.status,
|
|
141
|
+
WorkflowName=info.workflow_name,
|
|
142
|
+
WorkflowClassName=info.workflow_class_name,
|
|
143
|
+
WorkflowConfigName=info.workflow_config_name,
|
|
144
|
+
AuthenticatedUser=info.authenticated_user,
|
|
145
|
+
AssumedRole=info.assumed_role,
|
|
146
|
+
AuthenticatedRoles=info.authenticated_roles,
|
|
147
|
+
Input=inputs_str,
|
|
148
|
+
Output=outputs_str,
|
|
149
|
+
Request=request_str,
|
|
150
|
+
Error=info.error,
|
|
151
|
+
CreatedAt=created_at_str,
|
|
152
|
+
UpdatedAt=updated_at_str,
|
|
153
|
+
QueueName=info.queue_name,
|
|
154
|
+
ApplicationVersion=info.app_version,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ListWorkflowsRequest(BaseMessage):
|
|
160
|
+
body: ListWorkflowsBody
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ListWorkflowsResponse(BaseMessage):
|
|
165
|
+
output: List[WorkflowsOutput]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ListQueuedWorkflowsBody(TypedDict):
|
|
169
|
+
workflow_name: Optional[str]
|
|
170
|
+
start_time: Optional[str]
|
|
171
|
+
end_time: Optional[str]
|
|
172
|
+
status: Optional[str]
|
|
173
|
+
queue_name: Optional[str]
|
|
174
|
+
limit: Optional[int]
|
|
175
|
+
offset: Optional[int]
|
|
176
|
+
sort_desc: bool
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class ListQueuedWorkflowsRequest(BaseMessage):
|
|
181
|
+
body: ListQueuedWorkflowsBody
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class ListQueuedWorkflowsResponse(BaseMessage):
|
|
186
|
+
output: List[WorkflowsOutput]
|
dbos/_dbos.py
CHANGED
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import sys
|
|
10
10
|
import threading
|
|
11
11
|
import traceback
|
|
12
|
+
import uuid
|
|
12
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from logging import Logger
|
|
@@ -32,6 +33,7 @@ from typing import (
|
|
|
32
33
|
|
|
33
34
|
from opentelemetry.trace import Span
|
|
34
35
|
|
|
36
|
+
from dbos._conductor.conductor import ConductorWebsocket
|
|
35
37
|
from dbos._utils import GlobalParams
|
|
36
38
|
|
|
37
39
|
from ._classproperty import classproperty
|
|
@@ -258,6 +260,8 @@ class DBOS:
|
|
|
258
260
|
config: Optional[ConfigFile] = None,
|
|
259
261
|
fastapi: Optional["FastAPI"] = None,
|
|
260
262
|
flask: Optional["Flask"] = None,
|
|
263
|
+
conductor_url: Optional[str] = None,
|
|
264
|
+
conductor_key: Optional[str] = None,
|
|
261
265
|
) -> DBOS:
|
|
262
266
|
global _dbos_global_instance
|
|
263
267
|
global _dbos_global_registry
|
|
@@ -273,7 +277,7 @@ class DBOS:
|
|
|
273
277
|
config = _dbos_global_registry.config
|
|
274
278
|
|
|
275
279
|
_dbos_global_instance = super().__new__(cls)
|
|
276
|
-
_dbos_global_instance.__init__(fastapi=fastapi, config=config, flask=flask) # type: ignore
|
|
280
|
+
_dbos_global_instance.__init__(fastapi=fastapi, config=config, flask=flask, conductor_url=conductor_url, conductor_key=conductor_key) # type: ignore
|
|
277
281
|
else:
|
|
278
282
|
if (config is not None and _dbos_global_instance.config is not config) or (
|
|
279
283
|
_dbos_global_instance.fastapi is not fastapi
|
|
@@ -301,6 +305,8 @@ class DBOS:
|
|
|
301
305
|
config: Optional[ConfigFile] = None,
|
|
302
306
|
fastapi: Optional["FastAPI"] = None,
|
|
303
307
|
flask: Optional["Flask"] = None,
|
|
308
|
+
conductor_url: Optional[str] = None,
|
|
309
|
+
conductor_key: Optional[str] = None,
|
|
304
310
|
) -> None:
|
|
305
311
|
if hasattr(self, "_initialized") and self._initialized:
|
|
306
312
|
return
|
|
@@ -324,6 +330,9 @@ class DBOS:
|
|
|
324
330
|
self.flask: Optional["Flask"] = flask
|
|
325
331
|
self._executor_field: Optional[ThreadPoolExecutor] = None
|
|
326
332
|
self._background_threads: List[threading.Thread] = []
|
|
333
|
+
self.conductor_url: Optional[str] = conductor_url
|
|
334
|
+
self.conductor_key: Optional[str] = conductor_key
|
|
335
|
+
self.conductor_websocket: Optional[ConductorWebsocket] = None
|
|
327
336
|
|
|
328
337
|
# If using FastAPI, set up middleware and lifecycle events
|
|
329
338
|
if self.fastapi is not None:
|
|
@@ -399,6 +408,9 @@ class DBOS:
|
|
|
399
408
|
self._debug_mode = debug_mode
|
|
400
409
|
if GlobalParams.app_version == "":
|
|
401
410
|
GlobalParams.app_version = self._registry.compute_app_version()
|
|
411
|
+
if self.conductor_key is not None:
|
|
412
|
+
GlobalParams.executor_id = str(uuid.uuid4())
|
|
413
|
+
dbos_logger.info(f"Executor ID: {GlobalParams.executor_id}")
|
|
402
414
|
dbos_logger.info(f"Application version: {GlobalParams.app_version}")
|
|
403
415
|
self._executor_field = ThreadPoolExecutor(max_workers=64)
|
|
404
416
|
self._sys_db_field = SystemDatabase(self.config, debug_mode=debug_mode)
|
|
@@ -451,6 +463,22 @@ class DBOS:
|
|
|
451
463
|
bg_queue_thread.start()
|
|
452
464
|
self._background_threads.append(bg_queue_thread)
|
|
453
465
|
|
|
466
|
+
# Start the conductor thread if requested
|
|
467
|
+
if self.conductor_key is not None:
|
|
468
|
+
if self.conductor_url is None:
|
|
469
|
+
dbos_domain = os.environ.get("DBOS_DOMAIN", "cloud.dbos.dev")
|
|
470
|
+
self.conductor_url = f"wss://{dbos_domain}/conductor/v1alpha1"
|
|
471
|
+
evt = threading.Event()
|
|
472
|
+
self.stop_events.append(evt)
|
|
473
|
+
self.conductor_websocket = ConductorWebsocket(
|
|
474
|
+
self,
|
|
475
|
+
conductor_url=self.conductor_url,
|
|
476
|
+
conductor_key=self.conductor_key,
|
|
477
|
+
evt=evt,
|
|
478
|
+
)
|
|
479
|
+
self.conductor_websocket.start()
|
|
480
|
+
self._background_threads.append(self.conductor_websocket)
|
|
481
|
+
|
|
454
482
|
# Grab any pollers that were deferred and start them
|
|
455
483
|
for evt, func, args, kwargs in self._registry.pollers:
|
|
456
484
|
self.stop_events.append(evt)
|
|
@@ -501,6 +529,11 @@ class DBOS:
|
|
|
501
529
|
if self._admin_server_field is not None:
|
|
502
530
|
self._admin_server_field.stop()
|
|
503
531
|
self._admin_server_field = None
|
|
532
|
+
if (
|
|
533
|
+
self.conductor_websocket is not None
|
|
534
|
+
and self.conductor_websocket.websocket is not None
|
|
535
|
+
):
|
|
536
|
+
self.conductor_websocket.websocket.close()
|
|
504
537
|
# CB - This needs work, some things ought to stop before DBs are tossed out,
|
|
505
538
|
# on the other hand it hangs to move it
|
|
506
539
|
if self._executor_field is not None:
|
|
@@ -864,12 +897,14 @@ class DBOS:
|
|
|
864
897
|
@classmethod
|
|
865
898
|
def cancel_workflow(cls, workflow_id: str) -> None:
|
|
866
899
|
"""Cancel a workflow by ID."""
|
|
900
|
+
dbos_logger.info(f"Cancelling workflow: {workflow_id}")
|
|
867
901
|
_get_dbos_instance()._sys_db.cancel_workflow(workflow_id)
|
|
868
902
|
_get_or_create_dbos_registry().cancel_workflow(workflow_id)
|
|
869
903
|
|
|
870
904
|
@classmethod
|
|
871
905
|
def resume_workflow(cls, workflow_id: str) -> WorkflowHandle[Any]:
|
|
872
906
|
"""Resume a workflow by ID."""
|
|
907
|
+
dbos_logger.info(f"Resuming workflow: {workflow_id}")
|
|
873
908
|
_get_dbos_instance()._sys_db.resume_workflow(workflow_id)
|
|
874
909
|
_get_or_create_dbos_registry().clear_workflow_cancelled(workflow_id)
|
|
875
910
|
return execute_workflow_by_id(_get_dbos_instance(), workflow_id, False)
|
dbos/_recovery.py
CHANGED
|
@@ -27,8 +27,9 @@ def startup_recovery_thread(
|
|
|
27
27
|
pending_workflow.queue_name
|
|
28
28
|
and pending_workflow.queue_name != "_dbos_internal_queue"
|
|
29
29
|
):
|
|
30
|
-
dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
31
|
-
|
|
30
|
+
cleared = dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
31
|
+
if cleared:
|
|
32
|
+
continue
|
|
32
33
|
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
33
34
|
pending_workflows.remove(pending_workflow)
|
|
34
35
|
except DBOSWorkflowFunctionNotFoundError:
|
|
@@ -46,7 +47,6 @@ def recover_pending_workflows(
|
|
|
46
47
|
"""Attempt to recover pending workflows for a list of specific executors and return workflow handles for them."""
|
|
47
48
|
workflow_handles: List["WorkflowHandle[Any]"] = []
|
|
48
49
|
for executor_id in executor_ids:
|
|
49
|
-
dbos.logger.debug(f"Recovering pending workflows for executor: {executor_id}")
|
|
50
50
|
pending_workflows = dbos._sys_db.get_pending_workflows(
|
|
51
51
|
executor_id, GlobalParams.app_version
|
|
52
52
|
)
|
|
@@ -56,10 +56,15 @@ def recover_pending_workflows(
|
|
|
56
56
|
and pending_workflow.queue_name != "_dbos_internal_queue"
|
|
57
57
|
):
|
|
58
58
|
try:
|
|
59
|
-
dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
cleared = dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
|
|
60
|
+
if cleared:
|
|
61
|
+
workflow_handles.append(
|
|
62
|
+
dbos.retrieve_workflow(pending_workflow.workflow_uuid)
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
workflow_handles.append(
|
|
66
|
+
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
67
|
+
)
|
|
63
68
|
except Exception as e:
|
|
64
69
|
dbos.logger.error(e)
|
|
65
70
|
else:
|
|
@@ -67,6 +72,6 @@ def recover_pending_workflows(
|
|
|
67
72
|
execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
|
|
68
73
|
)
|
|
69
74
|
dbos.logger.info(
|
|
70
|
-
f"Recovering {len(pending_workflows)} workflows from version {GlobalParams.app_version}"
|
|
75
|
+
f"Recovering {len(pending_workflows)} workflows for executor {executor_id} from version {GlobalParams.app_version}"
|
|
71
76
|
)
|
|
72
77
|
return workflow_handles
|
dbos/_sys_db.py
CHANGED
|
@@ -124,7 +124,9 @@ class GetWorkflowsInput:
|
|
|
124
124
|
self.offset: Optional[int] = (
|
|
125
125
|
None # Offset into the matching records for pagination
|
|
126
126
|
)
|
|
127
|
-
self.sort_desc: bool =
|
|
127
|
+
self.sort_desc: bool = (
|
|
128
|
+
False # If true, sort by created_at in DESC order. Default false (in ASC order).
|
|
129
|
+
)
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
class GetQueuedWorkflowsInput(TypedDict):
|
|
@@ -1460,21 +1462,35 @@ class SystemDatabase:
|
|
|
1460
1462
|
.values(completed_at_epoch_ms=int(time.time() * 1000))
|
|
1461
1463
|
)
|
|
1462
1464
|
|
|
1463
|
-
|
|
1465
|
+
|
|
1466
|
+
def clear_queue_assignment(self, workflow_id: str) -> bool:
|
|
1464
1467
|
if self._debug_mode:
|
|
1465
1468
|
raise Exception("called clear_queue_assignment in debug mode")
|
|
1466
|
-
with self.engine.begin() as c:
|
|
1467
|
-
c.execute(
|
|
1468
|
-
sa.update(SystemSchema.workflow_queue)
|
|
1469
|
-
.where(SystemSchema.workflow_queue.c.workflow_uuid == workflow_id)
|
|
1470
|
-
.values(executor_id=None, started_at_epoch_ms=None)
|
|
1471
|
-
)
|
|
1472
|
-
c.execute(
|
|
1473
|
-
sa.update(SystemSchema.workflow_status)
|
|
1474
|
-
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
1475
|
-
.values(executor_id=None, status=WorkflowStatusString.ENQUEUED.value)
|
|
1476
|
-
)
|
|
1477
1469
|
|
|
1470
|
+
with self.engine.connect() as conn:
|
|
1471
|
+
with conn.begin() as transaction:
|
|
1472
|
+
res = conn.execute(
|
|
1473
|
+
sa.update(SystemSchema.workflow_queue)
|
|
1474
|
+
.where(SystemSchema.workflow_queue.c.workflow_uuid == workflow_id)
|
|
1475
|
+
.values(executor_id=None, started_at_epoch_ms=None)
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
# If no rows were affected, the workflow is not anymore in the queue
|
|
1479
|
+
if res.rowcount == 0:
|
|
1480
|
+
transaction.rollback()
|
|
1481
|
+
return False
|
|
1482
|
+
|
|
1483
|
+
res = conn.execute(
|
|
1484
|
+
sa.update(SystemSchema.workflow_status)
|
|
1485
|
+
.where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
|
|
1486
|
+
.values(executor_id=None, status=WorkflowStatusString.ENQUEUED.value)
|
|
1487
|
+
)
|
|
1488
|
+
if res.rowcount == 0:
|
|
1489
|
+
# This should never happen
|
|
1490
|
+
raise Exception(
|
|
1491
|
+
f"UNREACHABLE: Workflow {workflow_id} is found in the workflow_queue table but not found in the workflow_status table"
|
|
1492
|
+
)
|
|
1493
|
+
return True
|
|
1478
1494
|
|
|
1479
1495
|
def reset_system_database(config: ConfigFile) -> None:
|
|
1480
1496
|
sysdb_name = (
|
dbos/_workflow_commands.py
CHANGED
dbos/cli/cli.py
CHANGED
|
@@ -75,7 +75,10 @@ def start() -> None:
|
|
|
75
75
|
|
|
76
76
|
# If the child is still running, force kill it
|
|
77
77
|
if process.poll() is None:
|
|
78
|
-
|
|
78
|
+
try:
|
|
79
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
79
82
|
|
|
80
83
|
# Exit immediately
|
|
81
84
|
os._exit(process.returncode if process.returncode is not None else 1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dbos
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.0a11
|
|
4
4
|
Summary: Ultra-lightweight durable execution in Python
|
|
5
5
|
Author-Email: "DBOS, Inc." <contact@dbos.dev>
|
|
6
6
|
License: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Dist: docker>=7.1.0
|
|
|
23
23
|
Requires-Dist: cryptography>=43.0.3
|
|
24
24
|
Requires-Dist: rich>=13.9.4
|
|
25
25
|
Requires-Dist: pyjwt>=2.10.1
|
|
26
|
+
Requires-Dist: websockets>=15.0
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
|
|
28
29
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
dbos-0.23.
|
|
2
|
-
dbos-0.23.
|
|
3
|
-
dbos-0.23.
|
|
4
|
-
dbos-0.23.
|
|
1
|
+
dbos-0.23.0a11.dist-info/METADATA,sha256=pm213i_FtTxsXr1cfO0_iP5bcU0E60Dzmq9GWXYtFMQ,5556
|
|
2
|
+
dbos-0.23.0a11.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
3
|
+
dbos-0.23.0a11.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
|
|
4
|
+
dbos-0.23.0a11.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
|
5
5
|
dbos/__init__.py,sha256=CxRHBHEthPL4PZoLbZhp3rdm44-KkRTT2-7DkK9d4QQ,724
|
|
6
6
|
dbos/__main__.py,sha256=P7jAr-7L9XE5mrsQ7i4b-bLr2ap1tCQfhMByLCRWDj0,568
|
|
7
7
|
dbos/_admin_server.py,sha256=YiVn5lywz2Vg8_juyNHOYl0HVEy48--7b4phwK7r92o,5732
|
|
@@ -10,11 +10,13 @@ dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
|
|
|
10
10
|
dbos/_cloudutils/authentication.py,sha256=V0fCWQN9stCkhbuuxgPTGpvuQcDqfU3KAxPAh01vKW4,5007
|
|
11
11
|
dbos/_cloudutils/cloudutils.py,sha256=YC7jGsIopT0KveLsqbRpQk2KlRBk-nIRC_UCgep4f3o,7797
|
|
12
12
|
dbos/_cloudutils/databases.py,sha256=_shqaqSvhY4n2ScgQ8IP5PDZvzvcx3YBKV8fj-cxhSY,8543
|
|
13
|
+
dbos/_conductor/conductor.py,sha256=SkTQDMBabzgrgJDDDwJSkF0eLIFzBe-_DIPL8OrNjA4,10067
|
|
14
|
+
dbos/_conductor/protocol.py,sha256=mlAtcE2aT_egBiof_oyVVZPHTvWvU_U7-yu0fuNGKV0,4643
|
|
13
15
|
dbos/_context.py,sha256=Ue5qu3rzLfRmPkz-UUZi9ZS8iXpapRN0NTM4mbA2QmQ,17738
|
|
14
16
|
dbos/_core.py,sha256=UQb068FT59Op-F5RmtxreSeSQ1_wljOso0dQCUOPrC4,37528
|
|
15
17
|
dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
|
|
16
18
|
dbos/_db_wizard.py,sha256=6tfJaCRa1NtkUdNW75a2yvi_mEgnPJ9C1HP2zPG1hCU,8067
|
|
17
|
-
dbos/_dbos.py,sha256=
|
|
19
|
+
dbos/_dbos.py,sha256=0kX3fgdTqAn-eMKSbh73LFVR08YPMoB030g4dzvc9Yk,41150
|
|
18
20
|
dbos/_dbos_config.py,sha256=_VETbEsMZ66563A8sX05B_coKz2BrILbIm9H5BmnPmk,9572
|
|
19
21
|
dbos/_debug.py,sha256=wcvjM2k4BrK7mlYjImUZXNBUB00fPGjQrNimZXlj76c,1491
|
|
20
22
|
dbos/_error.py,sha256=xqB7b7g5AF_OwOvqLKLXL1xldn2gAtORix2ZC2B8zK0,5089
|
|
@@ -34,7 +36,7 @@ dbos/_migrations/versions/d76646551a6c_workflow_queue.py,sha256=G942nophZ2uC2vc4
|
|
|
34
36
|
dbos/_migrations/versions/eab0cc1d9a14_job_queue.py,sha256=uvhFOtqbBreCePhAxZfIT0qCAI7BiZTou9wt6QnbY7c,1412
|
|
35
37
|
dbos/_outcome.py,sha256=FDMgWVjZ06vm9xO-38H17mTqBImUYQxgKs_bDCSIAhE,6648
|
|
36
38
|
dbos/_queue.py,sha256=I2gBc7zQ4G0vyDDBnKwIFzxtqfD7DxHO2IZ41brFSOM,2927
|
|
37
|
-
dbos/_recovery.py,sha256=
|
|
39
|
+
dbos/_recovery.py,sha256=QIi0xQToYOpUlAzwTsuDonsWbGrI9HZBR2qVyOS2IJU,3077
|
|
38
40
|
dbos/_registrations.py,sha256=_zy6k944Ll8QwqU12Kr3OP23ukVtm8axPNN1TS_kJRc,6717
|
|
39
41
|
dbos/_request.py,sha256=cX1B3Atlh160phgS35gF1VEEV4pD126c9F3BDgBmxZU,929
|
|
40
42
|
dbos/_roles.py,sha256=iOsgmIAf1XVzxs3gYWdGRe1B880YfOw5fpU7Jwx8_A8,2271
|
|
@@ -43,7 +45,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
43
45
|
dbos/_schemas/application_database.py,sha256=KeyoPrF7hy_ODXV7QNike_VFSD74QBRfQ76D7QyE9HI,966
|
|
44
46
|
dbos/_schemas/system_database.py,sha256=rwp4EvCSaXcUoMaRczZCvETCxGp72k3-hvLyGUDkih0,5163
|
|
45
47
|
dbos/_serialization.py,sha256=YCYv0qKAwAZ1djZisBC7khvKqG-5OcIv9t9EC5PFIog,1743
|
|
46
|
-
dbos/_sys_db.py,sha256=
|
|
48
|
+
dbos/_sys_db.py,sha256=nJVosEwdc_mkP9cJQb0uFpOOLOX8KxkOs-pij0vu83c,64467
|
|
47
49
|
dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
|
|
48
50
|
dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
51
|
dbos/_templates/dbos-db-starter/__package/main.py,sha256=eI0SS9Nwj-fldtiuSzIlIG6dC91GXXwdRsoHxv6S_WI,2719
|
|
@@ -56,11 +58,11 @@ dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py,sh
|
|
|
56
58
|
dbos/_templates/dbos-db-starter/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
|
|
57
59
|
dbos/_tracer.py,sha256=_Id9j9kCrptSNpEpLiRk_g5VPp-DrTWP1WNZInd5BA4,2439
|
|
58
60
|
dbos/_utils.py,sha256=wjOJzxN66IzL9p4dwcEmQACRQah_V09G6mJI2exQfOM,155
|
|
59
|
-
dbos/_workflow_commands.py,sha256=
|
|
61
|
+
dbos/_workflow_commands.py,sha256=CEzR5XghoZscbc2RHb9G-7Eoo4MMuzfeTo-QBZu4VPY,4690
|
|
60
62
|
dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
|
|
61
63
|
dbos/cli/_template_init.py,sha256=AfuMaO8bmr9WsPNHr6j2cp7kjVVZDUpH7KpbTg0hhFs,2722
|
|
62
|
-
dbos/cli/cli.py,sha256=
|
|
64
|
+
dbos/cli/cli.py,sha256=ThomRytw7EP5iOcrjEgwnpaWgXNTLfnFEBBvCGHxtJs,15590
|
|
63
65
|
dbos/dbos-config.schema.json,sha256=X5TpXNcARGceX0zQs0fVgtZW_Xj9uBbY5afPt9Rz9yk,5741
|
|
64
66
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
|
65
67
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
|
66
|
-
dbos-0.23.
|
|
68
|
+
dbos-0.23.0a11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|