Skip to content

Commit b2d651b

Browse files
committed
[pac] refactor / rename AssetCheckPartitionState
1 parent a052bb1 commit b2d651b

4 files changed

Lines changed: 296 additions & 6 deletions

File tree

python_modules/dagster/dagster/_core/asset_graph_view/asset_graph_view.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -638,16 +638,15 @@ async def _get_partitioned_check_subset_with_status(
638638
status: Optional["AssetCheckExecutionResolvedStatus"],
639639
from_subset: EntitySubset,
640640
) -> EntitySubset[AssetCheckKey]:
641-
from dagster._core.storage.asset_check_status_cache import (
642-
get_updated_asset_check_status_cache_value,
643-
)
641+
from dagster._core.storage.asset_check_state import AssetCheckState
644642

645643
check_node = self.asset_graph.get(key)
646644
if not check_node or not check_node.partitions_def:
647645
check.failed(f"Asset check {key} not found or not partitioned.")
648646

649-
cache_value = get_updated_asset_check_status_cache_value(
650-
key, check_node.partitions_def, self
647+
cache_value = (
648+
await AssetCheckState.gen(self, (key, check_node.partitions_def))
649+
or AssetCheckState.empty()
651650
)
652651

653652
if status is None:
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
from collections import defaultdict
2+
from collections.abc import Iterable, Mapping, Sequence
3+
from typing import TYPE_CHECKING, Optional, TypeAlias
4+
5+
from dagster_shared.record import record
6+
from dagster_shared.serdes import whitelist_for_serdes
7+
8+
from dagster._core.asset_graph_view.serializable_entity_subset import SerializableEntitySubset
9+
from dagster._core.definitions.asset_key import AssetCheckKey
10+
from dagster._core.definitions.partitions.definition.partitions_definition import (
11+
PartitionsDefinition,
12+
)
13+
from dagster._core.loader import LoadableBy, LoadingContext
14+
from dagster._core.storage.asset_check_execution_record import (
15+
AssetCheckExecutionRecordStatus,
16+
AssetCheckExecutionResolvedStatus,
17+
AssetCheckPartitionInfo,
18+
)
19+
from dagster._core.storage.dagster_run import FINISHED_STATUSES, DagsterRunStatus, RunsFilter
20+
21+
if TYPE_CHECKING:
22+
from dagster._core.instance import DagsterInstance
23+
24+
StatusSubsets: TypeAlias = Mapping[
25+
AssetCheckExecutionResolvedStatus, SerializableEntitySubset[AssetCheckKey]
26+
]
27+
InProgressRuns: TypeAlias = Mapping[str, SerializableEntitySubset[AssetCheckKey]]
28+
29+
30+
@whitelist_for_serdes
31+
@record
32+
class AssetCheckState(LoadableBy[tuple[AssetCheckKey, Optional[PartitionsDefinition]]]):
33+
latest_storage_id: int
34+
subsets: StatusSubsets
35+
in_progress_runs: InProgressRuns
36+
37+
def compatible_with(self, partitions_def: Optional[PartitionsDefinition]) -> bool:
38+
subset = next(iter(self.subsets.values()), None)
39+
if subset is None:
40+
return True
41+
return subset.is_compatible_with_partitions_def(partitions_def)
42+
43+
@classmethod
44+
def empty(cls) -> "AssetCheckState":
45+
return cls(latest_storage_id=0, subsets={}, in_progress_runs={})
46+
47+
@classmethod
48+
def _blocking_batch_load(
49+
cls,
50+
keys: Iterable[tuple[AssetCheckKey, Optional[PartitionsDefinition]]],
51+
context: LoadingContext,
52+
) -> Iterable[Optional["AssetCheckState"]]:
53+
keys = list(keys)
54+
mapping = context.instance.event_log_storage.get_asset_check_state(keys)
55+
return [mapping.get(check_key) for check_key, _ in keys]
56+
57+
def with_updates(
58+
self,
59+
key: AssetCheckKey,
60+
partitions_def: Optional[PartitionsDefinition],
61+
partition_records: Sequence[AssetCheckPartitionInfo],
62+
run_statuses: Mapping[str, DagsterRunStatus],
63+
) -> "AssetCheckState":
64+
latest_storage_id = max(
65+
(
66+
max(r.last_storage_id, r.last_materialization_storage_id or 0)
67+
for r in partition_records
68+
),
69+
default=self.latest_storage_id,
70+
)
71+
72+
subsets = {
73+
status: self.subsets.get(status, SerializableEntitySubset.empty(key, partitions_def))
74+
for status in AssetCheckExecutionResolvedStatus
75+
}
76+
in_progress_runs = dict(self.in_progress_runs)
77+
78+
# update all subsets based on the new partition records
79+
subsets, in_progress_runs = _process_partition_records(
80+
key, partitions_def, subsets, in_progress_runs, partition_records
81+
)
82+
# then check the run statuses and resolve any previously in-progress runs that have completed
83+
subsets, in_progress_runs = _process_run_statuses(
84+
key, partitions_def, subsets, in_progress_runs, run_statuses
85+
)
86+
return AssetCheckState(
87+
latest_storage_id=latest_storage_id,
88+
subsets=subsets,
89+
in_progress_runs=in_progress_runs,
90+
)
91+
92+
93+
def bulk_update_asset_check_state(
94+
instance: "DagsterInstance",
95+
keys: Sequence[tuple[AssetCheckKey, Optional[PartitionsDefinition]]],
96+
initial_states: Mapping[AssetCheckKey, "AssetCheckState"],
97+
) -> Mapping[AssetCheckKey, "AssetCheckState"]:
98+
check_keys = [key for key, _ in keys]
99+
partitions_defs_by_key = {key: partitions_def for key, partitions_def in keys}
100+
101+
# we prefer to do a single fetch for all keys, so we use the minimum storage id of the initial states
102+
storage_id = min(state.latest_storage_id for state in initial_states.values())
103+
infos = instance.event_log_storage.get_asset_check_partition_info(
104+
check_keys, after_storage_id=storage_id
105+
)
106+
107+
# find the set of run ids we need to fetch to resolve the in-progress runs, and
108+
# group the partition infos by check key
109+
run_ids_to_fetch: set[str] = set().union(
110+
*(state.in_progress_runs.keys() for state in initial_states.values())
111+
)
112+
infos_by_key: dict[AssetCheckKey, list[AssetCheckPartitionInfo]] = defaultdict(list)
113+
for info in infos:
114+
infos_by_key[info.check_key].append(info)
115+
if info.last_execution_status == AssetCheckExecutionRecordStatus.PLANNED:
116+
run_ids_to_fetch.add(info.last_planned_run_id)
117+
118+
# do a bulk fetch for runs across all states
119+
finished_runs = (
120+
instance.get_runs(
121+
filters=RunsFilter(run_ids=list(run_ids_to_fetch), statuses=FINISHED_STATUSES)
122+
)
123+
if len(run_ids_to_fetch) > 0
124+
else []
125+
)
126+
finished_runs_status_by_id = {run.run_id: run.status for run in finished_runs}
127+
return {
128+
key: initial_states[key].with_updates(
129+
key, partitions_defs_by_key[key], infos_by_key[key], finished_runs_status_by_id
130+
)
131+
for key in check_keys
132+
}
133+
134+
135+
def _valid_partition_key(
136+
partition_key: Optional[str], partitions_def: Optional[PartitionsDefinition]
137+
) -> bool:
138+
if partitions_def is None:
139+
return partition_key is None
140+
else:
141+
return partition_key is not None and partitions_def.has_partition_key(partition_key)
142+
143+
144+
def _process_partition_records(
145+
key: AssetCheckKey,
146+
partitions_def: Optional[PartitionsDefinition],
147+
subsets: StatusSubsets,
148+
in_progress_runs: InProgressRuns,
149+
partition_infos: Sequence[AssetCheckPartitionInfo],
150+
) -> tuple[StatusSubsets, InProgressRuns]:
151+
"""Returns a set of updated subsets based on new partition records and the latest materialization storage ids."""
152+
new_subsets = dict(subsets)
153+
new_in_progress_runs = dict(in_progress_runs)
154+
155+
for partition_record in partition_infos:
156+
pk = partition_record.partition_key
157+
if not _valid_partition_key(pk, partitions_def):
158+
continue
159+
160+
partition_subset = SerializableEntitySubset.from_coercible_value(key, pk, partitions_def)
161+
162+
if partition_record.last_execution_status == AssetCheckExecutionRecordStatus.PLANNED:
163+
# Add to IN_PROGRESS and track run
164+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS] = new_subsets[
165+
AssetCheckExecutionResolvedStatus.IN_PROGRESS
166+
].compute_union(partition_subset)
167+
run_id = partition_record.last_planned_run_id
168+
new_in_progress_runs[run_id] = new_in_progress_runs.get(
169+
run_id, SerializableEntitySubset.empty(key, partitions_def)
170+
).compute_union(partition_subset)
171+
172+
elif partition_record.last_execution_status in (
173+
AssetCheckExecutionRecordStatus.SUCCEEDED,
174+
AssetCheckExecutionRecordStatus.FAILED,
175+
):
176+
if partition_record.is_current:
177+
# Check is current, set appropriate status
178+
status = (
179+
AssetCheckExecutionResolvedStatus.SUCCEEDED
180+
if partition_record.last_execution_status
181+
== AssetCheckExecutionRecordStatus.SUCCEEDED
182+
else AssetCheckExecutionResolvedStatus.FAILED
183+
)
184+
new_subsets[status] = new_subsets[status].compute_union(partition_subset)
185+
else:
186+
# new materialization, clear the check status
187+
for status in new_subsets:
188+
new_subsets[status] = new_subsets[status].compute_difference(partition_subset)
189+
190+
return new_subsets, new_in_progress_runs
191+
192+
193+
def _process_run_statuses(
194+
key: AssetCheckKey,
195+
partitions_def: Optional[PartitionsDefinition],
196+
subsets: StatusSubsets,
197+
in_progress_runs: InProgressRuns,
198+
run_statuses: Mapping[str, DagsterRunStatus],
199+
) -> tuple[StatusSubsets, InProgressRuns]:
200+
"""Resolve in-progress runs that have completed.
201+
202+
This checks if any runs tracked in in_progress_runs have finished,
203+
and moves their partitions to SKIPPED or EXECUTION_FAILED.
204+
"""
205+
if not in_progress_runs:
206+
return subsets, in_progress_runs
207+
208+
delta_skipped, delta_execution_failed, resolved_run_ids = _resolve_in_progress_subsets(
209+
key, partitions_def, in_progress_runs, run_statuses
210+
)
211+
212+
new_in_progress_runs = {
213+
run_id: subset
214+
for run_id, subset in in_progress_runs.items()
215+
if run_id not in resolved_run_ids
216+
}
217+
218+
new_subsets = dict(subsets)
219+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS] = (
220+
new_subsets[AssetCheckExecutionResolvedStatus.IN_PROGRESS]
221+
.compute_difference(delta_skipped)
222+
.compute_difference(delta_execution_failed)
223+
)
224+
new_subsets[AssetCheckExecutionResolvedStatus.SKIPPED] = new_subsets[
225+
AssetCheckExecutionResolvedStatus.SKIPPED
226+
].compute_union(delta_skipped)
227+
new_subsets[AssetCheckExecutionResolvedStatus.EXECUTION_FAILED] = new_subsets[
228+
AssetCheckExecutionResolvedStatus.EXECUTION_FAILED
229+
].compute_union(delta_execution_failed)
230+
231+
return new_subsets, new_in_progress_runs
232+
233+
234+
def _resolve_in_progress_subsets(
235+
key: AssetCheckKey,
236+
partitions_def: Optional[PartitionsDefinition],
237+
in_progress_runs: InProgressRuns,
238+
run_statuses: Mapping[str, DagsterRunStatus],
239+
) -> tuple[
240+
SerializableEntitySubset[AssetCheckKey],
241+
SerializableEntitySubset[AssetCheckKey],
242+
set[str],
243+
]:
244+
"""Resolve in-progress runs that have completed.
245+
246+
Returns:
247+
Tuple of (delta_skipped, delta_execution_failed, resolved_run_ids)
248+
"""
249+
empty_subset = SerializableEntitySubset.empty(key, partitions_def)
250+
delta_skipped = empty_subset
251+
delta_execution_failed = empty_subset
252+
resolved_run_ids: set[str] = set()
253+
254+
for run_id, run_status in run_statuses.items():
255+
if run_status in FINISHED_STATUSES and run_id in in_progress_runs:
256+
resolved_run_ids.add(run_id)
257+
run_subset = in_progress_runs[run_id]
258+
if run_status == DagsterRunStatus.FAILURE:
259+
delta_execution_failed = delta_execution_failed.compute_union(run_subset)
260+
else:
261+
delta_skipped = delta_skipped.compute_union(run_subset)
262+
263+
return delta_skipped, delta_execution_failed, resolved_run_ids

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
if TYPE_CHECKING:
5252
from dagster._core.events.log import EventLogEntry
53+
from dagster._core.storage.asset_check_state import AssetCheckState
5354
from dagster._core.storage.partition_status_cache import AssetStatusCacheValue
5455

