Skip to content

Commit 60182d2

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 60182d2

8 files changed

Lines changed: 194 additions & 42 deletions

File tree

src/workerd/api/container.c++

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ kj::Maybe<kj::Path> parseRestorePath(kj::StringPtr path) {
3232
}
3333
}
3434

35+
uint64_t validateSnapshotSize(double size) {
36+
JSG_REQUIRE(std::isfinite(size) && size >= 0 &&
37+
size <= static_cast<double>(jsg::MAX_SAFE_INTEGER) && std::floor(size) == size,
38+
RangeError, "Snapshot size must be a non-negative integer <= Number.MAX_SAFE_INTEGER");
39+
return static_cast<uint64_t>(size);
40+
}
41+
3542
} // namespace
3643

3744
// =======================================================================================
@@ -99,14 +106,16 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
99106
}
100107

101108
if (!flags.getWorkerdExperimental()) {
102-
JSG_REQUIRE(options.snapshots == kj::none, Error,
109+
JSG_REQUIRE(options.directorySnapshots == kj::none, Error,
103110
"Container snapshot restore requires the 'experimental' compatibility flag.");
111+
JSG_REQUIRE(options.containerSnapshot == kj::none, Error,
112+
"Container full snapshot restore requires the 'experimental' compatibility flag.");
104113
} else {
105-
KJ_IF_SOME(snapshots, options.snapshots) {
106-
auto list = req.initSnapshots(snapshots.size());
107-
for (auto i: kj::indices(snapshots)) {
114+
KJ_IF_SOME(directorySnapshots, options.directorySnapshots) {
115+
auto list = req.initDirectorySnapshots(directorySnapshots.size());
116+
for (auto i: kj::indices(directorySnapshots)) {
108117
auto entry = list[i];
109-
auto& restore = snapshots[i];
118+
auto& restore = directorySnapshots[i];
110119
auto& snap = restore.snapshot;
111120
auto effectiveRestoreDir = snap.dir.asPtr();
112121
KJ_IF_SOME(mp, restore.mountPoint) {
@@ -116,13 +125,9 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
116125
JSG_REQUIRE_NONNULL(parseRestorePath(effectiveRestoreDir), Error,
117126
"Directory snapshot cannot be restored to root directory.");
118127

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");
123128
auto snapshotBuilder = entry.initSnapshot();
124129
snapshotBuilder.setId(snap.id);
125-
snapshotBuilder.setSize(static_cast<uint64_t>(size));
130+
snapshotBuilder.setSize(validateSnapshotSize(snap.size));
126131
snapshotBuilder.setDir(snap.dir);
127132
KJ_IF_SOME(name, snap.name) {
128133
snapshotBuilder.setName(name);
@@ -132,6 +137,15 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
132137
}
133138
}
134139
}
140+
141+
KJ_IF_SOME(containerSnapshot, options.containerSnapshot) {
142+
auto builder = req.initContainerSnapshot();
143+
builder.setId(containerSnapshot.id);
144+
builder.setSize(validateSnapshotSize(containerSnapshot.size));
145+
KJ_IF_SOME(name, containerSnapshot.name) {
146+
builder.setName(name);
147+
}
148+
}
135149
}
136150

137151
req.setCompatibilityFlags(flags);
@@ -160,17 +174,45 @@ jsg::Promise<Container::DirectorySnapshot> Container::snapshotDirectory(
160174
.then(
161175
js, [](jsg::Lock& js, capnp::Response<rpc::Container::SnapshotDirectoryResults> results) {
162176
auto snapshot = results.getSnapshot();
177+
JSG_REQUIRE(snapshot.getSize() <= jsg::MAX_SAFE_INTEGER, RangeError,
178+
"Snapshot size exceeds Number.MAX_SAFE_INTEGER");
179+
163180
jsg::Optional<kj::String> name = kj::none;
164-
auto snapshotName = snapshot.getName();
165-
if (snapshotName.size() > 0) {
166-
name = kj::str(snapshotName);
181+
if (snapshot.getName().size() > 0) {
182+
name = kj::str(snapshot.getName());
167183
}
168184

185+
return Container::DirectorySnapshot{kj::str(snapshot.getId()),
186+
static_cast<double>(snapshot.getSize()), kj::str(snapshot.getDir()), kj::mv(name)};
187+
});
188+
}
189+
190+
jsg::Promise<Container::Snapshot> Container::snapshotContainer(
191+
jsg::Lock& js, SnapshotOptions options) {
192+
JSG_REQUIRE(
193+
running, Error, "snapshotContainer() cannot be called on a container that is not running.");
194+
195+
auto req = rpcClient->snapshotContainerRequest();
196+
197+
KJ_IF_SOME(name, options.name) {
198+
req.setName(name);
199+
}
200+
201+
return IoContext::current()
202+
.awaitIo(js, req.send())
203+
.then(
204+
js, [](jsg::Lock& js, capnp::Response<rpc::Container::SnapshotContainerResults> results) {
205+
auto snapshot = results.getSnapshot();
169206
JSG_REQUIRE(snapshot.getSize() <= jsg::MAX_SAFE_INTEGER, RangeError,
170207
"Snapshot size exceeds Number.MAX_SAFE_INTEGER");
171208

172-
return Container::DirectorySnapshot{kj::str(snapshot.getId()),
173-
static_cast<double>(snapshot.getSize()), kj::str(snapshot.getDir()), kj::mv(name)};
209+
jsg::Optional<kj::String> name = kj::none;
210+
if (snapshot.getName().size() > 0) {
211+
name = kj::str(snapshot.getName());
212+
}
213+
214+
return Container::Snapshot{
215+
kj::str(snapshot.getId()), static_cast<double>(snapshot.getSize()), kj::mv(name)};
174216
});
175217
}
176218

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(Error,
1738+
"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)