dbos 0.28.0a4__tar.gz → 0.28.0a7__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.
Files changed (106) hide show
  1. {dbos-0.28.0a4 → dbos-0.28.0a7}/PKG-INFO +2 -2
  2. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_admin_server.py +3 -3
  3. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_client.py +4 -8
  4. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_conductor/conductor.py +71 -8
  5. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_conductor/protocol.py +1 -1
  6. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_context.py +0 -4
  7. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_core.py +1 -8
  8. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_dbos.py +13 -17
  9. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_fastapi.py +2 -23
  10. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_flask.py +3 -37
  11. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_recovery.py +1 -1
  12. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_schemas/system_database.py +0 -1
  13. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_sys_db.py +46 -60
  14. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_utils.py +2 -0
  15. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_workflow_commands.py +4 -8
  16. {dbos-0.28.0a4 → dbos-0.28.0a7}/pyproject.toml +2 -2
  17. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_admin_server.py +59 -7
  18. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_dbos.py +0 -1
  19. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_fastapi.py +6 -14
  20. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_flask.py +0 -5
  21. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_scheduler.py +2 -2
  22. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_workflow_introspection.py +1 -2
  23. dbos-0.28.0a4/dbos/_request.py +0 -35
  24. {dbos-0.28.0a4 → dbos-0.28.0a7}/LICENSE +0 -0
  25. {dbos-0.28.0a4 → dbos-0.28.0a7}/README.md +0 -0
  26. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/__init__.py +0 -0
  27. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/__main__.py +0 -0
  28. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_app_db.py +0 -0
  29. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_classproperty.py +0 -0
  30. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_croniter.py +0 -0
  31. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_dbos_config.py +0 -0
  32. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_debug.py +0 -0
  33. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_docker_pg_helper.py +0 -0
  34. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_error.py +0 -0
  35. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_event_loop.py +0 -0
  36. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_kafka.py +0 -0
  37. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_kafka_message.py +0 -0
  38. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_logger.py +0 -0
  39. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/env.py +0 -0
  40. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/script.py.mako +0 -0
  41. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  42. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  43. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  44. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  45. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  46. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  47. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  48. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  49. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  50. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  51. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  52. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_outcome.py +0 -0
  53. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_queue.py +0 -0
  54. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_registrations.py +0 -0
  55. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_roles.py +0 -0
  56. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_scheduler.py +0 -0
  57. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_schemas/__init__.py +0 -0
  58. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_schemas/application_database.py +0 -0
  59. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_serialization.py +0 -0
  60. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/README.md +0 -0
  61. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  62. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  63. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  64. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  65. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  66. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  67. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  68. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  69. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  70. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/_tracer.py +0 -0
  71. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/cli/_github_init.py +0 -0
  72. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/cli/_template_init.py +0 -0
  73. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/cli/cli.py +0 -0
  74. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/dbos-config.schema.json +0 -0
  75. {dbos-0.28.0a4 → dbos-0.28.0a7}/dbos/py.typed +0 -0
  76. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/__init__.py +0 -0
  77. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/atexit_no_ctor.py +0 -0
  78. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/atexit_no_launch.py +0 -0
  79. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/classdefs.py +0 -0
  80. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/client_collateral.py +0 -0
  81. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/client_worker.py +0 -0
  82. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/conftest.py +0 -0
  83. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/dupname_classdefs1.py +0 -0
  84. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/dupname_classdefsa.py +0 -0
  85. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/more_classdefs.py +0 -0
  86. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/queuedworkflow.py +0 -0
  87. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_async.py +0 -0
  88. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_classdecorators.py +0 -0
  89. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_client.py +0 -0
  90. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_concurrency.py +0 -0
  91. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_config.py +0 -0
  92. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_croniter.py +0 -0
  93. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_debug.py +0 -0
  94. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_docker_secrets.py +0 -0
  95. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_failures.py +0 -0
  96. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_fastapi_roles.py +0 -0
  97. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_kafka.py +0 -0
  98. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_outcome.py +0 -0
  99. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_package.py +0 -0
  100. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_queue.py +0 -0
  101. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_schema_migration.py +0 -0
  102. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_singleton.py +0 -0
  103. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_spans.py +0 -0
  104. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_sqlalchemy.py +0 -0
  105. {dbos-0.28.0a4 → dbos-0.28.0a7}/tests/test_workflow_management.py +0 -0
  106. {dbos-0.28.0a4 → dbos-0.28.0a7}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.28.0a4
