apache/pulsar · critical · RuntimeException

there should not be multi ledger in a block %s %s

Error message

there should not be multi ledger in a block %s %s

What it means

BufferedOffloadStream buffers entries of exactly one BookKeeper ledger per offload block. When it pops the next head entry, if the entry's ledgerId differs from the stream's current ledgerId, it throws a RuntimeException because a single block must never span multiple ledgers.

Source

Thrown at tiered-storage/jcloud/src/main/java/org/apache/bookkeeper/mledger/offload/jcloud/impl/BufferedOffloadStream.java:106

        }

        if (blockSize <= offset) {
            return -1;
        } else if (validDataOffset != NOT_INITIALIZED) {
            return BLOCK_END_PADDING[(offset++ - validDataOffset) % BLOCK_END_PADDING.length];
        }


        if (entryBuffer.isEmpty()) {
            validDataOffset = offset;
            return read();
        }

        Entry headEntry = entryBuffer.remove(0);

        //create new block when a ledger end
        if (headEntry.getLedgerId() != this.ledgerId) {
            throw new RuntimeException(
                    String.format("there should not be multi ledger in a block %s %s", headEntry.getLedgerId(),
                            this.ledgerId));
        }

        final int entryLength = headEntry.getLength();
        final long entryId = headEntry.getEntryId();
        CompositeByteBuf entryBuf = PulsarByteBufAllocator.DEFAULT.compositeBuffer(2);
        ByteBuf entryHeaderBuf = PulsarByteBufAllocator.DEFAULT.buffer(ENTRY_HEADER_SIZE, ENTRY_HEADER_SIZE);
        entryHeaderBuf.writeInt(entryLength).writeLong(entryId);
        entryBuf.addComponents(true, entryHeaderBuf, headEntry.getDataBuffer().retain());
        endEntryId = headEntry.getEntryId();
        headEntry.release();
        currentEntry = entryBuf;
        return read();

    }

    @Override

View on GitHub (pinned to 820761864e)

Solutions

  1. Feed BufferedOffloadStream entries from a single ledger per segment; stop and flush the current block before switching ledgers (ensure the offloader closes the block at ledger end)
  2. Verify offload threshold/segment logic so the block ends exactly at each ledger boundary
  3. Re-run offload for the affected segment after fixing the boundary logic
  4. If it occurs after a broker/offloader upgrade, check for changes in entry-buffering behavior and align versions

Example fix

// before: keep feeding across ledger boundary
stream.feed(nextLedgerEntries); // RuntimeException
// after
if (nextEntry.getLedgerId() != currentLedgerId) {
  stream.finishBlock(); // flush + close current block
}
stream.feed(nextEntry);
Defensive patterns

Strategy: validation

Validate before calling

// before feeding entries to BufferedOffloadStream
if (!entryBuffer.isEmpty()
    && entryBuffer.get(0).getLedgerId() != pendingEntry.getLedgerId()) {
  stream.finishBlock(); // end block at ledger boundary
}

Try / catch

try {
  int n = stream.read(buf);
} catch (RuntimeException e) {
  if (e.getMessage() != null && e.getMessage().startsWith("there should not be multi ledger in a block")) {
    log.error("offload block spans ledgers — fix boundary logic and re-offload segment", e);
    // do not retry: offloaded block is malformed
  } else throw e;
}

Prevention

When it happens

Trigger: read() pops entryBuffer head and headEntry.getLedgerId() != this.ledgerId — i.e. the buffered entries fed into the stream mix entries from more than one ledger within one block.

Common situations: Offload boundary logic feeding the stream with entries from the next ledger without closing/rotating the current block; offloading across a ledger rollover that was previously cut at the wrong point; version/behavior change in how segment boundaries are computed.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/d97b89bdea48f106. Report an issue: GitHub.