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.
- {dbos-1.1.0a2 → dbos-1.1.0a3}/PKG-INFO +1 -1
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_sys_db.py +65 -14
- {dbos-1.1.0a2 → dbos-1.1.0a3}/pyproject.toml +1 -1
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_concurrency.py +65 -1
- {dbos-1.1.0a2 → dbos-1.1.0a3}/LICENSE +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/README.md +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/__init__.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/__main__.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_admin_server.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_app_db.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_classproperty.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_client.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_conductor/conductor.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_conductor/protocol.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_context.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_core.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_croniter.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_dbos.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_dbos_config.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_debug.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_docker_pg_helper.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_error.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_event_loop.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_fastapi.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_flask.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_kafka.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_kafka_message.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_logger.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/env.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/script.py.mako +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_outcome.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_queue.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_recovery.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_registrations.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_roles.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_scheduler.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/__init__.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/application_database.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_schemas/system_database.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_serialization.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_tracer.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_utils.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_workflow_commands.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/_github_init.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/_template_init.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/cli/cli.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/dbos-config.schema.json +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/py.typed +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/__init__.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/atexit_no_ctor.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/atexit_no_launch.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/classdefs.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/client_collateral.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/client_worker.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/conftest.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/dupname_classdefs1.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/dupname_classdefsa.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/more_classdefs.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/queuedworkflow.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_admin_server.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_async.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_classdecorators.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_cli.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_client.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_config.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_croniter.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_dbos.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_debug.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_docker_secrets.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_failures.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_fastapi.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_fastapi_roles.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_flask.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_kafka.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_outcome.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_package.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_queue.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_scheduler.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_schema_migration.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_singleton.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_spans.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_sqlalchemy.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_workflow_introspection.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/tests/test_workflow_management.py +0 -0
- {dbos-1.1.0a2 → dbos-1.1.0a3}/version/__init__.py +0 -0
@@ -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
|
252
|
-
self.workflow_events_map
|
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
|
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
|
-
|
1387
|
-
|
1388
|
-
|
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
|
-
|
1399
|
-
|
1400
|
-
|
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]
|
@@ -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
|
{dbos-1.1.0a2 → dbos-1.1.0a3}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py
RENAMED
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
|
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
|