aws_utils_scheduler 0.4.0

A Rust wrapper for AWS EventBridge Scheduler with type-safe builders for schedule expressions
Documentation

aws_utils_scheduler

A Rust wrapper for AWS EventBridge Scheduler with type-safe builders for schedule expressions.

Crates.io Documentation License

Overview

aws_utils_scheduler provides a convenient and type-safe interface for working with AWS EventBridge Scheduler. It includes:

  • Simple client creation with optional endpoint configuration
  • Type-safe builders for schedule expressions (at, rate, cron)
  • Stream-based pagination for listing schedules
  • Comprehensive error handling

Installation

Add this to your Cargo.toml:

[dependencies]
aws_utils_scheduler = "0.1.0"

Usage

Creating a Client

use aws_utils_scheduler;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client with default timeout configuration
    let client = aws_utils_scheduler::make_client_with_timeout_default(None).await;
    
    // Or with a custom endpoint and default timeouts
    let client = aws_utils_scheduler::make_client_with_timeout_default(
        Some("http://localhost:4566".to_string())
    ).await;
    
    // Or with custom timeout settings
    let client = aws_utils_scheduler::make_client_with_timeout(
        None,
        Some(Duration::from_secs(30)),   // connect timeout
        Some(Duration::from_secs(120)),  // operation timeout
        Some(Duration::from_secs(60)),   // operation attempt timeout
        Some(Duration::from_secs(30)),   // read timeout
    ).await;
    
    // Or without timeout configuration
    let client = aws_utils_scheduler::make_client(None, None, None).await;
    
    Ok(())
}

Logging AWS Communication

make_client accepts an optional [SharedInterceptor]. By passing an interceptor that implements aws_sdk_scheduler::config::Intercept, you can run custom logic — such as logging — every time the client communicates with AWS.

The interceptor below logs each request, response, and operation result. It uses the tracing crate, which is also what the AWS SDK uses internally.

use aws_utils_scheduler::make_client;
use aws_sdk_scheduler::config::{
    ConfigBag, Intercept, RuntimeComponents, SharedInterceptor,
    interceptors::{
        AfterDeserializationInterceptorContextRef, BeforeDeserializationInterceptorContextRef,
        BeforeTransmitInterceptorContextRef,
    },
};

type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;

#[derive(Debug, Clone)]
struct LoggingInterceptor;

impl Intercept for LoggingInterceptor {
    fn name(&self) -> &'static str {
        "SchedulerLoggingInterceptor"
    }

    // Called just before each HTTP request is sent (once per retry attempt).
    fn read_before_transmit(
        &self,
        context: &BeforeTransmitInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let request = context.request();
        tracing::info!(
            method = %request.method(),
            uri = %request.uri(),
            "Scheduler -> AWS request"
        );
        Ok(())
    }

    // Called right after each HTTP response is received.
    fn read_before_deserialization(
        &self,
        context: &BeforeDeserializationInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let response = context.response();
        tracing::info!(status = %response.status(), "AWS -> Scheduler response");
        Ok(())
    }

    // Called once when the operation completes (after retries), with success or error.
    fn read_after_deserialization(
        &self,
        context: &AfterDeserializationInterceptorContextRef<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        match context.output_or_error() {
            Ok(_) => tracing::info!("Scheduler operation succeeded"),
            Err(err) => tracing::warn!(error = %err, "Scheduler operation failed"),
        }
        Ok(())
    }
}

# async fn run() {
// Pass the interceptor as the third argument.
let client = make_client(None, None, Some(SharedInterceptor::new(LoggingInterceptor))).await;
# }

tracing does not emit anything until a subscriber is initialized. Set one up once in your application (for example with tracing-subscriber) and control verbosity with RUST_LOG:

// Add `tracing-subscriber` to your dependencies.
tracing_subscriber::fmt()
    .with_env_filter(
        tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "info".into()),
    )
    .init();

Example output (RUST_LOG=info):

INFO SchedulerLoggingInterceptor: Scheduler -> AWS request method=POST uri=https://scheduler.ap-northeast-1.amazonaws.com/
INFO SchedulerLoggingInterceptor: AWS -> Scheduler response status=200
INFO SchedulerLoggingInterceptor: Scheduler operation succeeded

Creating Schedules

One-time Schedule (At Expression)

use aws_utils_scheduler::{scheduler, builder::AtExpressionBuilder};
use aws_sdk_scheduler::types::{Target, FlexibleTimeWindow, FlexibleTimeWindowMode};
use chrono::{Utc, Duration};

let future_time = Utc::now() + Duration::hours(1);
let at_expression = AtExpressionBuilder::new(future_time).build()?;

let target = Target::builder()
    .arn("arn:aws:lambda:us-east-1:123456789012:function:MyFunction")
    .role_arn("arn:aws:iam::123456789012:role/MyRole")
    .build()
    .unwrap();

let flexible_window = FlexibleTimeWindow::builder()
    .mode(FlexibleTimeWindowMode::Off)
    .build()
    .unwrap();

scheduler::create_schedule(
    &client,
    "my-schedule",
    None,  // group_name
    &at_expression,
    None,  // start_date
    None,  // end_date
    None,  // description
    None,  // timezone
    None,  // state
    None,  // kms_key_arn
    Some(target),
    Some(flexible_window),
    None,  // client_token
    None,  // action_after_completion
).await?;

Recurring Schedule (Rate Expression)

use aws_utils_scheduler::builder::{RateExpressionBuilder, RateUnit};
use aws_sdk_scheduler::types::{Target, FlexibleTimeWindow, FlexibleTimeWindowMode};

let rate_expression = RateExpressionBuilder::new(5, RateUnit::Minutes).build()?;

