[
https://issues.apache.org/jira/browse/KAFKA-20860?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Matthias J. Sax updated KAFKA-20860:
------------------------------------
Description:
Report generate by Claude:
The async consumer's background thread has three error-containment gaps in
machinery shared by
consumer, share and streams groups. They compound: (2) hides (1), and (1)'s
failure mode is a
silent, permanent loss of a group member. Line numbers are against trunk
0d0ec5af1e.
h2. 1. reconciliationInProgress is a latch that a synchronous throw never
releases
markReconciliationInProgress() runs before synchronous work;
markReconciliationCompleted() is only
ever called from the asynchronous completion callbacks. Any exception escaping
the synchronous
portion leaves the flag true, and every later maybeReconcile() early-returns at
the gate.
In AbstractMembershipManager:
* \{{:885}} the gate -- if (reconciliationInProgress) then return
* \{{:931}} markReconciliationInProgress()
* \{{:949}} markPendingRevocationToPauseFetching(...) ->
subscriptions.markPendingRevocation ->
assignedState(tp), which throws IllegalStateException when the partition is
absent
(SubscriptionState.java:426-431)
* \{{:953}} signalReconciliationStarted() -- synchronous call returning a future
* \{{:956}} commitResult.whenComplete(...) -- the only route to the three
markReconciliationCompleted() sites (\{{:1022}}, \{{:1031}}, \{{:1050}}), all
asynchronous
StreamsMembershipManager has the same shape with a wider synchronous window:
markReconciliationInProgress() at \{{:1117}}, then an invariant
IllegalStateException at \{{:1154}}
and an unchecked subtopologies().get(...) at \{{:1292}}/\{{:1307}} reached from
\{{:1160}} (NPE), all
before the first future is built at \{{:1164}}.
There is a second window in both: revokeAndAssign(...) (consumer \{{:968}}) and
revokeActiveTasks(...) are invoked inside a whenComplete lambda but before the
inner future chain
exists, so a throw there is captured by that lambda's dependent stage instead
of reaching the
error branch that clears the flag. This window is wide in wall-clock terms --
it opens after the
auto-commit-before-rebalance round trip, i.e. many runOnce iterations later.
h3. Failure mode
The member does not die:
* state stays RECONCILING; every later maybeReconcile() early-returns at the
gate;
* heartbeats continue (the heartbeat manager is a separate RequestManager), so
no session timeout;
* *the latch survives fencing and rejoin.* transitionToJoining() does NOT clear
reconciliationInProgress; it sets rejoinedWhileReconciliationInProgress = true
(AbstractMembershipManager:581-582, StreamsMembershipManager:457-459). That
flag is only ever
consumed by maybeAbortReconciliation(), i.e. on the async completion path that
-- by the premise
of this bug -- never runs. So the member is wedged for the lifetime of the
membership-manager
instance: it can be fenced, rejoin, be assigned, and still never reconcile.
* the member keeps serving its old assignment and never re-echoes its owned
partitions/tasks, so
the broker never receives the ack it is waiting for.
Whether the broker intervenes depends on the shape of the new assignment,
because the rebalance
timeout is armed only while the member is broker-side UNREVOKED_TASKS and
cancelled otherwise
(GroupMetadataManager:4019-4028, timeout action at :5287-5305):
* *new assignment revokes something* -> timeout armed -> member fenced after
rebalanceTimeoutMs
(for Streams that is max.poll.interval.ms, default 5 min), releasing its tasks
so the other
members unblock. But the latch survives the rejoin, so this becomes a permanent
fence -> rejoin -> never-reconcile -> fence cycle, once per rebalanceTimeoutMs,
indefinitely.
* *new assignment only adds* -> no ack is required, so no timeout is armed and
the broker considers
the member reconciled -> no fencing at all, indefinitely, while the client
never starts those
partitions/tasks. Silent under-processing: the broker believes the member owns
partitions it is
not consuming, and lag grows without bound.
Either way, one swallowed exception permanently removes that client from useful
work, and in the
first case repeatedly stalls the group's migration for a rebalance-timeout
window at a time.
h3. Long-standing
The same latch shape exists in MembershipManagerImpl at 3.7.0
(markReconciliationInProgress
{\{:808}}, sole markReconciliationCompleted call \{{:911}}) and 3.8.0
(\{{:943}} vs
{\{:1054}}/\{{:1064}}/\{{:1083}}), and in AbstractMembershipManager at 4.1.0
(\{{:854}} vs
{\{:964}}/\{{:973}}/\{{:992}}). It therefore dates to the async consumer's
first release.
h3. Reachability, stated honestly
I found no currently reachable synchronous throw for consumer or share groups:
within a single
runOnce() the background thread is the only mutator of SubscriptionState (the
app thread only reads
pausedPartitions(), AsyncKafkaConsumer:1363), and an unsubscribe() during the
commit window is
caught by maybeAbortReconciliation() because it changes state. It IS reachable
for streams groups
-- see the linked issue. So this is a live bug for one group type and a
defensive fix for the
others, in shared code, where the cost asymmetry is stark: a few lines of
try/catch against a
permanent, silent loss of a group member.
h3. Fix
Wrap the synchronous body from markReconciliationInProgress() to the point the
future chain is
established in try/catch (RuntimeException); on throw, log at ERROR and
markReconciliationCompleted() so the member retries on the next poll instead of
freezing. Same for
the StreamsMembershipManager override and for the revokeAndAssign /
revokeActiveTasks prologues.
Additionally, clear the latch in transitionToJoining() so that a rejoin is a
genuine reset.
Note on scope: this restores containment, it does not make an unexecutable
assignment executable.
For a transient cause the next poll recovers, which is the value. For a
deterministic cause the
throw recurs every background-thread iteration (state still RECONCILING, target
still not equal to
current, gate now open), giving a loud retry loop rather than a silent freeze.
Deciding what to do
about input a client can never execute is a separate policy question, handled
in the linked issue.
h2. 2. Every response-handling callback silently swallows exceptions
{code:java}
// NetworkClientDelegate.java:385-388
UnsentRequest whenComplete(BiConsumer<ClientResponse, Throwable> callback) {
handler.future().whenComplete(callback); // dependent stage discarded
return this;
}
{code}
FutureCompletionHandler.onComplete completes the source future with
future.complete(response)
(\{{:451}}). Per the CompletableFuture contract, an exception thrown by a
whenComplete action is
captured into the DEPENDENT stage and does not propagate to complete(); the
source future and the
completing thread are unaffected. That dependent stage is discarded here, and
nothing anywhere
observes it -- this is the only call site in the module and there is no
isCompletedExceptionally()
check. The exception is lost with no log, no metric and no counter.
Blast radius, counting .whenComplete( handlers riding this seam per file:
OffsetsRequestManager 12,
AbstractMembershipManager 9, StreamsMembershipManager 8, CommitRequestManager 8,
StreamsGroupHeartbeatRequestManager 2, ShareConsumeRequestManager 2,
FetchRequestManager 2,
AbstractHeartbeatRequestManager 2, CoordinatorRequestManager 1,
TopicMetadataRequestManager 1,
StreamsGroupTopologyDescriptionRequestManager 1.
Present in 3.7.0 through trunk (verbatim at :385-388 in 4.2 and 4.3); the
current expression was
introduced by c81a725219 (KAFKA-15534, 2023-10-20).
This is why item 1 is undiagnosable, and it silently breaks forward
compatibility on its own. A
concrete example already in the tree:
StreamsGroupHeartbeatResponse.Status.fromCode (\{{:114-120}})
throws IllegalArgumentException on an unknown status code, and
StreamsMembershipManager.isGroupReady (\{{:802}}) decodes every status on every
successful
heartbeat. The member epoch has already been updated (\{{:763}}) when it
throws, so the client keeps
heartbeating healthily while silently ignoring every new assignment, with zero
log output
attributable to the cause. Any future broker release that adds a heartbeat
status code does this to
every existing client.
Fix: make the seam non-silent. In UnsentRequest.whenComplete, observe the
dependent stage and log
callback failures with the API key / request header for attribution. One place,
covers all eleven
managers; highest value-per-line in this ticket.
h2. 3. A throwing RequestManager.poll() skips the network pump for that
iteration
ConsumerNetworkThread.runOnce() (\{{:210-240}}) runs the manager loop
(\{{:222-226}}) before
networkClientDelegate.poll(...) (\{{:228}}), the maximumTimeToWait loop
(\{{:232-235}}) and
reapExpiredApplicationEvents (\{{:239}}). An exception from one manager's
poll() aborts all
remaining managers, the network pump and event reaping for that iteration; the
catch-all in run()
(\{{:163-166}}) logs "Unexpected error caught in consumer network thread" and
loops. A repeating
per-iteration throw in any single manager therefore stops all client I/O -- no
heartbeats sent, no
responses processed, no fetches -- while the thread spins, even though the
other managers are
healthy.
In combination with item 1 this is a one-shot, because the latch suppresses the
repeat (which is
why the streams symptom is a wedged member rather than a hot spin). A throw
that does not latch
gives the spin.
Fix: isolate per-manager failures in the runOnce loops (log and continue), so
one manager's defect
degrades that manager rather than the whole client.
was:
Report generate by Claude:
The async consumer's background thread has three error-containment gaps in
machinery shared by consumer, share and streams groups. They compound: (2)
hides (1), and (1)'s failure mode is a silent, permanent, group-wide rebalance
stall.
h3. 1. reconciliationInProgress is a latch that a synchronous throw never
releases
markReconciliationInProgress() runs before synchronous work;
markReconciliationCompleted() is only ever called from the asynchronous
completion callbacks. Any exception escaping the synchronous portion leaves the
flag true forever, and every later maybeReconcile() early-returns at the gate.
In AbstractMembershipManager:
* \{{:885}} the gate -- \{{if (reconciliationInProgress) { ... return; }}}
* \{{:931}} markReconciliationInProgress()
* \{{:949}} markPendingRevocationToPauseFetching(...) ->
subscriptions.markPendingRevocation ->
assignedState(tp), which throws IllegalStateException when the partition is
absent
(SubscriptionState.java:426-431)
* \{{:953}} signalReconciliationStarted() -- synchronous call returning a future
* \{{:956}} commitResult.whenComplete(...) -- the only route to the three
markReconciliationCompleted() sites (\{{:1022}}, \{{:1031}}, \{{:1050}}), all
asynchronous
StreamsMembershipManager has the same shape with a wider synchronous window:
markReconciliationInProgress() at \{{:1117}}, then an invariant
IllegalStateException at \{{:1154}} and an unchecked subtopologies().get(...)
at \{{:1292}}/\{{:1307}} (NPE), all before the first future is built at
\{{:1164}}.
There is a second window in both: revokeAndAssign(...) (consumer \{{:968}}) and
revokeActiveTasks(...) are invoked inside a whenComplete lambda but before the
inner future chain exists, so a throw is captured by that lambda's dependent
stage instead of reaching the error branch that clears the flag. This window is
wide in wall-clock terms -- it opens after the auto-commit-before-rebalance
round trip, i.e. many runOnce iterations later.
Failure mode -- the member neither dies nor gets fenced:
* state stays RECONCILING and reconciliation never runs again;
* heartbeats continue at the current epoch (separate RequestManager), so no
session timeout, no FENCED_MEMBER_EPOCH, no rejoin, ever;
* the member keeps serving its old assignment and never re-echoes its owned
partitions/tasks;
* the broker therefore never receives the ack it is waiting for,
partitions/tasks pending revocation are never released, and OTHER members
stall in UNRELEASED_PARTITIONS/UNRELEASED_TASKS.
So one client's swallowed exception freezes the entire group's rebalance
indefinitely.
Long-standing: the same latch shape exists in MembershipManagerImpl at 3.7.0
(markReconciliationInProgress \{{:808}}, sole markReconciliationCompleted
\{{:911}}) and 3.8.0 (\{{:943}} vs \{{:1054}}/\{{:1064}}/\{{:1083}}), and in
AbstractMembershipManager at 4.1.0 (\{{:854}} vs \{{:964}}/\{{:973}}/\{{:992}}).
Reachability, stated honestly: I found no currently reachable synchronous throw
for consumer or share groups. Within a single runOnce() the background thread
is the only mutator of SubscriptionState (the app thread only reads
pausedPartitions(), AsyncKafkaConsumer.java:1363), and an unsubscribe() during
the commit window is caught by maybeAbortReconciliation() because it changes
state. It IS reachable for streams groups (KIP-1071) -- see the linked issue.
So this is a live bug for one group type and a defensive fix for the others, in
shared code, where the cost
asymmetry is stark: a few lines of try/catch against a silent permanent
group-wide stall.
Fix: wrap the synchronous body from markReconciliationInProgress() to the point
the future chain is established in try/catch; on throw, log at ERROR and
markReconciliationCompleted() so the member retries on the next poll rather
than freezing. Same for the StreamsMembershipManager override and for the
revokeAndAssign / revokeActiveTasks prologues.
h3. 2. Every response-handling callback silently swallows exceptions
{code:java}
// NetworkClientDelegate.java:385-388
UnsentRequest whenComplete(BiConsumer<ClientResponse, Throwable> callback) {
handler.future().whenComplete(callback); // dependent stage discarded
return this;
}
{code}
FutureCompletionHandler.onComplete completes the source future with
future.complete(response) (\{{:451}}). Per the CompletableFuture contract, an
exception thrown by a whenComplete action is captured into the DEPENDENT stage
and does not propagate to complete(); the source future and the
completing thread are unaffected. That dependent stage is discarded here, and
nothing anywhere observes it -- this is the only call site in the module and
there is no isCompletedExceptionally() check. The exception is lost with no
log, no metric, no counter.
Blast radius (count of .whenComplete( handlers riding this seam per file):
OffsetsRequestManager 12, AbstractMembershipManager 9, StreamsMembershipManager
8, CommitRequestManager 8, StreamsGroupHeartbeatRequestManager 2,
ShareConsumeRequestManager 2, FetchRequestManager 2,
AbstractHeartbeatRequestManager 2, CoordinatorRequestManager 1,
TopicMetadataRequestManager 1, StreamsGroupTopologyDescriptionRequestManager 1.
Present in 3.7.0 through trunk (verbatim at :385-388 in 4.2 and 4.3); the
current expression was introduced by c81a725219 (KAFKA-15534, 2023-10-20).
This is why item 1 is undiagnosable, and it silently breaks forward
compatibility on its own. A concrete example already in the tree:
StreamsGroupHeartbeatResponse.Status.fromCode (\{{:114-120}}) throws
IllegalArgumentException on an unknown status code, and
StreamsMembershipManager.isGroupReady (\{{:802}}) decodes every status on every
successful heartbeat. The member epoch has already been updated (\{{:763}})
when it throws, so the client keeps heartbeating healthily while silently
ignoring every new assignment -- with zero log output attributable to the
cause. Any future broker release that adds a heartbeat status code does this to
every existing client.
Fix: make the seam non-silent. In UnsentRequest.whenComplete, observe the
dependent stage and log callback failures (with API key / request header for
attribution). One place, covers all eleven managers; highest value-per-line in
this ticket.
h3. 3. A throwing RequestManager.poll() skips the network pump for that
iteration
ConsumerNetworkThread.runOnce() (\{{:210-240}}) runs the manager loop
(\{{:222-226}}) before networkClientDelegate.poll(...) (\{{:228}}), the
maximumTimeToWait loop (\{{:232-235}}) and reapExpiredApplicationEvents
(\{{:239}}). An exception from one manager's poll() aborts all remaining
managers, the network pump and event reaping for that iteration; the catch-all
in run() (\{{:163-166}}) logs "Unexpected error caught in consumer network
thread" and loops. A repeating
per-iteration throw in any single manager therefore stops all client I/O -- no
heartbeats sent, no responses processed, no fetches -- while the thread spins,
even though the other managers are healthy.
With item 1 this is a one-shot, because the latch suppresses the repeat (which
is why the streams symptom is a zombie rather than a hot spin). A non-latching
repeating throw gives the spin.
Fix: isolate per-manager failures in the runOnce loops (log and continue), so
one manager's defect degrades that manager rather than the whole client.
> Broken error handling in the async consumer background thread
> -------------------------------------------------------------
>
> Key: KAFKA-20860
> URL: https://issues.apache.org/jira/browse/KAFKA-20860
> Project: Kafka
> Issue Type: Bug
> Components: clients, consumer
> Affects Versions: 3.7.0
> Reporter: Matthias J. Sax
> Priority: Critical
>
> Report generate by Claude:
> The async consumer's background thread has three error-containment gaps in
> machinery shared by
> consumer, share and streams groups. They compound: (2) hides (1), and (1)'s
> failure mode is a
> silent, permanent loss of a group member. Line numbers are against trunk
> 0d0ec5af1e.
> h2. 1. reconciliationInProgress is a latch that a synchronous throw never
> releases
> markReconciliationInProgress() runs before synchronous work;
> markReconciliationCompleted() is only
> ever called from the asynchronous completion callbacks. Any exception
> escaping the synchronous
> portion leaves the flag true, and every later maybeReconcile() early-returns
> at the gate.
> In AbstractMembershipManager:
> * \{{:885}} the gate -- if (reconciliationInProgress) then return
> * \{{:931}} markReconciliationInProgress()
> * \{{:949}} markPendingRevocationToPauseFetching(...) ->
> subscriptions.markPendingRevocation ->
> assignedState(tp), which throws IllegalStateException when the partition is
> absent
> (SubscriptionState.java:426-431)
> * \{{:953}} signalReconciliationStarted() -- synchronous call returning a
> future
> * \{{:956}} commitResult.whenComplete(...) -- the only route to the three
> markReconciliationCompleted() sites (\{{:1022}}, \{{:1031}}, \{{:1050}}), all
> asynchronous
> StreamsMembershipManager has the same shape with a wider synchronous window:
> markReconciliationInProgress() at \{{:1117}}, then an invariant
> IllegalStateException at \{{:1154}}
> and an unchecked subtopologies().get(...) at \{{:1292}}/\{{:1307}} reached
> from \{{:1160}} (NPE), all
> before the first future is built at \{{:1164}}.
> There is a second window in both: revokeAndAssign(...) (consumer \{{:968}})
> and
> revokeActiveTasks(...) are invoked inside a whenComplete lambda but before
> the inner future chain
> exists, so a throw there is captured by that lambda's dependent stage instead
> of reaching the
> error branch that clears the flag. This window is wide in wall-clock terms --
> it opens after the
> auto-commit-before-rebalance round trip, i.e. many runOnce iterations later.
> h3. Failure mode
> The member does not die:
> * state stays RECONCILING; every later maybeReconcile() early-returns at the
> gate;
> * heartbeats continue (the heartbeat manager is a separate RequestManager),
> so no session timeout;
> * *the latch survives fencing and rejoin.* transitionToJoining() does NOT
> clear
> reconciliationInProgress; it sets rejoinedWhileReconciliationInProgress = true
> (AbstractMembershipManager:581-582, StreamsMembershipManager:457-459). That
> flag is only ever
> consumed by maybeAbortReconciliation(), i.e. on the async completion path
> that -- by the premise
> of this bug -- never runs. So the member is wedged for the lifetime of the
> membership-manager
> instance: it can be fenced, rejoin, be assigned, and still never reconcile.
> * the member keeps serving its old assignment and never re-echoes its owned
> partitions/tasks, so
> the broker never receives the ack it is waiting for.
> Whether the broker intervenes depends on the shape of the new assignment,
> because the rebalance
> timeout is armed only while the member is broker-side UNREVOKED_TASKS and
> cancelled otherwise
> (GroupMetadataManager:4019-4028, timeout action at :5287-5305):
> * *new assignment revokes something* -> timeout armed -> member fenced after
> rebalanceTimeoutMs
> (for Streams that is max.poll.interval.ms, default 5 min), releasing its
> tasks so the other
> members unblock. But the latch survives the rejoin, so this becomes a
> permanent
> fence -> rejoin -> never-reconcile -> fence cycle, once per
> rebalanceTimeoutMs, indefinitely.
> * *new assignment only adds* -> no ack is required, so no timeout is armed
> and the broker considers
> the member reconciled -> no fencing at all, indefinitely, while the client
> never starts those
> partitions/tasks. Silent under-processing: the broker believes the member
> owns partitions it is
> not consuming, and lag grows without bound.
> Either way, one swallowed exception permanently removes that client from
> useful work, and in the
> first case repeatedly stalls the group's migration for a rebalance-timeout
> window at a time.
> h3. Long-standing
> The same latch shape exists in MembershipManagerImpl at 3.7.0
> (markReconciliationInProgress
> {\{:808}}, sole markReconciliationCompleted call \{{:911}}) and 3.8.0
> (\{{:943}} vs
> {\{:1054}}/\{{:1064}}/\{{:1083}}), and in AbstractMembershipManager at 4.1.0
> (\{{:854}} vs
> {\{:964}}/\{{:973}}/\{{:992}}). It therefore dates to the async consumer's
> first release.
> h3. Reachability, stated honestly
> I found no currently reachable synchronous throw for consumer or share
> groups: within a single
> runOnce() the background thread is the only mutator of SubscriptionState (the
> app thread only reads
> pausedPartitions(), AsyncKafkaConsumer:1363), and an unsubscribe() during the
> commit window is
> caught by maybeAbortReconciliation() because it changes state. It IS
> reachable for streams groups
> -- see the linked issue. So this is a live bug for one group type and a
> defensive fix for the
> others, in shared code, where the cost asymmetry is stark: a few lines of
> try/catch against a
> permanent, silent loss of a group member.
> h3. Fix
> Wrap the synchronous body from markReconciliationInProgress() to the point
> the future chain is
> established in try/catch (RuntimeException); on throw, log at ERROR and
> markReconciliationCompleted() so the member retries on the next poll instead
> of freezing. Same for
> the StreamsMembershipManager override and for the revokeAndAssign /
> revokeActiveTasks prologues.
> Additionally, clear the latch in transitionToJoining() so that a rejoin is a
> genuine reset.
> Note on scope: this restores containment, it does not make an unexecutable
> assignment executable.
> For a transient cause the next poll recovers, which is the value. For a
> deterministic cause the
> throw recurs every background-thread iteration (state still RECONCILING,
> target still not equal to
> current, gate now open), giving a loud retry loop rather than a silent
> freeze. Deciding what to do
> about input a client can never execute is a separate policy question, handled
> in the linked issue.
> h2. 2. Every response-handling callback silently swallows exceptions
> {code:java}
> // NetworkClientDelegate.java:385-388
> UnsentRequest whenComplete(BiConsumer<ClientResponse, Throwable> callback) {
> handler.future().whenComplete(callback); // dependent stage discarded
> return this;
> }
> {code}
> FutureCompletionHandler.onComplete completes the source future with
> future.complete(response)
> (\{{:451}}). Per the CompletableFuture contract, an exception thrown by a
> whenComplete action is
> captured into the DEPENDENT stage and does not propagate to complete(); the
> source future and the
> completing thread are unaffected. That dependent stage is discarded here, and
> nothing anywhere
> observes it -- this is the only call site in the module and there is no
> isCompletedExceptionally()
> check. The exception is lost with no log, no metric and no counter.
> Blast radius, counting .whenComplete( handlers riding this seam per file:
> OffsetsRequestManager 12,
> AbstractMembershipManager 9, StreamsMembershipManager 8, CommitRequestManager
> 8,
> StreamsGroupHeartbeatRequestManager 2, ShareConsumeRequestManager 2,
> FetchRequestManager 2,
> AbstractHeartbeatRequestManager 2, CoordinatorRequestManager 1,
> TopicMetadataRequestManager 1,
> StreamsGroupTopologyDescriptionRequestManager 1.
> Present in 3.7.0 through trunk (verbatim at :385-388 in 4.2 and 4.3); the
> current expression was
> introduced by c81a725219 (KAFKA-15534, 2023-10-20).
> This is why item 1 is undiagnosable, and it silently breaks forward
> compatibility on its own. A
> concrete example already in the tree:
> StreamsGroupHeartbeatResponse.Status.fromCode (\{{:114-120}})
> throws IllegalArgumentException on an unknown status code, and
> StreamsMembershipManager.isGroupReady (\{{:802}}) decodes every status on
> every successful
> heartbeat. The member epoch has already been updated (\{{:763}}) when it
> throws, so the client keeps
> heartbeating healthily while silently ignoring every new assignment, with
> zero log output
> attributable to the cause. Any future broker release that adds a heartbeat
> status code does this to
> every existing client.
> Fix: make the seam non-silent. In UnsentRequest.whenComplete, observe the
> dependent stage and log
> callback failures with the API key / request header for attribution. One
> place, covers all eleven
> managers; highest value-per-line in this ticket.
> h2. 3. A throwing RequestManager.poll() skips the network pump for that
> iteration
> ConsumerNetworkThread.runOnce() (\{{:210-240}}) runs the manager loop
> (\{{:222-226}}) before
> networkClientDelegate.poll(...) (\{{:228}}), the maximumTimeToWait loop
> (\{{:232-235}}) and
> reapExpiredApplicationEvents (\{{:239}}). An exception from one manager's
> poll() aborts all
> remaining managers, the network pump and event reaping for that iteration;
> the catch-all in run()
> (\{{:163-166}}) logs "Unexpected error caught in consumer network thread" and
> loops. A repeating
> per-iteration throw in any single manager therefore stops all client I/O --
> no heartbeats sent, no
> responses processed, no fetches -- while the thread spins, even though the
> other managers are
> healthy.
> In combination with item 1 this is a one-shot, because the latch suppresses
> the repeat (which is
> why the streams symptom is a wedged member rather than a hot spin). A throw
> that does not latch
> gives the spin.
> Fix: isolate per-manager failures in the runOnce loops (log and continue), so
> one manager's defect
> degrades that manager rather than the whole client.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)