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.
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
- tlsFactory must not be null
- tlsPolicy purpose and policy must not be null
- v4 authentication must not be null
- At least one of maxNumMessages, maxNumBytes, timeout must…
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/76deef911a0e2a12.
Report an issue: GitHub.
Appendix: 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)