3333import com .netflix .priam .utils .BoundedExponentialRetryCallable ;
3434import com .netflix .priam .utils .ByteBufferInputStream ;
3535import com .netflix .priam .utils .SystemUtils ;
36- import com .netflix .priam .utils .ThreadLocalByteBuffer ;
3736import org .apache .commons .io .IOUtils ;
3837import org .slf4j .Logger ;
3938import org .slf4j .LoggerFactory ;
40- import org .xerial .snappy .Snappy ;
4139import software .amazon .awssdk .core .exception .SdkException ;
4240import software .amazon .awssdk .core .sync .RequestBody ;
4341import software .amazon .awssdk .regions .Region ;
5048import javax .inject .Singleton ;
5149import java .io .*;
5250import java .nio .ByteBuffer ;
53- import java .nio .channels .FileChannel ;
5451import java .nio .file .Path ;
5552import java .nio .file .Paths ;
56- import java .nio .file .StandardOpenOption ;
5753import java .time .Instant ;
5854import java .util .*;
5955import java .util .concurrent .atomic .AtomicInteger ;
6258@ Singleton
6359public 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 }
0 commit comments