dbos 0.24.0a5__tar.gz → 0.24.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.

Potentially problematic release.


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

Files changed (98) hide show
  1. {dbos-0.24.0a5 → dbos-0.24.0a7}/PKG-INFO +1 -1
  2. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_app_db.py +16 -2
  3. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_core.py +3 -1
  4. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_dbos.py +8 -1
  5. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_dbos_config.py +52 -10
  6. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_error.py +9 -3
  7. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_sys_db.py +2 -2
  8. {dbos-0.24.0a5 → dbos-0.24.0a7}/pyproject.toml +1 -1
  9. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_admin_server.py +73 -1
  10. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_config.py +184 -8
  11. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_failures.py +69 -3
  12. {dbos-0.24.0a5 → dbos-0.24.0a7}/LICENSE +0 -0
  13. {dbos-0.24.0a5 → dbos-0.24.0a7}/README.md +0 -0
  14. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/__init__.py +0 -0
  15. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/__main__.py +0 -0
  16. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_admin_server.py +0 -0
  17. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_classproperty.py +0 -0
  18. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_cloudutils/authentication.py +0 -0
  19. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_cloudutils/cloudutils.py +0 -0
  20. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_cloudutils/databases.py +0 -0
  21. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_conductor/conductor.py +0 -0
  22. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_conductor/protocol.py +0 -0
  23. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_context.py +0 -0
  24. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_croniter.py +0 -0
  25. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_db_wizard.py +0 -0
  26. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_debug.py +0 -0
  27. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_fastapi.py +0 -0
  28. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_flask.py +0 -0
  29. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_kafka.py +0 -0
  30. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_kafka_message.py +0 -0
  31. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_logger.py +0 -0
  32. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/env.py +0 -0
  33. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/script.py.mako +0 -0
  34. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  35. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  36. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  37. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  38. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  39. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  40. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  41. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_outcome.py +0 -0
  42. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_queue.py +0 -0
  43. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_recovery.py +0 -0
  44. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_registrations.py +0 -0
  45. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_request.py +0 -0
  46. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_roles.py +0 -0
  47. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_scheduler.py +0 -0
  48. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_schemas/__init__.py +0 -0
  49. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_schemas/application_database.py +0 -0
  50. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_schemas/system_database.py +0 -0
  51. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_serialization.py +0 -0
  52. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/README.md +0 -0
  53. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  54. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  55. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  56. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  57. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  58. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  59. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  60. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  61. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  62. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_tracer.py +0 -0
  63. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_utils.py +0 -0
  64. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/_workflow_commands.py +0 -0
  65. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/cli/_github_init.py +0 -0
  66. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/cli/_template_init.py +0 -0
  67. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/cli/cli.py +0 -0
  68. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/dbos-config.schema.json +0 -0
  69. {dbos-0.24.0a5 → dbos-0.24.0a7}/dbos/py.typed +0 -0
  70. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/__init__.py +0 -0
  71. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/atexit_no_ctor.py +0 -0
  72. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/atexit_no_launch.py +0 -0
  73. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/classdefs.py +0 -0
  74. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/conftest.py +0 -0
  75. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/more_classdefs.py +0 -0
  76. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/queuedworkflow.py +0 -0
  77. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_async.py +0 -0
  78. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_classdecorators.py +0 -0
  79. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_concurrency.py +0 -0
  80. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_croniter.py +0 -0
  81. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_dbos.py +0 -0
  82. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_dbwizard.py +0 -0
  83. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_debug.py +0 -0
  84. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_fastapi.py +0 -0
  85. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_fastapi_roles.py +0 -0
  86. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_flask.py +0 -0
  87. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_kafka.py +0 -0
  88. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_outcome.py +0 -0
  89. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_package.py +0 -0
  90. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_queue.py +0 -0
  91. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_scheduler.py +0 -0
  92. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_schema_migration.py +0 -0
  93. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_singleton.py +0 -0
  94. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_spans.py +0 -0
  95. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_sqlalchemy.py +0 -0
  96. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_workflow_cancel.py +0 -0
  97. {dbos-0.24.0a5 → dbos-0.24.0a7}/tests/test_workflow_cmds.py +0 -0
  98. {dbos-0.24.0a5 → dbos-0.24.0a7}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.24.0a5
