dbos 0.23.0a10__py3-none-any.whl → 0.23.0a12__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.

@@ -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
@@ -14,6 +14,16 @@ if TYPE_CHECKING:
14
14
  from ._dbos import DBOS, WorkflowHandle
15
15
 
16
16
 
17
+ def _recover_workflow(
18
+ dbos: "DBOS", workflow: GetPendingWorkflowsOutput
19
+ ) -> "WorkflowHandle[Any]":
20
+ if workflow.queue_name and workflow.queue_name != "_dbos_internal_queue":
21
+ cleared = dbos._sys_db.clear_queue_assignment(workflow.workflow_uuid)
22
+ if cleared:
23
+ return dbos.retrieve_workflow(workflow.workflow_uuid)
24
+ return execute_workflow_by_id(dbos, workflow.workflow_uuid)
25
+
26
+
17
27
  def startup_recovery_thread(
18
28
  dbos: "DBOS", pending_workflows: List[GetPendingWorkflowsOutput]
19
29
  ) -> None:
@@ -23,14 +33,7 @@ def startup_recovery_thread(
23
33
  while not stop_event.is_set() and len(pending_workflows) > 0:
24
34
  try:
25
35
  for pending_workflow in list(pending_workflows):
26
- if (
27
- pending_workflow.queue_name
28
- and pending_workflow.queue_name != "_dbos_internal_queue"
29
- ):
30
- cleared = dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
31
- if cleared:
32
- continue
33
- execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
36
+ _recover_workflow(dbos, pending_workflow)
34
37
  pending_workflows.remove(pending_workflow)
35
38
  except DBOSWorkflowFunctionNotFoundError:
36
39
  time.sleep(1)
@@ -47,32 +50,19 @@ def recover_pending_workflows(
47
50
  """Attempt to recover pending workflows for a list of specific executors and return workflow handles for them."""
48
51
  workflow_handles: List["WorkflowHandle[Any]"] = []
49
52
  for executor_id in executor_ids:
50
- dbos.logger.debug(f"Recovering pending workflows for executor: {executor_id}")
51
53
  pending_workflows = dbos._sys_db.get_pending_workflows(
52
54
  executor_id, GlobalParams.app_version
53
55
  )
54
56
  for pending_workflow in pending_workflows:
55
- if (
56
- pending_workflow.queue_name
57
- and pending_workflow.queue_name != "_dbos_internal_queue"
58
- ):
59
- try:
60
- cleared = dbos._sys_db.clear_queue_assignment(pending_workflow.workflow_uuid)
61
- if cleared:
62
- workflow_handles.append(
63
- dbos.retrieve_workflow(pending_workflow.workflow_uuid)
64
- )
65
- else:
66
- workflow_handles.append(
67
- execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
68
- )
69
- except Exception as e:
70
- dbos.logger.error(e)
71
- else:
72
- workflow_handles.append(
73
- execute_workflow_by_id(dbos, pending_workflow.workflow_uuid)
57
+ try:
58
+ handle = _recover_workflow(dbos, pending_workflow)
59
+ workflow_handles.append(handle)
60
+ except Exception as e:
61
+ dbos.logger.error(
62
+ f"Exception encountered when recovering workflows: {traceback.format_exc()}"
74
63
  )
64
+ raise e
75
65
  dbos.logger.info(
76
- f"Recovering {len(pending_workflows)} workflows from version {GlobalParams.app_version}"
66
+ f"Recovering {len(pending_workflows)} workflows for executor {executor_id} from version {GlobalParams.app_version}"
77
67
  )
78
68
  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 = False # If true, sort by created_at in DESC order. Default false (in ASC order).
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,7 +1462,6 @@ class SystemDatabase:
1460
1462
  .values(completed_at_epoch_ms=int(time.time() * 1000))
1461
1463
  )
1462
1464
 
1463
-
1464
1465
  def clear_queue_assignment(self, workflow_id: str) -> bool:
1465
1466
  if self._debug_mode:
1466
1467
  raise Exception("called clear_queue_assignment in debug mode")
@@ -1481,7 +1482,9 @@ class SystemDatabase:
1481
1482
  res = conn.execute(
1482
1483
  sa.update(SystemSchema.workflow_status)
1483
1484
  .where(SystemSchema.workflow_status.c.workflow_uuid == workflow_id)
1484
- .values(executor_id=None, status=WorkflowStatusString.ENQUEUED.value)
1485
+ .values(
1486
+ executor_id=None, status=WorkflowStatusString.ENQUEUED.value
1487
+ )
1485
1488
  )
1486
1489
  if res.rowcount == 0:
1487
1490
  # This should never happen
@@ -1490,6 +1493,7 @@ class SystemDatabase:
1490
1493
  )
1491
1494
  return True
1492
1495
 
1496
+
1493
1497
  def reset_system_database(config: ConfigFile) -> None:
1494
1498
  sysdb_name = (
1495
1499
  config["database"]["sys_db_name"]
@@ -1,10 +1,6 @@
1
1
  from typing import List, Optional, cast
2
2
 
3
- import typer
4
-
5
3
  from . import _serialization
6
- from ._dbos_config import ConfigFile
7
- from ._logger import dbos_logger
8
4
  from ._sys_db import (
9
5
  GetQueuedWorkflowsInput,
10
6
  GetWorkflowsInput,
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
- os.killpg(os.getpgid(process.pid), signal.SIGKILL)
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.0a10
3
+ Version: 0.23.0a12
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.0a10.dist-info/METADATA,sha256=ij2aQb4i-QIMqQA7NwPXx8R-pVqhJoR8d1f9jkTghy0,5524
2
- dbos-0.23.0a10.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
- dbos-0.23.0a10.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.23.0a10.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-0.23.0a12.dist-info/METADATA,sha256=vaytGbqIq4czcptlNoggkKmYz6LWFxDuysPkAZ0o4M0,5556
2
+ dbos-0.23.0a12.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ dbos-0.23.0a12.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.23.0a12.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=jNh1R4b13a4g1VXeSPaC2d3J2JXiF6YNpCBek9hkcbM,39393
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=kaljwWky6Y73QnpMlY_cMSwiWf9wcyYigiUX7m3e4Jg,3137
39
+ dbos/_recovery.py,sha256=4KyZb0XJEUGH7ekYT1kpx38i6y5vygPeH75Ta7RZjYo,2596
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=lpgmCpuGfMbv18q7qjrDUw0vqjXn8aId4E3b64lSEp0,64443
48
+ dbos/_sys_db.py,sha256=05kxY9YY9iUZ1jQmfpymJuZS8Oi69ju_mF_M6lJi8qU,64513
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=Z1PwprvR_A8PXV2FNhcMrvV8B4NlDI9dc5naMeeNKGw,4774
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=ohrXoRshwxOwN-gFag_RW2yoNjwcCVv6nTYQdaArwek,15506
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.0a10.dist-info/RECORD,,
68
+ dbos-0.23.0a12.dist-info/RECORD,,