Skip to content

Add blocking (non-async) API wrapping the async API [prototype] - #837

Open
John Batty (johnbatty) wants to merge 1 commit into
microsoft:mainfrom
johnbatty:sync-tokio-wrapper
Open

John Batty (johnbatty) wants to merge 1 commit into
microsoft:mainfrom
johnbatty:sync-tokio-wrapper

Conversation

@johnbatty

Copy link
Copy Markdown
Collaborator

Summary

Adds a blocking (non-async) API that wraps the existing async API, as requested in #835. Each request runs on a hidden tokio runtime, so the blocking
API supports everything the async API does: all credential types (PAT, OAuth/TokenCredential, Azure CLI), retry options, and pipeline policies.

It's opt-in (the blocking feature) and additive: existing async code compiles unchanged, and default builds are unaffected.

use azure_devops_rust_api::git;

let git_client = git::blocking::ClientBuilder::new(credential).build();

let repos = git_client
    .repositories_client()
    .list(organization, project)
    .send()?          // blocking; no `.await` available
    .into_body()?
    .value;
azure_devops_rust_api = { version = "...", features = ["git", "blocking"] }

Addresses #835.

Design

  • Blocking clients wrap the async clients. Each module gets a generated blocking submodule containing Client, ClientBuilder and a client per
    operation group.
    • blocking::ClientBuilder wraps the async ClientBuilder and supports all of its options: endpoint, scopes, retry, transport, per-call/per-try
      policies.
    • blocking::Client::from(async_client) converts an existing async client.
  • Request builders have a send mode type parameter: RequestBuilder<M = ()>, held as a zero-sized PhantomData<M>.
    • (), the default, is async. Code that names list::RequestBuilder is unaffected, and .await / IntoFuture work as before.
    • blocking::Blocking is for builders created by a blocking client. These have only a blocking send(), so the compiler rejects any attempt to
      .await a blocking request.
    • Setters are shared by both modes.
  • Async and blocking share one code path. The existing async send body is now a private send_future(). The async send() returns it, and the
    blocking send() runs it on the hidden runtime. Requests are identical either way.
  • Hidden runtime (src/blocking.rs): a single tokio runtime shared by all blocking clients, created on first use, with one worker thread so
    connections keep being serviced between requests. It works when blocking clients are used from several threads at once.
  • Safety: calling a blocking send() from within an async runtime returns an error instead of panicking.

Limitations

  • The blocking API can't be used from within an async runtime; it returns an error. Use the async API there.
  • tokio is always a dependency. That was already the case: the default async transport uses it.
  • artifacts_download is async-only.
  • Not available on wasm32.

Breaking changes

  • Operation RequestBuilder types have a new mode type parameter. It defaults to async, so code that names the type is unaffected.

Code generator changes (autorust)

  • RunConfig.generate_blocking (set in ado-autorust/src/main.rs) turns on blocking client generation.
  • Generation fails with a clear error if an operation isn't in an operation group, or is pageable or long-running. None are today, across all 40
    modules.
  • Blocking client functions keep each operation's description and link to the async function for argument details, instead of repeating them.
  • Operations with no optional parameters don't get an empty impl block.

Cost

Measured on azure_devops_rust_api with --all-features:

Before After
Generated source 7.39 MB 9.61 MB
cargo check ~16.4s ~14.9s
Debug build ~18s ~20.6s
Release build ~116s ~130s

The cargo check difference is within run-to-run noise. The larger release-build increase is probably because each operation's async code is compiled
for both modes; this hasn't been confirmed. Builds without the blocking feature don't compile the blocking clients.

The blocking feature adds no crates to the dependency tree (135 with git either way); it only enables tokio's rt-multi-thread feature.

Other changes

  • Examples, both using the shared credential helper, so the Azure CLI works as well as a PAT:
    • git_repo_list_blocking
    • wit_work_item_create_blocking (a JSON Patch body)
  • tests/blocking.rs: tests against a local mock HTTP server:
    • GET: request line, Basic auth, and the User-Agent set by the azure_core pipeline
    • POST with a JSON body
    • a 404 becomes an HttpResponse error
    • a TokenCredential produces a Bearer header
    • converting an existing async client with From
    • concurrent requests from 4 threads
    • calling from inside an async runtime returns an error
    • the async path is unchanged
  • CI: the test step now runs cargo test --all-features, so the new tests run.
  • Docs: new README section "Blocking (non-async) API", CHANGELOG entry, CLAUDE.md and SKILL.md updates.

Testing

  • ./build.sh reproduces the committed generated code.
  • cargo fmt --check and clippy with --deny warnings pass: all features (library and examples), git, git,blocking and core,blocking.
  • cargo test passes with default features and with --all-features.
  • cargo doc --all-features passes, with no new warnings.
  • The examples have not been run against a live Azure DevOps organization.

🤖 Generated with Claude Code

Each module now has a generated `blocking` submodule (behind the new
`blocking` feature) with a Client and ClientBuilder that wrap the async
client, running each request on a hidden tokio runtime. All credential
types, retry options and pipeline policies are supported. Using the
blocking API from within an async runtime returns an error.

Request builders now have a send mode type parameter (defaulting to
async), which selects between the async and blocking send().

See microsoft#835.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnbatty John Batty (johnbatty) changed the title Add blocking (non-async) API wrapping the async API Add blocking (non-async) API wrapping the async API [prototype] Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant