Skip to content

Commit 708550f

Browse files
OwenKephartclnollclaude
authored andcommitted
[dj] Wire job entity evaluation and run request pipeline for Declarative Automation (#22184)
Updates the core run launching machinery for the asset daemon to kick off runs of the relevant jobs when interacting w/ automation results associated with an asset job (in contrast to the previous behavior of always just launching runs against the `__ASSET_JOB`) --------- Co-authored-by: Catherine Noll <catherine@dagsterlabs.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Internal-RevId: ebbc2c79ef0a05a340802990bdffaf3076a31180
1 parent 59ac9b8 commit 708550f

23 files changed

Lines changed: 1937 additions & 48 deletions

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,8 @@ def get_subset_from_serializable_subset(
331331
self, serializable_subset: SerializableEntitySubset[T_EntityKey]
332332
) -> EntitySubset[T_EntityKey] | None:
333333
key = serializable_subset.key
334-
# serialized subsets come from persisted cursors, which may reference key types this
335-
# view does not yet support
336-
if (
337-
isinstance(key, (AssetKey, AssetCheckKey))
338-
and self.asset_graph.has(key)
339-
and serializable_subset.is_compatible_with_partitions_def(self._get_partitions_def(key))
334+
if self.asset_graph.has(key) and serializable_subset.is_compatible_with_partitions_def(
335+
self._get_partitions_def(key)
340336
):
341337
return EntitySubset(
342338
self, key=key, value=_ValidatedEntitySubsetValue(serializable_subset.value)

python_modules/dagster/dagster/_core/definitions/automation_condition_sensor_definition.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
from collections.abc import Mapping, Sequence
22
from functools import partial
3-
from typing import Any, cast
3+
from typing import TYPE_CHECKING, AbstractSet, Any, Union, cast # noqa: UP035
44

55
import dagster._check as check
6-
from dagster._annotations import beta_param, public
6+
from dagster._annotations import (
7+
beta_param,
8+
hidden_param,
9+
only_allow_hidden_params_in_kwargs,
10+
public,
11+
)
12+
from dagster._core.definitions.asset_key import AssetJobKey
713
from dagster._core.definitions.asset_selection import AssetSelection, CoercibleToAssetSelection
814
from dagster._core.definitions.declarative_automation.automation_condition import (
915
AutomationCondition,
1016
)
11-
from dagster._core.definitions.metadata import RawMetadataMapping
17+
from dagster._core.definitions.metadata import JsonMetadataValue, RawMetadataMapping
1218
from dagster._core.definitions.run_request import SensorResult
1319
from dagster._core.definitions.sensor_definition import (
1420
DefaultSensorStatus,
@@ -22,11 +28,46 @@
2228
from dagster._utils import IHasInternalInit
2329
from dagster._utils.tags import normalize_tags
2430

31+
if TYPE_CHECKING:
32+
from dagster._core.remote_representation.external_data import SensorMetadataSnap
33+
2534
MAX_ENTITIES = 500
2635
EMIT_BACKFILLS_METADATA_KEY = "dagster/emit_backfills"
36+
ASSET_JOB_KEYS_METADATA_KEY = "dagster/asset_job_keys"
2737
DEFAULT_AUTOMATION_CONDITION_SENSOR_NAME = "default_automation_condition_sensor"
2838

2939

40+
def asset_job_keys_from_sensor_metadata(
41+
metadata: Union[Mapping[str, Any], "SensorMetadataSnap", None],
42+
) -> AbstractSet[AssetJobKey]:
43+
"""Parses the asset-job keys a sensor is responsible for evaluating out of its metadata.
44+
45+
Accepts two shapes because get_default_automation_condition_sensor_target iterates a
46+
mixed list of sensors: a SensorDefinition's metadata (a normalized
47+
Mapping[str, MetadataValue]) or a RemoteSensor's SensorMetadataSnap. Missing metadata
48+
or a missing key means the sensor claims no job keys; a present key with unexpected
49+
contents is an error.
50+
"""
51+
# local import: external_data imports from this module
52+
from dagster._core.remote_representation.external_data import SensorMetadataSnap
53+
54+
mapping: Mapping[str, Any] | None = (
55+
metadata.standard_metadata if isinstance(metadata, SensorMetadataSnap) else metadata
56+
)
57+
if not mapping or ASSET_JOB_KEYS_METADATA_KEY not in mapping:
58+
return set()
59+
# the sorted list of job names written in __init__ normalizes to a JsonMetadataValue,
60+
# on the definition side and (after the serdes round-trip) on the snapshot side alike
61+
job_names = check.inst(
62+
mapping[ASSET_JOB_KEYS_METADATA_KEY],
63+
JsonMetadataValue,
64+
f"unexpected value for sensor metadata key {ASSET_JOB_KEYS_METADATA_KEY}",
65+
).value
66+
return {
67+
AssetJobKey(job_name=check.str_param(job_name, "job_name")) for job_name in job_names or []
68+
}
69+
70+
3071
def _evaluate(sensor_def: "AutomationConditionSensorDefinition", context: SensorEvaluationContext):
3172
from dagster._core.definitions.automation_tick_evaluation_context import (
3273
AutomationTickEvaluationContext,
@@ -51,6 +92,7 @@ def _evaluate(sensor_def: "AutomationConditionSensorDefinition", context: Sensor
5192
observe_run_tags={},
5293
auto_observe_asset_keys=set(),
5394
asset_selection=sensor_def.asset_selection,
95+
asset_job_keys=sensor_def.asset_job_keys,
5496
emit_backfills=sensor_def.emit_backfills,
5597
default_condition=sensor_def.default_condition,
5698
logger=context.log,
@@ -81,6 +123,13 @@ def not_supported(context) -> None:
81123
@public
82124
@beta_param(param="use_user_code_server")
83125
@beta_param(param="default_condition")
126+
@hidden_param(
127+
param="asset_job_keys",
128+
breaking_version="",
129+
additional_warn_text="This parameter is internal: it is populated automatically on the "
130+
"default automation condition sensor and will be replaced by a job-selection API.",
131+
emit_runtime_warning=False,
132+
)
84133
class AutomationConditionSensorDefinition(SensorDefinition, IHasInternalInit):
85134
"""Targets a set of assets and repeatedly evaluates all the AutomationConditions on all of
86135
those assets to determine which to request runs for.
@@ -159,7 +208,9 @@ def __init__(
159208
emit_backfills: bool = True,
160209
use_user_code_server: bool = False,
161210
default_condition: AutomationCondition | None = None,
211+
**kwargs: Any,
162212
):
213+
only_allow_hidden_params_in_kwargs(AutomationConditionSensorDefinition, kwargs)
163214
self._use_user_code_server = use_user_code_server
164215
check.bool_param(emit_backfills, "allow_backfills")
165216

@@ -180,6 +231,20 @@ def __init__(
180231
if emit_backfills:
181232
metadata = {**(metadata or {}), EMIT_BACKFILLS_METADATA_KEY: True}
182233

234+
# asset-job keys are carried in metadata so they cross the snapshot boundary to
235+
# the daemon (the same channel emit_backfills uses)
236+
asset_job_keys = kwargs.get("asset_job_keys")
237+
if asset_job_keys:
238+
metadata = {
239+
**(metadata or {}),
240+
ASSET_JOB_KEYS_METADATA_KEY: sorted(
241+
key.job_name
242+
for key in check.set_param(
243+
asset_job_keys, "asset_job_keys", of_type=AssetJobKey
244+
)
245+
),
246+
}
247+
183248
super().__init__(
184249
name=check_valid_name(name),
185250
job_name=None,
@@ -211,6 +276,11 @@ def emit_backfills(self) -> bool:
211276
def default_condition(self) -> AutomationCondition | None:
212277
return self._default_condition
213278

279+
@property
280+
def asset_job_keys(self) -> AbstractSet[AssetJobKey]:
281+
"""The asset-job entity keys this sensor is responsible for evaluating."""
282+
return asset_job_keys_from_sensor_metadata(self.metadata)
283+
214284
@property
215285
def sensor_type(self) -> SensorType:
216286
return SensorType.AUTOMATION if self._use_user_code_server else SensorType.AUTO_MATERIALIZE
@@ -229,6 +299,7 @@ def dagster_internal_init( # type: ignore
229299
emit_backfills: bool,
230300
use_user_code_server: bool,
231301
default_condition: AutomationCondition | None,
302+
**kwargs: Any,
232303
) -> "AutomationConditionSensorDefinition":
233304
return AutomationConditionSensorDefinition(
234305
name=name,
@@ -242,6 +313,7 @@ def dagster_internal_init( # type: ignore
242313
emit_backfills=emit_backfills,
243314
use_user_code_server=use_user_code_server,
244315
default_condition=default_condition,
316+
**kwargs,
245317
)
246318

247319
def with_attributes(

python_modules/dagster/dagster/_core/definitions/automation_tick_evaluation_context.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from dagster import PartitionKeyRange
1111
from dagster._core.asset_graph_view.entity_subset import EntitySubset
1212
from dagster._core.definitions.asset_daemon_cursor import AssetDaemonCursor
13-
from dagster._core.definitions.asset_key import AssetCheckKey, AssetOrCheckKey, EntityKey
13+
from dagster._core.definitions.asset_key import (
14+
AssetCheckKey,
15+
AssetJobKey,
16+
AssetOrCheckKey,
17+
EntityKey,
18+
)
1419
from dagster._core.definitions.asset_selection import AssetSelection
1520
from dagster._core.definitions.assets.graph.asset_graph_subset import AssetGraphSubset
1621
from dagster._core.definitions.assets.graph.base_asset_graph import BaseAssetGraph
@@ -57,14 +62,23 @@ def __init__(
5762
emit_backfills: bool,
5863
default_condition: AutomationCondition | None = None,
5964
evaluation_time: datetime.datetime | None = None,
65+
asset_job_keys: AbstractSet[AssetJobKey] | None = None,
6066
):
61-
resolved_entity_keys = {
67+
resolved_entity_keys: set[EntityKey] = {
6268
entity_key
6369
for entity_key in (
6470
asset_selection.resolve(asset_graph) | asset_selection.resolve_checks(asset_graph)
6571
)
6672
if default_condition or asset_graph.get(entity_key).automation_condition is not None
6773
}
74+
# asset selections cannot express job keys, so conditioned jobs are added
75+
# directly from the graph
76+
resolved_entity_keys |= {
77+
key
78+
for key in (asset_job_keys or set())
79+
# default_condition does not extend to jobs
80+
if asset_graph.has(key) and asset_graph.get(key).automation_condition is not None
81+
}
6882
self._total_keys = len(resolved_entity_keys)
6983
self._evaluation_id = evaluation_id
7084
self._evaluator = AutomationConditionEvaluator(
@@ -323,6 +337,44 @@ def _flood_fill_asset_subsets(k: AssetOrCheckKey):
323337
return backfill_request, list(entity_subsets_by_key.values())
324338

325339

340+
def _build_job_run_requests(
341+
asset_job_subsets: Sequence[EntitySubset[AssetJobKey]],
342+
asset_graph: BaseAssetGraph,
343+
run_tags: Mapping[str, str] | None,
344+
) -> Sequence[RunRequest]:
345+
"""Builds RunRequests for job entities whose automation conditions fired this tick.
346+
347+
Unlike asset run requests, which list the specific assets and checks to execute, a
348+
job run request launches the whole job by name: job_name is set and there is no
349+
asset selection.
350+
351+
An unpartitioned job launches as a single run. A partitioned job launches as one
352+
run per requested partition, each tagged with its partition key.
353+
"""
354+
run_requests = []
355+
for subset in asset_job_subsets:
356+
if subset.is_empty:
357+
continue
358+
partitions_def = asset_graph.get(subset.key).partitions_def
359+
if partitions_def is None:
360+
run_requests.append(
361+
RunRequest(job_name=subset.key.job_name, tags=dict(run_tags) if run_tags else {})
362+
)
363+
else:
364+
run_requests.extend(
365+
RunRequest(
366+
job_name=subset.key.job_name,
367+
partition_key=partition_key,
368+
tags={
369+
**(run_tags or {}),
370+
**partitions_def.get_tags_for_partition_key(partition_key),
371+
},
372+
)
373+
for partition_key in sorted(subset.expensively_compute_partition_keys())
374+
)
375+
return sorted(run_requests, key=lambda rr: (rr.job_name or "", rr.partition_key or ""))
376+
377+
326378
def build_run_requests(
327379
entity_subsets: Sequence[EntitySubset],
328380
asset_graph: BaseAssetGraph,
@@ -332,22 +384,28 @@ def build_run_requests(
332384
"""For a single asset in a given tick, the asset will only be part of a run or a backfill, not both.
333385
If the asset is targetd by a backfill, there will only be one backfill that targets the asset.
334386
"""
387+
# job entity subsets become whole-job run requests; everything downstream of this
388+
# split (backfills, partition mapping) deals only in assets and checks
389+
asset_job_subsets = [es for es in entity_subsets if isinstance(es.key, AssetJobKey)]
390+
asset_and_check_subsets = [es for es in entity_subsets if not isinstance(es.key, AssetJobKey)]
391+
job_run_requests = _build_job_run_requests(asset_job_subsets, asset_graph, run_tags)
392+
335393
if emit_backfills:
336-
backfill_run_request, entity_subsets = _build_backfill_request(
337-
entity_subsets, asset_graph, run_tags
394+
backfill_run_request, asset_and_check_subsets = _build_backfill_request(
395+
asset_and_check_subsets, asset_graph, run_tags
338396
)
339397
else:
340398
backfill_run_request = None
341399

342400
run_requests = _build_run_requests_from_partitions_def_mapping(
343-
_get_mapping_from_entity_subsets(entity_subsets, asset_graph),
401+
_get_mapping_from_entity_subsets(asset_and_check_subsets, asset_graph),
344402
asset_graph,
345403
run_tags,
346404
)
347405
if backfill_run_request:
348406
run_requests = [backfill_run_request, *run_requests]
349407

350-
return run_requests
408+
return [*job_run_requests, *run_requests]
351409

352410

353411
def _any_check_uses_automation_condition(

python_modules/dagster/dagster/_core/definitions/job_definition.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,15 @@ def __init__(
243243
"_automation_condition",
244244
"AutomationCondition can only be provided for asset jobs.",
245245
)
246+
# every partitioned job synthesizes a PartitionedConfig internally, so only
247+
# reject when the user actually supplied config for it to resolve
248+
if self._original_config_argument is not None and self.partitioned_config is not None:
249+
raise DagsterInvalidDefinitionError(
250+
f"Job '{self.name}' has both an automation_condition and partitioned run"
251+
" config. Declarative automation submits partitioned-job runs without"
252+
" resolving per-partition config, so this combination is not currently"
253+
" supported."
254+
)
246255

247256
def dagster_internal_init(
248257
*,

0 commit comments

Comments
 (0)