[ 
https://issues.apache.org/jira/browse/KAFKA-20721?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Eswarar Siva updated KAFKA-20721:
---------------------------------
    Description: 
Updated 2026-06-22: the original mechanism in this ticket (a race between 
drainRestoredActiveTasks and
revokeTasksInStateUpdater) was wrong and is withdrawn. Both run on the 
StreamThread and cannot interleave. The
corrected root cause and a reproduction are below; see the comments for the 
history.

A StreamThread dies with:

java.lang.IllegalStateException: Task X_Y was not found in the state updater. 
This indicates a bug.
    at 
org.apache.kafka.streams.processor.internals.TaskManager.waitForFuture(...)
    at 
org.apache.kafka.streams.processor.internals.TaskManager.addToTasksToClose(...)
    at 
org.apache.kafka.streams.processor.internals.TaskManager.shutdownStateUpdater(...)

thrown when DefaultStateUpdater.removeTask completes a removal future with null.

Root cause: DefaultStateUpdater.tasks() can return the same TaskId twice. 
tasks() snapshots under
executeWithQueuesLocked, which holds tasksAndActionsLock, 
restoredActiveTasksLock and exceptionsAndFailedTasksLock,
but not the updatingTasks or pausedTasks maps. pauseTask does 
pausedTasks.put(id) then updatingTasks.remove(id), and
resumeTask does the reverse, so for a short window a task is in both maps and 
streamOfTasks() lists it twice.
ReadOnlyTask has no equals/hashCode, so the returned Set keeps both wrappers. A 
caller that does one remove per
tasks() entry (shutdownStateUpdater via addToTasksToClose, and 
handleAssignment) then removes the same task twice.
The first remove succeeds; the second reaches removeTask, finds the task in 
none of the four collections, and
completes the future with null, so waitForFuture throws the ISE. The same 
duplicate also breaks
TaskManager.allTasks(), which uses Collectors.toMap(Task::id, ...) and throws 
IllegalStateException: Duplicate key,
killing the StreamThread in runLoop.

Reproduction: a standalone Streams application, several KafkaStreams instances 
in one JVM, exactly_once_v2, against a
real broker, with cold start churn plus KafkaStreams.pause()/resume(). The 
duplicate window is normally sub
microsecond, so to make it deterministic a Byteman rule sleeps right after the 
put in pauseTask and resumeTask (no
logic change, it only widens the gap that is already there). With that, the ISE 
and the Duplicate key error both fire
within a couple of minutes. Reproduced on 4.1.2 and 4.3.0; trunk carries the 
same code on this path. With pause/resume
disabled there are no duplicates and no crash, even under aggressive churn. The 
reproducer, the Byteman rule and DEBUG
logs (TaskManager, DefaultStateUpdater, StoreChangelogReader) are attached.

Affected versions: 4.1.2 and 4.3.0 (reproduced); 4.2.x and trunk by code 
inspection.

Related: KAFKA-17402 is the same family (a duplicate from tasks(), seen there 
as shouldGetTasksFromRestoredActiveTasks
counting 3 instead of 2); its fix made the restore completion transition atomic 
but did not touch pauseTask/resumeTask,
which is the path here. The original production occurrence behind this ticket 
was on 4.1.2 with no pause/resume and very
large state, which looks closer to KAFKA-20456 (the state updater stalling and 
waitForFuture timing out, fixed in
4.3.0), so the production incident and this pause/resume reproduction may be 
two different routes to the same exception.

Suggested fix: stop tasks() from returning a duplicate, for example dedupe by 
task id before wrapping, or give
ReadOnlyTask equals/hashCode based on the task id, or make the pause/resume 
transition atomic with respect to tasks().
The ISE should stay; it is correctly catching the inconsistency.

  was:
Ran into this on a Streams app running exactly_once_v2, during a cold start 
while a bunch of stateful tasks were
still restoring and the group kept rebalancing. One of the StreamThreads just 
dies with:

{noformat}
java.lang.IllegalStateException: Task 1_2 was not found in the state updater. 
This indicates a bug.
  Please report at https://issues.apache.org/jira/projects/KAFKA/issues ...
    at 
org.apache.kafka.streams.processor.internals.TaskManager.waitForFuture(TaskManager.java:711)
    at 
org.apache.kafka.streams.processor.internals.TaskManager.lambda$getNonFailedTasks$10(TaskManager.java:672)
    at 
org.apache.kafka.streams.processor.internals.TaskManager.revokeTasksInStateUpdater(TaskManager.java:1168)
    at 
org.apache.kafka.streams.processor.internals.TaskManager.handleRevocation(TaskManager.java:1058)
{noformat}

It is thrown inside the rebalance callback, so it comes out as "User rebalance 
callback throws an error", the
default handler does SHUTDOWN_CLIENT, and the client goes REBALANCING to 
PENDING_ERROR. In my case there was also a
native RocksDB SIGSEGV on the way down. The thread does not recover without a 
restart.

After digging into it this is a race, not a real "impossible" state.

revokeTasksInStateUpdater walks stateUpdater.tasks() and calls 
stateUpdater.remove(taskId) for every revoked active
task it still sees. tasks() (streamOfNonPausedTasks) also includes tasks 
sitting in restoredActiveTasks, meaning
ones that already finished restoring but the stream thread has not drained yet. 
If that task leaves every one of the
updater collections (updatingTasks, pausedTasks, restoredActiveTasks, 
exceptionsAndFailedTasks) before the state
updater thread actually processes the queued remove (drained by 
handleRestoredTasksFromStateUpdater, or pulled out
by another action during the churn), removeTask matches none of the four 
branches and does future.complete(null).
waitForFuture then sees the null and throws the fatal "This indicates a bug".

So the task was just already gone by the time the remove ran. That is a benign 
lost race and it should not kill the
thread.

It is still there on the latest code. I read the source at 4.1.2, 4.2.1, 4.3.0 
and trunk: the throw in waitForFuture
and the future.complete(null) in removeTask are the same, and the 
tasks()/streamOfNonPausedTasks that exposes a
restored task is identical between 4.3.0 and trunk. So upgrading does not help.

h3. How to reproduce

I could not get a single thread schedule to fire it reliably (it is a timing 
race), so I split it into two
deterministic tests, both green on trunk. Patch attached.

* DefaultStateUpdaterTest: real DefaultStateUpdater, let a task finish 
restoring so it lands in restoredActiveTasks,
confirm tasks() still reports it, call drainRestoredActiveTasks(), then 
remove(id).get() comes back null.
* TaskManagerTest: drive the real handleRevocation -> revokeTasksInStateUpdater 
with tasks() reporting the task and
its remove() future completing null (the outcome the first test shows is real), 
and it throws the IllegalStateException
above. The stack trace at the top is the real one from this test.

h3. Fix idea

Treat "task is not in any collection when remove runs" as already removed 
instead of a fatal bug. Either have
remove() finish with an explicit already removed result that 
revokeTasksInStateUpdater / getNonFailedTasks skip, or
let waitForFuture log and skip on a null result for the revoke path instead of 
throwing.

        Summary: Streams: "Task not found in the state updater. This indicates 
a bug." caused by a duplicate TaskId from DefaultStateUpdater.tasks() under 
pause/resume  (was: Streams state updater: remove() during rebalance revocation 
can complete with null and throw a fatal "Task was not found in the state 
updater. This indicates a bug.")

> Streams: "Task not found in the state updater. This indicates a bug." caused 
> by a duplicate TaskId from DefaultStateUpdater.tasks() under pause/resume
> ------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: KAFKA-20721
>                 URL: https://issues.apache.org/jira/browse/KAFKA-20721
>             Project: Kafka
>          Issue Type: Bug
>          Components: streams
>    Affects Versions: 4.3.0, 4.1.2, 4.2.1
>         Environment: Kafka Streams, exactly_once_v2, default state updater, 
> several stateful tasks. Hit during a cold start with a lot of rebalancing. 
> Verified the code at 4.1.2, 4.2.1, 4.3.0 and trunk.
>            Reporter: Eswarar Siva
>            Assignee: Eswarar Siva
>            Priority: Critical
>         Attachments: captured-stacktrace.txt, task-not-found-repro.patch
>
>
> Updated 2026-06-22: the original mechanism in this ticket (a race between 
> drainRestoredActiveTasks and
> revokeTasksInStateUpdater) was wrong and is withdrawn. Both run on the 
> StreamThread and cannot interleave. The
> corrected root cause and a reproduction are below; see the comments for the 
> history.
> A StreamThread dies with:
> java.lang.IllegalStateException: Task X_Y was not found in the state updater. 
> This indicates a bug.
>     at 
> org.apache.kafka.streams.processor.internals.TaskManager.waitForFuture(...)
>     at 
> org.apache.kafka.streams.processor.internals.TaskManager.addToTasksToClose(...)
>     at 
> org.apache.kafka.streams.processor.internals.TaskManager.shutdownStateUpdater(...)
> thrown when DefaultStateUpdater.removeTask completes a removal future with 
> null.
> Root cause: DefaultStateUpdater.tasks() can return the same TaskId twice. 
> tasks() snapshots under
> executeWithQueuesLocked, which holds tasksAndActionsLock, 
> restoredActiveTasksLock and exceptionsAndFailedTasksLock,
> but not the updatingTasks or pausedTasks maps. pauseTask does 
> pausedTasks.put(id) then updatingTasks.remove(id), and
> resumeTask does the reverse, so for a short window a task is in both maps and 
> streamOfTasks() lists it twice.
> ReadOnlyTask has no equals/hashCode, so the returned Set keeps both wrappers. 
> A caller that does one remove per
> tasks() entry (shutdownStateUpdater via addToTasksToClose, and 
> handleAssignment) then removes the same task twice.
> The first remove succeeds; the second reaches removeTask, finds the task in 
> none of the four collections, and
> completes the future with null, so waitForFuture throws the ISE. The same 
> duplicate also breaks
> TaskManager.allTasks(), which uses Collectors.toMap(Task::id, ...) and throws 
> IllegalStateException: Duplicate key,
> killing the StreamThread in runLoop.
> Reproduction: a standalone Streams application, several KafkaStreams 
> instances in one JVM, exactly_once_v2, against a
> real broker, with cold start churn plus KafkaStreams.pause()/resume(). The 
> duplicate window is normally sub
> microsecond, so to make it deterministic a Byteman rule sleeps right after 
> the put in pauseTask and resumeTask (no
> logic change, it only widens the gap that is already there). With that, the 
> ISE and the Duplicate key error both fire
> within a couple of minutes. Reproduced on 4.1.2 and 4.3.0; trunk carries the 
> same code on this path. With pause/resume
> disabled there are no duplicates and no crash, even under aggressive churn. 
> The reproducer, the Byteman rule and DEBUG
> logs (TaskManager, DefaultStateUpdater, StoreChangelogReader) are attached.
> Affected versions: 4.1.2 and 4.3.0 (reproduced); 4.2.x and trunk by code 
> inspection.
> Related: KAFKA-17402 is the same family (a duplicate from tasks(), seen there 
> as shouldGetTasksFromRestoredActiveTasks
> counting 3 instead of 2); its fix made the restore completion transition 
> atomic but did not touch pauseTask/resumeTask,
> which is the path here. The original production occurrence behind this ticket 
> was on 4.1.2 with no pause/resume and very
> large state, which looks closer to KAFKA-20456 (the state updater stalling 
> and waitForFuture timing out, fixed in
> 4.3.0), so the production incident and this pause/resume reproduction may be 
> two different routes to the same exception.
> Suggested fix: stop tasks() from returning a duplicate, for example dedupe by 
> task id before wrapping, or give
> ReadOnlyTask equals/hashCode based on the task id, or make the pause/resume 
> transition atomic with respect to tasks().
> The ISE should stay; it is correctly catching the inconsistency.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to