Skip to content

Commit 83a16ed

Browse files
committed
Add snapshotContainer API
snapshotContainer creates a "full container" snapshot, which includes the entire disk/filesystem and (optionally) the state of the container memory when the snapshot is created. Unlike directory snapshots, only a single container snapshot can be restored (but multiple directory snapshots can be restored alongside a container snapshot).
1 parent 9a696e4 commit 83a16ed

8 files changed

Lines changed: 185 additions & 44 deletions

File tree

src/workerd/api/container.c++

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010

1111
#include <kj/filesystem.h>
1212

13-
#include <cmath>
14-
1513
namespace workerd::api {
1614

1715
namespace {
@@ -99,14 +97,16 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
9997
}
10098

10199
if (!flags.getWorkerdExperimental()) {
102-
JSG_REQUIRE(options.snapshots == kj::none, Error,
100+
JSG_REQUIRE(options.directorySnapshots == kj::none, Error,
103101
"Container snapshot restore requires the 'experimental' compatibility flag.");
102+
JSG_REQUIRE(options.containerSnapshot == kj::none, Error,
103+
"Container full snapshot restore requires the 'experimental' compatibility flag.");
104104
} else {
105-
KJ_IF_SOME(snapshots, options.snapshots) {
106-
auto list = req.initSnapshots(snapshots.size());
107-
for (auto i: kj::indices(snapshots)) {
105+
KJ_IF_SOME(directorySnapshots, options.directorySnapshots) {
106+
auto list = req.initDirectorySnapshots(directorySnapshots.size());
107+
for (auto i: kj::indices(directorySnapshots)) {
108108
auto entry = list[i];
109-
auto& restore = snapshots[i];
109+
auto& restore = directorySnapshots[i];
110110
auto& snap = restore.snapshot;
111111
auto effectiveRestoreDir = snap.dir.asPtr();
112112
KJ_IF_SOME(mp, restore.mountPoint) {
@@ -116,13 +116,8 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
116116
JSG_REQUIRE_NONNULL(parseRestorePath(effectiveRestoreDir), Error,
117117
"Directory snapshot cannot be restored to root directory.");
118118

119-
double size = snap.size;
120-
JSG_REQUIRE(std::isfinite(size) && size >= 0 &&
121-
size <= static_cast<double>(jsg::MAX_SAFE_INTEGER) && std::floor(size) == size,
122-
RangeError, "Snapshot size must be a non-negative integer <= Number.MAX_SAFE_INTEGER");
123119
auto snapshotBuilder = entry.initSnapshot();
124120
snapshotBuilder.setId(snap.id);
125-
snapshotBuilder.setSize(static_cast<uint64_t>(size));
126121
snapshotBuilder.setDir(snap.dir);
127122
KJ_IF_SOME(name, snap.name) {
128123
snapshotBuilder.setName(name);
@@ -132,6 +127,14 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
132127
}
133128
}
134129
}
130+
131+
KJ_IF_SOME(containerSnapshot, options.containerSnapshot) {
132+
auto builder = req.initContainerSnapshot();
133+
builder.setId(containerSnapshot.id);
134+
KJ_IF_SOME(name, containerSnapshot.name) {
135+
builder.setName(name);
136+
}
137+
}
135138
}
136139

137140
req.setCompatibilityFlags(flags);
@@ -160,17 +163,45 @@ jsg::Promise<Container::DirectorySnapshot> Container::snapshotDirectory(
160163
.then(
161164
js, [](jsg::Lock& js, capnp::Response<rpc::Container::SnapshotDirectoryResults> results) {
162165
auto snapshot = results.getSnapshot();
166+
JSG_REQUIRE(snapshot.getSize() <= jsg::MAX_SAFE_INTEGER, RangeError,
167+
"Snapshot size exceeds Number.MAX_SAFE_INTEGER");
168+
163169
jsg::Optional<kj::String> name = kj::none;
164-
auto snapshotName = snapshot.getName();
165-
if (snapshotName.size() > 0) {
166-
name = kj::str(snapshotName);
170+
if (snapshot.getName().size() > 0) {
171+
name = kj::str(snapshot.getName());
167172
}
168173

174+
return Container::DirectorySnapshot{kj::str(snapshot.getId()),
175+
static_cast<double>(snapshot.getSize()), kj::str(snapshot.getDir()), kj::mv(name)};
176+
});
177+
}
178+
179+
jsg::Promise<Container::Snapshot> Container::snapshotContainer(
180+
jsg::Lock& js, SnapshotOptions options) {
181+
JSG_REQUIRE(
182+
running, Error, "snapshotContainer() cannot be called on a container that is not running.");
183+
184+
auto req = rpcClient->snapshotContainerRequest();
185+
186+
KJ_IF_SOME(name, options.name) {
187+
req.setName(name);
188+
}
189+
190+
return IoContext::current()
191+
.awaitIo(js, req.send())
192+
.then(
193+
js, [](jsg::Lock& js, capnp::Response<rpc::Container::SnapshotContainerResults> results) {
194+
auto snapshot = results.getSnapshot();
169195
JSG_REQUIRE(snapshot.getSize() <= jsg::MAX_SAFE_INTEGER, RangeError,
170196
"Snapshot size exceeds Number.MAX_SAFE_INTEGER");
171197

172-
return Container::DirectorySnapshot{kj::str(snapshot.getId()),
173-
static_cast<double>(snapshot.getSize()), kj::str(snapshot.getDir()), kj::mv(name)};
198+
jsg::Optional<kj::String> name = kj::none;
199+
if (snapshot.getName().size() > 0) {
200+
name = kj::str(snapshot.getName());
201+
}
202+
203+
return Container::Snapshot{
204+
kj::str(snapshot.getId()), static_cast<double>(snapshot.getSize()), kj::mv(name)};
174205
});
175206
}
176207

src/workerd/api/container.h

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,49 @@ class Container: public jsg::Object {
4141
jsg::Optional<kj::String> name;
4242

4343
JSG_STRUCT(dir, name);
44+
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
45+
if (!flags.getWorkerdExperimental()) {
46+
JSG_TS_OVERRIDE(type DirectorySnapshotOptions = never);
47+
}
48+
}
4449
};
4550

46-
struct SnapshotRestoreParams {
51+
struct DirectorySnapshotRestoreParams {
4752
DirectorySnapshot snapshot;
4853
jsg::Optional<kj::String> mountPoint;
4954

5055
JSG_STRUCT(snapshot, mountPoint);
5156
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
5257
if (!flags.getWorkerdExperimental()) {
53-
JSG_TS_OVERRIDE(type SnapshotRestoreParams = never);
58+
JSG_TS_OVERRIDE(type DirectorySnapshotRestoreParams = never);
59+
}
60+
}
61+
};
62+
63+
struct Snapshot {
64+
kj::String id;
65+
double size;
66+
jsg::Optional<kj::String> name;
67+
68+
JSG_STRUCT(id, size, name);
69+
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
70+
if (!flags.getWorkerdExperimental()) {
71+
JSG_TS_OVERRIDE(type Snapshot = never);
72+
}
73+
}
74+
};
75+
76+
struct SnapshotOptions {
77+
jsg::Optional<kj::String> name;
78+
79+
JSG_STRUCT(name);
80+
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
81+
if (flags.getWorkerdExperimental()) {
82+
JSG_TS_OVERRIDE(ContainerSnapshotOptions {
83+
name?: string;
84+
});
85+
} else {
86+
JSG_TS_OVERRIDE(type SnapshotOptions = never);
5487
}
5588
}
5689
};
@@ -61,11 +94,18 @@ class Container: public jsg::Object {
6194
jsg::Optional<jsg::Dict<kj::String>> env;
6295
jsg::Optional<int64_t> hardTimeout;
6396
jsg::Optional<jsg::Dict<kj::String>> labels;
64-
jsg::Optional<kj::Array<SnapshotRestoreParams>> snapshots;
97+
jsg::Optional<kj::Array<DirectorySnapshotRestoreParams>> directorySnapshots;
98+
jsg::Optional<Snapshot> containerSnapshot;
6599

66100
// TODO(containers): Allow intercepting stdin/stdout/stderr by specifying streams here.
67101

68-
JSG_STRUCT(entrypoint, enableInternet, env, hardTimeout, labels, snapshots);
102+
JSG_STRUCT(entrypoint,
103+
enableInternet,
104+
env,
105+
hardTimeout,
106+
labels,
107+
directorySnapshots,
108+
containerSnapshot);
69109
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
70110
if (flags.getWorkerdExperimental()) {
71111
JSG_TS_OVERRIDE(ContainerStartupOptions {
@@ -74,7 +114,8 @@ class Container: public jsg::Object {
74114
env?: Record<string, string>;
75115
hardTimeout?: number | bigint;
76116
labels?: Record<string, string>;
77-
snapshots?: ContainerSnapshotRestoreParams[];
117+
directorySnapshots?: ContainerDirectorySnapshotRestoreParams[];
118+
containerSnapshot?: ContainerSnapshot;
78119
});
79120
} else {
80121
JSG_TS_OVERRIDE(ContainerStartupOptions {
@@ -83,7 +124,8 @@ class Container: public jsg::Object {
83124
env?: Record<string, string>;
84125
hardTimeout?: never;
85126
labels?: Record<string, string>;
86-
snapshots?: never;
127+
directorySnapshots?: never;
128+
containerSnapshot?: never;
87129
});
88130
}
89131
}
@@ -107,6 +149,7 @@ class Container: public jsg::Object {
107149
jsg::Lock& js, kj::String addr, jsg::Ref<Fetcher> binding);
108150
jsg::Promise<DirectorySnapshot> snapshotDirectory(
109151
jsg::Lock& js, DirectorySnapshotOptions options);
152+
jsg::Promise<Snapshot> snapshotContainer(jsg::Lock& js, SnapshotOptions options);
110153

111154
// TODO(containers): listenTcp()
112155

@@ -124,6 +167,7 @@ class Container: public jsg::Object {
124167
if (flags.getWorkerdExperimental()) {
125168
JSG_METHOD(interceptOutboundHttps);
126169
JSG_METHOD(snapshotDirectory);
170+
JSG_METHOD(snapshotContainer);
127171
}
128172
}
129173

@@ -147,6 +191,7 @@ class Container: public jsg::Object {
147191

148192
#define EW_CONTAINER_ISOLATE_TYPES \
149193
api::Container, api::Container::DirectorySnapshot, api::Container::DirectorySnapshotOptions, \
150-
api::Container::SnapshotRestoreParams, api::Container::StartupOptions
194+
api::Container::DirectorySnapshotRestoreParams, api::Container::Snapshot, \
195+
api::Container::SnapshotOptions, api::Container::StartupOptions
151196

152197
} // namespace workerd::api

src/workerd/io/container.capnp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,19 @@ interface Container @0x9aaceefc06523bca {
4848
labels @5 :List(Label);
4949
# Optional key-value metadata labels for metrics/observability.
5050

51-
snapshots @6 :List(SnapshotRestoreParams);
51+
directorySnapshots @6 :List(DirectorySnapshotRestoreParams);
5252
# Directory snapshots to restore before the container starts.
53+
54+
containerSnapshot @7 :ContainerSnapshot;
55+
# Full container snapshot to restore before the container starts.
5356
}
5457

5558
struct Label {
5659
name @0 :Text;
5760
value @1 :Text;
5861
}
5962

60-
struct SnapshotRestoreParams {
63+
struct DirectorySnapshotRestoreParams {
6164
snapshot @0 :DirectorySnapshot;
6265
# The snapshot to restore.
6366

@@ -90,6 +93,24 @@ interface Container @0x9aaceefc06523bca {
9093
# Optional human-friendly name. Empty string means not set.
9194
}
9295

96+
struct ContainerSnapshot {
97+
# Opaque handle to a full container snapshot.
98+
99+
id @0 :Text;
100+
# Unique identifier of the snapshot.
101+
102+
size @1 :UInt64;
103+
# Snapshot size, in bytes.
104+
105+
name @2 :Text;
106+
# Optional human-friendly name. Empty string means not set.
107+
}
108+
109+
struct SnapshotContainerParams {
110+
name @0 :Text;
111+
# Optional human-friendly name. Empty string means not set.
112+
}
113+
93114
monitor @2 () -> (exitCode: Int32);
94115
# Waits for the container to shut down.
95116
#
@@ -181,4 +202,7 @@ interface Container @0x9aaceefc06523bca {
181202

182203
snapshotDirectory @10 SnapshotDirectoryParams -> (snapshot :DirectorySnapshot);
183204
# Creates a snapshot for a directory in the running container.
205+
206+
snapshotContainer @11 SnapshotContainerParams -> (snapshot :ContainerSnapshot);
207+
# Creates a full container snapshot for the running container.
184208
}

src/workerd/server/container-client.c++

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,15 +1524,18 @@ kj::Promise<void> ContainerClient::start(StartContext context) {
15241524

15251525
internetEnabled = params.getEnableInternet();
15261526

1527+
JSG_REQUIRE(!params.hasContainerSnapshot(), Error,
1528+
"container.start({ containerSnapshot }) is not implemented for local Docker containers yet.");
1529+
15271530
// If startup fails after we clone any snapshot volumes, tear down the app container first and
15281531
// then delete those clone volumes so we don't leave mounted Docker volumes behind.
15291532
KJ_DEFER(if (!containerStarted.load(std::memory_order_acquire)) {
15301533
waitUntilTasks.add(destroyContainer().attach(addRef()));
15311534
});
15321535

15331536
kj::Vector<SnapshotRestoreMount> restoreMounts;
1534-
if (params.hasSnapshots()) {
1535-
auto snapshotList = params.getSnapshots();
1537+
if (params.hasDirectorySnapshots()) {
1538+
auto snapshotList = params.getDirectorySnapshots();
15361539
restoreMounts.reserve(snapshotList.size());
15371540
for (auto i: kj::zeroTo(snapshotList.size())) {
15381541
auto entry = snapshotList[i];
@@ -1724,6 +1727,17 @@ kj::Promise<void> ContainerClient::snapshotDirectory(SnapshotDirectoryContext co
17241727
}
17251728
}
17261729

1730+
kj::Promise<void> ContainerClient::snapshotContainer(SnapshotContainerContext context) {
1731+
auto [ready, done] = getRpcTurn();
1732+
co_await ready;
1733+
KJ_DEFER(done->fulfill());
1734+
1735+
JSG_REQUIRE(containerStarted.load(std::memory_order_acquire), Error,
1736+
"snapshotContainer() requires a running container.");
1737+
JSG_FAIL_REQUIRE(
1738+
Error, "snapshotContainer() is not implemented for local Docker containers yet.");
1739+
}
1740+
17271741
kj::Promise<void> ContainerClient::getTcpPort(GetTcpPortContext context) {
17281742
co_await mutationQueue.addBranch();
17291743

src/workerd/server/container-client.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class ContainerClient final: public rpc::Container::Server, public kj::Refcounte
7777
kj::Promise<void> setEgressHttp(SetEgressHttpContext context) override;
7878
kj::Promise<void> setEgressHttps(SetEgressHttpsContext context) override;
7979
kj::Promise<void> snapshotDirectory(SnapshotDirectoryContext context) override;
80+
kj::Promise<void> snapshotContainer(SnapshotContainerContext context) override;
8081

8182
kj::Own<ContainerClient> addRef();
8283

0 commit comments

Comments
 (0)