3
+ Version: 0.24.0a7
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
@@ -824,7 +824,9 @@ def decorate_step(
824
824
  stepOutcome = Outcome[R].make(functools.partial(func, *args, **kwargs))
825
825
  if retries_allowed:
826
826
  stepOutcome = stepOutcome.retry(
827
- max_attempts, on_exception, lambda i: DBOSMaxStepRetriesExceeded()
827
+ max_attempts,
828
+ on_exception,
829
+ lambda i: DBOSMaxStepRetriesExceeded(func.__name__, i),
828
830
  )
829
831
 
830
832
  outcome = (
@@ -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:
@@ -1,7 +1,7 @@
1
1
  """Errors thrown by DBOS."""
2
2
 
3
3
  from enum import Enum
4
- from typing import Optional
4
+ from typing import Any, Optional
5
5
 
6
6
 
7
7
  class DBOSException(Exception):
@@ -124,12 +124,18 @@ class DBOSNotAuthorizedError(DBOSException):
124
124
  class DBOSMaxStepRetriesExceeded(DBOSException):
125
125
  """Exception raised when a step was retried the maximimum number of times without success."""
126
126
 
127
- def __init__(self) -> None:
127
+ def __init__(self, step_name: str, max_retries: int) -> None:
128
+ self.step_name = step_name
129
+ self.max_retries = max_retries
128
130
  super().__init__(
129
- "Step reached maximum retries.",
131
+ f"Step {step_name} has exceeded its maximum of {max_retries} retries",
130
132
  dbos_error_code=DBOSErrorCode.MaxStepRetriesExceeded.value,
131
133
  )
132
134
 
135
+ def __reduce__(self) -> Any:
136
+ # Tell jsonpickle how to reconstruct this object
137
+ return (self.__class__, (self.step_name, self.max_retries))
138
+
133
139
 
134
140
  class DBOSWorkflowCancelledError(DBOSException):
135
141
  """Exception raised when the workflow has already been cancelled."""
@@ -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.0a5"
31
+ version = "0.24.0a7"
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()
@@ -5,6 +5,7 @@ from unittest.mock import mock_open
5
5
 
6
6
  import pytest
7
7
  import pytest_mock
8
+ from sqlalchemy import event
8
9
 
9
10
  # Public API
10
11
  from dbos import DBOS, load_config
@@ -313,7 +314,9 @@ def test_process_config_full():
313
314
  "password": "password",
314
315
  "connectionTimeoutMillis": 3000,
315
316
  "app_db_name": "example_db",
317
+ "app_db_pool_size": 45,
316
318
  "sys_db_name": "sys_db",
319
+ "sys_db_pool_size": 27,
317
320
  "ssl": True,
318
321
  "ssl_ca": "ca.pem",
319
322
  "local_suffix": False,
@@ -323,6 +326,7 @@ def test_process_config_full():
323
326
  "runtimeConfig": {
324
327
  "start": ["python3 main.py"],
325
328
  "admin_port": 8001,
329
+ "run_admin_server": False,
326
330
  "setup": ["echo 'hello'"],
327
331
  },
328
332
  "telemetry": {
@@ -353,8 +357,11 @@ def test_process_config_full():
353
357
  assert configFile["database"]["local_suffix"] == False
354
358
  assert configFile["database"]["migrate"] == ["alembic upgrade head"]
355
359
  assert configFile["database"]["rollback"] == ["alembic downgrade base"]
360
+ assert configFile["database"]["app_db_pool_size"] == 45
361
+ assert configFile["database"]["sys_db_pool_size"] == 27
356
362
  assert configFile["runtimeConfig"]["start"] == ["python3 main.py"]
357
363
  assert configFile["runtimeConfig"]["admin_port"] == 8001
364
+ assert configFile["runtimeConfig"]["run_admin_server"] == False
358
365
  assert configFile["runtimeConfig"]["setup"] == ["echo 'hello'"]
359
366
  assert configFile["telemetry"]["logs"]["logLevel"] == "DEBUG"
360
367
  assert configFile["telemetry"]["OTLPExporter"]["logsEndpoint"] == "thelogsendpoint"
@@ -380,8 +387,11 @@ def test_process_config_with_db_url():
380
387
  assert processed_config["database"]["ssl"] == True
381
388
  assert processed_config["database"]["ssl_ca"] == "ca.pem"
382
389
  assert processed_config["database"]["local_suffix"] == False
390
+ assert processed_config["database"]["app_db_pool_size"] == 20
391
+ assert processed_config["database"]["sys_db_pool_size"] == 20
383
392
  assert "rollback" not in processed_config["database"]
384
393
  assert "migrate" not in processed_config["database"]
394
+ assert processed_config["runtimeConfig"]["run_admin_server"] == True
385
395
 
386
396
 
387
397
  def test_process_config_with_db_url_taking_precedence_over_database():
@@ -413,9 +423,12 @@ def test_process_config_with_db_url_taking_precedence_over_database():
413
423
  assert processed_config["database"]["migrate"] == ["alembic upgrade head"]
414
424
  assert processed_config["database"]["rollback"] == ["alembic downgrade base"]
415
425
  assert processed_config["database"]["local_suffix"] == True
416
- assert "connectionTimeoutMillis" not in processed_config["database"]
426
+ assert processed_config["database"]["app_db_pool_size"] == 20
427
+ assert processed_config["database"]["sys_db_pool_size"] == 20
428
+ assert processed_config["database"]["connectionTimeoutMillis"] == 10000
417
429
  assert "ssl" not in processed_config["database"]
418
430
  assert "ssl_ca" not in processed_config["database"]
431
+ assert processed_config["runtimeConfig"]["run_admin_server"] == True
419
432
 
420
433
 
421
434
  # Note this exercise going through the db wizard
@@ -432,6 +445,11 @@ def test_process_config_load_defaults():
432
445
  assert processed_config["database"]["password"] == os.environ.get(
433
446
  "PGPASSWORD", "dbos"
434
447
  )
448
+ assert processed_config["database"]["connectionTimeoutMillis"] == 10000
449
+ assert processed_config["database"]["app_db_pool_size"] == 20
450
+ assert processed_config["database"]["sys_db_pool_size"] == 20
451
+ assert processed_config["telemetry"]["logs"]["logLevel"] == "INFO"
452
+ assert processed_config["runtimeConfig"]["run_admin_server"] == True
435
453
 
436
454
 
437
455
  def test_process_config_load_default_with_None_database_url():
@@ -448,6 +466,9 @@ def test_process_config_load_default_with_None_database_url():
448
466
  assert processed_config["database"]["password"] == os.environ.get(
449
467
  "PGPASSWORD", "dbos"
450
468
  )
469
+ assert processed_config["database"]["connectionTimeoutMillis"] == 10000
470
+ assert processed_config["database"]["app_db_pool_size"] == 20
471
+ assert processed_config["database"]["sys_db_pool_size"] == 20
451
472
 
452
473
 
453
474
  def test_process_config_with_None_app_db_name():
@@ -467,6 +488,9 @@ def test_process_config_with_None_app_db_name():
467
488
  assert processed_config["database"]["password"] == os.environ.get(
468
489
  "PGPASSWORD", "dbos"
469
490
  )
491
+ assert processed_config["database"]["connectionTimeoutMillis"] == 10000
492
+ assert processed_config["database"]["app_db_pool_size"] == 20
493
+ assert processed_config["database"]["sys_db_pool_size"] == 20
470
494
 
471
495
 
472
496
  def test_process_config_with_empty_app_db_name():
@@ -486,6 +510,9 @@ def test_process_config_with_empty_app_db_name():
486
510
  assert processed_config["database"]["password"] == os.environ.get(
487
511
  "PGPASSWORD", "dbos"
488
512
  )
513
+ assert processed_config["database"]["connectionTimeoutMillis"] == 10000
514
+ assert processed_config["database"]["app_db_pool_size"] == 20
515
+ assert processed_config["database"]["sys_db_pool_size"] == 20
489
516
 
490
517
 
491
518
  def test_config_missing_name():
@@ -534,6 +561,9 @@ def test_load_config_load_db_connection(mocker):
534
561
  assert configFile["database"]["password"] == "password"
535
562
  assert configFile["database"]["local_suffix"] == True
536
563
  assert configFile["database"]["app_db_name"] == "some_app_local"
564
+ assert configFile["database"]["app_db_pool_size"] == 20
565
+ assert configFile["database"]["sys_db_pool_size"] == 20
566
+ assert configFile["database"]["connectionTimeoutMillis"] == 10000
537
567
 
538
568
 
539
569
  def test_config_mixed_params():
@@ -543,6 +573,7 @@ def test_config_mixed_params():
543
573
  "port": 1234,
544
574
  "username": "some user",
545
575
  "password": "abc123",
576
+ "app_db_pool_size": 3,
546
577
  },
547
578
  }
548
579
 
@@ -552,6 +583,9 @@ def test_config_mixed_params():
552
583
  assert configFile["database"]["port"] == 1234
553
584
  assert configFile["database"]["username"] == "some user"
554
585
  assert configFile["database"]["password"] == "abc123"
586
+ assert configFile["database"]["app_db_pool_size"] == 3
587
+ assert configFile["database"]["sys_db_pool_size"] == 20
588
+ assert configFile["database"]["connectionTimeoutMillis"] == 10000
555
589
 
556
590
 
557
591
  def test_debug_override(mocker: pytest_mock.MockFixture):
@@ -575,6 +609,9 @@ def test_debug_override(mocker: pytest_mock.MockFixture):
575
609
  assert configFile["database"]["username"] == "fakeuser"
576
610
  assert configFile["database"]["password"] == "fakepassword"
577
611
  assert configFile["database"]["local_suffix"] == False
612
+ assert configFile["database"]["app_db_pool_size"] == 20
613
+ assert configFile["database"]["sys_db_pool_size"] == 20
614
+ assert configFile["database"]["connectionTimeoutMillis"] == 10000
578
615
 
579
616
 
580
617
  def test_local_config():
@@ -600,6 +637,8 @@ def test_local_config():
600
637
  assert processed_config["database"]["password"] == os.environ["PGPASSWORD"]
601
638
  assert processed_config["database"]["app_db_name"] == "some_db_local"
602
639
  assert processed_config["database"]["connectionTimeoutMillis"] == 3000
640
+ assert processed_config["database"]["app_db_pool_size"] == 20
641
+ assert processed_config["database"]["sys_db_pool_size"] == 20
603
642
 
604
643
 
605
644
  def test_local_config_without_name(mocker):
@@ -624,6 +663,8 @@ def test_local_config_without_name(mocker):
624
663
  assert processed_config["database"]["password"] == os.environ["PGPASSWORD"]
625
664
  assert processed_config["database"]["app_db_name"] == "some_app_local"
626
665
  assert processed_config["database"]["connectionTimeoutMillis"] == 3000
666
+ assert processed_config["database"]["app_db_pool_size"] == 20
667
+ assert processed_config["database"]["sys_db_pool_size"] == 20
627
668
 
628
669
 
629
670
  ####################
@@ -676,9 +717,9 @@ def test_query_parameters():
676
717
  """Test processing of various query parameters."""
677
718
 
678
719
  # Test connect_timeout conversion
679
- database_url = "postgresql://user:password@localhost:5432/dbname?connect_timeout=10"
720
+ database_url = "postgresql://user:password@localhost:5432/dbname?connect_timeout=7"
680
721
  db_config = parse_database_url_to_dbconfig(database_url)
681
- assert db_config["connectionTimeoutMillis"] == 10000
722
+ assert db_config["connectionTimeoutMillis"] == 7000
682
723
 
683
724
  # Test sslmode=require (should set ssl=True)
684
725
  database_url = "postgresql://user:password@localhost:5432/dbname?sslmode=require"
@@ -701,9 +742,9 @@ def test_query_parameters():
701
742
  assert db_config["ssl_ca"] == "ca.pem"
702
743
 
703
744
  # Test multiple parameters together
704
- database_url = "postgresql://user:password@localhost:5432/dbname?connect_timeout=10&sslmode=require&sslrootcert=ca.pem&application_name=myapp"
745
+ database_url = "postgresql://user:password@localhost:5432/dbname?connect_timeout=8&sslmode=require&sslrootcert=ca.pem&application_name=myapp"
705
746
  db_config = parse_database_url_to_dbconfig(database_url)
706
- assert db_config["connectionTimeoutMillis"] == 10000
747
+ assert db_config["connectionTimeoutMillis"] == 8000
707
748
  assert db_config["ssl"] == True
708
749
  assert db_config["ssl_ca"] == "ca.pem"
709
750
 
@@ -739,10 +780,13 @@ def test_translate_dbosconfig_full_input():
739
780
  config: DBOSConfig = {
740
781
  "name": "test-app",
741
782
  "database_url": "postgresql://user:password@localhost:5432/dbname?connect_timeout=10&sslmode=require&sslrootcert=ca.pem",
783
+ "app_db_pool_size": 45,
742
784
  "sys_db_name": "sysdb",
785
+ "sys_db_pool_size": 27,
743
786
  "log_level": "DEBUG",
744
787
  "otlp_traces_endpoints": ["http://otel:7777", "notused"],
745
788
  "admin_port": 8001,
789
+ "run_admin_server": False,
746
790
  }
747
791
 
748
792
  translated_config = translate_dbos_config_to_config_file(config)
@@ -757,6 +801,8 @@ def test_translate_dbosconfig_full_input():
757
801
  assert translated_config["database"]["connectionTimeoutMillis"] == 10000
758
802
  assert translated_config["database"]["app_db_name"] == "dbname"
759
803
  assert translated_config["database"]["sys_db_name"] == "sysdb"
804
+ assert translated_config["database"]["app_db_pool_size"] == 45
805
+ assert translated_config["database"]["sys_db_pool_size"] == 27
760
806
  assert "database_url" not in translated_config
761
807
  assert translated_config["telemetry"]["logs"]["logLevel"] == "DEBUG"
762
808
  assert (
@@ -765,6 +811,7 @@ def test_translate_dbosconfig_full_input():
765
811
  )
766
812
  assert "logsEndpoint" not in translated_config["telemetry"]["OTLPExporter"]
767
813
  assert translated_config["runtimeConfig"]["admin_port"] == 8001
814
+ assert translated_config["runtimeConfig"]["run_admin_server"] == False
768
815
  assert "start" not in translated_config["runtimeConfig"]
769
816
  assert "setup" not in translated_config["runtimeConfig"]
770
817
  assert "env" not in translated_config
@@ -778,9 +825,10 @@ def test_translate_dbosconfig_minimal_input():
778
825
 
779
826
  assert translated_config["name"] == "test-app"
780
827
  assert translated_config["telemetry"]["logs"]["logLevel"] == "INFO"
828
+ assert translated_config["runtimeConfig"]["run_admin_server"] == True
829
+ assert "admin_port" not in translated_config["runtimeConfig"]
781
830
  assert "database" not in translated_config
782
831
  assert "env" not in translated_config
783
- assert "runtimeConfig" not in translated_config
784
832
 
785
833
 
786
834
  def test_translate_dbosconfig_just_sys_db_name():
@@ -790,11 +838,66 @@ def test_translate_dbosconfig_just_sys_db_name():
790
838
  }