3
+ Version: 0.28.0a7
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -22,7 +22,7 @@ Requires-Dist: docker>=7.1.0
22
22
  Requires-Dist: cryptography>=43.0.3
23
23
  Requires-Dist: rich>=13.9.4
24
24
  Requires-Dist: pyjwt>=2.10.1
25
- Requires-Dist: websockets>=15.0
25
+ Requires-Dist: websockets>=14.0
26
26
  Description-Content-Type: text/markdown
27
27
 
28
28
 
@@ -66,11 +66,11 @@ class AdminRequestHandler(BaseHTTPRequestHandler):
66
66
  elif self.path == _deactivate_path:
67
67
  if not AdminRequestHandler.is_deactivated:
68
68
  dbos_logger.info(
69
- f"Deactivating DBOS executor {GlobalParams.executor_id} with version {GlobalParams.app_version}. This executor will complete existing workflows but will not start new workflows."
69
+ f"Deactivating DBOS executor {GlobalParams.executor_id} with version {GlobalParams.app_version}. This executor will complete existing workflows but will not create new workflows."
70
70
  )
71
71
  AdminRequestHandler.is_deactivated = True
72
- # Stop all scheduled workflows, queues, and kafka loops
73
- for event in self.dbos.stop_events:
72
+ # Stop all event receivers (scheduler and Kafka threads)
73
+ for event in self.dbos.poller_stop_events:
74
74
  event.set()
75
75
  self.send_response(200)
76
76
  self._end_headers()
@@ -70,7 +70,7 @@ class WorkflowHandleClientPolling(Generic[R]):
70
70
  return res
71
71
 
72
72
  def get_status(self) -> WorkflowStatus:
73
- status = get_workflow(self._sys_db, self.workflow_id, False)
73
+ status = get_workflow(self._sys_db, self.workflow_id)
74
74
  if status is None:
75
75
  raise DBOSNonExistentWorkflowError(self.workflow_id)
76
76
  return status
@@ -92,9 +92,7 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
92
92
  return res
93
93
 
94
94
  async def get_status(self) -> WorkflowStatus:
95
- status = await asyncio.to_thread(
96
- get_workflow, self._sys_db, self.workflow_id, False
97
- )
95
+ status = await asyncio.to_thread(get_workflow, self._sys_db, self.workflow_id)
98
96
  if status is None:
99
97
  raise DBOSNonExistentWorkflowError(self.workflow_id)
100
98
  return status
@@ -141,7 +139,6 @@ class DBOSClient:
141
139
  "authenticated_user": None,
142
140
  "assumed_role": None,
143
141
  "authenticated_roles": None,
144
- "request": None,
145
142
  "output": None,
146
143
  "error": None,
147
144
  "created_at": None,
@@ -181,13 +178,13 @@ class DBOSClient:
181
178
  return WorkflowHandleClientAsyncPolling[R](workflow_id, self._sys_db)
182
179
 
183
180
  def retrieve_workflow(self, workflow_id: str) -> WorkflowHandle[R]:
184
- status = get_workflow(self._sys_db, workflow_id, False)
181
+ status = get_workflow(self._sys_db, workflow_id)
185
182
  if status is None:
186
183
  raise DBOSNonExistentWorkflowError(workflow_id)
187
184
  return WorkflowHandleClientPolling[R](workflow_id, self._sys_db)
188
185
 
189
186
  async def retrieve_workflow_async(self, workflow_id: str) -> WorkflowHandleAsync[R]:
