dbos 0.25.1__py3-none-any.whl → 0.26.0__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.
dbos/_dbos_config.py CHANGED
@@ -15,7 +15,6 @@ from jsonschema import ValidationError, validate
15
15
  from rich import print
16
16
  from sqlalchemy import URL, make_url
17
17
 
18
- from ._db_wizard import db_wizard, load_db_connection
19
18
  from ._error import DBOSInitializationError
20
19
  from ._logger import dbos_logger
21
20
 
@@ -59,20 +58,28 @@ class RuntimeConfig(TypedDict, total=False):
59
58
 
60
59
 
61
60
  class DatabaseConfig(TypedDict, total=False):
62
- hostname: str
63
- port: int
64
- username: str
65
- password: str
66
- connectionTimeoutMillis: Optional[int]
67
- app_db_name: str
61
+ """
62
+ Internal data structure containing the DBOS database configuration.
63
+ Attributes:
64
+ app_db_pool_size (int): Application database pool size
65
+ sys_db_name (str): System database name
66
+ sys_db_pool_size (int): System database pool size
67
+ migrate (List[str]): Migration commands to run on startup
68
+ """
69
+
70
+ hostname: str # Will be removed in a future version
71
+ port: int # Will be removed in a future version
72
+ username: str # Will be removed in a future version
73
+ password: str # Will be removed in a future version
74
+ connectionTimeoutMillis: Optional[int] # Will be removed in a future version
75
+ app_db_name: str # Will be removed in a future version
68
76
  app_db_pool_size: Optional[int]
69
77
  sys_db_name: Optional[str]
70
78
  sys_db_pool_size: Optional[int]
71
- ssl: Optional[bool]
72
- ssl_ca: Optional[str]
73
- local_suffix: Optional[bool]
79
+ ssl: Optional[bool] # Will be removed in a future version
80
+ ssl_ca: Optional[str] # Will be removed in a future version
74
81
  migrate: Optional[List[str]]
75
- rollback: Optional[List[str]]
82
+ rollback: Optional[List[str]] # Will be removed in a future version
76
83
 
77
84
 
78
85
  def parse_database_url_to_dbconfig(database_url: str) -> DatabaseConfig:
@@ -119,7 +126,7 @@ class ConfigFile(TypedDict, total=False):
119
126
  Attributes:
120
127
  name (str): Application name
121
128
  runtimeConfig (RuntimeConfig): Configuration for request serving
122
- database (DatabaseConfig): Configuration for the application and system databases
129
+ database (DatabaseConfig): Configure pool sizes, migrate commands
123
130
  database_url (str): Database connection string
124
131
  telemetry (TelemetryConfig): Configuration for tracing / logging
125
132
  env (Dict[str,str]): Environment varialbes
@@ -215,9 +222,13 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
215
222
 
216
223
 
217
224
  def _substitute_env_vars(content: str, silent: bool = False) -> str:
218
- regex = r"\$\{([^}]+)\}" # Regex to match ${VAR_NAME} style placeholders
219
225
 
220
- def replace_func(match: re.Match[str]) -> str:
226
+ # Regex to match ${DOCKER_SECRET:SECRET_NAME} style placeholders for Docker secrets
227
+ secret_regex = r"\$\{DOCKER_SECRET:([^}]+)\}"
228
+ # Regex to match ${VAR_NAME} style placeholders for environment variables
229
+ env_regex = r"\$\{(?!DOCKER_SECRET:)([^}]+)\}"
230
+
231
+ def replace_env_func(match: re.Match[str]) -> str:
221
232
  var_name = match.group(1)
222
233
  value = os.environ.get(
223
234
  var_name, ""
@@ -228,7 +239,30 @@ def _substitute_env_vars(content: str, silent: bool = False) -> str:
228
239
  )
229
240
  return value
230
241
 
