1313import java .io .IOException ;
1414import java .io .InputStream ;
1515import java .io .OutputStream ;
16+ import java .io .RandomAccessFile ;
1617import java .net .URL ;
18+ import java .nio .ByteBuffer ;
19+ import java .nio .channels .FileChannel ;
1720import java .util .ArrayList ;
1821import java .util .Arrays ;
1922import java .util .List ;
23+ import java .util .Locale ;
2024import java .util .Map ;
25+ import java .util .Properties ;
2126import java .util .concurrent .ConcurrentHashMap ;
2227import java .util .concurrent .ExecutionException ;
2328import java .util .concurrent .ExecutorService ;
2429import java .util .concurrent .Executors ;
2530import java .util .concurrent .Future ;
31+ import java .util .concurrent .TimeUnit ;
2632import java .util .stream .Collectors ;
33+ import java .util .zip .CRC32 ;
2734
2835/**
2936 * This class will load the native dependencies.
3037 */
3138public class NativeDepsLoader {
3239 private static final Logger log = LoggerFactory .getLogger (NativeDepsLoader .class );
40+ private static final int COPY_BUFFER_SIZE = 1024 * 1024 ;
41+ // Positional extraction uses one copy buffer per worker.
42+ private static final int MAX_CONCURRENT_CHUNK_READS =
43+ Math .max (1 , Math .min (12 , Runtime .getRuntime ().availableProcessors ()));
44+ private static final String CHUNK_MANIFEST_SUFFIX = ".chunks.properties" ;
45+ private static final String CHUNK_DIRECTORY_SUFFIX = ".chunks/" ;
46+ private static final String CHUNK_FORMAT_VERSION = "1" ;
3347
3448 /**
3549 * Set this system property to true to prevent unpacked dependency files from
@@ -285,6 +299,9 @@ private static void loadNativeDeps(String[][] loadOrder, boolean preserveDeps) t
285299 * @throws IOException on any error trying to load the libraries.
286300 */
287301 public static File loadNativeDep (String depName , boolean preserveDep ) throws IOException {
302+ if (libNativeDir != null ) {
303+ validateLibNativeDir (new String []{depName });
304+ }
288305 String os = System .getProperty ("os.name" );
289306 String arch = System .getProperty ("os.arch" );
290307 return loadDep (os , arch , depName , preserveDep );
@@ -338,8 +355,8 @@ private static File createFileTimed(String os, String arch, String baseName,
338355 return loc ;
339356 }
340357
341- /** Extract the contents of a library resource into a temporary file */
342- private static File createFile (String os , String arch , String baseName ) throws IOException {
358+ /** Extract the contents of a library resource into a temporary file. */
359+ static File createFile (String os , String arch , String baseName ) throws IOException {
343360 String mappedName = System .mapLibraryName (baseName );
344361 // Fast path: when ai.rapids.cudf.lib-native-dir is set, the loader skips
345362 // JAR extraction entirely and uses the pre-unpacked file from the
@@ -354,21 +371,25 @@ private static File createFile(String os, String arch, String baseName) throws I
354371 return loc ;
355372 }
356373 String path = arch + "/" + os + "/" + mappedName ;
357- File loc ;
358- URL resource = loader .getResource (path );
359- if (resource == null ) {
374+ URL chunkManifestResource = loader . getResource ( path + CHUNK_MANIFEST_SUFFIX ) ;
375+ URL resource = chunkManifestResource == null ? loader .getResource (path ) : null ;
376+ if (chunkManifestResource == null && resource == null ) {
360377 throw new FileNotFoundException ("Could not locate native dependency " + path );
361378 }
362379 long t0 = System .currentTimeMillis ();
363- try (InputStream in = resource .openStream ()) {
364- loc = File .createTempFile (baseName , ".so" );
365- loc .deleteOnExit ();
366- try (OutputStream out = new FileOutputStream (loc )) {
367- byte [] buffer = new byte [1024 * 16 ];
368- int read = 0 ;
369- while ((read = in .read (buffer )) >= 0 ) {
370- out .write (buffer , 0 , read );
371- }
380+ File loc = File .createTempFile (baseName , ".so" );
381+ loc .deleteOnExit ();
382+ boolean success = false ;
383+ try {
384+ if (chunkManifestResource == null ) {
385+ extractConventionalResource (resource , loc );
386+ } else {
387+ extractChunkedResource (chunkManifestResource , mappedName , loc );
388+ }
389+ success = true ;
390+ } finally {
391+ if (!success && loc .exists () && !loc .delete ()) {
392+ log .warn ("Could not delete partial native dependency {}" , loc );
372393 }
373394 }
374395 if (libLogLoadTiming ) {
@@ -379,6 +400,261 @@ private static File createFile(String os, String arch, String baseName) throws I
379400 return loc ;
380401 }
381402
403+ private static void extractConventionalResource (URL resource , File loc ) throws IOException {
404+ try (InputStream in = resource .openStream ();
405+ OutputStream out = new FileOutputStream (loc )) {
406+ copy (in , out , new byte [COPY_BUFFER_SIZE ]);
407+ }
408+ }
409+
410+ private static void extractChunkedResource (URL manifestResource , String mappedName , File loc )
411+ throws IOException {
412+ extractChunkedResource (
413+ manifestResource , mappedName , loc , MAX_CONCURRENT_CHUNK_READS );
414+ }
415+
416+ static void extractChunkedResource (URL manifestResource , String mappedName , File loc ,
417+ int maxConcurrentReads ) throws IOException {
418+ if (maxConcurrentReads <= 0 ) {
419+ throw new IllegalArgumentException ("maxConcurrentReads must be positive" );
420+ }
421+ ChunkManifest manifest = ChunkManifest .load (manifestResource );
422+ int concurrentReads = Math .min (maxConcurrentReads , manifest .chunkCount );
423+ ExecutorService executor = Executors .newFixedThreadPool (concurrentReads );
424+ List <Future <?>> chunkFutures = new ArrayList <>(manifest .chunkCount );
425+ try (RandomAccessFile out = new RandomAccessFile (loc , "rw" )) {
426+ out .setLength (manifest .librarySize );
427+ FileChannel outputChannel = out .getChannel ();
428+ try {
429+ for (int i = 0 ; i < manifest .chunkCount ; i ++) {
430+ chunkFutures .add (submitChunkExtraction (
431+ executor , manifestResource , mappedName , manifest , outputChannel , i ));
432+ }
433+ executor .shutdown ();
434+ awaitChunks (chunkFutures , mappedName );
435+ } finally {
436+ shutdownAndAwait (executor );
437+ }
438+ }
439+ }
440+
441+ private static Future <?> submitChunkExtraction (
442+ ExecutorService executor , URL manifestResource , String mappedName ,
443+ ChunkManifest manifest , FileChannel outputChannel , int chunkIndex ) throws IOException {
444+ String chunkName = String .format (Locale .ROOT , "%05d" , chunkIndex );
445+ URL chunkResource = new URL (
446+ manifestResource , mappedName + CHUNK_DIRECTORY_SUFFIX + chunkName );
447+ long outputOffset = manifest .chunkSize * chunkIndex ;
448+ long expectedSize = manifest .expectedChunkSize (chunkIndex );
449+ long expectedCrc32 = manifest .expectedChunkCrc32 (chunkIndex );
450+ return executor .submit (() -> {
451+ extractChunk (chunkResource , outputChannel , outputOffset , expectedSize , expectedCrc32 );
452+ return null ;
453+ });
454+ }
455+
456+ private static void awaitChunks (List <Future <?>> futures , String mappedName )
457+ throws IOException {
458+ IOException failure = null ;
459+ for (int i = 0 ; i < futures .size (); i ++) {
460+ try {
461+ futures .get (i ).get ();
462+ } catch (InterruptedException e ) {
463+ Thread .currentThread ().interrupt ();
464+ throw new IOException (String .format (Locale .ROOT ,
465+ "Interrupted while extracting native dependency chunk %s/%05d" ,
466+ mappedName , i ), e );
467+ } catch (ExecutionException e ) {
468+ Throwable cause = e .getCause ();
469+ IOException chunkFailure = cause instanceof IOException
470+ ? (IOException ) cause
471+ : new IOException (String .format (Locale .ROOT ,
472+ "Could not extract native dependency chunk %s/%05d" , mappedName , i ), cause );
473+ if (failure == null ) {
474+ failure = chunkFailure ;
475+ } else {
476+ failure .addSuppressed (chunkFailure );
477+ }
478+ }
479+ }
480+ if (failure != null ) {
481+ throw failure ;
482+ }
483+ }
484+
485+ private static void shutdownAndAwait (ExecutorService executor ) {
486+ executor .shutdownNow ();
487+ boolean interrupted = Thread .interrupted ();
488+ while (!executor .isTerminated ()) {
489+ try {
490+ executor .awaitTermination (1 , TimeUnit .SECONDS );
491+ } catch (InterruptedException e ) {
492+ interrupted = true ;
493+ }
494+ }
495+ if (interrupted ) {
496+ Thread .currentThread ().interrupt ();
497+ }
498+ }
499+
500+ private static void extractChunk (
501+ URL resource , FileChannel outputChannel , long outputOffset ,
502+ long expectedSize , long expectedCrc32 ) throws IOException {
503+ byte [] buffer = new byte [COPY_BUFFER_SIZE ];
504+ ByteBuffer bytes = ByteBuffer .wrap (buffer );
505+ CRC32 crc = new CRC32 ();
506+ long totalBytes = 0 ;
507+ try (InputStream in = resource .openStream ()) {
508+ int read ;
509+ while ((read = in .read (buffer )) != -1 ) {
510+ if (read == 0 ) {
511+ continue ;
512+ }
513+ if (read > expectedSize - totalBytes ) {
514+ throw new IOException (String .format (Locale .ROOT ,
515+ "Native dependency chunk %s exceeds expected size of %d bytes" ,
516+ resource , expectedSize ));
517+ }
518+
519+ crc .update (buffer , 0 , read );
520+ bytes .clear ();
521+ bytes .limit (read );
522+ long writeOffset = outputOffset + totalBytes ;
523+ while (bytes .hasRemaining ()) {
524+ int written = outputChannel .write (bytes , writeOffset );
525+ if (written <= 0 ) {
526+ throw new IOException ("Could not make progress writing native dependency " + resource );
527+ }
528+ writeOffset += written ;
529+ }
530+ totalBytes += read ;
531+ }
532+ } catch (FileNotFoundException e ) {
533+ throw new IOException ("Could not locate native dependency chunk " + resource , e );
534+ }
535+
536+ if (totalBytes != expectedSize ) {
537+ throw new IOException (String .format (Locale .ROOT ,
538+ "Native dependency chunk %s has size %d bytes, expected %d" ,
539+ resource , totalBytes , expectedSize ));
540+ }
541+ if (crc .getValue () != expectedCrc32 ) {
542+ throw new IOException (String .format (Locale .ROOT ,
543+ "Native dependency chunk CRC32 mismatch for %s: expected %08x but extracted %08x" ,
544+ resource , expectedCrc32 , crc .getValue ()));
545+ }
546+ }
547+
548+ private static void copy (InputStream in , OutputStream out , byte [] buffer )
549+ throws IOException {
550+ int read ;
551+ while ((read = in .read (buffer )) != -1 ) {
552+ if (read > 0 ) {
553+ out .write (buffer , 0 , read );
554+ }
555+ }
556+ }
557+
558+ private static final class ChunkManifest {
559+ private static final String FORMAT_VERSION_KEY = "format.version" ;
560+ private static final String LIBRARY_SIZE_KEY = "library.size" ;
561+ private static final String CHUNK_SIZE_KEY = "chunk.size" ;
562+ private static final String CHUNK_COUNT_KEY = "chunk.count" ;
563+
564+ private final long librarySize ;
565+ private final long chunkSize ;
566+ private final int chunkCount ;
567+ private final long [] chunkCrc32 ;
568+
569+ private ChunkManifest (long librarySize , long chunkSize , int chunkCount , long [] chunkCrc32 ) {
570+ this .librarySize = librarySize ;
571+ this .chunkSize = chunkSize ;
572+ this .chunkCount = chunkCount ;
573+ this .chunkCrc32 = chunkCrc32 ;
574+ }
575+
576+ private static ChunkManifest load (URL resource ) throws IOException {
577+ Properties properties = new Properties ();
578+ try (InputStream in = resource .openStream ()) {
579+ properties .load (in );
580+ } catch (IllegalArgumentException e ) {
581+ throw new IOException ("Malformed native dependency chunk manifest " + resource , e );
582+ }
583+ String version = require (properties , FORMAT_VERSION_KEY , resource );
584+ if (!CHUNK_FORMAT_VERSION .equals (version )) {
585+ throw new IOException ("Unsupported native dependency chunk manifest version " + version
586+ + " in " + resource );
587+ }
588+ long librarySize = parsePositiveLong (properties , LIBRARY_SIZE_KEY , resource );
589+ long chunkSize = parsePositiveLong (properties , CHUNK_SIZE_KEY , resource );
590+ long chunkCountLong = parsePositiveLong (properties , CHUNK_COUNT_KEY , resource );
591+ if (chunkCountLong > Integer .MAX_VALUE ) {
592+ throw new IOException ("Native dependency chunk count is too large in " + resource );
593+ }
594+ long expectedChunkCount = 1 + ((librarySize - 1 ) / chunkSize );
595+ if (chunkCountLong != expectedChunkCount ) {
596+ throw new IOException (String .format (Locale .ROOT ,
597+ "Invalid native dependency chunk count in %s: expected %d but found %d" ,
598+ resource , expectedChunkCount , chunkCountLong ));
599+ }
600+ int chunkCount = (int ) chunkCountLong ;
601+ long [] chunkCrc32 = new long [chunkCount ];
602+ for (int i = 0 ; i < chunkCount ; i ++) {
603+ String key = String .format (Locale .ROOT , "chunk.%05d.crc32" , i );
604+ chunkCrc32 [i ] = parseCrc32 (properties , key , resource );
605+ }
606+ return new ChunkManifest (librarySize , chunkSize , chunkCount , chunkCrc32 );
607+ }
608+
609+ private static long parseCrc32 (Properties properties , String key , URL resource )
610+ throws IOException {
611+ String value = require (properties , key , resource );
612+ if (!value .matches ("[0-9a-fA-F]{8}" )) {
613+ throw new IOException ("Invalid " + key + " in " + resource + ": " + value );
614+ }
615+ try {
616+ return Long .parseLong (value , 16 );
617+ } catch (NumberFormatException e ) {
618+ throw new IOException ("Invalid " + key + " in " + resource + ": " + value , e );
619+ }
620+ }
621+
622+ private static long parsePositiveLong (Properties properties , String key , URL resource )
623+ throws IOException {
624+ String value = require (properties , key , resource );
625+ try {
626+ long parsed = Long .parseLong (value );
627+ if (parsed <= 0 ) {
628+ throw new NumberFormatException ("value must be positive" );
629+ }
630+ return parsed ;
631+ } catch (NumberFormatException e ) {
632+ throw new IOException ("Invalid " + key + " in " + resource + ": " + value , e );
633+ }
634+ }
635+
636+ private static String require (Properties properties , String key , URL resource )
637+ throws IOException {
638+ String value = properties .getProperty (key );
639+ if (value == null || value .trim ().isEmpty ()) {
640+ throw new IOException ("Missing " + key + " in native dependency chunk manifest "
641+ + resource );
642+ }
643+ return value .trim ();
644+ }
645+
646+ private long expectedChunkSize (int chunkIndex ) {
647+ if (chunkIndex == chunkCount - 1 ) {
648+ return librarySize - (chunkSize * chunkIndex );
649+ }
650+ return chunkSize ;
651+ }
652+
653+ private long expectedChunkCrc32 (int chunkIndex ) {
654+ return chunkCrc32 [chunkIndex ];
655+ }
656+ }
657+
382658 /**
383659 * Verify that every library named in {@code order} exists as a regular file
384660 * inside {@link #libNativeDir}. Throws {@link IOException} listing the first
0 commit comments