Rust · Calling

Calling

use ucall::Client;use ucall::ClientCall;use ucall::ClientCallOptions;use ucall::ClientOptions;use ucall::Completed;use ucall::Response; 1.0.0-rc.1 · 60202332

A Client submits calls without driving the network, and polls to drive them and collect what finished. It holds up to connections connections, reusing them across calls, and carries up to pipelined_calls calls on each.

use std::time::Duration;
use ucall::*;

let mut client = Client::new(ClientOptions { connections: 4, pipelined_calls: 16, ..Default::default() })?;
for index in 0..64u64 {
    client.submit(ClientCall::post("http://127.0.0.1:8545/infer", vec![index as u8]), index)?;
}
let mut completed: [Completed; 16] = Default::default();
let mut remaining = 64;
while remaining != 0 {
    for done in client.take_completed(&mut completed, Duration::from_secs(1))? {
        match &done.response {
            Ok(response) => println!("call {} answered {}", done.context, response.status_code().0),
            Err(error) => println!("call {} failed: {error}", done.context),
        }
        remaining -= 1;
    }
}

let response = client.get("http://127.0.0.1:8545/health")?;
let keep = response.header("cache-control").map(<[u8]>::to_vec);
drop(client);
println!("{:?} {:?}", response.body(), keep);

submit takes a ClientCall, which owns its URL, header lines, body and security until the call completes, and tags it with a u64 context. A call admitted once completes exactly once, and a client already holding calls unretrieved calls refuses more with Status::Contended. take_completed drives every admitted call and fills a caller-provided buffer with finished ones, waiting up to its timeout for the first, and a zero timeout performs one nonblocking round. get, post, put, delete, patch and call submit one call and poll until it finishes, keeping the completions of other calls they retrieve meanwhile for the next take_completed, which hands those out first. ClientCall::options carries the call’s own ClientCallOptions: a timeout in place of the client’s, retries, redirect hops, security, and the client and server keys.

ClientOptions::max_response_bytes caps what a call buffers, failing it with Status::Overflow past the cap, and ClientOptions::log reports dials that fail every address, refused handshakes and refused redirects. Host names resolve through the library’s own stub resolver, driven by the client’s polling, and connections race the answered addresses as Happy Eyeballs does.

Every received response is a Response, whatever its HTTP status, and only a failed exchange is an Error; Response::fields iterates its header fields as a request’s does. A Response owns its received bytes and is Send, so it stays readable after its Client is dropped and may move to another thread. Dropping the client cancels the calls still in flight and discards completions not yet retrieved.

API

StructClientAn HTTP client, created, submitted to, polled, and dropped on one thread.9
client.rs:331 ↗
pub struct Client

Allocations happen at construction, sized by the resolved call budget, and in header arrays that grow to their largest call once; polling allocates nothing. Dropping it cancels the calls in flight, while retrieved Responses stay readable.

use std::time::Duration;
use ucall::*;

let mut client = Client::new(ClientOptions { connections: 2, pipelined_calls: 4, ..Default::default() })?;
for index in 0..8u64 {
    client.submit(ClientCall::post(format!("http://{address}/echo"), vec![index as u8]), index)?;
}
let mut completed: [Completed; 8] = Default::default();
let mut answered = 0;
while answered < 8 {
    for done in client.take_completed(&mut completed, Duration::from_secs(1))? {
        assert_eq!(done.response.as_ref().map(Response::body), Ok([done.context as u8].as_slice()));
        answered += 1;
    }
}

let response = client.post(format!("http://{address}/echo"), b"blocking".as_slice())?;
drop(client);
assert_eq!(response.body(), b"blocking");
newStarts a client; it holds no connection until the first call is polled.
client.rs:346 ↗
pub fn new(options: ClientOptions<'_>) -> Result<Client>
submitAdmits request without driving the network; its completion carries context.
client.rs:455 ↗
pub fn submit(&mut self, request: ClientCall, context: u64) -> Result<()>

Success guarantees exactly one completion from Client::take_completed; a full client refuses with Status::Contended until completions are retrieved.

take_completedDrives every admitted call on this thread and fills the front of completed with finished ones, waiting up to timeout for the first; zero performs one nonblocking round.
client.rs:503 ↗
pub fn take_completed<'completed>(
    &mut self,
    completed: &'completed mut [Completed],
    timeout: Duration,
) -> Result<&'completed mut [Completed]>

Answers the filled prefix, empty when nothing finished in time. Completions kept by Client::call, Client::get or Client::post come first, without waiting.

callSubmits request and polls until it finishes, keeping other calls' completions for the next Client::take_completed.
client.rs:533 ↗
pub fn call(&mut self, request: ClientCall) -> Result<Response>

The response arrives whatever its HTTP status; a failed exchange answers its Error.

