dbos 1.1.0a2__tar.gz → 1.1.0a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {dbos-1.1.0a2 → dbos-1.1.0a3}/PKG-INFO +1 -1
  2. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_sys_db.py +65 -14
  3. {dbos-1.1.0a2 → dbos-1.1.0a3}/pyproject.toml +1 -1
  4. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_concurrency.py +65 -1
  5. {dbos-1.1.0a2 → dbos-1.1.0a3}/LICENSE +0 -0
  6. {dbos-1.1.0a2 → dbos-1.1.0a3}/README.md +0 -0
  7. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/__init__.py +0 -0
  8. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/__main__.py +0 -0
  9. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_admin_server.py +0 -0
  10. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_app_db.py +0 -0
  11. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_classproperty.py +0 -0
  12. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_client.py +0 -0
  13. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_conductor/conductor.py +0 -0
  14. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_conductor/protocol.py +0 -0
  15. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_context.py +0 -0
  16. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_core.py +0 -0
  17. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_croniter.py +0 -0
  18. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_dbos.py +0 -0
  19. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_dbos_config.py +0 -0
  20. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_debug.py +0 -0
  21. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_docker_pg_helper.py +0 -0
  22. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_error.py +0 -0
  23. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_event_loop.py +0 -0
  24. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_fastapi.py +0 -0
  25. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_flask.py +0 -0
  26. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_kafka.py +0 -0
  27. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_kafka_message.py +0 -0
  28. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_logger.py +0 -0
  29. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/env.py +0 -0
  30. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/script.py.mako +0 -0
  31. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  32. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  33. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  34. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  35. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  36. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  37. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  38. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  39. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  40. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  41. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  42. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_outcome.py +0 -0
  43. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_queue.py +0 -0
  44. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_recovery.py +0 -0
  45. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_registrations.py +0 -0
  46. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_roles.py +0 -0
  47. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_scheduler.py +0 -0
  48. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/__init__.py +0 -0
  49. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/application_database.py +0 -0
  50. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/system_database.py +0 -0
  51. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_serialization.py +0 -0
  52. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/README.md +0 -0
  53. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  54. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  55. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  56. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  57. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  58. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  59. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  60. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  61. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  62. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_tracer.py +0 -0
  63. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_utils.py +0 -0
  64. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_workflow_commands.py +0 -0
  65. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/_github_init.py +0 -0
  66. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/_template_init.py +0 -0
  67. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/cli.py +0 -0
  68. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/dbos-config.schema.json +0 -0
  69. {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/py.typed +0 -0
  70. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/__init__.py +0 -0
  71. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/atexit_no_ctor.py +0 -0
  72. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/atexit_no_launch.py +0 -0
  73. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/classdefs.py +0 -0
  74. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/client_collateral.py +0 -0
  75. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/client_worker.py +0 -0
  76. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/conftest.py +0 -0
  77. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/dupname_classdefs1.py +0 -0
  78. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/dupname_classdefsa.py +0 -0
  79. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/more_classdefs.py +0 -0
  80. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/queuedworkflow.py +0 -0
  81. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_admin_server.py +0 -0
  82. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_async.py +0 -0
  83. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_classdecorators.py +0 -0
  84. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_cli.py +0 -0
  85. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_client.py +0 -0
  86. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_config.py +0 -0
  87. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_croniter.py +0 -0
  88. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_dbos.py +0 -0
  89. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_debug.py +0 -0
  90. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_docker_secrets.py +0 -0
  91. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_failures.py +0 -0
  92. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_fastapi.py +0 -0
  93. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_fastapi_roles.py +0 -0
  94. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_flask.py +0 -0
  95. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_kafka.py +0 -0
  96. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_outcome.py +0 -0
  97. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_package.py +0 -0
  98. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_queue.py +0 -0
  99. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_scheduler.py +0 -0
  100. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_schema_migration.py +0 -0
  101. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_singleton.py +0 -0
  102. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_spans.py +0 -0
  103. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_sqlalchemy.py +0 -0
  104. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_workflow_introspection.py +0 -0
  105. {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_workflow_management.py +0 -0
  106. {dbos-1.1.0a2 → dbos-1.1.0a3}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.1.0a2
3
+ Version: 1.1.0a3
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -222,6 +222,47 @@ class StepInfo(TypedDict):
222
222
  _dbos_null_topic = "__null__topic__"
223
223
 
224
224
 
225
+ class ConditionCount(TypedDict):
226
+ condition: threading.Condition
227
+ count: int
228
+
229
+
230
+ class ThreadSafeConditionDict:
231
+ def __init__(self) -> None:
232
+ self._dict: Dict[str, ConditionCount] = {}
233
+ self._lock = threading.Lock()
234
+
235
+ def get(self, key: str) -> Optional[threading.Condition]:
236
+ with self._lock:
237
+ if key not in self._dict:
238
+ # Key does not exist, return None
239
+ return None
240
+ return self._dict[key]["condition"]
241
+
242
+ def set(
243
+ self, key: str, value: threading.Condition
244
+ ) -> tuple[bool, threading.Condition]:
245
+ with self._lock:
246
+ if key in self._dict:
247
+ # Key already exists, do not overwrite. Increment the wait count.
248
+ cc = self._dict[key]
249
+ cc["count"] += 1
250
+ return False, cc["condition"]
251
+ self._dict[key] = ConditionCount(condition=value, count=1)
252
+ return True, value
253
+
254
+ def pop(self, key: str) -> None:
255
+ with self._lock:
256
+ if key in self._dict:
257
+ cc = self._dict[key]
258
+ cc["count"] -= 1
259
+ if cc["count"] == 0:
260
+ # No more threads waiting on this condition, remove it
261
+ del self._dict[key]
262
+ else:
263
+ dbos_logger.warning(f"Key {key} not found in condition dictionary.")
264
+
265
+
225
266
  class SystemDatabase:
226
267
 
227
268
  def __init__(
@@ -248,8 +289,8 @@ class SystemDatabase:
248
289
  self._engine_kwargs = engine_kwargs
249
290
 
250
291
  self.notification_conn: Optional[psycopg.connection.Connection] = None
251
- self.notifications_map: Dict[str, threading.Condition] = {}
252
- self.workflow_events_map: Dict[str, threading.Condition] = {}
292
+ self.notifications_map = ThreadSafeConditionDict()
293
+ self.workflow_events_map = ThreadSafeConditionDict()
253
294
 
254
295
  # Now we can run background processes
255
296
  self._run_background_processes = True
@@ -1288,7 +1329,12 @@ class SystemDatabase:
1288
1329
  condition = threading.Condition()
1289
1330
  # Must acquire first before adding to the map. Otherwise, the notification listener may notify it before the condition is acquired and waited.
1290
1331
  condition.acquire()
1291
- self.notifications_map[payload] = condition
1332
+ success, _ = self.notifications_map.set(payload, condition)
1333
+ if not success:
1334
+ # This should not happen, but if it does, it means the workflow is executed concurrently.
1335
+ condition.release()
1336
+ self.notifications_map.pop(payload)
1337
+ raise DBOSWorkflowConflictIDError(workflow_uuid)
1292
1338
 
1293
1339
  # Check if the key is already in the database. If not, wait for the notification.
1294
1340
  init_recv: Sequence[Any]
@@ -1381,11 +1427,11 @@ class SystemDatabase:
1381
1427
  f"Received notification on channel: {channel}, payload: {notify.payload}"
1382
1428
  )
1383
1429
  if channel == "dbos_notifications_channel":
1384
- if (
1385
- notify.payload
1386
- and notify.payload in self.notifications_map
1387
- ):
1388
- condition = self.notifications_map[notify.payload]
1430
+ if notify.payload:
1431
+ condition = self.notifications_map.get(notify.payload)
1432
+ if condition is None:
1433
+ # No condition found for this payload
1434
+ continue
1389
1435
  condition.acquire()
1390
1436
  condition.notify_all()
1391
1437
  condition.release()
@@ -1393,11 +1439,11 @@ class SystemDatabase:
1393
1439
  f"Signaled notifications condition for {notify.payload}"
1394
1440
  )
1395
1441
  elif channel == "dbos_workflow_events_channel":
1396
- if (
1397
- notify.payload
1398
- and notify.payload in self.workflow_events_map
1399
- ):
1400
- condition = self.workflow_events_map[notify.payload]
1442
+ if notify.payload:
1443
+ condition = self.workflow_events_map.get(notify.payload)
1444
+ if condition is None:
1445
+ # No condition found for this payload
1446
+ continue
1401
1447
  condition.acquire()
1402
1448
  condition.notify_all()
1403
1449
  condition.release()
@@ -1535,8 +1581,13 @@ class SystemDatabase:
1535
1581
 
1536
1582
  payload = f"{target_uuid}::{key}"
1537
1583
  condition = threading.Condition()
1538
- self.workflow_events_map[payload] = condition
1539
1584
  condition.acquire()
1585
+ success, existing_condition = self.workflow_events_map.set(payload, condition)
1586
+ if not success:
1587
+ # Wait on the existing condition
1588
+ condition.release()
1589
+ condition = existing_condition
1590
+ condition.acquire()
1540
1591
 
1541
1592
  # Check if the key is already in the database. If not, wait for the notification.
1542
1593
  init_recv: Sequence[Any]
@@ -27,7 +27,7 @@ dependencies = [
27
27
  ]
28
28
  requires-python = ">=3.9"
29
29
  readme = "README.md"
30
- version = "1.1.0a2"
30
+ version = "1.1.0a3"
31
31
 
32
32
  [project.license]
33
33
  text = "MIT"
@@ -2,7 +2,7 @@ import threading
2
2
  import time
3
3
  import uuid
4
4
  from concurrent.futures import Future, ThreadPoolExecutor
5
- from typing import Tuple
5
+ from typing import Tuple, cast
6
6
 
7
7
  from sqlalchemy import text
8
8
 
@@ -108,3 +108,67 @@ def test_concurrent_conflict_uuid(dbos: DBOS) -> None:
108
108
 
109
109
  assert future1.result() == wfuuid
110
110
  assert future2.result() == wfuuid
111
+
112
+
113
+ def test_concurrent_recv(dbos: DBOS) -> None:
114
+ condition = threading.Condition()
115
+ counter = 0
116
+
117
+ @DBOS.workflow()
118
+ def test_workflow(topic: str) -> str:
119
+ nonlocal counter
120
+ condition.acquire()
121
+ counter += 1
122
+ if counter % 2 == 1:
123
+ # Wait for the other one to notify
124
+ condition.wait()
125
+ else:
126
+ # Notify the other one
127
+ condition.notify()
128
+ condition.release()
129
+ m = cast(str, DBOS.recv(topic, 5))
130
+ return m
131
+
132
+ def test_thread(id: str, topic: str) -> str:
133
+ with SetWorkflowID(id):
134
+ return test_workflow(topic)
135
+
136
+ wfuuid = str(uuid.uuid4())
137
+ topic = "test_topic"
138
+ with ThreadPoolExecutor(max_workers=2) as executor:
139
+ future1 = executor.submit(test_thread, wfuuid, topic)
140
+ future2 = executor.submit(test_thread, wfuuid, topic)
141
+
142
+ expected_message = "test message"
143
+ DBOS.send(wfuuid, expected_message, topic)
144
+ # Both should return the same message
145
+ assert future1.result() == future2.result()
146
+ assert future1.result() == expected_message
147
+ # Make sure the notification map is empty
148
+ assert not dbos._sys_db.notifications_map._dict
149
+
150
+
151
+ def test_concurrent_getevent(dbos: DBOS) -> None:
152
+ @DBOS.workflow()
153
+ def test_workflow(event_name: str, value: str) -> str:
154
+ DBOS.set_event(event_name, value)
155
+ return value
156
+
157
+ def test_thread(id: str, event_name: str) -> str:
158
+ return cast(str, DBOS.get_event(id, event_name, 5))
159
+
160
+ wfuuid = str(uuid.uuid4())
161
+ event_name = "test_event"
162
+ with ThreadPoolExecutor(max_workers=2) as executor:
163
+ future1 = executor.submit(test_thread, wfuuid, event_name)
164
+ future2 = executor.submit(test_thread, wfuuid, event_name)
165
+
166
+ expected_message = "test message"
167
+ with SetWorkflowID(wfuuid):
168
+ test_workflow(event_name, expected_message)
169
+
170
+ # Both should return the same message
171
+ assert future1.result() == future2.result()
172
+ assert future1.result() == expected_message
173
+ # Make sure the event map is empty
174
+ assert not dbos._sys_db.workflow_events_map._dict
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
File without changes
File without changes
File without changes