Skip to main content

arcbox_computer_runtime/sandbox/
templates.rs

1//! Template-catalog surface on [`ComputerManager`] (CORE-107).
2//!
3//! Thin delegation to [`TemplateCatalog`](crate::template_catalog::TemplateCatalog)
4//! plus the artifact side effects the catalog itself never performs: draining
5//! pre-warmed restore slots and deleting warm snapshots a mutation released.
6//! Build orchestration (rootfs conversion, prewarm) lives in the guest agent
7//! and lands its results here via [`register_template_draft`].
8//!
9//! [`register_template_draft`]: ComputerManager::register_template_draft
10
11use std::collections::HashMap;
12use std::path::Path;
13
14use tracing::{info, warn};
15use uuid::Uuid;
16
17use super::ComputerManager;
18use super::types::{ComputerId, ComputerSpec, ComputerState};
19use crate::error::{ComputerError, Result};
20use crate::snapshot::{SnapshotDraft, SnapshotGeometry};
21use crate::template_catalog::{
22    ReleasedArtifacts, ResolvedTemplate, TEMPLATE_LABEL, TemplateDefaultsSpec, TemplateEntry,
23};
24use arcbox_snapshot::SnapshotError;
25
26/// A checkpoint copied into template ownership: everything the build
27/// orchestrator needs to assemble the catalog entry.
28#[derive(Debug)]
29pub struct PromotedSnapshot {
30    /// The template-owned copy's snapshot id (carries [`TEMPLATE_LABEL`]).
31    pub snapshot_id: String,
32    /// Capture-time geometry the copy restores onto.
33    pub geometry: SnapshotGeometry,
34    /// The origin's rootfs image path (pinned by the copied meta).
35    pub rootfs_path: String,
36    /// On-disk footprint of the copied vmstate + memory files. Nominal on
37    /// Btrfs: the copy reflinks, so blocks are shared until either side
38    /// diverges.
39    pub artifact_bytes: u64,
40}
41
42impl ComputerManager {
43    /// Resolve a `name[:version]` catalog reference.
44    pub fn get_template(&self, reference: &str) -> Result<ResolvedTemplate> {
45        self.templates.resolve(reference).map_err(Into::into)
46    }
47
48    /// Every catalog row: `(name, entry)` — published versions in publish
49    /// order, then the draft, grouped by name.
50    pub fn list_templates(&self) -> Result<Vec<(String, TemplateEntry)>> {
51        self.templates.list().map_err(Into::into)
52    }
53
54    /// Install a built entry as `name`'s draft, reclaiming any warm snapshot
55    /// the displaced previous draft owned exclusively.
56    pub async fn register_template_draft(
57        &self,
58        name: &str,
59        entry: TemplateEntry,
60    ) -> Result<TemplateEntry> {
61        let outcome = self.templates.put_draft(name, entry)?;
62        self.delete_released_snapshots(&outcome.released).await;
63        info!(template = name, digest = %outcome.entry.digest, "template draft registered");
64        Ok(outcome.entry)
65    }
66
67    /// Freeze `name`'s draft as immutable `version`, reclaiming the draft's
68    /// exclusively-owned artifacts when an idempotent re-publish collapses a
69    /// same-digest rebuild.
70    pub async fn publish_template(&self, name: &str, version: &str) -> Result<TemplateEntry> {
71        let outcome = self.templates.publish(name, version)?;
72        self.delete_released_snapshots(&outcome.released).await;
73        Ok(outcome.entry)
74    }
75
76    /// Delete a template version (`name:version`) or a whole template
77    /// (`name`), including warm snapshots no remaining entry references.
78    /// Rootfs images are reclaimed separately by the rootfs-cache sweep once
79    /// [`Self::pinned_rootfs_paths`] stops reporting them.
80    pub async fn delete_template(&self, reference: &str) -> Result<()> {
81        self.check_reconcile()?;
82        let released = self.templates.delete(reference)?;
83        self.delete_released_snapshots(&released).await;
84        info!(template = reference, "template deleted");
85        Ok(())
86    }
87
88    /// Copy checkpoint `source_snapshot_id` into a template-owned snapshot
89    /// (new id, [`TEMPLATE_LABEL`] = `template_name`), decoupling the
90    /// template's lifetime from the user checkpoint. Files are reflinked
91    /// where the filesystem supports it (the data dir is Btrfs), so the copy
92    /// is cheap; the origin's rootfs stays pinned through the copied meta.
93    pub async fn promote_snapshot_to_template(
94        &self,
95        source_snapshot_id: &str,
96        template_name: &str,
97    ) -> Result<PromotedSnapshot> {
98        self.check_reconcile()?;
99        crate::template_catalog::validate_template_name(template_name)?;
100        let source = self.snapshots.find_by_id(source_snapshot_id)?;
101        if source.name.as_deref() == Some(super::pause::PAUSE_SNAPSHOT_NAME) {
102            return Err(ComputerError::FailedPrecondition(format!(
103                "snapshot {source_snapshot_id} is the internal pause checkpoint of a \
104                 paused sandbox; checkpoint the sandbox instead"
105            )));
106        }
107        let geometry = source.geometry.ok_or_else(|| {
108            ComputerError::FailedPrecondition(format!(
109                "snapshot {source_snapshot_id} predates geometry recording; \
110                 re-checkpoint the sandbox with this agent and promote the new snapshot"
111            ))
112        })?;
113        let rootfs_path = source.rootfs_path.clone().ok_or_else(|| {
114            ComputerError::FailedPrecondition(format!(
115                "snapshot {source_snapshot_id} records no rootfs path; \
116                 re-checkpoint the sandbox and promote the new snapshot"
117            ))
118        })?;
119
120        let pending = self.snapshots.begin(&format!("template-{template_name}"))?;
121        let staging = pending.dir();
122        let mut artifact_bytes = clone_or_copy(&source.vmstate_path, &staging.join("vmstate"))?;
123        if let Some(mem) = &source.mem_path {
124            artifact_bytes += clone_or_copy(mem, &staging.join("mem"))?;
125        }
126        let meta = pending.commit(SnapshotDraft {
127            name: None,
128            labels: HashMap::from([(TEMPLATE_LABEL.to_owned(), template_name.to_owned())]),
129            snapshot_type: source.snapshot_type,
130            parent_id: source.parent_id.clone(),
131            kernel_path: source.kernel_path.clone(),
132            rootfs_path: Some(rootfs_path.clone()),
133            net_invariant: source.net_invariant,
134            geometry: Some(geometry),
135            format: source.format,
136        })?;
137        info!(
138            template = template_name,
139            source = source_snapshot_id,
140            snapshot_id = %meta.id,
141            "checkpoint promoted into template ownership"
142        );
143        Ok(PromotedSnapshot {
144            snapshot_id: meta.id,
145            geometry,
146            rootfs_path,
147            artifact_bytes,
148        })
149    }
150
151    /// Boot the built rootfs once with the template's effective geometry and
152    /// an empty cmd, checkpoint at READY, and destroy the builder — the
153    /// snapshot then serves creates as the template's warm start point.
154    /// Requires jailer mode (restore does). The builder's warm behaviour is
155    /// disabled so a prewarm neither restores from nor publishes into the
156    /// warm-cache LRU.
157    pub async fn prewarm_template(
158        &self,
159        name: &str,
160        rootfs_path: &str,
161        defaults: &TemplateDefaultsSpec,
162    ) -> Result<PrewarmOutcome> {
163        if self.config.firecracker.jailer.is_none() {
164            return Err(ComputerError::FailedPrecondition(
165                "prewarm requires jailer mode (snapshot restore does)".into(),
166            ));
167        }
168        let vcpus = if defaults.vcpus != 0 {
169            defaults.vcpus
170        } else {
171            u32::try_from(self.config.defaults.vcpus).unwrap_or(1)
172        };
173        let memory_mib = if defaults.memory_mib != 0 {
174            defaults.memory_mib
175        } else {
176            self.config.defaults.memory_mib
177        };
178        // Short id on purpose: it must fit the driver's own id budget
179        // (`VmDriver::id_budget`, enforced by `validate_new_computer_id`) —
180        // `template-build-<full uuid>` was 51 chars and overflowed AF_UNIX's
181        // `sun_path`, failing the builder boot as an opaque socket timeout.
182        // 16 hex chars keep collisions out of reach for an ephemeral,
183        // single-flighted builder.
184        let mut suffix = Uuid::new_v4().simple().to_string();
185        suffix.truncate(16);
186        let builder_id = format!("tpl-build-{suffix}");
187        let spec = ComputerSpec {
188            id: Some(builder_id.clone()),
189            rootfs: rootfs_path.to_owned(),
190            vcpus,
191            memory_mib,
192            // Deliberately no cmd: the checkpoint captures freshly-Ready and
193            // idle. A cmd baked into the memory image would re-run on every
194            // restore on top of the effective spec's cmd.
195            ..Default::default()
196        };
197        let (id, _ip) = self
198            .create_computer_inner(
199                spec,
200                &Uuid::new_v4().to_string(),
201                super::lifecycle::WarmPolicy::Disabled,
202            )
203            .await?;
204        let checkpoint = self.prewarm_checkpoint(&id, name).await;
205        // The builder dies regardless of checkpoint success. A failed
206        // teardown fails the build: a leaked live builder VM has no reaper
207        // (no HealthMonitor loop), so success here would silently keep its
208        // vCPUs/memory allocated. The fresh snapshot is dropped too — no
209        // record references it yet — and the error names the builder id so
210        // the operator can remove it and retry.
211        if let Err(remove_error) = self.remove_computer(&id, true).await {
212            if let Ok((snapshot_id, _)) = &checkpoint {
213                self.discard_promoted_snapshot(snapshot_id).await;
214            }
215            return Err(ComputerError::Unavailable(format!(
216                "prewarm builder sandbox {id} could not be removed ({remove_error}); \
217                 remove it manually and retry the build"
218            )));
219        }
220        let (snapshot_id, artifact_bytes) = checkpoint?;
221        Ok(PrewarmOutcome {
222            warm: crate::template_catalog::WarmArtifact {
223                snapshot_id,
224                vcpus,
225                memory_mib,
226            },
227            artifact_bytes,
228        })
229    }
230
231    /// Wait for the builder to reach READY, then checkpoint it under the
232    /// template label. Polled rather than event-driven: a lagged broadcast
233    /// receiver would miss the READY edge, while polling cannot.
234    async fn prewarm_checkpoint(&self, id: &ComputerId, template: &str) -> Result<(String, u64)> {
235        const READY_POLL_MS: u64 = 250;
236        const READY_TIMEOUT_SECS: u64 = 180;
237        let deadline =
238            tokio::time::Instant::now() + std::time::Duration::from_secs(READY_TIMEOUT_SECS);
239        loop {
240            let info = self.inspect_computer(id)?;
241            match info.state {
242                ComputerState::Ready => break,
243                ComputerState::Failed => {
244                    return Err(ComputerError::FailedPrecondition(format!(
245                        "prewarm boot failed: {}",
246                        info.error.unwrap_or_else(|| "unknown boot failure".into())
247                    )));
248                }
249                _ if tokio::time::Instant::now() >= deadline => {
250                    return Err(ComputerError::DeadlineExceeded(format!(
251                        "prewarm builder did not reach READY within {READY_TIMEOUT_SECS}s"
252                    )));
253                }
254                _ => tokio::time::sleep(std::time::Duration::from_millis(READY_POLL_MS)).await,
255            }
256        }
257        // The prewarm builder is this manager's own computer, captured
258        // through the same path a user checkpoint takes.
259        let info = self
260            .capture_checkpoint(
261                &id.to_owned(),
262                format!("template-{template}"),
263                HashMap::from([(TEMPLATE_LABEL.to_owned(), template.to_owned())]),
264            )
265            .await
266        // The builder is force-removed by the caller whichever way this
267        // ends, so a guest left frozen needs no failing here: the removal
268        // kills and releases it.
269        ?;
270        let meta = self.snapshots.find_by_id(&info.snapshot_id)?;
271        let mut artifact_bytes = std::fs::metadata(&meta.vmstate_path).map_or(0, |m| m.len());
272        if let Some(mem) = &meta.mem_path {
273            artifact_bytes += std::fs::metadata(mem).map_or(0, |m| m.len());
274        }
275        Ok((info.snapshot_id, artifact_bytes))
276    }
277
278    /// Best-effort removal of a just-promoted copy whose catalog
279    /// registration failed — without it the copy would sit as a
280    /// (recoverable) orphan until an operator noticed the log line.
281    pub async fn discard_promoted_snapshot(&self, snapshot_id: &str) {
282        self.delete_released_snapshots(&ReleasedArtifacts {
283            snapshot_ids: vec![snapshot_id.to_owned()],
284        })
285        .await;
286    }
287
288    /// Best-effort deletion of snapshots a catalog mutation released,
289    /// draining any pre-warmed restore slots staged from each first
290    /// (mirroring `delete_checkpoint` — a slot restored from the snapshot
291    /// would otherwise outlive it). The catalog entry is already gone, so a
292    /// failed snapshot delete must not fail the mutation — it would strand
293    /// the caller with a half-deleted template it can only retry into
294    /// `TemplateNotFound`. Leftovers are orphaned but recoverable: they carry
295    /// no `arcbox.warm_key` (warm LRU ignores them), and once no record
296    /// references them the checkpoint deletion guard steps aside, so
297    /// `DeleteSnapshot` by id reclaims the disk — provided the catalog still
298    /// parses. An unreadable record makes that guard fail closed (an
299    /// unreadable catalog must never authorise a delete), so recovery then
300    /// starts with removing the bad record.
301    async fn delete_released_snapshots(&self, released: &ReleasedArtifacts) {
302        for snapshot_id in &released.snapshot_ids {
303            self.drain_pool(Some(snapshot_id)).await;
304            match self.snapshots.delete_by_id(snapshot_id) {
305                Ok(()) => {}
306                // `find_by_id` reports a missing snapshot as
307                // `SnapshotError::Snapshot("… not found")` — benign here: a
308                // retried mutation already deleted it.
309                Err(SnapshotError::Snapshot(msg)) if msg.ends_with("not found") => {}
310                Err(error) => warn!(
311                    snapshot_id,
312                    %error,
313                    "failed to delete a template-owned snapshot; artifact is orphaned"
314                ),
315            }
316        }
317    }
318}
319
320/// Result of a prewarm boot-and-checkpoint.
321#[derive(Debug)]
322pub struct PrewarmOutcome {
323    /// The template's warm artifact (snapshot id + capture geometry).
324    pub warm: crate::template_catalog::WarmArtifact,
325    /// On-disk footprint of the checkpoint files.
326    pub artifact_bytes: u64,
327}
328
329/// Whether a template's pre-warmed snapshot can serve this create: restore
330/// needs the jailer, the invariant tap identity, no caller-pinned boot
331/// recipe, and an exactly matching geometry — a memory snapshot resumes
332/// only onto its capture shape. Checked against the effective
333/// (defaults-applied) spec. Deliberately NOT gated on
334/// `config.firecracker.warm_create`: that flag governs the implicit CORE-77
335/// cache, while a template's prewarm was explicitly requested at build time
336/// — adding the gate would silently disable prewarm for anyone running with
337/// the cache off. Stopping restores from a bad snapshot = rebuild or delete
338/// the template.
339pub(super) fn template_restore_eligible(
340    config: &crate::config::RuntimeConfig,
341    spec: &ComputerSpec,
342    caller_supplied_boot: bool,
343    warm: &crate::sandbox::TemplateWarmRef,
344) -> bool {
345    config.firecracker.jailer.is_some()
346        && !caller_supplied_boot
347        && spec.network.mode == "tap"
348        && !spec.boot_args.contains("ip=")
349        && spec.mounts.is_empty()
350        && spec.ssh_public_key.is_none()
351        && spec.vcpus == warm.vcpus
352        && spec.memory_mib == warm.memory_mib
353}
354
355/// Copy `src` to `dst` (mode 0600, fsynced), reflinking when the filesystem
356/// supports it. Returns the byte length copied.
357fn clone_or_copy(src: &Path, dst: &Path) -> Result<u64> {
358    #[cfg(target_os = "linux")]
359    {
360        use std::os::fd::AsRawFd as _;
361        use std::os::unix::fs::OpenOptionsExt as _;
362        let source = std::fs::File::open(src).map_err(ComputerError::Io)?;
363        let dest = std::fs::OpenOptions::new()
364            .write(true)
365            .create_new(true)
366            .mode(0o600)
367            .open(dst)
368            .map_err(ComputerError::Io)?;
369        // SAFETY: both fds are open for the duration of the call; FICLONE
370        // only clones extents from the source fd into the destination fd.
371        let rc = unsafe { libc::ioctl(dest.as_raw_fd(), libc::FICLONE as _, source.as_raw_fd()) };
372        if rc == 0 {
373            dest.sync_all().map_err(ComputerError::Io)?;
374            return Ok(source.metadata().map_err(ComputerError::Io)?.len());
375        }
376        // Reflink unsupported here (non-Btrfs staging, cross-subvolume
377        // boundary) — fall back to a plain copy below.
378        drop(dest);
379        let _ = std::fs::remove_file(dst);
380    }
381    let bytes = std::fs::copy(src, dst).map_err(ComputerError::Io)?;
382    let dest = std::fs::File::open(dst).map_err(ComputerError::Io)?;
383    std::fs::set_permissions(dst, {
384        use std::os::unix::fs::PermissionsExt as _;
385        std::fs::Permissions::from_mode(0o600)
386    })
387    .map_err(ComputerError::Io)?;
388    dest.sync_all().map_err(ComputerError::Io)?;
389    Ok(bytes)
390}
391
392#[cfg(test)]
393mod tests {
394    use std::collections::HashMap;
395    use std::path::Path;
396
397    use crate::config::RuntimeConfig;
398    use crate::sandbox::ComputerManager;
399    use crate::snapshot::{SnapshotCatalog, SnapshotDraft};
400    use crate::template_catalog::{
401        TEMPLATE_LABEL, TemplateDefaultsSpec, TemplateEntry, WarmArtifact,
402    };
403
404    async fn manager(data_dir: &Path) -> ComputerManager {
405        let mut config = RuntimeConfig::default();
406        config.firecracker.data_dir = data_dir.to_string_lossy().into_owned();
407        let environment = crate::testkit::fake_environment(&config).unwrap();
408        let manager = ComputerManager::new(config, environment).unwrap();
409        manager.await_reconcile().await.unwrap();
410        manager
411    }
412
413    fn publish_template_snapshot(catalog: &SnapshotCatalog, template: &str) -> String {
414        let pending = catalog.begin(&format!("template-{template}")).unwrap();
415        std::fs::write(pending.dir().join("vmstate"), b"vmstate").unwrap();
416        pending
417            .commit(SnapshotDraft {
418                name: None,
419                labels: HashMap::from([(TEMPLATE_LABEL.to_owned(), template.to_owned())]),
420                snapshot_type: crate::config::SnapshotType::Full,
421                parent_id: None,
422                kernel_path: None,
423                rootfs_path: Some("/cache/rootfs-tpl.ext4".into()),
424                net_invariant: true,
425                geometry: None,
426                format: crate::sandbox::checkpoint::CHECKPOINT_FORMAT.to_owned(),
427            })
428            .unwrap()
429            .id
430    }
431
432    fn entry_with_warm(snapshot_id: &str) -> TemplateEntry {
433        TemplateEntry {
434            version: String::new(),
435            digest: "sha256:d1".into(),
436            rootfs_path: "/cache/rootfs-tpl.ext4".into(),
437            warm: Some(WarmArtifact {
438                snapshot_id: snapshot_id.to_owned(),
439                vcpus: 2,
440                memory_mib: 512,
441            }),
442            defaults: TemplateDefaultsSpec::default(),
443            labels: HashMap::new(),
444            created_at: chrono::Utc::now(),
445            size_bytes: 0,
446        }
447    }
448
449    #[tokio::test]
450    async fn template_snapshots_are_hidden_and_delete_refused_while_referenced() {
451        let dir = tempfile::tempdir().unwrap();
452        let m = manager(dir.path()).await;
453        let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
454        let snapshot_id = publish_template_snapshot(&catalog, "code");
455        m.register_template_draft("code", entry_with_warm(&snapshot_id))
456            .await
457            .unwrap();
458
459        let listed = m.list_checkpoints(None).unwrap();
460        assert!(
461            listed.iter().all(|s| s.id != snapshot_id),
462            "template snapshot leaked into the user checkpoint list"
463        );
464
465        let err = m.delete_checkpoint(&snapshot_id).await.unwrap_err();
466        assert!(
467            err.to_string().contains("owned by template"),
468            "unexpected refusal message: {err}"
469        );
470        assert!(
471            catalog.find_by_id(&snapshot_id).is_ok(),
472            "guard must not delete"
473        );
474    }
475
476    #[tokio::test]
477    async fn promotion_copies_the_checkpoint_with_independent_lifetime() {
478        let dir = tempfile::tempdir().unwrap();
479        let m = manager(dir.path()).await;
480        let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
481        // A user checkpoint with recorded geometry and a memory file.
482        let pending = catalog.begin("box-1").unwrap();
483        std::fs::write(pending.dir().join("vmstate"), b"vmstate").unwrap();
484        std::fs::write(pending.dir().join("mem"), b"memory-image").unwrap();
485        let source_id = pending
486            .commit(crate::snapshot::SnapshotDraft {
487                name: Some("user-checkpoint".into()),
488                labels: HashMap::new(),
489                snapshot_type: crate::config::SnapshotType::Full,
490                parent_id: None,
491                kernel_path: Some("/kernel".into()),
492                rootfs_path: Some("/cache/rootfs-tpl.ext4".into()),
493                net_invariant: true,
494                geometry: Some(crate::snapshot::SnapshotGeometry {
495                    vcpus: 2,
496                    memory_mib: 512,
497                }),
498                format: crate::sandbox::checkpoint::CHECKPOINT_FORMAT.to_owned(),
499            })
500            .unwrap()
501            .id;
502
503        let promoted = m
504            .promote_snapshot_to_template(&source_id, "code")
505            .await
506            .unwrap();
507        assert_ne!(promoted.snapshot_id, source_id);
508        assert_eq!(promoted.geometry.vcpus, 2);
509        assert_eq!(promoted.rootfs_path, "/cache/rootfs-tpl.ext4");
510        assert_eq!(
511            promoted.artifact_bytes,
512            b"vmstate".len() as u64 + b"memory-image".len() as u64
513        );
514
515        let copy = catalog.find_by_id(&promoted.snapshot_id).unwrap();
516        assert_eq!(
517            copy.labels.get(TEMPLATE_LABEL).map(String::as_str),
518            Some("code")
519        );
520        assert!(copy.net_invariant);
521
522        // Deleting the origin leaves the template copy intact.
523        m.delete_checkpoint(&source_id).await.unwrap();
524        assert!(catalog.find_by_id(&promoted.snapshot_id).is_ok());
525    }
526
527    #[tokio::test]
528    async fn promotion_refuses_a_snapshot_without_geometry() {
529        let dir = tempfile::tempdir().unwrap();
530        let m = manager(dir.path()).await;
531        let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
532        let source_id = publish_template_snapshot(&catalog, "seed");
533
534        let err = m
535            .promote_snapshot_to_template(&source_id, "code")
536            .await
537            .unwrap_err();
538        assert!(
539            err.to_string().contains("predates geometry recording"),
540            "unexpected error: {err}"
541        );
542    }
543
544    #[tokio::test]
545    async fn an_orphaned_template_snapshot_is_reclaimable_by_id() {
546        let dir = tempfile::tempdir().unwrap();
547        let m = manager(dir.path()).await;
548        let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
549        // Labeled, but no catalog record references it — the failure-mode
550        // orphan. DeleteSnapshot by id is the recovery path and must work.
551        let snapshot_id = publish_template_snapshot(&catalog, "ghost");
552
553        m.delete_checkpoint(&snapshot_id).await.unwrap();
554        assert!(catalog.find_by_id(&snapshot_id).is_err());
555    }
556
557    #[tokio::test]
558    async fn deleting_a_template_deletes_its_owned_snapshot() {
559        let dir = tempfile::tempdir().unwrap();
560        let m = manager(dir.path()).await;
561        let catalog = SnapshotCatalog::new(dir.path().to_str().unwrap());
562        let snapshot_id = publish_template_snapshot(&catalog, "code");
563        m.register_template_draft("code", entry_with_warm(&snapshot_id))
564            .await
565            .unwrap();
566
567        m.delete_template("code").await.unwrap();
568        assert!(
569            catalog.find_by_id(&snapshot_id).is_err(),
570            "snapshot must be reclaimed"
571        );
572        assert!(m.get_template("code").is_err());
573    }
574}