Skip to main content

Schema Registry

SchemaRegistry is the Confluent Schema Registry client: it sends nothing until the first lookup, and its clones share one cache.

SchemaRegistry::new records the base URL, so the app starts with no registry reachable. basic_auth and bearer_token set the credentials of every request, and HTTPS goes through rustls. request_timeout bounds each request, ten seconds by default, so a registry that accepts the connection and goes silent fails the lookup with KafkaError::SchemaRegistryTimeout. The client is configured before it is handed out, because clones keep the settings they were made from.

Every clone reads and writes one cache, so a schema or a subject is fetched once per process. schema_by_id answers from the cache or asks the registry once; cached_schema and cached_subject read the cache alone. A RegisteredSchema carries the id, the SchemaType (Avro, Protobuf or JSON) and the definition text.

A registry-backed payload carries the Confluent envelope: a zero byte, the schema id, then the datum. A broker meets it in one of two ways. The codecs, AvroCodec and SchemaFramed, hold the format where a serializer belongs and take their schemas through a SchemaPrefetch. The transcode converts at the broker's edges and leaves the handlers on plain JSON. The example holds the client as application state, and a handler asks it which schema a record was written with.

Example​

/// A record as it arrived, envelope included.
#[derive(Deserialized)]
struct Framed<'a>(&'a [u8]);

#[derive(FromRef)]
struct Audit {
registry: SchemaRegistry,
}

#[subscriber("orders")]
async fn audit(record: &Framed<'_>, State(registry): State<SchemaRegistry>) -> HandlerOutcome {
let Some((id, _)) = parse_envelope(record.0) else {
tracing::info!(topic = "orders", "a record without a schema");
return HandlerOutcome::ack();
};
// The first lookup of an id reaches the registry; every later one reads the cache.
match registry.schema_by_id(id).await {
Ok(schema) => {
tracing::info!(schema_id = id, schema_type = ?schema.schema_type(), "written with");
HandlerOutcome::ack()
}
Err(error) => {
tracing::warn!(topic = "orders", schema_id = id, %error, "schema lookup failed");
HandlerOutcome::retry()
}
}
}

fn registry(url: &str, password: &str) -> SchemaRegistry {
// Records the address and the settings; no request is sent until the first lookup.
SchemaRegistry::new(url)
.basic_auth("audit-svc", password)
.request_timeout(Duration::from_secs(2))
}

fn app(registry: SchemaRegistry) -> impl App {
let broker = KafkaBroker::new(["localhost:9092"]).default_group("audit-svc");
RustStream::new(AppInfo::new("audit", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Audit { registry }))
.with_broker(broker, |b| {
b.include(audit);
})
}

#[ruststream::app]
fn service() -> impl App {
let password = std::env::var("REGISTRY_PASSWORD").unwrap_or_default();
app(registry("http://localhost:8081", &password))
}