[][src]Crate conjure_runtime

An HTTP client for use with Conjure servers.

This crate provides both asynchronous and blocking clients. The clients can either be used in Conjure-generated wrapper types or standalone for use with non-Conjure servers.

Configuration

While a conjure_runtime client's configuration can be built up programmatically, the more common approach is for the configuration to be deserialized from a service's runtime-reloadable configuration file. The ServicesConfig supports configuration for multiple downstream services, as well as allowing for both global and per-service configuration overrides:

services:
  auth-service:
    uris:
      - https://auth.my-network.com/auth-service
  cache-service:
    uris:
      - https://cache-1.my-network.com/cache-service
      - https://cache-2.my-network.com/cache-service
    request-timeout: 10s
# options set at this level will apply as defaults to all configured services
security:
  ca-file: var/security/ca.pem

Once a client is constructed, its configuration can be dynamically reloaded by calling RefreshHandle::refresh on a handle obtained via Client::refresh_handle. Note that the refresh process will throw away the client's TLS session cache and HTTP connection pool, so it should not be called needlessly often.

Usage

First construct a raw conjure_runtime::Client:

use conjure_runtime::{UserAgent, Agent, HostMetricsRegistry, Client};
use conjure_runtime::config::{ServiceConfig, SecurityConfig};
use std::path::PathBuf;
use std::sync::Arc;
use witchcraft_metrics::MetricRegistry;

let user_agent = UserAgent::new(Agent::new("my-user-agent", "1.0.0"));
let host_metrics = Arc::new(HostMetricsRegistry::new());
let metrics = Arc::new(MetricRegistry::new());
let config = ServiceConfig::builder()
    .uris(vec!["https://url-to-server:1234/test-service".parse().unwrap()])
    .security(
        SecurityConfig::builder()
            .ca_file(Some(PathBuf::from("path/to/ca_file.pem")))
            .build(),
    )
    .build();

let client = Client::new(
    "test-service",
    user_agent,
    &host_metrics,
    &metrics,
    &config,
)?;

The client can then be used with Conjure-generated service interfaces:

use conjure_codegen::example_types::another::TestServiceAsyncClient;
use conjure_object::BearerToken;

let client = TestServiceAsyncClient::new(client);

let auth = BearerToken::new("my_auth_token").unwrap();
let file_systems = client.get_file_systems(&auth).await?;

Or manually if a service does not provide a Conjure API:

let response = client.delete("/widgets/{widgetId}")
    .param("widgetId", 12345)
    .send()
    .await?;

The blocking::Client's API is identical, with the exception that you don't .await on methods:

use conjure_codegen::example_types::another::TestServiceClient;
use conjure_object::BearerToken;

let client = TestServiceClient::new(client);

let auth = BearerToken::new("my_auth_token").unwrap();
let file_systems = client.get_file_systems(&auth)?;
let response = client.delete("/widgets/{widgetId}")
    .param("widgetId", 12345)
    .send()?;

Behavior

conjure_runtime wraps the hyper HTTP library with opinionated behavior designed to more effectively communicate between services in a distributed system. It is broadly designed to align with the conjure-java-runtime Java library, though it does differ in various ways.

Error Propagation

Servers should use the standard Conjure error format to propagate application-specific errors to callers. Non-QoS (see below) errors received from the server are treated as fatal. By default, conjure_runtime will return a conjure_error::Error that will generate a generic 500 Internal Server Error response. Its cause will be a RemoteError object that contains the serialized Conjure error information sent by the server. The Client::set_propagate_service_errors method can be used to change that behavior to instead transparently propagate the error received from the server. Rather than producing a generic 500 response, the returned conjure_error::Error will produce the same response the client received from the server.

Call Tracing

The client propagates trace information via the zipkin crate using the traditional X-B3-* HTTP headers. It also creates local spans covering various stages of request processing:

  • conjure-runtime: get /widget-service/{widgetId} - The name of this span is built from the request's method and path pattern.
    • conjure-runtime: attempt 1
      • conjure-runtime: wait-for-headers - This span is sent to the server, and lasts until the server sends the headers of the response.
      • conjure-runtime: wait-for-body - This span is tracked along with the response body, and lasts until the ResponseBody object is dropped. It is "detached" from the zipkin tracer so new spans created outside of conjure-runtime will not be parented to it, and can outlive the parent conjure-runtime spans. It will not be created if an IO error occurs before headers are received.
    • conjure-runtime: backoff-with-jitter - If the request encounters an IO error before receiving headers, this span covers the time spent waiting before the request is retried.
    • conjure-runtime: attempt 2
      • ...

Quality of Service: Retry, Failover, Throttling, and Backpressure

The client treats certain HTTP errors specially. Servers can advertise an overloaded state via the 429 Too Many Requests or 503 Service Unavailable status codes. Unlike other 4xx and 5xx status codes, these responses do not cause the request to fail. Instead, conjure_runtime will throttle itself and retry the request. Requests are retried a fixed number of times, with an exponentially growing backoff in between attempts. If a 429 response contains a Retry-After header, its backoff will be used rather than the default. IO errors also trigger a retry.

A 503 response or IO error will also cause that host to be temporarily "blacklisted" so it will not be used by other requests unless there is no other option.

Only some requests can be retried. By default, conjure_runtime will only retry requests with HTTP methods identified as idempotent - GET, PUT, DELETE, HEAD, TRACE, and OPTIONS. Non-idempotent requests cannot be safely retried to avoid the risk of unexpected behavior if the request ends up being applied twice. The Client::set_assume_idempotent method can be used to override this behavior and have the client assume all requests are idempotent. In addition, requests with streaming request bodies can only be retried if the body had either not started to be written when the error occurred or if it was successfully reset for another attempt.

Metrics

Clients record metrics to both a standard MetricsRegistry and a conjure_runtime-specific HostMetricsRegistry.

Standard Metrics

  • client.request (service: <service_name>) - A Timer recording request durations, tagged by service. Note that the requests timed by this metric are the individual raw HTTP request attempts, not conjure_runtime-level requests with retries/backoffs/etc. It only records the time until response headers are received, not until the entire response body is read.
  • client.request.error (service: <service_name>, reason: IOException) - A Meter tracking the rate of IO errors, tagged by service. Like the client.request metric, this tracks the raw HTTP requests. The reason tag has a value of IOException to align with conjure-java-runtime's metric.

Host Metrics

The HostMetricsRegistry contains metrics for every host of every service being actively used by a conjure_runtime client.

Modules

blocking

A blocking version of the client.

config

Client configuration.

errors

Error types.

Structs

Agent

A component of a UserAgent.

BodyWriter

The asynchronous writer passed to Body::write_body.

BytesBody

A simple type implementing Body which consists of a byte buffer and a content type.

Client

An asynchronous HTTP client to a remote service.

HostMetrics

Metrics about requests made to a specific host of a service.

HostMetricsRegistry

A collection of metrics about requests to service hosts.

Hosts

A snapshot of the nodes in a registry.

HostsIter

An iterator over host metrics.

RefreshHandle

A handle used to update the configuration of a Client.

RequestBuilder

A builder for an asynchronous HTTP request.

Response

An asynchronous HTTP response.

ResponseBody

An asynchronous streaming response body.

UserAgent

A representation of an HTTP User-Agent header value.

Traits

Body

A request body.