5556

@@ -659,6 +660,28 @@ def get_asset_check_partition_info(
659660
"""Get asset check partition records with execution status and planned run info."""
660661
pass
661662

663+
def get_stored_asset_check_state(
664+
self, keys: Sequence[AssetCheckKey]
665+
) -> Mapping[AssetCheckKey, "AssetCheckState"]:
666+
"""Get the current stored asset check state for a list of asset checks and their
667+
associated partitions definitions. This method is not guaranteed to return a
668+
state object that is up to date with the latest events.
669+
"""
670+
from dagster._core.storage.asset_check_state import AssetCheckState
671+
672+
return {key: AssetCheckState.empty() for key in keys}
673+
674+
def get_asset_check_state(
675+
self, keys: Sequence[tuple[AssetCheckKey, Optional[PartitionsDefinition]]]
676+
) -> Mapping[AssetCheckKey, "AssetCheckState"]:
677+
from dagster._core.storage.asset_check_state import bulk_update_asset_check_state
678+
679+
return bulk_update_asset_check_state(
680+
self._instance,
681+
keys,
682+
initial_states=self.get_stored_asset_check_state([key for key, _ in keys]),
683+
)
684+
662685
@abstractmethod
663686
def fetch_materializations(
664687
self,

python_modules/dagster/dagster/_core/storage/legacy_storage.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from dagster import _check as check
77
from dagster._config.config_schema import UserConfigSchema
8-
from dagster._core.definitions.asset_checks.asset_check_spec import AssetCheckKey
98
from dagster._core.definitions.asset_key import EntityKey
109
from dagster._core.definitions.declarative_automation.serialized_objects import (
1110
AutomationConditionEvaluationWithRunIds,
@@ -62,6 +61,7 @@
6261
)
6362
from dagster._core.snap.execution_plan_snapshot import ExecutionPlanSnapshot
6463
from dagster._core.snap.job_snapshot import JobSnap
64+
from dagster._core.storage.asset_check_state import AssetCheckState
6565
from dagster._core.storage.dagster_run import (
6666
DagsterRun,
6767
DagsterRunStatsSnapshot,
@@ -776,6 +776,11 @@ def get_asset_check_partition_info(
776776
keys=keys, after_storage_id=after_storage_id, partition_keys=partition_keys
777777
)
778778

779+
def get_stored_asset_check_state(
780+
self, keys: Sequence["AssetCheckKey"]
781+
) -> Mapping["AssetCheckKey", "AssetCheckState"]:
782+
return self._storage.event_log_storage.get_stored_asset_check_state(keys)
783+
779784

780785
class LegacyScheduleStorage(ScheduleStorage, ConfigurableClass):
781786
def __init__(self, storage: DagsterStorage, inst_data: Optional[ConfigurableClassData] = None):

0 commit comments

Comments
 (0)