231
- return re.sub(regex, replace_func, content)
242
+ def replace_secret_func(match: re.Match[str]) -> str:
243
+ secret_name = match.group(1)
244
+ try:
245
+ # Docker secrets are stored in /run/secrets/
246
+ secret_path = f"/run/secrets/{secret_name}"
247
+ if os.path.exists(secret_path):
248
+ with open(secret_path, "r") as f:
249
+ return f.read().strip()
250
+ elif not silent:
251
+ dbos_logger.warning(
252
+ f"Docker secret {secret_name} would be substituted from /run/secrets/{secret_name}, but the file does not exist"
253
+ )
254
+ return ""
255
+ except Exception as e:
256
+ if not silent:
257
+ dbos_logger.warning(
258
+ f"Error reading Docker secret {secret_name}: {str(e)}"
259
+ )
260
+ return ""
261
+
262
+ # First replace Docker secrets
263
+ content = re.sub(secret_regex, replace_secret_func, content)
264
+ # Then replace environment variables
265
+ return re.sub(env_regex, replace_env_func, content)
232
266
 
233
267
 
234
268
  def get_dbos_database_url(config_file_path: str = DBOS_CONFIG_PATH) -> str:
@@ -261,7 +295,6 @@ def load_config(
261
295
  config_file_path: str = DBOS_CONFIG_PATH,
262
296
  *,
263
297
  run_process_config: bool = True,
264
- use_db_wizard: bool = True,
265
298
  silent: bool = False,
266
299
  ) -> ConfigFile:
267
300
  """
@@ -312,13 +345,12 @@ def load_config(
312
345
 
313
346
  data = cast(ConfigFile, data)
314
347
  if run_process_config:
315
- data = process_config(data=data, use_db_wizard=use_db_wizard, silent=silent)
348
+ data = process_config(data=data, silent=silent)
316
349
  return data # type: ignore
317
350
 
318
351
 
319
352
  def process_config(
320
353
  *,
321
- use_db_wizard: bool = True,
322
354
  data: ConfigFile,
323
355
  silent: bool = False,
324
356
  ) -> ConfigFile:
@@ -345,22 +377,17 @@ def process_config(
345
377
  # database_url takes precedence over database config, but we need to preserve rollback and migrate if they exist
346
378
  migrate = data["database"].get("migrate", False)
347
379
  rollback = data["database"].get("rollback", False)
348
- local_suffix = data["database"].get("local_suffix", False)
349
380
  if data.get("database_url"):
350
381
  dbconfig = parse_database_url_to_dbconfig(cast(str, data["database_url"]))
351
382
  if migrate:
352
383
  dbconfig["migrate"] = cast(List[str], migrate)
353
384
  if rollback:
354
385
  dbconfig["rollback"] = cast(List[str], rollback)
355
- if local_suffix:
356
- dbconfig["local_suffix"] = cast(bool, local_suffix)
357
386
  data["database"] = dbconfig
358
387
 
359
388
  if "app_db_name" not in data["database"] or not (data["database"]["app_db_name"]):
360
389
  data["database"]["app_db_name"] = _app_name_to_db_name(data["name"])
361
390
 
362
- # Load the DB connection file. Use its values for missing connection parameters. Use defaults otherwise.
363
- db_connection = load_db_connection()
364
391
  connection_passed_in = data["database"].get("hostname", None) is not None
365
392
 
366
393
  dbos_dbport: Optional[int] = None
@@ -370,49 +397,22 @@ def process_config(
370
397
  dbos_dbport = int(dbport_env)
371
398
  except ValueError:
372
399
  pass
373
- dbos_dblocalsuffix: Optional[bool] = None
374
- dblocalsuffix_env = os.getenv("DBOS_DBLOCALSUFFIX")
375
- if dblocalsuffix_env:
376
- try:
377
- dbos_dblocalsuffix = dblocalsuffix_env.casefold() == "true".casefold()
378
- except ValueError:
379
- pass
380
400
 
381
401
  data["database"]["hostname"] = (
382
- os.getenv("DBOS_DBHOST")
383
- or data["database"].get("hostname")
384
- or db_connection.get("hostname")
385
- or "localhost"
402
+ os.getenv("DBOS_DBHOST") or data["database"].get("hostname") or "localhost"
386
403
  )
387
404
 
388
- data["database"]["port"] = (
389
- dbos_dbport or data["database"].get("port") or db_connection.get("port") or 5432
390
- )
405
+ data["database"]["port"] = dbos_dbport or data["database"].get("port") or 5432
391
406
  data["database"]["username"] = (
392
- os.getenv("DBOS_DBUSER")
393
- or data["database"].get("username")
394
- or db_connection.get("username")
395
- or "postgres"
407
+ os.getenv("DBOS_DBUSER") or data["database"].get("username") or "postgres"
396
408
  )
397
409
  data["database"]["password"] = (
398
410
  os.getenv("DBOS_DBPASSWORD")
399
411
  or data["database"].get("password")
400
- or db_connection.get("password")
401
412
  or os.environ.get("PGPASSWORD")
402
413
  or "dbos"
403
414
  )
404
415
 
405
- local_suffix = False
406
- dbcon_local_suffix = db_connection.get("local_suffix")
407
- if dbcon_local_suffix is not None:
408
- local_suffix = dbcon_local_suffix
409
- db_local_suffix = data["database"].get("local_suffix")
410
- if db_local_suffix is not None:
411
- local_suffix = db_local_suffix
412
- if dbos_dblocalsuffix is not None:
413
- local_suffix = dbos_dblocalsuffix
414
- data["database"]["local_suffix"] = local_suffix
415
-
416
416
  if not data["database"].get("app_db_pool_size"):
417
417
  data["database"]["app_db_pool_size"] = 20
418
418
  if not data["database"].get("sys_db_pool_size"):
@@ -427,10 +427,6 @@ def process_config(
427
427
  elif "run_admin_server" not in data["runtimeConfig"]:
428
428
  data["runtimeConfig"]["run_admin_server"] = True
429
429
 
430
- # Check the connectivity to the database and make sure it's properly configured
431
- # Note, never use db wizard if the DBOS is running in debug mode (i.e. DBOS_DEBUG_WORKFLOW_ID env var is set)
432
- debugWorkflowId = os.getenv("DBOS_DEBUG_WORKFLOW_ID")
433
-
434
430
  # Pretty-print where we've loaded database connection information from, respecting the log level
435
431
  if not silent and logs["logLevel"] == "INFO" or logs["logLevel"] == "DEBUG":
436
432
  d = data["database"]
@@ -443,21 +439,11 @@ def process_config(
443
439
  print(
444
440
  f"[bold blue]Using database connection string: {conn_string}[/bold blue]"
445
441
  )
446
- elif db_connection.get("hostname"):
447
- print(
448
- f"[bold blue]Loading database connection string from .dbos/db_connection: {conn_string}[/bold blue]"
449
- )
450
442
  else:
451
443
  print(
452
444
  f"[bold blue]Using default database connection string: {conn_string}[/bold blue]"
453
445
  )
454
446
 
455
- if use_db_wizard and debugWorkflowId is None:
456
- data = db_wizard(data)
457
-
458
- if "local_suffix" in data["database"] and data["database"]["local_suffix"]:
459
- data["database"]["app_db_name"] = f"{data['database']['app_db_name']}_local"
460
-
461
447
  # Return data as ConfigFile type
462
448
  return data
463
449
 
dbos/_debug.py CHANGED
@@ -28,7 +28,7 @@ def debug_workflow(workflow_id: str, entrypoint: Union[str, PythonModule]) -> No
28
28
 
29
29
  DBOS.logger.info(f"Debugging workflow {workflow_id}...")
30
30
  DBOS.launch(debug_mode=True)
31
- handle = DBOS.execute_workflow_id(workflow_id)
31
+ handle = DBOS._execute_workflow_id(workflow_id)
32
32
  handle.get_result()
33
33
  DBOS.logger.info("Workflow Debugging complete. Exiting process.")
34
34
 
@@ -0,0 +1,191 @@
1
+ import logging
2
+ import os
3
+ import subprocess
4
+ import time
5
+
6
+ import docker
7
+ import psycopg
8
+ from docker.errors import APIError, NotFound
9
+
10
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+
14
+ def start_docker_pg() -> None:
15
+ """
16
+ Starts a PostgreSQL database in a Docker container.
17
+
18
+ This function checks if Docker is installed, and if so, starts a local PostgreSQL
19
+ database in a Docker container. It configures the database with default settings
20
+ and provides connection information upon successful startup.
21
+
22
+ The function uses environment variable PGPASSWORD if available, otherwise
23
+ defaults to 'dbos' as the database password.
24
+
25
+ Returns:
26
+ None
27
+
28
+ Raises:
29
+ Exception: If there is an error starting the Docker container or if the
30
+ PostgreSQL service does not become available within the timeout period.
31
+ """
32
+
33
+ logging.info("Attempting to create a Docker Postgres container...")
34
+ has_docker = check_docker_installed()
35
+
36
+ pool_config = {
37
+ "host": "localhost",
38
+ "port": 5432,
39
+ "password": os.environ.get("PGPASSWORD", "dbos"),
40
+ "user": "postgres",
41
+ "database": "postgres",
42
+ "connect_timeout": 2,
43
+ }
44
+
45
+ # If Docker is installed, start a local Docker based Postgres
46
+ if has_docker:
47
+ start_docker_postgres(pool_config)
48
+ logging.info(
49
+ f"Postgres available at postgresql://postgres:{pool_config['password']}@{pool_config['host']}:{pool_config['port']}"
50
+ )
51
+ else:
52
+ logging.warning("Docker not detected locally")
53
+
54
+
55
+ def check_db_connectivity(config: Dict[str, Any]) -> Optional[Exception]:
56
+ conn = None
57
+ try:
58
+ conn = psycopg.connect(
59
+ host=config["host"],
60
+ port=config["port"],
61
+ user=config["user"],
62
+ password=config["password"],
63
+ dbname=config["database"],
64
+ connect_timeout=config.get("connect_timeout", 30),
65
+ )
66
+ cursor = conn.cursor()
67
+ cursor.execute("SELECT 1;")
68
+ cursor.close()
69
+ return None
70
+ except Exception as error:
71
+ return error
72
+ finally:
73
+ if conn is not None:
74
+ conn.close()
75
+
76
+
77
+ def exec_sync(cmd: str) -> Tuple[str, str]:
78
+ result = subprocess.run(cmd, shell=True, text=True, capture_output=True, check=True)
79
+ return result.stdout, result.stderr
80
+
81
+
82
+ def start_docker_postgres(pool_config: Dict[str, Any]) -> bool:
83
+ logging.info("Starting a Postgres Docker container...")
84
+ container_name = "dbos-db"
85
+ pg_data = "/var/lib/postgresql/data"
86
+
87
+ try:
88
+ client = docker.from_env()
89
+
90
+ # Check if the container already exists
91
+ try:
92
+ container = client.containers.get(container_name)
93
+ if container.status == "running":
94
+ logging.info(f"Container '{container_name}' is already running.")
95
+ return True
96
+ elif container.status == "exited":
97
+ container.start()
98
+ logging.info(
99
+ f"Container '{container_name}' was stopped and has been restarted."
100
+ )
101
+ return True
102
+ except NotFound:
103
+ # Container doesn't exist, proceed with creation
104
+ pass
105
+
106
+ # Create and start the container
107
+ container = client.containers.run(
108
+ image="pgvector/pgvector:pg16",
109
+ name=container_name,
110
+ detach=True,
111
+ environment={
112
+ "POSTGRES_PASSWORD": pool_config["password"],
113
+ "PGDATA": pg_data,
114
+ },
115
+ ports={"5432/tcp": pool_config["port"]},
116
+ volumes={pg_data: {"bind": pg_data, "mode": "rw"}},
117
+ remove=True, # Equivalent to --rm
118
+ )
119
+
120
+ logging.info(f"Created container: {container.id}")
121
+
122
+ except APIError as e:
123
+ raise Exception(f"Docker API error: {str(e)}")
124
+
125
+ # Wait for PostgreSQL to be ready
126
+ attempts = 30
127
+ while attempts > 0:
128
+ if attempts % 5 == 0:
129
+ logging.info("Waiting for Postgres Docker container to start...")
130
+
131
+ if check_db_connectivity(pool_config) is None:
132
+ return True
133
+
134
+ attempts -= 1
135
+ time.sleep(1)
136
+
137
+ raise Exception(
138
+ f"Failed to start Docker container: Container {container_name} did not start in time."
139
+ )
140
+
141
+
142
+ def check_docker_installed() -> bool:
143
+ """
144
+ Check if Docker is installed and running using the docker library.
145
+
146
+ Returns:
147
+ bool: True if Docker is installed and running, False otherwise.
148
+ """
149
+ try:
150
+ client = docker.from_env()
151
+ client.ping() # type: ignore
152
+ return True
153
+ except Exception:
154
+ return False
155
+
156
+
157
+ def stop_docker_pg() -> None:
158
+ """
159
+ Stops the Docker Postgres container.
160
+
161
+ Returns:
162
+ bool: True if the container was successfully stopped, False if it wasn't running
163
+
164
+ Raises:
165
+ Exception: If there was an error stopping the container
166
+ """
167
+ logger = logging.getLogger()
168
+ container_name = "dbos-db"
169
+ try:
170
+ logger.info(f"Stopping Docker Postgres container {container_name}...")
171
+
172
+ client = docker.from_env()
173
+
174
+ try:
175
+ container = client.containers.get(container_name)
176
+
177
+ if container.status == "running":
178
+ container.stop()
179
+ logger.info(
180
+ f"Successfully stopped Docker Postgres container {container_name}."
181
+ )
182
+ else:
183
+ logger.info(f"Container {container_name} exists but is not running.")
184
+
185
+ except docker.errors.NotFound:
186
+ logger.info(f"Container {container_name} does not exist.")
187
+
188
+ except Exception as error:
189
+ error_message = str(error)
190
+ logger.error(f"Failed to stop Docker Postgres container: {error_message}")
191
+ raise
dbos/_error.py CHANGED
@@ -26,6 +26,29 @@ class DBOSException(Exception):
26
26
  return f"DBOS Error: {self.message}"
27
27
 
28
28
 
29
+ class DBOSBaseException(BaseException):
30
+ """
31
+ This class is for DBOS exceptions that should not be caught by user code.
32
+ It inherits from BaseException instead of Exception so it cannot be caught
33
+ except by code specifically trying to catch it.
34
+
35
+ Attributes:
36
+ message(str): The error message string
37
+ dbos_error_code(DBOSErrorCode): The error code, from the `DBOSErrorCode` enum
38
+ """
39
+
40
+ def __init__(self, message: str, dbos_error_code: Optional[int] = None):
41
+ self.message = message
42
+ self.dbos_error_code = dbos_error_code
43
+ self.status_code: Optional[int] = None
44
+ super().__init__(self.message)
45
+
46
+ def __str__(self) -> str:
47
+ if self.dbos_error_code:
48
+ return f"DBOS Error {self.dbos_error_code}: {self.message}"
49
+ return f"DBOS Error: {self.message}"
50
+
51
+
29
52
  class DBOSErrorCode(Enum):
30
53
  ConflictingIDError = 1
31
54
  RecoveryError = 2
@@ -37,17 +60,13 @@ class DBOSErrorCode(Enum):
37
60
  NotAuthorized = 8
38
61
  ConflictingWorkflowError = 9
39
62
  WorkflowCancelled = 10
63
+ UnexpectedStep = 11
40
64
  ConflictingRegistrationError = 25
41
65
 
42
66
 
43
- class DBOSWorkflowConflictIDError(DBOSException):
44
- """Exception raised when a workflow database record already exists."""
45
-
46
- def __init__(self, workflow_id: str):
47
- super().__init__(
48
- f"Conflicting workflow ID {workflow_id}",
49
- dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
50
- )
67
+ #######################################
68
+ ## Exception
69
+ #######################################
51
70
 
52
71
 
53
72
  class DBOSConflictingWorkflowError(DBOSException):
@@ -137,8 +156,35 @@ class DBOSMaxStepRetriesExceeded(DBOSException):
137
156
  return (self.__class__, (self.step_name, self.max_retries))
138
157
 
139
158
 
140
- class DBOSWorkflowCancelledError(DBOSException):
141
- """Exception raised when the workflow has already been cancelled."""
159
+ class DBOSConflictingRegistrationError(DBOSException):
160
+ """Exception raised when conflicting decorators are applied to the same function."""
161
+
162
+ def __init__(self, name: str) -> None:
163
+ super().__init__(
164
+ f"Operation (Name: {name}) is already registered with a conflicting function type",
165
+ dbos_error_code=DBOSErrorCode.ConflictingRegistrationError.value,
166
+ )
167
+
168
+
169
+ class DBOSUnexpectedStepError(DBOSException):
170
+ """Exception raised when a step has an unexpected recorded name."""
171
+
172
+ def __init__(
173
+ self, workflow_id: str, step_id: int, expected_name: str, recorded_name: str
174
+ ) -> None:
175
+ super().__init__(
176
+ f"During execution of workflow {workflow_id} step {step_id}, function {recorded_name} was recorded when {expected_name} was expected. Check that your workflow is deterministic.",
177
+ dbos_error_code=DBOSErrorCode.UnexpectedStep.value,
178
+ )
179
+
180
+
181
+ #######################################
182
+ ## BaseException
183
+ #######################################
184
+
185
+
186
+ class DBOSWorkflowCancelledError(DBOSBaseException):
187
+ """BaseException raised when the workflow has already been cancelled."""
142
188
 
143
189
  def __init__(self, msg: str) -> None:
144
190
  super().__init__(
@@ -147,11 +193,11 @@ class DBOSWorkflowCancelledError(DBOSException):
147
193
  )
148
194
 
149
195
 
150
- class DBOSConflictingRegistrationError(DBOSException):
151
- """Exception raised when conflicting decorators are applied to the same function."""
196
+ class DBOSWorkflowConflictIDError(DBOSBaseException):
197
+ """BaseException raised when a workflow database record already exists."""
152
198
 
153
- def __init__(self, name: str) -> None:
199
+ def __init__(self, workflow_id: str):
154
200
  super().__init__(
155
- f"Operation (Name: {name}) is already registered with a conflicting function type",
156
- dbos_error_code=DBOSErrorCode.ConflictingRegistrationError.value,
201
+ f"Conflicting workflow ID {workflow_id}",
202
+ dbos_error_code=DBOSErrorCode.ConflictingIDError.value,
157
203
  )
dbos/_event_loop.py ADDED
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ import threading
3
+ from typing import Any, Coroutine, Optional, TypeVar
4
+
5
+
6
+ class BackgroundEventLoop:
7
+ """
8
+ This is the event loop to which DBOS submits any coroutines that are not started from within an event loop.
9
+ In particular, coroutines submitted to queues (such as from scheduled workflows) run on this event loop.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
14
+ self._thread: Optional[threading.Thread] = None
15
+ self._running = False
16
+ self._ready = threading.Event()
17
+
18
+ def start(self) -> None:
19
+ if self._running:
20
+ return
21
+
22
+ self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
23
+ self._thread.start()
24
+ self._ready.wait() # Wait until the loop is running
25
+
26
+ def stop(self) -> None:
27
+ if not self._running or self._loop is None or self._thread is None:
28
+ return
29
+
30
+ asyncio.run_coroutine_threadsafe(self._shutdown(), self._loop)
31
+ self._thread.join()
32
+ self._running = False
33
+
34
+ def _run_event_loop(self) -> None:
35
+ self._loop = asyncio.new_event_loop()
36
+ asyncio.set_event_loop(self._loop)
37
+
38
+ self._running = True
39
+ self._ready.set() # Signal that the loop is ready
40
+
41
+ try:
42
+ self._loop.run_forever()
43
+ finally:
44
+ self._loop.close()
45
+
46
+ async def _shutdown(self) -> None:
47
+ if self._loop is None:
48
+ raise RuntimeError("Event loop not started")
49
+ tasks = [
50
+ task
51
+ for task in asyncio.all_tasks(self._loop)
52
+ if task is not asyncio.current_task(self._loop)
53
+ ]
54
+
55
+ for task in tasks:
56
+ task.cancel()
57
+
58
+ await asyncio.gather(*tasks, return_exceptions=True)
59
+ self._loop.stop()
60
+
61
+ T = TypeVar("T")
62
+
63
+ def submit_coroutine(self, coro: Coroutine[Any, Any, T]) -> T:
64
+ """Submit a coroutine to the background event loop"""
65
+ if self._loop is None:
66
+ raise RuntimeError("Event loop not started")
67
+ return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
dbos/_kafka.py CHANGED
@@ -115,7 +115,7 @@ def kafka_consumer(
115
115
  _in_order_kafka_queues[topic] = queue
116
116
  else:
117
117
  global _kafka_queue
118
- _kafka_queue = Queue("_dbos_internal_queue")
118
+ _kafka_queue = dbosreg.get_internal_queue()
119
119
  stop_event = threading.Event()
120
120
  dbosreg.register_poller(
121
121
  stop_event, _kafka_consumer_loop, func, config, topics, stop_event, in_order
@@ -0,0 +1,44 @@
1
+ """workflow_timeout
2
+
3
+ Revision ID: 83f3732ae8e7
4
+ Revises: f4b9b32ba814
5
+ Create Date: 2025-04-16 17:05:36.642395
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "83f3732ae8e7"
16
+ down_revision: Union[str, None] = "f4b9b32ba814"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.add_column(
23
+ "workflow_status",
24
+ sa.Column(
25
+ "workflow_timeout_ms",
26
+ sa.BigInteger(),
27
+ nullable=True,
28
+ ),
29
+ schema="dbos",
30
+ )
31
+ op.add_column(
32
+ "workflow_status",
33
+ sa.Column(
34
+ "workflow_deadline_epoch_ms",
35
+ sa.BigInteger(),
36
+ nullable=True,
37
+ ),
38
+ schema="dbos",
39
+ )
40
+
41
+
42
+ def downgrade() -> None:
43
+ op.drop_column("workflow_status", "workflow_deadline_epoch_ms", schema="dbos")
44
+ op.drop_column("workflow_status", "workflow_timeout_ms", schema="dbos")
dbos/_queue.py CHANGED
@@ -82,7 +82,8 @@ def queue_thread(stop_event: threading.Event, dbos: "DBOS") -> None:
82
82
  while not stop_event.is_set():
83
83
  if stop_event.wait(timeout=1):
84
84
  return
85
- for _, queue in dbos._registry.queue_info_map.items():
85
+ queues = dict(dbos._registry.queue_info_map)
86
+ for _, queue in queues.items():
86
87
  try:
87
88
  wf_ids = dbos._sys_db.start_queued_workflows(
88
89
  queue, GlobalParams.executor_id, GlobalParams.app_version
dbos/_recovery.py CHANGED
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  def _recover_workflow(
18
18
  dbos: "DBOS", workflow: GetPendingWorkflowsOutput
19
19
  ) -> "WorkflowHandle[Any]":
20
- if workflow.queue_name and workflow.queue_name != "_dbos_internal_queue":
20
+ if workflow.queue_name:
21
21
  cleared = dbos._sys_db.clear_queue_assignment(workflow.workflow_uuid)
22
22
  if cleared:
23
23
  return dbos.retrieve_workflow(workflow.workflow_uuid)