Skip to content

Commit 8d1f62b

Browse files
committed
feat(libhdfs): add direct ByteBuffer write path
1 parent fdf765b commit 8d1f62b

13 files changed

Lines changed: 518 additions & 9 deletions

File tree

hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockOutputStream.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,45 @@ public void write(byte[] b, int off, int len) throws IOException {
394394
}
395395
}
396396

397+
/**
398+
* Writes {@code len} bytes starting at absolute position {@code off} of the
399+
* given {@link ByteBuffer}. This mirrors {@link #write(byte[], int, int)} but
400+
* feeds the buffer straight into the pooled {@link ChunkBuffer} through
401+
* {@link ChunkBuffer#put(ByteBuffer)}, avoiding an intermediate byte[] copy.
402+
* The caller's buffer position and limit are left unchanged.
403+
*/
404+
public void write(ByteBuffer b, int off, int len) throws IOException {
405+
checkOpen();
406+
if (b == null) {
407+
throw new NullPointerException();
408+
}
409+
if ((off < 0) || (off > b.capacity()) || (len < 0)
410+
|| ((off + len) > b.capacity()) || ((off + len) < 0)) {
411+
throw new IndexOutOfBoundsException("Offset=" + off + " and len=" + len
412+
+ " don't match the buffer capacity of " + b.capacity());
413+
}
414+
if (len == 0) {
415+
return;
416+
}
417+
synchronized (this) {
418+
final ByteBuffer src = b.duplicate();
419+
src.clear();
420+
src.position(off).limit(off + len);
421+
while (src.hasRemaining()) {
422+
allocateNewBufferIfNeeded();
423+
final int writeLen = Math.min(currentBufferRemaining, src.remaining());
424+
final int srcLimit = src.limit();
425+
src.limit(src.position() + writeLen);
426+
currentBuffer.put(src);
427+
src.limit(srcLimit);
428+
currentBufferRemaining -= writeLen;
429+
updateWrittenDataLength(writeLen);
430+
writeChunkIfNeeded();
431+
doFlushOrWatchIfNeeded();
432+
}
433+
}
434+
}
435+
397436
protected synchronized void updateWrittenDataLength(int writeLen) {
398437
writtenDataLength += writeLen;
399438
}

hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ByteArrayStreamOutput.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ public void write(ByteBuffer buffer, int off, int len) throws IOException {
4040
}
4141

4242
if (buffer.hasArray()) {
43-
write(buffer.array(), off, len);
43+
// off is a logical position within the buffer; translate it to an index
44+
// into the backing array (arrayOffset is non-zero e.g. for sliced buffers).
45+
write(buffer.array(), buffer.arrayOffset() + off, len);
4446
} else {
4547
final byte[] array = new byte[Math.min(ARRAY_SIZE_LIMIT, len)];
4648
for (; len > 0;) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.ozone.client.io;
19+
20+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
import java.io.ByteArrayOutputStream;
24+
import java.nio.ByteBuffer;
25+
import java.util.Arrays;
26+
import org.junit.jupiter.api.Test;
27+
28+
/**
29+
* Tests {@link ByteArrayStreamOutput#write(ByteBuffer, int, int)}, in
30+
* particular that a logical {@code off} is translated correctly for heap
31+
* buffers with a non-zero {@link ByteBuffer#arrayOffset()} (e.g. slices).
32+
*/
33+
public class TestByteArrayStreamOutput {
34+
35+
private static final byte[] DATA = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
36+
37+
/** Collects everything written through the byte[] sink. */
38+
private static final class Collector extends ByteArrayStreamOutput {
39+
private final ByteArrayOutputStream sink = new ByteArrayOutputStream();
40+
41+
@Override
42+
public void write(byte[] b, int off, int len) {
43+
sink.write(b, off, len);
44+
}
45+
46+
@Override
47+
public void flush() {
48+
}
49+
50+
@Override
51+
public void hflush() {
52+
}
53+
54+
@Override
55+
public void hsync() {
56+
}
57+
58+
byte[] written() {
59+
return sink.toByteArray();
60+
}
61+
}
62+
63+
private static void writeRemaining(Collector c, ByteBuffer buf)
64+
throws java.io.IOException {
65+
c.write(buf, buf.position(), buf.remaining());
66+
}
67+
68+
@Test
69+
public void writesSlicedHeapBufferFromCorrectOffset() throws Exception {
70+
ByteBuffer base = ByteBuffer.allocate(DATA.length);
71+
base.put(DATA);
72+
base.position(3);
73+
ByteBuffer slice = base.slice(); // arrayOffset() == 3, content = DATA[3..]
74+
assertEquals(3, slice.arrayOffset());
75+
76+
Collector c = new Collector();
77+
writeRemaining(c, slice);
78+
79+
assertArrayEquals(Arrays.copyOfRange(DATA, 3, DATA.length), c.written());
80+
}
81+
82+
@Test
83+
public void writesHeapBufferFromLogicalPosition() throws Exception {
84+
ByteBuffer buf = ByteBuffer.wrap(DATA.clone());
85+
buf.position(4).limit(9);
86+
87+
Collector c = new Collector();
88+
writeRemaining(c, buf);
89+
90+
assertArrayEquals(Arrays.copyOfRange(DATA, 4, 9), c.written());
91+
}
92+
93+
@Test
94+
public void writesDirectBuffer() throws Exception {
95+
ByteBuffer buf = ByteBuffer.allocateDirect(DATA.length);
96+
buf.put(DATA);
97+
buf.position(2).limit(8);
98+
99+
Collector c = new Collector();
100+
writeRemaining(c, buf);
101+
102+
assertArrayEquals(Arrays.copyOfRange(DATA, 2, 8), c.written());
103+
}
104+
}

hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/BlockOutputStreamEntry.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.common.annotations.VisibleForTesting;
2121
import java.io.IOException;
2222
import java.io.OutputStream;
23+
import java.nio.ByteBuffer;
2324
import java.util.Collection;
2425
import java.util.Collections;
2526
import java.util.concurrent.ExecutorService;
@@ -184,6 +185,12 @@ public void write(byte[] b, int off, int len) throws IOException {
184185
incCurrentPosition(len);
185186
}
186187

188+
void write(ByteBuffer b, int off, int len) throws IOException {
189+
checkStream();
190+
outputStream.write(b, off, len);
191+
incCurrentPosition(len);
192+
}
193+
187194
void writeOnRetry(long len) throws IOException {
188195
checkStream();
189196
BlockOutputStream out = (BlockOutputStream) getOutputStream();

hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/ECKeyOutputStream.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.stream.IntStream;
3636
import org.apache.commons.lang3.NotImplementedException;
3737
import org.apache.hadoop.fs.FSExceptionMessages;
38+
import org.apache.hadoop.fs.StreamCapabilities;
3839
import org.apache.hadoop.hdds.client.ECReplicationConfig;
3940
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
4041
import org.apache.hadoop.hdds.scm.client.HddsClientUtils;
@@ -158,6 +159,28 @@ public void write(byte[] b, int off, int len) throws IOException {
158159
writeOffset += len;
159160
}
160161

162+
/**
163+
* EC striping does not support the direct ByteBuffer write path; drain the
164+
* buffer through the byte[] path. Also see {@link #hasCapability(String)},
165+
* which refuses {@code WRITEBYTEBUFFER} so a wrapping CryptoOutputStream never
166+
* forwards a ciphertext buffer here.
167+
*/
168+
@Override
169+
public void write(ByteBuffer buf) throws IOException {
170+
final int len = buf.remaining();
171+
final byte[] tmp = new byte[len];
172+
buf.get(tmp);
173+
write(tmp, 0, len);
174+
}
175+
176+
@Override
177+
public boolean hasCapability(String capability) {
178+
if (StreamCapabilities.WRITEBYTEBUFFER.equalsIgnoreCase(capability)) {
179+
return false;
180+
}
181+
return super.hasCapability(capability);
182+
}
183+
161184
private void rollbackAndReset(ECChunkBuffers stripe) throws IOException {
162185
// Rollback the length/offset updated as part of this failed stripe write.
163186
final ByteBuffer[] dataBuffers = stripe.getDataBuffers();

hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/KeyOutputStream.java

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.IOException;
2323
import java.io.InterruptedIOException;
2424
import java.io.OutputStream;
25+
import java.nio.ByteBuffer;
2526
import java.util.Collection;
2627
import java.util.List;
2728
import java.util.Map;
@@ -33,7 +34,9 @@
3334
import java.util.function.Function;
3435
import java.util.function.Supplier;
3536
import java.util.stream.Collectors;
37+
import org.apache.hadoop.fs.ByteBufferWritable;
3638
import org.apache.hadoop.fs.FSExceptionMessages;
39+
import org.apache.hadoop.fs.StreamCapabilities;
3740
import org.apache.hadoop.fs.Syncable;
3841
import org.apache.hadoop.hdds.client.ReplicationConfig;
3942
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
@@ -73,7 +76,7 @@
7376
* TODO : currently not support multi-thread access.
7477
*/
7578
public class KeyOutputStream extends OutputStream
76-
implements Syncable, KeyMetadataAware {
79+
implements Syncable, KeyMetadataAware, ByteBufferWritable, StreamCapabilities {
7780

7881
private static final Logger LOG =
7982
LoggerFactory.getLogger(KeyOutputStream.class);
@@ -245,6 +248,58 @@ public void write(byte[] b, int off, int len)
245248
}
246249
}
247250

251+
/**
252+
* Writes {@code len} bytes starting at absolute position {@code off} of the
253+
* given {@link ByteBuffer}, mirroring {@link #write(byte[], int, int)} but
254+
* routing the buffer down to the block layer without an intermediate heap
255+
* copy. The caller's buffer position and limit are left unchanged.
256+
*/
257+
public void write(ByteBuffer b, int off, int len) throws IOException {
258+
try {
259+
getRequestSemaphore().acquire();
260+
checkNotClosed();
261+
if (b == null) {
262+
throw new NullPointerException();
263+
}
264+
if ((off < 0) || (off > b.capacity()) || (len < 0)
265+
|| ((off + len) > b.capacity()) || ((off + len) < 0)) {
266+
throw new IndexOutOfBoundsException();
267+
}
268+
if (len == 0) {
269+
return;
270+
}
271+
272+
doInWriteLock(() -> {
273+
doWrite((entry, o, writeLen) -> entry.write(b, o, writeLen),
274+
off, len, false);
275+
writeOffset += len;
276+
});
277+
} finally {
278+
getRequestSemaphore().release();
279+
}
280+
}
281+
282+
/**
283+
* {@link ByteBufferWritable} entry point. Writes all remaining bytes of
284+
* {@code buf} and advances its position to its limit. Lets a wrapping
285+
* {@link org.apache.hadoop.crypto.CryptoOutputStream} hand its direct
286+
* ciphertext buffer straight down without a heap copy.
287+
*/
288+
@Override
289+
public void write(ByteBuffer buf) throws IOException {
290+
final int off = buf.position();
291+
final int len = buf.remaining();
292+
write(buf, off, len);
293+
buf.position(buf.limit());
294+
}
295+
296+
@Override
297+
public boolean hasCapability(String capability) {
298+
return StreamCapabilities.WRITEBYTEBUFFER.equalsIgnoreCase(capability)
299+
|| StreamCapabilities.HFLUSH.equalsIgnoreCase(capability)
300+
|| StreamCapabilities.HSYNC.equalsIgnoreCase(capability);
301+
}
302+
248303
private <E extends Throwable> void doInWriteLock(CheckedRunnable<E> block) throws E {
249304
writeLock.lock();
250305
try {
@@ -257,6 +312,25 @@ private <E extends Throwable> void doInWriteLock(CheckedRunnable<E> block) throw
257312
@VisibleForTesting
258313
void handleWrite(byte[] b, int off, long len, boolean retry)
259314
throws IOException {
315+
doWrite((entry, o, writeLen) -> entry.write(b, o, writeLen), off, len,
316+
retry);
317+
}
318+
319+
/**
320+
* Writes a chunk of a data source into a {@link BlockOutputStreamEntry}.
321+
* The source (byte[] or {@link ByteBuffer}) is captured by the implementation
322+
* so that the block-allocation, retry and exception bookkeeping below stays
323+
* source-agnostic. Not invoked on the retry path, which replays from the
324+
* block's own buffered data.
325+
*/
326+
@FunctionalInterface
327+
private interface ChunkWriter {
328+
void write(BlockOutputStreamEntry entry, int off, int writeLen)
329+
throws IOException;
330+
}
331+
332+
private void doWrite(ChunkWriter writer, int off, long len, boolean retry)
333+
throws IOException {
260334
while (len > 0) {
261335
try {
262336
BlockOutputStreamEntry current =
@@ -271,8 +345,8 @@ void handleWrite(byte[] b, int off, long len, boolean retry)
271345
// or if it sees an exception, how much the actual write was
272346
// acknowledged.
273347
int writtenLength =
274-
writeToOutputStream(current, retry, len, b, expectedWriteLen,
275-
off, currentPos);
348+
writeToOutputStream(current, retry, len, writer,
349+
expectedWriteLen, off, currentPos);
276350
if (current.getRemaining() <= 0) {
277351
// since the current block is already written close the stream.
278352
handleFlushOrClose(StreamAction.FULL);
@@ -287,15 +361,15 @@ void handleWrite(byte[] b, int off, long len, boolean retry)
287361
}
288362

289363
private int writeToOutputStream(BlockOutputStreamEntry current,
290-
boolean retry, long len, byte[] b, int writeLen, int off, long currentPos)
291-
throws IOException {
364+
boolean retry, long len, ChunkWriter writer, int writeLen, int off,
365+
long currentPos) throws IOException {
292366
try {
293367
current.registerCallReceived();
294368
if (retry) {
295369
current.writeOnRetry(len);
296370
} else {
297371
current.waitForRetryHandling(retryHandlingCondition);
298-
current.write(b, off, writeLen);
372+
writer.write(current, off, writeLen);
299373
offset += writeLen;
300374
}
301375
current.registerCallFinished();

hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneOutputStream.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.IOException;
2121
import java.io.OutputStream;
22+
import java.nio.ByteBuffer;
2223
import java.util.Map;
2324
import java.util.Objects;
2425
import java.util.Optional;
@@ -93,6 +94,20 @@ public void write(byte[] b, int off, int len) throws IOException {
9394
outputStream.write(b, off, len);
9495
}
9596

97+
@Override
98+
public void write(ByteBuffer buffer, int off, int len) throws IOException {
99+
// The direct ByteBuffer write path only applies to a plain replicated key.
100+
// Encryption (CryptoOutputStream / CipherOutputStreamOzone) needs the
101+
// plaintext on the heap first, and EC uses a different striping path, so
102+
// both fall back to the byte[] path in ByteArrayStreamOutput.
103+
if (outputStream instanceof KeyOutputStream
104+
&& !(outputStream instanceof ECKeyOutputStream)) {
105+
((KeyOutputStream) outputStream).write(buffer, off, len);
106+
} else {
107+
super.write(buffer, off, len);
108+
}
109+
}
110+
96111
@Override
97112
public synchronized void flush() throws IOException {
98113
outputStream.flush();

hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/io/TestKeyOutputStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void testConcurrentWriteLimitOne() throws Exception {
6161
doAnswer(invocation -> {
6262
countWrite.getAndIncrement();
6363
return invocation.callRealMethod();
64-
}).when(keyOutputStream).write(any(), anyInt(), anyInt());
64+
}).when(keyOutputStream).write(any(byte[].class), anyInt(), anyInt());
6565

6666
final ConcurrentHashMap<Long, CountDownLatch> mapNotifiers = new ConcurrentHashMap<>();
6767

0 commit comments

Comments
 (0)