dbos 0.24.0a6__tar.gz → 0.24.0a8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dbos might be problematic. Click here for more details.

Files changed (98) hide show
  1. {dbos-0.24.0a6 → dbos-0.24.0a8}/PKG-INFO +1 -1
  2. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_app_db.py +16 -2
  3. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_conductor/conductor.py +84 -48
  4. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_conductor/protocol.py +14 -0
  5. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_dbos.py +8 -1
  6. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_dbos_config.py +52 -10
  7. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_sys_db.py +2 -2
  8. {dbos-0.24.0a6 → dbos-0.24.0a8}/pyproject.toml +1 -1
  9. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_admin_server.py +73 -1
  10. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_config.py +184 -8
  11. {dbos-0.24.0a6 → dbos-0.24.0a8}/LICENSE +0 -0
  12. {dbos-0.24.0a6 → dbos-0.24.0a8}/README.md +0 -0
  13. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/__init__.py +0 -0
  14. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/__main__.py +0 -0
  15. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_admin_server.py +0 -0
  16. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_classproperty.py +0 -0
  17. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_cloudutils/authentication.py +0 -0
  18. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_cloudutils/cloudutils.py +0 -0
  19. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_cloudutils/databases.py +0 -0
  20. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_context.py +0 -0
  21. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_core.py +0 -0
  22. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_croniter.py +0 -0
  23. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_db_wizard.py +0 -0
  24. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_debug.py +0 -0
  25. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_error.py +0 -0
  26. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_fastapi.py +0 -0
  27. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_flask.py +0 -0
  28. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_kafka.py +0 -0
  29. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_kafka_message.py +0 -0
  30. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_logger.py +0 -0
  31. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/env.py +0 -0
  32. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/script.py.mako +0 -0
  33. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  34. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  35. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  36. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  37. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  38. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  39. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  40. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_outcome.py +0 -0
  41. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_queue.py +0 -0
  42. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_recovery.py +0 -0
  43. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_registrations.py +0 -0
  44. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_request.py +0 -0
  45. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_roles.py +0 -0
  46. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_scheduler.py +0 -0
  47. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_schemas/__init__.py +0 -0
  48. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_schemas/application_database.py +0 -0
  49. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_schemas/system_database.py +0 -0
  50. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_serialization.py +0 -0
  51. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/README.md +0 -0
  52. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  53. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  54. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  55. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  56. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  57. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  58. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  59. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  60. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  61. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_tracer.py +0 -0
  62. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_utils.py +0 -0
  63. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/_workflow_commands.py +0 -0
  64. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/cli/_github_init.py +0 -0
  65. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/cli/_template_init.py +0 -0
  66. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/cli/cli.py +0 -0
  67. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/dbos-config.schema.json +0 -0
  68. {dbos-0.24.0a6 → dbos-0.24.0a8}/dbos/py.typed +0 -0
  69. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/__init__.py +0 -0
  70. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/atexit_no_ctor.py +0 -0
  71. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/atexit_no_launch.py +0 -0
  72. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/classdefs.py +0 -0
  73. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/conftest.py +0 -0
  74. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/more_classdefs.py +0 -0
  75. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/queuedworkflow.py +0 -0
  76. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_async.py +0 -0
  77. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_classdecorators.py +0 -0
  78. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_concurrency.py +0 -0
  79. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_croniter.py +0 -0
  80. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_dbos.py +0 -0
  81. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_dbwizard.py +0 -0
  82. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_debug.py +0 -0
  83. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_failures.py +0 -0
  84. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_fastapi.py +0 -0
  85. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_fastapi_roles.py +0 -0
  86. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_flask.py +0 -0
  87. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_kafka.py +0 -0
  88. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_outcome.py +0 -0
  89. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_package.py +0 -0
  90. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_queue.py +0 -0
  91. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_scheduler.py +0 -0
  92. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_schema_migration.py +0 -0
  93. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_singleton.py +0 -0
  94. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_spans.py +0 -0
  95. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_sqlalchemy.py +0 -0
  96. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_workflow_cancel.py +0 -0
  97. {dbos-0.24.0a6 → dbos-0.24.0a8}/tests/test_workflow_cmds.py +0 -0
  98. {dbos-0.24.0a6 → dbos-0.24.0a8}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.24.0a6
3
+ Version: 0.24.0a8
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -5,7 +5,7 @@ import sqlalchemy.dialects.postgresql as pg
5
5
  from sqlalchemy.exc import DBAPIError
