confluent-kafka: add build-confluent-kafka.yml for riscv64 wheels - #392
Conversation
dffae2c to
680a0c8
Compare
|
CI could not be started for this PR:
What was verified locally instead, on aarch64 in
riscv64-specific checks run under QEMU against
|
680a0c8 to
96fcd36
Compare
ae2543b to
83bbee4
Compare
confluent-kafka publishes per-interpreter compiled wheels (a `cimpl`
extension linked against librdkafka) for x86_64/aarch64 but not riscv64.
Upstream's released wheels take librdkafka from a prebuilt
`librdkafka.redist` NuGet package (tools/wheels/install-librdkafka.sh),
which has no riscv64 build. So this uses their other supported path,
`tools/prepare-cibuildwheel-linux.sh` -- the Linux `CIBW_BEFORE_BUILD` that
`tools/cibuildwheel-build.sh` sets -- which runs `bootstrap-librdkafka.sh
--require-ssl` so mklove builds zlib, OpenSSL 3.5.6, zstd 1.5.7 and curl
8.20.0 from source and links them statically into librdkafka.
Two riscv-only deviations:
- the Rocky 10 manylinux image ships only `perl-interpreter`, so OpenSSL's
`Configure` dies on `Can't locate lib.pm in @INC`; `dnf -y install perl`
brings in perl-lib, perl-FindBin, perl-IPC-Cmd, perl-Pod-Html and
perl-Time-Piece;
- `auditwheel repair --strip`, because a source-built librdkafka keeps its
debug symbols (58MB against the 11MB prebuilt one upstream bundles).
The librdkafka tag is read out of the checkout's `.semaphore/semaphore.yml`
with upstream's own sed from `tools/pre-release-validation.sh`, so it tracks
whatever the release pins rather than being hardcoded a second time.
Upstream tests its wheels with `pytest {project}/tests/test_error.py`; the
rest of `tests/` needs no cluster, so run all of it bar `tests/integration`
(needs a live Kafka cluster) and `tests/schema_registry` (needs the `rules`
extra, whose tink and google-re2 have no riscv64 build). The test deps are
upstream's `requirements-tests.txt` plus the `oauthbearer-aws` extra and
`avro`/`requests`, which is what the collected modules import; the `avro`
extra itself is avoided because it pulls authlib -> cryptography, whose
latest release has no riscv64 wheel on either index. 670 tests pass locally (16 skipped)
against upstream's own 2.15.0 wheel with this exact command.
Matrix is cp312/cp313/cp314 -- upstream's `build-wheels.sh` sets
`CIBW_SKIP=... *t-*` and PyPI carries no free-threaded wheel, so no cp314t.
83bbee4 to
2301471
Compare
test_callback_exception_no_system_error arms an error_cb and a stats_cb that raise unconditionally, asserts the exception surfaces out of consume(), and then calls close() with the callbacks still armed. librdkafka keeps delivering both events for the consumer's lifetime -- stats every statistics.interval.ms (100ms here), the broker-resolve failure on its retry backoff -- and close() dispatches whatever is queued, so when more than one interval elapses between the assertions and close() the callback fires again and its RuntimeError escapes close(). That is what failed the cp312 job of run 32915860381 while cp313 passed on the same tree. It is a latent race, not an architecture problem; the riscv64 runner is just slow enough to reach it. Reproduced on macOS/arm64 against upstream's released 2.15.0 wheel by inserting time.sleep(1.2) before each close() in the real test: it fails there unpatched and passes with this change. The patch makes each callback raise on its first invocation only, so every assertion in the test is unchanged and only the redundant later raises during close() go away.
The cp314 job of run 32915860381 died with a SIGSEGV inside
re/_compiler.py while pytest was compiling a literal regex -- the first
statement of tests/test_Consumer.py::test_basic_api, immediately after
tests/test_Admin.py had finished. Nothing about it is riscv64-specific:
the identical crash, same file and same line, reproduces on macOS/arm64
under CPython 3.14.7 against upstream's released 2.15.0 wheel.
Bisecting the run isolates it to
tests/test_Admin.py::test_uninitialized_admin_client_methods, and within
that test to `admin.delete_records([TopicPartition("topic", 0, 10)])`.
Admin_delete_records() parses topic_partition_offsets with
PyArg_ParseTupleAndKeywords("OO|ff", ...) -- a borrowed reference it
never Py_INCREFs -- and then Py_XDECREFs it on both the success and the
err: path, so every call drops a reference it does not own. Measurable
against the released wheel with sys.getrefcount(): 3 before the call,
2 after, on both paths and on 3.12 as well as 3.14.
The test's list is a temporary owned only by the argument tuple, so it
is freed while still referenced and the interpreter faults at whatever
runs next -- interpreter shutdown when the test runs alone, an unrelated
test when it runs in the suite. That is why cp313 passed on the same
tree: the corrupted allocation happened not to be reused before the run
ended.
patches/confluent-kafka/2.15.0/0003-* drops the two decrefs. It is the
only unbalanced one in the extension: the other Admin functions that
decref a parsed-in object do so for `future`, which they Py_INCREF first
because options_to_c() hands it to the background event callback.
|
On cb21ffd, is it an issue known upstream? EDIT: https://github.com/confluentinc/confluent-kafka-python/pull/2328/changes, make sure to backport that patch rather than doing your own |
Replaces the hand-written patch with a backport of upstream's own fix, confluentinc/confluent-kafka-python#2328 (253e0c6, merged 2026-08-24), which is not in the 2.15.0 tag. Same two Py_XDECREF removals, plus the regression test upstream added, and it drops out cleanly when we move to a release that contains it. The CHANGELOG.md hunk is omitted: it does not apply to 2.15.0 and carries no code.
|
Good catch — backported in 5db5778 rather than carrying our own.
The only thing dropped from upstream's commit is its |
confluent-kafka publishes per-interpreter compiled wheels (a
cimplextension linked against librdkafka) for x86_64/aarch64 but not riscv64.Upstream's released wheels take librdkafka from a prebuilt
librdkafka.redistNuGet package (tools/wheels/install-librdkafka.sh), which has no riscv64 build. So this workflow uses upstream's other supported path —tools/prepare-cibuildwheel-linux.sh, the LinuxCIBW_BEFORE_BUILDthat theirtools/cibuildwheel-build.shsets — which runsbootstrap-librdkafka.sh --require-ssl, so mklove builds zlib, OpenSSL 3.5.6, zstd 1.5.7 and curl 8.20.0 from source and links them statically into librdkafka.Two riscv-only deviations:
perl-interpreter, so OpenSSL'sConfiguredies withCan't locate lib.pm in @INC. Verified inquay.io/pypa/manylinux_2_39_riscv64under QEMU: beforednf -y install perlthat error reproduces exactly; after it,use lib; use FindBin; use IPC::Cmd; use Time::Piece; use Pod::Htmlall succeed (perl-lib,perl-FindBin,perl-IPC-Cmd,perl-Time-Piece,perl-Pod-Html). Installing the whole distribution rather than picking modules one CI cycle at a time — a first attempt that named four of them still missedlib.pm.wgetis not needed: mklove'smkl_download_filefalls back tocurl, which the image has.auditwheel repair --strip, because a source-built librdkafka keeps its debug symbols (58MB against the 11MB prebuilt one upstream bundles); stripped it lands at 9.8MB, i.e. upstream's wheel size.The librdkafka tag is read out of the checkout's
.semaphore/semaphore.ymlwith upstream's own sed fromtools/pre-release-validation.sh, so it tracks whatever the release pins rather than being hardcoded a second time.Licensing
The wheel statically links OpenSSL, zlib, zstd and curl into
librdkafka.so—stringson upstream's own 2.15.0 aarch64 wheel showsSTATIC_LINKING ... ZLIB SSL ZSTD CURLandOpenSSL 3.5.6. Upstream'sLICENSEhas a dedicated "The binary wheel distribution of confluent-kafka-python contains additional software with the following licenses" section, but it covers only OpenSSL, zlib and librdkafka's own bundled licences — zstd and curl are absent (grep -ci curl LICENSE→ 0). Both require their copyright notice to travel with binary redistributions, sopatches/confluent-kafka/2.15.0/0001-LICENSE-add-zstd-and-curl-notices.patchadds them in upstream's existing format, taggedUpstream-Status: To upstream.LICENSEis already shipped viaMANIFEST.in+ setuptools' defaultLICEN[CS]E*glob, so this is a content-only change with no packaging edit. The firsttest-commandasserts the notices are present in the installeddist-info/licenses/LICENSE, so a patch that silently stops applying fails the job rather than leaving it green — checked against an unpatched 2.15.0 wheel, where the assertion fails.Tests
Upstream's wheel job only runs
pytest {project}/tests/test_error.py. The rest oftests/needs no cluster, so this runs all of it bartests/integration(needs a live Kafka cluster) andtests/schema_registry(needs therulesextra, whosetinkandgoogle-re2have no riscv64 build).Test deps are upstream's
requirements/requirements-tests.txtplus theoauthbearer-awsextra andavro/requests— exactly what the collected modules import. Theavroextra is deliberately not used: it pullsauthlib→cryptography, and PyPI's latest cryptography (50.0.1) has no riscv64 wheel while our registry tops out at 49.0.0, so pip would resolve to the newer one and try a Rust build from sdist in a container with no cargo. Every remaining dep resolves to a riscv64 orpy3-none-anywheel (ast-serialize,librtandcoverage, the only compiled ones, all publish riscv64 wheels).Dry-run of the exact test command against upstream's released 2.15.0 wheel on macOS/arm64 (no compile, no QEMU): 670 passed, 16 skipped.
Matrix is cp312/cp313/cp314 — upstream's
build-wheels.shsetsCIBW_SKIP="... *t-*"and PyPI carries no free-threaded wheel, so no cp314t.A latent race in
test_callback_exception_no_system_errorpatches/confluent-kafka/2.15.0/0002-tests-make-the-raising-callbacks-in-test_callback_ex.patch.The test arms an
error_cband astats_cbthat raise unconditionally, asserts the exception surfaces out ofconsume(), and then callsclose()with the callbacks still armed. librdkafka keeps delivering both events for the consumer's lifetime — stats everystatistics.interval.ms(100ms here), the broker-resolve failure on its retry backoff — andclose()dispatches whatever is queued, so once more than one interval elapses between the assertions andclose()the callback fires again and itsRuntimeErrorescapesclose().That failed the cp312 job of run 32915860381 while cp313 passed on the same tree — a race, not an architecture problem; the riscv64 runner is simply slow enough to reach it. Reproduced on macOS/arm64 against upstream's released 2.15.0 wheel by inserting
time.sleep(1.2)before eachclose()in the real test: it fails there unpatched and passes with this change.The patch makes each callback raise on its first invocation only, so every assertion is unchanged and only the redundant later raises during
close()go away. Re-running the full reduced suite against the released wheel with both patches applied still gives 670 passed, 16 skipped.A refcount bug in
AdminClient.delete_records()patches/confluent-kafka/2.15.0/0003-Admin-stop-delete_records-dropping-a-borrowed-refere.patch.The cp314 job of the same run died with
Fatal Python error: Segmentation faultinsidere/_compiler.pywhile pytest compiled a literal regex — the first statement oftests/test_Consumer.py::test_basic_api, immediately aftertests/test_Admin.pyfinished. Nothing about it is riscv64-specific: the identical crash, same file and same line, reproduces on macOS/arm64 under CPython 3.14.7 against upstream's released 2.15.0 wheel, in about a second:Bisecting isolates it to
tests/test_Admin.py::test_uninitialized_admin_client_methods, and within that test toadmin.delete_records([TopicPartition("topic", 0, 10)]).Admin_delete_records()parsestopic_partition_offsetsout ofPyArg_ParseTupleAndKeywords("OO|ff", ...)— a borrowed reference it neverPy_INCREFs — and thenPy_XDECREFs it on both the success and theerr:path. Every call drops a reference it does not own, measurable against the released wheel without a broker:Both paths, on 3.12 as well as 3.14. In the test the list is a temporary owned only by the argument tuple, so it is freed while still referenced and the interpreter faults at whatever runs next — interpreter shutdown when the test runs alone, an unrelated test when it runs in a suite. That is why cp313 passed on the same tree.
The patch drops the two decrefs. It is the only unbalanced one in the extension: the other
Admin_*functions that decref a parsed-in object do so forfuture, which theyPy_INCREFfirst becauseoptions_to_c()hands it to the background event callback as the opaque.Note on triggers
A brand-new
build-<pkg>.ymlcannot beworkflow_dispatch-ed: GitHub resolves the workflow file on the default branch, sogh workflow run(and thereforepr-trigger.yml'sTrigger:line) answersHTTP 404: workflow build-confluent-kafka.yml not found on the default branch— that is what happened on run 32891418686. Thepull_request: pathstrigger is what produces the first run and registers the workflow, so it is declared here alongsideworkflow_dispatch, as every workflow onmaindoes.CI
Run 32962091431 is green on all three interpreters — 670 passed, 16 skipped on each of cp312/cp313/cp314 — and the
publishjob dry-ran cleanly:Each wheel is ~4.1 MB and carries
confluent_kafka/cimpl.cpython-3XX-riscv64-linux-gnu.soplus a stripped 8 MBconfluent_kafka.libs/librdkafka-*.so.1.