Skip to content

Update the vendored nats-io client to fix a stalled NATS subscription and a JetStream fetch deadlock - #119867

Open
groeneai wants to merge 1 commit into
ClickHouse:masterfrom
groeneai:bump-nats-io-libuv-attach-and-jetstream-fetch
Open

Update the vendored nats-io client to fix a stalled NATS subscription and a JetStream fetch deadlock#119867
groeneai wants to merge 1 commit into
ClickHouse:masterfrom
groeneai:bump-nats-io-libuv-attach-and-jetstream-fetch

Conversation

@groeneai

@groeneai groeneai commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Related: #110321
Related: #119853
Related: #118449

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Updated the vendored nats-io client, fixing three defects: a NATS table silently received nothing after its broker restarted; DROP DATABASE hung for 600 seconds on a database holding a NATS table with a JetStream consumer; and the server crashed on a NATS table whose credentials the broker rejects.

Description

Asked for by @ alexey-milovidov in #110321 (comment).

contrib/nats-io still pins a commit from April 2025, so master builds the library without three fixes merged on ClickHouse/v3.9.2, the branch it tracks. This moves the pin to that head, cb4edba96601.

  • 37532c32 (libuv adapter: do not let an attach overtake a queued poll removal nats.c#5): a reconnect's natsLibuv_Attach installed a new socket off the loop thread, ahead of a poll removal _evStopPolling had queued. Draining that older removal made uvPollUpdate see events == 0 and close the fd just installed, while the connection considered itself attached, so it never polled again. Poll state is now loop-thread-owned and the attach has the queue-jump guard its siblings already had. No new lock.
  • b41317ae (Backport upstream fix for the JetStream fetch lock-order inversion nats.c#6): js_maybeFetchMore published its pull request while holding the subscription lock, inverting the client's lock order.
  • 3e3a3f10, by @ alexey-milovidov: uvPollUpdate dereferenced a released poll handle.

Both of my commits sit on top of the third, so only the head delivers all three. #119853 pins 3e3a3f10, an ancestor of cb4edba9, so a conflict resolves to keeping cb4edba9; its StorageNATS recovery of a closed connection is not duplicated here. #119507 moves instead to ClickHouse/v3.13.0, which still writes nle->socket inline in natsLibuv_Attach, so it leaves the stall in place.

A standalone ThreadSanitizer harness linking the adapter against contrib/libuv reproduces the ordering defect 25 of 25 runs at the old pin and at 3e3a3f10 alone, and 0 of 25 at cb4edba9. Two broker-restart integration tests pass on the bumped build.

Seen on true master in Integration tests (amd_tsan, 2/6): the race at 60f0a24d70ac, the hang at cd52baad3efe.

Post Hooks is red by construction: the two defects I fix are races in the client, so this diff has no test; keeping Bug Fix is the reviewer's call.


Workflow [PR]
Sync PR [sync-upstream/pr/119867]

Bump contrib/nats-io from cf441828d30f to cb4edba96601, the head of the
ClickHouse/v3.9.2 fork branch that master's pin already tracks. It is a
fast-forward (ahead 4, behind 0) and brings three fixes:

* 37532c32 (ClickHouse/nats.c#5). The libuv adapter let a reconnect's
  natsLibuv_Attach install a new socket off the loop thread while a poll
  removal queued earlier by _evStopPolling was still pending. When the loop
  thread later drained that older removal, uvPollUpdate saw events == 0 and
  called natsConnection_ProcessCloseEvent on the fd the attach had just
  installed. The connection then reported itself attached and never polled
  again, so a NATS table silently received nothing after its broker
  restarted. ThreadSanitizer reported it as a data race in
  natsLibuv_Attach. Poll state is now owned by the loop thread: the socket
  travels inside the attach event and is assigned in uvAsyncAttach, and
  natsLibuv_Attach gained the queue-jump guard its three sibling callbacks
  already had. No new lock, because a lock would have hidden the report
  while leaving the socket close live.

* b41317ae (ClickHouse/nats.c#6). js_maybeFetchMore published its pull
  request while holding the subscription lock, so DROP DATABASE over a
  database holding a NATS table with a JetStream consumer blocked for the
  full 600 second query timeout.

* 3e3a3f10 (by @ alexey-milovidov). uvPollUpdate dereferenced a released
  poll handle when _evStopPolling ran twice, crashing the server on a NATS
  table whose credentials the broker rejects.

Both of my commits have cf441828 as their only parent and were merged as two
merge commits on top of 3e3a3f10, so no commit on the branch carries them
without also carrying 3e3a3f10. The branch head is therefore the only pin
that delivers all three. 3e3a3f10 is self-contained: two guards inside the
library, and ClickHouse names none of the internals it touches.

Validated with a standalone ThreadSanitizer harness that links the real
adapter header against the real contrib/libuv and drives the interleaving
from the CI report. At cf441828 the ordering defect reproduces 25 of 25 runs
and the race is reported in natsLibuv_Attach; at 3e3a3f10 alone it still
reproduces 25 of 25, which is why bumping only to that commit is not enough;
at cb4edba9 it is 0 of 25 with the close path still exercised, so the zero is
measured rather than an unexercised probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@groeneai groeneai added can be tested Allows running workflows for external contributors groeneai-origin-request PR origin: a maintainer pinged or directed groeneai labels Sep 14, 2026
@groeneai

Copy link
Copy Markdown
Collaborator Author
Internal second-model review: adjudication log (click to expand)

Pre-publication review by an independent model (engine: codex; 2 findings; bounded: 1 full pass).

# Sev Finding Verdict Evidence / action
1 If a queued reconnect attach fails to allocate its poll handle, the socket leaks, because uvPollUpdate now returns early on a released handle (src/adapters/libuv.h) DISAGREE on where the fix lands; the defect is conceded Confirmed by reading, and not introduced here: at the pin being replaced the same sequence reached uv_close on a NULL handle and took the server down, which is the fault in #118449, so this bump replaces a crash with one leaked descriptor under an allocation failure. ClickHouse/v3.13.0 has the identical uvAsyncAttach and no guard at all, so the residual is the library's shape rather than this fork's. The fix therefore belongs in a ClickHouse/nats.c PR against the adapter, not in a gitlink bump. I did not reproduce it (it needs malloc to fail), which is also why I am not opening a follow-up unasked.
2 💡 The changelog entry mentions a crash, so the category should be Critical Bug Fix DISAGREE The crash is fixed by 3e3a3f10, and #119853 already claims that closure and carries the Critical Bug Fix entry for it, which is why #118449 is Related here and not Closes. This PR's own contract is the silent stall and the JetStream fetch deadlock, so it stays Bug Fix. Happy to change it if you would rather this one carried it.

Severity: ❌ blocker / ⚠️ major / 💡 nit. DISAGREE verdicts carry recorded evidence and are terminal per finding.

Session id: cron:clickhouse-review-slot-10:20260913-232400

@clickhouse-gh clickhouse-gh Bot added the manual approve Manual approve required to run CI label Sep 14, 2026
@clickhouse-gh clickhouse-gh Bot closed this Sep 14, 2026
@clickhouse-gh clickhouse-gh Bot reopened this Sep 14, 2026
@clickhouse-gh

clickhouse-gh Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [549dda0]

Summary:

job_name test_name status info comment
Integration tests (arm_binary, distributed plan, 2/4) FAIL
test_storage_rabbitmq/test.py::test_rabbitmq_queue_consume FAIL cidb
Finish Workflow FAIL
python3 ./ci/jobs/scripts/workflow_hooks/new_tests_check.py FAIL

AI Review

Summary

This PR moves the contrib/nats-io pin forward to pick up the upstream libuv reconnect-ordering fix and the JetStream fetch deadlock fix. I found one blocker: the submodule bump replaces only the vendored half of the auth-rejection fix, so a NATS table whose credentials are temporarily rejected no longer takes down the server, but it still cannot recover once the credentials are accepted again.

Findings

❌ Blockers

  • [contrib/nats-io:1] The updated nats.c still closes a connection permanently after the same authorization error on two reconnect attempts, but ClickHouse still never recreates a non-null closed consumers_connection (src/Storages/NATS/StorageNATS.cpp:344-350) and threadFunc only handles closed subscriptions, not a closed connection (src/Storages/NATS/StorageNATS.cpp:726-739). After a password rotation is rolled back, core NATS remains dead and keeps losing messages, and JetStream remains stalled, until DETACH/ATTACH.
    Suggested fix: carry over the StorageNATS closed-connection recovery from Fix a server crash on a NATS table whose credentials the broker rejects #119853 (drop the old consumers, reset the closed connection, and let the init task rebuild them). If this PR supersedes that one, keep its focused auth-rotation integration test too.
Final Verdict

LLVM Coverage Report

Measured on commit 549dda0.

Metric Baseline Current Δ
Lines 89.00% 89.00% +0.00%
Functions 91.80% 91.80% +0.00%
Branches 81.40% 81.50% +0.10%

Changed lines: Uncovered code analysis did not run: No coverable C/C++ source files changed (contrib/ is excluded from coverage).

Newly covered: +187 lines in 53 files (-122 lines lost coverage) · Details

Full report

@clickhouse-gh clickhouse-gh Bot added pr-bugfix Pull request with bugfix, not backported by default submodule changed At least one submodule changed in this PR. labels Sep 14, 2026
Comment thread contrib/nats-io
@@ -1 +1 @@
Subproject commit cf441828d30fdd5de12d9da319e88d2586fdeeba
Subproject commit cb4edba96601711e82085153e11b512448f8b7b5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This gitlink-only replacement drops the ClickHouse-side half of the auth-rotation fix. The bumped nats.c still closes a connection for good after the same authorization error on two reconnect attempts (_processAuthError is unchanged), but StorageNATS::createConsumersConnection still returns as soon as consumers_connection is non-null (src/Storages/NATS/StorageNATS.cpp:344-350), and threadFunc only reacts to closed subscriptions, not to a permanently closed connection (src/Storages/NATS/StorageNATS.cpp:726-739).

That means the event-loop exception is gone, but once the password/token is accepted again the table still has no path to build a fresh NATSConnection: core NATS stays dead and keeps losing messages, and JetStream stays stalled, until DETACH/ATTACH.

If this PR is superseding #119853, it still needs the StorageNATS closed-connection recovery from that PR (drop consumers, reset the closed connection, and reinitialize) rather than only the submodule bump.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Both code claims are correct as written, and this PR changes neither site: createConsumersConnection returns at StorageNATS.cpp:346-347 whenever consumers_connection is non-null, and the only closed thing threadFunc reacts to is a subscription (consumersNeedResubscribe() at line 728).

The conditional the finding rests on is false: this PR does not supersede #119853 and is not a substitute for it. Measured on this branch:

  • 3e3a3f10 (that PR's pin) is an ancestor of cb4edba9 (this pin), and the four commits between them touch only src/adapters/libuv.h, src/js.c, src/natsp.h and test/. src/conn.c is byte-identical between the two pins, so this bump carries its _close released-handle guard verbatim and leaves _processAuthError exactly as master has it. Closing for good after repeated auth rejections is therefore neither introduced nor widened here; it is what master does today at cf441828.
  • The recovery being asked for is that PR's, and its hunks cover precisely the two sites cited above: an isClosed() drop-consumers-and-reset block inserted in createConsumersConnection, and a reinitialize branch at the top of threadFunc. Carrying it here means copying 62 source lines plus a 193-line fake broker and a 211-line rotation test out of an open PR, which would be taking over its author's change.

So the split is: this PR is the gitlink only, for the stall after a broker restart, the JetStream fetch lock-order deadlock, and the released-handle crash it inherits from #119853's commit; #119853 owns getting the table consuming again after the credentials are accepted, and its test. Either merge order works, because the one-line gitlink conflict resolves to cb4edba9, the other pin being its ancestor.

If a single PR is preferred, moving #119853's pin to cb4edba9 subsumes this one and I will close it. The description now states the split rather than leaving it to be inferred.

@clickhouse-gh clickhouse-gh Bot added the comp-external-dependencies Third-party deps updates in contrib/, vendored code, and platform base libraries. label Sep 14, 2026
@clickhouse-gh

clickhouse-gh Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing 549dda0c0 with master a5efd3a38 (stripped binary size, per-symbol sizes and ThinLTO time; compile times per translation unit against the most recent warmup build that recompiled it).

✅ No significant changes.

Binary sizes

programs/clickhouse-stripped: smaller than the master baseline by the known offset between the two builds, so the difference is not shown. A delta that differs from the offset by more than 50% of it is shown, in either direction.

The official master build is compiled with -g and a pull request build is not, and XRay counts debug instructions towards its instrumentation threshold, so master instruments thousands of functions more and its binary is ~0.4% larger no matter what the pull request does.

Compile time of recompiled translation units

43 translation units recompiled, 9 s compile time in total, 43 of them have a recent master baseline.

Job report

@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger - 549dda0

Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task whose
fixing-PR link will be posted here when it opens. Only CH Inc sync is exempt, and it passed here.

Check / test Reason Owner / fixing PR
Finish Workflow / Post Hooks new_tests_check.py reaches No new tests have been added: the whole diff is the contrib/nats-io gitlink and the Bug Fix category applies pr-bugfix. Deterministic, so it re-fires on every re-run of this head. The two defects the bump fixes are races in the vendored client, reachable only through a failpoint in nats.c or a scripted broker, so there is no test this diff can carry, and a test that does not redden on master HEAD would only move the same check to "none of the per-arch Bugfix validation jobs reported OK" PR-caused by design and owned by this PR (#119867): the resolution is the reviewer's changelog-category decision, and the category is the only lever, so I will not change it on my own judgement
Integration tests (arm_binary, distributed plan, 2/4) / test_storage_rabbitmq/test.py::test_rabbitmq_queue_consume flaky: the 60 second polling timeout in check_expected_result_polling at test.py:130, 8 rows / 4 pull requests / 0 true master in 30 days. Not reachable from this diff, which changes no RabbitMQ code and no AMQP client A fix task owns it, and its remaining design question is open with the component owner in #110180 (comment). The fixing-PR link will be posted here when one opens
CIDB breadth behind the RabbitMQ row
SELECT count() AS rows, uniqExact(pull_request_number) AS prs,
       countIf(head_ref = 'master' AND pull_request_number = 0) AS true_master
FROM default.checks
WHERE check_start_time > now() - INTERVAL 30 DAY
  AND test_name = 'test_storage_rabbitmq/test.py::test_rabbitmq_queue_consume'
  AND test_status IN ('FAIL', 'ERROR')
rows	prs	true_master
8	4	0

Grouped by pull request: 100185 four rows, 116050 two, 110180 one, this branch one.

For precedent rather than ownership: the identical Post Hooks red sat on the merged head of #114275, a contrib/NuRaft gitlink-only Bug Fix that added no test, and it was merged with it.

The 171 cancelled check-runs on this head are the first of its two pull_request dispatches, superseded 9 seconds later by the second and cancelled by the workflow concurrency group. They carry no CIDB rows and no verdict.

Session id: cron:clickhouse-maint-slot-12:20260914-044300

@PedroTadim

Copy link
Copy Markdown
Member

cc @alexey-milovidov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors comp-external-dependencies Third-party deps updates in contrib/, vendored code, and platform base libraries. groeneai-origin-request PR origin: a maintainer pinged or directed groeneai manual approve Manual approve required to run CI pr-bugfix Pull request with bugfix, not backported by default submodule changed At least one submodule changed in this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants