Skip to content

Commit e8c4c4d

Browse files
authored
Fix: Defer unpausing right to right before env finalization when ensuring finalized snapshots (#2147)
1 parent 1339e5b commit e8c4c4d

File tree

12 files changed

+88
-42
lines changed

12 files changed

+88
-42
lines changed

sqlmesh/core/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ def plan_builder(
10461046
enable_preview if enable_preview is not None else self.config.plan.enable_preview
10471047
),
10481048
end_bounded=not run,
1049+
ensure_finalized_snapshots=self.config.plan.use_finalized_state,
10491050
)
10501051

10511052
def apply(

sqlmesh/core/plan/builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class PlanBuilder:
5858
enable_preview: Whether to enable preview for forward-only models in development environments.
5959
end_bounded: If set to true, the missing intervals will be bounded by the target end date, disregarding lookback,
6060
allow_partials, and other attributes that could cause the intervals to exceed the target end date.
61+
ensure_finalized_snapshots: Whether to compare against snapshots from the latest finalized
62+
environment state, or to use whatever snapshots are in the current environment state even if
63+
the environment is not finalized.
6164
"""
6265

6366
def __init__(
@@ -84,6 +87,7 @@ def __init__(
8487
default_end: t.Optional[TimeLike] = None,
8588
enable_preview: bool = False,
8689
end_bounded: bool = False,
90+
ensure_finalized_snapshots: bool = False,
8791
):
8892
self._context_diff = context_diff
8993
self._no_gaps = no_gaps
@@ -92,6 +96,7 @@ def __init__(
9296
self._forward_only = forward_only
9397
self._enable_preview = enable_preview
9498
self._end_bounded = end_bounded
99+
self._ensure_finalized_snapshots = ensure_finalized_snapshots
95100
self._environment_ttl = environment_ttl
96101
self._categorizer_config = categorizer_config or CategorizerConfig()
97102
self._auto_categorization_enabled = auto_categorization_enabled
@@ -226,6 +231,7 @@ def build(self) -> Plan:
226231
effective_from=self._effective_from,
227232
execution_time=self._execution_time,
228233
end_bounded=self._end_bounded,
234+
ensure_finalized_snapshots=self._ensure_finalized_snapshots,
229235
)
230236
self._latest_plan = plan
231237
return plan

sqlmesh/core/plan/definition.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Plan(PydanticModel, frozen=True):
3636
forward_only: bool
3737
include_unmodified: bool
3838
end_bounded: bool
39+
ensure_finalized_snapshots: bool
3940

4041
environment_ttl: t.Optional[str] = None
4142
environment_naming_info: EnvironmentNamingInfo

sqlmesh/core/plan/evaluator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ def _promote(
217217
[s for s in plan.snapshots.values() if s.is_paused],
218218
plan.snapshots,
219219
)
220-
self.state_sync.unpause_snapshots(promotion_result.added, plan.end)
220+
if not plan.ensure_finalized_snapshots:
221+
# Only unpause at this point if we don't have to use the finalized snapshots
222+
# for subsequent plan applications. Otherwise, unpause right before finalizing
223+
# the environment.
224+
self.state_sync.unpause_snapshots(promotion_result.added, plan.end)
221225

222226
return promotion_result
223227

@@ -234,6 +238,12 @@ def _update_views(
234238
promotion_result: The result of the promotion.
235239
deployability_index: Indicates which snapshots are deployable in the context of this promotion.
236240
"""
241+
if not plan.is_dev and plan.ensure_finalized_snapshots:
242+
# Unpause right before finalizing the environment in case when
243+
# we need to use the finalized snapshots for subsequent plan applications.
244+
# Otherwise, unpause right after updatig the environment record.
245+
self.state_sync.unpause_snapshots(promotion_result.added, plan.end)
246+
237247
environment = plan.environment
238248

239249
self.console.start_promotion_progress(
@@ -359,6 +369,7 @@ def _apply_plan(self, plan: Plan, plan_request_id: str) -> None:
359369
forward_only=plan.forward_only,
360370
models_to_backfill=plan.models_to_backfill,
361371
end_bounded=plan.end_bounded,
372+
ensure_finalized_snapshots=plan.ensure_finalized_snapshots,
362373
)
363374
plan_dag_spec = create_plan_dag_spec(plan_application_request, self.state_sync)
364375
PlanDagState.from_state_sync(self.state_sync).add_dag_spec(plan_dag_spec)
@@ -428,6 +439,7 @@ def _apply_plan(self, plan: Plan, plan_request_id: str) -> None:
428439
forward_only=plan.forward_only,
429440
models_to_backfill=plan.models_to_backfill,
430441
end_bounded=plan.end_bounded,
442+
ensure_finalized_snapshots=plan.ensure_finalized_snapshots,
431443
)
432444

