Skip to content

Commit 243e63c

Browse files
committed
[pac] Add get_asset_check_partitions storage method
1 parent 5bcf6e9 commit 243e63c

7 files changed

Lines changed: 478 additions & 2 deletions

File tree

python_modules/dagster/dagster/_core/storage/asset_check_execution_record.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from collections.abc import Iterable
33
from typing import NamedTuple, Optional, cast
44

5+
from dagster_shared.record import record
56
from dagster_shared.serdes import deserialize_value
67

78
import dagster._check as check
@@ -238,3 +239,18 @@ async def targets_latest_materialization(self, loading_context: LoadingContext)
238239
)
239240
else:
240241
check.failed(f"Unexpected check status {resolved_status}")
242+
243+
244+
@record
245+
class AssetCheckPartitionRecord:
246+
partition_key: Optional[str]
247+
# the status of the last execution of the check
248+
last_execution_status: AssetCheckExecutionRecordStatus
249+
# the storage id of the materialization that the last execution of the check targeted
250+
last_execution_target_materialization_storage_id: Optional[int]
251+
# the run id of the last planned event for the check
252+
last_planned_run_id: str
253+
# the storage id of the last event for the check
254+
last_storage_id: int
255+
# the storage id of the last materialization for the asset this check targets
256+
last_materialization_storage_id: Optional[int]
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
from collections.abc import Mapping
2+
from typing import Optional
3+
4+
from dagster_shared.record import record
5+
from dagster_shared.serdes import whitelist_for_serdes
6+
7+
from dagster._core.asset_graph_view.serializable_entity_subset import SerializableEntitySubset
8+
from dagster._core.definitions.asset_key import AssetCheckKey
9+
from dagster._core.definitions.partitions.definition.partitions_definition import (
10+
PartitionsDefinition,
11+
)
12+
from dagster._core.loader import LoadingContext
13+
from dagster._core.storage.asset_check_execution_record import (
14+
AssetCheckExecutionRecordStatus,
15+
AssetCheckExecutionResolvedStatus,
16+
AssetCheckPartitionRecord,
17+
)
18+
from dagster._core.storage.dagster_run import DagsterRunStatus, RunRecord
19+
20+
21+
@whitelist_for_serdes
22+
@record
23+
class AssetCheckStatusCacheValue:
24+
latest_storage_id: int
25+
subsets: Mapping[AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]]
26+
in_progress_runs: Mapping[str, SerializableEntitySubset[AssetCheckKey]]
27+
28+
def compatible_with(self, partitions_def: Optional[PartitionsDefinition]) -> bool:
29+
subset = next(iter(self.subsets.values()), None)
30+
if subset is None:
31+
return True
32+
return subset.is_compatible_with_partitions_def(partitions_def)
33+
34+
@classmethod
35+
def empty(cls) -> "AssetCheckStatusCacheValue":
36+
return cls(latest_storage_id=0, subsets={}, in_progress_runs={})
37+
38+
39+
def _update_subsets_from_partition_records(
40+
partition_records: Mapping[str, AssetCheckPartitionRecord],
41+
key: AssetCheckKey,
42+
partitions_def: PartitionsDefinition,
43+
initial_subsets: dict[
44+
AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]
45+
],
46+
initial_in_progress_runs: dict[str, SerializableEntitySubset[AssetCheckKey]],
47+
) -> tuple[
48+
dict[AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]],
49+
dict[str, SerializableEntitySubset[AssetCheckKey]],
50+
]:
51+
"""Returns a set of updated subsets based on new partition records and the latest materialization storage ids."""
52+
new_subsets = {
53+
status: initial_subsets.get(status, SerializableEntitySubset.empty(key, partitions_def))
54+
for status in AssetCheckExecutionResolvedStatus
55+
}
56+
new_in_progress_runs = dict(initial_in_progress_runs)
57+
empty_subset = SerializableEntitySubset.empty(key, partitions_def)
58+
59+
for pk, check_record in partition_records.items():
60+
if pk is None or not partitions_def.has_partition_key(pk):
61+
continue
62+
63+
partition_subset = SerializableEntitySubset.from_coercible_value(key, pk, partitions_def)
64+
65+
if (check_record.last_materialization_storage_id or 0) > check_record.last_storage_id:
66+
# new materialization, clear the check status
67+
for status in new_subsets:
68+
new_subsets[status] = new_subsets[status].compute_difference(partition_subset)
69+
70+
elif check_record.last_execution_status == AssetCheckExecutionRecordStatus.PLANNED:
71+
# Add to IN_PROGRESS and track run
72+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS] = new_subsets[
73+
AssetCheckExecutionResolvedStatus.IN_PROGRESS
74+
].compute_union(partition_subset)
75+
run_id = check_record.last_planned_run_id
76+
new_in_progress_runs[run_id] = new_in_progress_runs.get(
77+
run_id, empty_subset
78+
).compute_union(partition_subset)
79+
80+
elif check_record.last_execution_status in (
81+
AssetCheckExecutionRecordStatus.SUCCEEDED,
82+
AssetCheckExecutionRecordStatus.FAILED,
83+
):
84+
last_mat_storage_id = check_record.last_materialization_storage_id
85+
last_target_mat_storage_id = (
86+
check_record.last_execution_target_materialization_storage_id
87+
)
88+
89+
if last_mat_storage_id is None or last_mat_storage_id == last_target_mat_storage_id:
90+
# Check is current, set appropriate status
91+
status = (
92+
AssetCheckExecutionResolvedStatus.SUCCEEDED
93+
if check_record.last_execution_status
94+
== AssetCheckExecutionRecordStatus.SUCCEEDED
95+
else AssetCheckExecutionResolvedStatus.FAILED
96+
)
97+
new_subsets[status] = new_subsets[status].compute_union(partition_subset)
98+
# else: stale check, partition stays unknown (already cleared)
99+
100+
return new_subsets, new_in_progress_runs
101+
102+
103+
def _apply_in_progress_resolution(
104+
loading_context: LoadingContext,
105+
key: AssetCheckKey,
106+
partitions_def: PartitionsDefinition,
107+
subsets: dict[AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]],
108+
in_progress_runs: dict[str, SerializableEntitySubset[AssetCheckKey]],
109+
) -> tuple[
110+
dict[AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]],
111+
dict[str, SerializableEntitySubset[AssetCheckKey]],
112+
]:
113+
"""Resolve in-progress runs that have completed.
114+
115+
This checks if any runs tracked in in_progress_runs have finished,
116+
and moves their partitions to SKIPPED or EXECUTION_FAILED.
117+
"""
118+
if not in_progress_runs:
119+
return subsets, in_progress_runs
120+
121+
delta_skipped, delta_execution_failed, resolved_run_ids = _resolve_in_progress_subsets(
122+
loading_context, key, partitions_def, in_progress_runs
123+
)
124+
125+
new_in_progress_runs = {
126+
run_id: subset
127+
for run_id, subset in in_progress_runs.items()
128+
if run_id not in resolved_run_ids
129+
}
130+
131+
new_subsets = dict(subsets)
132+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS] = (
133+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS]
134+
.compute_difference(delta_skipped)
135+
.compute_difference(delta_execution_failed)
136+
)
137+
new_subsets[AssetCheckExecutionResolvedStatus.SKIPPED] = new_subsets[
138+
AssetCheckExecutionResolvedStatus.SKIPPED
139+
].compute_union(delta_skipped)
140+
new_subsets[AssetCheckExecutionResolvedStatus.EXECUTION_FAILED] = new_subsets[
141+
AssetCheckExecutionResolvedStatus.EXECUTION_FAILED
142+
].compute_union(delta_execution_failed)
143+
144+
return new_subsets, new_in_progress_runs
145+
146+
147+
def _resolve_in_progress_subsets(
148+
loading_context: LoadingContext,
149+
key: AssetCheckKey,
150+
partitions_def: PartitionsDefinition,
151+
in_progress_runs: Mapping[str, SerializableEntitySubset[AssetCheckKey]],
152+
) -> tuple[
153+
SerializableEntitySubset[AssetCheckKey],
154+
SerializableEntitySubset[AssetCheckKey],
155+
set[str],
156+
]:
157+
"""Resolve in-progress runs that have completed.
158+
159+
Returns:
160+
Tuple of (delta_skipped, delta_execution_failed, resolved_run_ids)
161+
"""
162+
run_ids = list(in_progress_runs.keys())
163+
run_records = RunRecord.blocking_get_many(loading_context, in_progress_runs.keys())
164+
165+
empty_subset = SerializableEntitySubset.empty(key, partitions_def)
166+
delta_skipped = empty_subset
167+
delta_execution_failed = empty_subset
168+
resolved_run_ids: set[str] = set()
169+
170+
for run_id, run_record in zip(run_ids, run_records):
171+
if run_record is None or run_record.dagster_run.is_finished:
172+
resolved_run_ids.add(run_id)
173+
if run_record and run_record.dagster_run.status == DagsterRunStatus.FAILURE:
174+
delta_execution_failed = delta_execution_failed.compute_union(
175+
in_progress_runs[run_id]
176+
)
177+
else:
178+
delta_skipped = delta_skipped.compute_union(in_progress_runs[run_id])
179+
180+
return delta_skipped, delta_execution_failed, resolved_run_ids
181+
182+
183+
def get_updated_asset_check_status_cache_value(
184+
key: AssetCheckKey,
185+
partitions_def: PartitionsDefinition,
186+
loading_context: LoadingContext,
187+
) -> AssetCheckStatusCacheValue:
188+
"""Compute an updated cache value for the given asset check using per-partition records.
189+
190+
This function fetches new check and materialization events, identifies affected partitions,
191+
and resolves the status for each partition by comparing the latest check execution against
192+
the latest materialization.
193+
"""
194+
current_value = None # TODO: actually store / load this
195+
if current_value is None or not current_value.compatible_with(partitions_def):
196+
current_value = AssetCheckStatusCacheValue.empty()
197+
198+
empty_subset = SerializableEntitySubset.empty(key, partitions_def)
199+
200+
# Phase 1: Fetch new check records and partitions with new materializations
201+
check_records = loading_context.instance.event_log_storage.get_asset_check_partition_records(
202+
key, after_storage_id=current_value.latest_storage_id
203+
)
204+
205+
# Phase 2: Update subsets from partition records
206+
initial_subsets = {
207+
status: current_value.subsets.get(status, empty_subset)
208+
for status in AssetCheckExecutionResolvedStatus
209+
}
210+
if not check_records:
211+
# No updates needed, just resolve in-progress runs
212+
new_subsets, new_in_progress_runs = initial_subsets, dict(current_value.in_progress_runs)
213+
else:
214+
# Filter out None partition keys (only handle partitioned assets)
215+
partition_records: dict[str, AssetCheckPartitionRecord] = {}
216+
for r in check_records:
217+
if r.partition_key is not None:
218+
partition_records[r.partition_key] = r
219+
new_subsets, new_in_progress_runs = _update_subsets_from_partition_records(
220+
partition_records,
221+
key,
222+
partitions_def,
223+
initial_subsets,
224+
dict(current_value.in_progress_runs),
225+
)
226+
227+
# Phase 3: Resolve completed in-progress runs
228+
new_subsets, new_in_progress_runs = _apply_in_progress_resolution(
229+
loading_context, key, partitions_def, new_subsets, new_in_progress_runs
230+
)
231+
232+
# Compute new check cursor
233+
new_latest_storage_id = max(
234+
(max(r.last_storage_id, r.last_materialization_storage_id or 0) for r in check_records),
235+
default=current_value.latest_storage_id,
236+
)
237+
return AssetCheckStatusCacheValue(
238+
latest_storage_id=new_latest_storage_id,
239+
subsets=new_subsets,
240+
in_progress_runs=new_in_progress_runs,
241+
)

python_modules/dagster/dagster/_core/storage/event_log/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from dagster._core.storage.asset_check_execution_record import (
3737
AssetCheckExecutionRecord,
3838
AssetCheckExecutionRecordStatus,
39+
AssetCheckPartitionRecord,
3940
)
4041
from dagster._core.storage.dagster_run import DagsterRunStatsSnapshot
4142
from dagster._core.storage.partition_status_cache import get_and_update_asset_status_cache_value
@@ -648,6 +649,15 @@ def get_latest_asset_check_execution_by_key(
648649
"""Get the latest executions for a list of asset checks."""
649650
pass
650651

652+
@abstractmethod
653+
def get_asset_check_partition_records(
654+
self,
655+
check_key: AssetCheckKey,
656+
after_storage_id: Optional[int] = None,
657+
) -> Sequence[AssetCheckPartitionRecord]:
658+
"""Get asset check partition records with execution status and planned run info."""
659+
pass
660+
651661
@abstractmethod
652662
def fetch_materializations(
653663
self,

0 commit comments

Comments
 (0)