apache/pulsar · error · IllegalArgumentException
Positions must not be null
Error message
Positions must not be null
What it means
ManagedLedgerImpl.comparePositions() is a null-strict position comparator used by the entries-count/range APIs. It throws IllegalArgumentException when either Position argument is null. Unlike Position.compareTo, it validates inputs before comparing, since null positions would NPE deeper in the logic anyway.
Source
Thrown at managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerImpl.java:4020
.whenComplete((result, exception) -> {
if (exception == null) {
log.info().attr("ledgerId", ledgerId).attr("uuid", uuid).log("End Offload");
} else {
log.warn().attr("ledgerId", ledgerId)
.attr("uuid", uuid)
.exception(exception).log("Failed to complete offload");
}
});
}
/**
* Compare two positions. It is different with {@link Position#compareTo(Position)} when the params are invalid.
* For example: position-1 is "1:{latest entry}", and position-2 is "2:-1", they are the same position.
*/
@VisibleForTesting
int comparePositions(Position pos1, Position pos2) {
if (pos1 == null || pos2 == null) {
throw new IllegalArgumentException("Positions must not be null");
}
if (ledgers.isEmpty() || pos1.getLedgerId() < getFirstPosition().getLedgerId()
|| pos2.getLedgerId() < getFirstPosition().getLedgerId()
|| pos1.getLedgerId() > getLastPosition().getLedgerId()
|| pos2.getLedgerId() > getLastPosition().getLedgerId()) {
return pos1.compareTo(pos2);
}
if (pos1.getLedgerId() == pos2.getLedgerId()) {
return Long.compare(pos1.getEntryId(), pos2.getEntryId());
}
if (!isValidPosition(pos1) || !isValidPosition(pos2)) {
return getNextValidPosition(pos1).compareTo(getNextValidPosition(pos2));
}
return pos1.compareTo(pos2);
}
/**
* Get the number of entries between a contiguous range of two positions.View on GitHub (pinned to 820761864e)
Solutions
- Null-check positions before invoking the range API and skip/return 0 entries for null input
- For a fresh cursor, initialize or wait until markDeletePosition/readPosition is established
- Fix caller code that produces null Positions (uninitialized fields, failed deserialization)
Example fix
// before
long n = ml.getNumberOfEntries(Range.open(fromPos, toPos)); // NPE risk
// after
long n = (fromPos == null || toPos == null) ? 0
: ml.getNumberOfEntries(Range.open(fromPos, toPos)); Defensive patterns
Strategy: validation
Validate before calling
if (fromPos == null || toPos == null) {
return 0L; // or skip the computation
} Type guard
static boolean validPositions(Position a, Position b) {
return a != null && b != null;
} Try / catch
try {
return ml.getNumberOfEntries(range);
} catch (IllegalArgumentException e) {
log.warn("null/invalid positions for entry count", e);
return 0L;
} Prevention
- Null-check positions before range APIs
- Handle fresh cursors with uninitialized positions
- Treat null positions as 'no entries' rather than propagating
- Validate deserialized Position objects
When it happens
Trigger: Calling getNumberOfEntries(Range<Position>) or related range APIs with a null from/to position — typically a cursor with no mark-delete position yet, or caller code passing an uninitialized Position.
Common situations: Newly created subscription whose read position has never been initialized; application code computing stats ranges before a first consume; deserialization producing null positions.
Related errors
- Invalid range ${range}
- v4 authentication must not be null
- tlsPolicy purpose and policy must not be null
- tlsFactory must not be null
- Timeout during managed ledger close
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/76deef911a0e2a12.
Report an issue: GitHub.