33import datetime
44import re
55
6+ from dagster import DagsterRunStatus , RunsFilter
67from imap_data_access import processing_input
78
89from sds_data_manager .orchestration import imap_job , types
10+ from sds_data_manager .orchestration .dagster_utilities import (
11+ parse_dates_from_partition_key ,
12+ )
913from 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