@@ -49,7 +49,7 @@ pub(crate) struct Chunk {
4949 pub ( crate ) packages : Vec < String > ,
5050}
5151
52- #[ derive( Debug , Deserialize , Serialize ) ]
52+ #[ derive( Debug , Clone , Deserialize , Serialize ) ]
5353/// Object metadata, but with additional size data
5454pub struct ObjectSourceMetaSized {
5555 /// The original metadata
@@ -276,9 +276,10 @@ impl Chunking {
276276 meta : & ObjectMetaSized ,
277277 max_layers : & Option < NonZeroU32 > ,
278278 prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
279+ specific_contentmeta : Option < & ObjectMetaSized > ,
279280 ) -> Result < Self > {
280281 let mut r = Self :: new ( repo, rev) ?;
281- r. process_mapping ( meta, max_layers, prior_build_metadata) ?;
282+ r. process_mapping ( meta, max_layers, prior_build_metadata, specific_contentmeta ) ?;
282283 Ok ( r)
283284 }
284285
@@ -294,6 +295,7 @@ impl Chunking {
294295 meta : & ObjectMetaSized ,
295296 max_layers : & Option < NonZeroU32 > ,
296297 prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
298+ specific_contentmeta : Option < & ObjectMetaSized > ,
297299 ) -> Result < ( ) > {
298300 self . max = max_layers
299301 . unwrap_or ( NonZeroU32 :: new ( MAX_CHUNKS ) . unwrap ( ) )
@@ -314,6 +316,25 @@ impl Chunking {
314316 rmap. entry ( Rc :: clone ( contentid) ) . or_default ( ) . push ( checksum) ;
315317 }
316318
319+ // Create exclusive chunks first if specified
320+ let mut processed_specific_components = BTreeSet :: new ( ) ;
321+ if let Some ( specific_meta) = specific_contentmeta {
322+ for component in & specific_meta. sizes {
323+ let mut chunk = Chunk :: new ( & component. meta . name ) ;
324+ chunk. packages = vec ! [ component. meta. name. to_string( ) ] ;
325+
326+ // Move all objects belonging to this exclusive component
327+ if let Some ( objects) = rmap. get ( & component. meta . identifier ) {
328+ for & obj in objects {
329+ self . remainder . move_obj ( & mut chunk, obj) ;
330+ }
331+ }
332+
333+ self . chunks . push ( chunk) ;
334+ processed_specific_components. insert ( & * component. meta . identifier ) ;
335+ }
336+ }
337+
317338 // Safety: Let's assume no one has over 4 billion components.
318339 self . n_provided_components = meta. sizes . len ( ) . try_into ( ) . unwrap ( ) ;
319340 self . n_sized_components = sizes
@@ -323,49 +344,72 @@ impl Chunking {
323344 . try_into ( )
324345 . unwrap ( ) ;
325346
326- // TODO: Compute bin packing in a better way
327- let start = Instant :: now ( ) ;
328- let packing = basic_packing (
329- sizes,
330- NonZeroU32 :: new ( self . max ) . unwrap ( ) ,
331- prior_build_metadata,
332- ) ?;
333- let duration = start. elapsed ( ) ;
334- tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
335-
336- for bin in packing. into_iter ( ) {
337- let name = match bin. len ( ) {
338- 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
339- 1 => {
340- let first = bin[ 0 ] ;
341- let first_name = & * first. meta . identifier ;
342- Cow :: Borrowed ( first_name)
343- }
344- 2 ..=5 => {
345- let first = bin[ 0 ] ;
346- let first_name = & * first. meta . identifier ;
347- let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
348- String :: from ( first_name) ,
349- |mut acc, v| {
350- write ! ( acc, " and {}" , v) . unwrap ( ) ;
351- acc
352- } ,
353- ) ;
354- Cow :: Owned ( r)
355- }
356- n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
357- } ;
358- let mut chunk = Chunk :: new ( & name) ;
359- chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
360- for szmeta in bin {
361- for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
362- self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
347+ // Filter out exclusive components for regular packing
348+ let regular_sizes: Vec < ObjectSourceMetaSized > = sizes
349+ . iter ( )
350+ . filter ( |component| {
351+ !processed_specific_components. contains ( & * component. meta . identifier )
352+ } )
353+ . map ( |component| ObjectSourceMetaSized {
354+ meta : ObjectSourceMeta {
355+ identifier : Rc :: clone ( & component. meta . identifier ) ,
356+ name : Rc :: clone ( & component. meta . name ) ,
357+ srcid : Rc :: clone ( & component. meta . srcid ) ,
358+ change_time_offset : component. meta . change_time_offset ,
359+ change_frequency : component. meta . change_frequency ,
360+ } ,
361+ size : component. size ,
362+ } )
363+ . collect ( ) ;
364+
365+ // Process regular components with bin packing if we have remaining layers
366+ if self . remaining ( ) > 0 {
367+ let start = Instant :: now ( ) ;
368+ let packing = basic_packing (
369+ & regular_sizes,
370+ NonZeroU32 :: new ( self . remaining ( ) ) . unwrap ( ) ,
371+ prior_build_metadata,
372+ ) ?;
373+ let duration = start. elapsed ( ) ;
374+ tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
375+
376+ for bin in packing. into_iter ( ) {
377+ let name = match bin. len ( ) {
378+ 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
379+ 1 => {
380+ let first = bin[ 0 ] ;
381+ let first_name = & * first. meta . identifier ;
382+ Cow :: Borrowed ( first_name)
383+ }
384+ 2 ..=5 => {
385+ let first = bin[ 0 ] ;
386+ let first_name = & * first. meta . identifier ;
387+ let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
388+ String :: from ( first_name) ,
389+ |mut acc, v| {
390+ write ! ( acc, " and {}" , v) . unwrap ( ) ;
391+ acc
392+ } ,
393+ ) ;
394+ Cow :: Owned ( r)
395+ }
396+ n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
397+ } ;
398+ let mut chunk = Chunk :: new ( & name) ;
399+ chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
400+ for szmeta in bin {
401+ for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
402+ self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
403+ }
363404 }
405+ self . chunks . push ( chunk) ;
364406 }
365- self . chunks . push ( chunk) ;
366407 }
367408
368- assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
409+ // Check that all objects have been processed
410+ if !processed_specific_components. is_empty ( ) || !regular_sizes. is_empty ( ) {
411+ assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
412+ }
369413
370414 Ok ( ( ) )
371415 }
@@ -1003,4 +1047,223 @@ mod test {
10031047 assert_eq ! ( structure_derived, v2_expected_structure) ;
10041048 Ok ( ( ) )
10051049 }
1050+
1051+ fn setup_exclusive_test (
1052+ component_data : & [ ( u32 , u32 , u32 ) ] ,
1053+ max_layers : u32 ,
1054+ num_fake_objects : Option < usize > ,
1055+ ) -> Result < ( Vec < ObjectSourceMetaSized > , ObjectMetaSized , ObjectMetaSized , Chunking ) > {
1056+ // Create content metadata from provided data
1057+ let contentmeta: Vec < ObjectSourceMetaSized > = component_data
1058+ . iter ( )
1059+ . map ( |& ( id, freq, size) | ObjectSourceMetaSized {
1060+ meta : ObjectSourceMeta {
1061+ identifier : RcStr :: from ( format ! ( "pkg{}.0" , id) ) ,
1062+ name : RcStr :: from ( format ! ( "pkg{}" , id) ) ,
1063+ srcid : RcStr :: from ( format ! ( "srcpkg{}" , id) ) ,
1064+ change_time_offset : 0 ,
1065+ change_frequency : freq,
1066+ } ,
1067+ size,
1068+ } )
1069+ . collect ( ) ;
1070+
1071+ // Create object maps with fake checksums
1072+ let mut object_map = IndexMap :: new ( ) ;
1073+ let mut regular_map = IndexMap :: new ( ) ;
1074+
1075+ for ( i, component) in contentmeta. iter ( ) . enumerate ( ) {
1076+ let checksum = format ! ( "checksum_{}" , i) ;
1077+ regular_map. insert ( checksum. clone ( ) , component. meta . identifier . clone ( ) ) ;
1078+ object_map. insert ( checksum, component. meta . identifier . clone ( ) ) ;
1079+ }
1080+
1081+ let regular_meta = ObjectMetaSized {
1082+ map : regular_map,
1083+ sizes : contentmeta. clone ( ) ,
1084+ } ;
1085+
1086+ // Create exclusive metadata (initially empty, to be populated by individual tests)
1087+ let exclusive_meta = ObjectMetaSized {
1088+ map : object_map,
1089+ sizes : Vec :: new ( ) ,
1090+ } ;
1091+
1092+ // Set up chunking with remainder chunk
1093+ let mut chunking = Chunking :: default ( ) ;
1094+ chunking. max = max_layers;
1095+ chunking. remainder = Chunk :: new ( "remainder" ) ;
1096+
1097+ // Add fake objects to the remainder chunk if specified
1098+ if let Some ( num_objects) = num_fake_objects {
1099+ for i in 0 ..num_objects {
1100+ let checksum = format ! ( "checksum_{}" , i) ;
1101+ chunking
1102+ . remainder
1103+ . content
1104+ . insert ( RcStr :: from ( checksum) , ( 1000 , vec ! [ ] ) ) ;
1105+ chunking. remainder . size += 1000 ;
1106+ }
1107+ }
1108+
1109+ Ok ( ( contentmeta, regular_meta, exclusive_meta, chunking) )
1110+ }
1111+
1112+ #[ test]
1113+ fn test_exclusive_chunks ( ) -> Result < ( ) > {
1114+ // Test that exclusive chunks are created first and get their own layers
1115+ let component_data = [
1116+ ( 1 , 100 , 50000 ) ,
1117+ ( 2 , 200 , 40000 ) ,
1118+ ( 3 , 300 , 30000 ) ,
1119+ ( 4 , 400 , 20000 ) ,
1120+ ( 5 , 500 , 10000 ) ,
1121+ ] ;
1122+
1123+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1124+ setup_exclusive_test ( & component_data, 8 , Some ( 5 ) ) ?;
1125+
1126+ // Create exclusive content metadata for pkg1 and pkg2
1127+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1128+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1129+ exclusive_meta. sizes = exclusive_content;
1130+
1131+ chunking. process_mapping (
1132+ & regular_meta,
1133+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1134+ None ,
1135+ Some ( & exclusive_meta) ,
1136+ ) ?;
1137+
1138+ // Verify exclusive chunks are created first
1139+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1140+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1141+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1142+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1143+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1144+
1145+ Ok ( ( ) )
1146+ }
1147+
1148+ #[ test]
1149+ fn test_exclusive_chunks_with_regular_packing ( ) -> Result < ( ) > {
1150+ // Test that exclusive chunks are created first, then regular packing continues
1151+ let component_data = [
1152+ ( 1 , 100 , 50000 ) , // exclusive
1153+ ( 2 , 200 , 40000 ) , // exclusive
1154+ ( 3 , 300 , 30000 ) , // regular
1155+ ( 4 , 400 , 20000 ) , // regular
1156+ ( 5 , 500 , 10000 ) , // regular
1157+ ( 6 , 600 , 5000 ) , // regular
1158+ ] ;
1159+
1160+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1161+ setup_exclusive_test ( & component_data, 8 , Some ( 6 ) ) ?;
1162+
1163+ // Create exclusive content metadata for pkg1 and pkg2
1164+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1165+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1166+ exclusive_meta. sizes = exclusive_content;
1167+
1168+ chunking. process_mapping (
1169+ & regular_meta,
1170+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1171+ None ,
1172+ Some ( & exclusive_meta) ,
1173+ ) ?;
1174+
1175+ // Verify exclusive chunks are created first
1176+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1177+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1178+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1179+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1180+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1181+
1182+ // Verify regular components are not in exclusive chunks
1183+ for chunk in & chunking. chunks [ 2 ..] {
1184+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1185+ assert ! ( !chunk. packages. contains( & "pkg2" . to_string( ) ) ) ;
1186+ }
1187+
1188+ Ok ( ( ) )
1189+ }
1190+
1191+ #[ test]
1192+ fn test_exclusive_chunks_layer_limit ( ) -> Result < ( ) > {
1193+ // Test that exclusive chunks respect layer limits
1194+ let component_data = [
1195+ ( 1 , 100 , 50000 ) ,
1196+ ( 2 , 200 , 40000 ) ,
1197+ ( 3 , 300 , 30000 ) ,
1198+ ( 4 , 400 , 20000 ) ,
1199+ ] ;
1200+
1201+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1202+ setup_exclusive_test ( & component_data, 2 , None ) ?;
1203+
1204+ // Try to create 4 exclusive chunks with only 2 layers available
1205+ exclusive_meta. sizes = contentmeta;
1206+
1207+ // This should fail because we don't have enough layers
1208+ let result = chunking. process_mapping (
1209+ & regular_meta,
1210+ & Some ( NonZeroU32 :: new ( 2 ) . unwrap ( ) ) ,
1211+ None ,
1212+ Some ( & exclusive_meta) ,
1213+ ) ;
1214+
1215+ assert ! ( result. is_err( ) ) ;
1216+ assert ! ( result
1217+ . unwrap_err( )
1218+ . to_string( )
1219+ . contains( "No remaining layers" ) ) ;
1220+
1221+ Ok ( ( ) )
1222+ }
1223+
1224+ #[ test]
1225+ fn test_exclusive_chunks_isolation ( ) -> Result < ( ) > {
1226+ // Test that exclusive chunks properly isolate components
1227+ let component_data = [
1228+ ( 1 , 100 , 50000 ) ,
1229+ ( 2 , 200 , 40000 ) ,
1230+ ( 3 , 300 , 30000 ) ,
1231+ ] ;
1232+
1233+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1234+ setup_exclusive_test ( & component_data, 8 , Some ( 3 ) ) ?;
1235+
1236+ // Create exclusive content metadata for pkg1 only
1237+ let exclusive_content: Vec < ObjectSourceMetaSized > = vec ! [ contentmeta[ 0 ] . clone( ) ] ;
1238+ exclusive_meta. sizes = exclusive_content;
1239+
1240+ chunking. process_mapping (
1241+ & regular_meta,
1242+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1243+ None ,
1244+ Some ( & exclusive_meta) ,
1245+ ) ?;
1246+
1247+ // Verify pkg1 is in its own exclusive chunk
1248+ assert ! ( chunking. chunks. len( ) >= 1 ) ;
1249+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1250+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1251+
1252+ // Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1253+ let mut found_pkg2 = false ;
1254+ let mut found_pkg3 = false ;
1255+ for chunk in & chunking. chunks [ 1 ..] {
1256+ if chunk. packages . contains ( & "pkg2" . to_string ( ) ) {
1257+ found_pkg2 = true ;
1258+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1259+ }
1260+ if chunk. packages . contains ( & "pkg3" . to_string ( ) ) {
1261+ found_pkg3 = true ;
1262+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1263+ }
1264+ }
1265+ assert ! ( found_pkg2 || found_pkg3) ; // At least one should be found in regular chunks
1266+
1267+ Ok ( ( ) )
1268+ }
10061269}
0 commit comments