getFetches url and polls until the response arrives.
client.rs:553 ↗
pub fn get(&mut self, url: impl Into<Cow<'static, str>>) -> Result<Response>
postPosts body to url and polls until the response arrives.
client.rs:558 ↗
pub fn post(
    &mut self,
    url: impl Into<Cow<'static, str>>,
    body: impl Into<Cow<'static, [u8]>>,
) -> Result<Response>
putPuts body at url and polls until the response arrives.
client.rs:567 ↗
pub fn put(
    &mut self,
    url: impl Into<Cow<'static, str>>,
    body: impl Into<Cow<'static, [u8]>>,
) -> Result<Response>
deleteDeletes url and polls until the response arrives.
client.rs:576 ↗
pub fn delete(&mut self, url: impl Into<Cow<'static, str>>) -> Result<Response>
patchPatches url with body and polls until the response arrives.
client.rs:581 ↗
pub fn patch(
    &mut self,
    url: impl Into<Cow<'static, str>>,
    body: impl Into<Cow<'static, [u8]>>,
) -> Result<Response>
StructClientCallOne HTTP call, owning everything the library reads until the call completes.5
client.rs:204 ↗
pub struct ClientCall

Fields

verbVerbThe HTTP method.
urlCow<'static, str>The full URL, http:// or http+noise://.
headersVec<Cow<'static, [u8]>>Full header lines like "accept: text/plain"; Host, Content-Length and User-Agent are generated.
bodyCow<'static, [u8]>The request body; empty sends no payload.
optionsClientCallOptionsThe call's own time budget, retries, redirects and security.

Implements

Clone, Debug
getA GET of url with no headers.
client.rs:220 ↗
pub fn get(url: impl Into<Cow<'static, str>>) -> Self
postA POST of body to url with no headers.
client.rs:231 ↗
pub fn post(url: impl Into<Cow<'static, str>>, body: impl Into<Cow<'static, [u8]>>) -> Self
putA PUT of body to url with no headers.
client.rs:240 ↗
pub fn put(url: impl Into<Cow<'static, str>>, body: impl Into<Cow<'static, [u8]>>) -> Self
deleteA DELETE of url with no headers.
client.rs:248 ↗
pub fn delete(url: impl Into<Cow<'static, str>>) -> Self
patchA PATCH of body to url with no headers.
client.rs:256 ↗
pub fn patch(url: impl Into<Cow<'static, str>>, body: impl Into<Cow<'static, [u8]>>) -> Self
StructClientCallOptionsOne call's time budget, retries, redirects and security, each zero field taking its default.
client.rs:187 ↗
pub struct ClientCallOptions

Fields

timeoutDurationTime budget including queueing and redirects; zero takes the client's call_timeout.
max_retriesusizeConnection attempts made again before sending the request; zero means 3.
max_redirectsusizeRedirect hops followed before failing; zero means 5.
securitySecurityThe transport security; an http+noise:// URL implies Noise XX.
noise_private_keyOption<NoiseKey>The private key this call authenticates with in place of the client's own.
noise_peer_keyOption<NoiseKey>The server's static key, which Noise IK and KK need in advance.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Default
StructClientOptionsWhat a client may hold open at once, how long it waits, and who it is under Noise.
client.rs:152 ↗
pub struct ClientOptions<'server>

connections is required; every other zero field takes its documented default.

Fields

connectionsusizeConnections open at once; zero fails with Status::InvalidArgument.
pipelined_callsusizeCalls one connection may carry at once; zero means one.
callsusizeCalls admitted at once, unretrieved completions included; zero means connections times pipelined_calls.
connect_timeoutDurationTime budget for establishing a connection; zero means 5 seconds.
call_timeoutDurationTime budget for a full exchange, including queueing and redirects; zero means 30 seconds.
noise_private_keyOption<NoiseKey>The client identity for Noise handshakes.
peer_authorizerOption<Arc<PeerAuthorizer>>Vets each server's authenticated static key; required for Noise XX calls.
backendBackendThe requested I/O backend.
cpu_classOption<CPUClass>The CPU class the client confines itself to; None takes what the CPU offers.
logOption<Log>Where log lines go; None keeps the client silent.
max_response_bytesusizeThe largest response a call buffers, past which it fails with Status::Overflow; zero takes the largest receive buffer the kernel grants a socket.
serverOption<&'server Server>A server whose event loop drives this client through that loop's polling, sharing its engine; bound before the loop first polls, used only on its polling thread, and its Client::take_completed only hands out what the loop finished.
event_loopusizeWhich of server's event loops; ignored without server.

Implements

Default
StructCompletedOne finished call: the context it was submitted with, and its response or failure.
client.rs:591 ↗
pub struct Completed

Fields

contextu64The context passed to Client::submit.
responseResult<Response>The received response, whatever its HTTP status, or why none arrived.

Implements

Default
StructResponseA received HTTP response, freed on drop.4
client.rs:644 ↗
pub struct Response

It owns its received bytes, so it stays readable after its Client is dropped and may move to another thread.

use ucall::*;

let mut client = Client::new(ClientOptions { connections: 1, ..Default::default() })?;
let response = client.get(format!("http://{address}/missing"))?;
drop(client);
let reader = std::thread::spawn(move || {
    assert_eq!(response.status_code(), HTTPStatus::NOT_FOUND);
    assert_eq!(response.header("X-Reason"), Some(b"gone".as_slice()));
    assert_eq!(response.body(), b"no such page");
});
reader.join().unwrap();

Implements

Send, Drop
status_codeThe status code the server sent.
client.rs:653 ↗
pub fn status_code(&self) -> HTTPStatus
bodyThe response body, decoded from chunked framing if it arrived that way.
client.rs:658 ↗
pub fn body(&self) -> &[u8]
fieldsEvery header field line in arrival order, repeated names apart: each name lowercased as the parser keeps it, and its value trimmed.
client.rs:686 ↗
pub fn fields(&self) -> impl Iterator<Item = (&[u8], &[u8])> + '_