190
- status = asyncio.to_thread(get_workflow, self._sys_db, workflow_id, False)
187
+ status = asyncio.to_thread(get_workflow, self._sys_db, workflow_id)
191
188
  if status is None:
192
189
  raise DBOSNonExistentWorkflowError(workflow_id)
193
190
  return WorkflowHandleClientAsyncPolling[R](workflow_id, self._sys_db)
@@ -210,7 +207,6 @@ class DBOSClient:
210
207
  "authenticated_user": None,
211
208
  "assumed_role": None,
212
209
  "authenticated_roles": None,
213
- "request": None,
214
210
  "output": None,
215
211
  "error": None,
216
212
  "created_at": None,
@@ -2,6 +2,7 @@ import socket
2
2
  import threading
3
3
  import time
4
4
  import traceback
5
+ from importlib.metadata import version
5
6
  from typing import TYPE_CHECKING, Optional
6
7
 
7
8
  from websockets import ConnectionClosed, ConnectionClosedOK, InvalidStatus
@@ -21,6 +22,9 @@ from . import protocol as p
21
22
  if TYPE_CHECKING:
22
23
  from dbos import DBOS
23
24
 
25
+ ws_version = version("websockets")
26
+ use_keepalive = ws_version < "15.0"
27
+
24
28
 
25
29
  class ConductorWebsocket(threading.Thread):
26
30
 
@@ -35,14 +39,64 @@ class ConductorWebsocket(threading.Thread):
35
39
  self.url = (
36
40
  conductor_url.rstrip("/") + f"/websocket/{self.app_name}/{conductor_key}"
37
41
  )
42
+ # TODO: once we can upgrade to websockets>=15.0, we can always use the built-in keepalive
43
+ self.ping_interval = 20 # Time between pings in seconds
44
+ self.ping_timeout = 15 # Time to wait for a pong response in seconds
45
+ self.keepalive_thread: Optional[threading.Thread] = None
46
+ self.pong_event: Optional[threading.Event] = None
47
+ self.last_ping_time = -1.0
48
+
49
+ self.dbos.logger.debug(
50
+ f"Connecting to conductor at {self.url} using websockets version {ws_version}"
51
+ )
52
+
53
+ def keepalive(self) -> None:
54
+ self.dbos.logger.debug("Starting keepalive thread")
55
+ while not self.evt.is_set():
56
+ if self.websocket is None or self.websocket.close_code is not None:
57
+ time.sleep(1)
58
+ continue
59
+ try:
60
+ self.last_ping_time = time.time()
61
+ self.pong_event = self.websocket.ping()
62
+ self.dbos.logger.debug("> Sent ping to conductor")
63
+ pong_result = self.pong_event.wait(self.ping_timeout)
64
+ elapsed_time = time.time() - self.last_ping_time
65
+ if not pong_result:
66
+ self.dbos.logger.warning(
67
+ f"Failed to receive pong from conductor after {elapsed_time:.2f} seconds. Reconnecting."
68
+ )
69
+ self.websocket.close()
70
+ continue
71
+ # Wait for the next ping interval
72
+ self.dbos.logger.debug(
73
+ f"< Received pong from conductor after {elapsed_time:.2f} seconds"
74
+ )
75
+ wait_time = self.ping_interval - elapsed_time
76
+ self.evt.wait(max(0, wait_time))
77
+ except ConnectionClosed:
78
+ # The main loop will try to reconnect
79
+ self.dbos.logger.debug("Connection to conductor closed.")
80
+ except Exception as e:
81
+ self.dbos.logger.warning(f"Failed to send ping to conductor: {e}.")
82
+ self.websocket.close()
38
83
 
39
84
  def run(self) -> None:
40
85
  while not self.evt.is_set():
41
86
  try:
42
87
  with connect(
43
- self.url, open_timeout=5, logger=self.dbos.logger
88
+ self.url,
89
+ open_timeout=5,
90
+ close_timeout=5,
91
+ logger=self.dbos.logger,
44
92
  ) as websocket:
45
93
  self.websocket = websocket
