Skip to content

Commit bdd1b3b

Browse files
committed
MAG L1C: wait for the previous day's L1C when it is in flight or expected
Downlinks arrive in multi-day batches, so day N's job can start before day N-1's L1C exists. _check_for_running_dependencies now also reports a running dependency while day N-1's L1C job is in flight (sensor or backfill run) or expected (day N-1 has L1B data but no L1C and no finished L1C run), feeding the existing RetryRequested path. On the final retry the job proceeds without the previous day, so the wait can delay a run but never fail one.
1 parent 02d0225 commit bdd1b3b

2 files changed

Lines changed: 384 additions & 9 deletions

File tree

sds_data_manager/orchestration/custom_behavior/mag.py

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
import datetime
44
import re
55

6+
from dagster import DagsterRunStatus, RunsFilter
67
from imap_data_access import processing_input
78

89
from sds_data_manager.orchestration import imap_job, types
10+
from sds_data_manager.orchestration.dagster_utilities import (
11+
parse_dates_from_partition_key,
12+
)
913
from sds_data_manager.orchestration.job_handler_registry import JobBuilderRegistry
1014

15+
# run_job retries with RetryRequested(max_retries=10) when
16+
# _check_for_running_dependencies returns True. On the last allowed attempt
17+
# MagL1CJob stops waiting for the previous day's L1C, so the added wait can
18+
# delay a run but never fail one.
19+
FINAL_RETRY_NUMBER = 10
20+
1121

1222
@JobBuilderRegistry.register("mag", "l1c", "norm-mago")
1323
@JobBuilderRegistry.register("mag", "l1c", "norm-magi")
@@ -20,13 +30,108 @@ class MagL1CJob(imap_job.IMAPJobHandler):
2030
declared as an input in imap_mag_dependencies.yaml: a declared self-input
2131
would put a cycle in the Dagster asset graph, and the generic input query
2232
would feed a reprocessing run its own earlier output. The file is fetched
23-
here at input-collection time instead. If the previous day's L1C does not
24-
exist (or its job has not finished), processing proceeds with the current
25-
day alone. Reprocessing runs are not ordered by date, so a reprocessed
26-
day can inherit the previous generation's L1C from the day before;
27-
reprocess in date order when regenerated timeline continuity matters.
33+
here at input-collection time instead.
34+
35+
Downlinks arrive in multi-day batches, so day N's job can start before
36+
day N-1's L1C has been built. _check_for_running_dependencies therefore
37+
treats a pending previous-day L1C as a running dependency: the job
38+
retries while day N-1's L1C job is in flight or expected (day N-1 has
39+
L1B data but no L1C and no finished L1C run), proceeds immediately when
40+
day N-1 provably has nothing to deliver, and proceeds without the
41+
previous day on the last retry. A reprocessed day can still inherit the
42+
previous generation's L1C when the previous day's rerun is not in flight
43+
at the time (acceptable per MAG when that earlier version was complete).
2844
"""
2945

46+
def _check_for_running_dependencies(self, context):
47+
"""Also treat a pending previous-day L1C as a running dependency."""
48+
if super()._check_for_running_dependencies(context):
49+
return True
50+
if context.retry_number >= FINAL_RETRY_NUMBER:
51+
context.log.info(
52+
"Out of retries waiting for the previous day's L1C; "
53+
"proceeding without it."
54+
)
55+
return False
56+
return self._previous_day_l1c_pending(context)
57+
58+
def _previous_day_l1c_pending(self, context):
59+
"""Return True while the previous day's L1C is in flight or expected."""
60+
target_start, _ = parse_dates_from_partition_key(context.partition_key)
61+
# One day's partition ends at the exact midnight the next day's
62+
# begins, and _get_overlapping_target_partitions matches inclusively,
63+
# so trim one second from both ends of the previous day: the window
64+
# then touches only day N-1's partition, not day N-2's (which ends at
65+
# day N-1's midnight) or this run's own (which starts at target_start).
66+
previous_day_keys = set(
67+
self._get_overlapping_target_partitions(
68+
None,
69+
target_start - datetime.timedelta(days=1, seconds=-1),
70+
target_start - datetime.timedelta(seconds=1),
71+
context.instance,
72+
)
73+
)
74+
if not previous_day_keys:
75+
return False
76+
77+
own_asset = self.job_config.outputs[0].to_dagster_asset()
78+
in_flight_runs = context.instance.get_runs(
79+
filters=RunsFilter(
80+
statuses=[
81+
DagsterRunStatus.QUEUED,
82+
DagsterRunStatus.STARTING,
83+
DagsterRunStatus.STARTED,
84+
]
85+
)
86+
)
87+
for run in in_flight_runs:
88+
if run.tags.get("dagster/partition") not in previous_day_keys:
89+
continue
90+
# Sensor-requested runs carry this job's name; reprocessing
91+
# backfill runs carry the asset selection instead.
92+
if run.job_name == self.dagster_job_name or own_asset in (
93+
run.asset_selection or ()
94+
):
95+
context.log.info(
96+
f"Previous day's L1C job is in flight ({run.run_id}); waiting."
97+
)
98+
return True
99+
100+
materialized_l1c = set(context.instance.get_materialized_partitions(own_asset))
101+
if materialized_l1c & previous_day_keys:
102+
return False # a previous-day L1C exists; it will be delivered
103+
104+
if not any(
105+
previous_day_keys
106+
& set(context.instance.get_materialized_partitions(dep.to_dagster_asset()))
107+
for dep in self.job_config.science_inputs
108+
):
109+
return False # the previous day has no L1B data: nothing to wait for
110+
111+
for key in previous_day_keys:
112+
finished_runs = context.instance.get_runs(
113+
filters=RunsFilter(
114+
statuses=[
115+
DagsterRunStatus.SUCCESS,
116+
DagsterRunStatus.FAILURE,
117+
DagsterRunStatus.CANCELED,
118+
],
119+
tags={"dagster/partition": key},
120+
)
121+
)
122+
for run in finished_runs:
123+
# The partition tag is shared by every daily job, so match
124+
# this job the same way as the in-flight check above.
125+
if run.job_name == self.dagster_job_name or own_asset in (
126+
run.asset_selection or ()
127+
):
128+
return False # its job already ran and skipped or failed
129+
130+
context.log.info(
131+
"The previous day has L1B data but no L1C yet; waiting for its job."
132+
)
133+
return True
134+
30135
def get_science_files_inputs(self, context, target_start, target_end):
31136
"""Return the base science inputs plus the previous day's L1C, if any."""
32137
science_processing_inputs = super().get_science_files_inputs(

0 commit comments

Comments
 (0)