Skip to content

Commit 7f35f22

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

6 files changed

Lines changed: 283 additions & 49 deletions

File tree

ostree-ext/src/chunking.rs

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

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)