let target = Target::builder()
    .arn("arn:aws:lambda:us-east-1:123456789012:function:MyFunction")
    .role_arn("arn:aws:iam::123456789012:role/MyRole")
    .build()
    .unwrap();

let flexible_window = FlexibleTimeWindow::builder()
    .mode(FlexibleTimeWindowMode::Off)
    .build()
    .unwrap();

scheduler::create_schedule(
    &client,
    "my-recurring-schedule",
    None,  // group_name
    &rate_expression,
    None,  // start_date
    None,  // end_date
    None,  // description
    None,  // timezone
    None,  // state
    None,  // kms_key_arn
    Some(target),
    Some(flexible_window),
    None,  // client_token
    None,  // action_after_completion
).await?;

Cron Schedule

use aws_utils_scheduler::builder::CronExpressionBuilder;
use aws_sdk_scheduler::types::{Target, FlexibleTimeWindow, FlexibleTimeWindowMode};

let cron_expression = CronExpressionBuilder::new()
    .minutes("0")
    .hours("12")
    .days_of_month("*")
    .months("*")
    .days_of_week("MON-FRI")
    .build()?;

let target = Target::builder()
    .arn("arn:aws:lambda:us-east-1:123456789012:function:MyFunction")
    .role_arn("arn:aws:iam::123456789012:role/MyRole")
    .build()
    .unwrap();

let flexible_window = FlexibleTimeWindow::builder()
    .mode(FlexibleTimeWindowMode::Off)
    .build()
    .unwrap();

scheduler::create_schedule(
    &client,
    "weekday-noon-schedule",
    None,  // group_name
    &cron_expression,
    None,  // start_date
    None,  // end_date
    None,  // description
    None,  // timezone
    None,  // state
    None,  // kms_key_arn
    Some(target),
    Some(flexible_window),
    None,  // client_token
    None,  // action_after_completion
).await?;

Listing Schedules

Stream-based Listing

use futures_util::TryStreamExt;

let stream = scheduler::list_schedules_stream(
    &client,
    None::<String>,  // name_prefix
    None::<String>,  // group_name
    None,            // state
);
futures_util::pin_mut!(stream);

while let Some(schedule) = stream.try_next().await? {
    println!("Schedule: {:?}", schedule.name());
}

Batch Listing

let schedules = scheduler::list_schedules_all(
    &client,
    None::<String>,  // name_prefix
    None::<String>,  // group_name
    None,            // state
).await?;
for schedule in schedules {
    println!("Schedule: {:?}", schedule.name());
}

Other Operations

use aws_sdk_scheduler::types::{Target, FlexibleTimeWindow, FlexibleTimeWindowMode};

// Get schedule details
let schedule = scheduler::get_scheduler(
    &client,
    "my-schedule",
    None::<String>,  // group_name
).await?;

// Update a schedule
let target = Target::builder()
    .arn("arn:aws:lambda:us-east-1:123456789012:function:NewFunction")
    .role_arn("arn:aws:iam::123456789012:role/MyRole")
    .build()
    .unwrap();

let flexible_window = FlexibleTimeWindow::builder()
    .mode(FlexibleTimeWindowMode::Off)
    .build()
    .unwrap();

scheduler::update_schedule(
    &client,
    "my-schedule",
    None,  // group_name
    &new_expression,
    None,  // start_date
    None,  // end_date
    None,  // description
    None,  // timezone
    None,  // state
    None,  // kms_key_arn
    Some(target),
    Some(flexible_window),
    None,  // client_token
    None,  // action_after_completion
).await?;

// Delete a schedule
scheduler::delete_schedule(
    &client,
    "my-schedule",
    None::<String>,  // group_name
    None::<String>,  // client_token
).await?;

Schedule Expression Builders

AtExpressionBuilder

Creates one-time schedules that run at a specific date and time.

use chrono::Utc;
let at_expr = AtExpressionBuilder::new(Utc::now() + Duration::days(1)).build()?;
// Returns: "at(2024-01-02T15:30:00)"

RateExpressionBuilder

Creates recurring schedules that run at regular intervals.

let rate_expr = RateExpressionBuilder::new(30, RateUnit::Minutes).build()?;
// Returns: "rate(30 minutes)"

CronExpressionBuilder

Creates schedules using cron expressions for complex timing requirements.

let cron_expr = CronExpressionBuilder::new()
    .minutes("0")
    .hours("9")
    .days_of_month("*")
    .months("*")
    .days_of_week("MON-FRI")
    .build()?;
// Returns: "cron(0 9 * * MON-FRI)"

Error Handling

The crate provides comprehensive error handling through the SchedulerError enum:

use aws_utils_scheduler::error::SchedulerError;

match scheduler::create_schedule(
    &client,
    name,
    group,
    expr,
    None,  // start_date
    None,  // end_date
    None,  // description
    None,  // timezone
    None,  // state
    None,  // kms_key_arn
    Some(target),
    Some(flexible_window),
    None,  // client_token
    None,  // action_after_completion
).await {
    Ok(_) => println!("Schedule created successfully"),
    Err(SchedulerError::Aws(e)) => eprintln!("AWS error: {}", e),
    Err(SchedulerError::InvalidScheduleExpression) => eprintln!("Invalid expression"),
    Err(e) => eprintln!("Other error: {}", e),
}

Important Notes

  • The make_client_with_timeout_default function provides reasonable default timeout values (connect: 3100s, operation: 60s, operation attempt: 55s, read: 50s) suitable for most use cases.
  • All schedule names must be unique within a schedule group.
  • The IAM role must have the necessary permissions to invoke the target.
  • The client uses the AWS SDK's default credential chain, supporting IAM roles, environment variables, and other standard authentication methods.

License

This project is licensed under either of

at your option.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.