cache: shared-memory-backed Dir for fast restart - #13328
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an opt-in “fast restart” cache directory implementation by hosting each stripe’s in-memory directory (raw_dir) in POSIX shared memory, allowing subsequent traffic_server starts to attach the prior directory quickly instead of rebuilding from disk. It also adds operator tooling (traffic_ctl cache shm status|clear), configuration records, extensive AuTest coverage, unit tests for trust gates, and design/admin documentation.
Changes:
- Add shared-memory directory infrastructure (
CacheShm*) plus integration into cache startup, stripe directory allocation, and shutdown/clean-marking paths. - Add
traffic_ctl cache shm status|clearcommands and newproxy.config.cache.shm.*records. - Add unit + AuTest suites and documentation for the shm fast-restart feature.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/gold_tests/logging/ts_process_handler.py | Hardens psutil cmdline handling for macOS/permission-restricted processes. |
| tests/gold_tests/cache/shm_poke.py | Adds Linux-only helper to tamper with /dev/shm segments for trust-gate testing. |
| tests/gold_tests/cache/replay/cache-shm-fast-restart.replay.yaml | Proxy Verifier replay traffic used to validate hit-after-restart behavior. |
| tests/gold_tests/cache/gold/cache_shm_state_after_shutdown.gold | Gold output for traffic_ctl cache shm status validation. |
| tests/gold_tests/cache/cache_shm_unclean_shutdown.test.py | AuTest for rejecting dirty segments after SIGKILL and rebuilding from disk. |
| tests/gold_tests/cache/cache_shm_storage_mismatch.test.py | AuTest for partial attach behavior when storage layout changes. |
| tests/gold_tests/cache/cache_shm_schema_mismatch.test.py | Linux-only AuTest that pokes schema_version and verifies drop+rebuild. |
| tests/gold_tests/cache/cache_shm_purge_on_disable.test.py | AuTest for purge-on-disabled-start behavior and traffic_ctl exit codes. |
| tests/gold_tests/cache/cache_shm_fast_restart.test.py | End-to-end AuTest for clean shutdown -> shm attach -> cache HIT without origin contact. |
| tests/gold_tests/cache/cache_shm_concurrent_attach.test.py | AuTest for concurrent-attach guard (flock vs owner_pid liveness backstop). |
| tests/gold_tests/cache/cache_shm_bad_disk_dropped.test.py | AuTest for partial attach and orphan reclaim when a disk is removed from storage. |
| src/traffic_ctl/traffic_ctl.cc | Adds `traffic_ctl cache shm status |
| src/traffic_ctl/CMakeLists.txt | Builds the new traffic_ctl command source and adds cache include path for shared headers. |
| src/traffic_ctl/CacheShmCommand.h | Declares the traffic_ctl cache shm command handler. |
| src/traffic_ctl/CacheShmCommand.cc | Implements shm status/clear via direct shm_open/mmap, shared purge primitive, and exit codes. |
| src/records/RecordsConfig.cc | Registers proxy.config.cache.shm.* configuration records. |
| src/iocore/cache/unit_tests/test_CacheShm.cc | Adds unit tests for ABI hash, storage signature, layout round-trip, prefix normalization, and liveness checks. |
| src/iocore/cache/StripeSM.cc | Adds fast-attach path that can skip disk dir read + recovery when shm directory is trusted; adjusts shutdown behavior. |
| src/iocore/cache/Stripe.h | Adds _shm_directory_is_valid() and strengthens flush API via [[nodiscard]]. |
| src/iocore/cache/Stripe.cc | Uses shm-backed directory allocation; adds shm bounds validation; detaches shm mappings safely in destructor. |
| src/iocore/cache/CMakeLists.txt | Builds new CacheShm.cc and adds test_CacheShm to the cache unit tests. |
| src/iocore/cache/CacheShmPurge.h | Introduces shared header-only purge/enumerate/unlink primitive for server startup and traffic_ctl. |
| src/iocore/cache/CacheShmLayout.h | Defines the shared control-segment layout and prefix normalization utilities. |
| src/iocore/cache/CacheShm.h | Declares the CacheShm facade for lifecycle + stripe attach/create + trust gates. |
| src/iocore/cache/CacheShm.cc | Implements shm control segment lifecycle, trust gates, stripe attach/create, orphan reclaim, and clean-shutdown marking. |
| src/iocore/cache/CacheProcessor.cc | Calls CacheShm initialization before stripe construction and finalization after cache init. |
| src/iocore/cache/CacheDir.cc | Marks shm control segment clean only after shutdown sync has quiesced writers. |
| src/iocore/cache/AggregateWriteBuffer.h | Marks flush() as [[nodiscard]] to encourage handling short-write failures. |
| src/iocore/cache/AggregateWriteBuffer.cc | Converts flush failure from an assertion to a false return for graceful handling. |
| doc/developer-guide/cache-architecture/shm-fast-restart.en.rst | Adds detailed design doc: layout, gates, attach modes, shutdown semantics, tooling, and platform notes. |
| doc/developer-guide/cache-architecture/index.en.rst | Hooks shm fast-restart document into the cache architecture index. |
| doc/admin-guide/files/records.yaml.en.rst | Documents new proxy.config.cache.shm.* records and operator-facing behavior/tooling. |
8358d37 to
e0ee0af
Compare
There was a problem hiding this comment.
Approving. This is well built and safe to land. It is opt-in and off by default, the shared-memory attach path validates the mapping size before use and bounds the stripe count, and every failing system call is logged with its name and error string and then propagated back to the caller.
One correction to my earlier note. I wrote that a fast-attach bug returning a wrong offset or length would still pass CI. That is not accurate, and I withdraw it. The fast_restart test wires the origin to return a 502 on the hit transaction, so if the shared-memory directory failed to resolve the cached object, ATS would fall through to the origin and the 502 would fail the test. The real gap is narrower than I first stated.
The items below are all non-blocking for an off-by-default feature, but worth closing out before this is relied on in production.
-
The testing table in
doc/developer-guide/cache-architecture/shm-fast-restart.en.rstlists acache_shm_data_integrityautest that is not in this PR. Please either add the test or drop the row. -
The objects the tests serve are small and single-fragment. A multi-fragment or large-object case after a fast attach would cover the one path the 502 trick does not fully exercise.
-
The rejection branches in
_shm_directory_is_valid()and the invalidate-on-shutdown paths have no direct unit coverage, as the caveats note. They are close to pure header math, so each branch is a cheap unit test.
I also agree with the open Copilot threads on this PR.
-
In
CacheShm.cc:229,unlink_all_known_segments()unlinks stripe names read from the control segment. On the path that drops an untrusted or corrupt control segment those names are untrusted, so a corrupt segment could lead toshm_unlinkon unrelated objects. Since the names are always built fromname_prefixviabuild_stripe_shm_name(), gating the unlink loop on a prefix match, and only running it when the control magic is valid, closes this with no effect on the normal path. -
The three
storage.configreferences in the design doc at lines 132, 340, and 430 should bestorage.yamlto match the tests and the rest of this PR.
Thanks for the clean, well-documented change. The approval stands.
|
Thank you for taking a look. I addressed 1. 4. and 5. by 0cc3b52. |
91899f8 to
161f39c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/iocore/cache/AggregateWriteBuffer.cc:52
AggregateWriteBuffer::flush()relies on a singlepwrite()writing the entire buffer and stores the return value in anint. On regular files,pwrite()can legally return a short write or fail withEINTR, and the narrowing cast can mis-handle larger writes. With the new shutdown path treating a flush failure as a reason to invalidate the shm directory, these false negatives can unnecessarily drop the fast-restart optimization (and potentially lose buffered data on shutdown when the error was transient).
AggregateWriteBuffer::flush(int fd, off_t write_pos) const
{
int r = pwrite(fd, this->_buffer, this->_buffer_pos, write_pos);
if (r != this->_buffer_pos) {
return false;
src/iocore/cache/Stripe.cc:224
Stripe::_shm_directory_is_valid()does multiple full passes over every directory entry on fast-restart attach (a fullsegment_entriesscan, plus_shm_segment_membership_is_valid()'svisited.assign(...)+ finalforscan, plusdirectory.check_segment(s)walking all buckets). For large caches this is O(directory_size) in CPU and can substantially eat into the “fast restart” benefit even when the segment is clean and valid.
for (int64_t i = 0; i < segment_entries; i++) {
Dir *e = dir_in_seg(seg, i);
if (dir_next(e) >= segment_entries) {
return false;
}
Cold-start cache initialization rebuilds each stripe's in-memory directory from disk on every restart, which is multi-minute on large caches. Host the directory in POSIX shared memory so the next process start attaches the existing segment in milliseconds instead of rebuilding it. Recovery stays binary and fail-safe: when the segment cannot be trusted -- crash, reboot, ABI or schema mismatch, storage change, or failed validation -- the start drops it and rebuilds through the existing disk path. Reads still validate Doc magic and key, so a stale entry is a miss and never corruption. A stripe the previous shutdown could not vouch for is marked in the control segment, never in the stripe's own header: that header aliases raw_dir, which is also the source buffer for the on-disk directory write, so a mark there could reach disk and make the next start clear the stripe instead of recovering it. Opt-in behind proxy.config.cache.shm.enabled, default 0, where it is a functional no-op. `traffic_ctl cache shm status` and `clear` inspect and drop segments out of band. The design and the full recovery matrix are in doc/developer-guide/cache-architecture/shm-fast-restart.en.rst.
The status/clear leaves passed their own `[&]() { command->execute(); }`
lambda, where every other leaf in the file passes Command_Execute.
Reuse it so the null guard applies and the wiring is uniform. No
behavior change: `command` is assigned before args.invoke() and a null
one throws, so the guard cannot fire today.
The untrusted-entry test simulated a restart by calling initialize() again in one process, which the concurrent-attach guard correctly refuses: the first start still holds LOCK_EX on the control fd, and the second open of the same object conflicts with it. The test only passed where flock is not honored for POSIX shm (macOS, FreeBSD) and failed on Fedora and Debian; the other Linux builds compile the feature out, so the target is not built there at all. Add CacheShm::release_for_test() to drop the process-wide state, standing in for the process exit that releases the flock in production.
Skipping the write for a shm-backed stripe looked free -- the segment is already current and is attached directly next start -- but the on-disk copy is the only thing the fallback has, and recover_data() cannot always rebuild from it. handle_recover_from_data() returns without scanning the data region when the on-disk header still has sync_serial == 0, so an empty directory is accepted as-is. A stripe filled and cleanly shut down before the first periodic dir sync (60 s by default) is exactly that case: nothing had written the on-disk dir, so the next start that cannot use the segment found an empty directory and lost every object. cache_shm_dir_invalid caught it as a 502 against its deliberately absent origin, once the poked segment was correctly rejected. Only start time is what this feature set out to improve, so the shutdown write costs what it did before and the fallback stays recoverable.
Review of the fast-restart path found four ways a shared-memory segment could be trusted, or cleared, when it should not be. The attach gate bounded directory links but neither a live entry's offset -- which CacheVC::handleRead turns into a negative, so huge unsigned, read length -- nor the free list's structure, where an in-range cycle lets freelist_pop write a link over a live entry's tag bits. Clean shutdown cleared owner_pid while event threads were still writing, which on a platform where flock is a no-op is the only guard against a concurrent attach. And traffic_ctl swept a control segment smaller than this build's before checking for a live owner, so a newer build could unlink a running older build's segments -- the upgrade case the frozen header exists to support. The entry bound is on where an entry starts, not on its extent: dir_approx_size rounds up, so the last object in a stripe legitimately overhangs the stripe end, which is what handleRead's truncation is for.
A walk from the free-list head cannot see an entry that Directory::insert unlinked but never filled, so a shutdown torn in that window published a directory whose stale prev/next the next insert would write through. Require every entry to be reached exactly once, as a bucket root, an empty free-list node, or an in-use chain node, and delay the clean-shutdown mark until after the event system is down so fewer tears reach the gate at all.
2cfbb88 to
cd4bff9
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/traffic_ctl/CacheShmCommand.cc:65
shm_segment_exists()reports a stripe segment as absent whenevershm_open()fails. That includesEACCES, which means the segment likely exists but is not readable by the current user; printingMISSINGin that case is misleading during troubleshooting.
Treat EACCES as “exists” (or surface the errno in the output) so traffic_ctl cache shm status doesn’t incorrectly claim a segment is missing.
bool
shm_segment_exists(const std::string &name)
{
int fd = shm_open(name.c_str(), O_RDONLY, 0);
if (fd < 0) {
return false;
}
close(fd);
return true;
}
* cache: shared-memory-backed Dir for fast restart
Cold-start cache initialization rebuilds each stripe's in-memory
directory from disk on every restart, which is multi-minute on large
caches. Host the directory in POSIX shared memory so the next process
start attaches the existing segment in milliseconds instead of
rebuilding it.
Recovery stays binary and fail-safe: when the segment cannot be
trusted -- crash, reboot, ABI or schema mismatch, storage change, or
failed validation -- the start drops it and rebuilds through the
existing disk path. Reads still validate Doc magic and key, so a
stale entry is a miss and never corruption. A stripe the previous
shutdown could not vouch for is marked in the control segment, never
in the stripe's own header: that header aliases raw_dir, which is
also the source buffer for the on-disk directory write, so a mark
there could reach disk and make the next start clear the stripe
instead of recovering it.
Opt-in behind proxy.config.cache.shm.enabled, default 0, where it is
a functional no-op. `traffic_ctl cache shm status` and `clear`
inspect and drop segments out of band. The design and the full
recovery matrix are in
doc/developer-guide/cache-architecture/shm-fast-restart.en.rst.
* traffic_ctl: run cache shm subcommands through Command_Execute
The status/clear leaves passed their own `[&]() { command->execute(); }`
lambda, where every other leaf in the file passes Command_Execute.
Reuse it so the null guard applies and the wiring is uniform. No
behavior change: `command` is assigned before args.invoke() and a null
one throws, so the guard cannot fire today.
* cache: fix shm shutdown test where flock is honored
The untrusted-entry test simulated a restart by calling initialize()
again in one process, which the concurrent-attach guard correctly
refuses: the first start still holds LOCK_EX on the control fd, and the
second open of the same object conflicts with it. The test only passed
where flock is not honored for POSIX shm (macOS, FreeBSD) and failed on
Fedora and Debian; the other Linux builds compile the feature out, so
the target is not built there at all.
Add CacheShm::release_for_test() to drop the process-wide state,
standing in for the process exit that releases the flock in production.
* cache: always write the on-disk dir at shm shutdown
Skipping the write for a shm-backed stripe looked free -- the segment is
already current and is attached directly next start -- but the on-disk
copy is the only thing the fallback has, and recover_data() cannot
always rebuild from it. handle_recover_from_data() returns without
scanning the data region when the on-disk header still has
sync_serial == 0, so an empty directory is accepted as-is.
A stripe filled and cleanly shut down before the first periodic dir
sync (60 s by default) is exactly that case: nothing had written the
on-disk dir, so the next start that cannot use the segment found an
empty directory and lost every object. cache_shm_dir_invalid caught it
as a 502 against its deliberately absent origin, once the poked segment
was correctly rejected.
Only start time is what this feature set out to improve, so the
shutdown write costs what it did before and the fallback stays
recoverable.
* cache: harden the shm trust gates and ownership guard
Review of the fast-restart path found four ways a shared-memory segment
could be trusted, or cleared, when it should not be. The attach gate
bounded directory links but neither a live entry's offset -- which
CacheVC::handleRead turns into a negative, so huge unsigned, read length
-- nor the free list's structure, where an in-range cycle lets
freelist_pop write a link over a live entry's tag bits. Clean shutdown
cleared owner_pid while event threads were still writing, which on a
platform where flock is a no-op is the only guard against a concurrent
attach. And traffic_ctl swept a control segment smaller than this build's
before checking for a live owner, so a newer build could unlink a running
older build's segments -- the upgrade case the frozen header exists to
support.
The entry bound is on where an entry starts, not on its extent:
dir_approx_size rounds up, so the last object in a stripe legitimately
overhangs the stripe end, which is what handleRead's truncation is for.
* cache: prove shm segment membership at attach
A walk from the free-list head cannot see an entry that
Directory::insert unlinked but never filled, so a shutdown torn in
that window published a directory whose stale prev/next the next
insert would write through. Require every entry to be reached exactly
once, as a bucket root, an empty free-list node, or an in-use chain
node, and delay the clean-shutdown mark until after the event system
is down so fewer tears reach the gate at all.
* Include algorithm header
(cherry picked from commit fc115b1)
|
Cherry-picked to the 10.2.x branch as abfbfcd for the 10.2.0 release. |
Picks up apache#13328 (shared-memory cache directory for fast restart) and apache#13418 (traffic_ctl cache clear). The shm directory gets its own section since it is a new opt-in feature with four new records and a traffic_ctl subcommand.
Picks up apache#13328 (shared-memory cache directory for fast restart) and apache#13418 (traffic_ctl cache clear). The shm directory gets its own section since it is a new opt-in feature with four new records and a traffic_ctl subcommand.
* Add 10.2.0 changelog and release notes Generate CHANGELOG-10.2.0 from the 10.2.0 milestone and document the release in whats-new and upgrading. The connect retry change (#13102) is called out as a necessary incompatible change, since the retry limits were not previously applied according to origin state. * Address review: fix PR count and token_key markup The PR count was 655 before five stale milestone entries were dropped; the changelog has 650. Use :ts:cv: for proxy.config.quic.server.token_key.filename, which is documented on 10.2.x even though it is absent from master, where it was first checked. * Add late 10.2.x additions to changelog and release notes Picks up #13328 (shared-memory cache directory for fast restart) and #13418 (traffic_ctl cache clear). The shm directory gets its own section since it is a new opt-in feature with four new records and a traffic_ctl subcommand. * Add July 2026 security fixes to changelog and release notes The Release 2 security bundle (#13452) landed directly on 10.2.x without public PRs, so those commits never appear in a milestone. Source them from the commit range with the changelog tool's git-range mode and append them as bare subjects, matching how CHANGELOG-10.1.4 lists them. Link the advisory from whats-new for the CVE mapping. * Add #13352, #13517 and #13523 to the changelog Three late bug fixes on 10.2.x. All are fixes with no new configuration, metrics or API surface, so only the changelog and the commit/PR counts change.
Motivation
Cold-start cache initialization rebuilds each stripe's in-memory directory from disk on every
restart — multi-minute on large caches.
Approach
Host the directory (
raw_dir) in POSIX shared memory so the next process start attaches theexisting segment in milliseconds instead of rebuilding it. Recovery is binary and fail-safe:
anything untrustworthy (crash, reboot, ABI/schema mismatch, failed validation, bad disk) falls
back to the existing disk-rebuild path, and reads still validate
Docmagic + key so a staleentry is a miss, never served corruption.
New configs & traffic_ctl commands
Opt-in behind
proxy.config.cache.shm.enabled(default0, a functional no-op). Also addsproxy.config.cache.shm.{name_prefix,use_hugepages,purge_stale_on_start}and atraffic_ctl cache shm status|clearcommand.Details
Design, recovery model, configuration, and platform notes are in
doc/developer-guide/cache-architecture/shm-fast-restart.en.rst(added in this PR) and theproxy.config.cache.shm.*entries inrecords.yaml.Testing
test_CacheShm(ABI hash, storage signature, control round-trip, name length, prefixnormalization, process liveness).
cache_shm_*suites — fast restart, unclean shutdown, schema/storage mismatch,bad-disk drop + orphan reclaim, concurrent-attach refusal, purge-on-disable.
Caveats
_shm_directory_is_valid()rejection branches or thebad-disk / flush-failure
invalidate_stripe_directory()paths (planned follow-up).flockguard is authoritative on Linux but ano-op on macOS POSIX shm, where it falls back to an owner-pid liveness check.