1use 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
34fn 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 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 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 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 FlowQueryFallbackReason::SnapshotFenceExpired
91 } else if matches!(coverage, QueryCoverage::IncrementalDelta) {
92 FlowQueryFallbackReason::IncrementalQueryFailure
93 } else {
94 FlowQueryFallbackReason::QueryFailure
95 }
96 }
97
98 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 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 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 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}