dbos 2.4.0a3__py3-none-any.whl → 2.6.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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- dbos/__init__.py +2 -0
- dbos/_app_db.py +29 -87
- dbos/_client.py +16 -11
- dbos/_conductor/conductor.py +65 -34
- dbos/_conductor/protocol.py +23 -0
- dbos/_context.py +2 -3
- dbos/_core.py +98 -30
- dbos/_dbos.py +32 -28
- dbos/_dbos_config.py +3 -21
- dbos/_debouncer.py +4 -5
- dbos/_fastapi.py +4 -4
- dbos/_flask.py +2 -3
- dbos/_logger.py +14 -7
- dbos/_migration.py +30 -0
- dbos/_queue.py +94 -37
- dbos/_schemas/system_database.py +20 -0
- dbos/_sys_db.py +329 -92
- dbos/_sys_db_postgres.py +18 -12
- dbos/_tracer.py +10 -3
- dbos/_utils.py +10 -0
- dbos/_workflow_commands.py +2 -17
- dbos/cli/cli.py +8 -18
- dbos/cli/migration.py +29 -1
- {dbos-2.4.0a3.dist-info → dbos-2.6.0a8.dist-info}/METADATA +1 -1
- {dbos-2.4.0a3.dist-info → dbos-2.6.0a8.dist-info}/RECORD +28 -28
- {dbos-2.4.0a3.dist-info → dbos-2.6.0a8.dist-info}/WHEEL +1 -1
- {dbos-2.4.0a3.dist-info → dbos-2.6.0a8.dist-info}/entry_points.txt +0 -0
- {dbos-2.4.0a3.dist-info → dbos-2.6.0a8.dist-info}/licenses/LICENSE +0 -0
dbos/_sys_db.py
CHANGED
|
@@ -158,6 +158,16 @@ class WorkflowStatusInternal(TypedDict):
|
|
|
158
158
|
forked_from: Optional[str]
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
class MetricData(TypedDict):
|
|
162
|
+
"""
|
|
163
|
+
Metrics data for workflows and steps within a time range.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
metric_type: str # Type of metric: "workflow" or "step"
|
|
167
|
+
metric_name: str # Name of the workflow or step
|
|
168
|
+
value: int # Number of times the operation ran in the time interval
|
|
169
|
+
|
|
170
|
+
|
|
161
171
|
class EnqueueOptionsInternal(TypedDict):
|
|
162
172
|
# Unique ID for deduplication on a queue
|
|
163
173
|
deduplication_id: Optional[str]
|
|
@@ -170,16 +180,17 @@ class EnqueueOptionsInternal(TypedDict):
|
|
|
170
180
|
|
|
171
181
|
|
|
172
182
|
class RecordedResult(TypedDict):
|
|
173
|
-
output: Optional[str] #
|
|
174
|
-
error: Optional[str] #
|
|
183
|
+
output: Optional[str] # Serialized
|
|
184
|
+
error: Optional[str] # Serialized
|
|
185
|
+
child_workflow_id: Optional[str]
|
|
175
186
|
|
|
176
187
|
|
|
177
188
|
class OperationResultInternal(TypedDict):
|
|
178
189
|
workflow_uuid: str
|
|
179
190
|
function_id: int
|
|
180
191
|
function_name: str
|
|
181
|
-
output: Optional[str] #
|
|
182
|
-
error: Optional[str] #
|
|
192
|
+
output: Optional[str] # Serialized
|
|
193
|
+
error: Optional[str] # Serialized
|
|
183
194
|
started_at_epoch_ms: int
|
|
184
195
|
|
|
185
196
|
|
|
@@ -351,6 +362,7 @@ class SystemDatabase(ABC):
|
|
|
351
362
|
engine: Optional[sa.Engine],
|
|
352
363
|
schema: Optional[str],
|
|
353
364
|
serializer: Serializer,
|
|
365
|
+
executor_id: Optional[str],
|
|
354
366
|
debug_mode: bool = False,
|
|
355
367
|
) -> "SystemDatabase":
|
|
356
368
|
"""Factory method to create the appropriate SystemDatabase implementation based on URL."""
|
|
@@ -363,6 +375,7 @@ class SystemDatabase(ABC):
|
|
|
363
375
|
engine=engine,
|
|
364
376
|
schema=schema,
|
|
365
377
|
serializer=serializer,
|
|
378
|
+
executor_id=executor_id,
|
|
366
379
|
debug_mode=debug_mode,
|
|
367
380
|
)
|
|
368
381
|
else:
|
|
@@ -374,6 +387,7 @@ class SystemDatabase(ABC):
|
|
|
374
387
|
engine=engine,
|
|
375
388
|
schema=schema,
|
|
376
389
|
serializer=serializer,
|
|
390
|
+
executor_id=executor_id,
|
|
377
391
|
debug_mode=debug_mode,
|
|
378
392
|
)
|
|
379
393
|
|
|
@@ -385,11 +399,32 @@ class SystemDatabase(ABC):
|
|
|
385
399
|
engine: Optional[sa.Engine],
|
|
386
400
|
schema: Optional[str],
|
|
387
401
|
serializer: Serializer,
|
|
402
|
+
executor_id: Optional[str],
|
|
388
403
|
debug_mode: bool = False,
|
|
389
404
|
):
|
|
390
405
|
import sqlalchemy.dialects.postgresql as pg
|
|
391
406
|
import sqlalchemy.dialects.sqlite as sq
|
|
392
407
|
|
|
408
|
+
# Log system database connection information
|
|
409
|
+
if engine:
|
|
410
|
+
dbos_logger.info("Initializing DBOS system database with custom engine")
|
|
411
|
+
else:
|
|
412
|
+
printable_sys_db_url = sa.make_url(system_database_url).render_as_string(
|
|
413
|
+
hide_password=True
|
|
414
|
+
)
|
|
415
|
+
dbos_logger.info(
|
|
416
|
+
f"Initializing DBOS system database with URL: {printable_sys_db_url}"
|
|
417
|
+
)
|
|
418
|
+
if system_database_url.startswith("sqlite"):
|
|
419
|
+
dbos_logger.info(
|
|
420
|
+
f"Using SQLite as a system database. The SQLite system database is for development and testing. PostgreSQL is recommended for production use."
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
dbos_logger.info(
|
|
424
|
+
f"DBOS system database engine parameters: {engine_kwargs}"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Configure and initialize the system database
|
|
393
428
|
self.dialect = sq if system_database_url.startswith("sqlite") else pg
|
|
394
429
|
|
|
395
430
|
self.serializer = serializer
|
|
@@ -410,6 +445,8 @@ class SystemDatabase(ABC):
|
|
|
410
445
|
|
|
411
446
|
self.notifications_map = ThreadSafeConditionDict()
|
|
412
447
|
self.workflow_events_map = ThreadSafeConditionDict()
|
|
448
|
+
self.executor_id = executor_id
|
|
449
|
+
|
|
413
450
|
self._listener_thread_lock = threading.Lock()
|
|
414
451
|
|
|
415
452
|
# Now we can run background processes
|
|
@@ -694,11 +731,7 @@ class SystemDatabase(ABC):
|
|
|
694
731
|
name=status["name"],
|
|
695
732
|
class_name=status["class_name"],
|
|
696
733
|
config_name=status["config_name"],
|
|
697
|
-
application_version=
|
|
698
|
-
application_version
|
|
699
|
-
if application_version is not None
|
|
700
|
-
else status["app_version"]
|
|
701
|
-
),
|
|
734
|
+
application_version=application_version,
|
|
702
735
|
application_id=status["app_id"],
|
|
703
736
|
authenticated_user=status["authenticated_user"],
|
|
704
737
|
authenticated_roles=status["authenticated_roles"],
|
|
@@ -710,34 +743,124 @@ class SystemDatabase(ABC):
|
|
|
710
743
|
)
|
|
711
744
|
|
|
712
745
|
if start_step > 1:
|
|
746
|
+
# Copy the original workflow's step checkpoints
|
|
747
|
+
c.execute(
|
|
748
|
+
sa.insert(SystemSchema.operation_outputs).from_select(
|
|
749
|
+
[
|
|
750
|
+
"workflow_uuid",
|
|
751
|
+
"function_id",
|
|
752
|
+
"output",
|
|
753
|
+
"error",
|
|
754
|
+
"function_name",
|
|
755
|
+
"child_workflow_id",
|
|
756
|
+
"started_at_epoch_ms",
|
|
757
|
+
"completed_at_epoch_ms",
|
|
758
|
+
],
|
|
759
|
+
sa.select(
|
|
760
|
+
sa.literal(forked_workflow_id).label("workflow_uuid"),
|
|
761
|
+
SystemSchema.operation_outputs.c.function_id,
|
|
762
|
+
SystemSchema.operation_outputs.c.output,
|
|
763
|
+
SystemSchema.operation_outputs.c.error,
|
|
764
|
+
SystemSchema.operation_outputs.c.function_name,
|
|
765
|
+
SystemSchema.operation_outputs.c.child_workflow_id,
|
|
766
|
+
SystemSchema.operation_outputs.c.started_at_epoch_ms,
|
|
767
|
+
SystemSchema.operation_outputs.c.completed_at_epoch_ms,
|
|
768
|
+
).where(
|
|
769
|
+
(
|
|
770
|
+
SystemSchema.operation_outputs.c.workflow_uuid
|
|
771
|
+
== original_workflow_id
|
|
772
|
+
)
|
|
773
|
+
& (
|
|
774
|
+
SystemSchema.operation_outputs.c.function_id
|
|
775
|
+
< start_step
|
|
776
|
+
)
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
# Copy the original workflow's events
|
|
781
|
+
c.execute(
|
|
782
|
+
sa.insert(SystemSchema.workflow_events_history).from_select(
|
|
783
|
+
[
|
|
784
|
+
"workflow_uuid",
|
|
785
|
+
"function_id",
|
|
786
|
+
"key",
|
|
787
|
+
"value",
|
|
788
|
+
],
|
|
789
|
+
sa.select(
|
|
790
|
+
sa.literal(forked_workflow_id).label("workflow_uuid"),
|
|
791
|
+
SystemSchema.workflow_events_history.c.function_id,
|
|
792
|
+
SystemSchema.workflow_events_history.c.key,
|
|
793
|
+
SystemSchema.workflow_events_history.c.value,
|
|
794
|
+
).where(
|
|
795
|
+
(
|
|
796
|
+
SystemSchema.workflow_events_history.c.workflow_uuid
|
|
797
|
+
== original_workflow_id
|
|
798
|
+
)
|
|
799
|
+
& (
|
|
800
|
+
SystemSchema.workflow_events_history.c.function_id
|
|
801
|
+
< start_step
|
|
802
|
+
)
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
# Copy only the latest version of each workflow event from the history table
|
|
807
|
+
# (the one with the maximum function_id for each key where function_id < start_step)
|
|
808
|
+
weh1 = SystemSchema.workflow_events_history.alias("weh1")
|
|
809
|
+
weh2 = SystemSchema.workflow_events_history.alias("weh2")
|
|
713
810
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
)
|
|
736
|
-
|
|
737
|
-
|
|
811
|
+
max_function_id_subquery = (
|
|
812
|
+
sa.select(sa.func.max(weh2.c.function_id))
|
|
813
|
+
.where(
|
|
814
|
+
(weh2.c.workflow_uuid == original_workflow_id)
|
|
815
|
+
& (weh2.c.key == weh1.c.key)
|
|
816
|
+
& (weh2.c.function_id < start_step)
|
|
817
|
+
)
|
|
818
|
+
.scalar_subquery()
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
c.execute(
|
|
822
|
+
sa.insert(SystemSchema.workflow_events).from_select(
|
|
823
|
+
[
|
|
824
|
+
"workflow_uuid",
|
|
825
|
+
"key",
|
|
826
|
+
"value",
|
|
827
|
+
],
|
|
828
|
+
sa.select(
|
|
829
|
+
sa.literal(forked_workflow_id).label("workflow_uuid"),
|
|
830
|
+
weh1.c.key,
|
|
831
|
+
weh1.c.value,
|
|
832
|
+
).where(
|
|
833
|
+
(weh1.c.workflow_uuid == original_workflow_id)
|
|
834
|
+
& (weh1.c.function_id == max_function_id_subquery)
|
|
835
|
+
),
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
# Copy the original workflow's streams
|
|
839
|
+
c.execute(
|
|
840
|
+
sa.insert(SystemSchema.streams).from_select(
|
|
841
|
+
[
|
|
842
|
+
"workflow_uuid",
|
|
843
|
+
"function_id",
|
|
844
|
+
"key",
|
|
845
|
+
"value",
|
|
846
|
+
"offset",
|
|
847
|
+
],
|
|
848
|
+
sa.select(
|
|
849
|
+
sa.literal(forked_workflow_id).label("workflow_uuid"),
|
|
850
|
+
SystemSchema.streams.c.function_id,
|
|
851
|
+
SystemSchema.streams.c.key,
|
|
852
|
+
SystemSchema.streams.c.value,
|
|
853
|
+
SystemSchema.streams.c.offset,
|
|
854
|
+
).where(
|
|
855
|
+
(
|
|
856
|
+
SystemSchema.streams.c.workflow_uuid
|
|
857
|
+
== original_workflow_id
|
|
858
|
+
)
|
|
859
|
+
& (SystemSchema.streams.c.function_id < start_step)
|
|
860
|
+
),
|
|
861
|
+
)
|
|
738
862
|
)
|
|
739
863
|
|
|
740
|
-
c.execute(insert_stmt)
|
|
741
864
|
return forked_workflow_id
|
|
742
865
|
|
|
743
866
|
@db_retry()
|
|
@@ -828,7 +951,7 @@ class SystemDatabase(ABC):
|
|
|
828
951
|
return workflow_id
|
|
829
952
|
|
|
830
953
|
@db_retry()
|
|
831
|
-
def await_workflow_result(self, workflow_id: str) -> Any:
|
|
954
|
+
def await_workflow_result(self, workflow_id: str, polling_interval: float) -> Any:
|
|
832
955
|
while True:
|
|
833
956
|
with self.engine.begin() as c:
|
|
834
957
|
row = c.execute(
|
|
@@ -853,7 +976,7 @@ class SystemDatabase(ABC):
|
|
|
853
976
|
raise DBOSAwaitedWorkflowCancelledError(workflow_id)
|
|
854
977
|
else:
|
|
855
978
|
pass # CB: I guess we're assuming the WF will show up eventually.
|
|
856
|
-
time.sleep(
|
|
979
|
+
time.sleep(polling_interval)
|
|
857
980
|
|
|
858
981
|
def get_workflows(
|
|
859
982
|
self,
|
|
@@ -896,11 +1019,12 @@ class SystemDatabase(ABC):
|
|
|
896
1019
|
|
|
897
1020
|
if input.queues_only:
|
|
898
1021
|
query = sa.select(*load_columns).where(
|
|
899
|
-
|
|
900
|
-
SystemSchema.workflow_status.c.queue_name.isnot(None),
|
|
901
|
-
SystemSchema.workflow_status.c.status.in_(["ENQUEUED", "PENDING"]),
|
|
902
|
-
)
|
|
1022
|
+
SystemSchema.workflow_status.c.queue_name.isnot(None),
|
|
903
1023
|
)
|
|
1024
|
+
if not input.status:
|
|
1025
|
+
query = query.where(
|
|
1026
|
+
SystemSchema.workflow_status.c.status.in_(["ENQUEUED", "PENDING"])
|
|
1027
|
+
)
|
|
904
1028
|
else:
|
|
905
1029
|
query = sa.select(*load_columns)
|
|
906
1030
|
if input.sort_desc:
|
|
@@ -1027,7 +1151,7 @@ class SystemDatabase(ABC):
|
|
|
1027
1151
|
for row in rows
|
|
1028
1152
|
]
|
|
1029
1153
|
|
|
1030
|
-
def
|
|
1154
|
+
def list_workflow_steps(self, workflow_id: str) -> List[StepInfo]:
|
|
1031
1155
|
with self.engine.begin() as c:
|
|
1032
1156
|
rows = c.execute(
|
|
1033
1157
|
sa.select(
|
|
@@ -1038,7 +1162,9 @@ class SystemDatabase(ABC):
|
|
|
1038
1162
|
SystemSchema.operation_outputs.c.child_workflow_id,
|
|
1039
1163
|
SystemSchema.operation_outputs.c.started_at_epoch_ms,
|
|
1040
1164
|
SystemSchema.operation_outputs.c.completed_at_epoch_ms,
|
|
1041
|
-
)
|
|
1165
|
+
)
|
|
1166
|
+
.where(SystemSchema.operation_outputs.c.workflow_uuid == workflow_id)
|
|
1167
|
+
.order_by(SystemSchema.operation_outputs.c.function_id)
|
|
1042
1168
|
).fetchall()
|
|
1043
1169
|
steps = []
|
|
1044
1170
|
for row in rows:
|
|
@@ -1069,17 +1195,44 @@ class SystemDatabase(ABC):
|
|
|
1069
1195
|
error = result["error"]
|
|
1070
1196
|
output = result["output"]
|
|
1071
1197
|
assert error is None or output is None, "Only one of error or output can be set"
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1198
|
+
|
|
1199
|
+
# Check if the executor ID belong to another process.
|
|
1200
|
+
# Reset it to this process's executor ID if so.
|
|
1201
|
+
wf_executor_id_row = conn.execute(
|
|
1202
|
+
sa.select(
|
|
1203
|
+
SystemSchema.workflow_status.c.executor_id,
|
|
1204
|
+
).where(
|
|
1205
|
+
SystemSchema.workflow_status.c.workflow_uuid == result["workflow_uuid"]
|
|
1206
|
+
)
|
|
1207
|
+
).fetchone()
|
|
1208
|
+
assert wf_executor_id_row is not None
|
|
1209
|
+
wf_executor_id = wf_executor_id_row[0]
|
|
1210
|
+
if self.executor_id is not None and wf_executor_id != self.executor_id:
|
|
1211
|
+
dbos_logger.debug(
|
|
1212
|
+
f'Resetting executor_id from {wf_executor_id} to {self.executor_id} for workflow {result["workflow_uuid"]}'
|
|
1213
|
+
)
|
|
1214
|
+
conn.execute(
|
|
1215
|
+
sa.update(SystemSchema.workflow_status)
|
|
1216
|
+
.values(executor_id=self.executor_id)
|
|
1217
|
+
.where(
|
|
1218
|
+
SystemSchema.workflow_status.c.workflow_uuid
|
|
1219
|
+
== result["workflow_uuid"]
|
|
1220
|
+
)
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
# Record the outcome, throwing DBOSWorkflowConflictIDError if it is already present
|
|
1081
1224
|
try:
|
|
1082
|
-
conn.execute(
|
|
1225
|
+
conn.execute(
|
|
1226
|
+
sa.insert(SystemSchema.operation_outputs).values(
|
|
1227
|
+
workflow_uuid=result["workflow_uuid"],
|
|
1228
|
+
function_id=result["function_id"],
|
|
1229
|
+
function_name=result["function_name"],
|
|
1230
|
+
started_at_epoch_ms=result["started_at_epoch_ms"],
|
|
1231
|
+
completed_at_epoch_ms=int(time.time() * 1000),
|
|
1232
|
+
output=output,
|
|
1233
|
+
error=error,
|
|
1234
|
+
)
|
|
1235
|
+
)
|
|
1083
1236
|
except DBAPIError as dbapi_error:
|
|
1084
1237
|
if self._is_unique_constraint_violation(dbapi_error):
|
|
1085
1238
|
raise DBOSWorkflowConflictIDError(result["workflow_uuid"])
|
|
@@ -1168,6 +1321,7 @@ class SystemDatabase(ABC):
|
|
|
1168
1321
|
SystemSchema.operation_outputs.c.output,
|
|
1169
1322
|
SystemSchema.operation_outputs.c.error,
|
|
1170
1323
|
SystemSchema.operation_outputs.c.function_name,
|
|
1324
|
+
SystemSchema.operation_outputs.c.child_workflow_id,
|
|
1171
1325
|
).where(
|
|
1172
1326
|
(SystemSchema.operation_outputs.c.workflow_uuid == workflow_id)
|
|
1173
1327
|
& (SystemSchema.operation_outputs.c.function_id == function_id)
|
|
@@ -1196,10 +1350,11 @@ class SystemDatabase(ABC):
|
|
|
1196
1350
|
return None
|
|
1197
1351
|
|
|
1198
1352
|
# Extract operation output data
|
|
1199
|
-
output, error, recorded_function_name = (
|
|
1353
|
+
output, error, recorded_function_name, child_workflow_id = (
|
|
1200
1354
|
operation_output_rows[0][0],
|
|
1201
1355
|
operation_output_rows[0][1],
|
|
1202
1356
|
operation_output_rows[0][2],
|
|
1357
|
+
operation_output_rows[0][3],
|
|
1203
1358
|
)
|
|
1204
1359
|
|
|
1205
1360
|
# If the provided and recorded function name are different, throw an exception
|
|
@@ -1214,6 +1369,7 @@ class SystemDatabase(ABC):
|
|
|
1214
1369
|
result: RecordedResult = {
|
|
1215
1370
|
"output": output,
|
|
1216
1371
|
"error": error,
|
|
1372
|
+
"child_workflow_id": child_workflow_id,
|
|
1217
1373
|
}
|
|
1218
1374
|
return result
|
|
1219
1375
|
|
|
@@ -1226,31 +1382,6 @@ class SystemDatabase(ABC):
|
|
|
1226
1382
|
workflow_id, function_id, function_name, c
|
|
1227
1383
|
)
|
|
1228
1384
|
|
|
1229
|
-
@db_retry()
|
|
1230
|
-
def check_child_workflow(
|
|
1231
|
-
self, workflow_uuid: str, function_id: int
|
|
1232
|
-
) -> Optional[str]:
|
|
1233
|
-
sql = sa.select(
|
|
1234
|
-
SystemSchema.operation_outputs.c.child_workflow_id,
|
|
1235
|
-
SystemSchema.operation_outputs.c.error,
|
|
1236
|
-
).where(
|
|
1237
|
-
SystemSchema.operation_outputs.c.workflow_uuid == workflow_uuid,
|
|
1238
|
-
SystemSchema.operation_outputs.c.function_id == function_id,
|
|
1239
|
-
)
|
|
1240
|
-
|
|
1241
|
-
# If in a transaction, use the provided connection
|
|
1242
|
-
row: Any
|
|
1243
|
-
with self.engine.begin() as c:
|
|
1244
|
-
row = c.execute(sql).fetchone()
|
|
1245
|
-
|
|
1246
|
-
if row is None:
|
|
1247
|
-
return None
|
|
1248
|
-
elif row[1]:
|
|
1249
|
-
e: Exception = self.serializer.deserialize(row[1])
|
|
1250
|
-
raise e
|
|
1251
|
-
else:
|
|
1252
|
-
return str(row[0])
|
|
1253
|
-
|
|
1254
1385
|
@db_retry()
|
|
1255
1386
|
def send(
|
|
1256
1387
|
self,
|
|
@@ -1503,6 +1634,19 @@ class SystemDatabase(ABC):
|
|
|
1503
1634
|
set_={"value": self.serializer.serialize(message)},
|
|
1504
1635
|
)
|
|
1505
1636
|
)
|
|
1637
|
+
c.execute(
|
|
1638
|
+
self.dialect.insert(SystemSchema.workflow_events_history)
|
|
1639
|
+
.values(
|
|
1640
|
+
workflow_uuid=workflow_uuid,
|
|
1641
|
+
function_id=function_id,
|
|
1642
|
+
key=key,
|
|
1643
|
+
value=self.serializer.serialize(message),
|
|
1644
|
+
)
|
|
1645
|
+
.on_conflict_do_update(
|
|
1646
|
+
index_elements=["workflow_uuid", "key", "function_id"],
|
|
1647
|
+
set_={"value": self.serializer.serialize(message)},
|
|
1648
|
+
)
|
|
1649
|
+
)
|
|
1506
1650
|
output: OperationResultInternal = {
|
|
1507
1651
|
"workflow_uuid": workflow_uuid,
|
|
1508
1652
|
"function_id": function_id,
|
|
@@ -1516,6 +1660,7 @@ class SystemDatabase(ABC):
|
|
|
1516
1660
|
def set_event_from_step(
|
|
1517
1661
|
self,
|
|
1518
1662
|
workflow_uuid: str,
|
|
1663
|
+
function_id: int,
|
|
1519
1664
|
key: str,
|
|
1520
1665
|
message: Any,
|
|
1521
1666
|
) -> None:
|
|
@@ -1532,6 +1677,19 @@ class SystemDatabase(ABC):
|
|
|
1532
1677
|
set_={"value": self.serializer.serialize(message)},
|
|
1533
1678
|
)
|
|
1534
1679
|
)
|
|
1680
|
+
c.execute(
|
|
1681
|
+
self.dialect.insert(SystemSchema.workflow_events_history)
|
|
1682
|
+
.values(
|
|
1683
|
+
workflow_uuid=workflow_uuid,
|
|
1684
|
+
function_id=function_id,
|
|
1685
|
+
key=key,
|
|
1686
|
+
value=self.serializer.serialize(message),
|
|
1687
|
+
)
|
|
1688
|
+
.on_conflict_do_update(
|
|
1689
|
+
index_elements=["workflow_uuid", "key", "function_id"],
|
|
1690
|
+
set_={"value": self.serializer.serialize(message)},
|
|
1691
|
+
)
|
|
1692
|
+
)
|
|
1535
1693
|
|
|
1536
1694
|
def get_all_events(self, workflow_id: str) -> Dict[str, Any]:
|
|
1537
1695
|
"""
|
|
@@ -1550,7 +1708,6 @@ class SystemDatabase(ABC):
|
|
|
1550
1708
|
SystemSchema.workflow_events.c.value,
|
|
1551
1709
|
).where(SystemSchema.workflow_events.c.workflow_uuid == workflow_id)
|
|
1552
1710
|
).fetchall()
|
|
1553
|
-
|
|
1554
1711
|
events: Dict[str, Any] = {}
|
|
1555
1712
|
for row in rows:
|
|
1556
1713
|
key = row[0]
|
|
@@ -1705,10 +1862,6 @@ class SystemDatabase(ABC):
|
|
|
1705
1862
|
sa.select(sa.func.count())
|
|
1706
1863
|
.select_from(SystemSchema.workflow_status)
|
|
1707
1864
|
.where(SystemSchema.workflow_status.c.queue_name == queue.name)
|
|
1708
|
-
.where(
|
|
1709
|
-
SystemSchema.workflow_status.c.queue_partition_key
|
|
1710
|
-
== queue_partition_key
|
|
1711
|
-
)
|
|
1712
1865
|
.where(
|
|
1713
1866
|
SystemSchema.workflow_status.c.status
|
|
1714
1867
|
!= WorkflowStatusString.ENQUEUED.value
|
|
@@ -1718,6 +1871,11 @@ class SystemDatabase(ABC):
|
|
|
1718
1871
|
> start_time_ms - limiter_period_ms
|
|
1719
1872
|
)
|
|
1720
1873
|
)
|
|
1874
|
+
if queue_partition_key is not None:
|
|
1875
|
+
query = query.where(
|
|
1876
|
+
SystemSchema.workflow_status.c.queue_partition_key
|
|
1877
|
+
== queue_partition_key
|
|
1878
|
+
)
|
|
1721
1879
|
num_recent_queries = c.execute(query).fetchone()[0] # type: ignore
|
|
1722
1880
|
if num_recent_queries >= queue.limiter["limit"]:
|
|
1723
1881
|
return []
|
|
@@ -1733,16 +1891,17 @@ class SystemDatabase(ABC):
|
|
|
1733
1891
|
)
|
|
1734
1892
|
.select_from(SystemSchema.workflow_status)
|
|
1735
1893
|
.where(SystemSchema.workflow_status.c.queue_name == queue.name)
|
|
1736
|
-
.where(
|
|
1737
|
-
SystemSchema.workflow_status.c.queue_partition_key
|
|
1738
|
-
== queue_partition_key
|
|
1739
|
-
)
|
|
1740
1894
|
.where(
|
|
1741
1895
|
SystemSchema.workflow_status.c.status
|
|
1742
1896
|
== WorkflowStatusString.PENDING.value
|
|
1743
1897
|
)
|
|
1744
1898
|
.group_by(SystemSchema.workflow_status.c.executor_id)
|
|
1745
1899
|
)
|
|
1900
|
+
if queue_partition_key is not None:
|
|
1901
|
+
pending_tasks_query = pending_tasks_query.where(
|
|
1902
|
+
SystemSchema.workflow_status.c.queue_partition_key
|
|
1903
|
+
== queue_partition_key
|
|
1904
|
+
)
|
|
1746
1905
|
pending_workflows = c.execute(pending_tasks_query).fetchall()
|
|
1747
1906
|
pending_workflows_dict = {row[0]: row[1] for row in pending_workflows}
|
|
1748
1907
|
local_pending_workflows = pending_workflows_dict.get(executor_id, 0)
|
|
@@ -1778,10 +1937,6 @@ class SystemDatabase(ABC):
|
|
|
1778
1937
|
)
|
|
1779
1938
|
.select_from(SystemSchema.workflow_status)
|
|
1780
1939
|
.where(SystemSchema.workflow_status.c.queue_name == queue.name)
|
|
1781
|
-
.where(
|
|
1782
|
-
SystemSchema.workflow_status.c.queue_partition_key
|
|
1783
|
-
== queue_partition_key
|
|
1784
|
-
)
|
|
1785
1940
|
.where(
|
|
1786
1941
|
SystemSchema.workflow_status.c.status
|
|
1787
1942
|
== WorkflowStatusString.ENQUEUED.value
|
|
@@ -1798,6 +1953,11 @@ class SystemDatabase(ABC):
|
|
|
1798
1953
|
# to ensure all processes have a consistent view of the table.
|
|
1799
1954
|
.with_for_update(skip_locked=skip_locks, nowait=(not skip_locks))
|
|
1800
1955
|
)
|
|
1956
|
+
if queue_partition_key is not None:
|
|
1957
|
+
query = query.where(
|
|
1958
|
+
SystemSchema.workflow_status.c.queue_partition_key
|
|
1959
|
+
== queue_partition_key
|
|
1960
|
+
)
|
|
1801
1961
|
if queue.priority_enabled:
|
|
1802
1962
|
query = query.order_by(
|
|
1803
1963
|
SystemSchema.workflow_status.c.priority.asc(),
|
|
@@ -1941,7 +2101,9 @@ class SystemDatabase(ABC):
|
|
|
1941
2101
|
dbos_logger.error(f"Error connecting to the DBOS system database: {e}")
|
|
1942
2102
|
raise
|
|
1943
2103
|
|
|
1944
|
-
def write_stream_from_step(
|
|
2104
|
+
def write_stream_from_step(
|
|
2105
|
+
self, workflow_uuid: str, function_id: int, key: str, value: Any
|
|
2106
|
+
) -> None:
|
|
1945
2107
|
"""
|
|
1946
2108
|
Write a key-value pair to the stream at the first unused offset.
|
|
1947
2109
|
"""
|
|
@@ -1971,6 +2133,7 @@ class SystemDatabase(ABC):
|
|
|
1971
2133
|
c.execute(
|
|
1972
2134
|
sa.insert(SystemSchema.streams).values(
|
|
1973
2135
|
workflow_uuid=workflow_uuid,
|
|
2136
|
+
function_id=function_id,
|
|
1974
2137
|
key=key,
|
|
1975
2138
|
value=serialized_value,
|
|
1976
2139
|
offset=next_offset,
|
|
@@ -2027,6 +2190,7 @@ class SystemDatabase(ABC):
|
|
|
2027
2190
|
c.execute(
|
|
2028
2191
|
sa.insert(SystemSchema.streams).values(
|
|
2029
2192
|
workflow_uuid=workflow_uuid,
|
|
2193
|
+
function_id=function_id,
|
|
2030
2194
|
key=key,
|
|
2031
2195
|
value=serialized_value,
|
|
2032
2196
|
offset=next_offset,
|
|
@@ -2124,3 +2288,76 @@ class SystemDatabase(ABC):
|
|
|
2124
2288
|
return cutoff_epoch_timestamp_ms, [
|
|
2125
2289
|
row[0] for row in pending_enqueued_result
|
|
2126
2290
|
]
|
|
2291
|
+
|
|
2292
|
+
def get_metrics(self, start_time: str, end_time: str) -> List[MetricData]:
|
|
2293
|
+
"""
|
|
2294
|
+
Retrieve the number of workflows and steps that ran in a time range.
|
|
2295
|
+
|
|
2296
|
+
Args:
|
|
2297
|
+
start_time: ISO 8601 formatted start time
|
|
2298
|
+
end_time: ISO 8601 formatted end time
|
|
2299
|
+
"""
|
|
2300
|
+
# Convert ISO 8601 times to epoch milliseconds
|
|
2301
|
+
start_epoch_ms = int(
|
|
2302
|
+
datetime.datetime.fromisoformat(start_time).timestamp() * 1000
|
|
2303
|
+
)
|
|
2304
|
+
end_epoch_ms = int(datetime.datetime.fromisoformat(end_time).timestamp() * 1000)
|
|
2305
|
+
|
|
2306
|
+
metrics: List[MetricData] = []
|
|
2307
|
+
|
|
2308
|
+
with self.engine.begin() as c:
|
|
2309
|
+
# Query workflow metrics
|
|
2310
|
+
workflow_query = (
|
|
2311
|
+
sa.select(
|
|
2312
|
+
SystemSchema.workflow_status.c.name,
|
|
2313
|
+
func.count(SystemSchema.workflow_status.c.workflow_uuid).label(
|
|
2314
|
+
"count"
|
|
2315
|
+
),
|
|
2316
|
+
)
|
|
2317
|
+
.where(
|
|
2318
|
+
sa.and_(
|
|
2319
|
+
SystemSchema.workflow_status.c.created_at >= start_epoch_ms,
|
|
2320
|
+
SystemSchema.workflow_status.c.created_at < end_epoch_ms,
|
|
2321
|
+
)
|
|
2322
|
+
)
|
|
2323
|
+
.group_by(SystemSchema.workflow_status.c.name)
|
|
2324
|
+
)
|
|
2325
|
+
|
|
2326
|
+
workflow_results = c.execute(workflow_query).fetchall()
|
|
2327
|
+
for row in workflow_results:
|
|
2328
|
+
metrics.append(
|
|
2329
|
+
MetricData(
|
|
2330
|
+
metric_type="workflow_count",
|
|
2331
|
+
metric_name=row[0],
|
|
2332
|
+
value=row[1],
|
|
2333
|
+
)
|
|
2334
|
+
)
|
|
2335
|
+
|
|
2336
|
+
# Query step metrics
|
|
2337
|
+
step_query = (
|
|
2338
|
+
sa.select(
|
|
2339
|
+
SystemSchema.operation_outputs.c.function_name,
|
|
2340
|
+
func.count().label("count"),
|
|
2341
|
+
)
|
|
2342
|
+
.where(
|
|
2343
|
+
sa.and_(
|
|
2344
|
+
SystemSchema.operation_outputs.c.started_at_epoch_ms
|
|
2345
|
+
>= start_epoch_ms,
|
|
2346
|
+
SystemSchema.operation_outputs.c.started_at_epoch_ms
|
|
2347
|
+
< end_epoch_ms,
|
|
2348
|
+
)
|
|
2349
|
+
)
|
|
2350
|
+
.group_by(SystemSchema.operation_outputs.c.function_name)
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
step_results = c.execute(step_query).fetchall()
|
|
2354
|
+
for row in step_results:
|
|
2355
|
+
metrics.append(
|
|
2356
|
+
MetricData(
|
|
2357
|
+
metric_type="step_count",
|
|
2358
|
+
metric_name=row[0],
|
|
2359
|
+
value=row[1],
|
|
2360
|
+
)
|
|
2361
|
+
)
|
|
2362
|
+
|
|
2363
|
+
return metrics
|