Skip to content

ExitStatus#setExitException breaks immutability contract #5366

@config25

Description

@config25

Bug description

ExitStatus class Javadoc declares the class as immutable and thread-safe:

▎ ExitStatus is immutable and, therefore, thread-safe.

However, setExitException(Throwable) (introduced in 6.0.3 for #5336) mutates the object in place, breaking this contract. Since ExitStatus.FAILED, ExitStatus.COMPLETED, etc. are shared static constants, calling
setExitException() on them permanently corrupts the global state for the entire application.

Environment

  • Spring Batch: 6.0.4-SNAPSHOT (also affects 6.0.3)
  • Java: 17+

Steps to reproduce

@test
void globalConstantCorruption() {
// precondition: FAILED has no exception
assertNull(ExitStatus.FAILED.getExitException());

  // mutate the shared constant
  ExitStatus.FAILED.setExitException(new RuntimeException("oops"));

  // the global constant is now permanently corrupted
  assertNotNull(ExitStatus.FAILED.getExitException()); // passes — should not be possible

}

Expected behavior

ExitStatus should be truly immutable. setExitException() should return a new instance, consistent with addExitDescription() and replaceExitCode() which already follow this pattern. The exitException field should be final.

Minimal Complete Reproducible example

The test above is self-contained — no database or Spring context needed. The root cause is in ExitStatus.java:

// line 76: non-final mutable field
@nullable private Throwable exitException;

// line 294-296: mutates this instead of returning a new instance
public ExitStatus setExitException(Throwable exitException) {
this.exitException = exitException;
return this;
}

A possible fix would be:

private final @nullable Throwable exitException;

public ExitStatus withExitException(Throwable exitException) {
return new ExitStatus(this.exitCode, this.exitDescription, exitException);
}

Note on backward compatibility: setExitException was introduced in 6.0.3.
If 6.0.3 is already released, deprecating it in favor of a new withExitException
method would preserve backward compatibility. If not yet released, a direct
replacement would be possible without breaking changes.


Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions