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

Siyao Meng updated HDDS-16456:
------------------------------
    Description: 
h3. Finding
A non-atomic timeout re-arm on the write RPC tail resurrects a 
recoveringContainerMap entry for a container that concurrently moved to 
CLOSING/CLOSED; the scrub then marks that valid replica UNHEALTHY, driving SCM 
re-replication of a good replica. Triggered by a concurrent write RPC and 
CloseContainerCommand on a RECOVERING EC target; same bounded, self-healing 
external effect as the stale-scrub demotion.

h3. Classification
* Verdict: REPRODUCED
* Severity: High
* Source: Specula TLA+ model checking and confirmation debate, finding MC-2

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       dn-ec-reconstruction
Guidance:     
campaigns/ozone-9fbf9ee/targets/022-dn-ec-reconstruction/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=high --keep-original --max-parallel=2 \
  --enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8 
\
  "dn-ec-reconstruction|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16433 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).

h3. Steps to reproduce

1. An EC reconstruction target replica is created RECOVERING; {{addContainer}} 
records a recovering-map entry (deadline = now + recovering timeout).
2. A delayed or retried PutBlock/WriteChunk RPC to that replica reaches the 
tail of {{handlePutBlock}}/{{handleWriteChunk}}, which calls the private 
{{KeyValueHandler.updateRecoveringContainerTimeout}}. That method reads the 
container state ({{!= RECOVERING}} guard) and then calls 
{{ContainerSet.updateRecoveringContainerTimeout}} (an unconditional 
{{recoveringContainerMap.put}}), all without holding the container write lock.
3. Concurrently an SCM CloseContainerCommand drives 
{{KeyValueHandler.markContainerForClose}}, which does hold the container write 
lock: it removes the recovering-map entry and moves the container RECOVERING to 
CLOSING.
4. The close executes fully between the write tail's state read and its put, so 
the put re-inserts a stale ("zombie") recovering-map entry for a container that 
is now CLOSING/CLOSED. The stale-recovering scrub later marks that valid 
replica UNHEALTHY (a second route into the close-race), driving SCM to 
re-replicate a good replica.

h3. Root cause

{{KeyValueHandler.updateRecoveringContainerTimeout}} performs a check-then-put 
({{getContainerState()}} then 
{{ContainerSet.updateRecoveringContainerTimeout}}) that is not atomic with 
{{markContainerForClose}}. {{markContainerForClose}} takes the container write 
lock while it removes the recovering-map entry and changes state, but the 
write-tail update takes no lock, so the two can interleave and the put outlives 
the close.

h3. Suggested fix

Attached patch [^HDDS-16456.001.patch] wraps the state check and the map update 
in {{updateRecoveringContainerTimeout}} in the container write lock, mirroring 
the lock discipline of {{markContainerForClose}}. The two operations are now 
mutually exclusive: either the update runs first (puts the entry, which the 
close then removes) or the close runs first (the update observes a 
non-RECOVERING state and skips the put). Either way no stale entry survives. A 
unit test drives a close holding the write lock and asserts the write-tail 
update blocks on that lock and leaves no recovering-map entry once the 
container is CLOSING.

Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
test, and review before merge.


  was:
h3. Finding
A non-atomic timeout re-arm on the write RPC tail resurrects a 
recoveringContainerMap entry for a container that concurrently moved to 
CLOSING/CLOSED; the scrub then marks that valid replica UNHEALTHY, driving SCM 
re-replication of a good replica. Triggered by a concurrent write RPC and 
CloseContainerCommand on a RECOVERING EC target; same bounded, self-healing 
external effect as the stale-scrub demotion.

h3. Classification
* Verdict: REPRODUCED
* Severity: High
* Source: Specula TLA+ model checking and confirmation debate, finding MC-2

h3. Reproduce
{noformat}
Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
Specula:      v1.1.0 (commit c6aa3dfa)
Target:       dn-ec-reconstruction
Guidance:     
campaigns/ozone-9fbf9ee/targets/022-dn-ec-reconstruction/.prompt-extra.md
{noformat}
{code:none}
specula run --agent=claude-code --effort=high --keep-original --max-parallel=2 \
  --enable-reviews --confirm-debate --tlc-memory-limit=28G --tlc-worker-limit=8 
\
  "dn-ec-reconstruction|apache/ozone|Java|Use the target-specific 
.prompt-extra.md"
{code}
Discovered under HDDS-16433 (Specula TLA+ verification effort). The TLA+ 
specification, counterexample, and confirmation debate live in the Specula run 
artifacts.

