Skip to content

Commit 4acd5bd

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

6 files changed

Lines changed: 279 additions & 49 deletions

File tree

ostree-ext/src/chunking.rs

Lines changed: 268 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,68 @@ 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 let Some(remaining) = NonZeroU32::new(self.remaining()) {
367+
let start = Instant::now();
368+
let packing = basic_packing(&regular_sizes, remaining, prior_build_metadata)?;
369+
let duration = start.elapsed();
370+
tracing::debug!("Time elapsed in packing: {:#?}", duration);
371+
372+
for bin in packing.into_iter() {
373+
let name = match bin.len() {
374+
0 => Cow::Borrowed("Reserved for new packages"),
375+
1 => {
376+
let first = bin[0];
377+
let first_name = &*first.meta.identifier;
378+
Cow::Borrowed(first_name)
379+
}
380+
2..=5 => {
381+
let first = bin[0];
382+
let first_name = &*first.meta.identifier;
383+
let r = bin.iter().map(|v| &*v.meta.identifier).skip(1).fold(
384+
String::from(first_name),
385+
|mut acc, v| {
386+
write!(acc, " and {}", v).unwrap();
387+
acc
388+
},
389+
);
390+
Cow::Owned(r)
391+
}
392+
n => Cow::Owned(format!("{n} components")),
393+
};
394+
let mut chunk = Chunk::new(&name);
395+
chunk.packages = bin.iter().map(|v| String::from(&*v.meta.name)).collect();
396+
for szmeta in bin {
397+
for &obj in rmap.get(&szmeta.meta.identifier).unwrap() {
398+
self.remainder.move_obj(&mut chunk, obj.as_str());
399+
}
363400
}
401+
self.chunks.push(chunk);
364402
}
365-
self.chunks.push(chunk);
366403
}
367404

368-
assert_eq!(self.remainder.content.len(), 0);
405+
// Check that all objects have been processed
406+
if !processed_specific_components.is_empty() || !regular_sizes.is_empty() {
407+
assert_eq!(self.remainder.content.len(), 0);
408+
}
369409

370410
Ok(())
371411
}
@@ -1003,4 +1043,191 @@ mod test {
10031043
assert_eq!(structure_derived, v2_expected_structure);
10041044
Ok(())
10051045
}
1046+
1047+
fn setup_exclusive_test(
1048+
component_data: &[(u32, u32, u64)],
1049+
max_layers: u32,
1050+
num_fake_objects: Option<usize>,
1051+
) -> Result<(
1052+
Vec<ObjectSourceMetaSized>,
1053+
ObjectMetaSized,
1054+
ObjectMetaSized,
1055+
Chunking,
1056+
)> {
1057+
// Create content metadata from provided data
1058+
let contentmeta: Vec<ObjectSourceMetaSized> = component_data
1059+
.iter()
1060+
.map(|&(id, freq, size)| ObjectSourceMetaSized {
1061+
meta: ObjectSourceMeta {
1062+
identifier: RcStr::from(format!("pkg{}.0", id)),
1063+
name: RcStr::from(format!("pkg{}", id)),
1064+
srcid: RcStr::from(format!("srcpkg{}", id)),
1065+
change_time_offset: 0,
1066+
change_frequency: freq,
1067+
},
1068+
size,
1069+
})
1070+
.collect();
1071+
1072+
// Create object maps with fake checksums
1073+
let mut object_map = IndexMap::new();
1074+
let mut regular_map = IndexMap::new();
1075+
1076+
for (i, component) in contentmeta.iter().enumerate() {
1077+
let checksum = format!("checksum_{}", i);
1078+
regular_map.insert(checksum.clone(), component.meta.identifier.clone());
1079+
object_map.insert(checksum, component.meta.identifier.clone());
1080+
}
1081+
1082+
let regular_meta = ObjectMetaSized {
1083+
map: regular_map,
1084+
sizes: contentmeta.clone(),
1085+
};
1086+
1087+
// Create exclusive metadata (initially empty, to be populated by individual tests)
1088+
let exclusive_meta = ObjectMetaSized {
1089+
map: object_map,
1090+
sizes: Vec::new(),
1091+
};
1092+
1093+
// Set up chunking with remainder chunk
1094+
let mut chunking = Chunking::default();
1095+
chunking.max = max_layers;
1096+
chunking.remainder = Chunk::new("remainder");
1097+
1098+
// Add fake objects to the remainder chunk if specified
1099+
if let Some(num_objects) = num_fake_objects {
1100+
for i in 0..num_objects {
1101+
let checksum = format!("checksum_{}", i);
1102+
chunking
1103+
.remainder
1104+
.content
1105+
.insert(RcStr::from(checksum), (1000, vec![]));
1106+
chunking.remainder.size += 1000;
1107+
}
1108+
}
1109+
1110+
Ok((contentmeta, regular_meta, exclusive_meta, chunking))
1111+
}
1112+
1113+
#[test]
1114+
fn test_exclusive_chunks() -> Result<()> {
1115+
// Test that exclusive chunks are created first and get their own layers
1116+
let component_data = [
1117+
(1, 100, 50000),
1118+
(2, 200, 40000),
1119+
(3, 300, 30000),
1120+
(4, 400, 20000),
1121+
(5, 500, 10000),
1122+
];
1123+
1124+
let (contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1125+
setup_exclusive_test(&component_data, 8, Some(5))?;
1126+
1127+
// Create exclusive content metadata for pkg1 and pkg2
1128+
let exclusive_content: Vec<ObjectSourceMetaSized> =
1129+
vec![contentmeta[0].clone(), contentmeta[1].clone()];
1130+
exclusive_meta.sizes = exclusive_content;
1131+
1132+
chunking.process_mapping(
1133+
&regular_meta,
1134+
&Some(NonZeroU32::new(8).unwrap()),
1135+
None,
1136+
Some(&exclusive_meta),
1137+
)?;
1138+
1139+
// Verify exclusive chunks are created first
1140+
assert!(chunking.chunks.len() >= 2);
1141+
assert_eq!(chunking.chunks[0].name, "pkg1");
1142+
assert_eq!(chunking.chunks[1].name, "pkg2");
1143+
assert_eq!(chunking.chunks[0].packages, vec!["pkg1".to_string()]);
1144+
assert_eq!(chunking.chunks[1].packages, vec!["pkg2".to_string()]);
1145+
1146+
Ok(())
1147+
}
1148+
1149+
#[test]
1150+
fn test_exclusive_chunks_with_regular_packing() -> Result<()> {
1151+
// Test that exclusive chunks are created first, then regular packing continues
1152+
let component_data = [
1153+
(1, 100, 50000), // exclusive
1154+
(2, 200, 40000), // exclusive
1155+
(3, 300, 30000), // regular
1156+
(4, 400, 20000), // regular
1157+
(5, 500, 10000), // regular
1158+
(6, 600, 5000), // regular
1159+
];
1160+
1161+
let (contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1162+
setup_exclusive_test(&component_data, 8, Some(6))?;
1163+
1164+
// Create exclusive content metadata for pkg1 and pkg2
1165+
let exclusive_content: Vec<ObjectSourceMetaSized> =
1166+
vec![contentmeta[0].clone(), contentmeta[1].clone()];
1167+
exclusive_meta.sizes = exclusive_content;
1168+
1169+
chunking.process_mapping(
1170+
&regular_meta,
1171+
&Some(NonZeroU32::new(8).unwrap()),
1172+
None,
1173+
Some(&exclusive_meta),
1174+
)?;
1175+
1176+
// Verify exclusive chunks are created first
1177+
assert!(chunking.chunks.len() >= 2);
1178+
assert_eq!(chunking.chunks[0].name, "pkg1");
1179+
assert_eq!(chunking.chunks[1].name, "pkg2");
1180+
assert_eq!(chunking.chunks[0].packages, vec!["pkg1".to_string()]);
1181+
assert_eq!(chunking.chunks[1].packages, vec!["pkg2".to_string()]);
1182+
1183+
// Verify regular components are not in exclusive chunks
1184+
for chunk in &chunking.chunks[2..] {
1185+
assert!(!chunk.packages.contains(&"pkg1".to_string()));
1186+
assert!(!chunk.packages.contains(&"pkg2".to_string()));
1187+
}
1188+
1189+
Ok(())
1190+
}
1191+
1192+
#[test]
1193+
fn test_exclusive_chunks_isolation() -> Result<()> {
1194+
// Test that exclusive chunks properly isolate components
1195+
let component_data = [(1, 100, 50000), (2, 200, 40000), (3, 300, 30000)];
1196+
1197+
let (contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1198+
setup_exclusive_test(&component_data, 8, Some(3))?;
1199+
1200+
// Create exclusive content metadata for pkg1 only
1201+
let exclusive_content: Vec<ObjectSourceMetaSized> = vec![contentmeta[0].clone()];
1202+
exclusive_meta.sizes = exclusive_content;
1203+
1204+
chunking.process_mapping(
1205+
&regular_meta,
1206+
&Some(NonZeroU32::new(8).unwrap()),
1207+
None,
1208+
Some(&exclusive_meta),
1209+
)?;
1210+
1211+
// Verify pkg1 is in its own exclusive chunk
1212+
assert!(chunking.chunks.len() >= 1);
1213+
assert_eq!(chunking.chunks[0].name, "pkg1");
1214+
assert_eq!(chunking.chunks[0].packages, vec!["pkg1".to_string()]);
1215+
1216+
// Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1217+
let mut found_pkg2 = false;
1218+
let mut found_pkg3 = false;
1219+
for chunk in &chunking.chunks[1..] {
1220+
if chunk.packages.contains(&"pkg2".to_string()) {
1221+
found_pkg2 = true;
1222+
assert!(!chunk.packages.contains(&"pkg1".to_string()));
1223+
}
1224+
if chunk.packages.contains(&"pkg3".to_string()) {
1225+
found_pkg3 = true;
1226+
assert!(!chunk.packages.contains(&"pkg1".to_string()));
1227+
}
1228+
}
1229+
assert!(found_pkg2 && found_pkg3);
1230+
1231+
Ok(())
1232+
}
10061233
}

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)