Skip to content

Commit 0ac3ef1

Browse files
authored
Merge pull request #1397 from ckyrouac/chunking-xattrs
chunking: Support exclusive chunks defined via xattrs
2 parents 5ed53c3 + 81b3c27 commit 0ac3ef1

File tree

6 files changed

+270
-49
lines changed

6 files changed

+270
-49
lines changed

ostree-ext/src/chunking.rs

Lines changed: 259 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,59 @@ 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+
.cloned()
354+
.collect();
355+
356+
// Process regular components with bin packing if we have remaining layers
357+
if let Some(remaining) = NonZeroU32::new(self.remaining()) {
358+
let start = Instant::now();
359+
let packing = basic_packing(&regular_sizes, remaining, prior_build_metadata)?;
360+
let duration = start.elapsed();
361+
tracing::debug!("Time elapsed in packing: {:#?}", duration);
362+
363+
for bin in packing.into_iter() {
364+
let name = match bin.len() {
365+
0 => Cow::Borrowed("Reserved for new packages"),
366+
1 => {
367+
let first = bin[0];
368+
let first_name = &*first.meta.identifier;
369+
Cow::Borrowed(first_name)
370+
}
371+
2..=5 => {
372+
let first = bin[0];
373+
let first_name = &*first.meta.identifier;
374+
let r = bin.iter().map(|v| &*v.meta.identifier).skip(1).fold(
375+
String::from(first_name),
376+
|mut acc, v| {
377+
write!(acc, " and {}", v).unwrap();
378+
acc
379+
},
380+
);
381+
Cow::Owned(r)
382+
}
383+
n => Cow::Owned(format!("{n} components")),
384+
};
385+
let mut chunk = Chunk::new(&name);
386+
chunk.packages = bin.iter().map(|v| String::from(&*v.meta.name)).collect();
387+
for szmeta in bin {
388+
for &obj in rmap.get(&szmeta.meta.identifier).unwrap() {
389+
self.remainder.move_obj(&mut chunk, obj.as_str());
390+
}
363391
}
392+
self.chunks.push(chunk);
364393
}
365-
self.chunks.push(chunk);
366394
}
367395

368-
assert_eq!(self.remainder.content.len(), 0);
396+
// Check that all objects have been processed
397+
if !processed_specific_components.is_empty() || !regular_sizes.is_empty() {
398+
assert_eq!(self.remainder.content.len(), 0);
399+
}
369400

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

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)