dbos 0.24.1__py3-none-any.whl → 0.25.0__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 +5 -1
- dbos/__main__.py +3 -0
- dbos/_admin_server.py +28 -2
- dbos/_app_db.py +14 -15
- dbos/_client.py +206 -0
- dbos/_conductor/conductor.py +33 -2
- dbos/_conductor/protocol.py +47 -7
- dbos/_context.py +48 -0
- dbos/_core.py +173 -48
- dbos/_db_wizard.py +3 -7
- dbos/_dbos.py +134 -85
- dbos/_fastapi.py +4 -1
- dbos/_logger.py +14 -0
- dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +46 -0
- dbos/_outcome.py +6 -2
- dbos/_queue.py +5 -5
- dbos/_schemas/system_database.py +2 -0
- dbos/_sys_db.py +159 -178
- dbos/_templates/dbos-db-starter/__package/main.py +6 -11
- dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +2 -4
- dbos/_workflow_commands.py +90 -63
- dbos/cli/_template_init.py +8 -3
- dbos/cli/cli.py +22 -6
- {dbos-0.24.1.dist-info → dbos-0.25.0.dist-info}/METADATA +2 -1
- {dbos-0.24.1.dist-info → dbos-0.25.0.dist-info}/RECORD +28 -26
- {dbos-0.24.1.dist-info → dbos-0.25.0.dist-info}/WHEEL +1 -1
- {dbos-0.24.1.dist-info → dbos-0.25.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.24.1.dist-info → dbos-0.25.0.dist-info}/licenses/LICENSE +0 -0
dbos/_sys_db.py
CHANGED
|
@@ -28,11 +28,11 @@ from sqlalchemy.sql import func
|
|
|
28
28
|
from dbos._utils import GlobalParams
|
|
29
29
|
|
|
30
30
|
from . import _serialization
|
|
31
|
-
from .
|
|
31
|
+
from ._context import get_local_dbos_context
|
|
32
|
+
from ._dbos_config import ConfigFile, DatabaseConfig
|
|
32
33
|
from ._error import (
|
|
33
34
|
DBOSConflictingWorkflowError,
|
|
34
35
|
DBOSDeadLetterQueueError,
|
|
35
|
-
DBOSException,
|
|
36
36
|
DBOSNonExistentWorkflowError,
|
|
37
37
|
DBOSWorkflowConflictIDError,
|
|
38
38
|
)
|
|
@@ -89,6 +89,7 @@ class RecordedResult(TypedDict):
|
|
|
89
89
|
class OperationResultInternal(TypedDict):
|
|
90
90
|
workflow_uuid: str
|
|
91
91
|
function_id: int
|
|
92
|
+
function_name: str
|
|
92
93
|
output: Optional[str] # JSON (jsonpickle)
|
|
93
94
|
error: Optional[str] # JSON (jsonpickle)
|
|
94
95
|
|
|
@@ -114,7 +115,7 @@ class GetWorkflowsInput:
|
|
|
114
115
|
self.authenticated_user: Optional[str] = None # The user who ran the workflow.
|
|
115
116
|
self.start_time: Optional[str] = None # Timestamp in ISO 8601 format
|
|
116
117
|
self.end_time: Optional[str] = None # Timestamp in ISO 8601 format
|
|
117
|
-
self.status: Optional[
|
|
118
|
+
self.status: Optional[str] = None
|
|
118
119
|
self.application_version: Optional[str] = (
|
|
119
120
|
None # The application version that ran this workflow. = None
|
|
120
121
|
)
|
|
@@ -151,30 +152,39 @@ class GetPendingWorkflowsOutput:
|
|
|
151
152
|
self.queue_name: Optional[str] = queue_name
|
|
152
153
|
|
|
153
154
|
|
|
155
|
+
class StepInfo(TypedDict):
|
|
156
|
+
# The unique ID of the step in the workflow
|
|
157
|
+
function_id: int
|
|
158
|
+
# The (fully qualified) name of the step
|
|
159
|
+
function_name: str
|
|
160
|
+
# The step's output, if any
|
|
161
|
+
output: Optional[Any]
|
|
162
|
+
# The error the step threw, if any
|
|
163
|
+
error: Optional[Exception]
|
|
164
|
+
# If the step starts or retrieves the result of a workflow, its ID
|
|
165
|
+
child_workflow_id: Optional[str]
|
|
166
|
+
|
|
167
|
+
|
|
154
168
|
_dbos_null_topic = "__null__topic__"
|
|
155
|
-
_buffer_flush_batch_size = 100
|
|
156
|
-
_buffer_flush_interval_secs = 1.0
|
|
157
169
|
|
|
158
170
|
|
|
159
171
|
class SystemDatabase:
|
|
160
172
|
|
|
161
|
-
def __init__(self,
|
|
162
|
-
self.config = config
|
|
163
|
-
|
|
173
|
+
def __init__(self, database: DatabaseConfig, *, debug_mode: bool = False):
|
|
164
174
|
sysdb_name = (
|
|
165
|
-
|
|
166
|
-
if "sys_db_name" in
|
|
167
|
-
else
|
|
175
|
+
database["sys_db_name"]
|
|
176
|
+
if "sys_db_name" in database and database["sys_db_name"]
|
|
177
|
+
else database["app_db_name"] + SystemSchema.sysdb_suffix
|
|
168
178
|
)
|
|
169
179
|
|
|
170
180
|
if not debug_mode:
|
|
171
181
|
# If the system database does not already exist, create it
|
|
172
182
|
postgres_db_url = sa.URL.create(
|
|
173
183
|
"postgresql+psycopg",
|
|
174
|
-
username=
|
|
175
|
-
password=
|
|
176
|
-
host=
|
|
177
|
-
port=
|
|
184
|
+
username=database["username"],
|
|
185
|
+
password=database["password"],
|
|
186
|
+
host=database["hostname"],
|
|
187
|
+
port=database["port"],
|
|
178
188
|
database="postgres",
|
|
179
189
|
# fills the "application_name" column in pg_stat_activity
|
|
180
190
|
query={"application_name": f"dbos_transact_{GlobalParams.executor_id}"},
|
|
@@ -191,19 +201,23 @@ class SystemDatabase:
|
|
|
191
201
|
|
|
192
202
|
system_db_url = sa.URL.create(
|
|
193
203
|
"postgresql+psycopg",
|
|
194
|
-
username=
|
|
195
|
-
password=
|
|
196
|
-
host=
|
|
197
|
-
port=
|
|
204
|
+
username=database["username"],
|
|
205
|
+
password=database["password"],
|
|
206
|
+
host=database["hostname"],
|
|
207
|
+
port=database["port"],
|
|
198
208
|
database=sysdb_name,
|
|
199
209
|
# fills the "application_name" column in pg_stat_activity
|
|
200
210
|
query={"application_name": f"dbos_transact_{GlobalParams.executor_id}"},
|
|
201
211
|
)
|
|
202
212
|
|
|
203
213
|
# Create a connection pool for the system database
|
|
214
|
+
pool_size = database.get("sys_db_pool_size")
|
|
215
|
+
if pool_size is None:
|
|
216
|
+
pool_size = 20
|
|
217
|
+
|
|
204
218
|
self.engine = sa.create_engine(
|
|
205
219
|
system_db_url,
|
|
206
|
-
pool_size=
|
|
220
|
+
pool_size=pool_size,
|
|
207
221
|
max_overflow=0,
|
|
208
222
|
pool_timeout=30,
|
|
209
223
|
connect_args={"connect_timeout": 10},
|
|
@@ -250,32 +264,17 @@ class SystemDatabase:
|
|
|
250
264
|
self.notifications_map: Dict[str, threading.Condition] = {}
|
|
251
265
|
self.workflow_events_map: Dict[str, threading.Condition] = {}
|
|
252
266
|
|
|
253
|
-
# Initialize the workflow status and inputs buffers
|
|
254
|
-
self._workflow_status_buffer: Dict[str, WorkflowStatusInternal] = {}
|
|
255
|
-
self._workflow_inputs_buffer: Dict[str, str] = {}
|
|
256
|
-
# Two sets for tracking which single-transaction workflows have been exported to the status table
|
|
257
|
-
self._exported_temp_txn_wf_status: Set[str] = set()
|
|
258
|
-
self._temp_txn_wf_ids: Set[str] = set()
|
|
259
|
-
self._is_flushing_status_buffer = False
|
|
260
|
-
|
|
261
267
|
# Now we can run background processes
|
|
262
268
|
self._run_background_processes = True
|
|
263
269
|
self._debug_mode = debug_mode
|
|
264
270
|
|
|
265
271
|
# Destroy the pool when finished
|
|
266
272
|
def destroy(self) -> None:
|
|
267
|
-
self.wait_for_buffer_flush()
|
|
268
273
|
self._run_background_processes = False
|
|
269
274
|
if self.notification_conn is not None:
|
|
270
275
|
self.notification_conn.close()
|
|
271
276
|
self.engine.dispose()
|
|
272
277
|
|
|
273
|
-
def wait_for_buffer_flush(self) -> None:
|
|
274
|
-
# Wait until the buffers are flushed.
|
|
275
|
-
while self._is_flushing_status_buffer or not self._is_buffers_empty:
|
|
276
|
-
dbos_logger.debug("Waiting for system buffers to be exported")
|
|
277
|
-
time.sleep(1)
|
|
278
|
-
|
|
279
278
|
def insert_workflow_status(
|
|
280
279
|
self,
|
|
281
280
|
status: WorkflowStatusInternal,
|
|
@@ -426,10 +425,6 @@ class SystemDatabase:
|
|
|
426
425
|
with self.engine.begin() as c:
|
|
427
426
|
c.execute(cmd)
|
|
428
427
|
|
|
429
|
-
# If this is a single-transaction workflow, record that its status has been exported
|
|
430
|
-
if status["workflow_uuid"] in self._temp_txn_wf_ids:
|
|
431
|
-
self._exported_temp_txn_wf_status.add(status["workflow_uuid"])
|
|
432
|
-
|
|
433
428
|
def cancel_workflow(
|
|
434
429
|
self,
|
|
435
430
|
workflow_id: str,
|
|
@@ -531,31 +526,6 @@ class SystemDatabase:
|
|
|
531
526
|
}
|
|
532
527
|
return status
|
|
533
528
|
|
|
534
|
-
def get_workflow_status_within_wf(
|
|
535
|
-
self, workflow_uuid: str, calling_wf: str, calling_wf_fn: int
|
|
536
|
-
) -> Optional[WorkflowStatusInternal]:
|
|
537
|
-
res = self.check_operation_execution(calling_wf, calling_wf_fn)
|
|
538
|
-
if res is not None:
|
|
539
|
-
if res["output"]:
|
|
540
|
-
resstat: WorkflowStatusInternal = _serialization.deserialize(
|
|
541
|
-
res["output"]
|
|
542
|
-
)
|
|
543
|
-
return resstat
|
|
544
|
-
else:
|
|
545
|
-
raise DBOSException(
|
|
546
|
-
"Workflow status record not found. This should not happen! \033[1m Hint: Check if your workflow is deterministic.\033[0m"
|
|
547
|
-
)
|
|
548
|
-
stat = self.get_workflow_status(workflow_uuid)
|
|
549
|
-
self.record_operation_result(
|
|
550
|
-
{
|
|
551
|
-
"workflow_uuid": calling_wf,
|
|
552
|
-
"function_id": calling_wf_fn,
|
|
553
|
-
"output": _serialization.serialize(stat),
|
|
554
|
-
"error": None,
|
|
555
|
-
}
|
|
556
|
-
)
|
|
557
|
-
return stat
|
|
558
|
-
|
|
559
529
|
def await_workflow_result_internal(self, workflow_uuid: str) -> dict[str, Any]:
|
|
560
530
|
polling_interval_secs: float = 1.000
|
|
561
531
|
|
|
@@ -632,10 +602,7 @@ class SystemDatabase:
|
|
|
632
602
|
f"Workflow {workflow_uuid} has been called multiple times with different inputs"
|
|
633
603
|
)
|
|
634
604
|
# TODO: actually changing the input
|
|
635
|
-
|
|
636
|
-
# Clean up the single-transaction tracking sets
|
|
637
|
-
self._exported_temp_txn_wf_status.discard(workflow_uuid)
|
|
638
|
-
self._temp_txn_wf_ids.discard(workflow_uuid)
|
|
605
|
+
|
|
639
606
|
return
|
|
640
607
|
|
|
641
608
|
def get_workflow_inputs(
|
|
@@ -771,6 +738,36 @@ class SystemDatabase:
|
|
|
771
738
|
for row in rows
|
|
772
739
|
]
|
|
773
740
|
|
|
741
|
+
def get_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
|
|
742
|
+
with self.engine.begin() as c:
|
|
743
|
+
rows = c.execute(
|
|
744
|
+
sa.select(
|
|
745
|
+
SystemSchema.operation_outputs.c.function_id,
|
|
746
|
+
SystemSchema.operation_outputs.c.function_name,
|
|
747
|
+
SystemSchema.operation_outputs.c.output,
|
|
748
|
+
SystemSchema.operation_outputs.c.error,
|
|
749
|
+
SystemSchema.operation_outputs.c.child_workflow_id,
|
|
750
|
+
).where(SystemSchema.operation_outputs.c.workflow_uuid == workflow_id)
|
|
751
|
+
).fetchall()
|
|
752
|
+
return [
|
|
753
|
+
StepInfo(
|
|
754
|
+
function_id=row[0],
|
|
755
|
+
function_name=row[1],
|
|
756
|
+
output=(
|
|
757
|
+
_serialization.deserialize(row[2])
|
|
758
|
+
if row[2] is not None
|
|
759
|
+
else row[2]
|
|
760
|
+
),
|
|
761
|
+
error=(
|
|
762
|
+
_serialization.deserialize_exception(row[3])
|
|
763
|
+
if row[3] is not None
|
|
764
|
+
else row[3]
|
|
765
|
+
),
|
|
766
|
+
child_workflow_id=row[4],
|
|
767
|
+
)
|
|
768
|
+
for row in rows
|
|
769
|
+
]
|
|
770
|
+
|
|
774
771
|
def record_operation_result(
|
|
775
772
|
self, result: OperationResultInternal, conn: Optional[sa.Connection] = None
|
|
776
773
|
) -> None:
|
|
@@ -782,6 +779,7 @@ class SystemDatabase:
|
|
|
782
779
|
sql = pg.insert(SystemSchema.operation_outputs).values(
|
|
783
780
|
workflow_uuid=result["workflow_uuid"],
|
|
784
781
|
function_id=result["function_id"],
|
|
782
|
+
function_name=result["function_name"],
|
|
785
783
|
output=output,
|
|
786
784
|
error=error,
|
|
787
785
|
)
|
|
@@ -796,6 +794,55 @@ class SystemDatabase:
|
|
|
796
794
|
raise DBOSWorkflowConflictIDError(result["workflow_uuid"])
|
|
797
795
|
raise
|
|
798
796
|
|
|
797
|
+
def record_get_result(
|
|
798
|
+
self, result_workflow_id: str, output: Optional[str], error: Optional[str]
|
|
799
|
+
) -> None:
|
|
800
|
+
ctx = get_local_dbos_context()
|
|
801
|
+
# Only record get_result called in workflow functions
|
|
802
|
+
if ctx is None or not ctx.is_workflow():
|
|
803
|
+
return
|
|
804
|
+
ctx.function_id += 1 # Record the get_result as a step
|
|
805
|
+
# Because there's no corresponding check, we do nothing on conflict
|
|
806
|
+
# and do not raise a DBOSWorkflowConflictIDError
|
|
807
|
+
sql = (
|
|
808
|
+
pg.insert(SystemSchema.operation_outputs)
|
|
809
|
+
.values(
|
|
810
|
+
workflow_uuid=ctx.workflow_id,
|
|
811
|
+
function_id=ctx.function_id,
|
|
812
|
+
function_name="DBOS.getResult",
|
|
813
|
+
output=output,
|
|
814
|
+
error=error,
|
|
815
|
+
child_workflow_id=result_workflow_id,
|
|
816
|
+
)
|
|
817
|
+
.on_conflict_do_nothing()
|
|
818
|
+
)
|
|
819
|
+
with self.engine.begin() as c:
|
|
820
|
+
c.execute(sql)
|
|
821
|
+
|
|
822
|
+
def record_child_workflow(
|
|
823
|
+
self,
|
|
824
|
+
parentUUID: str,
|
|
825
|
+
childUUID: str,
|
|
826
|
+
functionID: int,
|
|
827
|
+
functionName: str,
|
|
828
|
+
) -> None:
|
|
829
|
+
if self._debug_mode:
|
|
830
|
+
raise Exception("called record_child_workflow in debug mode")
|
|
831
|
+
|
|
832
|
+
sql = pg.insert(SystemSchema.operation_outputs).values(
|
|
833
|
+
workflow_uuid=parentUUID,
|
|
834
|
+
function_id=functionID,
|
|
835
|
+
function_name=functionName,
|
|
836
|
+
child_workflow_id=childUUID,
|
|
837
|
+
)
|
|
838
|
+
try:
|
|
839
|
+
with self.engine.begin() as c:
|
|
840
|
+
c.execute(sql)
|
|
841
|
+
except DBAPIError as dbapi_error:
|
|
842
|
+
if dbapi_error.orig.sqlstate == "23505": # type: ignore
|
|
843
|
+
raise DBOSWorkflowConflictIDError(parentUUID)
|
|
844
|
+
raise
|
|
845
|
+
|
|
799
846
|
def check_operation_execution(
|
|
800
847
|
self, workflow_uuid: str, function_id: int, conn: Optional[sa.Connection] = None
|
|
801
848
|
) -> Optional[RecordedResult]:
|
|
@@ -822,6 +869,23 @@ class SystemDatabase:
|
|
|
822
869
|
}
|
|
823
870
|
return result
|
|
824
871
|
|
|
872
|
+
def check_child_workflow(
|
|
873
|
+
self, workflow_uuid: str, function_id: int
|
|
874
|
+
) -> Optional[str]:
|
|
875
|
+
sql = sa.select(SystemSchema.operation_outputs.c.child_workflow_id).where(
|
|
876
|
+
SystemSchema.operation_outputs.c.workflow_uuid == workflow_uuid,
|
|
877
|
+
SystemSchema.operation_outputs.c.function_id == function_id,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# If in a transaction, use the provided connection
|
|
881
|
+
row: Any
|
|
882
|
+
with self.engine.begin() as c:
|
|
883
|
+
row = c.execute(sql).fetchone()
|
|
884
|
+
|
|
885
|
+
if row is None:
|
|
886
|
+
return None
|
|
887
|
+
return str(row[0])
|
|
888
|
+
|
|
825
889
|
def send(
|
|
826
890
|
self,
|
|
827
891
|
workflow_uuid: str,
|
|
@@ -866,6 +930,7 @@ class SystemDatabase:
|
|
|
866
930
|
output: OperationResultInternal = {
|
|
867
931
|
"workflow_uuid": workflow_uuid,
|
|
868
932
|
"function_id": function_id,
|
|
933
|
+
"function_name": "DBOS.send",
|
|
869
934
|
"output": None,
|
|
870
935
|
"error": None,
|
|
871
936
|
}
|
|
@@ -959,6 +1024,7 @@ class SystemDatabase:
|
|
|
959
1024
|
{
|
|
960
1025
|
"workflow_uuid": workflow_uuid,
|
|
961
1026
|
"function_id": function_id,
|
|
1027
|
+
"function_name": "DBOS.recv",
|
|
962
1028
|
"output": _serialization.serialize(
|
|
963
1029
|
message
|
|
964
1030
|
), # None will be serialized to 'null'
|
|
@@ -1049,6 +1115,7 @@ class SystemDatabase:
|
|
|
1049
1115
|
{
|
|
1050
1116
|
"workflow_uuid": workflow_uuid,
|
|
1051
1117
|
"function_id": function_id,
|
|
1118
|
+
"function_name": "DBOS.sleep",
|
|
1052
1119
|
"output": _serialization.serialize(end_time),
|
|
1053
1120
|
"error": None,
|
|
1054
1121
|
}
|
|
@@ -1096,6 +1163,7 @@ class SystemDatabase:
|
|
|
1096
1163
|
output: OperationResultInternal = {
|
|
1097
1164
|
"workflow_uuid": workflow_uuid,
|
|
1098
1165
|
"function_id": function_id,
|
|
1166
|
+
"function_name": "DBOS.setEvent",
|
|
1099
1167
|
"output": None,
|
|
1100
1168
|
"error": None,
|
|
1101
1169
|
}
|
|
@@ -1176,6 +1244,7 @@ class SystemDatabase:
|
|
|
1176
1244
|
{
|
|
1177
1245
|
"workflow_uuid": caller_ctx["workflow_uuid"],
|
|
1178
1246
|
"function_id": caller_ctx["function_id"],
|
|
1247
|
+
"function_name": "DBOS.getEvent",
|
|
1179
1248
|
"output": _serialization.serialize(
|
|
1180
1249
|
value
|
|
1181
1250
|
), # None will be serialized to 'null'
|
|
@@ -1184,106 +1253,6 @@ class SystemDatabase:
|
|
|
1184
1253
|
)
|
|
1185
1254
|
return value
|
|
1186
1255
|
|
|
1187
|
-
def _flush_workflow_status_buffer(self) -> None:
|
|
1188
|
-
if self._debug_mode:
|
|
1189
|
-
raise Exception("called _flush_workflow_status_buffer in debug mode")
|
|
1190
|
-
|
|
1191
|
-
"""Export the workflow status buffer to the database, up to the batch size."""
|
|
1192
|
-
if len(self._workflow_status_buffer) == 0:
|
|
1193
|
-
return
|
|
1194
|
-
|
|
1195
|
-
# Record the exported status so far, and add them back on errors.
|
|
1196
|
-
exported_status: Dict[str, WorkflowStatusInternal] = {}
|
|
1197
|
-
with self.engine.begin() as c:
|
|
1198
|
-
exported = 0
|
|
1199
|
-
status_iter = iter(list(self._workflow_status_buffer))
|
|
1200
|
-
wf_id: Optional[str] = None
|
|
1201
|
-
while (
|
|
1202
|
-
exported < _buffer_flush_batch_size
|
|
1203
|
-
and (wf_id := next(status_iter, None)) is not None
|
|
1204
|
-
):
|
|
1205
|
-
# Pop the first key in the buffer (FIFO)
|
|
1206
|
-
status = self._workflow_status_buffer.pop(wf_id, None)
|
|
1207
|
-
if status is None:
|
|
1208
|
-
continue
|
|
1209
|
-
exported_status[wf_id] = status
|
|
1210
|
-
try:
|
|
1211
|
-
self.update_workflow_status(status, conn=c)
|
|
1212
|
-
exported += 1
|
|
1213
|
-
except Exception as e:
|
|
1214
|
-
dbos_logger.error(f"Error while flushing status buffer: {e}")
|
|
1215
|
-
c.rollback()
|
|
1216
|
-
# Add the exported status back to the buffer, so they can be retried next time
|
|
1217
|
-
self._workflow_status_buffer.update(exported_status)
|
|
1218
|
-
break
|
|
1219
|
-
|
|
1220
|
-
def _flush_workflow_inputs_buffer(self) -> None:
|
|
1221
|
-
if self._debug_mode:
|
|
1222
|
-
raise Exception("called _flush_workflow_inputs_buffer in debug mode")
|
|
1223
|
-
|
|
1224
|
-
"""Export the workflow inputs buffer to the database, up to the batch size."""
|
|
1225
|
-
if len(self._workflow_inputs_buffer) == 0:
|
|
1226
|
-
return
|
|
1227
|
-
|
|
1228
|
-
# Record exported inputs so far, and add them back on errors.
|
|
1229
|
-
exported_inputs: Dict[str, str] = {}
|
|
1230
|
-
with self.engine.begin() as c:
|
|
1231
|
-
exported = 0
|
|
1232
|
-
input_iter = iter(list(self._workflow_inputs_buffer))
|
|
1233
|
-
wf_id: Optional[str] = None
|
|
1234
|
-
while (
|
|
1235
|
-
exported < _buffer_flush_batch_size
|
|
1236
|
-
and (wf_id := next(input_iter, None)) is not None
|
|
1237
|
-
):
|
|
1238
|
-
if wf_id not in self._exported_temp_txn_wf_status:
|
|
1239
|
-
# Skip exporting inputs if the status has not been exported yet
|
|
1240
|
-
continue
|
|
1241
|
-
inputs = self._workflow_inputs_buffer.pop(wf_id, None)
|
|
1242
|
-
if inputs is None:
|
|
1243
|
-
continue
|
|
1244
|
-
exported_inputs[wf_id] = inputs
|
|
1245
|
-
try:
|
|
1246
|
-
self.update_workflow_inputs(wf_id, inputs, conn=c)
|
|
1247
|
-
exported += 1
|
|
1248
|
-
except Exception as e:
|
|
1249
|
-
dbos_logger.error(f"Error while flushing inputs buffer: {e}")
|
|
1250
|
-
c.rollback()
|
|
1251
|
-
# Add the exported inputs back to the buffer, so they can be retried next time
|
|
1252
|
-
self._workflow_inputs_buffer.update(exported_inputs)
|
|
1253
|
-
break
|
|
1254
|
-
|
|
1255
|
-
def flush_workflow_buffers(self) -> None:
|
|
1256
|
-
"""Flush the workflow status and inputs buffers periodically, via a background thread."""
|
|
1257
|
-
while self._run_background_processes:
|
|
1258
|
-
try:
|
|
1259
|
-
self._is_flushing_status_buffer = True
|
|
1260
|
-
# Must flush the status buffer first, as the inputs table has a foreign key constraint on the status table.
|
|
1261
|
-
self._flush_workflow_status_buffer()
|
|
1262
|
-
self._flush_workflow_inputs_buffer()
|
|
1263
|
-
self._is_flushing_status_buffer = False
|
|
1264
|
-
if self._is_buffers_empty:
|
|
1265
|
-
# Only sleep if both buffers are empty
|
|
1266
|
-
time.sleep(_buffer_flush_interval_secs)
|
|
1267
|
-
except Exception as e:
|
|
1268
|
-
dbos_logger.error(f"Error while flushing buffers: {e}")
|
|
1269
|
-
time.sleep(_buffer_flush_interval_secs)
|
|
1270
|
-
# Will retry next time
|
|
1271
|
-
|
|
1272
|
-
def buffer_workflow_status(self, status: WorkflowStatusInternal) -> None:
|
|
1273
|
-
self._workflow_status_buffer[status["workflow_uuid"]] = status
|
|
1274
|
-
|
|
1275
|
-
def buffer_workflow_inputs(self, workflow_id: str, inputs: str) -> None:
|
|
1276
|
-
# inputs is a serialized WorkflowInputs string
|
|
1277
|
-
self._workflow_inputs_buffer[workflow_id] = inputs
|
|
1278
|
-
self._temp_txn_wf_ids.add(workflow_id)
|
|
1279
|
-
|
|
1280
|
-
@property
|
|
1281
|
-
def _is_buffers_empty(self) -> bool:
|
|
1282
|
-
return (
|
|
1283
|
-
len(self._workflow_status_buffer) == 0
|
|
1284
|
-
and len(self._workflow_inputs_buffer) == 0
|
|
1285
|
-
)
|
|
1286
|
-
|
|
1287
1256
|
def enqueue(self, workflow_id: str, queue_name: str) -> None:
|
|
1288
1257
|
if self._debug_mode:
|
|
1289
1258
|
raise Exception("called enqueue in debug mode")
|
|
@@ -1297,7 +1266,9 @@ class SystemDatabase:
|
|
|
1297
1266
|
.on_conflict_do_nothing()
|
|
1298
1267
|
)
|
|
1299
1268
|
|
|
1300
|
-
def start_queued_workflows(
|
|
1269
|
+
def start_queued_workflows(
|
|
1270
|
+
self, queue: "Queue", executor_id: str, app_version: str
|
|
1271
|
+
) -> List[str]:
|
|
1301
1272
|
if self._debug_mode:
|
|
1302
1273
|
return []
|
|
1303
1274
|
|
|
@@ -1412,26 +1383,36 @@ class SystemDatabase:
|
|
|
1412
1383
|
break
|
|
1413
1384
|
|
|
1414
1385
|
# To start a function, first set its status to PENDING and update its executor ID
|
|
1415
|
-
c.execute(
|
|
1386
|
+
res = c.execute(
|
|
1416
1387
|
SystemSchema.workflow_status.update()
|
|
1417
1388
|
.where(SystemSchema.workflow_status.c.workflow_uuid == id)
|
|
1418
1389
|
.where(
|
|
1419
1390
|
SystemSchema.workflow_status.c.status
|
|
1420
1391
|
== WorkflowStatusString.ENQUEUED.value
|
|
1421
1392
|
)
|
|
1393
|
+
.where(
|
|
1394
|
+
sa.or_(
|
|
1395
|
+
SystemSchema.workflow_status.c.application_version
|
|
1396
|
+
== app_version,
|
|
1397
|
+
SystemSchema.workflow_status.c.application_version.is_(
|
|
1398
|
+
None
|
|
1399
|
+
),
|
|
1400
|
+
)
|
|
1401
|
+
)
|
|
1422
1402
|
.values(
|
|
1423
1403
|
status=WorkflowStatusString.PENDING.value,
|
|
1404
|
+
application_version=app_version,
|
|
1424
1405
|
executor_id=executor_id,
|
|
1425
1406
|
)
|
|
1426
1407
|
)
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1408
|
+
if res.rowcount > 0:
|
|
1409
|
+
# Then give it a start time and assign the executor ID
|
|
1410
|
+
c.execute(
|
|
1411
|
+
SystemSchema.workflow_queue.update()
|
|
1412
|
+
.where(SystemSchema.workflow_queue.c.workflow_uuid == id)
|
|
1413
|
+
.values(started_at_epoch_ms=start_time_ms)
|
|
1414
|
+
)
|
|
1415
|
+
ret_ids.append(id)
|
|
1435
1416
|
|
|
1436
1417
|
# If we have a limiter, garbage-collect all completed functions started
|
|
1437
1418
|
# before the period. If there's no limiter, there's no need--they were
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# First, let's do imports, create a FastAPI app, and initialize DBOS.
|
|
8
8
|
|
|
9
|
+
import uvicorn
|
|
9
10
|
from fastapi import FastAPI
|
|
10
11
|
from fastapi.responses import HTMLResponse
|
|
11
12
|
|
|
@@ -37,7 +38,7 @@ def example_transaction(name: str) -> str:
|
|
|
37
38
|
return greeting
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
#
|
|
41
|
+
# Now, let's use FastAPI to serve an HTML + CSS readme
|
|
41
42
|
# from the root path.
|
|
42
43
|
|
|
43
44
|
|
|
@@ -66,14 +67,8 @@ def readme() -> HTMLResponse:
|
|
|
66
67
|
return HTMLResponse(readme)
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
#
|
|
70
|
-
# - "npm i -g @dbos-inc/dbos-cloud@latest" to install the Cloud CLI (requires Node)
|
|
71
|
-
# - "dbos-cloud app deploy" to deploy your app
|
|
72
|
-
# - Deploy outputs a URL--visit it to see your app!
|
|
70
|
+
# Finally, we'll launch DBOS then start the FastAPI server.
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# - "dbos migrate" to set up your database tables
|
|
78
|
-
# - "dbos start" to start the app
|
|
79
|
-
# - Visit localhost:8000 to see your app!
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
DBOS.launch()
|
|
74
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|