6
6
  from sqlalchemy.orm import Session, sessionmaker
7
7
 
8
- from ._dbos_config import ConfigFile
8
+ from ._dbos_config import ConfigFile, DatabaseConfig
9
9
  from ._error import DBOSWorkflowConflictIDError
10
10
  from ._schemas.application_database import ApplicationSchema
11
11
 
@@ -61,8 +61,22 @@ class ApplicationDatabase:
61
61
  port=config["database"]["port"],
62
62
  database=app_db_name,
63
63
  )
64
+
65
+ connect_args = {}
66
+ if (
67
+ "connectionTimeoutMillis" in config["database"]
68
+ and config["database"]["connectionTimeoutMillis"]
69
+ ):
70
+ connect_args["connect_timeout"] = int(
71
+ config["database"]["connectionTimeoutMillis"] / 1000
72
+ )
73
+
64
74
  self.engine = sa.create_engine(
65
- app_db_url, pool_size=20, max_overflow=5, pool_timeout=30
75
+ app_db_url,
76
+ pool_size=config["database"]["app_db_pool_size"],
77
+ max_overflow=0,
78
+ pool_timeout=30,
79
+ connect_args=connect_args,
66
80
  )
67
81
  self.sessionmaker = sessionmaker(bind=self.engine)
68
82
  self.debug_mode = debug_mode
@@ -33,7 +33,7 @@ class ConductorWebsocket(threading.Thread):
33
33
  def run(self) -> None:
34
34
  while not self.evt.is_set():
35
35
  try:
36
- with connect(self.url) as websocket:
36
+ with connect(self.url, open_timeout=5) as websocket:
37
37
  self.websocket = websocket
38
38
  while not self.evt.is_set():
39
39
  message = websocket.recv()
@@ -44,6 +44,7 @@ class ConductorWebsocket(threading.Thread):
44
44
  continue
45
45
  base_message = p.BaseMessage.from_json(message)
46
46
  msg_type = base_message.type
47
+ error_message = None
47
48
  if msg_type == p.MessageType.EXECUTOR_INFO:
48
49
  info_response = p.ExecutorInfoResponse(
49
50
  type=p.MessageType.EXECUTOR_INFO,
@@ -61,14 +62,14 @@ class ConductorWebsocket(threading.Thread):
61
62
  recovery_message.executor_ids
62
63
  )
63
64
  except Exception as e:
64
- self.dbos.logger.error(
65
- f"Exception encountered when recovering workflows: {traceback.format_exc()}"
66
- )
65
+ error_message = f"Exception encountered when recovering workflows: {traceback.format_exc()}"
66
+ self.dbos.logger.error(error_message)
67
67
  success = False
68
68
  recovery_response = p.RecoveryResponse(
69
69
  type=p.MessageType.RECOVERY,
70
70
  request_id=base_message.request_id,
71
71
  success=success,
72
+ error_message=error_message,
72
73
  )
73
74
  websocket.send(recovery_response.to_json())
74
75
  elif msg_type == p.MessageType.CANCEL:
@@ -77,14 +78,14 @@ class ConductorWebsocket(threading.Thread):
77
78
  try:
78
79
  self.dbos.cancel_workflow(cancel_message.workflow_id)
79
80
  except Exception as e:
80
- self.dbos.logger.error(
81
- f"Exception encountered when cancelling workflow {cancel_message.workflow_id}: {traceback.format_exc()}"
82
- )
81
+ error_message = f"Exception encountered when cancelling workflow {cancel_message.workflow_id}: {traceback.format_exc()}"
82
+ self.dbos.logger.error(error_message)
83
83
  success = False
84
84
  cancel_response = p.CancelResponse(
85
85
  type=p.MessageType.CANCEL,
86
86
  request_id=base_message.request_id,
87
87
  success=success,
88
+ error_message=error_message,
88
89
  )
89
90
  websocket.send(cancel_response.to_json())
90
91
  elif msg_type == p.MessageType.RESUME:
@@ -93,14 +94,14 @@ class ConductorWebsocket(threading.Thread):
93
94
  try:
94
95
  self.dbos.resume_workflow(resume_message.workflow_id)
95
96
  except Exception as e:
