dbos 1.14.0a6__tar.gz → 1.14.0a9__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 (118) hide show
  1. {dbos-1.14.0a6 → dbos-1.14.0a9}/PKG-INFO +1 -1
  2. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_debouncer.py +18 -17
  3. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_sys_db.py +6 -1
  4. {dbos-1.14.0a6 → dbos-1.14.0a9}/pyproject.toml +1 -1
  5. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_debouncer.py +101 -54
  6. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_streaming.py +15 -7
  7. {dbos-1.14.0a6 → dbos-1.14.0a9}/LICENSE +0 -0
  8. {dbos-1.14.0a6 → dbos-1.14.0a9}/README.md +0 -0
  9. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/__init__.py +0 -0
  10. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/__main__.py +0 -0
  11. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_admin_server.py +0 -0
  12. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/env.py +0 -0
  13. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/script.py.mako +0 -0
  14. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/01ce9f07bd10_streaming.py +0 -0
  15. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  16. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  17. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/471b60d64126_dbos_migrations.py +0 -0
  18. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  19. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  20. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
  21. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  22. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  23. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  24. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  25. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  26. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
  27. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  28. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_alembic_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  29. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_app_db.py +0 -0
  30. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_classproperty.py +0 -0
  31. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_client.py +0 -0
  32. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_conductor/conductor.py +0 -0
  33. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_conductor/protocol.py +0 -0
  34. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_context.py +0 -0
  35. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_core.py +0 -0
  36. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_croniter.py +0 -0
  37. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_dbos.py +0 -0
  38. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_dbos_config.py +0 -0
  39. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_debug.py +0 -0
  40. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_docker_pg_helper.py +0 -0
  41. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_error.py +0 -0
  42. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_event_loop.py +0 -0
  43. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_fastapi.py +0 -0
  44. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_flask.py +0 -0
  45. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_kafka.py +0 -0
  46. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_kafka_message.py +0 -0
  47. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_logger.py +0 -0
  48. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_migration.py +0 -0
  49. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_outcome.py +0 -0
  50. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_queue.py +0 -0
  51. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_recovery.py +0 -0
  52. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_registrations.py +0 -0
  53. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_roles.py +0 -0
  54. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_scheduler.py +0 -0
  55. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_schemas/__init__.py +0 -0
  56. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_schemas/application_database.py +0 -0
  57. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_schemas/system_database.py +0 -0
  58. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_serialization.py +0 -0
  59. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_sys_db_postgres.py +0 -0
  60. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_sys_db_sqlite.py +0 -0
  61. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/README.md +0 -0
  62. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  63. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  64. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  65. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  66. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  67. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  68. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  69. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  70. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  71. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_tracer.py +0 -0
  72. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_utils.py +0 -0
  73. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/_workflow_commands.py +0 -0
  74. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/cli/_github_init.py +0 -0
  75. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/cli/_template_init.py +0 -0
  76. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/cli/cli.py +0 -0
  77. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/cli/migration.py +0 -0
  78. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/dbos-config.schema.json +0 -0
  79. {dbos-1.14.0a6 → dbos-1.14.0a9}/dbos/py.typed +0 -0
  80. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/__init__.py +0 -0
  81. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/atexit_no_ctor.py +0 -0
  82. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/atexit_no_launch.py +0 -0
  83. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/classdefs.py +0 -0
  84. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/client_collateral.py +0 -0
  85. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/client_worker.py +0 -0
  86. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/conftest.py +0 -0
  87. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/dupname_classdefs1.py +0 -0
  88. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/dupname_classdefsa.py +0 -0
  89. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/more_classdefs.py +0 -0
  90. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/queuedworkflow.py +0 -0
  91. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_admin_server.py +0 -0
  92. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_async.py +0 -0
  93. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_async_workflow_management.py +0 -0
  94. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_classdecorators.py +0 -0
  95. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_cli.py +0 -0
  96. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_client.py +0 -0
  97. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_concurrency.py +0 -0
  98. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_config.py +0 -0
  99. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_croniter.py +0 -0
  100. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_dbos.py +0 -0
  101. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_debug.py +0 -0
  102. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_docker_secrets.py +0 -0
  103. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_failures.py +0 -0
  104. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_fastapi.py +0 -0
  105. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_fastapi_roles.py +0 -0
  106. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_flask.py +0 -0
  107. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_kafka.py +0 -0
  108. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_outcome.py +0 -0
  109. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_package.py +0 -0
  110. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_queue.py +0 -0
  111. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_scheduler.py +0 -0
  112. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_schema_migration.py +0 -0
  113. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_singleton.py +0 -0
  114. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_spans.py +0 -0
  115. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_sqlalchemy.py +0 -0
  116. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_workflow_introspection.py +0 -0
  117. {dbos-1.14.0a6 → dbos-1.14.0a9}/tests/test_workflow_management.py +0 -0
  118. {dbos-1.14.0a6 → dbos-1.14.0a9}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.14.0a6
3
+ Version: 1.14.0a9
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -147,7 +147,6 @@ class Debouncer(Generic[P, R]):
147
147
  self,
148
148
  workflow_name: str,
149
149
  *,
150
- debounce_key: str,
151
150
  debounce_timeout_sec: Optional[float] = None,
152
151
  queue: Optional[Queue] = None,
153
152
  ):
@@ -157,13 +156,11 @@ class Debouncer(Generic[P, R]):
157
156
  "queue_name": queue.name if queue else None,
158
157
  "workflow_name": workflow_name,
159
158
  }
160
- self.debounce_key = debounce_key
161
159
 
162
160
  @staticmethod
163
161
  def create(
164
162
  workflow: Callable[P, R],
165
163
  *,
166
- debounce_key: str,
167
164
  debounce_timeout_sec: Optional[float] = None,
168
165
  queue: Optional[Queue] = None,
169
166
  ) -> "Debouncer[P, R]":
@@ -172,7 +169,6 @@ class Debouncer(Generic[P, R]):
172
169
  raise TypeError("Only workflow functions may be debounced, not methods")
173
170
  return Debouncer[P, R](
174
171
  get_dbos_func_name(workflow),
175
- debounce_key=debounce_key,
176
172
  debounce_timeout_sec=debounce_timeout_sec,
177
173
  queue=queue,
178
174
  )
@@ -181,7 +177,6 @@ class Debouncer(Generic[P, R]):
181
177
  def create_async(
182
178
  workflow: Callable[P, Coroutine[Any, Any, R]],
183
179
  *,
184
- debounce_key: str,
185
180
  debounce_timeout_sec: Optional[float] = None,
186
181
  queue: Optional[Queue] = None,
187
182
  ) -> "Debouncer[P, R]":
@@ -190,13 +185,16 @@ class Debouncer(Generic[P, R]):
190
185
  raise TypeError("Only workflow functions may be debounced, not methods")
191
186
  return Debouncer[P, R](
192
187
  get_dbos_func_name(workflow),
193
- debounce_key=debounce_key,
194
188
  debounce_timeout_sec=debounce_timeout_sec,
195
189
  queue=queue,
196
190
  )
197
191
 
198
192
  def debounce(
199
- self, debounce_period_sec: float, *args: P.args, **kwargs: P.kwargs
193
+ self,
194
+ debounce_key: str,
195
+ debounce_period_sec: float,
196
+ *args: P.args,
197
+ **kwargs: P.kwargs,
200
198
  ) -> "WorkflowHandle[R]":
201
199
  from dbos._dbos import DBOS, _get_dbos_instance
202
200
 
@@ -232,7 +230,8 @@ class Debouncer(Generic[P, R]):
232
230
  while True:
233
231
  try:
234
232
  # Attempt to enqueue a debouncer for this workflow.
235
- with SetEnqueueOptions(deduplication_id=self.debounce_key):
233
+ deduplication_id = f"{self.options['workflow_name']}-{debounce_key}"
234
+ with SetEnqueueOptions(deduplication_id=deduplication_id):
236
235
  with SetWorkflowTimeout(None):
237
236
  internal_queue.enqueue(
238
237
  debouncer_workflow,
@@ -249,7 +248,7 @@ class Debouncer(Generic[P, R]):
249
248
  def get_deduplicated_workflow() -> Optional[str]:
250
249
  return dbos._sys_db.get_deduplicated_workflow(
251
250
  queue_name=internal_queue.name,
252
- deduplication_id=self.debounce_key,
251
+ deduplication_id=deduplication_id,
253
252
  )
254
253
 
255
254
  dedup_wfid = dbos._sys_db.call_function_as_step(
@@ -281,6 +280,7 @@ class Debouncer(Generic[P, R]):
281
280
 
282
281
  async def debounce_async(
283
282
  self,
283
+ debounce_key: str,
284
284
  debounce_period_sec: float,
285
285
  *args: P.args,
286
286
  **kwargs: P.kwargs,
@@ -289,7 +289,7 @@ class Debouncer(Generic[P, R]):
289
289
 
290
290
  dbos = _get_dbos_instance()
291
291
  handle = await asyncio.to_thread(
292
- self.debounce, debounce_period_sec, *args, **kwargs
292
+ self.debounce, debounce_key, debounce_period_sec, *args, **kwargs
293
293
  )
294
294
  return WorkflowHandleAsyncPolling(handle.workflow_id, dbos)
295
295
 
@@ -301,7 +301,6 @@ class DebouncerClient:
301
301
  client: DBOSClient,
302
302
  workflow_options: EnqueueOptions,
303
303
  *,
304
- debounce_key: str,
305
304
  debounce_timeout_sec: Optional[float] = None,
306
305
  queue: Optional[Queue] = None,
307
306
  ):
@@ -311,11 +310,10 @@ class DebouncerClient:
311
310
  "queue_name": queue.name if queue else None,
312
311
  "workflow_name": workflow_options["workflow_name"],
313
312
  }
314
- self.debounce_key = debounce_key
315
313
  self.client = client
316
314
 
317
315
  def debounce(
318
- self, debounce_period_sec: float, *args: Any, **kwargs: Any
316
+ self, debounce_key: str, debounce_period_sec: float, *args: Any, **kwargs: Any
319
317
  ) -> "WorkflowHandle[R]":
320
318
 
321
319
  ctxOptions: ContextOptions = {
@@ -333,10 +331,13 @@ class DebouncerClient:
333
331
  while True:
334
332
  try:
335
333
  # Attempt to enqueue a debouncer for this workflow.
334
+ deduplication_id = (
335
+ f"{self.debouncer_options['workflow_name']}-{debounce_key}"
336
+ )
336
337
  debouncer_options: EnqueueOptions = {
337
338
  "workflow_name": DEBOUNCER_WORKFLOW_NAME,
338
339
  "queue_name": INTERNAL_QUEUE_NAME,
339
- "deduplication_id": self.debounce_key,
340
+ "deduplication_id": deduplication_id,
340
341
  }
341
342
  self.client.enqueue(
342
343
  debouncer_options,
@@ -353,7 +354,7 @@ class DebouncerClient:
353
354
  # If there is already a debouncer, send a message to it.
354
355
  dedup_wfid = self.client._sys_db.get_deduplicated_workflow(
355
356
  queue_name=INTERNAL_QUEUE_NAME,
356
- deduplication_id=self.debounce_key,
357
+ deduplication_id=deduplication_id,
357
358
  )
358
359
  if dedup_wfid is None:
359
360
  continue
@@ -384,10 +385,10 @@ class DebouncerClient:
384
385
  )
385
386
 
386
387
  async def debounce_async(
387
- self, debounce_period_sec: float, *args: Any, **kwargs: Any
388
+ self, deboucne_key: str, debounce_period_sec: float, *args: Any, **kwargs: Any
388
389
  ) -> "WorkflowHandleAsync[R]":
389
390
  handle: "WorkflowHandle[R]" = await asyncio.to_thread(
390
- self.debounce, debounce_period_sec, *args, **kwargs
391
+ self.debounce, deboucne_key, debounce_period_sec, *args, **kwargs
391
392
  )
392
393
  return WorkflowHandleClientAsyncPolling[R](
393
394
  handle.workflow_id, self.client._sys_db
@@ -1934,8 +1934,13 @@ class SystemDatabase(ABC):
1934
1934
  )
1935
1935
  if self._debug_mode and recorded_output is None:
1936
1936
  raise Exception(
1937
- "called set_event in debug mode without a previous execution"
1937
+ "called writeStream in debug mode without a previous execution"
1938
1938
  )
1939
+ if recorded_output is not None:
1940
+ dbos_logger.debug(
1941
+ f"Replaying writeStream, id: {function_id}, key: {key}"
1942
+ )
1943
+ return
1939
1944
  # Find the maximum offset for this workflow_uuid and key combination
1940
1945
  max_offset_result = c.execute(
1941
1946
  sa.select(sa.func.max(SystemSchema.streams.c.offset)).where(
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.14.0a6"
30
+ version = "1.14.0a9"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -17,17 +17,12 @@ from dbos._queue import Queue
17
17
  from dbos._utils import GlobalParams
18
18
 
19
19
 
20
- def workflow(x: int) -> int:
21
- return x
22
-
23
-
24
- async def workflow_async(x: int) -> int:
25
- return x
26
-
27
-
28
20
  def test_debouncer(dbos: DBOS) -> None:
29
21
 
30
- DBOS.workflow()(workflow)
22
+ @DBOS.workflow()
23
+ def workflow(x: int) -> int:
24
+ return x
25
+
31
26
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
32
27
 
33
28
  @DBOS.step()
@@ -35,18 +30,21 @@ def test_debouncer(dbos: DBOS) -> None:
35
30
  return str(uuid.uuid4())
36
31
 
37
32
  def debouncer_test() -> None:
38
- debouncer = Debouncer.create(workflow, debounce_key="key")
39
33
 
40
34
  debounce_period = 2
41
35
 
42
- first_handle = debouncer.debounce(debounce_period, first_value)
43
- second_handle = debouncer.debounce(debounce_period, second_value)
36
+ debouncer = Debouncer.create(workflow)
37
+ first_handle = debouncer.debounce("key", debounce_period, first_value)
38
+ debouncer = Debouncer.create(workflow)
39
+ second_handle = debouncer.debounce("key", debounce_period, second_value)
44
40
  assert first_handle.workflow_id == second_handle.workflow_id
45
41
  assert first_handle.get_result() == second_value
46
42
  assert second_handle.get_result() == second_value
47
43
 
48
- third_handle = debouncer.debounce(debounce_period, third_value)
49
- fourth_handle = debouncer.debounce(debounce_period, fourth_value)
44
+ debouncer = Debouncer.create(workflow)
45
+ third_handle = debouncer.debounce("key", debounce_period, third_value)
46
+ debouncer = Debouncer.create(workflow)
47
+ fourth_handle = debouncer.debounce("key", debounce_period, fourth_value)
50
48
  assert third_handle.workflow_id != first_handle.workflow_id
51
49
  assert third_handle.workflow_id == fourth_handle.workflow_id
52
50
  assert third_handle.get_result() == fourth_value
@@ -55,7 +53,7 @@ def test_debouncer(dbos: DBOS) -> None:
55
53
  # Test SetWorkflowID works
56
54
  wfid = generate_uuid()
57
55
  with SetWorkflowID(wfid):
58
- handle = debouncer.debounce(debounce_period, first_value)
56
+ handle = debouncer.debounce("key", debounce_period, first_value)
59
57
  assert handle.workflow_id == wfid
60
58
  assert handle.get_result() == first_value
61
59
 
@@ -76,25 +74,27 @@ def test_debouncer(dbos: DBOS) -> None:
76
74
 
77
75
  def test_debouncer_timeout(dbos: DBOS) -> None:
78
76
 
79
- DBOS.workflow()(workflow)
77
+ @DBOS.workflow()
78
+ def workflow(x: int) -> int:
79
+ return x
80
+
80
81
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
81
82
 
82
83
  # Set a huge period but small timeout, verify workflows start after the timeout
83
84
  debouncer = Debouncer.create(
84
85
  workflow,
85
- debounce_key="key",
86
86
  debounce_timeout_sec=2,
87
87
  )
88
88
  long_debounce_period = 10000000
89
89
 
90
- first_handle = debouncer.debounce(long_debounce_period, first_value)
91
- second_handle = debouncer.debounce(long_debounce_period, second_value)
90
+ first_handle = debouncer.debounce("key", long_debounce_period, first_value)
91
+ second_handle = debouncer.debounce("key", long_debounce_period, second_value)
92
92
  assert first_handle.workflow_id == second_handle.workflow_id
93
93
  assert first_handle.get_result() == second_value
94
94
  assert second_handle.get_result() == second_value
95
95
 
96
- third_handle = debouncer.debounce(long_debounce_period, third_value)
97
- fourth_handle = debouncer.debounce(long_debounce_period, fourth_value)
96
+ third_handle = debouncer.debounce("key", long_debounce_period, third_value)
97
+ fourth_handle = debouncer.debounce("key", long_debounce_period, fourth_value)
98
98
  assert third_handle.workflow_id != first_handle.workflow_id
99
99
  assert third_handle.workflow_id == fourth_handle.workflow_id
100
100
  assert third_handle.get_result() == fourth_value
@@ -103,29 +103,57 @@ def test_debouncer_timeout(dbos: DBOS) -> None:
103
103
  # Submit first with a long period then with a short one, verify workflows start on time
104
104
  debouncer = Debouncer.create(
105
105
  workflow,
106
- debounce_key="key",
107
106
  )
108
107
  short_debounce_period = 1
109
108
 
110
- first_handle = debouncer.debounce(long_debounce_period, first_value)
111
- second_handle = debouncer.debounce(short_debounce_period, second_value)
109
+ first_handle = debouncer.debounce("key", long_debounce_period, first_value)
110
+ second_handle = debouncer.debounce("key", short_debounce_period, second_value)
112
111
  assert fourth_handle.workflow_id != first_handle.workflow_id
113
112
  assert first_handle.workflow_id == second_handle.workflow_id
114
113
  assert first_handle.get_result() == second_value
115
114
  assert second_handle.get_result() == second_value
116
115
 
117
116
 
117
+ def test_multiple_debouncers(dbos: DBOS) -> None:
118
+
119
+ @DBOS.workflow()
120
+ def workflow(x: int) -> int:
121
+ return x
122
+
123
+ first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
124
+
125
+ # Set a huge period but small timeout, verify workflows start after the timeout
126
+ debouncer_one = Debouncer.create(workflow)
127
+ debouncer_two = Debouncer.create(workflow)
128
+ debounce_period = 2
129
+
130
+ first_handle = debouncer_one.debounce("key_one", debounce_period, first_value)
131
+ second_handle = debouncer_one.debounce("key_one", debounce_period, second_value)
132
+ third_handle = debouncer_two.debounce("key_two", debounce_period, third_value)
133
+ fourth_handle = debouncer_two.debounce("key_two", debounce_period, fourth_value)
134
+ assert first_handle.workflow_id == second_handle.workflow_id
135
+ assert first_handle.workflow_id != third_handle.workflow_id
136
+ assert third_handle.workflow_id == fourth_handle.workflow_id
137
+ assert first_handle.get_result() == second_value
138
+ assert second_handle.get_result() == second_value
139
+ assert third_handle.get_result() == fourth_value
140
+ assert fourth_handle.get_result() == fourth_value
141
+
142
+
118
143
  def test_debouncer_queue(dbos: DBOS) -> None:
119
144
 
120
- DBOS.workflow()(workflow)
145
+ @DBOS.workflow()
146
+ def workflow(x: int) -> int:
147
+ return x
148
+
121
149
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
122
150
  queue = Queue("test-queue")
123
151
 
124
- debouncer = Debouncer.create(workflow, debounce_key="key", queue=queue)
152
+ debouncer = Debouncer.create(workflow, queue=queue)
125
153
  debounce_period_sec = 2
126
154
 
127
- first_handle = debouncer.debounce(debounce_period_sec, first_value)
128
- second_handle = debouncer.debounce(debounce_period_sec, second_value)
155
+ first_handle = debouncer.debounce("key", debounce_period_sec, first_value)
156
+ second_handle = debouncer.debounce("key", debounce_period_sec, second_value)
129
157
  assert first_handle.workflow_id == second_handle.workflow_id
130
158
  assert first_handle.get_result() == second_value
131
159
  assert second_handle.get_result() == second_value
@@ -133,8 +161,8 @@ def test_debouncer_queue(dbos: DBOS) -> None:
133
161
 
134
162
  # Test SetWorkflowTimeout works
135
163
  with SetWorkflowTimeout(5.0):
136
- third_handle = debouncer.debounce(debounce_period_sec, third_value)
137
- fourth_handle = debouncer.debounce(debounce_period_sec, fourth_value)
164
+ third_handle = debouncer.debounce("key", debounce_period_sec, third_value)
165
+ fourth_handle = debouncer.debounce("key", debounce_period_sec, fourth_value)
138
166
  assert third_handle.workflow_id != first_handle.workflow_id
139
167
  assert third_handle.workflow_id == fourth_handle.workflow_id
140
168
  assert third_handle.get_result() == fourth_value
@@ -146,7 +174,7 @@ def test_debouncer_queue(dbos: DBOS) -> None:
146
174
  # Test SetWorkflowID works
147
175
  wfid = str(uuid.uuid4())
148
176
  with SetWorkflowID(wfid):
149
- handle = debouncer.debounce(debounce_period_sec, first_value)
177
+ handle = debouncer.debounce("key", debounce_period_sec, first_value)
150
178
  assert handle.workflow_id == wfid
151
179
  assert handle.get_result() == first_value
152
180
  assert handle.get_status().queue_name == queue.name
@@ -157,7 +185,7 @@ def test_debouncer_queue(dbos: DBOS) -> None:
157
185
  with SetEnqueueOptions(
158
186
  priority=1, deduplication_id="test", app_version=test_version
159
187
  ):
160
- handle = debouncer.debounce(debounce_period_sec, first_value)
188
+ handle = debouncer.debounce("key", debounce_period_sec, first_value)
161
189
  assert handle.get_result() == first_value
162
190
  assert handle.get_status().queue_name == queue.name
163
191
  assert handle.get_status().app_version == test_version
@@ -166,20 +194,31 @@ def test_debouncer_queue(dbos: DBOS) -> None:
166
194
  @pytest.mark.asyncio
167
195
  async def test_debouncer_async(dbos: DBOS) -> None:
168
196
 
169
- DBOS.workflow()(workflow_async)
197
+ @DBOS.workflow()
198
+ async def workflow_async(x: int) -> int:
199
+ return x
200
+
170
201
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
171
202
 
172
- debouncer = Debouncer.create_async(workflow_async, debounce_key="key")
203
+ debouncer = Debouncer.create_async(workflow_async)
173
204
  debounce_period_sec = 2
174
205
 
175
- first_handle = await debouncer.debounce_async(debounce_period_sec, first_value)
176
- second_handle = await debouncer.debounce_async(debounce_period_sec, second_value)
206
+ first_handle = await debouncer.debounce_async(
207
+ "key", debounce_period_sec, first_value
208
+ )
209
+ second_handle = await debouncer.debounce_async(
210
+ "key", debounce_period_sec, second_value
211
+ )
177
212
  assert first_handle.workflow_id == second_handle.workflow_id
178
213
  assert await first_handle.get_result() == second_value
179
214
  assert await second_handle.get_result() == second_value
180
215
 
181
- third_handle = await debouncer.debounce_async(debounce_period_sec, third_value)
182
- fourth_handle = await debouncer.debounce_async(debounce_period_sec, fourth_value)
216
+ third_handle = await debouncer.debounce_async(
217
+ "key", debounce_period_sec, third_value
218
+ )
219
+ fourth_handle = await debouncer.debounce_async(
220
+ "key", debounce_period_sec, fourth_value
221
+ )
183
222
  assert third_handle.workflow_id != first_handle.workflow_id
184
223
  assert third_handle.workflow_id == fourth_handle.workflow_id
185
224
  assert await third_handle.get_result() == fourth_value
@@ -188,32 +227,35 @@ async def test_debouncer_async(dbos: DBOS) -> None:
188
227
 
189
228
  def test_debouncer_client(dbos: DBOS, client: DBOSClient) -> None:
190
229
 
191
- DBOS.workflow()(workflow)
230
+ @DBOS.workflow()
231
+ def workflow(x: int) -> int:
232
+ return x
233
+
192
234
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
193
235
  queue = Queue("test-queue")
194
236
 
195
237
  options: EnqueueOptions = {
196
- "workflow_name": workflow.__name__,
238
+ "workflow_name": workflow.__qualname__,
197
239
  "queue_name": queue.name,
198
240
  }
199
- debouncer = DebouncerClient(client, options, debounce_key="key")
241
+ debouncer = DebouncerClient(client, options)
200
242
  debounce_period_sec = 2
201
243
 
202
244
  first_handle: WorkflowHandle[int] = debouncer.debounce(
203
- debounce_period_sec, first_value
245
+ "key", debounce_period_sec, first_value
204
246
  )
205
247
  second_handle: WorkflowHandle[int] = debouncer.debounce(
206
- debounce_period_sec, second_value
248
+ "key", debounce_period_sec, second_value
207
249
  )
208
250
  assert first_handle.workflow_id == second_handle.workflow_id
209
251
  assert first_handle.get_result() == second_value
210
252
  assert second_handle.get_result() == second_value
211
253
 
212
254
  third_handle: WorkflowHandle[int] = debouncer.debounce(
213
- debounce_period_sec, third_value
255
+ "key", debounce_period_sec, third_value
214
256
  )
215
257
  fourth_handle: WorkflowHandle[int] = debouncer.debounce(
216
- debounce_period_sec, fourth_value
258
+ "key", debounce_period_sec, fourth_value
217
259
  )
218
260
  assert third_handle.workflow_id != first_handle.workflow_id
219
261
  assert third_handle.workflow_id == fourth_handle.workflow_id
@@ -222,7 +264,9 @@ def test_debouncer_client(dbos: DBOS, client: DBOSClient) -> None:
222
264
 
223
265
  wfid = str(uuid.uuid4())
224
266
  options["workflow_id"] = wfid
225
- handle: WorkflowHandle[int] = debouncer.debounce(debounce_period_sec, first_value)
267
+ handle: WorkflowHandle[int] = debouncer.debounce(
268
+ "key", debounce_period_sec, first_value
269
+ )
226
270
  assert handle.workflow_id == wfid
227
271
  assert handle.get_result() == first_value
228
272
 
@@ -230,32 +274,35 @@ def test_debouncer_client(dbos: DBOS, client: DBOSClient) -> None:
230
274
  @pytest.mark.asyncio
231
275
  async def test_debouncer_client_async(dbos: DBOS, client: DBOSClient) -> None:
232
276
 
233
- DBOS.workflow()(workflow_async)
277
+ @DBOS.workflow()
278
+ async def workflow_async(x: int) -> int:
279
+ return x
280
+
234
281
  first_value, second_value, third_value, fourth_value = 0, 1, 2, 3
235
282
  queue = Queue("test-queue")
236
283
 
237
284
  options: EnqueueOptions = {
238
- "workflow_name": workflow_async.__name__,
285
+ "workflow_name": workflow_async.__qualname__,
239
286
  "queue_name": queue.name,
240
287
  }
241
- debouncer = DebouncerClient(client, options, debounce_key="key")
288
+ debouncer = DebouncerClient(client, options)
242
289
  debounce_period_sec = 2
243
290
 
244
291
  first_handle: WorkflowHandleAsync[int] = await debouncer.debounce_async(
245
- debounce_period_sec, first_value
292
+ "key", debounce_period_sec, first_value
246
293
  )
247
294
  second_handle: WorkflowHandleAsync[int] = await debouncer.debounce_async(
248
- debounce_period_sec, second_value
295
+ "key", debounce_period_sec, second_value
249
296
  )
250
297
  assert first_handle.workflow_id == second_handle.workflow_id
251
298
  assert await first_handle.get_result() == second_value
252
299
  assert await second_handle.get_result() == second_value
253
300
 
254
301
  third_handle: WorkflowHandleAsync[int] = await debouncer.debounce_async(
255
- debounce_period_sec, third_value
302
+ "key", debounce_period_sec, third_value
256
303
  )
257
304
  fourth_handle: WorkflowHandleAsync[int] = await debouncer.debounce_async(
258
- debounce_period_sec, fourth_value
305
+ "key", debounce_period_sec, fourth_value
259
306
  )
260
307
  assert third_handle.workflow_id != first_handle.workflow_id
261
308
  assert third_handle.workflow_id == fourth_handle.workflow_id
@@ -265,7 +312,7 @@ async def test_debouncer_client_async(dbos: DBOS, client: DBOSClient) -> None:
265
312
  wfid = str(uuid.uuid4())
266
313
  options["workflow_id"] = wfid
267
314
  handle: WorkflowHandleAsync[int] = await debouncer.debounce_async(
268
- debounce_period_sec, first_value
315
+ "key", debounce_period_sec, first_value
269
316
  )
270
317
  assert handle.workflow_id == wfid
271
318
  assert await handle.get_result() == first_value
@@ -231,16 +231,19 @@ def test_stream_error_cases(dbos: DBOS) -> None:
231
231
  def test_stream_workflow_recovery(dbos: DBOS) -> None:
232
232
  """Test that stream operations are properly recovered during workflow replay."""
233
233
 
234
- call_count = 0
234
+ workflow_call_count = 0
235
+ step_call_count = 0
235
236
 
236
237
  @DBOS.step()
237
238
  def counting_step() -> int:
238
- nonlocal call_count
239
- call_count += 1
240
- return call_count
239
+ nonlocal step_call_count
240
+ step_call_count += 1
241
+ return step_call_count
241
242
 
242
243
  @DBOS.workflow()
243
244
  def recovery_test_workflow() -> None:
245
+ nonlocal workflow_call_count
246
+ workflow_call_count += 1
244
247
  count1 = counting_step()
245
248
  DBOS.write_stream("recovery_stream", f"step_{count1}")
246
249
 
@@ -254,13 +257,18 @@ def test_stream_workflow_recovery(dbos: DBOS) -> None:
254
257
  with SetWorkflowID(wfid):
255
258
  recovery_test_workflow()
256
259
 
260
+ # Validate stream contents
261
+ values = list(DBOS.read_stream(wfid, "recovery_stream"))
262
+ assert values == ["step_1", "step_2"]
263
+
257
264
  # Reset call count and run the same workflow ID again (should replay)
258
- call_count = 0
265
+ dbos._sys_db.update_workflow_outcome(wfid, "PENDING")
259
266
  with SetWorkflowID(wfid):
260
267
  recovery_test_workflow()
261
268
 
262
- # The counting step should not have been called again (replayed from recorded results)
263
- assert call_count == 0
269
+ # The workflow should have been called again
270
+ assert workflow_call_count == 2
271
+ assert step_call_count == 2
264
272
 
265
273
  # Stream should still be readable and contain the same values
266
274
  values = list(DBOS.read_stream(wfid, "recovery_stream"))
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
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