Packet Circuit

Make Any Function Async with sync_async

Published on December 04, 2024

Working with Rust's async ecosystem can often feel like navigating a tightrope. The strict boundaries between synchronous and asynchronous code, while ensuring performance and safety, can present challenges when integrating legacy synchronous code with modern asynchronous workflows. The async_sync crate aims to address these challenges by providing robust tools to simplify sync-to-async transitions with features like retries, timeouts, diagnostics, and more.

Before we dive in, it's worth noting the existence of the maybe_async crate, which provides a powerful macro-based approach for writing code that can work in both sync and async contexts. While maybe_async is a fantastic tool for abstracting over sync/async code, async_sync takes a more focused approach, offering practical, implementation-oriented utilities to manage specific scenarios where synchronous Rust code needs to coexist and integrate with async ecosystems.

Whether you’re working with legacy code, performing CPU-intensive computations, or handling transient failures, async_sync equips you with tools tailored to bridge these sync-async gaps seamlessly.


Why Choose async_sync?

async_sync provides:

  1. Implementation-Focused Design: Tools like retries, backoff strategies, and timeout handling solve real-world challenges without relying on abstraction layers.
  2. Compatibility with Any Async Ecosystem: Works seamlessly with popular runtimes like Tokio and AsyncStd.
  3. Built-In Diagnostics: Capture execution times and errors for better debugging and performance insights.
  4. Developer-Friendly API: High-level utilities minimize boilerplate, making sync-to-async transitions straightforward.

Features in Action

Let’s explore the key features of async_sync with real-world examples based on its comprehensive test suite.


1. Retry Mechanism with Backoff Strategies

Retries are essential for transient errors like network issues. async_sync makes it easy to implement retries with configurable backoff strategies, such as constant or exponential delays.

#[tokio::test]
async fn test_sync_to_async_with_retries_success() {
    let counter = Arc::new(Mutex::new(0));
    let result = sync_to_async_with_retries(
        || flaky_function(Arc::clone(&counter)),
        5,
        Duration::from_millis(100),
        Backoff::Constant,
    ).await;

    assert_eq!(result.unwrap(), "Success");
}

/// Mock function for retries
fn flaky_function(counter: Arc<Mutex<i32>>) -> Result<String, &'static str> {
    let mut count = counter.lock().unwrap();
    *count += 1;
    if *count < 3 {
        Err("Failure")
    } else {
        Ok("Success".to_string())
    }
}

2. Timeout Handling with Cleanup

Ensure that long-running tasks don’t block indefinitely by adding timeouts. With async_sync, you can also specify a cleanup action to execute if the timeout occurs.

#[tokio::test]
async fn test_sync_to_async_with_timeout_and_cleanup() {
    let result = sync_to_async_with_timeout_and_cleanup(
        || heavy_computation(5),
        Duration::from_millis(50),
        cleanup_action,
    ).await;

    assert!(result.is_err());
}

/// Mock computation
fn heavy_computation(x: i32) -> i32 {
    std::thread::sleep(Duration::from_millis(200));
    x * x
}

/// Mock cleanup action
fn cleanup_action() {
    println!("Cleanup executed!");
}

3. Parallel Execution of Sync Tasks

Run multiple synchronous tasks in parallel to maximize efficiency, especially for independent CPU-bound operations.

#[tokio::test]
async fn test_parallel_sync_to_async() {
    let tasks: Vec<_> = (1..=5).map(|x| move || heavy_computation(x)).collect();
    let results = parallel_sync_to_async(tasks).await;

    for (i, result) in results.into_iter().enumerate() {
        assert_eq!(result.unwrap(), (i + 1).pow(2));
    }
}

4. Detailed Diagnostics

Log execution times of synchronous tasks to gain insight into performance.

#[tokio::test]
async fn test_sync_to_async_with_diagnostics() {
    let result = sync_to_async_with_diagnostics(|| heavy_computation(5)).await;
    assert_eq!(result.unwrap(), 25);
}

5. Flexible Argument Passing

Simplify passing arguments to synchronous functions using sync_to_async_with_args.

#[tokio::test]
async fn test_sync_to_async_with_args() {
    let result = sync_to_async_with_args(|x: i32| heavy_computation(x), 5).await;
    assert_eq!(result.unwrap(), 25);
}

6. Sync-Async Integration with Multiple Runtimes

Integrate synchronous functions into async workflows, choosing between Tokio or AsyncStd runtimes.

#[test]
fn test_sync_to_async_with_runtime_tokio() {
    let result = sync_to_async_with_runtime(Runtime::Tokio, || heavy_computation(4));
    assert_eq!(result.unwrap(), 16);
}

7. Aggregation of Results

Combine results from multiple synchronous tasks in a batch, ensuring all tasks are executed and their results returned.

#[tokio::test]
async fn test_aggregate_results() {
    let tasks: Vec<_> = (1..=3).map(|x| move || heavy_computation(x)).collect();
    let results = aggregate_results(tasks).await;

    for (i, result) in results.into_iter().enumerate() {
        assert_eq!(result.unwrap(), (i + 1).pow(2));
    }
}

The async_sync crate is your go-to tool for managing sync-async transitions in Rust. Its feature set includes retries, timeouts, parallel execution, and diagnostics—all tested and ready for real-world use.

Whether you're integrating legacy code or building new async systems, async_sync ensures seamless operation and robust performance, reducing the complexity of bridging synchronous and asynchronous Rust codebases.

By simplifying these interactions, async_sync allows you to focus on your core logic rather than managing sync-async boundaries.


Getting Started

  1. Add async_sync to your Cargo.toml:
   [dependencies]
   async_sync = "0.1.0"
  1. Import and use the library in your project:
   use async_sync::{sync_to_async_with_retries, Backoff};

   #[tokio::main]
   async fn main() {
       let result = sync_to_async_with_retries(
           || some_sync_function(),
           3,
           Duration::from_millis(200),
           Backoff::Exponential,
       ).await;

       match result {
           Ok(value) => println!("Task succeeded with value: {:?}", value),
           Err(err) => eprintln!("Task failed with error: {:?}", err),
       }
   }

This snippet demonstrates using the sync_to_async_with_retries function with exponential backoff to handle transient failures in a synchronous function.

Start bridging the gap between sync and async in Rust today with async_sync. It's lightweight, powerful, and designed to make your Rust development smoother and more efficient!


Check out this YouTube video. It's a great introduction to the async_sync crate and how it can help you bridge the gap between sync and async code in Rust.

For more information, check out the official async_sync documentation and GitHub repository.