{"record":{"id":"d97b89bdea48f106","repo":"apache/pulsar","slug":"there-should-not-be-multi-ledger-in-a-block-s-s","errorCode":null,"errorMessage":"there should not be multi ledger in a block %s %s","messagePattern":"there should not be multi ledger in a block (.+?) (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"critical","filePath":"tiered-storage/jcloud/src/main/java/org/apache/bookkeeper/mledger/offload/jcloud/impl/BufferedOffloadStream.java","lineNumber":106,"sourceCode":"        }\n\n        if (blockSize <= offset) {\n            return -1;\n        } else if (validDataOffset != NOT_INITIALIZED) {\n            return BLOCK_END_PADDING[(offset++ - validDataOffset) % BLOCK_END_PADDING.length];\n        }\n\n\n        if (entryBuffer.isEmpty()) {\n            validDataOffset = offset;\n            return read();\n        }\n\n        Entry headEntry = entryBuffer.remove(0);\n\n        //create new block when a ledger end\n        if (headEntry.getLedgerId() != this.ledgerId) {\n            throw new RuntimeException(\n                    String.format(\"there should not be multi ledger in a block %s %s\", headEntry.getLedgerId(),\n                            this.ledgerId));\n        }\n\n        final int entryLength = headEntry.getLength();\n        final long entryId = headEntry.getEntryId();\n        CompositeByteBuf entryBuf = PulsarByteBufAllocator.DEFAULT.compositeBuffer(2);\n        ByteBuf entryHeaderBuf = PulsarByteBufAllocator.DEFAULT.buffer(ENTRY_HEADER_SIZE, ENTRY_HEADER_SIZE);\n        entryHeaderBuf.writeInt(entryLength).writeLong(entryId);\n        entryBuf.addComponents(true, entryHeaderBuf, headEntry.getDataBuffer().retain());\n        endEntryId = headEntry.getEntryId();\n        headEntry.release();\n        currentEntry = entryBuf;\n        return read();\n\n    }\n\n    @Override","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/tiered-storage/jcloud/src/main/java/org/apache/bookkeeper/mledger/offload/jcloud/impl/BufferedOffloadStream.java#L88-L124","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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)","Verify offload threshold/segment logic so the block ends exactly at each ledger boundary","Re-run offload for the affected segment after fixing the boundary logic","If it occurs after a broker/offloader upgrade, check for changes in entry-buffering behavior and align versions"],"exampleFix":"// before: keep feeding across ledger boundary\nstream.feed(nextLedgerEntries); // RuntimeException\n// after\nif (nextEntry.getLedgerId() != currentLedgerId) {\n  stream.finishBlock(); // flush + close current block\n}\nstream.feed(nextEntry);","handlingStrategy":"validation","validationCode":"// before feeding entries to BufferedOffloadStream\nif (!entryBuffer.isEmpty()\n    && entryBuffer.get(0).getLedgerId() != pendingEntry.getLedgerId()) {\n  stream.finishBlock(); // end block at ledger boundary\n}","typeGuard":null,"tryCatchPattern":"try {\n  int n = stream.read(buf);\n} catch (RuntimeException e) {\n  if (e.getMessage() != null && e.getMessage().startsWith(\"there should not be multi ledger in a block\")) {\n    log.error(\"offload block spans ledgers — fix boundary logic and re-offload segment\", e);\n    // do not retry: offloaded block is malformed\n  } else throw e;\n}","preventionTips":["Always close/flush the current block when the ledger changes","Keep offload threshold logic aligned with ledger rollover points","Add assertions when filling the entry buffer that all ledgerIds match","Test offloads that cross ledger boundaries before upgrading the offloader"],"tags":["tiered-storage","bookkeeper","offload","invariant-violation"],"backgroundTag":"multi-ledger-block-violation","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}