96
- self.dbos.logger.error(
97
- f"Exception encountered when resuming workflow {resume_message.workflow_id}: {traceback.format_exc()}"
98
- )
97
+ error_message = f"Exception encountered when resuming workflow {resume_message.workflow_id}: {traceback.format_exc()}"
98
+ self.dbos.logger.error(error_message)
99
99
  success = False
100
100
  resume_response = p.ResumeResponse(
101
101
  type=p.MessageType.RESUME,
102
102
  request_id=base_message.request_id,
103
103
  success=success,
104
+ error_message=error_message,
104
105
  )
105
106
  websocket.send(resume_response.to_json())
106
107
  elif msg_type == p.MessageType.RESTART:
@@ -109,14 +110,14 @@ class ConductorWebsocket(threading.Thread):
109
110
  try:
110
111
  self.dbos.restart_workflow(restart_message.workflow_id)
111
112
  except Exception as e:
112
- self.dbos.logger.error(
113
- f"Exception encountered when restarting workflow {restart_message.workflow_id}: {traceback.format_exc()}"
114
- )
113
+ error_message = f"Exception encountered when restarting workflow {restart_message.workflow_id}: {traceback.format_exc()}"
114
+ self.dbos.logger.error(error_message)
115
115
  success = False
116
116
  restart_response = p.RestartResponse(
117
117
  type=p.MessageType.RESTART,
118
118
  request_id=base_message.request_id,
119
119
  success=success,
120
+ error_message=error_message,
120
121
  )
121
122
  websocket.send(restart_response.to_json())
122
123
  elif msg_type == p.MessageType.LIST_WORKFLOWS:
@@ -124,20 +125,26 @@ class ConductorWebsocket(threading.Thread):
124
125
  message
125
126
  )
126
127
  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
- )
128
+ infos = []
129
+ try:
130
+ infos = list_workflows(
131
+ self.dbos._sys_db,
132
+ workflow_ids=body["workflow_uuids"],
133
+ user=body["authenticated_user"],
134
+ start_time=body["start_time"],
135
+ end_time=body["end_time"],
136
+ status=body["status"],
137
+ request=False,
138
+ app_version=body["application_version"],
139
+ name=body["workflow_name"],
140
+ limit=body["limit"],
141
+ offset=body["offset"],
142
+ sort_desc=body["sort_desc"],
143
+ )
144
+ except Exception as e:
145
+ error_message = f"Exception encountered when listing workflows: {traceback.format_exc()}"
146
+ self.dbos.logger.error(error_message)
147
+
141
148
  list_workflows_response = p.ListWorkflowsResponse(
142
149
  type=p.MessageType.LIST_WORKFLOWS,
143
150
  request_id=base_message.request_id,
@@ -145,6 +152,7 @@ class ConductorWebsocket(threading.Thread):
145
152
  p.WorkflowsOutput.from_workflow_information(i)
146
153
  for i in infos
147
154
  ],
155
+ error_message=error_message,
148
156
  )
149
157
  websocket.send(list_workflows_response.to_json())
150
158
  elif msg_type == p.MessageType.LIST_QUEUED_WORKFLOWS:
@@ -152,18 +160,24 @@ class ConductorWebsocket(threading.Thread):
152
160
  p.ListQueuedWorkflowsRequest.from_json(message)
153
161
  )
154
162
  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
- )
163
+ infos = []
164
+ try:
165
+ infos = list_queued_workflows(
166
+ self.dbos._sys_db,
167
+ start_time=q_body["start_time"],
168
+ end_time=q_body["end_time"],
169
+ status=q_body["status"],
170
+ request=False,
171
+ name=q_body["workflow_name"],
172
+ limit=q_body["limit"],
173
+ offset=q_body["offset"],
174
+ queue_name=q_body["queue_name"],
175
+ sort_desc=q_body["sort_desc"],
176
+ )
177
+ except Exception as e:
178
+ error_message = f"Exception encountered when listing queued workflows: {traceback.format_exc()}"
179
+ self.dbos.logger.error(error_message)
180
+
167
181
  list_queued_workflows_response = (
168
182
  p.ListQueuedWorkflowsResponse(
169
183
  type=p.MessageType.LIST_QUEUED_WORKFLOWS,
@@ -172,6 +186,7 @@ class ConductorWebsocket(threading.Thread):
172
186
  p.WorkflowsOutput.from_workflow_information(i)
173
187
  for i in infos
174
188
  ],
189
+ error_message=error_message,
175
190
  )
176
191
  )
177
192
  websocket.send(list_queued_workflows_response.to_json())
