Skip to main content

common_meta/ddl/
create_flow.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
15mod metadata;
16
17use std::collections::{BTreeMap, HashMap};
18use std::fmt;
19
20use api::v1::ExpireAfter;
21use api::v1::flow::flow_request::Body as PbFlowRequest;
22use api::v1::flow::{CreateRequest, FlowRequest, FlowRequestHeader};
23use async_trait::async_trait;
24use chrono::{DateTime, Utc};
25use common_catalog::format_full_flow_name;
26use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
27use common_procedure::{
28    Context as ProcedureContext, EventContext, EventTrigger, LockKey, Procedure, ProcedureState,
29    Result as ProcedureResult, Status,
30};
31use common_telemetry::info;
32use common_telemetry::tracing_context::TracingContext;
33use futures::future::join_all;
34use itertools::Itertools;
35use serde::{Deserialize, Serialize};
36use snafu::{OptionExt, ResultExt, ensure};
37use strum::AsRefStr;
38use table::metadata::TableId;
39use table::table_name::TableName;
40
41use crate::cache_invalidator::Context;
42use crate::ddl::DdlContext;
43use crate::ddl::event::flow::{CREATE_FLOW_EVENT_TYPE, CreateFlowEventIntent, FlowDdlEvent};
44use crate::ddl::utils::{add_peer_context_if_needed, map_to_procedure_error};
45use crate::error::{self, Result, UnexpectedSnafu};
46use crate::instruction::{CacheIdent, CreateFlow, DropFlow};
47use crate::key::flow::flow_info::{FlowInfoValue, FlowScheduleConfig, FlowStatus};
48use crate::key::flow::flow_route::FlowRouteValue;
49use crate::key::table_name::TableNameKey;
50use crate::key::{DeserializedValueWithBytes, FlowId, FlowPartitionId};
51use crate::lock_key::{CatalogLock, FlowNameLock};
52use crate::metrics;
53use crate::peer::Peer;
54use crate::rpc::ddl::{CreateFlowTask, FlowQueryContext, QueryContext};
55
56/// The procedure of flow creation.
57pub struct CreateFlowProcedure {
58    pub context: DdlContext,
59    pub data: CreateFlowData,
60}
61
62impl CreateFlowProcedure {
63    pub const TYPE_NAME: &'static str = "metasrv-procedure::CreateFlow";
64
65    /// Returns a new [CreateFlowProcedure].
66    pub fn new(task: CreateFlowTask, query_context: QueryContext, context: DdlContext) -> Self {
67        Self {
68            context,
69            data: CreateFlowData {
70                task,
71                flow_id: None,
72                peers: vec![],
73                source_table_ids: vec![],
74                unresolved_source_table_names: vec![],
75                flow_context: without_scheduled_time_extension(query_context).into(),
76                state: CreateFlowState::Prepare,
77                prev_flow_info_value: None,
78                did_replace: false,
79                flow_type: None,
80            },
81        }
82    }
83
84    /// Deserializes from `json`.
85    pub fn from_json(json: &str, context: DdlContext) -> ProcedureResult<Self> {
86        let data = serde_json::from_str(json).context(FromJsonSnafu)?;
87        Ok(CreateFlowProcedure { context, data })
88    }
89
90    pub(crate) async fn on_prepare(&mut self) -> Result<Status> {
91        let catalog_name = &self.data.task.catalog_name;
92        let flow_name = &self.data.task.flow_name;
93        let sink_table_name = &self.data.task.sink_table_name;
94        let create_if_not_exists = self.data.task.create_if_not_exists;
95        let or_replace = self.data.task.or_replace;
96
97        validate_flow_options(&self.data.task)?;
98
99        let flow_name_value = self
100            .context
101            .flow_metadata_manager
102            .flow_name_manager()
103            .get(catalog_name, flow_name)
104            .await?;
105
106        if create_if_not_exists && or_replace {
107            // this is forbidden because not clear what does that mean exactly
108            return error::UnsupportedSnafu {
109                operation: "Create flow with both `IF NOT EXISTS` and `OR REPLACE`",
110            }
111            .fail();
112        }
113
114        if let Some(value) = flow_name_value {
115            ensure!(
116                create_if_not_exists || or_replace,
117                error::FlowAlreadyExistsSnafu {
118                    flow_name: format_full_flow_name(catalog_name, flow_name),
119                }
120            );
121
122            let flow_id = value.flow_id();
123            if create_if_not_exists {
124                info!("Flow already exists, flow_id: {}", flow_id);
125                return Ok(Status::done_with_output(flow_id));
126            }
127
128            let flow_id = value.flow_id();
129            let peers = self
130                .context
131                .flow_metadata_manager
132                .flow_route_manager()
133                .routes(flow_id)
134                .await?
135                .into_iter()
136                .map(|(_, value)| value.peer)
137                .collect::<Vec<_>>();
138            self.data.flow_id = Some(flow_id);
139            self.data.peers = peers;
140            info!("Replacing flow, flow_id: {}", flow_id);
141
142            let flow_info_value = self
143                .context
144                .flow_metadata_manager
145                .flow_info_manager()
146                .get_raw(flow_id)
147                .await?;
148
149            ensure!(
150                flow_info_value.is_some(),
151                error::FlowNotFoundSnafu {
152                    flow_name: format_full_flow_name(catalog_name, flow_name),
153                }
154            );
155
156            self.data.prev_flow_info_value = flow_info_value;
157        }
158
159        //  Ensures sink table doesn't exist.
160        let exists = self
161            .context
162            .table_metadata_manager
163            .table_name_manager()
164            .exists(TableNameKey::new(
165                &sink_table_name.catalog_name,
166                &sink_table_name.schema_name,
167                &sink_table_name.table_name,
168            ))
169            .await?;
170        // TODO(discord9): due to undefined behavior in flow's plan in how to transform types in mfp, sometime flow can't deduce correct schema
171        // and require manually create sink table
172        if exists {
173            common_telemetry::warn!("Table already exists, table: {}", sink_table_name);
174        }
175
176        self.collect_source_tables().await?;
177        ensure!(
178            self.data.unresolved_source_table_names.is_empty()
179                || defer_on_missing_source(&self.data.task)?,
180            error::UnsupportedSnafu {
181                operation: format!(
182                    "Create flow with missing source tables requires WITH ('{DEFER_ON_MISSING_SOURCE_KEY}'='true'): {}",
183                    self.data
184                        .unresolved_source_table_names
185                        .iter()
186                        .map(ToString::to_string)
187                        .join(", ")
188                )
189            }
190        );
191        self.ensure_supported_replace_transition()?;
192
193        // Validate that source and sink tables are not the same
194        let sink_table_name = &self.data.task.sink_table_name;
195        if self
196            .data
197            .task
198            .source_table_names
199            .iter()
200            .any(|source| source == sink_table_name)
201        {
202            return error::UnsupportedSnafu {
203                operation: format!(
204                    "Creating flow with source and sink table being the same: {}",
205                    sink_table_name
206                ),
207            }
208            .fail();
209        }
210
211        if self.data.flow_id.is_none() {
212            self.allocate_flow_id().await?;
213        }
214        self.data.flow_type = Some(get_flow_type_from_options(&self.data.task)?);
215
216        // Resolve schedule defaults into task.eval_schedule once, before
217        // CreateRequest / FlowInfoValue are constructed. This ensures the same
218        // typed schedule config is sent to flownodes and persisted in metadata.
219        // Idempotent: if eval_schedule is already present we do not recompute.
220        resolve_schedule_defaults_into_task(
221            &mut self.data.task,
222            self.data
223                .prev_flow_info_value
224                .as_ref()
225                .map(|v| v.get_inner_ref()),
226        )?;
227
228        self.data.state = if self.data.is_pending() {
229            self.data.peers.clear();
230            CreateFlowState::CreateMetadata
231        } else {
232            CreateFlowState::CreateFlows
233        };
234
235        Ok(Status::executing(true))
236    }
237
238    fn ensure_supported_replace_transition(&self) -> Result<()> {
239        if !self.data.task.or_replace {
240            return Ok(());
241        }
242
243        let Some(prev_flow_info) = self.data.prev_flow_info_value.as_ref() else {
244            return Ok(());
245        };
246        let prev_pending = prev_flow_info.get_inner_ref().is_pending();
247        let new_pending = self.data.is_pending();
248        ensure!(
249            prev_pending == new_pending,
250            error::UnsupportedSnafu {
251                operation: "Replacing between pending and active flow states is not supported yet"
252            }
253        );
254
255        Ok(())
256    }
257
258    async fn on_flownode_create_flows(&mut self) -> Result<Status> {
259        // Safety: must be allocated.
260        let mut create_flow = Vec::with_capacity(self.data.peers.len());
261        for peer in &self.data.peers {
262            let requester = self.context.node_manager.flownode(peer).await;
263            let request = FlowRequest {
264                header: Some(FlowRequestHeader {
265                    tracing_context: TracingContext::from_current_span().to_w3c(),
266                    // Convert FlowQueryContext to QueryContext
267                    query_context: Some(
268                        without_scheduled_time_extension(QueryContext::from(
269                            self.data.flow_context.clone(),
270                        ))
271                        .into(),
272                    ),
273                }),
274                body: Some(PbFlowRequest::Create((&self.data).into())),
275            };
276            create_flow.push(async move {
277                requester
278                    .handle(request)
279                    .await
280                    .map_err(add_peer_context_if_needed(peer.clone()))
281            });
282        }
283        info!(
284            "Creating flow({:?}, type={:?}) on flownodes with peers={:?}",
285            self.data.flow_id, self.data.flow_type, self.data.peers
286        );
287        join_all(create_flow)
288            .await
289            .into_iter()
290            .collect::<Result<Vec<_>>>()?;
291
292        self.data.state = CreateFlowState::CreateMetadata;
293        Ok(Status::executing(true))
294    }
295
296    /// Creates flow metadata.
297    ///
298    /// Abort(not-retry):
299    /// - Failed to create table metadata.
300    async fn on_create_metadata(&mut self) -> Result<Status> {
301        // Safety: The flow id must be allocated.
302        let flow_id = self.data.flow_id.unwrap();
303        let (flow_info, flow_routes) = (&self.data).into();
304        if let Some(prev_flow_value) = self.data.prev_flow_info_value.as_ref()
305            && self.data.task.or_replace
306        {
307            self.context
308                .flow_metadata_manager
309                .update_flow_metadata(flow_id, prev_flow_value, &flow_info, flow_routes)
310                .await?;
311            info!("Replaced flow metadata for flow {flow_id}");
312            self.data.did_replace = true;
313        } else {
314            self.context
315                .flow_metadata_manager
316                .create_flow_metadata(flow_id, flow_info, flow_routes)
317                .await?;
318            info!("Created flow metadata for flow {flow_id}");
319        }
320
321        self.data.state = CreateFlowState::InvalidateFlowCache;
322        Ok(Status::executing(true))
323    }
324
325    async fn on_broadcast(&mut self) -> Result<Status> {
326        debug_assert!(self.data.state == CreateFlowState::InvalidateFlowCache);
327        // Safety: The flow id must be allocated.
328        let flow_id = self.data.flow_id.unwrap();
329        let did_replace = self.data.did_replace;
330        let ctx = Context {
331            subject: Some("Invalidate flow cache by creating flow".to_string()),
332        };
333
334        let mut caches = vec![];
335
336        // if did replaced, invalidate the flow cache with drop the old flow
337        if did_replace {
338            let old_flow_info = self.data.prev_flow_info_value.as_ref().unwrap();
339
340            // only drop flow is needed, since flow name haven't changed, and flow id already invalidated below
341            caches.extend([CacheIdent::DropFlow(DropFlow {
342                flow_id,
343                source_table_ids: old_flow_info.source_table_ids.clone(),
344                flow_part2node_id: old_flow_info.flownode_ids().clone().into_iter().collect(),
345            })]);
346        }
347
348        let (_flow_info, flow_routes) = (&self.data).into();
349        let flow_part2peers = flow_routes
350            .into_iter()
351            .map(|(part_id, route)| (part_id, route.peer))
352            .collect();
353
354        caches.extend([
355            CacheIdent::CreateFlow(CreateFlow {
356                flow_id,
357                source_table_ids: self.data.source_table_ids.clone(),
358                partition_to_peer_mapping: flow_part2peers,
359            }),
360            CacheIdent::FlowId(flow_id),
361        ]);
362
363        self.context
364            .cache_invalidator
365            .invalidate(&ctx, &caches)
366            .await?;
367
368        Ok(Status::done_with_output(flow_id))
369    }
370}
371
372#[async_trait]
373impl Procedure for CreateFlowProcedure {
374    fn type_name(&self) -> &str {
375        Self::TYPE_NAME
376    }
377
378    async fn execute(&mut self, _ctx: &ProcedureContext) -> ProcedureResult<Status> {
379        let state = &self.data.state;
380
381        let _timer = metrics::METRIC_META_PROCEDURE_CREATE_FLOW
382            .with_label_values(&[state.as_ref()])
383            .start_timer();
384
385        match state {
386            CreateFlowState::Prepare => self.on_prepare().await,
387            CreateFlowState::CreateFlows => self.on_flownode_create_flows().await,
388            CreateFlowState::CreateMetadata => self.on_create_metadata().await,
389            CreateFlowState::InvalidateFlowCache => self.on_broadcast().await,
390        }
391        .map_err(map_to_procedure_error)
392    }
393
394    fn dump(&self) -> ProcedureResult<String> {
395        serde_json::to_string(&self.data).context(ToJsonSnafu)
396    }
397
398    fn lock_key(&self) -> LockKey {
399        let catalog_name = &self.data.task.catalog_name;
400        let flow_name = &self.data.task.flow_name;
401
402        LockKey::new(vec![
403            CatalogLock::Read(catalog_name).into(),
404            FlowNameLock::new(catalog_name, flow_name).into(),
405        ])
406    }
407
408    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
409        if !ctx.event_type_filter.allows(CREATE_FLOW_EVENT_TYPE) {
410            return None;
411        }
412
413        let event = match &ctx.trigger {
414            EventTrigger::Submitted => FlowDdlEvent::create_submitted(
415                &self.data.task.catalog_name,
416                &self.data.task.flow_name,
417                CreateFlowEventIntent {
418                    or_replace: self.data.task.or_replace,
419                    create_if_not_exists: self.data.task.create_if_not_exists,
420                    expire_after: self.data.task.expire_after,
421                    eval_interval_secs: self.data.task.eval_interval_secs,
422                },
423            ),
424            EventTrigger::Succeeded => {
425                let flow_id = match ctx.lifecycle_state {
426                    ProcedureState::Done {
427                        output: Some(output),
428                    } => output
429                        .downcast_ref::<FlowId>()
430                        .copied()
431                        .or(self.data.flow_id),
432                    _ => self.data.flow_id,
433                };
434                FlowDdlEvent::create_succeeded(
435                    &self.data.task.catalog_name,
436                    &self.data.task.flow_name,
437                    flow_id,
438                )
439            }
440            _ => FlowDdlEvent::create_lifecycle(
441                &self.data.task.catalog_name,
442                &self.data.task.flow_name,
443            ),
444        };
445
446        Some(Box::new(event))
447    }
448}
449
450pub fn get_flow_type_from_options(flow_task: &CreateFlowTask) -> Result<FlowType> {
451    let flow_type = flow_task
452        .flow_options
453        .get(FlowType::FLOW_TYPE_KEY)
454        .map(|s| s.as_str());
455    match flow_type {
456        Some(FlowType::BATCHING) => Ok(FlowType::Batching),
457        Some(FlowType::STREAMING) => Ok(FlowType::Streaming),
458        Some(unknown) => UnexpectedSnafu {
459            err_msg: format!("Unknown flow type: {}", unknown),
460        }
461        .fail(),
462        None => Ok(FlowType::Batching),
463    }
464}
465
466/// The flow option key for creating pending flow metadata when source tables do not exist.
467pub const DEFER_ON_MISSING_SOURCE_KEY: &str = "defer_on_missing_source";
468
469/// Internal transient key used to pass the typed `EVAL OFFSET` (whole seconds)
470/// from the operator to meta through `CreateFlowExpr.flow_options`. Inserted by
471/// the operator only after user option validation; parsed and stripped by
472/// `CreateFlowTask::try_from`. Must never be accepted as a user-provided option
473/// and must never be persisted into `FlowInfoValue.options` or be visible in
474/// user runtime options / SHOW CREATE.
475pub const INTERNAL_EVAL_OFFSET_KEY: &str = "__greptime_internal_eval_offset_secs";
476
477/// Internal transient key used to pass the serialized `FlowScheduleConfig` from
478/// meta to flownode through `CreateRequest.flow_options`. This key must never
479/// be accepted as a user-provided option and must never be persisted into
480/// `FlowInfoValue.options`.
481/// TODO(discord9): Replace this transient flow_options transport with a typed
482/// field in the flow create request.
483pub const INTERNAL_EVAL_SCHEDULE_KEY: &str = "__greptime_internal_eval_schedule";
484
485const FLOW_SCHEDULED_TIME_MILLIS_EXTENSION_KEY: &str = "flow.scheduled_time_millis";
486
487fn without_scheduled_time_extension(mut query_context: QueryContext) -> QueryContext {
488    query_context
489        .extensions
490        .remove(FLOW_SCHEDULED_TIME_MILLIS_EXTENSION_KEY);
491    query_context
492}
493
494pub fn defer_on_missing_source(flow_task: &CreateFlowTask) -> Result<bool> {
495    flow_task
496        .flow_options
497        .get(DEFER_ON_MISSING_SOURCE_KEY)
498        .map(|value| {
499            value
500                .trim()
501                .to_ascii_lowercase()
502                .parse::<bool>()
503                .map_err(|_| {
504                    error::UnexpectedSnafu {
505                        err_msg: format!(
506                            "Invalid flow option '{DEFER_ON_MISSING_SOURCE_KEY}': {value}"
507                        ),
508                    }
509                    .build()
510                })
511        })
512        .transpose()
513        .map(|value| value.unwrap_or(false))
514}
515
516pub fn validate_flow_options(flow_task: &CreateFlowTask) -> Result<()> {
517    // Reject non-positive eval_interval_secs (zero or negative).
518    if let Some(secs) = flow_task.eval_interval_secs
519        && secs <= 0
520    {
521        return UnexpectedSnafu {
522            err_msg: format!("EVAL INTERVAL must be positive, got {secs} seconds"),
523        }
524        .fail();
525    }
526
527    for key in [INTERNAL_EVAL_OFFSET_KEY, INTERNAL_EVAL_SCHEDULE_KEY] {
528        if flow_task.flow_options.contains_key(key) {
529            return UnexpectedSnafu {
530                err_msg: format!("flow option '{key}' is reserved for internal use"),
531            }
532            .fail();
533        }
534    }
535
536    // EVAL OFFSET semantics: only legal with EVAL INTERVAL and must be in
537    // `[0, eval_interval_secs)`. Never modulo-normalized.
538    if let Some(offset_secs) = flow_task.eval_offset_secs {
539        let Some(eval_interval_secs) = flow_task.eval_interval_secs else {
540            return UnexpectedSnafu {
541                err_msg: "EVAL OFFSET requires EVAL INTERVAL to be specified".to_string(),
542            }
543            .fail();
544        };
545        if !(0..eval_interval_secs).contains(&offset_secs) {
546            return UnexpectedSnafu {
547                err_msg: format!(
548                    "EVAL OFFSET must be in range [0, EVAL INTERVAL), got {offset_secs} seconds with EVAL INTERVAL {eval_interval_secs} seconds"
549                ),
550            }
551            .fail();
552        }
553    }
554
555    for key in flow_task.flow_options.keys() {
556        match key.as_str() {
557            DEFER_ON_MISSING_SOURCE_KEY
558            | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY
559            | FlowType::FLOW_TYPE_KEY => {}
560            unknown => {
561                return UnexpectedSnafu {
562                    err_msg: format!(
563                        "Unknown flow option '{unknown}', supported user options: {DEFER_ON_MISSING_SOURCE_KEY}, {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}"
564                    ),
565                }
566                .fail();
567            }
568        }
569    }
570
571    defer_on_missing_source(flow_task)?;
572    get_flow_type_from_options(flow_task)?;
573    Ok(())
574}
575
576/// Computes the ceiling of `time` to the next schedule boundary aligned with `anchor + k * interval`.
577/// All values are Unix timestamps in seconds.
578///
579/// Fallible: if the next boundary after `time` does not fit in `i64`, an
580/// explicit error is returned instead of clamping to a non-phase value such as
581/// `i64::MAX`.
582pub(crate) fn ceil_to_boundary(time: i64, anchor: i64, interval: i64) -> Result<i64> {
583    if interval <= 0 {
584        return Ok(time);
585    }
586    if time <= anchor {
587        return Ok(anchor);
588    }
589
590    let diff = i128::from(time) - i128::from(anchor);
591    let interval = i128::from(interval);
592    let k = (diff + interval - 1) / interval;
593    let boundary = i128::from(anchor) + k * interval;
594
595    i64::try_from(boundary).map_err(|_| {
596        UnexpectedSnafu {
597            err_msg: format!(
598                "Cannot align time {time} to the next `anchor + k * interval` boundary (anchor={anchor}, interval={interval}): result {boundary} does not fit in i64"
599            ),
600        }
601        .build()
602    })
603}
604
605/// Rounds a `Utc` instant up to the next whole second (Unix seconds), with
606/// nanosecond precision.
607///
608/// `timestamp()` / `timestamp_millis()` floor the sub-second fraction; using
609/// them unchanged could produce a `start_secs` in the past relative to the
610/// exact prepare instant, which would make the very first evaluation due before
611/// the flow finished being prepared. Uses checked arithmetic so an instant at
612/// the very end of the `i64` second range yields an explicit error instead of
613/// wrapping.
614pub(crate) fn ceil_to_whole_sec(now: DateTime<Utc>) -> Result<i64> {
615    ceil_whole_sec_from_parts(now.timestamp(), now.timestamp_subsec_nanos() != 0)
616}
617
618/// Pure ceiling computation factored out of [`ceil_to_whole_sec`] so the
619/// overflow path is testable: `chrono::DateTime<Utc>` cannot represent
620/// instants near `i64::MAX` seconds, but the arithmetic below guards it anyway.
621pub(crate) fn ceil_whole_sec_from_parts(secs: i64, has_fraction: bool) -> Result<i64> {
622    if !has_fraction {
623        return Ok(secs);
624    }
625    secs.checked_add(1).context(error::UnexpectedSnafu {
626        err_msg: format!(
627            "Cannot round instant at second {secs} up to the next whole second: timestamp overflow"
628        ),
629    })
630}
631
632/// Returns the effective typed schedule config for flow metadata.
633///
634/// New metadata should carry `FlowInfoValue.eval_schedule`. For older metadata
635/// that lacks the typed field, derive a deterministic config from `created_time`
636/// and defaults. This avoids recovery-time wall-clock drift.
637pub fn effective_eval_schedule_from_flow_info(
638    flow_info: &FlowInfoValue,
639) -> Result<Option<FlowScheduleConfig>> {
640    if let Some(schedule) = &flow_info.eval_schedule {
641        return Ok(Some(schedule.clone()));
642    }
643
644    let Some(eval_interval_secs) = flow_info.eval_interval_secs else {
645        return Ok(None);
646    };
647    if eval_interval_secs <= 0 {
648        return Ok(None);
649    }
650
651    // Round the created_time up to the next whole second (same helper as new
652    // flow resolution) before aligning to the epoch-anchored boundary.
653    let created_ceil = ceil_to_whole_sec(flow_info.created_time)?;
654    let start_secs = ceil_to_boundary(
655        created_ceil,
656        FlowScheduleConfig::DEFAULT_ANCHOR_SECS,
657        eval_interval_secs,
658    )?;
659
660    Ok(Some(FlowScheduleConfig::default_with_start(
661        start_secs,
662        eval_interval_secs,
663    )))
664}
665
666/// Resolve `FlowScheduleConfig` into `task.eval_schedule`.
667///
668/// This must be called in `on_prepare` after `prev_flow_info` is loaded so that
669/// `CreateRequest` and `FlowInfoValue` both see the same resolved defaults.
670///
671/// The function is idempotent: if `task.eval_schedule` is already `Some`,
672/// it returns immediately (important for procedure retry / dump-restore).
673///
674/// The schedule phase (anchor) is the `EVAL OFFSET` value: boundaries are
675/// `offset + k * interval` (Unix epoch seconds), independent of timezone/DST.
676/// An omitted offset means zero. It is NOT anchored to create time and does
677/// not drift. Schedule configuration is NOT read from `task.flow_options`
678/// (those keys are no longer user-facing options).
679///
680/// For OR REPLACE, the previous typed config is preserved when interval+offset
681/// are unchanged; otherwise the schedule is recomputed.
682///
683/// Fallible: if the next phase boundary cannot fit in `i64` (extremely far
684/// future), an explicit error is returned and flow creation fails instead of
685/// silently clamping to a non-phase timestamp.
686pub(crate) fn resolve_schedule_defaults_into_task(
687    task: &mut CreateFlowTask,
688    prev_flow_info: Option<&FlowInfoValue>,
689) -> Result<()> {
690    // Idempotent: if already computed, skip recomputation.
691    if task.eval_schedule.is_some() {
692        return Ok(());
693    }
694
695    let Some(eval_interval_secs) = task.eval_interval_secs else {
696        return Ok(());
697    };
698    if eval_interval_secs <= 0 {
699        return Ok(());
700    }
701
702    let anchor_secs = task.eval_offset_secs.unwrap_or(0);
703
704    // Defense: `validate_flow_options` (called before this in `on_prepare`)
705    // already rejects out-of-range offsets; guard here to never schedule on a
706    // garbage anchor.
707    if !(0..eval_interval_secs).contains(&anchor_secs) {
708        return Ok(());
709    }
710
711    // For OR REPLACE: if interval+anchor unchanged, preserve the entire
712    // existing typed config so start / policy / limits are stable.
713    if task.or_replace
714        && let Some(prev) = prev_flow_info
715        && let Some(old_sched) = effective_eval_schedule_from_flow_info(prev)?
716    {
717        let old_interval = prev.eval_interval_secs.unwrap_or(0);
718        if old_interval == eval_interval_secs && old_sched.anchor_secs == anchor_secs {
719            task.eval_schedule = Some(old_sched);
720            return Ok(());
721        }
722    }
723
724    // New flow, or OR REPLACE with changed interval/offset: start at the next
725    // aligned boundary strictly after the exact prepare instant, rounded up to
726    // the next whole second so the first boundary is never in the past. The
727    // value is written into task.eval_schedule once so procedure retry does not
728    // recompute it.
729    let prepare_secs = ceil_to_whole_sec(chrono::Utc::now())?;
730    let start_secs = ceil_to_boundary(prepare_secs, anchor_secs, eval_interval_secs)?;
731
732    task.eval_schedule = Some(FlowScheduleConfig::with_anchor(
733        anchor_secs,
734        start_secs,
735        eval_interval_secs,
736    ));
737    Ok(())
738}
739
740fn user_runtime_flow_options(options: &HashMap<String, String>) -> HashMap<String, String> {
741    let mut options = options.clone();
742    options.remove(DEFER_ON_MISSING_SOURCE_KEY);
743    options.remove(INTERNAL_EVAL_SCHEDULE_KEY);
744    options.remove(INTERNAL_EVAL_OFFSET_KEY);
745    options
746}
747
748/// The state of [CreateFlowProcedure].
749#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, PartialEq)]
750pub enum CreateFlowState {
751    /// Prepares to create the flow.
752    Prepare,
753    /// Creates flows on the flownode.
754    CreateFlows,
755    /// Invalidate flow cache.
756    InvalidateFlowCache,
757    /// Create metadata.
758    CreateMetadata,
759}
760
761/// The type of flow.
762#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
763pub enum FlowType {
764    /// The flow is a batching task.
765    #[default]
766    Batching,
767    /// The flow is a streaming task.
768    Streaming,
769}
770
771pub const FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY: &str =
772    "experimental_enable_incremental_read";
773
774impl FlowType {
775    pub const BATCHING: &str = "batching";
776    pub const STREAMING: &str = "streaming";
777    pub const FLOW_TYPE_KEY: &str = "flow_type";
778}
779
780impl fmt::Display for FlowType {
781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
782        match self {
783            FlowType::Batching => write!(f, "{}", FlowType::BATCHING),
784            FlowType::Streaming => write!(f, "{}", FlowType::STREAMING),
785        }
786    }
787}
788
789/// The serializable data.
790#[derive(Debug, Serialize, Deserialize)]
791pub struct CreateFlowData {
792    pub(crate) state: CreateFlowState,
793    pub(crate) task: CreateFlowTask,
794    pub(crate) flow_id: Option<FlowId>,
795    pub(crate) peers: Vec<Peer>,
796    pub(crate) source_table_ids: Vec<TableId>,
797    #[serde(default)]
798    pub(crate) unresolved_source_table_names: Vec<TableName>,
799    /// Use alias for backward compatibility with QueryContext serialized data
800    #[serde(alias = "query_context")]
801    pub(crate) flow_context: FlowQueryContext,
802    /// For verify if prev value is consistent when need to update flow metadata.
803    /// only set when `or_replace` is true.
804    pub(crate) prev_flow_info_value: Option<DeserializedValueWithBytes<FlowInfoValue>>,
805    /// Only set to true when replace actually happened.
806    /// This is used to determine whether to invalidate the cache.
807    #[serde(default)]
808    pub(crate) did_replace: bool,
809    pub(crate) flow_type: Option<FlowType>,
810}
811
812impl CreateFlowData {
813    pub(crate) fn is_pending(&self) -> bool {
814        !self.unresolved_source_table_names.is_empty()
815    }
816
817    pub(crate) fn is_active(&self) -> bool {
818        !self.is_pending()
819    }
820}
821
822impl From<&CreateFlowData> for CreateRequest {
823    fn from(value: &CreateFlowData) -> Self {
824        let flow_id = value.flow_id.unwrap();
825        let source_table_ids = &value.source_table_ids;
826
827        let mut req = CreateRequest {
828            flow_id: Some(api::v1::FlowId { id: flow_id }),
829            source_table_ids: source_table_ids
830                .iter()
831                .map(|table_id| api::v1::TableId { id: *table_id })
832                .collect_vec(),
833            sink_table_name: Some(value.task.sink_table_name.clone().into()),
834            // Always be true to ensure idempotent in case of retry
835            create_if_not_exists: true,
836            or_replace: value.task.or_replace,
837            expire_after: value.task.expire_after.map(|value| ExpireAfter { value }),
838            eval_interval: value
839                .task
840                .eval_interval_secs
841                .map(|seconds| api::v1::EvalInterval { seconds }),
842            comment: value.task.comment.clone(),
843            sql: value.task.sql.clone(),
844            flow_options: user_runtime_flow_options(&value.task.flow_options),
845        };
846
847        let flow_type = value.flow_type.unwrap_or_default().to_string();
848        req.flow_options
849            .insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type);
850
851        // Pass typed schedule config via internal transient key in flow_options.
852        if let Some(ref sched) = value.task.eval_schedule {
853            let json = serde_json::to_string(sched)
854                .expect("FlowScheduleConfig serialization should not fail");
855            req.flow_options
856                .insert(INTERNAL_EVAL_SCHEDULE_KEY.to_string(), json);
857        }
858
859        req
860    }
861}
862
863impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteValue)>) {
864    fn from(value: &CreateFlowData) -> Self {
865        let catalog_name = value.task.catalog_name.clone();
866        let flow_name = value.task.flow_name.clone();
867        let sink_table_name = value.task.sink_table_name.clone();
868        let expire_after = value.task.expire_after;
869        let eval_interval = value.task.eval_interval_secs;
870        let comment = value.task.comment.clone();
871        let sql = value.task.sql.clone();
872        let eval_schedule = value.task.eval_schedule.clone();
873
874        // Start with a clean options map. The transient schedule/offset payloads
875        // are only for the meta→flownode / frontend→meta boundaries and must not
876        // be persisted in FlowInfoValue.options.
877        let mut options: HashMap<String, String> = value
878            .task
879            .flow_options
880            .iter()
881            .filter(|(k, _)| {
882                k.as_str() != INTERNAL_EVAL_SCHEDULE_KEY && k.as_str() != INTERNAL_EVAL_OFFSET_KEY
883            })
884            .map(|(k, v)| (k.clone(), v.clone()))
885            .collect();
886
887        let flownode_ids = value
888            .peers
889            .iter()
890            .enumerate()
891            .map(|(idx, peer)| (idx as u32, peer.id))
892            .collect::<BTreeMap<_, _>>();
893        let flow_routes = value
894            .peers
895            .iter()
896            .enumerate()
897            .map(|(idx, peer)| (idx as u32, FlowRouteValue { peer: peer.clone() }))
898            .collect::<Vec<_>>();
899
900        let flow_type = value.flow_type.unwrap_or_default().to_string();
901        options.insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type);
902
903        let mut create_time = chrono::Utc::now();
904        if let Some(prev_flow_value) = value.prev_flow_info_value.as_ref()
905            && value.task.or_replace
906        {
907            create_time = prev_flow_value.get_inner_ref().created_time;
908        }
909
910        // This conversion borrows the procedure data: the procedure keeps using
911        // `self.data` after metadata creation (for cache invalidation, retry and
912        // procedure dump/restore), while `FlowInfoValue` owns the persisted
913        // metadata. Cloning these owned fields is therefore intentional.
914        let flow_info: FlowInfoValue = FlowInfoValue {
915            source_table_ids: value.source_table_ids.clone(),
916            all_source_table_names: value.task.source_table_names.clone(),
917            unresolved_source_table_names: value.unresolved_source_table_names.clone(),
918            sink_table_name: sink_table_name.clone(),
919            flownode_ids,
920            catalog_name: catalog_name.clone(),
921            query_context: Some(without_scheduled_time_extension(QueryContext::from(
922                value.flow_context.clone(),
923            ))),
924            flow_name: flow_name.clone(),
925            raw_sql: sql.clone(),
926            expire_after,
927            eval_interval_secs: eval_interval,
928            comment: comment.clone(),
929            options,
930            status: if value.is_active() {
931                FlowStatus::Active
932            } else {
933                FlowStatus::PendingSources
934            },
935            created_time: create_time,
936            updated_time: chrono::Utc::now(),
937            eval_schedule: eval_schedule.clone(),
938        };
939
940        (flow_info, flow_routes)
941    }
942}