Skip to content

Commit bd38bb6

Browse files
committed
chunking: Support exclusive chunks defined via xattrs
Signed-off-by: ckyrouac <ckyrouac@redhat.com>
1 parent e7d15d4 commit bd38bb6

6 files changed

Lines changed: 282 additions & 49 deletions

File tree

ostree-ext/src/chunking.rs

Lines changed: 271 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5454
pub 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,190 @@ mod test {
10031047
assert_eq!(structure_derived, v2_expected_structure);
10041048
Ok(())
10051049
}
1050+
1051+
fn setup_exclusive_test(
1052+
component_data: &[(u32, u32, u64)],
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_isolation() -> Result<()> {
1193+
// Test that exclusive chunks properly isolate components
1194+
let component_data = [
1195+
(1, 100, 50000),
1196+
(2, 200, 40000),
1197+
(3, 300, 30000),
1198+
];
1199+
1200+
let (contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1201+
setup_exclusive_test(&component_data, 8, Some(3))?;
1202+
1203+
// Create exclusive content metadata for pkg1 only
1204+
let exclusive_content: Vec<ObjectSourceMetaSized> = vec![contentmeta[0].clone()];
1205+
exclusive_meta.sizes = exclusive_content;
1206+
1207+
chunking.process_mapping(
1208+
&regular_meta,
1209+
&Some(NonZeroU32::new(8).unwrap()),
1210+
None,
1211+
Some(&exclusive_meta),
1212+
)?;
1213+
1214+
// Verify pkg1 is in its own exclusive chunk
1215+
assert!(chunking.chunks.len() >= 1);
1216+
assert_eq!(chunking.chunks[0].name, "pkg1");
1217+
assert_eq!(chunking.chunks[0].packages, vec!["pkg1".to_string()]);
1218+
1219+
// Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1220+
let mut found_pkg2 = false;
1221+
let mut found_pkg3 = false;
1222+
for chunk in &chunking.chunks[1..] {
1223+
if chunk.packages.contains(&"pkg2".to_string()) {
1224+
found_pkg2 = true;
1225+
assert!(!chunk.packages.contains(&"pkg1".to_string()));
1226+
}
1227+
if chunk.packages.contains(&"pkg3".to_string()) {
1228+
found_pkg3 = true;
1229+
assert!(!chunk.packages.contains(&"pkg1".to_string()));
1230+
}
1231+
}
1232+
assert!(found_pkg2 && found_pkg3);
1233+
1234+
Ok(())
1235+
}
10061236
}

ostree-ext/src/cli.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ async fn container_export(
766766
container_config: Option<Utf8PathBuf>,
767767
cmd: Option<Vec<String>>,
768768
compression_fast: bool,
769-
contentmeta: Option<Utf8PathBuf>,
769+
package_contentmeta: Option<Utf8PathBuf>,
770770
) -> Result<()> {
771771
let container_config = if let Some(container_config) = container_config {
772772
serde_json::from_reader(File::open(container_config).map(BufReader::new)?)?
@@ -777,7 +777,7 @@ async fn container_export(
777777
let mut contentmeta_data = None;
778778
let mut created = None;
779779
let mut labels = labels.clone();
780-
if let Some(contentmeta) = contentmeta {
780+
if let Some(contentmeta) = package_contentmeta {
781781
let buf = File::open(contentmeta).map(BufReader::new);
782782
let raw: RawMeta = serde_json::from_reader(buf?)?;
783783

@@ -842,7 +842,7 @@ async fn container_export(
842842
container_config,
843843
authfile,
844844
skip_compression: compression_fast, // TODO rename this in the struct at the next semver break
845-
contentmeta: contentmeta_data.as_ref(),
845+
package_contentmeta: contentmeta_data.as_ref(),
846846
max_layers,
847847
created,
848848
..Default::default()

0 commit comments

Comments
 (0)