Skip to content

Commit cf87c27

Browse files
committed
Fix DagsterInvalidInvocationError when reading historical entity subsets with deleted partitions
1 parent 9fbbf5e commit cf87c27

2 files changed

Lines changed: 66 additions & 12 deletions

File tree

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import operator
22
from collections.abc import Callable, Sequence
33
from dataclasses import dataclass, replace
4-
from typing import Any, Generic, Optional, TypeAlias
4+
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeAlias
5+
6+
if TYPE_CHECKING:
7+
from dagster._core.asset_graph_view.entity_subset import AssetSubset
58

69
from dagster_shared.serdes.serdes import DataclassSerializer, whitelist_for_serdes
710
from typing_extensions import Self
811

912
import dagster._check as check
1013
from dagster._core.definitions.asset_key import T_EntityKey
11-
from dagster._core.definitions.events import AssetKeyPartitionKey
1214
from dagster._core.definitions.partitions.context import partition_loading_context
1315
from dagster._core.definitions.partitions.definition import PartitionsDefinition
1416
from dagster._core.definitions.partitions.snap.snap import PartitionsSnap
@@ -19,6 +21,7 @@
1921
TimeWindowPartitionsSubset,
2022
)
2123
from dagster._core.definitions.partitions.subset.key_ranges import KeyRangesPartitionsSubset
24+
from dagster._core.errors import DagsterInvalidInvocationError
2225

2326
EntitySubsetValue: TypeAlias = bool | PartitionsSubset
2427

@@ -43,6 +46,7 @@ def before_pack(self, value: "SerializableEntitySubset") -> "SerializableEntityS
4346
storage_field_names={"key": "asset_key"},
4447
old_storage_names={"AssetSubset"},
4548
)
49+
4650
@dataclass(frozen=True)
4751
class SerializableEntitySubset(Generic[T_EntityKey]):
4852
"""Represents a serializable subset of a given EntityKey."""
@@ -123,18 +127,29 @@ def subset_value(self) -> PartitionsSubset:
123127

124128
@property
125129
def size(self) -> int:
130+
126131
if not self.is_partitioned:
127132
return int(self.bool_value)
128-
else:
133+
134+
try:
129135
return len(self.subset_value)
130-
136+
except DagsterInvalidInvocationError:
137+
# If a dynamic partition key was deleted out from under this historical
138+
# subset evaluation, we gracefully fall back to a size of 0.
139+
return 0
140+
131141
@property
132142
def is_empty(self) -> bool:
133-
if self.is_partitioned:
134-
return self.subset_value.is_empty
135-
else:
143+
144+
if not self.is_partitioned:
136145
return not self.bool_value
137146

147+
try:
148+
return self.subset_value.is_empty
149+
except DagsterInvalidInvocationError:
150+
# If partition keys no longer exist, treat the historical subset as empty
151+
return True
152+
138153
def is_compatible_with_partitions_def(
139154
self, partitions_def: PartitionsDefinition | None
140155
) -> bool:
@@ -188,11 +203,16 @@ def compute_union(self, other: Self) -> Self:
188203
def compute_intersection(self, other: Self) -> Self:
189204
return self._oper(other, operator.and_)
190205

191-
def __contains__(self, item: AssetKeyPartitionKey) -> bool:
206+
def __contains__(self, item: "AssetSubset") -> bool:
192207
if not self.is_partitioned:
193-
return item.asset_key == self.key and item.partition_key is None and self.bool_value
194-
else:
195-
return item.asset_key == self.key and item.partition_key in self.subset_value
196-
208+
return self.bool_value and item.bool_value
209+
210+
try:
211+
return item.partition_key in self.subset_value
212+
except DagsterInvalidInvocationError:
213+
# If a dynamic partition key was deleted out from under this historical
214+
# subset evaluation, gracefully treat it as not contained.
215+
return False
216+
197217
def __repr__(self) -> str:
198218
return f"{self.__class__.__name__}<{self.key}>({self.value})"

python_modules/dagster/dagster_tests/core_tests/asset_graph_view_tests/test_serializable_entity_subset.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from unittest import mock
2+
13
import dagster as dg
24
import pytest
35
from dagster._check import CheckError
46
from dagster._core.asset_graph_view.serializable_entity_subset import SerializableEntitySubset
57
from dagster._core.definitions.partitions.subset import DefaultPartitionsSubset
8+
from dagster._core.errors import DagsterInvalidInvocationError
69

710

811
def test_union():
@@ -354,3 +357,34 @@ def test_is_compatible_with_partitions_def_default_subset_time_window():
354357
assert (
355358
out_of_range_subset.is_compatible_with_partitions_def(time_window_partitions_def) is False
356359
)
360+
361+
362+
def test_deleted_partition_graceful_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
363+
# Creates a valid, empty subset using the framework helper
364+
partitions_def = dg.StaticPartitionsDefinition([])
365+
broken_subset = partitions_def.empty_subset()
366+
367+
# Safely mock the methods on the class level using the monkeypatch fixture
368+
# automatically tears down and restores original functionality after the test ends
369+
monkeypatch.setattr(
370+
DefaultPartitionsSubset,
371+
"__len__",
372+
lambda self: (_ for _ in ()).throw(DagsterInvalidInvocationError("Nonexistent partition keys")),
373+
)
374+
monkeypatch.setattr(
375+
DefaultPartitionsSubset,
376+
"is_empty",
377+
property(lambda self: (_ for _ in ()).throw(DagsterInvalidInvocationError("Nonexistent partition keys"))),
378+
)
379+
380+
# Instantiate wrapper class
381+
entity_subset = SerializableEntitySubset(key=dg.AssetKey("test_asset"), value=broken_subset)
382+
383+
# Assert that size and is_empty gracefully handle the error fallback
384+
assert entity_subset.size == 0
385+
assert entity_subset.is_empty is True
386+
387+
# Test that membership checking (__contains__) also handles the fallback safely
388+
mock_item = mock.MagicMock()
389+
mock_item.partition_key = "deleted_key"
390+
assert (mock_item in entity_subset) is False

0 commit comments

Comments
 (0)