Skip to main content

flow/batching_mode/task/
ckpt.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::error::Error as StdError;
16use std::time::Duration;
17
18use client::OutputWithMetrics;
19use common_error::ext::ErrorExt;
20use common_error::status_code::StatusCode;
21use common_telemetry::tracing::warn;
22use common_telemetry::{debug, info};
23
24use crate::batching_mode::checkpoint::{
25    FlowCheckpointDecision, FlowQueryFallbackReason, checkpoint_mode_label,
26};
27use crate::batching_mode::state::{CheckpointMode, TaskState};
28use crate::batching_mode::task::{BatchingTask, QueryCoverage};
29use crate::metrics::{
30    METRIC_FLOW_BATCHING_ENGINE_CHECKPOINT_DECISION_CNT, METRIC_FLOW_BATCHING_ENGINE_QUERY_MODE_CNT,
31};
32use crate::{Error, FlowId};
33
34/// Liveness guard: when a fenced repair query fails with a wrapped error whose
35/// text indicates a stale snapshot fence (even when `StatusCode::RequestOutdated`
36/// was lost through client layers), classify it as `SnapshotFenceExpired` to
37/// break the retry loop and force a rebind of the fence high `H`.
38///
39/// Long-term the structured `StatusCode` / retry hint path should be preserved
40/// end-to-end; this text fallback is a narrow safety measure.
41fn matches_stale_snapshot_fence_text(err: &Error) -> bool {
42    let markers = [
43        "STALE_SNAPSHOT_FENCE",
44        "REBIND_SNAPSHOT_FENCE",
45        "snapshot upper bound stale",
46    ];
47    // Check the top-level error Display and Debug.
48    let debug_str = format!("{:?}", err);
49    let display_str = err.to_string();
50    for marker in &markers {
51        if debug_str.contains(marker) || display_str.contains(marker) {
52            return true;
53        }
54    }
55    // Walk the error source chain.
56    let mut source = err.source();
57    while let Some(s) = source {
58        let debug_str = format!("{:?}", s);
59        let display_str = s.to_string();
60        for marker in &markers {
61            if debug_str.contains(marker) || display_str.contains(marker) {
62                return true;
63            }
64        }
65        source = s.source();
66    }
67    false
68}
69
70impl BatchingTask {
71    /// Classify execution errors into checkpoint fallback reasons. A stale
72    /// snapshot fence is special only for fenced repair chunks.
73    pub(super) fn query_failure_reason(
74        err: &Error,
75        coverage: &QueryCoverage,
76    ) -> FlowQueryFallbackReason {
77        if err.status_code() == StatusCode::RequestOutdated {
78            if matches!(coverage, QueryCoverage::FencedRepairChunk { .. }) {
79                FlowQueryFallbackReason::SnapshotFenceExpired
80            } else {
81                FlowQueryFallbackReason::StaleCursor
82            }
83        } else if matches!(coverage, QueryCoverage::FencedRepairChunk { .. })
84            && matches_stale_snapshot_fence_text(err)
85        {
86            // Narrow text-based fallback for wrapped errors where the
87            // structured StatusCode::RequestOutdated was lost through
88            // frontend/client layers. Without this fenced repair will
89            // retry the same stale `given_seq` every refresh tick.
90            FlowQueryFallbackReason::SnapshotFenceExpired
91        } else if matches!(coverage, QueryCoverage::IncrementalDelta) {
92            FlowQueryFallbackReason::IncrementalQueryFailure
93        } else {
94            FlowQueryFallbackReason::QueryFailure
95        }
96    }
97
98    /// Apply the conservative state transition for a failed query. This never
99    /// advances checkpoints; it only decides whether to abandon repair state or
100    /// fall back from incremental mode.
101    pub(super) fn apply_query_failure_to_state(
102        state: &mut TaskState,
103        elapsed: Duration,
104        coverage: &QueryCoverage,
105        reason: FlowQueryFallbackReason,
106    ) -> Option<FlowCheckpointDecision> {
107        state.after_query_exec(elapsed, false);
108        let checkpoint_mode = state.checkpoint_mode();
109        if matches!(coverage, QueryCoverage::FencedRepairChunk { .. })
110            && matches!(reason, FlowQueryFallbackReason::SnapshotFenceExpired)
111        {
112            // `abandon_fenced_repair()` restores the not-yet-in-flight pending
113            // windows from the stale repair back to live dirty windows. The
114            // currently executing chunk is restored separately by the outer
115            // failure path (`handle_executed_query_failure`), after this state
116            // transition has removed `pending_fenced_repair`, so it also lands
117            // in live dirty windows for the next scoped base repair.
118            state.abandon_fenced_repair();
119            return Some(FlowCheckpointDecision::FallbackToFullSnapshot {
120                previous_mode: checkpoint_mode,
121                reason,
122            });
123        }
124
125        if checkpoint_mode == CheckpointMode::Incremental {
126            state.mark_full_snapshot();
127        }
128        Some(FlowCheckpointDecision::FallbackToFullSnapshot {
129            previous_mode: checkpoint_mode,
130            reason,
131        })
132    }
133
134    /// Apply checkpoint transitions for a successfully executed query using its
135    /// terminal watermark proof and declared coverage.
136    pub(super) fn apply_query_result_to_state(
137        state: &mut TaskState,
138        res: &OutputWithMetrics,
139        elapsed: Duration,
140        coverage: &QueryCoverage,
141    ) -> FlowCheckpointDecision {
142        state.after_query_exec(elapsed, true);
143        let checkpoint_mode = state.checkpoint_mode();
144        if let (Some(participating_regions), Some(watermark_map)) =
145            (res.participating_regions(), res.region_watermark_map())
146        {
147            let participating_region_count = participating_regions.len();
148            let watermark_count = watermark_map.len();
149            match coverage {
150                QueryCoverage::ScopedBaseRepair => {
151                    if !state.can_advance_full_snapshot_checkpoints(
152                        &participating_regions,
153                        &watermark_map,
154                    ) {
155                        return FlowCheckpointDecision::FallbackToFullSnapshot {
156                            previous_mode: checkpoint_mode,
157                            reason: FlowQueryFallbackReason::IncompleteRegionWatermark,
158                        };
159                    }
160
161                    if state.is_incremental_disabled() {
162                        return FlowCheckpointDecision::FallbackToFullSnapshot {
163                            previous_mode: CheckpointMode::FullSnapshot,
164                            reason: FlowQueryFallbackReason::IncrementalDisabled,
165                        };
166                    }
167
168                    if state.dirty_time_windows.is_empty() {
169                        state.advance_checkpoints(watermark_map);
170                        FlowCheckpointDecision::AdvancedFromFullSnapshot {
171                            participating_regions: participating_region_count,
172                            watermarks: watermark_count,
173                        }
174                    } else if let Some(repair) =
175                        state.start_fenced_repair(watermark_map.into_iter().collect())
176                    {
177                        FlowCheckpointDecision::ContinuedFencedRepair {
178                            pending_windows: repair.pending_windows().len(),
179                            watermarks: repair.high().len(),
180                        }
181                    } else {
182                        FlowCheckpointDecision::FallbackToFullSnapshot {
183                            previous_mode: checkpoint_mode,
184                            reason: FlowQueryFallbackReason::DirtyBacklogPending,
185                        }
186                    }
187                }
188                QueryCoverage::FencedRepairChunk { .. } => {
189                    if !state
190                        .fenced_repair_watermarks_match_high(&participating_regions, &watermark_map)
191                    {
192                        // A successful repair chunk whose terminal watermark
193                        // differs from the frozen fence `H` cannot prove that
194                        // continuing the same repair is safe. The pre-`H`
195                        // repair work item already executed under the snapshot
196                        // fence, so do not requeue it; restore only
197                        // not-yet-in-flight pending windows, then start over
198                        // with a fresh H. Later/post-`H` writes still rely on
199                        // live dirty notifications.
200                        state.abandon_fenced_repair();
201                        return FlowCheckpointDecision::FallbackToFullSnapshot {
202                            previous_mode: checkpoint_mode,
203                            reason: FlowQueryFallbackReason::IncompleteRegionWatermark,
204                        };
205                    }
206
207                    if state.fenced_repair_pending_is_empty() {
208                        state.finish_fenced_repair();
209                        if state.is_incremental_disabled() {
210                            FlowCheckpointDecision::FallbackToFullSnapshot {
211                                previous_mode: CheckpointMode::FullSnapshot,
212                                reason: FlowQueryFallbackReason::IncrementalDisabled,
213                            }
214                        } else {
215                            FlowCheckpointDecision::AdvancedFromFullSnapshot {
216                                participating_regions: participating_region_count,
217                                watermarks: watermark_count,
218                            }
219                        }
220                    } else {
221                        let repair = state
222                            .pending_fenced_repair()
223                            .expect("fenced repair exists after matching repair chunk watermark");
224                        FlowCheckpointDecision::ContinuedFencedRepair {
225                            pending_windows: repair.pending_windows().len(),
226                            watermarks: repair.high().len(),
227                        }
228                    }
229                }
230                QueryCoverage::UnfilteredFull => {
231                    if state.can_advance_full_snapshot_checkpoints(
232                        &participating_regions,
233                        &watermark_map,
234                    ) {
235                        state.advance_checkpoints(watermark_map);
236                        if state.is_incremental_disabled() {
237                            FlowCheckpointDecision::FallbackToFullSnapshot {
238                                previous_mode: CheckpointMode::FullSnapshot,
239                                reason: FlowQueryFallbackReason::IncrementalDisabled,
240                            }
241                        } else {
242                            FlowCheckpointDecision::AdvancedFromFullSnapshot {
243                                participating_regions: participating_region_count,
244                                watermarks: watermark_count,
245                            }
246                        }
247                    } else {
248                        debug_assert_ne!(checkpoint_mode, CheckpointMode::Incremental);
249                        FlowCheckpointDecision::FallbackToFullSnapshot {
250                            previous_mode: checkpoint_mode,
251                            reason: FlowQueryFallbackReason::IncompleteRegionWatermark,
252                        }
253                    }
254                }
255                QueryCoverage::IncrementalDelta => {
256                    if state.can_advance_incremental_checkpoints_with_participation(
257                        &participating_regions,
258                        &watermark_map,
259                    ) {
260                        state.advance_incremental_checkpoints_with_participation(
261                            &participating_regions,
262                            watermark_map,
263                        );
264                        FlowCheckpointDecision::AdvancedIncremental {
265                            participating_regions: participating_region_count,
266                            watermarks: watermark_count,
267                        }
268                    } else {
269                        state.mark_full_snapshot();
270                        FlowCheckpointDecision::FallbackToFullSnapshot {
271                            previous_mode: checkpoint_mode,
272                            reason: FlowQueryFallbackReason::IncompleteRegionWatermark,
273                        }
274                    }
275                }
276            }
277        } else {
278            if matches!(coverage, QueryCoverage::FencedRepairChunk { .. }) {
279                state.abandon_fenced_repair();
280            }
281            if matches!(checkpoint_mode, CheckpointMode::Incremental) {
282                state.mark_full_snapshot();
283            }
284            FlowCheckpointDecision::FallbackToFullSnapshot {
285                previous_mode: checkpoint_mode,
286                reason: FlowQueryFallbackReason::MissingRegionWatermark,
287            }
288        }
289    }
290
291    pub(super) fn record_checkpoint_decision(flow_id: FlowId, decision: FlowCheckpointDecision) {
292        let flow_id = flow_id.to_string();
293        METRIC_FLOW_BATCHING_ENGINE_CHECKPOINT_DECISION_CNT
294            .with_label_values(&[
295                flow_id.as_str(),
296                decision.mode_label(),
297                decision.decision_label(),
298                decision.reason_label(),
299            ])
300            .inc();
301
302        match decision {
303            FlowCheckpointDecision::AdvancedFromFullSnapshot {
304                participating_regions,
305                watermarks,
306            } => {
307                info!(
308                    "Flow {flow_id} switched to incremental mode after full snapshot, participating_regions={participating_regions}, watermarks={watermarks}"
309                );
310            }
311            FlowCheckpointDecision::AdvancedIncremental {
312                participating_regions,
313                watermarks,
314            } => {
315                debug!(
316                    "Flow {flow_id} advanced incremental checkpoints, participating_regions={participating_regions}, watermarks={watermarks}"
317                );
318            }
319            FlowCheckpointDecision::ContinuedFencedRepair {
320                pending_windows,
321                watermarks,
322            } => {
323                debug!(
324                    "Flow {flow_id} continued fenced repair, pending_windows={pending_windows}, watermarks={watermarks}"
325                );
326            }
327            FlowCheckpointDecision::FallbackToFullSnapshot {
328                previous_mode: CheckpointMode::FullSnapshot,
329                reason: FlowQueryFallbackReason::IncrementalDisabled,
330            } => {
331                debug!(
332                    "Flow {flow_id} remains in full snapshot mode, reason={}",
333                    FlowQueryFallbackReason::IncrementalDisabled.as_label()
334                );
335            }
336            FlowCheckpointDecision::FallbackToFullSnapshot {
337                previous_mode,
338                reason,
339            } => {
340                warn!(
341                    "Flow {flow_id} switched to full snapshot mode, previous_mode={}, reason={}",
342                    checkpoint_mode_label(previous_mode),
343                    reason.as_label()
344                );
345            }
346        }
347    }
348
349    pub(super) fn record_query_mode(flow_id: FlowId, mode: CheckpointMode) {
350        let flow_id = flow_id.to_string();
351        METRIC_FLOW_BATCHING_ENGINE_QUERY_MODE_CNT
352            .with_label_values(&[flow_id.as_str(), checkpoint_mode_label(mode)])
353            .inc();
354    }
355}