94
+ if use_keepalive and self.keepalive_thread is None:
95
+ self.keepalive_thread = threading.Thread(
96
+ target=self.keepalive,
97
+ daemon=True,
98
+ )
99
+ self.keepalive_thread.start()
46
100
  while not self.evt.is_set():
47
101
  message = websocket.recv()
48
102
  if not isinstance(message, str):
@@ -143,7 +197,6 @@ class ConductorWebsocket(threading.Thread):
143
197
  start_time=body["start_time"],
144
198
  end_time=body["end_time"],
145
199
  status=body["status"],
146
- request=False,
147
200
  app_version=body["application_version"],
148
201
  name=body["workflow_name"],
149
202
  limit=body["limit"],
@@ -176,7 +229,6 @@ class ConductorWebsocket(threading.Thread):
176
229
  start_time=q_body["start_time"],
177
230
  end_time=q_body["end_time"],
178
231
  status=q_body["status"],
179
- request=False,
180
232
  name=q_body["workflow_name"],
181
233
  limit=q_body["limit"],
182
234
  offset=q_body["offset"],
@@ -206,9 +258,7 @@ class ConductorWebsocket(threading.Thread):
206
258
  info = None
207
259
  try:
208
260
  info = get_workflow(
209
- self.dbos._sys_db,
210
- get_workflow_message.workflow_id,
211
- get_request=False,
261
+ self.dbos._sys_db, get_workflow_message.workflow_id
212
262
  )
213
263
  except Exception as e:
214
264
  error_message = f"Exception encountered when getting workflow {get_workflow_message.workflow_id}: {traceback.format_exc()}"
@@ -287,8 +337,15 @@ class ConductorWebsocket(threading.Thread):
287
337
  # Still need to send a response to the conductor
288
338
  websocket.send(unknown_message.to_json())
289
339
  except ConnectionClosedOK:
290
- self.dbos.logger.info("Conductor connection terminated")
291
- break
340
+ if self.evt.is_set():
341
+ self.dbos.logger.info("Conductor connection terminated")
342
+ break
343
+ # Otherwise, we are trying to reconnect
344
+ self.dbos.logger.warning(
345
+ "Connection to conductor lost. Reconnecting..."
346
+ )
347
+ time.sleep(1)
348
+ continue
292
349
  except ConnectionClosed as e:
293
350
  self.dbos.logger.warning(
294
351
  f"Connection to conductor lost. Reconnecting: {e}"
@@ -309,3 +366,9 @@ class ConductorWebsocket(threading.Thread):
309
366
  )
310
367
  time.sleep(1)
311
368
  continue
369
+
370
+ # Wait for the keepalive thread to finish
371
+ if self.keepalive_thread is not None:
372
+ if self.pong_event is not None:
373
+ self.pong_event.set()
374
+ self.keepalive_thread.join()
@@ -149,7 +149,7 @@ class WorkflowsOutput:
149
149
  inputs_str = str(info.input) if info.input is not None else None
150
150
  outputs_str = str(info.output) if info.output is not None else None
151
151
  error_str = str(info.error) if info.error is not None else None
152
- request_str = str(info.request) if info.request is not None else None
152
+ request_str = None
153
153
  roles_str = (
154
154
  str(info.authenticated_roles)
155
155
  if info.authenticated_roles is not None
@@ -16,7 +16,6 @@ from sqlalchemy.orm import Session
16
16
  from dbos._utils import GlobalParams
17
17
 
18
18
  from ._logger import dbos_logger
19
- from ._request import Request
20
19
  from ._tracer import dbos_tracer
21
20
 
22
21
 
@@ -76,8 +75,6 @@ class DBOSContext:
76
75
 
77
76
  self.logger = dbos_logger
78
77
 
79
- self.request: Optional["Request"] = None
80
-
81
78
  self.id_assigned_for_next_workflow: str = ""
82
79
  self.is_within_set_workflow_id_block: bool = False
83
80
 
@@ -120,7 +117,6 @@ class DBOSContext:
120
117
  if self.authenticated_roles is not None
121
118
  else None
122
119
  )
