2525#include < kj/exception.h>
2626#include < kj/string.h>
2727
28+ #include < atomic>
29+
2830namespace workerd ::server {
2931
3032namespace {
@@ -35,13 +37,17 @@ constexpr uint16_t SIDECAR_INGRESS_PORT = 39001;
3537constexpr uint64_t MAX_JSON_RESPONSE_SIZE = 16ULL * 1024 * 1024 ;
3638
3739constexpr kj::StringPtr SNAPSHOT_VOLUME_PREFIX = " workerd-snap-" _kj;
40+ constexpr kj::StringPtr SNAPSHOT_VOLUME_CREATED_AT_LABEL = " dev.workerd.snapshot-created-at" _kj;
41+ constexpr auto SNAPSHOT_STALE_AGE = 30 * kj::DAYS ;
3842
3943// Maximum size of a snapshot tar archive held in memory during snapshot create/restore.
4044constexpr size_t MAX_SNAPSHOT_TAR_SIZE = 1ULL * 1024 * 1024 * 1024 ; // 1 GiB
4145
4246static_assert (static_cast <double >(MAX_SNAPSHOT_TAR_SIZE ) == MAX_SNAPSHOT_TAR_SIZE ,
4347 " MAX_SNAPSHOT_TAR_SIZE must be exactly representable as double" );
4448
49+ std::atomic<bool > staleSnapshotVolumeCheckScheduled = false ;
50+
4551// Validate a snapshot directory path. Rejects relative paths, embedded null bytes,
4652// and path traversal components (".."). Returns the validated parent directory path
4753// (with leading '/') suitable for the Docker archive API.
@@ -357,6 +363,64 @@ kj::Promise<DockerBinaryResponse> dockerApiBinaryRequest(kj::Network& network,
357363 bodyBytes, " application/x-tar" _kj, maxResponseSize);
358364}
359365
366+ kj::String currentSnapshotVolumeTimestamp () {
367+ return kj::str ((kj::systemPreciseCalendarClock ().now () - kj::UNIX_EPOCH ) / kj::SECONDS );
368+ }
369+
370+ kj::Maybe<int64_t > tryGetSnapshotCreatedAt (capnp::JsonValue::Reader labels) {
371+ if (!labels.isObject ()) {
372+ return kj::none;
373+ }
374+
375+ for (auto field: labels.getObject ()) {
376+ if (field.getName () != SNAPSHOT_VOLUME_CREATED_AT_LABEL ) {
377+ continue ;
378+ }
379+
380+ auto value = field.getValue ();
381+ if (!value.isString ()) {
382+ return kj::none;
383+ }
384+ return value.getString ().tryParseAs <int64_t >();
385+ }
386+
387+ return kj::none;
388+ }
389+
390+ kj::Promise<void > warnAboutStaleSnapshotVolumes (kj::Network& network, kj::String dockerPath) {
391+ capnp::JsonCodec codec;
392+ codec.handleByAnnotation <docker_api::Docker::VolumeListFilters>();
393+ capnp::MallocMessageBuilder filterMessage;
394+ auto filters = filterMessage.initRoot <docker_api::Docker::VolumeListFilters>();
395+ auto names = filters.initName (1 );
396+ names.set (0 , SNAPSHOT_VOLUME_PREFIX );
397+
398+ auto response = co_await dockerApiRequest (network, kj::mv (dockerPath), kj::HttpMethod::GET ,
399+ kj::str (" /volumes?filters=" , kj::encodeUriComponent (codec.encode (filters))));
400+ if (response.statusCode != 200 ) {
401+ co_return ;
402+ }
403+
404+ auto message = decodeJsonResponse<docker_api::Docker::VolumeListResponse>(response.body );
405+ auto root = message->getRoot <docker_api::Docker::VolumeListResponse>();
406+ auto now = kj::systemPreciseCalendarClock ().now ();
407+ kj::Vector<kj::String> staleVolumes;
408+
409+ for (auto volume: root.getVolumes ()) {
410+ KJ_IF_SOME (createdAtSeconds, tryGetSnapshotCreatedAt (volume.getLabels ())) {
411+ auto createdAt = kj::UNIX_EPOCH + createdAtSeconds * kj::SECONDS ;
412+ if (now - createdAt >= SNAPSHOT_STALE_AGE ) {
413+ staleVolumes.add (kj::str (volume.getName ()));
414+ }
415+ }
416+ }
417+
418+ if (staleVolumes.size () > 0 ) {
419+ KJ_LOG (WARNING , " the following snapshot volumes were created 30+ days ago and may be stale" ,
420+ kj::strArray (staleVolumes, " , " ));
421+ }
422+ }
423+
360424} // namespace
361425
362426ContainerClient::ContainerClient (capnp::ByteStreamFactory& byteStreamFactory,
@@ -381,7 +445,14 @@ ContainerClient::ContainerClient(capnp::ByteStreamFactory& byteStreamFactory,
381445 waitUntilTasks(waitUntilTasks),
382446 pendingCleanup(kj::mv(pendingCleanup).fork()),
383447 cleanupCallback(kj::mv(cleanupCallback)),
384- channelTokenHandler(channelTokenHandler) {}
448+ channelTokenHandler(channelTokenHandler) {
449+ if (!staleSnapshotVolumeCheckScheduled.exchange (true , std::memory_order_relaxed)) {
450+ waitUntilTasks.add (warnAboutStaleSnapshotVolumes (network, kj::str (this ->dockerPath ))
451+ .catch_ ([](kj::Exception&& e) {
452+ KJ_LOG (WARNING , " failed to inspect snapshot volumes for staleness" , e);
453+ }));
454+ }
455+ }
385456
386457ContainerClient::~ContainerClient () noexcept (false ) {
387458 stopEgressListener ();
@@ -1055,6 +1126,9 @@ kj::Promise<void> ContainerClient::createDockerVolume(kj::StringPtr volumeName)
10551126 capnp::MallocMessageBuilder message;
10561127 auto req = message.initRoot <docker_api::Docker::VolumeCreateRequest>();
10571128 req.setName (volumeName);
1129+ auto labels = req.initLabels ().initObject (1 );
1130+ labels[0 ].setName (SNAPSHOT_VOLUME_CREATED_AT_LABEL );
1131+ labels[0 ].initValue ().setString (currentSnapshotVolumeTimestamp ());
10581132
10591133 auto response = co_await dockerApiRequest (network, kj::str (dockerPath), kj::HttpMethod::POST ,
10601134 kj::str (" /volumes/create" ), codec.encode (req));
@@ -1334,6 +1408,7 @@ kj::Promise<void> ContainerClient::snapshotDirectory(SnapshotDirectoryContext co
13341408 " ': " , putResponse.statusCode );
13351409
13361410 volumeCommitted = true ;
1411+ KJ_LOG (INFO , " created snapshot volume" , volumeName, dir, tarSize);
13371412
13381413 // Populate the capnp response.
13391414 auto result = context.getResults ().initSnapshot ();
0 commit comments