Skip to content

Repository files navigation

AWS Durable Execution SDK for C++

A dependency-free C++23 foundation for building high-performance AWS Lambda durable executions. The API follows C++ value semantics and templates for hot paths while retaining the replay and checkpoint behavior of async-durable-execution.

Project status: early development. The replay/checkpoint core, durable steps, waits, retries, deterministic IDs, protocol models, and service-client boundary are implemented. The dependency-free Lambda wire codec, optional AWS SDK for C++ transport, and optional aws-lambda-cpp handler adapter are also implemented and compile-checked against the current official headers. Durable callbacks, child contexts, chained invoke, and wait-for-callback are implemented. Parallel/map composition and contention-aware checkpoint batching are implemented with deterministic branch IDs. Wait-for-condition, whole-block retry, replay-safe values, and recursive invoke are implemented. Typed declarative flow/DAG composition is implemented with pre-checkpoint validation and durable node isolation. A deterministic local runner provides virtual time, callback controls, mocked invokes, and inspectable history. Versioned instrumentation plugins expose invocation, operation, attempt, replay, and state-change lifecycle hooks with per-plugin fault isolation. Independent reference fixtures and the complete upstream conformance handler matrix are integrated. Deployed AWS conformance runs through the repository integration-test role.

Design goals

  • C++23 with strong types, RAII, std::expected, std::chrono, concepts, and zero-cost serializer customization.
  • Performance close to C for local hot paths. Known wire enums and history lookups do not allocate; serializers and operations are statically dispatched.
  • Deterministic replay with a stable, SDK-owned BLAKE2b operation-ID scheme. The algorithm and truncation are part of this SDK's durable-history contract.
  • Backward and forward compatibility built into the public model. ABI symbols use the inline v1 namespace; wire enums retain unknown future AWS values.
  • AWS integration behind service_client, keeping network and AWS SDK types out of workflow code and the core library.
  • A specialized wire parser skips unknown fields without constructing a general JSON DOM and decodes a minimal durable invocation in under one microsecond on the current development host.

Current API

#include <chrono>
#include <string>
#include <string_view>

#include <aws/durable_execution/durable_execution.hpp>

namespace durable = aws::durable_execution;
using namespace std::chrono_literals;

durable::invocation_output handle(
    const durable::invocation_input& input,
    durable::service_client& aws_client) {
  return durable::run(input, aws_client, [](std::string_view event) {
    const int validation = durable::step(
        [event] {
          // Nondeterministic I/O belongs inside a durable step.
          return event.empty() ? 0 : 1;
        },
        durable::step_config{.name = "validate_order"});

    if (validation == 0) {
      return std::string{"rejected"};
    }

    durable::wait(5s, "await_confirmation");
    return std::string{"approved"};
  });
}

run accepts handlers taking (durable_context&, std::string_view), std::string_view, durable_context&, or no arguments. Primitive results use the built-in JSON serializers. Applications provide a serializer object for domain types; its calls are statically dispatched.

AWS-native durable primitives use typed C++ handles and std::optional for backend results that may be absent:

auto callback = durable::create_callback(
    durable::callback_config{.name = "approval", .timeout = 1h});
send_approval_request(callback.callback_id());
const auto approval = callback.result();  // suspends until completed

const auto downstream = durable::invoke<std::string>(
    "worker:prod", std::string{"payload"},
    durable::invoke_config{.name = "worker-call"});

const int child_result = durable::run_in_child_context(
    [] { return durable::step([] { return 42; }); },
    durable::child_context_config{.name = "calculation"});

wait_for_callback composes callback creation, a checkpointed submitter step, and callback suspension inside a child context. Its submitter receives the callback ID directly:

const auto approval = durable::wait_for_callback(
    [](std::string_view callback_id) {
      submit_for_external_approval(callback_id);
    },
    durable::wait_for_callback_config{.name = "approval"});

Parallel and map operations use bounded workers, checkpoint batching, and durable child contexts:

auto prices = durable::map(
    [](const LineItem& item) {
      return durable::step([&] { return fetch_price(item); });
    },
    line_items,
    durable::map_config{.name = "price-items", .max_concurrency = 8});

auto first = durable::parallel(
    std::tuple{query_primary, query_replica},
    durable::parallel_config{
        .name = "race-replicas",
        .completion = durable::completion_config::first_successful()});

Replay-safe helpers checkpoint nondeterministic values through normal steps:

const double sample = durable::replay_safe::random();
const auto created_at = durable::replay_safe::now();
const durable::uuid_value request_id = durable::replay_safe::uuid();

Stateful polling and whole-block retry expose the attempt directly to the callable:

auto state = durable::wait_for_condition<PollState>(
    poll,
    PollState{},
    durable::polling_strategy{.max_attempts = 20});

auto result = durable::with_retry(
    [](std::uint32_t attempt) { return run_workflow_attempt(attempt); },
    durable::with_retry_config{.name = "workflow-retry"});

recurse records a chained self-invocation rather than growing the C++ stack:

auto child = durable::recurse_json<Result>(
    R"({"remaining":9})",
    durable::recurse_config{.with_recursive_level = true});

Declarative graphs use typed node handles and immutable dependency expressions:

durable::flow_builder graph;
auto load = graph.node("load", load_order);
auto price = graph.node(
    "price",
    [load](durable::flow_node_context& context) {
      return price_order(context.outcome(load));
    });
graph.depends_on(price, load.succeeded());
graph.outputs(price.outcome());

auto result = durable::flow(
    graph, durable::flow_config{.name = "order-flow"});
const Money total = result.output<Money>();

Local tests run without AWS credentials or wall-clock waits:

auto runner = durable::make_local_runner(
    order_handler,
    durable::local_runner_options{
        .input_json = R"({"order_id":"order-123"})"});

auto result = runner.run();
if (result.status() == durable::local_run_status::pending_external) {
  const auto callback_id = result.pending_callback_ids().front();
  runner.send_callback_success(callback_id, R"({"approved":true})");
  result = runner.resume();
}

assert(result.status() == durable::local_run_status::succeeded);
assert(result.step("validate-order") != nullptr);

See docs/local-runner.md. Reference fixtures and conformance coverage are documented in docs/conformance.md. Instrumentation plugins are documented in docs/plugins.md. Third-party durable primitives are documented in docs/custom-operations.md.

See docs/operations.md for replay and result semantics. See docs/concurrency.md for scheduling, batching, and early-completion behavior.

For a raw Lambda runtime payload, run_json performs tolerant wire decoding, runs the durable handler, and serializes the invocation response:

auto response = durable::run_json(request.payload, service, durable_handler);

AWS integration

The core remains dependency-free. Enable either optional integration explicitly:

cmake -S . -B build \
  -DDURABLE_EXECUTION_BUILD_AWS_SDK_ADAPTER=ON \
  -DDURABLE_EXECUTION_BUILD_LAMBDA_RUNTIME_ADAPTER=ON

The AWS SDK adapter uses CheckpointDurableExecution and GetDurableExecutionState. Applications retain responsibility for Aws::InitAPI/Aws::ShutdownAPI and the lifetime/configuration of the Lambda client.

auto lambda_client = std::make_shared<Aws::Lambda::LambdaClient>();
durable::aws_sdk_service_client service{lambda_client};
auto handler = durable::make_lambda_handler(service, durable_function);
aws::lambda_runtime::run_handler(handler);

See examples/lambda_main.cpp and docs/aws-integration.md.

Build and test

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure

Enable the self-contained microbenchmarks with:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
  -DDURABLE_EXECUTION_BUILD_BENCHMARKS=ON
cmake --build build --parallel
./build/durable_execution_microbench

On the current development host, the initial core measures roughly 370 ns per operation-ID generation, 20 ns per integer serialize / deserialize round trip, 800 ns to decode a minimal invocation containing one operation, and 34 ns for a thread-safe history lookup. These figures are development baselines, not portable performance guarantees.

Install and consume

cmake --install build --prefix /your/prefix
find_package(aws_durable_execution 0.1 CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE aws::durable_execution)

Compatibility

The compatibility contract covers source, ABI, durable history, wire protocol, and serialized payloads. Unknown wire enum values are retained without allocation for known values, and unsafe unknown operation states fail closed instead of accidentally rerunning user code.

See docs/compatibility.md for the policy and docs/architecture.md for the layer boundaries and performance constraints.

Roadmap

  1. Run all 171 mapped requirements against deployed AWS Lambda durable functions.
  2. Retain replay fixtures from every released minor version and verify them in CI as the SDK evolves.
  3. Stable 1.0 API/ABI after every required conformance, compatibility, and performance gate passes.

License

Apache License 2.0. See LICENSE and NOTICE.

About

Lambda Durable Execution SDK in C++

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages