Skip to content

[DOCS-39462]-Add a Schema Registry producer example - #5565

Merged
Jamie Gillenwater (jgillenwater) merged 15 commits into
masterfrom
DOCS-39462
Oct 2, 2026
Merged

Jamie Gillenwater (jgillenwater) merged 15 commits into
masterfrom
DOCS-39462

Conversation

@jgillenwater

Copy link
Copy Markdown
Member

Summary

  • Add examples/producer_schema_registry.c, a producer that registers an Avro schema in Schema Registry and produces a schema-framed message using libserdes and avro-c.
  • Add an opt-in producer_schema_registry Makefile target. It is deliberately not in $(EXAMPLES), so plain make still works for contributors without libserdes and avro-c installed — same precedent as producer_cpp and rdkafka_consume_batch.
  • Point at the new example from examples/README.md and from the "Usage in code" section of the top-level README.md, noting that librdkafka has no built-in Schema Registry serializer.

Addresses DOCS-39462: the sibling clients all now lead with a Schema Registry producer. librdkafka's README carries no inline code example, so instead of swapping a first example this adds the missing Schema Registry example and links to it. This is one of five coordinated PRs across librdkafka, confluent-kafka-python, confluent-kafka-go, confluent-kafka-dotnet, and confluent-kafka-javascript.

Verification

  • Verified by an SME: compiled against librdkafka + avro-c + libserdes built from source in a container, and produced a real message to a local broker.
  • RD_KAFKA_PRODUCER (not RD_KAFKA_TYPE_PRODUCER) confirmed against src/rdkafka.h.
  • Not compiled on this machine — libserdes and avro-c are not installed locally.

@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

librdkafka has no built-in Schema Registry serializer, so schema-first
production in C is done with libserdes plus avro-c. Add an example
showing that path, and point to it from the README, so C/C++ readers
are not left with raw-bytes producers as the only reference.

libserdes and avro-c are not librdkafka dependencies, so the example is
deliberately left out of $(EXAMPLES) and built on request with
'make producer_schema_registry', matching how producer_cpp and
rdkafka_consume_batch are already handled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reformat producer_schema_registry.c with clang-format 18 so it passes the
repo style check, and derive the Schema Registry subject from the `topic`
variable with snprintf() instead of hardcoding "myTopic-value", so the two
cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
serdes_conf_new() returns NULL if a configuration property is rejected.
Passing that NULL to serdes_new() does not crash: serdes_new() treats a
NULL conf as "use defaults" and initializes an empty configuration with
no Schema Registry URL, so the mistake only surfaces later as an opaque
serdes_schema_add() failure.

The example also passes (NULL, 0) for errstr, which suppresses the
explanation entirely. Rather than add error handling that would clutter
the example's main line, call out both points in a comment so readers
carrying this into production know to pass a real buffer and check the
result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rayokota Robert Yokota (rayokota) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libserdes is deprecated. We should have an example using libschemaregistry instead. See https://github.com/confluentinc/libschemaregistry

* Bump bundled OpenSSL to 3.5.8 and libcurl to 8.22.0 for source/autoconf builds

* TEMP: drop tag gate on x64/arm64 release-artifact builds to verify dep bump in CI

* Revert "TEMP: drop tag gate on x64/arm64 release-artifact builds to verify dep bump in CI"

This reverts commit bd573d7.

* Update Changelog
…nce starts (#5600)

* Tests for KIP-899 and KIP-1102 re-bootstrap cases

* Test that a KIP-1102 re-bootstrap actually reaches a bootstrap broker

The existing KIP-1102 cases only count "Starting re-bootstrap sequence"
log lines. That line is emitted at the top of
`rd_kafka_rebootstrap_tmr_cb()`, before any broker is added or connected,
so it can't distinguish a working re-bootstrap from an inert one that
starts and then keeps querying the very brokers that reported the
client's metadata as stale.

It also can't be distinguished by address: the harness passes every mock
broker in `bootstrap.servers` and all of them are in Metadata, so
"asked a bootstrap broker" and "asked a learned broker" are the same
host:port on the wire.

Add a 6th broker to the `REBOOTSTRAP_REQUIRED` variations and remove it
from Metadata responses with
`rd_kafka_mock_broker_remove_from_metadata()`. It keeps listening and
stays in `bootstrap.servers`, so it is only ever reachable through a
bootstrap (`RD_KAFKA_CONFIGURED`) broker object, never as a learned one.
A Metadata request arriving there is therefore proof that the sequence
resulted in a bootstrap broker being used. Replica assignment already
skips brokers that aren't in metadata and clamps the replication factor
to the eligible count, so the expected learned broker set is unchanged.

Only the error-code variations get the assertion. The ERR__TRANSPORT
ones are left at 5 brokers and untouched: there the mock closes the
connections, so the client reaches a bootstrap broker through the
"all brokers down" path regardless, and the assertion wouldn't be
exercising the KIP-1102 path.

Request tracking is started at the first action rather than at
configuration time, since `rd_kafka_mock_start_request_tracking()`
clears the request list and this discards the initial bootstrap
requests, which may legitimately have gone to the bootstrap-only
broker. The count is sampled from the after-action callback because the
harness destroys the mock cluster before returning to the test.

Both error-code variations currently fail, reporting 70 re-bootstrap
sequences and 0 Metadata requests to the bootstrap-only broker. That is
the defect being pinned down, not a test bug: with learned brokers up,
`rd_kafka_broker_any_usable()` keeps returning one of them (weight_usable()
gives RD_KAFKA_LEARNED a +1000 bonus), so `rd_kafka_connect_any()` is
never reached and the re-added bootstrap brokers are never connected.

* Fix KIP-1102 re-bootstrap never reaching a bootstrap broker

Decommission learned brokers when a re-bootstrap sequence starts so that
only the bootstrap servers are used until a Metadata response rebuilds the
broker list. Previously the client kept asking the very (still connected)
learned brokers that had reported its metadata as stale, since
rd_kafka_broker_any_usable() weighs learned brokers higher and always
found one usable, so a bootstrap broker was never actually reached.

Queued messages on decommissioned brokers are handed back to their
partitions and re-sent once new leaders are known from the next Metadata
response. Admin requests in flight on a decommissioned broker now fail
with RD_KAFKA_RESP_ERR__TRANSPORT (previously
RD_KAFKA_RESP_ERR__DESTROY_BROKER) so callers retry instead of treating
it as a hard failure.

* Fix spurious re-bootstrap and mock cgrp crash from broker decommission

rd_kafka_broker_decommission() forced its broker down without marking
the failure as planned, so decommissioning several still-up learned
brokers at once (as re-bootstrap now does) could itself cross the "all
brokers down" threshold and spuriously start a second re-bootstrap
sequence. Use rd_kafka_broker_planned_fail() so a decommission is never
counted toward ALL_BROKERS_DOWN.

Separately, decommissioning the current group coordinator immediately
marks it dead and triggers a JoinGroup/SyncGroup retry on a new
connection, which can arrive at the mock broker before it has processed
the old connection's closure. The mock's per-member response tracking
only tolerated one live request per member, asserting on the retry;
discard the stale, now-unreachable response instead.

* Change the 0151 expectations according to the new policy of removing learned brokers when re-bootstrapping

* Bound the 0151 re-bootstrap count by the connect interval, not a measured count

do_test_down_then_up_no_rebootstrap_loop asserted 7-12 re-bootstrap
sequences, which failed branch-wide on the arm64 local-quick CI jobs
with 20 or 21. The count is not a property of the configuration, so no
observed range is stable.

The subtest's premise was wrong: the attempts are not paced by
`reconnect.backoff.ms`. A connect scheduled through
rd_kafka_connect_any() ends up in RD_KAFKA_OP_CONNECT, which zeroes
rkb_ts_reconnect to expedite the reconnect, so rd_kafka_broker_
reconnect_backoff() returns 0 and the backoff never ramps. The rate
instead follows how many subsystems ask for a cluster connection while
every broker is down: the sparse-connection keepalive in
rd_kafka_1s_tmr_cb(), its deferred one-shot retry, and - the piece that
only shows up in CI - ProducerID acquisition, since run_local_quick
passes -P and enables idempotence. That third caller takes the rate
from ~1/s to ~3/s, giving the 20-21 seen on CI. It reproduces anywhere
with 'TESTS=0151 SUBTESTS=down_then_up_no_rebootstrap_loop
./test-runner -P', which yields 9 or 21 roughly half the time each.

Assert the one bound that is actually guaranteed: sparse_connect_intvl,
max(11, min(reconnect.backoff.ms / 2, 1000)) and so 50ms by default,
gates rd_kafka_connect_any(), allowing at most 6000 / 50 = 120
sequences in the 6s window. A loop not gated by connection attempts
exceeds that, so the assertion still catches what the subtest is for.

Also correct the comment, which described the old pacing theory.

* Flaky tests 0118 fix and revert 0151 min count

* Test 0151: await a request is sent to the bootstrap-only broker

* Avoid adding back the coordinator as learned broker when the response comes from a broker that is being decommissioned

* Changelog entry

* Upgrade considerations

* Report ALL_BROKERS_DOWN at most once per reconnect.backoff.max.ms instead of on every re-bootstrap cycle

With metadata.recovery.strategy=rebootstrap, the re-bootstrap timer
callback resets every broker's rkb_down_reported flag so that all
brokers are retried before the "all brokers down" condition can be
declared again. During a sustained outage every retry fails, the
threshold is crossed again on each cycle and ALL_BROKERS_DOWN was
delivered to the application roughly once per reconnect interval for
the whole duration of the outage.

Track the time of the last report in rk_last_all_brokers_down_reported_ts
and report again only when more than reconnect.backoff.max.ms has passed
since it, so a long outage still surfaces periodically but not on every
cycle. Reset the timestamp when any broker connection comes up, so a new
outage is reported immediately regardless of how recently the previous
report was made. The timestamp is compared and updated with a CAS, since
the last broker down can be observed concurrently from several broker
threads. The re-bootstrap itself is unchanged and still runs on every
cycle.

Test 0151 do_test_down_then_up_no_rebootstrap_loop now takes the outage
duration, polls the handle while the broker is down so errors reach the
error callback, and asserts that a 5s and a 15s outage deliver exactly
outage / reconnect.backoff.max.ms + 1 errors (1 and 2) over the many
re-bootstrap sequences they produce.

Fixes #5546

* Don't skip FindCoordinator or Metadata responses from bootstrap brokers that are decommissioning. Fixes failure in 0143_exponential_backoff_mock

* Additional changelog entry

* test 0151: use atomics to prevent thread sync issues
* Don't commit offsets of a lost assignment

A consumer with enable.auto.commit=true could send an OffsetCommit for
an assignment it had already lost. On a client-side session timeout, or
when max.poll.interval.ms is exceeded, the member id is reset to empty,
the assignment is marked lost, and its partitions are revoked. The
revoke-time auto-commit of those partitions was still sent because the
assignment-lost flag was cleared inside rd_kafka_cgrp_unassign() and
rd_kafka_cgrp_incremental_unassign(), before the removed partitions were
served, defeating the guard that skips commits for a lost assignment.

The commit left with an empty member id and the previous generation. A
broker rejects it with UNKNOWN_MEMBER_ID, or, for a static member
(group.instance.id set), with FENCED_INSTANCE_ID, which librdkafka
raises as a fatal error that stops the consumer.

Keep the assignment-lost flag set until the following rejoin applies a
new assignment (assign()/incremental_assign() still clear it), so the
offsets of a lost assignment are never committed. This restores the
behavior documented in rd_kafka_cgrp_incremental_unassign()'s own
comment.

Add test 0192 (static member, cooperative-sticky, mock) which asserts
that no OffsetCommit is sent after a session timeout.

Happening since 1.6.0.

* Clear assignment lost flag when unassign completes

* Add more tests

* Add pre-existing missing check for unsubscribe as valid call after assignment lost

* Style fix

* Update change log
libserdes is deprecated; Confluent's supported C/C++ Schema Registry
serde library is libschemaregistry. Replace the C example built on
libserdes + avro-c with a C++ example built on libschemaregistry +
avro-cpp, modeled on libschemaregistry's own AvroProducer example, and
update the Makefile target, .gitignore and docs links accordingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@rayokota Robert Yokota (rayokota) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jgillenwater

Copy link
Copy Markdown
Member Author

Emanuele Sabellico (@emasab) Can you help me troubleshoot this build so we can get these changes merged?

@emasab

Copy link
Copy Markdown
Contributor

LGTM! Thanks Jamie Gillenwater (@jgillenwater) !

@jgillenwater
Jamie Gillenwater (jgillenwater) merged commit c6c9973 into master Oct 2, 2026
4 checks passed
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.

6 participants