Skip to content

Commit 36b67d7

Browse files
committed
Support an optional created_at on pipeline creation
PipelinePost takes an optional created_at in microseconds since the Unix epoch, applied to both the pipeline and its initial job. When omitted the inserts fall back to CURRENT_TIMESTAMP via COALESCE, preserving the existing database-clock behavior.
1 parent e78f7b5 commit 36b67d7

6 files changed

Lines changed: 45 additions & 8 deletions

File tree

crates/arroyo-api/queries/api_queries.sql

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ WHERE organization_id = :organization_id AND pub_id = :pub_id;
123123

124124
--: DbPipeline (state?, ttl_micros?, state_url?)
125125

126-
--! create_pipeline(textual_repr?, state_url?)
127-
INSERT INTO pipelines (pub_id, organization_id, created_by, name, type, textual_repr, udfs, program, proto_version, state_url, tags)
128-
VALUES (:pub_id, :organization_id, :created_by, :name, :type, :textual_repr, :udfs, :program, :proto_version, :state_url, :tags);
126+
--! create_pipeline(textual_repr?, state_url?, created_at?)
127+
INSERT INTO pipelines (pub_id, organization_id, created_by, name, type, textual_repr, udfs, program, proto_version, state_url, tags, created_at)
128+
VALUES (:pub_id, :organization_id, :created_by, :name, :type, :textual_repr, :udfs, :program, :proto_version, :state_url, :tags, COALESCE(:created_at, CURRENT_TIMESTAMP));
129129

130130
--! get_pipelines : DbPipeline
131131
SELECT pipelines.id, pipelines.pub_id, name, type, textual_repr, udfs, program, checkpoint_interval_micros, stop, pipelines.created_at, state, parallelism_overrides, ttl_micros, state_url, tags
@@ -189,10 +189,10 @@ SET
189189
ignore_state_before_epoch = :ignore_state_before_epoch
190190
WHERE id = :job_id AND organization_id = :organization_id;
191191

192-
--! create_job(ttl_micros?)
192+
--! create_job(ttl_micros?, created_at?)
193193
INSERT INTO job_configs
194-
(id, organization_id, pipeline_name, created_by, pipeline_id, checkpoint_interval_micros, ttl_micros, env_vars, scheduler_config)
195-
VALUES (:id, :organization_id, :pipeline_name, :created_by, :pipeline_id, :checkpoint_interval_micros, :ttl_micros, :env_vars, :scheduler_config);
194+
(id, organization_id, pipeline_name, created_by, pipeline_id, checkpoint_interval_micros, ttl_micros, env_vars, scheduler_config, created_at)
195+
VALUES (:id, :organization_id, :pipeline_name, :created_by, :pipeline_id, :checkpoint_interval_micros, :ttl_micros, :env_vars, :scheduler_config, COALESCE(:created_at, CURRENT_TIMESTAMP));
196196

197197
--! create_job_status
198198
INSERT INTO job_statuses (pub_id, id, organization_id) VALUES (:pub_id, :id, :organization_id);

crates/arroyo-api/src/jobs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use futures_util::stream::Stream;
1818
use std::convert::Infallible;
1919
use std::str::FromStr;
2020
use std::{collections::HashMap, time::Duration};
21+
use time::OffsetDateTime;
2122
use tokio_stream::StreamExt as _;
2223
use tokio_stream::wrappers::ReceiverStream;
2324
use tonic::{Code, Request};
@@ -123,6 +124,7 @@ pub(crate) async fn create_job(
123124
db: &DatabaseSource,
124125
env_vars: HashMap<String, String>,
125126
scheduler_config: serde_json::Value,
127+
created_at: Option<OffsetDateTime>,
126128
) -> Result<String, ErrorResp> {
127129
let checkpoint_interval = if preview {
128130
Duration::from_secs(24 * 60 * 60)
@@ -180,6 +182,7 @@ pub(crate) async fn create_job(
180182
}),
181183
&env_vars_json,
182184
&scheduler_config,
185+
&created_at,
183186
)
184187
.await?;
185188

crates/arroyo-api/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::pipelines::{
3838
__path_restart_pipeline, __path_validate_query,
3939
};
4040
use crate::rest::__path_ping;
41-
use crate::rest_utils::{ErrorResp, service_unavailable};
41+
use crate::rest_utils::{ErrorResp, bad_request, service_unavailable};
4242
use crate::udfs::{__path_create_udf, __path_delete_udf, __path_get_udfs, __path_validate_udf};
4343
use arroyo_rpc::api_types::{checkpoints::*, connections::*, metrics::*, pipelines::*, udfs::*, *};
4444
use arroyo_rpc::config::{ApiAuthMode, config};
@@ -119,6 +119,11 @@ pub(crate) fn to_micros(dt: OffsetDateTime) -> u64 {
119119
(dt.unix_timestamp_nanos() / 1_000) as u64
120120
}
121121