@@ -179,11 +194,17 @@ class ConductorWebsocket(threading.Thread):
179
194
  get_workflow_message = p.GetWorkflowRequest.from_json(
180
195
  message
181
196
  )
182
- info = get_workflow(
183
- self.dbos._sys_db,
184
- get_workflow_message.workflow_id,
185
- getRequest=False,
186
- )
197
+ info = None
198
+ try:
199
+ info = get_workflow(
200
+ self.dbos._sys_db,
201
+ get_workflow_message.workflow_id,
202
+ getRequest=False,
203
+ )
204
+ except Exception as e:
205
+ error_message = f"Exception encountered when getting workflow {get_workflow_message.workflow_id}: {traceback.format_exc()}"
206
+ self.dbos.logger.error(error_message)
207
+
187
208
  get_workflow_response = p.GetWorkflowResponse(
188
209
  type=p.MessageType.GET_WORKFLOW,
189
210
  request_id=base_message.request_id,
@@ -192,21 +213,29 @@ class ConductorWebsocket(threading.Thread):
192
213
  if info is not None
193
214
  else None
194
215
  ),
216
+ error_message=error_message,
195
217
  )
196
218
  websocket.send(get_workflow_response.to_json())
197
219
  elif msg_type == p.MessageType.EXIST_PENDING_WORKFLOWS:
198
220
  exist_pending_workflows_message = (
199
221
  p.ExistPendingWorkflowsRequest.from_json(message)
200
222
  )
201
- pending_wfs = self.dbos._sys_db.get_pending_workflows(
202
- exist_pending_workflows_message.executor_id,
203
- exist_pending_workflows_message.application_version,
204
- )
223
+ pending_wfs = []
224
+ try:
225
+ pending_wfs = self.dbos._sys_db.get_pending_workflows(
226
+ exist_pending_workflows_message.executor_id,
227
+ exist_pending_workflows_message.application_version,
228
+ )
229
+ except Exception as e:
230
+ error_message = f"Exception encountered when checking for pending workflows: {traceback.format_exc()}"
231
+ self.dbos.logger.error(error_message)
232
+
205
233
  exist_pending_workflows_response = (
206
234
  p.ExistPendingWorkflowsResponse(
207
235
  type=p.MessageType.EXIST_PENDING_WORKFLOWS,
208
236
  request_id=base_message.request_id,
209
237
  exist=len(pending_wfs) > 0,
238
+ error_message=error_message,
210
239
  )
211
240
  )
212
241
  websocket.send(exist_pending_workflows_response.to_json())
@@ -214,6 +243,13 @@ class ConductorWebsocket(threading.Thread):
214
243
  self.dbos.logger.warning(
215
244
  f"Unexpected message type: {msg_type}"
216
245
  )
246
+ unknown_message = p.BaseResponse(
247
+ request_id=base_message.request_id,
248
+ type=msg_type,
249
+ error_message="Unknown message type",
250
+ )
251
+ # Still need to send a response to the conductor
252
+ websocket.send(unknown_message.to_json())
217
253
  except ConnectionClosedOK:
218
254
  self.dbos.logger.info("Conductor connection terminated")
219
255
  break
@@ -45,6 +45,11 @@ class BaseMessage:
45
45
  return json.dumps(dict_data)
46
46
 
47
47
 
48
+ @dataclass
49
+ class BaseResponse(BaseMessage):
50
+ error_message: Optional[str] = None
51
+
52
+
48
53
  @dataclass
49
54
  class ExecutorInfoRequest(BaseMessage):
50
55
  pass
@@ -54,6 +59,7 @@ class ExecutorInfoRequest(BaseMessage):
54
59
  class ExecutorInfoResponse(BaseMessage):
55
60
  executor_id: str
56
61
  application_version: str
62
+ error_message: Optional[str] = None
57
63
 
58
64
 
59
65
  @dataclass
@@ -64,6 +70,7 @@ class RecoveryRequest(BaseMessage):
64
70
  @dataclass
65
71
  class RecoveryResponse(BaseMessage):
66
72
  success: bool
73
+ error_message: Optional[str] = None
67
74
 
68
75
 
69
76
  @dataclass
@@ -74,6 +81,7 @@ class CancelRequest(BaseMessage):
74
81
  @dataclass
75
82
  class CancelResponse(BaseMessage):
76
83
  success: bool
84
+ error_message: Optional[str] = None
77
85
 
