dbos 0.7.0a5__py3-none-any.whl → 0.7.0a8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dbos/__init__.py CHANGED
@@ -3,6 +3,7 @@ from .context import DBOSContextEnsure, SetWorkflowID
3
3
  from .dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle, WorkflowStatus
4
4
  from .dbos_config import ConfigFile, get_dbos_database_url, load_config
5
5
  from .kafka_message import KafkaMessage
6
+ from .queue import Queue
6
7
  from .system_database import GetWorkflowsInput, WorkflowStatusString
7
8
 
8
9
  __all__ = [
@@ -19,4 +20,5 @@ __all__ = [
19
20
  "load_config",
20
21
  "get_dbos_database_url",
21
22
  "error",
23
+ "Queue",
22
24
  ]
dbos/context.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  import uuid
6
+ from contextlib import AbstractContextManager
6
7
  from contextvars import ContextVar
7
8
  from enum import Enum
8
9
  from types import TracebackType
@@ -344,7 +345,7 @@ class SetWorkflowRecovery:
344
345
  return False # Did not handle
345
346
 
346
347
 
347
- class EnterDBOSWorkflow:
348
+ class EnterDBOSWorkflow(AbstractContextManager[DBOSContext, Literal[False]]):
348
349
  def __init__(self, attributes: TracedAttributes) -> None:
349
350
  self.created_ctx = False
350
351
  self.attributes = attributes
@@ -377,7 +378,7 @@ class EnterDBOSWorkflow:
377
378
  return False # Did not handle
378
379
 
379
380
 
380
- class EnterDBOSChildWorkflow:
381
+ class EnterDBOSChildWorkflow(AbstractContextManager[DBOSContext, Literal[False]]):
381
382
  def __init__(self, attributes: TracedAttributes) -> None:
382
383
  self.parent_ctx: Optional[DBOSContext] = None
383
384
  self.child_ctx: Optional[DBOSContext] = None
dbos/core.py CHANGED
@@ -63,6 +63,7 @@ from dbos.system_database import (
63
63
  OperationResultInternal,
64
64
  WorkflowInputs,
65
65
  WorkflowStatusInternal,
66
+ WorkflowStatusString,
66
67
  )
67
68
 
68
69
  if TYPE_CHECKING:
@@ -108,7 +109,7 @@ class _WorkflowHandlePolling(Generic[R]):
108
109
  return self.workflow_id
109
110
 
110
111
  def get_result(self) -> R:
111
- res: R = self.dbos.sys_db.await_workflow_result(self.workflow_id)
112
+ res: R = self.dbos._sys_db.await_workflow_result(self.workflow_id)
112
113
  return res
113
114
 
114
115
  def get_status(self) -> "WorkflowStatus":
@@ -126,6 +127,7 @@ def _init_workflow(
126
127
  class_name: Optional[str],
127
128
  config_name: Optional[str],
128
129
  temp_wf_type: Optional[str],
130
+ queue: Optional[str] = None,
129
131
  ) -> WorkflowStatusInternal:
130
132
  wfid = (
131
133
  ctx.workflow_id
@@ -134,7 +136,11 @@ def _init_workflow(
134
136
  )
135
137
  status: WorkflowStatusInternal = {
136
138
  "workflow_uuid": wfid,
137
- "status": "PENDING",
139
+ "status": (
140
+ WorkflowStatusString.PENDING.value
141
+ if queue is None
142
+ else WorkflowStatusString.ENQUEUED.value
143
+ ),
138
144
  "name": wf_name,
139
145
  "class_name": class_name,
140
146
  "config_name": config_name,
@@ -150,20 +156,25 @@ def _init_workflow(
150
156
  json.dumps(ctx.authenticated_roles) if ctx.authenticated_roles else None
151
157
  ),
152
158
  "assumed_role": ctx.assumed_role,
159
+ "queue_name": queue,
153
160
  }
154
161
 
155
162
  # If we have a class name, the first arg is the instance and do not serialize
156
163
  if class_name is not None:
157
164
  inputs = {"args": inputs["args"][1:], "kwargs": inputs["kwargs"]}
158
165
 
159
- if temp_wf_type != "transaction":
166
+ if temp_wf_type != "transaction" or queue is not None:
160
167
  # Synchronously record the status and inputs for workflows and single-step workflows
161
168
  # We also have to do this for single-step workflows because of the foreign key constraint on the operation outputs table
162
- dbos.sys_db.update_workflow_status(status, False, ctx.in_recovery)
163
- dbos.sys_db.update_workflow_inputs(wfid, utils.serialize(inputs))
169
+ # TODO: Make this transactional (and with the queue step below)
170
+ dbos._sys_db.update_workflow_status(status, False, ctx.in_recovery)
171
+ dbos._sys_db.update_workflow_inputs(wfid, utils.serialize(inputs))
164
172
  else:
165
173
  # Buffer the inputs for single-transaction workflows, but don't buffer the status
166
- dbos.sys_db.buffer_workflow_inputs(wfid, utils.serialize(inputs))
174
+ dbos._sys_db.buffer_workflow_inputs(wfid, utils.serialize(inputs))
175
+
176
+ if queue is not None:
177
+ dbos._sys_db.enqueue(wfid, queue)
167
178
 
168
179
  return status
169
180
 
@@ -179,7 +190,9 @@ def _execute_workflow(
179
190
  output = func(*args, **kwargs)
180
191
  status["status"] = "SUCCESS"
181
192
  status["output"] = utils.serialize(output)
182
- dbos.sys_db.buffer_workflow_status(status)
193
+ if status["queue_name"] is not None:
194
+ dbos._sys_db.remove_from_queue(status["workflow_uuid"])
195
+ dbos._sys_db.buffer_workflow_status(status)
183
196
  except DBOSWorkflowConflictIDError:
184
197
  # Retrieve the workflow handle and wait for the result.
185
198
  # Must use existing_workflow=False because workflow status might not be set yet for single transaction workflows.
@@ -191,7 +204,9 @@ def _execute_workflow(
191
204
  except Exception as error:
192
205
  status["status"] = "ERROR"
193
206
  status["error"] = utils.serialize(error)
194
- dbos.sys_db.update_workflow_status(status)
207
+ if status["queue_name"] is not None:
208
+ dbos._sys_db.remove_from_queue(status["workflow_uuid"])
209
+ dbos._sys_db.update_workflow_status(status)
195
210
  raise
196
211
 
197
212
  return output
@@ -221,10 +236,10 @@ def _execute_workflow_wthread(
221
236
 
222
237
 
223
238
  def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]":
224
- status = dbos.sys_db.get_workflow_status(workflow_id)
239
+ status = dbos._sys_db.get_workflow_status(workflow_id)
225
240
  if not status:
226
241
  raise DBOSRecoveryError(workflow_id, "Workflow status not found")
227
- inputs = dbos.sys_db.get_workflow_inputs(workflow_id)
242
+ inputs = dbos._sys_db.get_workflow_inputs(workflow_id)
228
243
  if not inputs:
229
244
  raise DBOSRecoveryError(workflow_id, "Workflow inputs not found")
230
245
  wf_func = dbos._registry.workflow_info_map.get(status["name"], None)
@@ -249,6 +264,8 @@ def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]
249
264
  return _start_workflow(
250
265
  dbos,
251
266
  wf_func,
267
+ status["queue_name"],
268
+ True,
252
269
  dbos._registry.instance_info_map[iname],
253
270
  *inputs["args"],
254
271
  **inputs["kwargs"],
@@ -264,6 +281,8 @@ def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]
264
281
  return _start_workflow(
265
282
  dbos,
266
283
  wf_func,
284
+ status["queue_name"],
285
+ True,
267
286
  dbos._registry.class_info_map[class_name],
268
287
  *inputs["args"],
269
288
  **inputs["kwargs"],
@@ -271,7 +290,12 @@ def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]
271
290
  else:
272
291
  with SetWorkflowID(workflow_id):
273
292
  return _start_workflow(
274
- dbos, wf_func, *inputs["args"], **inputs["kwargs"]
293
+ dbos,
294
+ wf_func,
295
+ status["queue_name"],
296
+ True,
297
+ *inputs["args"],
298
+ **inputs["kwargs"],
275
299
  )
276
300
 
277
301
 
@@ -298,34 +322,22 @@ def _workflow_wrapper(dbosreg: "_DBOSRegistry", func: F) -> F:
298
322
  "kwargs": kwargs,
299
323
  }
300
324
  ctx = get_local_dbos_context()
301
- if ctx and ctx.is_workflow():
302
- with EnterDBOSChildWorkflow(attributes), DBOSAssumeRole(rr):
303
- ctx = assert_current_dbos_context() # Now the child ctx
304
- status = _init_workflow(
305
- dbos,
306
- ctx,
307
- inputs=inputs,
308
- wf_name=get_dbos_func_name(func),
309
- class_name=get_dbos_class_name(fi, func, args),
310
- config_name=get_config_name(fi, func, args),
311
- temp_wf_type=get_temp_workflow_type(func),
312
- )
313
-
314
- return _execute_workflow(dbos, status, func, *args, **kwargs)
315
- else:
316
- with EnterDBOSWorkflow(attributes), DBOSAssumeRole(rr):
317
- ctx = assert_current_dbos_context()
318
- status = _init_workflow(
319
- dbos,
320
- ctx,
321
- inputs=inputs,
322
- wf_name=get_dbos_func_name(func),
323
- class_name=get_dbos_class_name(fi, func, args),
324
- config_name=get_config_name(fi, func, args),
325
- temp_wf_type=get_temp_workflow_type(func),
326
- )
325
+ enterWorkflowCtxMgr = (
326
+ EnterDBOSChildWorkflow if ctx and ctx.is_workflow() else EnterDBOSWorkflow
327
+ )
328
+ with enterWorkflowCtxMgr(attributes), DBOSAssumeRole(rr):
329
+ ctx = assert_current_dbos_context() # Now the child ctx
330
+ status = _init_workflow(
331
+ dbos,
332
+ ctx,
333
+ inputs=inputs,
334
+ wf_name=get_dbos_func_name(func),
335
+ class_name=get_dbos_class_name(fi, func, args),
336
+ config_name=get_config_name(fi, func, args),
337
+ temp_wf_type=get_temp_workflow_type(func),
338
+ )
327
339
 
328
- return _execute_workflow(dbos, status, func, *args, **kwargs)
340
+ return _execute_workflow(dbos, status, func, *args, **kwargs)
329
341
 
330
342
  wrapped_func = cast(F, wrapper)
331
343
  return wrapped_func
@@ -343,6 +355,8 @@ def _workflow(reg: "_DBOSRegistry") -> Callable[[F], F]:
343
355
  def _start_workflow(
344
356
  dbos: "DBOS",
345
357
  func: "Workflow[P, R]",
358
+ queue_name: Optional[str],
359
+ execute_workflow: bool,
346
360
  *args: P.args,
347
361
  **kwargs: P.kwargs,
348
362
  ) -> "WorkflowHandle[R]":
@@ -396,10 +410,14 @@ def _start_workflow(
396
410
  class_name=get_dbos_class_name(fi, func, gin_args),
397
411
  config_name=get_config_name(fi, func, gin_args),
398
412
  temp_wf_type=get_temp_workflow_type(func),
413
+ queue=queue_name,
399
414
  )
400
415
 
416
+ if not execute_workflow:
417
+ return _WorkflowHandlePolling(new_wf_id, dbos)
418
+
401
419
  if fself is not None:
402
- future = dbos.executor.submit(
420
+ future = dbos._executor.submit(
403
421
  cast(Callable[..., R], _execute_workflow_wthread),
404
422
  dbos,
405
423
  status,
@@ -410,7 +428,7 @@ def _start_workflow(
410
428
  **kwargs,
411
429
  )
412
430
  else:
413
- future = dbos.executor.submit(
431
+ future = dbos._executor.submit(
414
432
  cast(Callable[..., R], _execute_workflow_wthread),
415
433
  dbos,
416
434
  status,
@@ -432,7 +450,7 @@ def _transaction(
432
450
  f"Function {func.__name__} invoked before DBOS initialized"
433
451
  )
434
452
  dbos = dbosreg.dbos
435
- with dbos.app_db.sessionmaker() as session:
453
+ with dbos._app_db.sessionmaker() as session:
436
454
  attributes: TracedAttributes = {
437
455
  "name": func.__name__,
438
456
  "operationType": OperationType.TRANSACTION.value,
@@ -510,7 +528,7 @@ def _transaction(
510
528
  # Don't record the error if it was already recorded
511
529
  if not has_recorded_error:
512
530
  txn_output["error"] = utils.serialize(error)
513
- dbos.app_db.record_transaction_error(txn_output)
531
+ dbos._app_db.record_transaction_error(txn_output)
514
532
  raise
515
533
  return output
516
534
 
@@ -541,6 +559,7 @@ def _transaction(
541
559
  set_dbos_func_name(temp_wf, "<temp>." + func.__qualname__)
542
560
  set_temp_workflow_type(temp_wf, "transaction")
543
561
  dbosreg.register_wf_function(get_dbos_func_name(temp_wf), wrapped_wf)
562
+ wrapper.__orig_func = temp_wf # type: ignore
544
563
 
545
564
  return cast(F, wrapper)
546
565
 
@@ -575,7 +594,7 @@ def _step(
575
594
  "output": None,
576
595
  "error": None,
577
596
  }
578
- recorded_output = dbos.sys_db.check_operation_execution(
597
+ recorded_output = dbos._sys_db.check_operation_execution(
579
598
  ctx.workflow_id, ctx.function_id
580
599
  )
581
600
  if recorded_output:
@@ -622,7 +641,7 @@ def _step(
622
641
  step_output["error"] = (
623
642
  utils.serialize(error) if error is not None else None
624
643
  )
625
- dbos.sys_db.record_operation_result(step_output)
644
+ dbos._sys_db.record_operation_result(step_output)
626
645
 
627
646
  if error is not None:
628
647
  raise error
@@ -657,6 +676,7 @@ def _step(
657
676
  set_dbos_func_name(temp_wf, "<temp>." + func.__qualname__)
658
677
  set_temp_workflow_type(temp_wf, "step")
659
678
  dbosreg.register_wf_function(get_dbos_func_name(temp_wf), wrapped_wf)
679
+ wrapper.__orig_func = temp_wf # type: ignore
660
680
 
661
681
  return cast(F, wrapper)
662
682
 
@@ -671,7 +691,7 @@ def _send(
671
691
  "name": "send",
672
692
  }
673
693
  with EnterDBOSStep(attributes) as ctx:
674
- dbos.sys_db.send(
694
+ dbos._sys_db.send(
675
695
  ctx.workflow_id,
676
696
  ctx.curr_step_function_id,
677
697
  destination_id,
@@ -702,7 +722,7 @@ def _recv(
702
722
  with EnterDBOSStep(attributes) as ctx:
703
723
  ctx.function_id += 1 # Reserve for the sleep
704
724
  timeout_function_id = ctx.function_id
705
- return dbos.sys_db.recv(
725
+ return dbos._sys_db.recv(
706
726
  ctx.workflow_id,
707
727
  ctx.curr_step_function_id,
708
728
  timeout_function_id,
@@ -725,7 +745,7 @@ def _set_event(dbos: "DBOS", key: str, value: Any) -> None:
725
745
  "name": "set_event",
726
746
  }
727
747
  with EnterDBOSStep(attributes) as ctx:
728
- dbos.sys_db.set_event(
748
+ dbos._sys_db.set_event(
729
749
  ctx.workflow_id, ctx.curr_step_function_id, key, value
730
750
  )
731
751
  else:
@@ -753,7 +773,7 @@ def _get_event(
753
773
  "function_id": ctx.curr_step_function_id,
754
774
  "timeout_function_id": timeout_function_id,
755
775
  }
756
- return dbos.sys_db.get_event(workflow_id, key, timeout_seconds, caller_ctx)
776
+ return dbos._sys_db.get_event(workflow_id, key, timeout_seconds, caller_ctx)
757
777
  else:
758
778
  # Directly call it outside of a workflow
759
- return dbos.sys_db.get_event(workflow_id, key, timeout_seconds)
779
+ return dbos._sys_db.get_event(workflow_id, key, timeout_seconds)
dbos/dbos.py CHANGED
@@ -20,7 +20,6 @@ from typing import (
20
20
  Tuple,
21
21
  Type,
22
22
  TypeVar,
23
- cast,
24
23
  )
25
24
 
26
25
  from opentelemetry.trace import Span
@@ -40,6 +39,7 @@ from dbos.core import (
40
39
  _WorkflowHandlePolling,
41
40
  )
42
41
  from dbos.decorators import classproperty
42
+ from dbos.queue import Queue, queue_thread
43
43
  from dbos.recovery import _recover_pending_workflows, _startup_recovery_thread
44
44
  from dbos.registrations import (
45
45
  DBOSClassInfo,
@@ -138,6 +138,7 @@ class _DBOSRegistry:
138
138
  self.workflow_info_map: dict[str, Workflow[..., Any]] = {}
139
139
  self.class_info_map: dict[str, type] = {}
140
140
  self.instance_info_map: dict[str, object] = {}
141
+ self.queue_info_map: dict[str, Queue] = {}
141
142
  self.pollers: list[_RegisteredJob] = []
142
143
  self.dbos: Optional[DBOS] = None
143
144
  self.config: Optional[ConfigFile] = None
@@ -163,7 +164,7 @@ class _DBOSRegistry:
163
164
  ) -> None:
164
165
  if self.dbos and self.dbos._launched:
165
166
  self.dbos.stop_events.append(evt)
166
- self.dbos.executor.submit(func, *args, **kwargs)
167
+ self.dbos._executor.submit(func, *args, **kwargs)
167
168
  else:
168
169
  self.pollers.append((evt, func, args, kwargs))
169
170
 
@@ -265,15 +266,15 @@ class DBOS:
265
266
  dbos_logger.info("Initializing DBOS")
266
267
  self.config: ConfigFile = config
267
268
  self._launched: bool = False
268
- self._sys_db: Optional[SystemDatabase] = None
269
- self._app_db: Optional[ApplicationDatabase] = None
269
+ self._sys_db_field: Optional[SystemDatabase] = None
270
+ self._app_db_field: Optional[ApplicationDatabase] = None
270
271
  self._registry: _DBOSRegistry = _get_or_create_dbos_registry()
271
272
  self._registry.dbos = self
272
- self._admin_server: Optional[AdminServer] = None
273
+ self._admin_server_field: Optional[AdminServer] = None
273
274
  self.stop_events: List[threading.Event] = []
274
275
  self.fastapi: Optional["FastAPI"] = fastapi
275
276
  self.flask: Optional["Flask"] = flask
276
- self._executor: Optional[ThreadPoolExecutor] = None
277
+ self._executor_field: Optional[ThreadPoolExecutor] = None
277
278
 
278
279
  # If using FastAPI, set up middleware and lifecycle events
279
280
  if self.fastapi is not None:
@@ -302,33 +303,33 @@ class DBOS:
302
303
  handler.flush()
303
304
 
304
305
  @property
305
- def executor(self) -> ThreadPoolExecutor:
306
- if self._executor is None:
306
+ def _executor(self) -> ThreadPoolExecutor:
307
+ if self._executor_field is None:
307
308
  raise DBOSException("Executor accessed before DBOS was launched")
308
- rv: ThreadPoolExecutor = self._executor
309
+ rv: ThreadPoolExecutor = self._executor_field
309
310
  return rv
310
311
 
311
312
  @property
312
- def sys_db(self) -> SystemDatabase:
313
- if self._sys_db is None:
313
+ def _sys_db(self) -> SystemDatabase:
314
+ if self._sys_db_field is None:
314
315
  raise DBOSException("System database accessed before DBOS was launched")
315
- rv: SystemDatabase = self._sys_db
316
+ rv: SystemDatabase = self._sys_db_field
316
317
  return rv
317
318
 
318
319
  @property
319
- def app_db(self) -> ApplicationDatabase:
320
- if self._app_db is None:
320
+ def _app_db(self) -> ApplicationDatabase:
321
+ if self._app_db_field is None:
321
322
  raise DBOSException(
322
323
  "Application database accessed before DBOS was launched"
323
324
  )
324
- rv: ApplicationDatabase = self._app_db
325
+ rv: ApplicationDatabase = self._app_db_field
325
326
  return rv
326
327
 
327
328
  @property
328
- def admin_server(self) -> AdminServer:
329
- if self._admin_server is None:
329
+ def _admin_server(self) -> AdminServer:
330
+ if self._admin_server_field is None:
330
331
  raise DBOSException("Admin server accessed before DBOS was launched")
331
- rv: AdminServer = self._admin_server
332
+ rv: AdminServer = self._admin_server_field
332
333
  return rv
333
334
 
334
335
  @classmethod
@@ -341,25 +342,30 @@ class DBOS:
341
342
  dbos_logger.warning(f"DBOS was already launched")
342
343
  return
343
344
  self._launched = True
344
- self._executor = ThreadPoolExecutor(max_workers=64)
345
- self._sys_db = SystemDatabase(self.config)
346
- self._app_db = ApplicationDatabase(self.config)
347
- self._admin_server = AdminServer(dbos=self)
345
+ self._executor_field = ThreadPoolExecutor(max_workers=64)
346
+ self._sys_db_field = SystemDatabase(self.config)
347
+ self._app_db_field = ApplicationDatabase(self.config)
348
+ self._admin_server_field = AdminServer(dbos=self)
348
349
 
349
350
  if not os.environ.get("DBOS__VMID"):
350
- workflow_ids = self.sys_db.get_pending_workflows("local")
351
- self.executor.submit(_startup_recovery_thread, self, workflow_ids)
351
+ workflow_ids = self._sys_db.get_pending_workflows("local")
352
+ self._executor.submit(_startup_recovery_thread, self, workflow_ids)
352
353
 
353
354
  # Listen to notifications
354
- self.executor.submit(self.sys_db._notification_listener)
355
+ self._executor.submit(self._sys_db._notification_listener)
355
356
 
356
357
  # Start flush workflow buffers thread
357
- self.executor.submit(self.sys_db.flush_workflow_buffers)
358
+ self._executor.submit(self._sys_db.flush_workflow_buffers)
359
+
360
+ # Start the queue thread
361
+ evt = threading.Event()
362
+ self.stop_events.append(evt)
363
+ self._executor.submit(queue_thread, evt, self)
358
364
 
359
365
  # Grab any pollers that were deferred and start them
360
366
  for evt, func, args, kwargs in self._registry.pollers:
361
367
  self.stop_events.append(evt)
362
- self.executor.submit(func, *args, **kwargs)
368
+ self._executor.submit(func, *args, **kwargs)
363
369
  self._registry.pollers = []
364
370
 
365
371
  dbos_logger.info("DBOS launched")
@@ -374,20 +380,20 @@ class DBOS:
374
380
  self._initialized = False
375
381
  for event in self.stop_events:
376
382
  event.set()
377
- if self._sys_db is not None:
378
- self._sys_db.destroy()
379
- self._sys_db = None
380
- if self._app_db is not None:
381
- self._app_db.destroy()
382
- self._app_db = None
383
- if self._admin_server is not None:
384
- self._admin_server.stop()
385
- self._admin_server = None
383
+ if self._sys_db_field is not None:
384
+ self._sys_db_field.destroy()
385
+ self._sys_db_field = None
386
+ if self._app_db_field is not None:
387
+ self._app_db_field.destroy()
388
+ self._app_db_field = None
389
+ if self._admin_server_field is not None:
390
+ self._admin_server_field.stop()
391
+ self._admin_server_field = None
386
392
  # CB - This needs work, some things ought to stop before DBs are tossed out,
387
393
  # on the other hand it hangs to move it
388
- if self._executor is not None:
389
- self._executor.shutdown(cancel_futures=True)
390
- self._executor = None
394
+ if self._executor_field is not None:
395
+ self._executor_field.shutdown(cancel_futures=True)
396
+ self._executor_field = None
391
397
 
392
398
  @classmethod
393
399
  def register_instance(cls, inst: object) -> None:
@@ -488,13 +494,18 @@ class DBOS:
488
494
 
489
495
  @classmethod
490
496
  def kafka_consumer(
491
- cls, config: dict[str, Any], topics: list[str]
497
+ cls,
498
+ config: dict[str, Any],
499
+ topics: list[str],
500
+ in_order: bool = False,
492
501
  ) -> Callable[[KafkaConsumerWorkflow], KafkaConsumerWorkflow]:
493
502
  """Decorate a function to be used as a Kafka consumer."""
494
503
  try:
495
504
  from dbos.kafka import kafka_consumer
496
505
 
497
- return kafka_consumer(_get_or_create_dbos_registry(), config, topics)
506
+ return kafka_consumer(
507
+ _get_or_create_dbos_registry(), config, topics, in_order
508
+ )
498
509
  except ModuleNotFoundError as e:
499
510
  raise DBOSException(
500
511
  f"{e.name} dependency not found. Please install {e.name} via your package manager."
@@ -508,7 +519,7 @@ class DBOS:
508
519
  **kwargs: P.kwargs,
509
520
  ) -> WorkflowHandle[R]:
510
521
  """Invoke a workflow function in the background, returning a handle to the ongoing execution."""
511
- return _start_workflow(_get_dbos_instance(), func, *args, **kwargs)
522
+ return _start_workflow(_get_dbos_instance(), func, None, True, *args, **kwargs)
512
523
 
513
524
  @classmethod
514
525
  def get_workflow_status(cls, workflow_id: str) -> Optional[WorkflowStatus]:
@@ -516,11 +527,11 @@ class DBOS:
516
527
  ctx = get_local_dbos_context()
517
528
  if ctx and ctx.is_within_workflow():
518
529
  ctx.function_id += 1
519
- stat = _get_dbos_instance().sys_db.get_workflow_status_within_wf(
530
+ stat = _get_dbos_instance()._sys_db.get_workflow_status_within_wf(
520
531
  workflow_id, ctx.workflow_id, ctx.function_id
521
532
  )
522
533
  else:
523
- stat = _get_dbos_instance().sys_db.get_workflow_status(workflow_id)
534
+ stat = _get_dbos_instance()._sys_db.get_workflow_status(workflow_id)
524
535
  if stat is None:
525
536
  return None
526
537
 
@@ -584,7 +595,7 @@ class DBOS:
584
595
  if seconds <= 0:
585
596
  return
586
597
  with EnterDBOSStep(attributes) as ctx:
587
- _get_dbos_instance().sys_db.sleep(
598
+ _get_dbos_instance()._sys_db.sleep(
588
599
  ctx.workflow_id, ctx.curr_step_function_id, seconds
589
600
  )
590
601
 
dbos/kafka.py CHANGED
@@ -1,26 +1,30 @@
1
1
  import threading
2
- import traceback
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn, Optional, Union
2
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn
5
3
 
6
4
  from confluent_kafka import Consumer, KafkaError, KafkaException
7
- from confluent_kafka import Message as CTypeMessage
5
+
6
+ from dbos.queue import Queue
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from dbos.dbos import _DBOSRegistry
11
10
 
12
11
  from .context import SetWorkflowID
12
+ from .error import DBOSInitializationError
13
13
  from .kafka_message import KafkaMessage
14
14
  from .logger import dbos_logger
15
15
 
16
16
  KafkaConsumerWorkflow = Callable[[KafkaMessage], None]
17
17
 
18
+ kafka_queue: Queue
19
+ in_order_kafka_queues: dict[str, Queue] = {}
20
+
18
21
 
19
22
  def _kafka_consumer_loop(
20
23
  func: KafkaConsumerWorkflow,
21
24
  config: dict[str, Any],
22
25
  topics: list[str],
23
26
  stop_event: threading.Event,
27
+ in_order: bool,
24
28
  ) -> None:
25
29
 
26
30
  def on_error(err: KafkaError) -> NoReturn:
@@ -70,24 +74,35 @@ def _kafka_consumer_loop(
70
74
  with SetWorkflowID(
71
75
  f"kafka-unique-id-{msg.topic}-{msg.partition}-{msg.offset}"
72
76
  ):
73
- try:
74
- func(msg)
75
- except Exception as e:
76
- dbos_logger.error(
77
- f"Exception encountered in Kafka consumer: {traceback.format_exc()}"
78
- )
77
+ if in_order:
78
+ assert msg.topic is not None
79
+ queue = in_order_kafka_queues[msg.topic]
80
+ queue.enqueue(func, msg)
81
+ else:
82
+ kafka_queue.enqueue(func, msg)
79
83
 
80
84
  finally:
81
85
  consumer.close()
82
86
 
83
87
 
84
88
  def kafka_consumer(
85
- dbosreg: "_DBOSRegistry", config: dict[str, Any], topics: list[str]
89
+ dbosreg: "_DBOSRegistry", config: dict[str, Any], topics: list[str], in_order: bool
86
90
  ) -> Callable[[KafkaConsumerWorkflow], KafkaConsumerWorkflow]:
87
91
  def decorator(func: KafkaConsumerWorkflow) -> KafkaConsumerWorkflow:
92
+ if in_order:
93
+ for topic in topics:
94
+ if topic.startswith("^"):
95
+ raise DBOSInitializationError(
96
+ f"Error: in-order processing is not supported for regular expression topic selectors ({topic})"
97
+ )
98
+ queue = Queue(f"_dbos_kafka_queue_topic_{topic}", concurrency=1)
99
+ in_order_kafka_queues[topic] = queue
100
+ else:
101
+ global kafka_queue
102
+ kafka_queue = Queue("_dbos_internal_queue")
88
103
  stop_event = threading.Event()
89
104
  dbosreg.register_poller(
90
- stop_event, _kafka_consumer_loop, func, config, topics, stop_event
105
+ stop_event, _kafka_consumer_loop, func, config, topics, stop_event, in_order
91
106
  )
92
107
  return func
93
108
 
@@ -0,0 +1,55 @@
1
+ """job_queue
2
+
3
+ Revision ID: eab0cc1d9a14
4
+ Revises: a3b18ad34abe
5
+ Create Date: 2024-09-13 14:50:00.531294
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "eab0cc1d9a14"
16
+ down_revision: Union[str, None] = "a3b18ad34abe"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.create_table(
23
+ "job_queue",
24
+ sa.Column("workflow_uuid", sa.Text(), nullable=False),
25
+ sa.Column("queue_name", sa.Text(), nullable=False),
26
+ sa.Column(
27
+ "created_at_epoch_ms",
28
+ sa.BigInteger(),
29
+ server_default=sa.text(
30
+ "(EXTRACT(epoch FROM now()) * 1000::numeric)::bigint"
31
+ ),
32
+ nullable=False,
33
+ primary_key=True,
34
+ ),
35
+ sa.ForeignKeyConstraint(
36
+ ["workflow_uuid"],
37
+ ["dbos.workflow_status.workflow_uuid"],
38
+ onupdate="CASCADE",
39
+ ondelete="CASCADE",
40
+ ),
41
+ schema="dbos",
42
+ )
43
+ op.add_column(
44
+ "workflow_status",
45
+ sa.Column(
46
+ "queue_name",
47
+ sa.Text(),
48
+ ),
49
+ schema="dbos",
50
+ )
51
+
52
+
53
+ def downgrade() -> None:
54
+ op.drop_table("job_queue", schema="dbos")
55
+ op.drop_column("workflow_status", "queue_name", schema="dbos")
dbos/queue.py ADDED
@@ -0,0 +1,36 @@
1
+ import threading
2
+ import time
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from dbos.core import P, R, _execute_workflow_id, _start_workflow
6
+ from dbos.error import DBOSInitializationError
7
+
8
+ if TYPE_CHECKING:
9
+ from dbos.dbos import DBOS, Workflow, WorkflowHandle
10
+
11
+
12
+ class Queue:
13
+ def __init__(self, name: str, concurrency: Optional[int] = None) -> None:
14
+ self.name = name
15
+ self.concurrency = concurrency
16
+ from dbos.dbos import _get_or_create_dbos_registry
17
+
18
+ registry = _get_or_create_dbos_registry()
19
+ registry.queue_info_map[self.name] = self
20
+
21
+ def enqueue(
22
+ self, func: "Workflow[P, R]", *args: P.args, **kwargs: P.kwargs
23
+ ) -> "WorkflowHandle[R]":
24
+ from dbos.dbos import _get_dbos_instance
25
+
26
+ dbos = _get_dbos_instance()
27
+ return _start_workflow(dbos, func, self.name, False, *args, **kwargs)
28
+
29
+
30
+ def queue_thread(stop_event: threading.Event, dbos: "DBOS") -> None:
31
+ while not stop_event.is_set():
32
+ time.sleep(1)
33
+ for queue_name, queue in dbos._registry.queue_info_map.items():
34
+ wf_ids = dbos._sys_db.start_queued_workflows(queue_name, queue.concurrency)
35
+ for id in wf_ids:
36
+ _execute_workflow_id(dbos, id)
dbos/recovery.py CHANGED
@@ -41,7 +41,7 @@ def _recover_pending_workflows(
41
41
  f"Skip local recovery because it's running in a VM: {os.environ.get('DBOS__VMID')}"
42
42
  )
43
43
  dbos.logger.debug(f"Recovering pending workflows for executor: {executor_id}")
44
- workflow_ids = dbos.sys_db.get_pending_workflows(executor_id)
44
+ workflow_ids = dbos._sys_db.get_pending_workflows(executor_id)
45
45
  dbos.logger.debug(f"Pending workflows: {workflow_ids}")
46
46
 
47
47
  for workflowID in workflow_ids:
@@ -1,17 +1,19 @@
1
1
  import threading
2
- import traceback
3
2
  from datetime import datetime, timezone
4
3
  from typing import TYPE_CHECKING, Callable
5
4
 
5
+ from dbos.queue import Queue
6
+
6
7
  if TYPE_CHECKING:
7
8
  from dbos.dbos import _DBOSRegistry
8
9
 
9
10
  from ..context import SetWorkflowID
10
- from ..logger import dbos_logger
11
11
  from .croniter import croniter # type: ignore
12
12
 
13
13
  ScheduledWorkflow = Callable[[datetime, datetime], None]
14
14
 
15
+ scheduler_queue: Queue
16
+
15
17
 
16
18
  def scheduler_loop(
17
19
  func: ScheduledWorkflow, cron: str, stop_event: threading.Event
@@ -23,19 +25,15 @@ def scheduler_loop(
23
25
  if stop_event.wait(timeout=sleepTime.total_seconds()):
24
26
  return
25
27
  with SetWorkflowID(f"sched-{func.__qualname__}-{nextExecTime.isoformat()}"):
26
- try:
27
- func(nextExecTime, datetime.now(timezone.utc))
28
- except Exception as e:
29
- dbos_logger.error(
30
- f"Exception encountered in scheduled workflow: {traceback.format_exc()}"
31
- )
32
- pass # Let the thread keep running
28
+ scheduler_queue.enqueue(func, nextExecTime, datetime.now(timezone.utc))
33
29
 
34
30
 
35
31
  def scheduled(
36
32
  dbosreg: "_DBOSRegistry", cron: str
37
33
  ) -> Callable[[ScheduledWorkflow], ScheduledWorkflow]:
38
34
  def decorator(func: ScheduledWorkflow) -> ScheduledWorkflow:
35
+ global scheduler_queue
36
+ scheduler_queue = Queue("_dbos_internal_queue")
39
37
  stop_event = threading.Event()
40
38
  dbosreg.register_poller(stop_event, scheduler_loop, func, cron, stop_event)
41
39
  return func
@@ -1,5 +1,6 @@
1
1
  from sqlalchemy import (
2
2
  BigInteger,
3
+ Boolean,
3
4
  Column,
4
5
  ForeignKey,
5
6
  Index,
@@ -53,6 +54,7 @@ class SystemSchema:
53
54
  nullable=True,
54
55
  server_default=text("'0'::bigint"),
55
56
  ),
57
+ Column("queue_name", Text),
56
58
  Index("workflow_status_created_at_index", "created_at"),
57
59
  Index("workflow_status_executor_id_index", "executor_id"),
58
60
  )
@@ -139,3 +141,24 @@ class SystemSchema:
139
141
  Column("workflow_fn_name", Text, primary_key=True, nullable=False),
140
142
  Column("last_run_time", BigInteger, nullable=False),
141
143
  )
144
+
145
+ job_queue = Table(
146
+ "job_queue",
147
+ metadata_obj,
148
+ Column(
149
+ "workflow_uuid",
150
+ Text,
151
+ ForeignKey(
152
+ "workflow_status.workflow_uuid", onupdate="CASCADE", ondelete="CASCADE"
153
+ ),
154
+ nullable=False,
155
+ primary_key=True,
156
+ ),
157
+ Column("queue_name", Text, nullable=False),
158
+ Column(
159
+ "created_at_epoch_ms",
160
+ BigInteger,
161
+ nullable=False,
162
+ server_default=text("(EXTRACT(epoch FROM now()) * 1000::numeric)::bigint"),
163
+ ),
164
+ )
dbos/system_database.py CHANGED
@@ -33,10 +33,11 @@ class WorkflowStatusString(Enum):
33
33
  ERROR = "ERROR"
34
34
  RETRIES_EXCEEDED = "RETRIES_EXCEEDED"
35
35
  CANCELLED = "CANCELLED"
36
+ ENQUEUED = "ENQUEUED"
36
37
 
37
38
 
38
39
  WorkflowStatuses = Literal[
39
- "PENDING", "SUCCESS", "ERROR", "RETRIES_EXCEEDED", "CANCELLED"
40
+ "PENDING", "SUCCESS", "ERROR", "RETRIES_EXCEEDED", "CANCELLED", "ENQUEUED"
40
41
  ]
41
42
 
42
43
 
@@ -61,6 +62,7 @@ class WorkflowStatusInternal(TypedDict):
61
62
  authenticated_user: Optional[str]
62
63
  assumed_role: Optional[str]
63
64
  authenticated_roles: Optional[str] # JSON list of roles.
65
+ queue_name: Optional[str]
64
66
 
65
67
 
66
68
  class RecordedResult(TypedDict):
@@ -247,6 +249,7 @@ class SystemDatabase:
247
249
  authenticated_user=status["authenticated_user"],
248
250
  authenticated_roles=status["authenticated_roles"],
249
251
  assumed_role=status["assumed_role"],
252
+ queue_name=status["queue_name"],
250
253
  )
251
254
  if replace:
252
255
  cmd = cmd.on_conflict_do_update(
@@ -320,6 +323,7 @@ class SystemDatabase:
320
323
  SystemSchema.workflow_status.c.authenticated_user,
321
324
  SystemSchema.workflow_status.c.authenticated_roles,
322
325
  SystemSchema.workflow_status.c.assumed_role,
326
+ SystemSchema.workflow_status.c.queue_name,
323
327
  ).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
324
328
  ).fetchone()
325
329
  if row is None:
@@ -340,6 +344,7 @@ class SystemDatabase:
340
344
  "authenticated_user": row[6],
341
345
  "authenticated_roles": row[7],
342
346
  "assumed_role": row[8],
347
+ "queue_name": row[9],
343
348
  }
344
349
  return status
345
350
 
@@ -379,6 +384,7 @@ class SystemDatabase:
379
384
  SystemSchema.workflow_status.c.authenticated_user,
380
385
  SystemSchema.workflow_status.c.authenticated_roles,
381
386
  SystemSchema.workflow_status.c.assumed_role,
387
+ SystemSchema.workflow_status.c.queue_name,
382
388
  ).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
383
389
  ).fetchone()
384
390
  if row is None:
@@ -399,6 +405,7 @@ class SystemDatabase:
399
405
  "authenticated_user": row[7],
400
406
  "authenticated_roles": row[8],
401
407
  "assumed_role": row[9],
408
+ "queue_name": row[10],
402
409
  }
403
410
  return status
404
411
 
@@ -1010,3 +1017,50 @@ class SystemDatabase:
1010
1017
  len(self._workflow_status_buffer) == 0
1011
1018
  and len(self._workflow_inputs_buffer) == 0
1012
1019
  )
1020
+
1021
+ def enqueue(self, workflow_id: str, queue_name: str) -> None:
1022
+ with self.engine.begin() as c:
1023
+ c.execute(
1024
+ pg.insert(SystemSchema.job_queue)
1025
+ .values(
1026
+ workflow_uuid=workflow_id,
1027
+ queue_name=queue_name,
1028
+ )
1029
+ .on_conflict_do_nothing()
1030
+ )
1031
+
1032
+ def start_queued_workflows(
1033
+ self, queue_name: str, concurrency: Optional[int]
1034
+ ) -> List[str]:
1035
+ with self.engine.begin() as c:
1036
+ query = sa.select(SystemSchema.job_queue.c.workflow_uuid).where(
1037
+ SystemSchema.job_queue.c.queue_name == queue_name
1038
+ )
1039
+ if concurrency is not None:
1040
+ query = query.order_by(
1041
+ SystemSchema.job_queue.c.created_at_epoch_ms.asc()
1042
+ ).limit(concurrency)
1043
+ rows = c.execute(query).fetchall()
1044
+ dequeued_ids: List[str] = [row[0] for row in rows]
1045
+ ret_ids = []
1046
+ for id in dequeued_ids:
1047
+ result = c.execute(
1048
+ SystemSchema.workflow_status.update()
1049
+ .where(SystemSchema.workflow_status.c.workflow_uuid == id)
1050
+ .where(
1051
+ SystemSchema.workflow_status.c.status
1052
+ == WorkflowStatusString.ENQUEUED.value
1053
+ )
1054
+ .values(status=WorkflowStatusString.PENDING.value)
1055
+ )
1056
+ if result.rowcount > 0:
1057
+ ret_ids.append(id)
1058
+ return ret_ids
1059
+
1060
+ def remove_from_queue(self, workflow_id: str) -> None:
1061
+ with self.engine.begin() as c:
1062
+ c.execute(
1063
+ sa.delete(SystemSchema.job_queue).where(
1064
+ SystemSchema.job_queue.c.workflow_uuid == workflow_id
1065
+ )
1066
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.7.0a5
3
+ Version: 0.7.0a8
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,38 +1,40 @@
1
- dbos-0.7.0a5.dist-info/METADATA,sha256=GpkOCHTB3NQUEMNI2bezOCYqBYMnn1FUzSemILcJBF0,5010
2
- dbos-0.7.0a5.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
- dbos-0.7.0a5.dist-info/entry_points.txt,sha256=3PmOPbM4FYxEmggRRdJw0oAsiBzKR8U0yx7bmwUmMOM,39
4
- dbos-0.7.0a5.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
- dbos/__init__.py,sha256=heuB3bqRXlVdfea9sKHBIVKSqFP6UuwhQecQfV4hyas,642
1
+ dbos-0.7.0a8.dist-info/METADATA,sha256=WCNkAofLXq_TaFlZzWzLRCBhYqN1JeaOE7zydAtR5TU,5010
2
+ dbos-0.7.0a8.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
3
+ dbos-0.7.0a8.dist-info/entry_points.txt,sha256=3PmOPbM4FYxEmggRRdJw0oAsiBzKR8U0yx7bmwUmMOM,39
4
+ dbos-0.7.0a8.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
+ dbos/__init__.py,sha256=jjlBFzSAzO2e-LD5IKJw7bRqZjrxpF5Sn-_JUJJptHU,680
6
6
  dbos/admin_sever.py,sha256=Qg5T3YRrbPW05PR_99yAaxgo1ugQrAp_uTeTqSfjm_k,3397
7
7
  dbos/application_database.py,sha256=knFK8We8y6WrIpnFCKvFq5hvSuFQqUuJqOqDpSVMCPI,5521
8
8
  dbos/cli.py,sha256=z5dXbbnGWzSC3E1rfS8Lp1_OIImzcDKM7jP-iu_Q4aI,8602
9
- dbos/context.py,sha256=NVMGyvAa2RIiBVspvDz-8MBk_BQyGyYdPdorgO-GSng,16407
10
- dbos/core.py,sha256=av2O5BsUH5UqQ6ICuH63RgAJFCjBkSHPKchbX_KtKcc,28560
9
+ dbos/context.py,sha256=1Xp0i-ZP72Vj6eMdHuyfXi9RHnoT2w4MZ2Kx1CrKnQ8,16559
10
+ dbos/core.py,sha256=LnQRHLjfpdlzlRkElduay8nObNMyRNC8SJSZEzqei7o,29161
11
11
  dbos/dbos-config.schema.json,sha256=azpfmoDZg7WfSy3kvIsk9iEiKB_-VZt03VEOoXJAkqE,5331
12
- dbos/dbos.py,sha256=SnY5CSHLnrjx128wGaaB-4zC4VU2basCMLZq9hXrAIg,28768
12
+ dbos/dbos.py,sha256=RtDcvKe4sm1TlnCGU4cyex-UI7hMMlhgzmOl1NuRLo4,29294
13
13
  dbos/dbos_config.py,sha256=NJVze2GkKgYUmcPP31Unb-QpsA0TzImEeQGJgVq6W6k,5352
14
14
  dbos/decorators.py,sha256=lbPefsLK6Cya4cb7TrOcLglOpGT3pc6qjZdsQKlfZLg,629
15
15
  dbos/error.py,sha256=DDhB0VHmoZE_CP51ICdFMZSL2gmVS3Dm0aPNWncci94,3876
16
16
  dbos/fastapi.py,sha256=gx9hlpxYOiwbuhSlbY9bn5C-F_FsCbrJvkX9ZAvDG6U,3418
17
17
  dbos/flask.py,sha256=azr4geMEGuuTBCyxIZmgDmmP-6s_pTIF-lGyp9Q4IB8,2430
18
- dbos/kafka.py,sha256=FtngQHBu2TKfyDF7GFsKJAawFQJiOFxgKEUlNNxrdrw,3055
18
+ dbos/kafka.py,sha256=LH3hbNapnkjLcuXNUtdGU0398JafWb-t0GwUl3LOzkc,3645
19
19
  dbos/kafka_message.py,sha256=NYvOXNG3Qn7bghn1pv3fg4Pbs86ILZGcK4IB-MLUNu0,409
20
20
  dbos/logger.py,sha256=D-aFSZUCHBP34J1IZ5YNkTrJW-rDiH3py_v9jLU4Yrk,3565
21
21
  dbos/migrations/env.py,sha256=38SIGVbmn_VV2x2u1aHLcPOoWgZ84eCymf3g_NljmbU,1626
22
22
  dbos/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
23
23
  dbos/migrations/versions/5c361fc04708_added_system_tables.py,sha256=QMgFMb0aLgC25YicsvPSr6AHRCA6Zd66hyaRUhwKzrQ,6404
24
24
  dbos/migrations/versions/a3b18ad34abe_added_triggers.py,sha256=Rv0ZsZYZ_WdgGEULYsPfnp4YzaO5L198gDTgYY39AVA,2022
25
+ dbos/migrations/versions/eab0cc1d9a14_job_queue.py,sha256=_9-FCW-zOpCQfblTS_yRLtFiUaWlC1tM4BoKBTDeH9k,1395
25
26
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
26
- dbos/recovery.py,sha256=xfwQFWNuD8DXg5HD5_-3tG7Neo9j-x1lrqiwtn5FSh8,2015
27
+ dbos/queue.py,sha256=5unMPX1ThoVWfXOR2IUSeIjgATgcX_YmZS3Fz1Uoc7o,1228
28
+ dbos/recovery.py,sha256=zqtO_ExGoIErLMVnbneU3VeHLVWvhV4jnfqssAVlQQk,2016
27
29
  dbos/registrations.py,sha256=gMI-u05tv5bpvyddQGtoUgCsqARx51aOY7p0JXPafQo,6539
28
30
  dbos/request.py,sha256=-FIwtknayvRl6OjvqO4V2GySVzSdP1Ft3cc9ZBS-PLY,928
29
31
  dbos/roles.py,sha256=7Lh7uwUq1dpa6TXCOHre4mPTd5qmXzK_QPkvYR52DXg,2285
30
32
  dbos/scheduler/croniter.py,sha256=hbhgfsHBqclUS8VeLnJ9PSE9Z54z6mi4nnrr1aUXn0k,47561
31
- dbos/scheduler/scheduler.py,sha256=ks5zdOtt524TIuquh4u1ZwmDNWzguOpNLeFZ8bWTeUA,1498
33
+ dbos/scheduler/scheduler.py,sha256=Sz4EIpAtur7so2YajTic64GrTpa4qPw8QxXn0M34v80,1360
32
34
  dbos/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
35
  dbos/schemas/application_database.py,sha256=q_Wr2XbiZNBYFkOtu7uKavo1T_cSOBblxKGHThYGGsY,962
34
- dbos/schemas/system_database.py,sha256=5V3vqnEzry0Hn7ZbVS9Gs_dJKia8uX8p7mGC82Ru8rk,4303
35
- dbos/system_database.py,sha256=Jf2O1O7mM0_9xZSSBqRKpi4uPtjsNkq9FlilzR5vQis,39796
36
+ dbos/schemas/system_database.py,sha256=ed4c1UntsD-cqXD0ekM4jvcYYEViavDh_G6c0pVDe7k,4938
37
+ dbos/system_database.py,sha256=y59uFRWsNlp_tBSyZpq4geXDAC1Vii6074iFQZkgltY,41974
36
38
  dbos/templates/hello/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
37
39
  dbos/templates/hello/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
40
  dbos/templates/hello/__package/main.py,sha256=eI0SS9Nwj-fldtiuSzIlIG6dC91GXXwdRsoHxv6S_WI,2719
@@ -46,4 +48,4 @@ dbos/templates/hello/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKs
46
48
  dbos/tracer.py,sha256=GaXDhdKKF_IQp5SAMipGXiDVwteRKjNbrXyYCH1mor0,2520
47
49
  dbos/utils.py,sha256=hWj9iWDrby2cVEhb0pG-IdnrxLqP64NhkaWUXiLc8bA,402
48
50
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
49
- dbos-0.7.0a5.dist-info/RECORD,,
51
+ dbos-0.7.0a8.dist-info/RECORD,,
File without changes