This is an automated email from the ASF dual-hosted git repository.
merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git
The following commit(s) were added to refs/heads/master by this push:
new d476a7b9e2 Write journal entry length prefix and payload in a single
BufferedChannel write (#4833)
d476a7b9e2 is described below
commit d476a7b9e2adf317fee614913740b3677ae295df
Author: Matteo Merli <[email protected]>
AuthorDate: Tue Jul 14 08:25:47 2026 -0700
Write journal entry length prefix and payload in a single BufferedChannel
write (#4833)
---
.../apache/bookkeeper/bookie/BufferedChannel.java | 73 ++++++++++++++++------
.../java/org/apache/bookkeeper/bookie/Journal.java | 3 +-
.../bookkeeper/bookie/SlowBufferedChannel.java | 6 ++
.../bookkeeper/bookie/BufferedChannelTest.java | 40 ++++++++++++
4 files changed, 100 insertions(+), 22 deletions(-)
diff --git
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BufferedChannel.java
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BufferedChannel.java
index dbba31083d..cddeff9cf2 100644
---
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BufferedChannel.java
+++
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BufferedChannel.java
@@ -115,35 +115,68 @@ public class BufferedChannel extends BufferedReadChannel
implements Closeable {
* @throws IOException if a write operation fails.
*/
public void write(ByteBuf src) throws IOException {
- int copied = 0;
boolean shouldForceWrite = false;
synchronized (this) {
- int len = src.readableBytes();
- while (copied < len) {
- int bytesToCopy = Math.min(src.readableBytes() - copied,
writeBuffer.writableBytes());
- writeBuffer.writeBytes(src, src.readerIndex() + copied,
bytesToCopy);
- copied += bytesToCopy;
+ int copied = copyIntoWriteBuffer(src);
+ shouldForceWrite = updatePositionAndFlushIfNeeded(copied);
+ }
+ if (shouldForceWrite) {
+ forceWrite(false);
+ }
+ }
- // if we have run out of buffer space, we should flush to the
- // file
- if (!writeBuffer.isWritable()) {
- flush();
- }
- }
- position += copied;
- if (doRegularFlushes) {
- unpersistedBytes.addAndGet(copied);
- if (unpersistedBytes.get() >= unpersistedBytesBound) {
- flush();
- shouldForceWrite = true;
- }
- }
+ /**
+ * Write all the data in src1 and src2, in order, to the {@link
FileChannel}, taking the
+ * lock only once. This avoids the overhead of two lock acquisitions when
writing an
+ * entry preceded by its length prefix.
+ *
+ * @param src1 The first source buffer which contains the data to be
written.
+ * @param src2 The second source buffer which contains the data to be
written.
+ * @throws IOException if a write operation fails.
+ */
+ public void write(ByteBuf src1, ByteBuf src2) throws IOException {
+ boolean shouldForceWrite = false;
+ synchronized (this) {
+ int copied = copyIntoWriteBuffer(src1);
+ copied += copyIntoWriteBuffer(src2);
+ shouldForceWrite = updatePositionAndFlushIfNeeded(copied);
}
if (shouldForceWrite) {
forceWrite(false);
}
}
+ // must be called while holding the lock on this instance
+ private int copyIntoWriteBuffer(ByteBuf src) throws IOException {
+ int copied = 0;
+ int len = src.readableBytes();
+ while (copied < len) {
+ int bytesToCopy = Math.min(src.readableBytes() - copied,
writeBuffer.writableBytes());
+ writeBuffer.writeBytes(src, src.readerIndex() + copied,
bytesToCopy);
+ copied += bytesToCopy;
+
+ // if we have run out of buffer space, we should flush to the
+ // file
+ if (!writeBuffer.isWritable()) {
+ flush();
+ }
+ }
+ return copied;
+ }
+
+ // must be called while holding the lock on this instance
+ private boolean updatePositionAndFlushIfNeeded(int copied) throws
IOException {
+ position += copied;
+ if (doRegularFlushes) {
+ unpersistedBytes.addAndGet(copied);
+ if (unpersistedBytes.get() >= unpersistedBytesBound) {
+ flush();
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Get the position where the next write operation will begin writing from.
* @return
diff --git
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Journal.java
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Journal.java
index 830763aa02..ad637f0156 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Journal.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Journal.java
@@ -1232,8 +1232,7 @@ public class Journal implements CheckpointSource {
// preAlloc based on size
logFile.preAllocIfNeeded(4 + entrySize);
- bc.write(lenBuff);
- bc.write(qe.entry);
+ bc.write(lenBuff, qe.entry);
memoryLimitController.releaseMemory(qe.entry.readableBytes());
ReferenceCountUtil.release(qe.entry);
}
diff --git
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SlowBufferedChannel.java
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SlowBufferedChannel.java
index 13bc74e0c1..b70fc4d05b 100644
---
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SlowBufferedChannel.java
+++
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SlowBufferedChannel.java
@@ -63,6 +63,12 @@ public class SlowBufferedChannel extends BufferedChannel {
super.write(src);
}
+ @Override
+ public synchronized void write(ByteBuf src1, ByteBuf src2) throws
IOException {
+ delayMs(addDelay);
+ super.write(src1, src2);
+ }
+
@Override
public void flush() throws IOException {
delayMs(flushDelay);
diff --git
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BufferedChannelTest.java
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BufferedChannelTest.java
index 81e7c62af4..0114dbc125 100644
---
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BufferedChannelTest.java
+++
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BufferedChannelTest.java
@@ -24,12 +24,14 @@ package org.apache.bookkeeper.bookie;
import static org.junit.Assert.assertThrows;
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledByteBufAllocator;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
+import java.nio.file.Files;
import java.util.Random;
import org.junit.Assert;
import org.junit.Test;
@@ -129,6 +131,44 @@ public class BufferedChannelTest {
fileChannel.close();
}
+ @Test
+ public void testWriteTwoBuffers() throws Exception {
+ File newLogFile = File.createTempFile("test", "log");
+ newLogFile.deleteOnExit();
+ FileChannel fileChannel = new RandomAccessFile(newLogFile,
"rw").getChannel();
+
+ // small write capacity so the pairs below cross the buffer boundary
at varied offsets,
+ // including a payload equal to the capacity and one larger than it
+ int writeCapacity = 64;
+ BufferedChannel logChannel = new
BufferedChannel(UnpooledByteBufAllocator.DEFAULT, fileChannel,
+ writeCapacity, INTERNAL_BUFFER_READ_CAPACITY, 0);
+
+ int[] payloadSizes = { 25, 31, 60, 3, 64, 128, 1, 41 };
+ ByteBuf expected = Unpooled.buffer();
+ ByteBuf lenBuf = Unpooled.buffer(4);
+
+ for (int payloadSize : payloadSizes) {
+ ByteBuf payload = generateEntry(payloadSize);
+ lenBuf.clear();
+ lenBuf.writeInt(payloadSize);
+
+ expected.writeBytes(lenBuf.slice());
+ expected.writeBytes(payload.slice());
+
+ logChannel.write(lenBuf, payload);
+ }
+
+ Assert.assertEquals(expected.readableBytes(), logChannel.position());
+
+ logChannel.flush();
+
+ byte[] fileBytes = Files.readAllBytes(newLogFile.toPath());
+ Assert.assertArrayEquals(ByteBufUtil.getBytes(expected), fileBytes);
+
+ logChannel.close();
+ fileChannel.close();
+ }
+
@Test
public void testBufferedChannelReadWhenDestBufSizeExceedsReadLength()
throws IOException {
doTestBufferedChannelReadThrowing(100, 60);