78
86
 
79
87
  @dataclass
@@ -84,6 +92,7 @@ class ResumeRequest(BaseMessage):
84
92
  @dataclass
85
93
  class ResumeResponse(BaseMessage):
86
94
  success: bool
95
+ error_message: Optional[str] = None
87
96
 
88
97
 
89
98
  @dataclass
@@ -94,6 +103,7 @@ class RestartRequest(BaseMessage):
94
103
  @dataclass
95
104
  class RestartResponse(BaseMessage):
96
105
  success: bool
106
+ error_message: Optional[str] = None
97
107
 
98
108
 
99
109
  class ListWorkflowsBody(TypedDict):
@@ -165,6 +175,7 @@ class ListWorkflowsRequest(BaseMessage):
165
175
  @dataclass
166
176
  class ListWorkflowsResponse(BaseMessage):
167
177
  output: List[WorkflowsOutput]
178
+ error_message: Optional[str] = None
168
179
 
169
180
 
170
181
  class ListQueuedWorkflowsBody(TypedDict):
@@ -186,6 +197,7 @@ class ListQueuedWorkflowsRequest(BaseMessage):
186
197
  @dataclass
187
198
  class ListQueuedWorkflowsResponse(BaseMessage):
188
199
  output: List[WorkflowsOutput]
200
+ error_message: Optional[str] = None
189
201
 
190
202
 
191
203
  @dataclass
@@ -196,6 +208,7 @@ class GetWorkflowRequest(BaseMessage):
196
208
  @dataclass
197
209
  class GetWorkflowResponse(BaseMessage):
198
210
  output: Optional[WorkflowsOutput]
211
+ error_message: Optional[str] = None
199
212
 
200
213
 
201
214
  @dataclass
@@ -207,3 +220,4 @@ class ExistPendingWorkflowsRequest(BaseMessage):
207
220
  @dataclass
208
221
  class ExistPendingWorkflowsResponse(BaseMessage):
209
222
  exist: bool
223
+ error_message: Optional[str] = None
@@ -457,7 +457,14 @@ class DBOS:
457
457
  admin_port = self.config.get("runtimeConfig", {}).get("admin_port")
458
458
  if admin_port is None:
459
459
  admin_port = 3001
460
- self._admin_server_field = AdminServer(dbos=self, port=admin_port)
460
+ run_admin_server = self.config.get("runtimeConfig", {}).get(
461
+ "run_admin_server"
462
+ )
463
+ if run_admin_server:
464
+ try:
465
+ self._admin_server_field = AdminServer(dbos=self, port=admin_port)
466
+ except Exception as e:
467
+ dbos_logger.warning(f"Failed to start admin server: {e}")
461
468
 