123
- rv.request = self.request
124
120
  rv.assumed_role = self.assumed_role
125
121
  return rv
126
122
 
@@ -266,9 +266,6 @@ def _init_workflow(
266
266
  "app_id": ctx.app_id,
267
267
  "app_version": GlobalParams.app_version,
268
268
  "executor_id": ctx.executor_id,
269
- "request": (
270
- _serialization.serialize(ctx.request) if ctx.request is not None else None
271
- ),
272
269
  "recovery_attempts": None,
273
270
  "authenticated_user": ctx.authenticated_user,
274
271
  "authenticated_roles": (
@@ -296,7 +293,7 @@ def _init_workflow(
296
293
 
297
294
  if workflow_deadline_epoch_ms is not None:
298
295
  evt = threading.Event()
299
- dbos.stop_events.append(evt)
296
+ dbos.background_thread_stop_events.append(evt)
300
297
 
301
298
  def timeout_func() -> None:
302
299
  try:
@@ -443,10 +440,6 @@ def execute_workflow_by_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[An
443
440
  )
444
441
  with DBOSContextEnsure():
445
442
  ctx = assert_current_dbos_context()
446
- request = status["request"]
447
- ctx.request = (
448
- _serialization.deserialize(request) if request is not None else None
449
- )
450
443
  # If this function belongs to a configured class, add that class instance as its first argument
451
444
  if status["config_name"] is not None:
452
445
  config_name = status["config_name"]
@@ -71,14 +71,11 @@ from ._tracer import DBOSTracer, dbos_tracer
71
71
  if TYPE_CHECKING:
72
72
  from fastapi import FastAPI
73
73
  from ._kafka import _KafkaConsumerWorkflow
74
- from ._request import Request
75
74
  from flask import Flask
76
75
 
77
76
  from sqlalchemy import URL
78
77
  from sqlalchemy.orm import Session
79
78
 
80
- from ._request import Request
81
-
82
79
  if sys.version_info < (3, 10):
83
80
  from typing_extensions import ParamSpec
84
81
  else:
@@ -197,7 +194,7 @@ class DBOSRegistry:
197
194
  self, evt: threading.Event, func: Callable[..., Any], *args: Any, **kwargs: Any
198
195
  ) -> None:
199
196
  if self.dbos and self.dbos._launched:
200
- self.dbos.stop_events.append(evt)
197
+ self.dbos.poller_stop_events.append(evt)
201
198
  self.dbos._executor.submit(func, *args, **kwargs)
202
199
  else:
203
200
  self.pollers.append((evt, func, args, kwargs))
@@ -247,7 +244,7 @@ class DBOS:
247
244
  2. Starting workflow functions
248
245
  3. Retrieving workflow status information
249
246
  4. Interacting with workflows via events and messages
250
- 5. Accessing context, including the current user, request, SQL session, logger, and tracer
247
+ 5. Accessing context, including the current user, SQL session, logger, and tracer
251
248
 
252
249
  """
253
250
 
@@ -330,7 +327,10 @@ class DBOS:
330
327
  self._registry: DBOSRegistry = _get_or_create_dbos_registry()
331
328
  self._registry.dbos = self
332
329
  self._admin_server_field: Optional[AdminServer] = None
333
- self.stop_events: List[threading.Event] = []
330
+ # Stop internal background threads (queue thread, timeout threads, etc.)
331
+ self.background_thread_stop_events: List[threading.Event] = []
332
+ # Stop pollers (event receivers) that can create new workflows (scheduler, Kafka)
333
+ self.poller_stop_events: List[threading.Event] = []
334
334
  self.fastapi: Optional["FastAPI"] = fastapi
335
335
  self.flask: Optional["Flask"] = flask
336
336
  self._executor_field: Optional[ThreadPoolExecutor] = None
@@ -502,7 +502,7 @@ class DBOS:
502
502
 
503
503
  # Start the queue thread
504
504
  evt = threading.Event()
505
- self.stop_events.append(evt)
505
+ self.background_thread_stop_events.append(evt)
506
506
  bg_queue_thread = threading.Thread(
507
507
  target=queue_thread, args=(evt, self), daemon=True
508
508
  )
@@ -515,7 +515,7 @@ class DBOS:
515
515
  dbos_domain = os.environ.get("DBOS_DOMAIN", "cloud.dbos.dev")
516
516
  self.conductor_url = f"wss://{dbos_domain}/conductor/v1alpha1"
517
517
  evt = threading.Event()
518
- self.stop_events.append(evt)
518
+ self.background_thread_stop_events.append(evt)
519
519
  self.conductor_websocket = ConductorWebsocket(
520
520
  self,
521
521
  conductor_url=self.conductor_url,
@@ -527,7 +527,7 @@ class DBOS:
527
527
 
528
528
  # Grab any pollers that were deferred and start them
529
529
  for evt, func, args, kwargs in self._registry.pollers:
530
- self.stop_events.append(evt)
530
+ self.poller_stop_events.append(evt)
531
531
  poller_thread = threading.Thread(
532
532
  target=func, args=args, kwargs=kwargs, daemon=True
533
533
  )
@@ -583,7 +583,9 @@ class DBOS:
583
583
 
584
584
  def _destroy(self) -> None:
585
585
  self._initialized = False
586
- for event in self.stop_events:
586
+ for event in self.poller_stop_events:
587
+ event.set()
588
+ for event in self.background_thread_stop_events:
587
589
  event.set()
588
590
  self._background_event_loop.stop()
589
591
  if self._sys_db_field is not None:
@@ -760,7 +762,7 @@ class DBOS:
760
762
  """Return the status of a workflow execution."""
761
763
 
762
764
  def fn() -> Optional[WorkflowStatus]:
763
- return get_workflow(_get_dbos_instance()._sys_db, workflow_id, True)
765
+ return get_workflow(_get_dbos_instance()._sys_db, workflow_id)
764
766
 
765
767
  return _get_dbos_instance()._sys_db.call_function_as_step(fn, "DBOS.getStatus")
766
768
 
@@ -1156,12 +1158,6 @@ class DBOS:
1156
1158
  assert span
1157
1159
  return span
1158
1160
 
1159
- @classproperty
1160
- def request(cls) -> Optional["Request"]:
1161
- """Return the HTTP `Request`, if any, associated with the current context."""
1162
- ctx = assert_current_dbos_context()
1163
- return ctx.request
1164
-
1165
1161
  @classproperty
1166
1162
  def authenticated_user(cls) -> Optional[str]:
1167
1163
  """Return the current authenticated user, if any, associated with the current context."""
@@ -7,15 +7,9 @@ from fastapi.responses import JSONResponse
7
7
  from starlette.types import ASGIApp, Receive, Scope, Send
8
8
 
9
9
  from . import DBOS
10
- from ._context import (
11
- EnterDBOSHandler,
12
- OperationType,
13
- SetWorkflowID,
14
- TracedAttributes,
15
- assert_current_dbos_context,
16
- )
10
+ from ._context import EnterDBOSHandler, OperationType, SetWorkflowID, TracedAttributes
17
11
  from ._error import DBOSException
18
- from ._request import Address, Request, request_id_header
12
+ from ._utils import request_id_header
19
13
 
20
14
 
21
15
  def _get_or_generate_request_id(request: FastAPIRequest) -> str:
@@ -26,19 +20,6 @@ def _get_or_generate_request_id(request: FastAPIRequest) -> str:
26
20
  return str(uuid.uuid4())
27
21
 
28
22
 
29
- def _make_request(request: FastAPIRequest) -> Request:
30
- return Request(
31
- headers=request.headers,
32
- path_params=request.path_params,
33
- query_params=request.query_params,
34
- url=str(request.url),
35
- base_url=str(request.base_url),
36
- client=Address(*request.client) if request.client is not None else None,
37
- cookies=request.cookies,
38
- method=request.method,
39
- )
40
-
41
-
42
23
  async def _dbos_error_handler(request: FastAPIRequest, gexc: Exception) -> JSONResponse:
43
24
  exc: DBOSException = cast(DBOSException, gexc)
44
25
  status_code = 500
@@ -96,8 +77,6 @@ def setup_fastapi_middleware(app: FastAPI, dbos: DBOS) -> None:
96
77
  "operationType": OperationType.HANDLER.value,
97
78
  }
98
79
  with EnterDBOSHandler(attributes):
99
- ctx = assert_current_dbos_context()
100
- ctx.request = _make_request(request)
101
80
  workflow_id = request.headers.get("dbos-idempotency-key")
102
81
  if workflow_id is not None:
103
82
  # Set the workflow ID for the handler
@@ -2,17 +2,11 @@ import uuid
2
2
  from typing import Any
3
3
  from urllib.parse import urlparse
4
4
 
5
- from flask import Flask, request
5
+ from flask import Flask
6
6
  from werkzeug.wrappers import Request as WRequest
7
7
 
8
- from ._context import (
9
- EnterDBOSHandler,
10
- OperationType,
11
- SetWorkflowID,
12
- TracedAttributes,
13
- assert_current_dbos_context,
14
- )
15
- from ._request import Address, Request, request_id_header
8
+ from ._context import EnterDBOSHandler, OperationType, SetWorkflowID, TracedAttributes
9
+ from ._utils import request_id_header
16
10
 
17
11
 
18
12
  class FlaskMiddleware:
@@ -32,8 +26,6 @@ class FlaskMiddleware:
32
26
  "operationType": OperationType.HANDLER.value,
33
27
  }
34
28
  with EnterDBOSHandler(attributes):
35
- ctx = assert_current_dbos_context()
36
- ctx.request = _make_request(request)
37
29
  workflow_id = request.headers.get("dbos-idempotency-key")
38
30
  if workflow_id is not None:
39
31
  # Set the workflow ID for the handler
@@ -52,31 +44,5 @@ def _get_or_generate_request_id(request: WRequest) -> str:
52
44
  return str(uuid.uuid4())
53
45
 
54
46
 
55
- def _make_request(request: WRequest) -> Request:
56
- parsed_url = urlparse(request.url)
57
- base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
58
-
59
- client = None
60
- if request.remote_addr:
61
- hostname = request.remote_addr
62
- port = request.environ.get("REMOTE_PORT")
63
- if port:
64
- client = Address(hostname=hostname, port=int(port))
65
- else:
66
- # If port is not available, use 0 as a placeholder
67
- client = Address(hostname=hostname, port=0)
68
-
69
- return Request(
70
- headers=dict(request.headers),
71
- path_params={},
72
- query_params=dict(request.args),
73
- url=request.url,
74
- base_url=base_url,
75
- client=client,
76
- cookies=dict(request.cookies),
77
- method=request.method,
78
- )
79
-
80
-
81
47
  def setup_flask_middleware(app: Flask) -> None:
82
48
  app.wsgi_app = FlaskMiddleware(app.wsgi_app) # type: ignore
@@ -29,7 +29,7 @@ def startup_recovery_thread(
29
29
  ) -> None:
30
30
  """Attempt to recover local pending workflows on startup using a background thread."""
31
31
  stop_event = threading.Event()
32
- dbos.stop_events.append(stop_event)
32
+ dbos.background_thread_stop_events.append(stop_event)
33
33
  while not stop_event.is_set() and len(pending_workflows) > 0:
34
34
  try:
35
35
  for pending_workflow in list(pending_workflows):
@@ -29,7 +29,6 @@ class SystemSchema:
29
29
  Column("authenticated_user", Text, nullable=True),
30
30
  Column("assumed_role", Text, nullable=True),
31
31
  Column("authenticated_roles", Text, nullable=True),
32
- Column("request", Text, nullable=True),
33
32
  Column("output", Text, nullable=True),
34
33
  Column("error", Text, nullable=True),
35
34
  Column("executor_id", Text, nullable=True),