Generated with Specula (Claude Opus 4.8).


> Non atomic timeout re arm resurrects a recovering container entry and marks a 
> valid EC replica UNHEALTHY
> --------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16456
>                 URL: https://issues.apache.org/jira/browse/HDDS-16456
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: HDDS-16456.001.patch
>
>
> h3. Finding
> A non-atomic timeout re-arm on the write RPC tail resurrects a 
> recoveringContainerMap entry for a container that concurrently moved to 
> CLOSING/CLOSED; the scrub then marks that valid replica UNHEALTHY, driving 
> SCM re-replication of a good replica. Triggered by a concurrent write RPC and 
> CloseContainerCommand on a RECOVERING EC target; same bounded, self-healing 
> external effect as the stale-scrub demotion.
> h3. Classification
> * Verdict: REPRODUCED
> * Severity: High
> * Source: Specula TLA+ model checking and confirmation debate, finding MC-2
> h3. Reproduce
> {noformat}
> Ozone commit: 9fbf9ee0cb1bd2f5f5d437b6719ebbe5309351fb
> Specula:      v1.1.0 (commit c6aa3dfa)
> Target:       dn-ec-reconstruction
> Guidance:     
> campaigns/ozone-9fbf9ee/targets/022-dn-ec-reconstruction/.prompt-extra.md
> {noformat}
> {code:none}
> specula run --agent=claude-code --effort=high --keep-original 
> --max-parallel=2 \
>   --enable-reviews --confirm-debate --tlc-memory-limit=28G 
> --tlc-worker-limit=8 \
>   "dn-ec-reconstruction|apache/ozone|Java|Use the target-specific 
> .prompt-extra.md"
> {code}
> Discovered under HDDS-16433 (Specula TLA+ verification effort). The TLA+ 
> specification, counterexample, and confirmation debate live in the Specula 
> run artifacts.
> Generated with Specula (Claude Opus 4.8).
> h3. Steps to reproduce
> 1. An EC reconstruction target replica is created RECOVERING; 
> {{addContainer}} records a recovering-map entry (deadline = now + recovering 
> timeout).
> 2. A delayed or retried PutBlock/WriteChunk RPC to that replica reaches the 
> tail of {{handlePutBlock}}/{{handleWriteChunk}}, which calls the private 
> {{KeyValueHandler.updateRecoveringContainerTimeout}}. That method reads the 
> container state ({{!= RECOVERING}} guard) and then calls 
> {{ContainerSet.updateRecoveringContainerTimeout}} (an unconditional 
> {{recoveringContainerMap.put}}), all without holding the container write lock.
> 3. Concurrently an SCM CloseContainerCommand drives 
> {{KeyValueHandler.markContainerForClose}}, which does hold the container 
> write lock: it removes the recovering-map entry and moves the container 
> RECOVERING to CLOSING.
> 4. The close executes fully between the write tail's state read and its put, 
> so the put re-inserts a stale ("zombie") recovering-map entry for a container 
> that is now CLOSING/CLOSED. The stale-recovering scrub later marks that valid 
> replica UNHEALTHY (a second route into the close-race), driving SCM to 
> re-replicate a good replica.
> h3. Root cause
> {{KeyValueHandler.updateRecoveringContainerTimeout}} performs a 
> check-then-put ({{getContainerState()}} then 
> {{ContainerSet.updateRecoveringContainerTimeout}}) that is not atomic with 
> {{markContainerForClose}}. {{markContainerForClose}} takes the container 
> write lock while it removes the recovering-map entry and changes state, but 
> the write-tail update takes no lock, so the two can interleave and the put 
> outlives the close.
> h3. Suggested fix
> Attached patch [^HDDS-16456.001.patch] wraps the state check and the map 
> update in {{updateRecoveringContainerTimeout}} in the container write lock, 
> mirroring the lock discipline of {{markContainerForClose}}. The two 
> operations are now mutually exclusive: either the update runs first (puts the 
> entry, which the close then removes) or the close runs first (the update 
> observes a non-RECOVERING state and skips the put). Either way no stale entry 
> survives. A unit test drives a close holding the write lock and asserts the 
> write-tail update blocks on that lock and leaves no recovering-map entry once 
> the container is CLOSING.
> Patch is an AI-drafted proposal (Specula + Claude); pending human build, full 
> test, and review before merge.



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

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to