Li Guo created KAFKA-21035:
------------------------------

             Summary: Utils.atomicMoveWithFallback masks the move failure when 
the parent directory flush also fails
                 Key: KAFKA-21035
                 URL: https://issues.apache.org/jira/browse/KAFKA-21035
             Project: Kafka
          Issue Type: Bug
          Components: clients, core
    Affects Versions: 4.3.1
            Reporter: Li Guo


{{Utils.atomicMoveWithFallback}} fsyncs the parent directory of {{target}} in a 
{{finally}} block. If the move fails and the fsync fails too, the fsync 
exception replaces the move exception with nothing suppressed. Which file 
failed to move, and why, is lost.

{code:java}
// clients/src/main/java/org/apache/kafka/common/utils/Utils.java (logging 
elided)
try {
    Files.move(source, target, ATOMIC_MOVE);
} catch (IOException outer) {
    try {
        Files.move(source, target, REPLACE_EXISTING);
    } catch (IOException inner) {
        inner.addSuppressed(outer);
        throw inner;                       // discarded if flushDir below throws
    }
} finally {
    if (needFlushParentDir) {
        flushDir(target.toAbsolutePath().normalize().getParent());
    }
}
{code}

h3. Reproduce (clients jar only)

Save this as {{Repro.java}} in an extracted Kafka distribution:

{code:java}
import java.nio.file.*;
import org.apache.kafka.common.utils.Utils;

public class Repro {
    public static void main(String[] args) throws Exception {
        Path dir = Files.createTempDirectory("repro");
        try {
            Utils.atomicMoveWithFallback(dir.resolve("missing-src"), 
dir.resolve("missing-dir/target"), true);
        } catch (Exception e) {
            System.out.println("thrown:     " + e);
            System.out.println("suppressed: " + 
java.util.Arrays.toString(e.getSuppressed()));
        }
    }
}
{code}

{noformat}
javac -proc:none -cp "libs/*" Repro.java && java -cp "libs/*:." Repro
{noformat}

Output (4.3.1 and trunk):

{noformat}
thrown:     java.nio.file.NoSuchFileException: 
/tmp/repro1272131299401924814/missing-dir
suppressed: []
{noformat}

Nothing points at {{missing-src}}. Expected:

{noformat}
thrown:     java.nio.file.NoSuchFileException: 
/tmp/repro4632013055863457313/missing-src
suppressed: [java.nio.file.NoSuchFileException: 
/tmp/repro4632013055863457313/missing-src -> 
/tmp/repro4632013055863457313/missing-dir/target, 
java.nio.file.NoSuchFileException: /tmp/repro4632013055863457313/missing-dir]
{noformat}

Primary and first suppressed are the two move failures, exactly as thrown today 
when the fsync succeeds. The second suppressed is the fsync failure.

h3. Effect on a broker

Every caller with the flush enabled is affected: {{CheckpointFile}}, 
{{PartitionMetadataFile}}, {{PropertiesUtils}} (meta.properties), 
{{FileQuorumStateStore}}, {{FileRawSnapshotWriter}}, {{LocalLog.renameDir}}, 
Streams {{OffsetCheckpoint}}. When a log directory fails, the masked exception 
is what {{LogDirFailureChannel}} logs while taking it offline.

Single-node KRaft broker, {{log.dirs=/data/a,/data/b}}, {{metadata.log.dir}} 
outside both. Add these to {{server.properties}} and restart, so the periodic 
checkpoint tasks do not take the directory offline first with a different error:

{noformat}
log.initial.task.delay.ms=86400000
replica.high.watermark.checkpoint.interval.ms=86400000
log.cleaner.backoff.ms=86400000
{noformat}

Then:

{noformat}
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic e2e 
--partitions 1 --replication-factor 1
bin/kafka-producer-perf-test.sh --topic e2e --num-records 200 --record-size 512 
--throughput -1 --producer-props bootstrap.servers=localhost:9092
ls -d /data/*/e2e-0                 # pick the log dir that holds e2e-0, here 
/data/a
mv /data/a /data/a.gone
bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic e2e
{noformat}

Within a second the broker log (trunk) shows:

{noformat}
ERROR Error while renaming dir for e2e-0 in log dir /data/a 
(org.apache.kafka.storage.internals.log.LogDirFailureChannel)
java.nio.file.NoSuchFileException: /data/a
      ...
      at org.apache.kafka.common.utils.Utils.flushDir(Utils.java:969)
      at 
org.apache.kafka.common.utils.Utils.atomicMoveWithFallback(Utils.java:955)
      at 
org.apache.kafka.storage.internals.log.LocalLog.lambda$renameDir$1(LocalLog.java:204)
      ...
{noformat}

No {{Suppressed:}} line, and nothing in the ERROR names the rename of 
{{/data/a/e2e-0}}. The only trace of it is the WARN that {{Utils}} logs for the 
first, atomic attempt. The fallback attempt's failure is gone.

h3. Fix

Catch the flush failure in the {{finally}}. If a move exception is in flight, 
add the flush failure to it as suppressed and rethrow the move exception, as 
{{Utils.closeAll}} does. Otherwise rethrow the flush failure.



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

Reply via email to