Skip to content

Commit 49a5f5b

Browse files
committed
fork test: assert mem-file at snapshot-base after firecracker restore
Firecracker enables snapshot base reuse, which renames the post-restore snapshot dir from snapshot-latest to snapshot-base. The hardlink survives the rename (same inode), so the test just needs the right path.
1 parent 44c658e commit 49a5f5b

1 file changed

Lines changed: 39 additions & 20 deletions

File tree

lib/instances/firecracker_test.go

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"strings"
13+
"syscall"
1314
"testing"
1415
"time"
1516

@@ -554,13 +555,15 @@ func TestFirecrackerSnapshotFeature(t *testing.T) {
554555

555556
// TestFirecrackerForkFromTemplate exercises the full template-driven fork
556557
// path under the state-machine design: a firecracker source goes Running →
557-
// Standby, the first fork implicitly promotes it to Template, and the fork:
558+
// Standby → Template (explicit promote), then a fork:
558559
//
559560
// (a) reaches Running,
560-
// (b) has its mem-file as a symlink into the template's snapshot dir,
561-
// (c) bumps the template's ForkCount to 1,
561+
// (b) has its mem-file hardlinked to the source's snapshot mem-file
562+
// (the fan-out optimisation),
563+
// (c) is counted as a live fork of the template,
562564
// (d) registers with the per-template uffd page server,
563-
// (e) on delete, decrements the ForkCount and detaches from uffd.
565+
// (e) on delete, the fork count drops back to 0 and the fork detaches
566+
// from uffd.
564567
func TestFirecrackerForkFromTemplate(t *testing.T) {
565568
t.Parallel()
566569
requireFirecrackerIntegrationPrereqs(t)
@@ -600,7 +603,11 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
600603
require.Equal(t, StateStandby, source.State)
601604
require.True(t, source.HasSnapshot)
602605

603-
// Forking from the standby source implicitly promotes it to Template.
606+
// Promote to Template explicitly — only Template sources get fan-out.
607+
source, err = mgr.PromoteToTemplate(ctx, sourceID)
608+
require.NoError(t, err)
609+
require.Equal(t, StateTemplate, source.State)
610+
604611
forked, err := mgr.ForkInstance(ctx, sourceID, ForkInstanceRequest{
605612
Name: "fc-tpl-fork",
606613
TargetState: StateRunning,
@@ -617,21 +624,33 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
617624
}
618625
})
619626

620-
// (b) The fork's mem-file must be a symlink into the source's snapshot.
621-
forkMemPath := filepath.Join(p.InstanceSnapshotLatest(forkID), templateSharedMemFileName)
622-
info, err := os.Lstat(forkMemPath)
623-
require.NoError(t, err, "fork mem-file should exist at snapshot-latest/memory")
624-
assert.Equal(t, os.ModeSymlink, info.Mode()&os.ModeSymlink, "fork mem-file should be a symlink, not a copy")
625-
target, err := os.Readlink(forkMemPath)
626-
require.NoError(t, err)
627-
expectedTarget := filepath.Join(p.InstanceSnapshotLatest(sourceID), templateSharedMemFileName)
628-
assert.Equal(t, expectedTarget, target, "fork mem-file symlink should point at the template's mem-file")
629-
630-
// (c) The source instance is now a Template with ForkCount=1.
627+
// (b) The fork's mem-file must share the source's inode (hardlink), not
628+
// be a copy. We can't compare paths because the link is by inode; we
629+
// compare st_ino + st_dev between the two instances' mem-files.
630+
//
631+
// Firecracker retains the post-restore snapshot dir as snapshot-base
632+
// (see restoreRetainedSnapshotBase), so after the Standby -> Running
633+
// transition the hardlink lives under snapshot-base/, not snapshot-latest/.
634+
// Hardlinks survive the rename because they bind to the inode.
635+
forkMemPath := filepath.Join(p.InstanceSnapshotBase(forkID), templateSharedMemFileName)
636+
srcMemPath := filepath.Join(p.InstanceSnapshotLatest(sourceID), templateSharedMemFileName)
637+
forkInfo, err := os.Stat(forkMemPath)
638+
require.NoError(t, err, "fork mem-file should exist at snapshot-base/memory after restore")
639+
assert.True(t, forkInfo.Mode().IsRegular(), "fork mem-file should be a regular file (hardlink), not a symlink")
640+
srcInfo, err := os.Stat(srcMemPath)
641+
require.NoError(t, err)
642+
forkSys := forkInfo.Sys().(*syscall.Stat_t)
643+
srcSys := srcInfo.Sys().(*syscall.Stat_t)
644+
assert.Equal(t, srcSys.Ino, forkSys.Ino, "fork mem-file should share the source's inode (hardlink, not copy)")
645+
assert.Equal(t, srcSys.Dev, forkSys.Dev, "fork mem-file should be on the same filesystem as source")
646+
647+
// (c) The source instance is a Template with exactly one live fork.
631648
sourceMeta, err := mgr.loadMetadata(sourceID)
632649
require.NoError(t, err)
633-
assert.True(t, sourceMeta.StoredMetadata.IsTemplate, "source should be promoted to Template on first fork")
634-
assert.Equal(t, 1, sourceMeta.StoredMetadata.ForkCount, "template fork refcount should be 1 after one fork")
650+
assert.True(t, sourceMeta.StoredMetadata.IsTemplate, "source should be a Template")
651+
forks, err := mgr.countTemplateForks(sourceID)
652+
require.NoError(t, err)
653+
assert.Equal(t, 1, forks, "template fork count should be 1 after one fork")
635654

636655
// (d) The per-template uffd page server should be tracking this fork.
637656
require.NotNil(t, mgr.uffd)
@@ -641,8 +660,8 @@ func TestFirecrackerForkFromTemplate(t *testing.T) {
641660
require.NoError(t, mgr.DeleteInstance(ctx, forkID))
642661
deletedFork = true
643662

644-
sourceMetaAfter, err := mgr.loadMetadata(sourceID)
663+
forksAfter, err := mgr.countTemplateForks(sourceID)
645664
require.NoError(t, err)
646-
assert.Equal(t, 0, sourceMetaAfter.StoredMetadata.ForkCount, "template fork refcount should drop back to 0")
665+
assert.Equal(t, 0, forksAfter, "template fork count should drop back to 0")
647666
assert.False(t, mgr.uffd.hasFork(sourceID, forkID), "uffd tracker should no longer track the deleted fork")
648667
}

0 commit comments

Comments
 (0)