Skip to content

Commit 50f3b97

Browse files
refactor(hm-vm): Magic "ephemeral" sentinel string passed through snapshot() label couples vm.rs and docker.rs (#121)
1 parent b753567 commit 50f3b97

5 files changed

Lines changed: 45 additions & 19 deletions

File tree

crates/hm-vm/src/backend.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::path::Path;
66
use anyhow::Result;
77
use async_trait::async_trait;
88

9-
use crate::types::{OutputSink, SnapshotId, VmConfig};
9+
use crate::types::{OutputSink, SnapshotId, SnapshotLabel, VmConfig};
1010

1111
/// Factory that creates and manages virtual machines.
1212
#[async_trait]
@@ -40,7 +40,7 @@ pub trait Vm: Send {
4040
) -> Result<i32>;
4141

4242
/// Capture the current VM state as a named snapshot.
43-
async fn snapshot(&mut self, label: &str) -> Result<SnapshotId>;
43+
async fn snapshot(&mut self, label: &SnapshotLabel) -> Result<SnapshotId>;
4444

4545
/// Tear down the VM and release all resources.
4646
async fn destroy(&mut self) -> Result<()>;

crates/hm-vm/src/docker.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use futures::StreamExt;
2222
use tracing::instrument;
2323

2424
use crate::backend::{Vm, VmBackend};
25-
use crate::types::{OutputSink, SnapshotId, VmConfig};
25+
use crate::types::{OutputSink, SnapshotId, SnapshotLabel, VmConfig};
2626

2727
/// Docker-based VM backend.
2828
///
@@ -308,20 +308,22 @@ impl Vm for DockerVm {
308308
}
309309

310310
#[instrument(skip(self))]
311-
async fn snapshot(&mut self, label: &str) -> Result<SnapshotId> {
311+
async fn snapshot(&mut self, label: &SnapshotLabel) -> Result<SnapshotId> {
312312
let cid = self
313313
.container_id
314314
.as_deref()
315315
.context("container already destroyed")?;
316-
let parts: Vec<&str> = label.splitn(2, ':').collect();
317-
// A bare label (no explicit `repo:tag`) is an ephemeral, uncached
318-
// snapshot. Tag it with the unique container id rather than a shared
319-
// `:latest`: concurrent sibling leaf steps off the same parent all
320-
// commit ephemeral snapshots, and racing to write the same
321-
// `ephemeral:latest` image fails the loser of the race in dockerd.
322-
let (repo, tag) = match parts.as_slice() {
323-
[r, v] => (*r, *v),
324-
_ => (label, cid),
316+
// An ephemeral, uncached snapshot is committed under a unique tag (the
317+
// container id) rather than a shared `:latest`: concurrent sibling leaf
318+
// steps off the same parent all commit ephemeral snapshots, and racing
319+
// to write the same `ephemeral:latest` image fails the loser of the
320+
// race in dockerd. A cached snapshot parses its cache key as `repo:tag`.
321+
let (repo, tag) = match label {
322+
SnapshotLabel::Ephemeral => ("ephemeral", cid),
323+
SnapshotLabel::Cached(key) => match key.split_once(':') {
324+
Some((r, v)) => (r, v),
325+
None => (key.as_str(), cid),
326+
},
325327
};
326328
let opts = CommitContainerOptions {
327329
container: cid,

crates/hm-vm/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod docker;
1212
pub use backend::{Vm, VmBackend};
1313
pub use registry::ImageRegistry;
1414
pub use types::{
15-
Action, CachingPolicy, ExecutionResult, ImageSource, NullSink, OutputSink, SnapshotId, VmConfig,
15+
Action, CachingPolicy, ExecutionResult, ImageSource, NullSink, OutputSink, SnapshotId,
16+
SnapshotLabel, VmConfig,
1617
};
1718
pub use vm::HmVm;

crates/hm-vm/src/types.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ pub enum CachingPolicy {
3232
Cache { key: String },
3333
}
3434

35+
/// Typed instruction for `Vm::snapshot`, describing how the committed
36+
/// snapshot should be tagged.
37+
///
38+
/// This replaces a previously stringly-encoded convention where a bare label
39+
/// meant "ephemeral" and a `repo:tag` label meant "cached", a contract that
40+
/// the producer (`vm.rs`) and consumer (the backend) had to agree on
41+
/// out-of-band. Encoding the distinction as an enum makes it compiler-checked.
42+
#[derive(Clone, Debug, PartialEq, Eq)]
43+
pub enum SnapshotLabel {
44+
/// Uncached snapshot. The backend chooses a unique tag (e.g. the
45+
/// container id) so concurrent sibling steps do not race to write the
46+
/// same image reference.
47+
Ephemeral,
48+
/// Cached snapshot tagged from this cache key (parsed as `repo:tag`).
49+
Cached(String),
50+
}
51+
3552
/// Opaque snapshot handle. Backend-specific contents.
3653
#[derive(Clone, Debug, Hash, PartialEq, Eq, derive_more::Display)]
3754
#[display("{_0}")]

crates/hm-vm/src/vm.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use tracing::{instrument, warn};
77

88
use crate::backend::VmBackend;
99
use crate::registry::ImageRegistry;
10-
use crate::types::{Action, CachingPolicy, ExecutionResult, ImageSource, OutputSink, VmConfig};
10+
use crate::types::{
11+
Action, CachingPolicy, ExecutionResult, ImageSource, OutputSink, SnapshotLabel, VmConfig,
12+
};
1113

1214
/// High-level orchestrator that drives the VM lifecycle.
1315
///
@@ -107,10 +109,10 @@ impl HmVm {
107109
// 5. Snapshot and cache on success
108110
let snapshot = if exit_code == 0 {
109111
let label = match policy {
110-
CachingPolicy::Cache { key } => key.as_str(),
111-
CachingPolicy::None => "ephemeral",
112+
CachingPolicy::Cache { key } => SnapshotLabel::Cached(key.clone()),
113+
CachingPolicy::None => SnapshotLabel::Ephemeral,
112114
};
113-
let snap = vm.snapshot(label).await?;
115+
let snap = vm.snapshot(&label).await?;
114116

115117
if let CachingPolicy::Cache { key } = policy {
116118
let evicted = self.registry.put(key, &snap);
@@ -236,7 +238,11 @@ mod tests {
236238
Ok(self.exit_code)
237239
}
238240

239-
async fn snapshot(&mut self, label: &str) -> Result<SnapshotId> {
241+
async fn snapshot(&mut self, label: &SnapshotLabel) -> Result<SnapshotId> {
242+
let label = match label {
243+
SnapshotLabel::Ephemeral => "ephemeral".to_string(),
244+
SnapshotLabel::Cached(key) => key.clone(),
245+
};
240246
self.calls
241247
.lock()
242248
.map_or_else(|_| {}, |mut c| c.push(format!("snapshot:{label}")));

0 commit comments

Comments
 (0)