blob: 69569822b79cfa4f0f978c32b0f8dadd18a11087 [file]
/** @file
Shared-memory-backed cache directory for fast restart.
@section license License
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "CacheShm.h"
#include "shared/cache_shm/Layout.h"
#include "shared/cache_shm/Purge.h"
#include "P_CacheDir.h"
#include "iocore/cache/Store.h"
#include "records/RecCore.h"
#include "tscore/Diags.h"
#include "tscore/HashFNV.h"
#include "tscore/hugepages.h"
#include "tscore/ink_align.h"
#include "tscore/ink_config.h"
#include "tscore/ink_memory.h"
#include "tscore/ink_string.h"
#include "tsutil/DbgCtl.h"
#include <fcntl.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <csignal>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
// Always compiled: none of these touches an shm syscall, so a build without POSIX shm still gets the real fingerprints
// and their test coverage rather than a stub reporting a zero ABI hash.
CacheShm::Mode CacheShm::_mode = CacheShm::Mode::Disabled;
namespace
{
void
fnv_update(ATSHash64FNV1a &h, uint64_t v)
{
h.update(&v, sizeof v);
}
} // namespace
uint64_t
CacheShm::abi_hash()
{
ATSHash64FNV1a h;
h.update(tag.data(), tag.size());
fnv_update(h, sizeof(Dir));
fnv_update(h, sizeof(StripeHeaderFooter));
fnv_update(h, sizeof(cache_shm::CacheShmControl));
fnv_update(h, sizeof(cache_shm::StripeEntry));
fnv_update(h, DIR_DEPTH);
fnv_update(h, SIZEOF_DIR);
fnv_update(h, cache_shm::MAX_STRIPES);
return h.get();
}
uint64_t
CacheShm::storage_signature(const Store &store)
{
ATSHash64FNV1a h;
for (unsigned i = 0; i < store.n_spans; ++i) {
const Span *span = store.spans[i];
if (span == nullptr) {
continue;
}
if (span->pathname) {
std::string_view path{span->pathname.get()};
h.update(path.data(), path.size());
}
fnv_update(h, static_cast<uint64_t>(span->blocks));
fnv_update(h, static_cast<uint64_t>(span->offset));
fnv_update(h, static_cast<uint64_t>(span->hw_sector_size));
}
return h.get();
}
bool
CacheShm::process_is_alive(int pid)
{
return cache_shm::process_is_alive(pid);
}
#if !TS_USE_CACHE_SHM
// No POSIX shm in libc; glibc < 2.34 puts shm_open/shm_unlink in librt, which ATS does not link. Mode stays Disabled, so
// every stripe takes the heap path and the cache behaves as it did before this feature existed.
void
CacheShm::initialize(const Store &)
{
// Only when the operator asked for it; the default is off, so an unconditional note would be noise on every start.
if (RecGetRecordInt("proxy.config.cache.shm.enabled").value_or(0) != 0) {
Warning("cache shm: proxy.config.cache.shm.enabled is set, but this build has no POSIX shared memory support "
"(shm_open is not in libc); using heap directories");
}
}
char *
CacheShm::attach_or_create_stripe(const char *, std::size_t)
{
return nullptr;
}
void
CacheShm::finalize_attach()
{
}
bool
CacheShm::is_shm_pointer(char *)
{
return false;
}
void
CacheShm::mark_clean_shutdown()
{
}
void
CacheShm::invalidate_stripe_directory(char *)
{
}
void
CacheShm::detach_stripe(char *)
{
}
void
CacheShm::release_for_test()
{
}
#else
namespace
{
DbgCtl dbg_ctl{"cache_shm"};
using cache_shm::CACHE_SHM_MAGIC;
using cache_shm::CACHE_SHM_SCHEMA_VERSION;
using cache_shm::CacheShmControl;
using cache_shm::CONTROL_HEADER_SIZE;
using cache_shm::control_segment_name;
using cache_shm::CONTROL_SIZE;
using cache_shm::LockResult;
using cache_shm::MAX_SHM_NAME_LEN;
using cache_shm::MAX_STRIPES;
using cache_shm::read_shm_name;
using cache_shm::StripeEntry;
using cache_shm::try_lock_control;
// Sanity bound: the control struct (header + stripe table) must stay small.
constexpr std::size_t MAX_CONTROL_SEGMENT_BYTES = 32 * 1024;
static_assert(sizeof(CacheShmControl) <= MAX_CONTROL_SEGMENT_BYTES, "control segment unexpectedly large");
// Configuration loaded at initialize() time.
struct Config {
bool enabled = false;
bool use_hugepages = false;
bool purge_stale_on_start = false;
std::string name_prefix; // normalized "/<word>-" (see normalize_name_prefix); set in load_config.
};
Config g_config;
// Live state for the open control segment.
CacheShmControl *g_control = nullptr;
std::string g_control_name;
// Held for the process lifetime so the OS releases it on exit. Only set on the path that owns the segment.
ats_scoped_fd g_control_fd;
// Pointers we returned, so ~Stripe can choose munmap over ats_free and unmap the right span, and so an invalidation can
// reach the stripe's control-table entry without every caller having to carry the index.
struct MappedStripe {
std::size_t size;
uint32_t index;
};
std::mutex g_pointers_mutex;
std::unordered_map<char *, MappedStripe> g_pointers;
// Guards the stripe table and the claim bookkeeping below; stripes initialize concurrently across disk threads.
std::mutex g_table_mutex;
// Per-run partial-attach bookkeeping, indexed in lockstep with g_control->stripes[].
// An entry still unclaimed once init completes is an orphan for finalize_attach(). Process-local, reset each run.
bool g_entry_claimed[MAX_STRIPES] = {};
uint32_t g_claims_this_run = 0;
/// Full 64-bit stripe identity used to match a stripe to its prior shm segment.
uint64_t
compute_stripe_key_hash(const char *stripe_key)
{
ATSHash64FNV1a hash;
hash.update(stripe_key, std::strlen(stripe_key));
return hash.get();
}
/// Shared with the purge path, which sweeps the index space by name when the stripe table cannot be trusted.
using cache_shm::stripe_segment_name;
// Named so the two cannot be transposed at a call site.
enum class ShmAccess { Open, Create };
enum class HugePages { Off, On };
/// nullptr on failure. `out_fd`, when set, keeps the fd open so the caller can flock it; otherwise it is closed, which
/// the mapping survives.
void *
open_and_map_shm(const std::string &name, std::size_t size, ShmAccess access, [[maybe_unused]] HugePages hugepages,
int *out_fd = nullptr, int *out_errno = nullptr)
{
if (out_errno != nullptr) {
*out_errno = 0;
}
int oflags = O_RDWR;
if (access == ShmAccess::Create) {
// O_EXCL so a create never adopts a pre-existing (attacker-planted) object.
oflags |= O_CREAT | O_EXCL;
}
ats_scoped_fd fd{shm_open(name.c_str(), oflags, 0600)};
if (fd < 0) {
int e = errno;
Dbg(dbg_ctl, "shm_open(%s, %s) failed: %s", name.c_str(), access == ShmAccess::Create ? "create" : "open", strerror(e));
if (out_errno != nullptr) {
*out_errno = e;
}
return nullptr;
}
if (access == ShmAccess::Create) {
if (ftruncate(fd, size) < 0) {
int e = errno;
Warning("ftruncate(%s, %zu) failed: %s", name.c_str(), size, strerror(e));
shm_unlink(name.c_str());
if (out_errno != nullptr) {
*out_errno = e;
}
return nullptr;
}
} else {
struct stat sb {
};
if (fstat(fd, &sb) < 0 || sb.st_size < 0 || !cache_shm::is_expected_shm_size(static_cast<std::size_t>(sb.st_size), size)) {
Dbg(dbg_ctl, "shm %s size mismatch (have %lld, want %zu)", name.c_str(), static_cast<long long>(sb.st_size), size);
return nullptr;
}
}
int prot = PROT_READ | PROT_WRITE;
int flags = MAP_SHARED;
void *addr = mmap(nullptr, size, prot, flags, fd, 0);
if (addr == MAP_FAILED) {
int e = errno;
Warning("mmap(%s, %zu) failed: %s", name.c_str(), size, strerror(e));
// Or the leak wedges the next O_EXCL create on EEXIST.
if (access == ShmAccess::Create) {
shm_unlink(name.c_str());
}
if (out_errno != nullptr) {
*out_errno = e;
}
return nullptr;
}
// MAP_HUGETLB is not usable on a tmpfs-backed fd, so advise THP instead; needs shmem THP enabled on the host.
#if defined(MADV_HUGEPAGE)
if (hugepages == HugePages::On) {
if (madvise(addr, size, MADV_HUGEPAGE) != 0) {
Dbg(dbg_ctl, "madvise(MADV_HUGEPAGE) on %s failed: %s", name.c_str(), strerror(errno));
}
}
#endif
if (out_fd != nullptr) {
*out_fd = fd.release(); // caller owns the fd and keeps it open for flock
}
return addr;
}
/// How the pre-existing control segment could be mapped.
enum class ControlMap {
Absent, ///< No segment with this name (ENOENT); nothing to attach.
Full, ///< This build's size, fully mapped: eligible for the trust gates.
Foreign, ///< Another build's size: only the frozen header is mapped (if it even
///< fits). Never trusted -- guard the owner, then drop and recreate.
Failed, ///< Exists but is unusable (permissions, fstat or mmap failure).
};
struct ControlOpen {
ControlMap map = ControlMap::Absent;
CacheShmControl *ctrl = nullptr; ///< nullptr when Absent/Failed, or Foreign and too small for the header.
std::size_t mapped = 0; ///< length to munmap.
std::size_t size = 0; ///< the segment's actual size, for the Foreign diagnostic.
int fd = -1; ///< open on Full/Foreign so the caller can flock it.
int sys_err = 0; ///< errno behind Absent/Failed.
};
/// Not routed through open_and_map_shm: failing on a foreign size would leave the O_EXCL create below wedged on EEXIST
/// every restart until an operator ran `traffic_ctl cache shm clear`. Mapping just the frozen header instead keeps the
/// owner guard usable so the segment can be dropped.
ControlOpen
open_control_segment(const std::string &name)
{
ControlOpen out;
ats_scoped_fd fd{shm_open(name.c_str(), O_RDWR, 0600)};
if (fd < 0) {
out.sys_err = errno;
out.map = out.sys_err == ENOENT ? ControlMap::Absent : ControlMap::Failed;
Dbg(dbg_ctl, "shm_open(%s, open) failed: %s", name.c_str(), strerror(out.sys_err));
return out;
}
// clang-format off
struct stat sb{};
// clang-format on
if (fstat(fd, &sb) < 0) {
out.sys_err = errno;
out.map = ControlMap::Failed;
Warning("cache shm: fstat(%s) failed: %s", name.c_str(), strerror(out.sys_err));
return out;
}
const std::size_t actual = sb.st_size < 0 ? 0 : static_cast<std::size_t>(sb.st_size);
const bool own_size = cache_shm::is_own_control_size(actual);
out.map = own_size ? ControlMap::Full : ControlMap::Foreign;
out.mapped = own_size ? CONTROL_SIZE : std::min(actual, CONTROL_HEADER_SIZE);
out.size = actual;
if (out.mapped >= CONTROL_HEADER_SIZE) {
void *addr = mmap(nullptr, out.mapped, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
out.sys_err = errno;
out.map = ControlMap::Failed;
Warning("mmap(%s, %zu) failed: %s", name.c_str(), out.mapped, strerror(out.sys_err));
out.mapped = 0;
return out;
}
out.ctrl = static_cast<CacheShmControl *>(addr);
} else {
// No ATS build wrote this, so there is nothing to read and the flock is the only owner guard left.
out.mapped = 0;
}
out.fd = fd.release(); // caller owns it: flock, then close or keep for the run
return out;
}
// `table` is nullptr when the stripe table may not be read (foreign layout or bad magic), which sweeps the name space
// instead so a stripe segment -- and the whole directory in it -- is never left behind.
void
unlink_all_known_segments(void *mapping, std::size_t mapping_len, const CacheShmControl *table)
{
// Shares purge_segments()'s primitives so the prefix filter guarding them cannot drift. Cannot call purge_segments()
// itself: this path already holds the control fd, mapping, and the exclusive lock that makes unlinking safe.
std::vector<cache_shm::PurgeUnlink> unlinked;
if (table != nullptr) {
cache_shm::unlink_table_stripes(g_config.name_prefix, table, unlinked);
} else {
cache_shm::unlink_stripe_name_space(g_config.name_prefix, unlinked);
}
for (const auto &u : unlinked) {
Dbg(dbg_ctl, "shm_unlink stripe %s%s", u.name.c_str(), table != nullptr ? "" : " (table untrusted; swept by name)");
}
if (mapping != nullptr) {
munmap(mapping, mapping_len);
}
g_control = nullptr;
if (!g_control_name.empty()) {
Dbg(dbg_ctl, "shm_unlink control %s", g_control_name.c_str());
shm_unlink(g_control_name.c_str());
}
}
// Opt-in and best-effort: logs but never blocks startup. Shares the enumerate-and-unlink work with `traffic_ctl cache
// shm clear`; this only renders the result into diags.
void
purge_stale_segments(const std::string &prefix)
{
const cache_shm::PurgeReport report = cache_shm::purge_segments(prefix);
switch (report.outcome) {
case cache_shm::PurgeOutcome::BadPrefix:
// load_config() already warned about a bad prefix; stay quiet here.
case cache_shm::PurgeOutcome::NotPresent:
return; // ENOENT: shm never used with this prefix.
case cache_shm::PurgeOutcome::OpenFailed:
Warning("cache shm: cannot open control segment %s to purge stale segments: %s", report.control_name.c_str(),
strerror(report.sys_errno));
return;
case cache_shm::PurgeOutcome::MapFailed:
Warning("cache shm: mmap of control segment %s failed while purging: %s", report.control_name.c_str(),
strerror(report.sys_errno));
return;
case cache_shm::PurgeOutcome::StatFailed:
Warning("cache shm: cannot stat control segment %s to purge stale segments: %s", report.control_name.c_str(),
strerror(report.sys_errno));
return;
case cache_shm::PurgeOutcome::TooSmall:
Warning("cache shm: leftover control segment %s is too small to read (%lld bytes); unlinking it", report.control_name.c_str(),
report.segment_size);
break; // purge_segments() already unlinked the control object; render the result below.
case cache_shm::PurgeOutcome::OwnedByLive:
Warning("cache shm: control segment %s is owned by a live process; leaving stale segments in place",
report.control_name.c_str());
return;
case cache_shm::PurgeOutcome::Purged:
break;
}
if (report.table_untrusted) {
Warning("cache shm: leftover control segment %s has an unreadable stripe table (%lld bytes); swept the '%ss<N>' name space",
report.control_name.c_str(), report.segment_size, prefix.c_str());
}
for (const auto &u : report.unlinked) {
if (u.error == 0) {
Dbg(dbg_ctl, "purge: unlinked %s %s", u.is_control ? "control" : "stripe", u.name.c_str());
} else if (u.error != ENOENT) {
Warning("cache shm: failed to unlink %s %s while purging: %s", u.is_control ? "control segment" : "stripe", u.name.c_str(),
strerror(u.error));
}
}
Note("cache shm: purged stale segments while disabled (removed %u, %u failure(s), prefix '%s')", report.removed(),
report.failures(), prefix.c_str());
}
bool
load_config()
{
RecInt enabled = RecGetRecordInt("proxy.config.cache.shm.enabled").value_or(0);
g_config.enabled = enabled != 0;
RecInt use_hugepages = RecGetRecordInt("proxy.config.cache.shm.use_hugepages").value_or(0);
g_config.use_hugepages = use_hugepages != 0;
// Inherit the global hugepage intent unless the operator set the shm knob explicitly.
RecSourceT hp_source = REC_SOURCE_NULL;
if (!g_config.use_hugepages && ats_hugepage_enabled() &&
RecGetRecordSource("proxy.config.cache.shm.use_hugepages", &hp_source) == REC_ERR_OKAY && hp_source == REC_SOURCE_DEFAULT) {
g_config.use_hugepages = true;
}
RecInt purge_stale_on_start = RecGetRecordInt("proxy.config.cache.shm.purge_stale_on_start").value_or(0);
g_config.purge_stale_on_start = purge_stale_on_start != 0;
char prefix_buf[256] = {0};
std::string configured = "ats"; // operator sets only the middle word; framing is added below.
if (RecGetRecordString("proxy.config.cache.shm.name_prefix", prefix_buf, sizeof(prefix_buf)).has_value() &&
prefix_buf[0] != '\0') {
configured = prefix_buf;
}
g_config.name_prefix = cache_shm::normalize_name_prefix(configured);
return g_config.enabled;
}
// Marks the slot non-empty so a concurrent create cannot pick the same index. MAX_STRIPES when full. Caller must hold
// g_table_mutex.
uint32_t
reserve_stripe_slot(uint64_t key_hash, std::size_t directory_size, std::string &out_name)
{
uint32_t idx = g_control->stripe_count;
bool reuse_slot = false;
for (uint32_t i = 0; i < g_control->stripe_count && i < MAX_STRIPES; ++i) {
if (g_control->stripes[i].shm_name[0] == '\0') {
idx = i;
reuse_slot = true;
break;
}
}
if (!reuse_slot && g_control->stripe_count >= MAX_STRIPES) {
Warning("cache shm: stripe count exceeds MAX_STRIPES (%zu); falling back", MAX_STRIPES);
return MAX_STRIPES;
}
out_name = stripe_segment_name(g_config.name_prefix, idx);
if (!reuse_slot) {
g_control->stripe_count++;
}
StripeEntry &e = g_control->stripes[idx];
ink_strlcpy(e.shm_name, out_name.c_str(), sizeof(e.shm_name));
e.raw_dir_size = directory_size;
e.stripe_key_hash = key_hash;
e.dir_untrusted = 0; // fresh segment, so any mark from a prior occupant of this slot is stale
return idx;
}
// Tombstones the slot for reuse. Caller must hold g_table_mutex.
void
release_reserved_slot(uint32_t idx)
{
StripeEntry &e = g_control->stripes[idx];
e.shm_name[0] = '\0';
e.raw_dir_size = 0;
e.stripe_key_hash = 0;
e.dir_untrusted = 0;
}
// Takes the locks itself, so the shm syscalls that produced `p` could run with g_table_mutex dropped.
char *
claim_mapped_stripe(uint32_t idx, void *p, std::size_t size)
{
{
std::scoped_lock lk{g_table_mutex};
g_entry_claimed[idx] = true;
++g_claims_this_run;
}
{
std::scoped_lock plk{g_pointers_mutex};
g_pointers.insert({
static_cast<char *>(p), MappedStripe{size, idx}
});
}
return static_cast<char *>(p);
}
} // namespace
void
CacheShm::initialize(const Store &store)
{
if (!load_config()) {
_mode = Mode::Disabled;
// Leftovers would keep consuming memory, and a later re-enable would attach a directory that went stale meanwhile.
if (g_config.purge_stale_on_start) {
purge_stale_segments(g_config.name_prefix);
}
Dbg(dbg_ctl, "shm disabled");
return;
}
// Surface the MAP_HUGETLB -> THP substitution once so it isn't a silent downgrade.
if (ats_hugepage_enabled()) {
if (g_config.use_hugepages) {
Note("cache shm: global hugepages enabled; MAP_HUGETLB is not usable for the tmpfs-backed dir, "
"advising MADV_HUGEPAGE (transparent huge pages) on the mapping instead");
} else {
Warning("cache shm: global hugepages enabled but proxy.config.cache.shm.use_hugepages is 0; "
"the tmpfs-backed dir will use base pages (MAP_HUGETLB does not apply to shm)");
}
}
g_control_name = control_segment_name(g_config.name_prefix);
if (g_control_name.size() >= MAX_SHM_NAME_LEN) {
Warning("shm name_prefix too long (control segment name '%s' exceeds %zu chars); shm disabled", g_control_name.c_str(),
MAX_SHM_NAME_LEN);
_mode = Mode::Disabled;
return;
}
const uint64_t expected_abi = abi_hash();
const uint64_t expected_signature = storage_signature(store);
// Try to attach an existing control segment first.
ControlOpen opened = open_control_segment(g_control_name);
if (opened.map == ControlMap::Failed) {
Warning("cache shm: cannot use existing control segment %s: %s; shm disabled", g_control_name.c_str(),
strerror(opened.sys_err));
_mode = Mode::Disabled;
return;
}
if (opened.map != ControlMap::Absent) {
CacheShmControl *ctrl = opened.ctrl;
// Refuse shm, and rebuild from disk, if another live process still owns this segment.
int flock_errno = 0;
const LockResult lock = try_lock_control(opened.fd, &flock_errno);
bool live_owner = false;
switch (lock) {
case LockResult::Acquired:
break; // we hold the exclusive lock, so any prior owner is gone
case LockResult::HeldByOther:
live_owner = true;
break;
case LockResult::Unsupported: // macOS POSIX shm: flock is a no-op, fall back to owner_pid
Dbg(dbg_ctl, "flock unsupported for control segment %s (errno %d: %s); using owner-pid liveness guard",
g_control_name.c_str(), flock_errno, strerror(flock_errno));
live_owner = ctrl != nullptr && ctrl->owner_pid != 0 && ctrl->owner_pid != static_cast<int32_t>(getpid()) &&
process_is_alive(ctrl->owner_pid);
break;
}
if (live_owner) {
Warning("cache shm: control segment %s has a live owner (pid %d); disabling shm this run to avoid concurrent attach",
g_control_name.c_str(), ctrl != nullptr ? ctrl->owner_pid : 0);
if (ctrl != nullptr) {
munmap(ctrl, opened.mapped);
}
close(opened.fd);
_mode = Mode::Disabled;
return;
}
// Only the frozen header was mapped; the stripe table behind it may have a different layout entirely.
if (opened.map == ControlMap::Foreign) {
Note("cache shm: control segment %s is %zu bytes, not this build's %zu; dropping it", g_control_name.c_str(), opened.size,
CONTROL_SIZE);
}
const bool magic_ok = opened.map == ControlMap::Full && std::memcmp(ctrl->magic, CACHE_SHM_MAGIC, sizeof(CACHE_SHM_MAGIC)) == 0;
bool ok = magic_ok;
if (ok && ctrl->schema_version != CACHE_SHM_SCHEMA_VERSION) {
Note("cache shm: schema mismatch (%u vs %u), dropping", ctrl->schema_version, CACHE_SHM_SCHEMA_VERSION);
ok = false;
}
if (ok && ctrl->abi_hash != expected_abi) {
Note("cache shm: ABI mismatch, dropping");
ok = false;
}
// Not a hard gate: a storage change keeps the segment and each stripe attaches by its own identity.
const bool storage_changed = ok && ctrl->storage_signature != expected_signature;
if (ok && ctrl->clean_shutdown == 0) {
// A crash may have left dir entries pointing at content never flushed, so no
// stripe can safely skip recovery -- whole-segment drop.
Note("cache shm: previous run did not shutdown cleanly, dropping");
ok = false;
}
if (ok) {
Note("cache shm: attaching up to %u stripes (fast restart%s)", ctrl->stripe_count,
storage_changed ? ", partial -- storage changed" : "");
g_control = ctrl;
g_control_fd = opened.fd; // hold the exclusive lock for the process lifetime
std::memset(g_entry_claimed, 0, sizeof(g_entry_claimed));
g_claims_this_run = 0;
if (storage_changed) {
g_control->storage_signature = expected_signature;
}
// Become owner and clear clean_shutdown so a crash this run drops shm next time.
g_control->owner_pid = static_cast<int32_t>(getpid());
g_control->clean_shutdown = 0;
msync(g_control, CONTROL_SIZE, MS_SYNC);
_mode = Mode::AttachExisting;
return;
}
// Drop everything and fall through to fresh-create. We hold the exclusive lock,
// so unlinking cannot pull segments out from under a live owner.
unlink_all_known_segments(ctrl, opened.mapped, magic_ok ? ctrl : nullptr);
close(opened.fd); // releases the lock on the now-unlinked object
}
// Create fresh control segment.
int fresh_fd = -1;
int create_errno = 0;
void *fresh = open_and_map_shm(g_control_name, CONTROL_SIZE, ShmAccess::Create, HugePages::Off, &fresh_fd, &create_errno);
if (fresh == nullptr) {
// Surface the errno + offending name: e.g. an embedded '/' in name_prefix yields EINVAL here.
Warning("cache shm: failed to create control segment %s: %s; shm disabled", g_control_name.c_str(), strerror(create_errno));
_mode = Mode::Disabled;
return;
}
// Lock the freshly created segment. Another starting process could have created
// and locked it first in the window since the drop above; if so, refuse.
if (try_lock_control(fresh_fd) == LockResult::HeldByOther) {
Warning("cache shm: lost the create race for control segment %s; disabling shm this run", g_control_name.c_str());
munmap(fresh, CONTROL_SIZE);
close(fresh_fd);
_mode = Mode::Disabled;
return;
}
g_control = static_cast<CacheShmControl *>(fresh);
g_control_fd = fresh_fd; // hold the exclusive lock for the process lifetime
std::memset(g_control, 0, CONTROL_SIZE);
std::memset(g_entry_claimed, 0, sizeof(g_entry_claimed));
g_claims_this_run = 0;
std::memcpy(g_control->magic, CACHE_SHM_MAGIC, sizeof(CACHE_SHM_MAGIC));
g_control->schema_version = CACHE_SHM_SCHEMA_VERSION;
g_control->abi_hash = expected_abi;
g_control->storage_signature = expected_signature;
g_control->clean_shutdown = 0;
g_control->owner_pid = static_cast<int32_t>(getpid());
g_control->stripe_count = 0;
_mode = Mode::CreateFresh;
Note("cache shm: creating fresh control segment %s (owner pid %d)", g_control_name.c_str(), static_cast<int>(getpid()));
return;
}
char *
CacheShm::attach_or_create_stripe(const char *stripe_key, std::size_t directory_size)
{
if (_mode == Mode::Disabled || g_control == nullptr) {
return nullptr;
}
const uint64_t key_hash = compute_stripe_key_hash(stripe_key);
const HugePages hugepages = g_config.use_hugepages ? HugePages::On : HugePages::Off;
// Decide under the table lock, then run the shm syscalls with it dropped (holding
// it would serialize every disk thread's init; each stripe owns a distinct segment).
std::string attach_name; // non-empty => map this existing segment
std::string create_name; // set when a fresh slot was reserved (the create path)
uint32_t idx = MAX_STRIPES;
{
std::scoped_lock lk{g_table_mutex};
// 1. Try to attach this stripe's prior segment, matched by 64-bit identity (not
// name), so a span going offline shifts indices but not identities.
for (uint32_t i = 0; i < g_control->stripe_count && i < MAX_STRIPES; ++i) {
StripeEntry &e = g_control->stripes[i];
if (e.shm_name[0] == '\0' || e.stripe_key_hash != key_hash) {
continue; // tombstoned slot, or a different stripe
}
if (g_entry_claimed[i]) {
// Another stripe this run already took this entry, so two distinct stripes
// hashed to one identity (duplicate hash_text, or a 64-bit FNV-1a collision).
// Sharing one directory between them would corrupt both; create fresh instead.
Warning("cache shm: stripe key collision on %s; creating a fresh segment for key=%s", read_shm_name(e.shm_name).c_str(),
stripe_key);
break;
}
if (e.dir_untrusted) {
// Last shutdown could not vouch for this directory (bad disk, or a write still in flight). Tombstone it here rather
// than leaving an orphan for finalize_attach: the slot is reused immediately, so stripe_count cannot creep across
// runs that never reach finalize, and the old segment never coexists with its replacement.
// The unlink stays under the lock even though the other shm syscalls do not: once the slot is a tombstone another
// disk thread can reserve it and derive this same name, and unlinking after that would strip the name off the
// segment it just created.
const std::string name = read_shm_name(e.shm_name);
Note("cache shm: stripe %s was marked untrusted at shutdown; recreating", name.c_str());
shm_unlink(name.c_str());
release_reserved_slot(i);
break;
}
if (e.raw_dir_size != directory_size) {
// Same identity, different size: shouldn't happen (size derives from the
// keyed blocks). Treat as a miss and recreate; the stale entry is reaped by
// finalize_attach().
Note("cache shm: stripe %s size mismatch (have %llu, want %zu); recreating", read_shm_name(e.shm_name).c_str(),
static_cast<unsigned long long>(e.raw_dir_size), directory_size);
break;
}
attach_name = read_shm_name(e.shm_name);
idx = i;
break;
}
// 2. No usable prior segment -- reserve a slot for a fresh create under the lock.
if (attach_name.empty() && (idx = reserve_stripe_slot(key_hash, directory_size, create_name)) == MAX_STRIPES) {
return nullptr; // table full (already logged)
}
}
// Attach path: map the existing segment outside the lock.
if (!attach_name.empty()) {
void *p = open_and_map_shm(attach_name, directory_size, ShmAccess::Open, hugepages);
if (p != nullptr) {
Note("cache shm: attached stripe %s (%zu bytes) for key=%s", attach_name.c_str(), directory_size, stripe_key);
return claim_mapped_stripe(idx, p, directory_size);
}
// Attach failed (segment vanished/unmappable): reserve a fresh slot and fall
// through to create. The stale entry is reaped by finalize_attach().
Note("cache shm: failed to attach stripe %s; recreating", attach_name.c_str());
std::scoped_lock lk{g_table_mutex};
if ((idx = reserve_stripe_slot(key_hash, directory_size, create_name)) == MAX_STRIPES) {
return nullptr;
}
}
// Create path: slot already reserved; syscalls run outside the lock. A fresh
// ftruncate'd segment is zero-filled (magic 0), so Stripe::init falls back to the
// disk read and repopulates it. shm_unlink clears any leftover with this name.
shm_unlink(create_name.c_str());
void *p = open_and_map_shm(create_name, directory_size, ShmAccess::Create, hugepages);
if (p == nullptr) {
std::scoped_lock lk{g_table_mutex};
release_reserved_slot(idx);
return nullptr;
}
Note("cache shm: created stripe %s (%zu bytes) for key=%s", create_name.c_str(), directory_size, stripe_key);
return claim_mapped_stripe(idx, p, directory_size);
}
void
CacheShm::finalize_attach()
{
if (g_control == nullptr) {
return;
}
std::scoped_lock lk{g_table_mutex};
// With zero claims this run we cannot distinguish "genuinely empty cache" from
// "init aborted" (e.g. a transient volume.config error), so leave every segment
// intact rather than risk reclaiming a valid cache.
if (g_claims_this_run == 0) {
Dbg(dbg_ctl, "finalize_attach: no stripes claimed this run; leaving %u segment(s) intact", g_control->stripe_count);
return;
}
uint32_t reclaimed = 0;
for (uint32_t i = 0; i < g_control->stripe_count && i < MAX_STRIPES; ++i) {
StripeEntry &e = g_control->stripes[i];
if (e.shm_name[0] == '\0' || g_entry_claimed[i]) {
continue; // already empty, or claimed by a live stripe this run
}
// Unclaimed, non-empty entry: its stripe left the cache (span dropped, or disk
// failed to open). Unlink the orphan and tombstone the slot for reuse.
std::string name = read_shm_name(e.shm_name);
Note("cache shm: reclaiming orphaned stripe segment %s", name.c_str());
shm_unlink(name.c_str());
release_reserved_slot(i);
++reclaimed;
}
if (reclaimed > 0) {
Note("cache shm: reclaimed %u orphaned stripe segment(s) after attach", reclaimed);
}
// Trim trailing tombstones so stripe_count tracks the live high-water mark;
// interior tombstones stay (reused by attach_or_create_stripe).
uint32_t live_count = 0;
for (uint32_t i = 0; i < g_control->stripe_count && i < MAX_STRIPES; ++i) {
if (g_control->stripes[i].shm_name[0] != '\0') {
live_count = i + 1;
}
}
const bool count_changed = live_count != g_control->stripe_count;
if (count_changed) {
Note("cache shm: trimming stripe_count %u -> %u after reclaim", g_control->stripe_count, live_count);
g_control->stripe_count = live_count;
}
if (reclaimed > 0 || count_changed) {
msync(g_control, CONTROL_SIZE, MS_SYNC);
}
}
bool
CacheShm::is_shm_pointer(char *raw_dir)
{
if (raw_dir == nullptr) {
return false;
}
std::scoped_lock lk{g_pointers_mutex};
return g_pointers.find(raw_dir) != g_pointers.end();
}
void
CacheShm::mark_clean_shutdown()
{
if (g_control == nullptr) {
return;
}
Note("cache shm: marking clean shutdown");
g_control->clean_shutdown = 1;
// owner_pid stays until the process exits. The event system is shut down by the time we get here, but that flag joins
// no thread, and where flock is a no-op -- macOS, FreeBSD -- the PID is the only thing that stops a starting process
// from attaching to directories a straggler may still be writing. A stale PID only costs the next start its fast
// restart; a concurrent attach costs correctness.
msync(g_control, CONTROL_SIZE, MS_SYNC);
}
void
CacheShm::invalidate_stripe_directory(char *raw_dir)
{
uint32_t idx = MAX_STRIPES;
{
std::scoped_lock lk{g_pointers_mutex};
auto it = g_pointers.find(raw_dir);
if (it == g_pointers.end()) {
return; // not a shm-backed dir
}
idx = it->second.index;
}
// Marked in the control segment, never in the stripe's own header: that header aliases raw_dir, which both the shutdown
// pwrite and the periodic dir sync copy to disk, so a mark there can clear the stripe on the next start instead of
// rebuilding it. Next start, attach_or_create_stripe refuses the entry and creates a fresh segment, whose zero magic
// sends Stripe::init down the disk-read path.
std::scoped_lock lk{g_table_mutex};
if (g_control == nullptr || idx >= MAX_STRIPES) {
return;
}
g_control->stripes[idx].dir_untrusted = 1;
msync(g_control, CONTROL_SIZE, MS_SYNC);
}
void
CacheShm::detach_stripe(char *raw_dir)
{
if (raw_dir == nullptr) {
return;
}
std::scoped_lock lk{g_pointers_mutex};
auto it = g_pointers.find(raw_dir);
if (it == g_pointers.end()) {
return;
}
// munmap the recorded span; never shm_unlink -- the segment must survive for the
// next start to attach.
munmap(it->first, it->second.size);
g_pointers.erase(it);
}
void
CacheShm::release_for_test()
{
std::scoped_lock lk{g_table_mutex};
if (g_control != nullptr) {
munmap(g_control, CONTROL_SIZE);
g_control = nullptr;
}
// Closing the fd is what releases the control flock, so the next initialize() in this process is not refused as a
// concurrent attach.
g_control_fd = ats_scoped_fd{};
g_control_name.clear();
std::memset(g_entry_claimed, 0, sizeof(g_entry_claimed));
g_claims_this_run = 0;
_mode = Mode::Disabled;
}
#endif // TS_USE_CACHE_SHM