462
469
  workflow_ids = self._sys_db.get_pending_workflows(
463
470
  GlobalParams.executor_id, GlobalParams.app_version
@@ -22,30 +22,38 @@ from ._logger import dbos_logger
22
22
  DBOS_CONFIG_PATH = "dbos-config.yaml"
23
23
 
24
24
 
25
- class DBOSConfig(TypedDict):
25
+ class DBOSConfig(TypedDict, total=False):
26
26
  """
27
27
  Data structure containing the DBOS library configuration.
28
28
 
29
29
  Attributes:
30
30
  name (str): Application name
31
31
  database_url (str): Database connection string
32
+ app_db_pool_size (int): Application database pool size
32
33
  sys_db_name (str): System database name
34
+ sys_db_pool_size (int): System database pool size
33
35
  log_level (str): Log level
34
36
  otlp_traces_endpoints: List[str]: OTLP traces endpoints
37
+ admin_port (int): Admin port
38
+ run_admin_server (bool): Whether to run the DBOS admin server
35
39
  """
36
40
 
37
41
  name: str
38
42
  database_url: Optional[str]
43
+ app_db_pool_size: Optional[int]
39
44
  sys_db_name: Optional[str]
45
+ sys_db_pool_size: Optional[int]
40
46
  log_level: Optional[str]
41
47
  otlp_traces_endpoints: Optional[List[str]]
42
48
  admin_port: Optional[int]
49
+ run_admin_server: Optional[bool]
43
50
 
44
51
 
45
52
  class RuntimeConfig(TypedDict, total=False):
46
53
  start: List[str]
47
54
  setup: Optional[List[str]]
48
55
  admin_port: Optional[int]
56
+ run_admin_server: Optional[bool]
49
57
 
50
58
 
51
59
  class DatabaseConfig(TypedDict, total=False):
@@ -55,7 +63,9 @@ class DatabaseConfig(TypedDict, total=False):
55
63
  password: str
56
64
  connectionTimeoutMillis: Optional[int]
57
65
  app_db_name: str
66
+ app_db_pool_size: Optional[int]
58
67
  sys_db_name: Optional[str]
68
+ sys_db_pool_size: Optional[int]
59
69
  ssl: Optional[bool]
60
70
  ssl_ca: Optional[str]
61
71
  local_suffix: Optional[bool]
@@ -160,12 +170,21 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
160
170
  db_config = parse_database_url_to_dbconfig(database_url)
161
171
  if "sys_db_name" in config:
162
172
  db_config["sys_db_name"] = config.get("sys_db_name")
173
+ if "app_db_pool_size" in config:
174
+ db_config["app_db_pool_size"] = config.get("app_db_pool_size")
175
+ if "sys_db_pool_size" in config:
176
+ db_config["sys_db_pool_size"] = config.get("sys_db_pool_size")
163
177
  if db_config:
164
178
  translated_config["database"] = db_config
165
179
 
166
- # Admin port
180
+ # Runtime config
181
+ translated_config["runtimeConfig"] = {"run_admin_server": True}
167
182
  if "admin_port" in config:
168
- translated_config["runtimeConfig"] = {"admin_port": config["admin_port"]}
183
+ translated_config["runtimeConfig"]["admin_port"] = config["admin_port"]
184
+ if "run_admin_server" in config:
185
+ translated_config["runtimeConfig"]["run_admin_server"] = config[
186
+ "run_admin_server"
187
+ ]
169
188
 
170
189
  # Telemetry config
171
190
  telemetry = {}
@@ -289,6 +308,15 @@ def process_config(
289
308
  f'Invalid app name {data["name"]}. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores.'
290
309
  )
291
310
 
311
+ if data.get("telemetry") is None:
312
+ data["telemetry"] = {}
313
+ telemetry = cast(TelemetryConfig, data["telemetry"])
314
+ if telemetry.get("logs") is None:
315
+ telemetry["logs"] = {}
316
+ logs = cast(LoggerConfig, telemetry["logs"])
317
+ if logs.get("logLevel") is None:
318
+ logs["logLevel"] = "INFO"
319
+
292
320
  if "database" not in data:
293
321
  data["database"] = {}
294
322
 
@@ -379,6 +407,20 @@ def process_config(
379
407
  local_suffix = dbos_dblocalsuffix
380
408
  data["database"]["local_suffix"] = local_suffix
381
409
 
410
+ if not data["database"].get("app_db_pool_size"):
411
+ data["database"]["app_db_pool_size"] = 20
412
+ if not data["database"].get("sys_db_pool_size"):
413
+ data["database"]["sys_db_pool_size"] = 20
414
+ if not data["database"].get("connectionTimeoutMillis"):
415
+ data["database"]["connectionTimeoutMillis"] = 10000
416
+
417
+ if not data.get("runtimeConfig"):
418
+ data["runtimeConfig"] = {
419
+ "run_admin_server": True,
420
+ }
421
+ elif "run_admin_server" not in data["runtimeConfig"]:
422
+ data["runtimeConfig"]["run_admin_server"] = True
423
+
382
424
  # Check the connectivity to the database and make sure it's properly configured
383
425
  # Note, never use db wizard if the DBOS is running in debug mode (i.e. DBOS_DEBUG_WORKFLOW_ID env var is set)
384
426
  debugWorkflowId = os.getenv("DBOS_DEBUG_WORKFLOW_ID")
@@ -475,13 +517,13 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
475
517
  otlp_exporter["logsEndpoint"] = logsEndpoint
476
518
 
477
519
  # Runtime config
478
- if (
479
- "runtimeConfig" in provided_config
480
- and "admin_port" in provided_config["runtimeConfig"]
481
- ):
482
- del provided_config["runtimeConfig"][
483
- "admin_port"
484
- ] # Admin port is expected to be 3001 (the default in dbos/_admin_server.py::__init__ ) by DBOS Cloud
520
+ if "runtimeConfig" in provided_config:
521
+ if "admin_port" in provided_config["runtimeConfig"]:
522
+ del provided_config["runtimeConfig"][
523
+ "admin_port"
524
+ ] # Admin port is expected to be 3001 (the default in dbos/_admin_server.py::__init__ ) by DBOS Cloud
525
+ if "run_admin_server" in provided_config["runtimeConfig"]:
526
+ del provided_config["runtimeConfig"]["run_admin_server"]
485
527
 
486
528
  # Env should be set from the hosting provider (e.g., DBOS Cloud)
487
529
  if "env" in provided_config:
@@ -203,8 +203,8 @@ class SystemDatabase:
203
203
  # Create a connection pool for the system database
204
204
  self.engine = sa.create_engine(
205
205
  system_db_url,
206
- pool_size=20,
207
- max_overflow=5,
206
+ pool_size=config["database"]["sys_db_pool_size"],
207
+ max_overflow=0,
208
208
  pool_timeout=30,
209
209
  connect_args={"connect_timeout": 10},
210
210
  )
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.24.0a6"
31
+ version = "0.24.0a8"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -1,13 +1,16 @@
1
1
  import os
2
+ import socket
2
3
  import threading
3
4
  import time
4
5
  import uuid
5
6
 
7
+ import pytest
6
8
  import requests
7
9
  import sqlalchemy as sa
10
+ from requests.exceptions import ConnectionError
8
11
 
9
12
  # Public API
10
- from dbos import DBOS, ConfigFile, Queue, SetWorkflowID, _workflow_commands
13
+ from dbos import DBOS, ConfigFile, DBOSConfig, Queue, SetWorkflowID, _workflow_commands
11
14
  from dbos._schemas.system_database import SystemSchema
12
15
  from dbos._sys_db import SystemDatabase, WorkflowStatusString
13
16
  from dbos._utils import GlobalParams
@@ -172,6 +175,75 @@ runtimeConfig:
172
175
  os.remove("dbos-config.yaml")
173
176
 
174
177
 
178
+ def test_disable_admin_server(cleanup_test_databases: None) -> None:
179
+ # Initialize singleton
180
+ DBOS.destroy() # In case of other tests leaving it
181
+
182
+ config: DBOSConfig = {
183
+ "name": "test-app",
184
+ "run_admin_server": False,
185
+ }
186
+ try:
187
+ DBOS(config=config)
188
+ DBOS.launch()
189
+
190
+ with pytest.raises(ConnectionError):
191
+ requests.get("http://localhost:3001/dbos-healthz", timeout=1)
192
+ finally:
193
+ # Clean up after the test
194
+ DBOS.destroy()
195
+
196
+
197
+ def test_busy_admin_server_port_does_not_throw() -> None:
198
+ # Initialize singleton
199
+ DBOS.destroy() # In case of other tests leaving it
200
+
201
+ config: DBOSConfig = {
202
+ "name": "test-app",
203
+ }
204
+ server_thread = None
205
+ stop_event = threading.Event()
206
+ try:
207
+
208
+ def start_dummy_server(port: int, stop_event: threading.Event) -> None:
209
+ """Starts a simple TCP server on the given port."""
210
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
211
+ server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
212
+ server_socket.bind(("0.0.0.0", port))
213
+ server_socket.listen(1)
214
+ # We need to call accept for the port to be considered busy by the OS...
215
+ while not stop_event.is_set():
216
+ try:
217
+ server_socket.settimeout(
218
+ 1
219
+ ) # Timeout for accept in case the thread is stopped.
220
+ client_socket, _ = server_socket.accept()
221
+ client_socket.close()
222
+ except socket.timeout:
223
+ pass
224
+ server_socket.close()
225
+
226
+ port_to_block = 3001
227
+ server_thread = threading.Thread(
228
+ target=start_dummy_server, args=(port_to_block, stop_event)
229
+ )
230
+ server_thread.daemon = (
231
+ True # Allows the thread to be terminated when the main thread exits
232
+ )
233
+ server_thread.start()
234
+
235
+ DBOS(config=config)
236
+ DBOS.launch()
237
+ finally:
238
+ # Clean up after the test
239
+ DBOS.destroy()
240
+ if server_thread and server_thread.is_alive():
241
+ stop_event.set()
242
+ server_thread.join(2)
243
+ if server_thread.is_alive():
244
+ print("Warning: Server thread did not terminate gracefully.")
245
+
246
+
175
247
  def test_admin_workflow_resume(dbos: DBOS, sys_db: SystemDatabase) -> None:
176
248
  counter: int = 0
177
249
  event = threading.Event()