433445

sqlmesh/schedulers/airflow/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def apply_plan(
197197
forward_only: bool = False,
198198
models_to_backfill: t.Optional[t.Set[str]] = None,
199199
end_bounded: bool = False,
200+
ensure_finalized_snapshots: bool = False,
200201
) -> None:
201202
request = common.PlanApplicationRequest(
202203
new_snapshots=list(new_snapshots),
@@ -213,6 +214,7 @@ def apply_plan(
213214
forward_only=forward_only,
214215
models_to_backfill=models_to_backfill,
215216
end_bounded=end_bounded,
217+
ensure_finalized_snapshots=ensure_finalized_snapshots,
216218
)
217219

218220
response = self._session.post(

sqlmesh/schedulers/airflow/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class PlanApplicationRequest(PydanticModel):
5151
forward_only: bool
5252
models_to_backfill: t.Optional[t.Set[str]]
5353
end_bounded: bool
54+
ensure_finalized_snapshots: bool
5455

5556
def is_selected_for_backfill(self, model_fqn: str) -> bool:
5657
return self.models_to_backfill is None or model_fqn in self.models_to_backfill
@@ -81,6 +82,7 @@ class PlanDagSpec(PydanticModel):
8182
deployability_index_for_creation: DeployabilityIndex = DeployabilityIndex.all_deployable()
8283
no_gaps_snapshot_names: t.Optional[t.Set[str]] = None
8384
models_to_backfill: t.Optional[t.Set[str]] = None
85+
ensure_finalized_snapshots: bool = False
8486

8587

8688
class EnvironmentsResponse(PydanticModel):

sqlmesh/schedulers/airflow/dag_generator.py

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,14 @@ def _create_plan_application_dag(self, plan_dag_spec: common.PlanDagSpec) -> DAG
148148
plan_dag_spec.environment.name,
149149
)
150150

151-
environment = plan_dag_spec.environment
152-
153151
all_snapshots = {
154152
**{s.snapshot_id: s for s in plan_dag_spec.new_snapshots},
155-
**self._state_reader.get_snapshots(environment.snapshots),
153+
**self._state_reader.get_snapshots(plan_dag_spec.environment.snapshots),
156154
}
157155

158156
snapshots_to_create = [
159157
all_snapshots[snapshot.snapshot_id]
160-
for snapshot in environment.snapshots
158+
for snapshot in plan_dag_spec.environment.snapshots
161159
if snapshot.snapshot_id in all_snapshots
162160
and (
163161
plan_dag_spec.models_to_backfill is None
@@ -216,22 +214,36 @@ def _create_plan_application_dag(self, plan_dag_spec: common.PlanDagSpec) -> DAG
216214
(
217215
promote_start_task,
218216
promote_end_task,
219-
) = self._create_promotion_demotion_tasks(plan_dag_spec, environment, all_snapshots)
220-
221-
update_views_task_pair = self._create_update_views_tasks(plan_dag_spec, all_snapshots)
222-
223-
finalize_task = self._create_finalize_task(environment)
217+
) = self._create_promotion_demotion_tasks(plan_dag_spec, all_snapshots)
224218

225219
start_task >> create_start_task
226220
create_end_task >> backfill_before_promote_start_task
227221
backfill_before_promote_end_task >> promote_start_task
228-
promote_end_task >> backfill_after_promote_start_task
229222

223+
update_views_task_pair = self._create_update_views_tasks(plan_dag_spec, all_snapshots)
230224
if update_views_task_pair:
231225
backfill_after_promote_end_task >> update_views_task_pair[0]
232-
update_views_task_pair[1] >> finalize_task
226+
before_finalize_task = update_views_task_pair[1]
233227
else:
234-
backfill_after_promote_end_task >> finalize_task
228+
before_finalize_task = backfill_after_promote_end_task
229+
230+
unpause_snapshots_task = self._create_unpause_snapshots_task(plan_dag_spec)
231+
if unpause_snapshots_task:
232+
if not plan_dag_spec.ensure_finalized_snapshots:
233+
# Only unpause right after updatign the environment record if we don't
234+
# have to use the finalized snapshots for subsequent plan applications.
235+
promote_end_task >> unpause_snapshots_task
236+
unpause_snapshots_task >> backfill_after_promote_start_task
237+
else:
238+
# Otherwise, unpause right before finalizing the environment.
239+
promote_end_task >> backfill_after_promote_start_task
240+
before_finalize_task >> unpause_snapshots_task
241+
before_finalize_task = unpause_snapshots_task
242+
else:
243+
promote_end_task >> backfill_after_promote_start_task
244+
245+
finalize_task = self._create_finalize_task(plan_dag_spec.environment)
246+
before_finalize_task >> finalize_task
235247

236248
self._add_notification_target_tasks(plan_dag_spec, start_task, end_task, finalize_task)
237249
return dag
@@ -310,51 +322,52 @@ def _create_creation_tasks(
310322
def _create_promotion_demotion_tasks(
311323
self,
312324
request: common.PlanDagSpec,
313-
environment: Environment,
314325
snapshots: t.Dict[SnapshotId, Snapshot],
315326
) -> t.Tuple[BaseOperator, BaseOperator]:
316327
update_state_task = PythonOperator(
317328
task_id="snapshot_promotion_update_state",
318329
python_callable=promotion_update_state_task,
319330
op_kwargs={
320-
"environment": environment,
331+
"environment": request.environment,
321332
"no_gaps_snapshot_names": (
322333
request.no_gaps_snapshot_names if request.no_gaps else set()
323334
),
324335
},
325336
)
326337

327338
start_task = update_state_task
328-
end_task = update_state_task
329-
330-
if request.environment.promoted_snapshots:
331-
if not request.is_dev and request.unpaused_dt:
332-
migrate_tables_task = self._create_snapshot_migrate_tables_operator(
333-
[
334-
snapshots[s.snapshot_id]
335-
for s in request.environment.promoted_snapshots
336-
if snapshots[s.snapshot_id].is_paused
337-
],
338-
request.ddl_concurrent_tasks,
339-
"snapshot_promotion_migrate_tables",
340-
)
341-
342-
unpause_snapshots_task = PythonOperator(
343-
task_id="snapshot_promotion_unpause_snapshots",
344-
python_callable=promotion_unpause_snapshots_task,
345-
op_kwargs={
346-
"environment": environment,
347-
"unpaused_dt": request.unpaused_dt,
348-
},
349-
trigger_rule="none_failed",
350-
)
351-
352-
update_state_task >> migrate_tables_task
353-
migrate_tables_task >> unpause_snapshots_task
354-
end_task = unpause_snapshots_task
339+
end_task: BaseOperator = update_state_task
340+
341+
if request.environment.promoted_snapshots and not request.is_dev and request.unpaused_dt:
342+
migrate_tables_task = self._create_snapshot_migrate_tables_operator(
343+
[
344+
snapshots[s.snapshot_id]
345+
for s in request.environment.promoted_snapshots
346+
if snapshots[s.snapshot_id].is_paused
347+
],
348+
request.ddl_concurrent_tasks,
349+
"snapshot_promotion_migrate_tables",
350+
)
351+
update_state_task >> migrate_tables_task
352+
end_task = migrate_tables_task
355353

356354
return (start_task, end_task)
357355

356+
def _create_unpause_snapshots_task(
357+
self, request: common.PlanDagSpec
358+
) -> t.Optional[BaseOperator]:
359+
if request.is_dev or not request.unpaused_dt:
360+
return None
361+
return PythonOperator(
362+
task_id="snapshot_promotion_unpause_snapshots",
363+
python_callable=promotion_unpause_snapshots_task,
364+
op_kwargs={
365+
"environment": request.environment,
366+
"unpaused_dt": request.unpaused_dt,
367+
},
368+
trigger_rule="none_failed",
369+
)
370+
358371
def _create_update_views_tasks(
359372
self, request: common.PlanDagSpec, snapshots: t.Dict[SnapshotId, Snapshot]
360373
) -> t.Optional[t.Tuple[BaseOperator, BaseOperator]]:

sqlmesh/schedulers/airflow/plan.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def create_plan_dag_spec(
170170
deployability_index_for_creation=deployability_index_for_creation,
171171
no_gaps_snapshot_names=no_gaps_snapshot_names,
172172
models_to_backfill=request.models_to_backfill,
173+
ensure_finalized_snapshots=request.ensure_finalized_snapshots,
173174
)
174175

175176

tests/core/test_plan.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def test_missing_intervals_lookback(make_snapshot, mocker: MockerFixture):
243243
deployability_index=DeployabilityIndex.all_deployable(),
244244
restatements={},
245245
end_bounded=False,
246+
ensure_finalized_snapshots=False,
246247
)
247248

248249
assert not plan.missing_intervals

tests/core/test_plan_evaluator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def test_airflow_evaluator(sushi_plan: Plan, mocker: MockerFixture):
100100
forward_only=False,
101101
models_to_backfill=None,
102102
end_bounded=False,
103+
ensure_finalized_snapshots=False,
103104
)
104105

105106
airflow_client_mock.wait_for_dag_run_completion.assert_called_once()

0 commit comments

Comments
 (0)