791
839
  translated_config = translate_dbos_config_to_config_file(config)
792
840
 
841
+ assert translated_config["database"]["sys_db_name"] == "sysdb"
842
+ assert "app_db_pool_size" not in translated_config["database"]
843
+ assert "sys_db_pool_size" not in translated_config["database"]
844
+ assert "env" not in translated_config
845
+ assert "admin_port" not in translated_config["runtimeConfig"]
846
+
847
+
848
+ def test_translate_dbosconfig_just_app_db_pool_size():
849
+ config: DBOSConfig = {
850
+ "name": "test-app",
851
+ "app_db_pool_size": 45,
852
+ }
853
+ translated_config = translate_dbos_config_to_config_file(config)
854
+
855
+ assert translated_config["database"]["app_db_pool_size"] == 45
856
+ assert "sys_db_name" not in translated_config["database"]
857
+ assert "sys_db_pool_size" not in translated_config["database"]
858
+ assert "env" not in translated_config
859
+ assert "admin_port" not in translated_config["runtimeConfig"]
860
+
861
+
862
+ def test_translate_dbosconfig_just_sys_db_pool_size():
863
+ config: DBOSConfig = {
864
+ "name": "test-app",
865
+ "sys_db_pool_size": 27,
866
+ }
867
+ translated_config = translate_dbos_config_to_config_file(config)
868
+
869
+ assert translated_config["database"]["sys_db_pool_size"] == 27
870
+ assert "app_db_pool_size" not in translated_config["database"]
871
+ assert "sys_db_name" not in translated_config["database"]
872
+ assert "env" not in translated_config
873
+
874
+
875
+ def test_translate_dbosconfig_just_admin_port():
876
+ config: DBOSConfig = {
877
+ "name": "test-app",
878
+ "admin_port": 8001,
879
+ }
880
+ translated_config = translate_dbos_config_to_config_file(config)
881
+
882
+ assert translated_config["runtimeConfig"]["admin_port"] == 8001
883
+ assert translated_config["runtimeConfig"]["run_admin_server"] == True
884
+ assert "env" not in translated_config
885
+ assert "database" not in translated_config
886
+
887
+
888
+ def test_translate_dbosconfig_just_run_admin_server():
889
+ config: DBOSConfig = {
890
+ "name": "test-app",
891
+ "run_admin_server": True,
892
+ }
893
+ translated_config = translate_dbos_config_to_config_file(config)
894
+
793
895
  assert translated_config["name"] == "test-app"