122+
pub(crate) fn from_micros(micros: u64) -> Result<OffsetDateTime, ErrorResp> {
123+
OffsetDateTime::from_unix_timestamp_nanos((micros as i128) * 1_000)
124+
.map_err(|_| bad_request(format!("timestamp out of range: {micros}")))
125+
}
126+
122127
pub async fn compiler_service() -> Result<CompilerGrpcClient<Channel>, ErrorResp> {
123128
// TODO: cache this
124129
let config = config();

crates/arroyo-api/src/pipelines.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use crate::rest_utils::{
5151
};
5252
use crate::types::public::{PipelineType, RestartMode, StopMode};
5353
use crate::udfs::build_udf;
54-
use crate::{connection_tables, to_micros};
54+
use crate::{connection_tables, from_micros, to_micros};
5555
use arroyo_rpc::config::{JobControllerMode, config};
5656
use arroyo_rpc::errors::ErrorDomain;
5757
use arroyo_types::to_millis;
@@ -307,6 +307,7 @@ pub(crate) async fn create_pipeline_int(
307307
tags: HashMap<String, String>,
308308
env_vars: HashMap<String, String>,
309309
scheduler_config: serde_json::Value,
310+
created_at: Option<OffsetDateTime>,
310311
) -> Result<String, ErrorResp> {
311312
if parallelism > auth.org_metadata.max_parallelism as u64 {
312313
return Err(bad_request(format!(
@@ -413,6 +414,7 @@ pub(crate) async fn create_pipeline_int(
413414
&2,
414415
&state_url,
415416
&tags_json,
417+
&created_at,
416418
)
417419
.await?;
418420

@@ -445,6 +447,7 @@ pub(crate) async fn create_pipeline_int(
445447
db,
446448
env_vars,
447449
scheduler_config,
450+
created_at,
448451
)
449452
.await?;
450453

@@ -652,6 +655,8 @@ async fn create_pipeline_inner(
652655

653656
let udfs = pipeline_post.udfs.unwrap_or_default();
654657

658+
let created_at = pipeline_post.created_at.map(from_micros).transpose()?;
659+
655660
let compiled = compile_sql(
656661
pipeline_post.query.clone(),
657662
&udfs,
@@ -681,6 +686,7 @@ async fn create_pipeline_inner(
681686
None | Some(serde_json::Value::Null) => serde_json::Value::Object(Default::default()),
682687
Some(v) => v,
683688
},
689+
created_at,
684690
)
685691
.await?;
686692

@@ -736,6 +742,7 @@ pub async fn create_preview_pipeline(
736742
HashMap::default(),
737743
HashMap::default(),
738744
serde_json::Value::Object(Default::default()),
745+
None,
739746
)
740747
.await?;
741748

crates/arroyo-rpc/src/api_types/pipelines.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ pub struct PipelinePost {
3939
/// all mean "use the controller's global scheduler config
4040
/// unchanged".
4141
pub scheduler_config: Option<serde_json::Value>,
42+
/// Optional creation timestamp, in microseconds since the Unix epoch,
43+
/// applied to both the pipeline and its initial job. If omitted, the
44+
/// database's current time is used.
45+
pub created_at: Option<u64>,
4246
}
4347

4448
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]

webui/src/gen/api-types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,13 +861,31 @@ export interface components {
861861
PipelinePatch: {
862862
/** Format: int64 */
863863
checkpoint_interval_micros?: number | null;
864+
/** @description Per-job environment variables forwarded to workers. */
865+
env_vars?: {
866+
[key: string]: string;
867+
} | null;
864868
/** Format: int64 */
865869
parallelism?: number | null;
870+
/** @description Per-job scheduler configuration overlay. The shape mirrors the
871+
* controller's global scheduler config (e.g. the
872+
* `kubernetes-scheduler.*` block) and is merged on top of it at
873+
* scheduling time. An omitted field, `null`, or an empty object
874+
* all mean "use the controller's global scheduler config
875+
* unchanged". */
876+
scheduler_config?: unknown;
866877
stop?: components["schemas"]["StopType"] | null;
867878
};
868879
PipelinePost: {
869880
/** Format: int64 */
870881
checkpoint_interval_micros?: number | null;
882+
/**
883+
* Format: int64
884+
* @description Optional creation timestamp, in microseconds since the Unix epoch,
885+
* applied to both the pipeline and its initial job. If omitted, the
886+
* database's current time is used.
887+
*/
888+
created_at?: number | null;
871889
/** @description Per-job environment variables forwarded to workers. */
872890
env_vars?: {
873891
[key: string]: string;

0 commit comments

Comments
 (0)