Skip to content

Commit 2d0cddf

Browse files
committed
Ensure Snappy compression matches snappy-java framing when uploading in parts and reusing upload buffers. Consolidate this logic into single new class BufferIterator.
1 parent 43f0d97 commit 2d0cddf

5 files changed

Lines changed: 464 additions & 88 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.netflix.priam.aws;
2+
3+
import com.netflix.priam.backup.AbstractBackupPath;
4+
import com.netflix.priam.compress.CompressionType;
5+
import com.netflix.priam.utils.ThreadLocalByteBuffer;
6+
import org.xerial.snappy.Snappy;
7+
8+
import java.io.Closeable;
9+
import java.io.IOException;
10+
import java.nio.ByteBuffer;
11+
import java.nio.channels.FileChannel;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.nio.file.StandardOpenOption;
15+
16+
public class BufferIterator implements Closeable {
17+
private static final int SNAPPY_CHUNK_SIZE = 4;
18+
static final byte[] SNAPPY_HEADER = new byte[] {
19+
(byte) 0x82, 'S', 'N', 'A', 'P', 'P', 'Y', (byte) 0x00,
20+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
21+
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01
22+
};
23+
private final ByteBuffer readBuffer;
24+
private final ByteBuffer compressBuffer;
25+
private final FileChannel channel;
26+
private final CompressionType compressionType;
27+
private final long fileSize;
28+
private final long chunkSize;
29+
private boolean headerApplied;
30+
private long position;
31+
32+
BufferIterator(ThreadLocal<ByteBuffer> inputBuffer,
33+
ThreadLocal<ByteBuffer> compressBuffer,
34+
AbstractBackupPath path,
35+
long minimumChunkSize) throws IOException {
36+
this(inputBuffer, compressBuffer, path, minimumChunkSize,
37+
FileChannel.open(Paths.get(path.getBackupFile().getAbsolutePath()), StandardOpenOption.READ));
38+
}
39+
40+
@com.google.common.annotations.VisibleForTesting
41+
BufferIterator(ThreadLocal<ByteBuffer> inputBuffer,
42+
ThreadLocal<ByteBuffer> compressBuffer,
43+
AbstractBackupPath path,
44+
long minimumChunkSize,
45+
FileChannel fileChannel) throws IOException {
46+
Path localPath = Paths.get(path.getBackupFile().getAbsolutePath());
47+
fileSize = localPath.toFile().length();
48+
chunkSize = S3FileSystem.getChunkSize(localPath, minimumChunkSize);
49+
compressionType = path.getCompression();
50+
// Direct buffer use is obviated when compressing.
51+
boolean useDirectBuffer = compressionType != CompressionType.SNAPPY;
52+
readBuffer = ThreadLocalByteBuffer.get(inputBuffer, (int) Math.min(chunkSize, fileSize), useDirectBuffer);
53+
if (compressionType == CompressionType.SNAPPY) {
54+
int maxCompressedLength = Snappy.maxCompressedLength((int) chunkSize) + SNAPPY_HEADER.length + SNAPPY_CHUNK_SIZE;
55+
this.compressBuffer = ThreadLocalByteBuffer.get(compressBuffer, maxCompressedLength, false);
56+
} else {
57+
this.compressBuffer = null;
58+
}
59+
this.channel = fileChannel;
60+
}
61+
62+
public boolean hasNext() {
63+
return position < fileSize;
64+
}
65+
66+
public ByteBuffer next() throws IOException {
67+
long bytesToRead = Math.min(chunkSize, fileSize - position);
68+
readBuffer.clear();
69+
readBuffer.limit((int) bytesToRead);
70+
int bytesRead = 0;
71+
while (bytesRead < bytesToRead) {
72+
int read = channel.read(readBuffer, position + bytesRead);
73+
if (read == -1) break;
74+
bytesRead += read;
75+
}
76+
readBuffer.flip();
77+
position += bytesRead;
78+
if (compressionType != CompressionType.SNAPPY) {
79+
return readBuffer;
80+
}
81+
compressBuffer.clear();
82+
int offset = headerApplied ? SNAPPY_CHUNK_SIZE : SNAPPY_CHUNK_SIZE + SNAPPY_HEADER.length;
83+
int compressedSize = Snappy.compress(readBuffer.array(), 0, bytesRead, compressBuffer.array(), offset);
84+
compressBuffer.position(0);
85+
if (!headerApplied) {
86+
compressBuffer.put(SNAPPY_HEADER);
87+
headerApplied = true;
88+
}
89+
compressBuffer.putInt(compressedSize);
90+
compressBuffer.limit(compressedSize + offset);
91+
compressBuffer.position(0);
92+
return compressBuffer;
93+
}
94+
95+
@Override
96+
public void close() throws IOException {
97+
channel.close();
98+
}
99+
}

priam/src/main/java/com/netflix/priam/aws/S3FileSystem.java

Lines changed: 20 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,9 @@
3333
import com.netflix.priam.utils.BoundedExponentialRetryCallable;
3434
import com.netflix.priam.utils.ByteBufferInputStream;
3535
import com.netflix.priam.utils.SystemUtils;
36-
import com.netflix.priam.utils.ThreadLocalByteBuffer;
3736
import org.apache.commons.io.IOUtils;
3837
import org.slf4j.Logger;
3938
import org.slf4j.LoggerFactory;
40-
import org.xerial.snappy.Snappy;
4139
import software.amazon.awssdk.core.exception.SdkException;
4240
import software.amazon.awssdk.core.sync.RequestBody;
4341
import software.amazon.awssdk.regions.Region;
@@ -50,10 +48,8 @@
5048
import javax.inject.Singleton;
5149
import java.io.*;
5250
import java.nio.ByteBuffer;
53-
import java.nio.channels.FileChannel;
5451
import java.nio.file.Path;
5552
import java.nio.file.Paths;
56-
import java.nio.file.StandardOpenOption;
5753
import java.time.Instant;
5854
import java.util.*;
5955
import java.util.concurrent.atomic.AtomicInteger;
@@ -62,11 +58,12 @@
6258
@Singleton
6359
public class S3FileSystem extends S3FileSystemBase {
6460
private static final Logger logger = LoggerFactory.getLogger(S3FileSystem.class);
61+
private static final int MAX_CHUNKS = 9995; // 10K is AWS limit, minus a small buffer
6562
private static final long MAX_BUFFER_SIZE = 5L * 1024L * 1024L;
63+
6664
private final DynamicRateLimiter dynamicRateLimiter;
6765
private final ThreadLocal<ByteBuffer> inputBufferThreadLocal = new ThreadLocal<>();
6866
private final ThreadLocal<ByteBuffer> compressBufferThreadLocal = new ThreadLocal<>();
69-
private final ThreadLocal<ByteBuffer> chunkBufferThreadLocal = new ThreadLocal<>();
7067

7168
@Inject
7269
public S3FileSystem(
@@ -87,6 +84,10 @@ public S3FileSystem(
8784
this.dynamicRateLimiter = dynamicRateLimiter;
8885
}
8986

87+
static long getChunkSize(Path path, long minimumChunkSize) {
88+
return Math.max(path.toFile().length() / MAX_CHUNKS, minimumChunkSize);
89+
}
90+
9091
@Override
9192
protected void downloadFileImpl(AbstractBackupPath path, String suffix)
9293
throws BackupRestoreException {
@@ -149,12 +150,11 @@ private long uploadMultipartWithBuffers(AbstractBackupPath path, Instant target)
149150
throws BackupRestoreException {
150151
Path localPath = Paths.get(path.getBackupFile().getAbsolutePath());
151152
String remotePath = path.getRemotePath();
152-
long chunkSize = getChunkSize(localPath);
153+
long chunkSize = getChunkSize(localPath, config.getBackupChunkSize());
153154
String prefix = config.getBackupPrefix();
154155
if (logger.isDebugEnabled())
155156
logger.debug("Uploading to {}/{} with chunk size {} (using ByteBuffer implementation)", prefix, remotePath, chunkSize);
156157
File localFile = localPath.toFile();
157-
long fileSize = localFile.length();
158158

159159
CreateMultipartUploadRequest.Builder initRequestBuilder = CreateMultipartUploadRequest.builder()
160160
.bucket(prefix)
@@ -169,66 +169,25 @@ private long uploadMultipartWithBuffers(AbstractBackupPath path, Instant target)
169169
String uploadId = s3Client.createMultipartUpload(initRequest).uploadId();
170170
List<CompletedPart> completedParts = new ArrayList<>();
171171

172-
try (FileChannel channel = FileChannel.open(localPath, StandardOpenOption.READ)) {
172+
try (BufferIterator bufferIterator = new BufferIterator(inputBufferThreadLocal, compressBufferThreadLocal, path, config.getBackupChunkSize())) {
173173
int partNum = 0;
174174
long compressedFileSize = 0;
175-
long position = 0;
176-
177-
// Allocate thread-local buffers for reading and compression
178-
ByteBuffer readBuffer = ThreadLocalByteBuffer.get(inputBufferThreadLocal, (int) Math.min(chunkSize, fileSize));
179-
ByteBuffer compressBuffer = null;
180-
if (path.getCompression() == CompressionType.SNAPPY) {
181-
int maxCompressedLength = Snappy.maxCompressedLength((int) chunkSize);
182-
compressBuffer = ThreadLocalByteBuffer.get(compressBufferThreadLocal, maxCompressedLength);
183-
}
184-
185-
while (position < fileSize) {
186-
// Read chunk from file
187-
long bytesToRead = Math.min(chunkSize, fileSize - position);
188-
readBuffer.clear();
189-
readBuffer.limit((int) bytesToRead);
190-
191-
int bytesRead = 0;
192-
while (bytesRead < bytesToRead) {
193-
int read = channel.read(readBuffer, position + bytesRead);
194-
if (read == -1) break;
195-
bytesRead += read;
196-
}
197-
readBuffer.flip();
198-
position += bytesRead;
199-
200-
// Compress if needed
201-
ByteBuffer uploadBuffer;
202-
int uploadSize;
203-
if (path.getCompression() == CompressionType.SNAPPY) {
204-
compressBuffer.clear();
205-
uploadSize = Snappy.compress(readBuffer, compressBuffer);
206-
compressBuffer.limit(uploadSize);
207-
compressBuffer.position(0);
208-
uploadBuffer = compressBuffer;
209-
} else {
210-
uploadBuffer = readBuffer;
211-
uploadSize = bytesRead;
212-
}
213-
214-
// Apply rate limiting
175+
while (bufferIterator.hasNext()) {
176+
ByteBuffer uploadBuffer = bufferIterator.next();
177+
int uploadSize = uploadBuffer.limit();
215178
rateLimiter.acquire(uploadSize);
216179
dynamicRateLimiter.acquire(path, target, uploadSize);
217-
218-
// Upload part directly from ByteBuffer
219180
partNum++;
220181
UploadPartRequest.Builder req = UploadPartRequest.builder()
221182
.bucket(prefix)
222183
.key(remotePath)
223184
.uploadId(uploadId)
224185
.partNumber(partNum)
225186
.contentLength((long) uploadSize);
226-
227187
byte[] md5 = SystemUtils.md5(uploadBuffer);
228188
if (config.addMD5ToBackupUploads()) {
229189
req.contentMD5(SystemUtils.toBase64(md5));
230190
}
231-
232191
UploadPartResponse uploadResult = new BoundedExponentialRetryCallable<UploadPartResponse>(200, 10000, 5) {
233192
@Override
234193
public UploadPartResponse retriableCall() {
@@ -246,15 +205,12 @@ public UploadPartResponse retriableCall() {
246205
validateUpload(eTag, md5, partNum);
247206
completedParts.add(CompletedPart.builder().partNumber(partNum).eTag(eTag).build());
248207
compressedFileSize += uploadSize;
249-
208+
250209
if (logger.isDebugEnabled()) {
251210
logger.debug("Uploaded part {} of size {}", partNum, uploadSize);
252211
}
253212
}
254-
255213
logger.info("{} done. part count: {}", localFile, partNum);
256-
257-
// Complete the multipart upload
258214
CompleteMultipartUploadResponse multipartUploadResponse =
259215
s3Client.completeMultipartUpload(
260216
CompleteMultipartUploadRequest.builder()
@@ -298,7 +254,7 @@ private long uploadMultipartLegacy(AbstractBackupPath path, Instant target)
298254
throws BackupRestoreException {
299255
Path localPath = Paths.get(path.getBackupFile().getAbsolutePath());
300256
String remotePath = path.getRemotePath();
301-
long chunkSize = getChunkSize(localPath);
257+
long chunkSize = getChunkSize(localPath, config.getBackupChunkSize());
302258
String prefix = config.getBackupPrefix();
303259
if (logger.isDebugEnabled())
304260
logger.debug("Uploading to {}/{} with chunk size {} (using legacy implementation)", prefix, remotePath, chunkSize);
@@ -450,29 +406,14 @@ private PutObjectRequest generatePutFromByteArray(AbstractBackupPath path, byte[
450406

451407
@VisibleForTesting
452408
public ByteBuffer getFileByteBuffer(AbstractBackupPath path) throws BackupRestoreException {
453-
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
454-
long fileSize = localFile.length();
455-
456-
try {
457-
// Get input buffer and read file data directly into it
458-
ByteBuffer inputBuffer = ThreadLocalByteBuffer.get(inputBufferThreadLocal, (int) fileSize);
459-
try (FileChannel channel = FileChannel.open(localFile.toPath(), StandardOpenOption.READ)) {
460-
channel.read(inputBuffer);
461-
inputBuffer.flip();
462-
}
463-
464-
// Compress if needed, otherwise return input buffer
465-
if (path.getCompression() == CompressionType.SNAPPY) {
466-
int maxCompressedLength = Snappy.maxCompressedLength((int) fileSize);
467-
ByteBuffer compressBuffer = ThreadLocalByteBuffer.get(compressBufferThreadLocal, maxCompressedLength);
468-
int compressedSize = Snappy.compress(inputBuffer, compressBuffer);
469-
compressBuffer.limit(compressedSize);
470-
compressBuffer.position(0);
471-
return compressBuffer;
472-
} else {
473-
return inputBuffer;
474-
}
409+
try (BufferIterator bufferIterator = new BufferIterator(
410+
inputBufferThreadLocal,
411+
compressBufferThreadLocal,
412+
path,
413+
config.getBackupChunkSize())) {
414+
return bufferIterator.next();
475415
} catch (Exception e) {
416+
File localFile = Paths.get(path.getBackupFile().getAbsolutePath()).toFile();
476417
throw new BackupRestoreException("Error reading file: " + localFile.getName(), e);
477418
}
478419
}

priam/src/main/java/com/netflix/priam/aws/S3FileSystemBase.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import software.amazon.awssdk.services.s3.model.*;
3838

3939
public abstract class S3FileSystemBase extends AbstractFileSystem {
40-
private static final int MAX_CHUNKS = 9995; // 10K is AWS limit, minus a small buffer
4140
private static final Logger logger = LoggerFactory.getLogger(S3FileSystemBase.class);
4241
S3Client s3Client;
4342
final IConfiguration config;
@@ -189,8 +188,4 @@ public void deleteFiles(List<Path> remotePaths) throws BackupRestoreException {
189188
throw new BackupRestoreException(e + " while trying to delete the objects");
190189
}
191190
}
192-
193-
final long getChunkSize(Path path) {
194-
return Math.max(path.toFile().length() / MAX_CHUNKS, config.getBackupChunkSize());
195-
}
196191
}

priam/src/main/java/com/netflix/priam/utils/ThreadLocalByteBuffer.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@
2121
public class ThreadLocalByteBuffer {
2222

2323
public static ByteBuffer get(ThreadLocal<ByteBuffer> threadLocalBuffer, int bytes) {
24+
return get(threadLocalBuffer, bytes, true);
25+
}
26+
27+
public static ByteBuffer get(ThreadLocal<ByteBuffer> threadLocalBuffer, int bytes, boolean direct) {
2428
ByteBuffer buffer = threadLocalBuffer.get();
25-
26-
if (buffer == null || buffer.capacity() < bytes) {
27-
buffer = ByteBuffer.allocateDirect(bytes);
29+
30+
if (buffer == null || buffer.capacity() < bytes || buffer.isDirect() != direct) {
31+
buffer = direct ? ByteBuffer.allocateDirect(bytes) : ByteBuffer.allocate(bytes);
2832
threadLocalBuffer.set(buffer);
2933
}
30-
34+
3135
buffer.clear();
3236
buffer.limit(bytes);
3337
return buffer;

0 commit comments

Comments
 (0)