794
896
  assert translated_config["telemetry"]["logs"]["logLevel"] == "INFO"
795
- assert translated_config["database"]["sys_db_name"] == "sysdb"
897
+ assert translated_config["runtimeConfig"]["run_admin_server"] == True
898
+ assert "admin_port" not in translated_config["runtimeConfig"]
796
899
  assert "env" not in translated_config
797
- assert "runtimeConfig" not in translated_config
900
+ assert "database" not in translated_config
798
901
 
799
902
 
800
903
  def test_translate_empty_otlp_traces_endpoints():
@@ -875,6 +978,7 @@ def test_overwrite_config(mocker):
875
978
  "app_db_name": "dbostestpy",
876
979
  "sys_db_name": "sysdb",
877
980
  "connectionTimeoutMillis": 10000,
981
+ "app_db_pool_size": 10,
878
982
  },
879
983
  "telemetry": {
880
984
  "OTLPExporter": {
@@ -887,6 +991,7 @@ def test_overwrite_config(mocker):
887
991
  },
888
992
  "runtimeConfig": {
889
993
  "admin_port": 8001,
994
+ "run_admin_server": True,
890
995
  },
891
996
  "env": {
892
997
  "FOO": "BAR",
@@ -904,10 +1009,13 @@ def test_overwrite_config(mocker):
904
1009
  assert config["database"]["ssl"] == True
905
1010
  assert config["database"]["ssl_ca"] == "cert.pem"
906
1011
  assert config["database"]["connectionTimeoutMillis"] == 10000
1012
+ assert config["database"]["app_db_pool_size"] == 10
1013
+ assert "sys_db_pool_size" not in config["database"]
907
1014
  assert config["telemetry"]["logs"]["logLevel"] == "DEBUG"
908
1015
  assert config["telemetry"]["OTLPExporter"]["tracesEndpoint"] == "thetracesendpoint"
909
1016
  assert config["telemetry"]["OTLPExporter"]["logsEndpoint"] == "thelogsendpoint"
910
1017
  assert "admin_port" not in config["runtimeConfig"]
1018
+ assert "run_admin_server" not in config["runtimeConfig"]
911
1019
  assert "env" not in config
912
1020
 
913
1021
 
@@ -1185,3 +1293,71 @@ def test_name_does_no_match(mocker):
1185
1293
  def test_no_config_file():
1186
1294
  # Handles FileNotFoundError
1187
1295
  check_config_consistency(name="stock-prices")
1296
+
1297
+
1298
+ ####################
1299
+ # DATABASES CONNECTION POOLS
1300
+ ####################
1301
+
1302
+
1303
+ def test_configured_pool_sizes():
1304
+ config: DBOSConfig = {
1305
+ "name": "test-app",
1306
+ "app_db_pool_size": 42,
1307
+ "sys_db_pool_size": 43,
1308
+ }
1309
+
1310
+ dbos = DBOS(config=config)
1311
+ dbos.launch()
1312
+ assert dbos._app_db.engine.pool._pool.maxsize == 42
1313
+ assert dbos._sys_db.engine.pool._pool.maxsize == 43
1314
+ dbos.destroy()
1315
+
1316
+
1317
+ def test_default_pool_params():
1318
+ config: DBOSConfig = {
1319
+ "name": "test-app",
1320
+ }
1321
+
1322
+ dbos = DBOS(config=config)
1323
+ dbos.launch()
1324
+ app_db_engine = dbos._app_db.engine
1325
+ assert app_db_engine.pool._pool.maxsize == 20
1326
+
1327
+ # force the release of connections so we can intercept on connect.
1328
+ app_db_engine.dispose()
1329
+
1330
+ @event.listens_for(app_db_engine, "connect")
1331
+ def inspect_connection(dbapi_connection, connection_record):
1332
+ connect_timeout = dbapi_connection.info.get_parameters()["connect_timeout"]
1333
+ assert connect_timeout == "10"
1334
+
1335
+ with app_db_engine.connect() as conn:
1336
+ pass
1337
+
1338
+ assert dbos._sys_db.engine.pool._pool.maxsize == 20
1339
+ dbos.destroy()
1340
+
1341
+
1342
+ def test_configured_app_db_connect_timeout():
1343
+ config: DBOSConfig = {
1344
+ "name": "test-app",
1345
+ "database_url": f"postgresql://postgres:@localhost:5432/dbname?connect_timeout=7",
1346
+ }
1347
+
1348
+ dbos = DBOS(config=config)
1349
+ dbos.launch()
1350
+ app_db_engine = dbos._app_db.engine
1351
+
1352
+ # force the release of connections so we can intercept on connect.
1353
+ app_db_engine.dispose()
1354
+
1355
+ @event.listens_for(app_db_engine, "connect")
1356
+ def inspect_connection(dbapi_connection, connection_record):
1357
+ connect_timeout = dbapi_connection.info.get_parameters()["connect_timeout"]
1358
+ assert connect_timeout == "7"
1359
+
1360
+ with app_db_engine.connect() as conn:
1361
+ pass
1362
+
1363
+ dbos.destroy()
@@ -8,11 +8,16 @@ import sqlalchemy as sa
8
8
  from psycopg.errors import SerializationFailure
9
9
  from sqlalchemy.exc import InvalidRequestError, OperationalError
10
10
 
11
- # Public API
12
- from dbos import DBOS, GetWorkflowsInput, SetWorkflowID
13
- from dbos._error import DBOSDeadLetterQueueError, DBOSException
11
+ from dbos import DBOS, GetWorkflowsInput, Queue, SetWorkflowID
12
+ from dbos._error import (
13
+ DBOSDeadLetterQueueError,
14
+ DBOSException,
15
+ DBOSMaxStepRetriesExceeded,
16
+ )
14
17
  from dbos._sys_db import WorkflowStatusString
15
18
 
19
+ from .conftest import queue_entries_are_cleaned_up
20
+
16
21
 
17
22
  def test_transaction_errors(dbos: DBOS) -> None:
18
23
  retry_counter: int = 0
@@ -258,3 +263,64 @@ def test_wfstatus_invalid(dbos: DBOS) -> None:
258
263
  with SetWorkflowID(wfuuid):
259
264
  non_deterministic_worklow()
260
265
  assert "Hint: Check if your workflow is deterministic." in str(exc_info.value)
266
+
267
+
268
+ def test_step_retries(dbos: DBOS) -> None:
269
+ step_counter = 0
270
+
271
+ queue = Queue("test-queue")
272
+ max_attempts = 2
273
+
274
+ @DBOS.step(retries_allowed=True, interval_seconds=0, max_attempts=max_attempts)
275
+ def failing_step() -> None:
276
+ nonlocal step_counter
277
+ step_counter += 1
278
+ raise Exception("fail")
279
+
280
+ @DBOS.workflow()
281
+ def failing_workflow() -> None:
282
+ failing_step()
283
+
284
+ @DBOS.workflow()
285
+ def enqueue_failing_step() -> None:
286
+ queue.enqueue(failing_step).get_result()
287
+
288
+ error_message = f"Step {failing_step.__name__} has exceeded its maximum of {max_attempts} retries"
289
+
290
+ # Test calling the step directly
291
+ with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
292
+ failing_step()
293
+ assert error_message in str(excinfo.value)
294
+ assert step_counter == max_attempts
295
+
296
+ # Test calling the workflow
297
+ step_counter = 0
298
+ with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
299
+ failing_workflow()
300
+ assert error_message in str(excinfo.value)
301
+ assert step_counter == max_attempts
302
+
303
+ # Test enqueueing the step
304
+ step_counter = 0
305
+ handle = queue.enqueue(failing_step)
306
+ with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
307
+ handle.get_result()
308
+ assert error_message in str(excinfo.value)
309
+ assert step_counter == max_attempts
310
+
311
+ # Test enqueuing the workflow
312
+ step_counter = 0
313
+ handle = queue.enqueue(failing_workflow)
314
+ with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
315
+ handle.get_result()
316
+ assert error_message in str(excinfo.value)
317
+ assert step_counter == max_attempts
318
+
319
+ # Test enqueuing the step from a workflow
320
+ step_counter = 0
321
+ with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
322
+ enqueue_failing_step()
323
+ assert error_message in str(excinfo.value)
324
+ assert step_counter == max_attempts
325
+
326
+ assert queue_entries_are_cleaned_up(dbos)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes