/* * Copyright (C) 1996-2023 The Squid Software Foundation and contributors * * Squid software is distributed under GPLv2+ license and includes * contributions from numerous individuals and organizations. * Please see the COPYING and CONTRIBUTORS files for details. */ /* DEBUG: section 20 Store Controller */ #include "squid.h" #include "mem_node.h" #include "MemStore.h" #include "SquidConfig.h" #include "SquidMath.h" #include "store/Controller.h" #include "store/Disks.h" #include "store/LocalSearch.h" #include "tools.h" #include "Transients.h" #if HAVE_SYS_WAIT_H #include #endif /* * store_dirs_rebuilding is initialized to _1_ as a hack so that * storeDirWriteCleanLogs() doesn't try to do anything unless _all_ * cache_dirs have been read. For example, without this hack, Squid * will try to write clean log files if -kparse fails (because it * calls fatal()). */ int Store::Controller::store_dirs_rebuilding = 1; Store::Controller::Controller() : disks(new Disks), sharedMemStore(nullptr), localMemStore(false), transients(nullptr) { assert(!store_table); } /// this destructor is never called because Controller singleton is immortal Store::Controller::~Controller() { // assert at runtime because we cannot `= delete` an overridden destructor assert(!"Controller is never destroyed"); } void Store::Controller::init() { if (IamWorkerProcess()) { if (MemStore::Enabled()) { sharedMemStore = new MemStore; sharedMemStore->init(); } else if (Config.memMaxSize > 0) { localMemStore = true; } } disks->init(); if (Transients::Enabled() && IamWorkerProcess()) { transients = new Transients; transients->init(); } } void Store::Controller::create() { disks->create(); #if !_SQUID_WINDOWS_ pid_t pid; do { PidStatus status; pid = WaitForAnyPid(status, WNOHANG); } while (pid > 0 || (pid < 0 && errno == EINTR)); #endif } void Store::Controller::maintain() { static time_t last_warn_time = 0; disks->maintain(); /* this should be emitted by the oversize dir, not globally */ if (Root().currentSize() > Store::Root().maxSize()) { if (squid_curtime - last_warn_time > 10) { debugs(20, DBG_CRITICAL, "WARNING: Disk space over limit: " << Store::Root().currentSize() / 1024.0 << " KB > " << (Store::Root().maxSize() >> 10) << " KB"); last_warn_time = squid_curtime; } } } void Store::Controller::getStats(StoreInfoStats &stats) const { if (sharedMemStore) sharedMemStore->getStats(stats); else { // move this code to a non-shared memory cache class when we have it stats.mem.shared = false; stats.mem.capacity = Config.memMaxSize; stats.mem.size = mem_node::StoreMemSize(); if (localMemStore) { // XXX: also count internal/in-transit objects stats.mem.count = hot_obj_count; } else { // XXX: count internal/in-transit objects instead stats.mem.count = hot_obj_count; } } disks->getStats(stats); // low-level info not specific to memory or disk cache stats.store_entry_count = StoreEntry::inUseCount(); stats.mem_object_count = MemObject::inUseCount(); } void Store::Controller::stat(StoreEntry &output) const { storeAppendPrintf(&output, "Store Directory Statistics:\n"); storeAppendPrintf(&output, "Store Entries : %lu\n", (unsigned long int)StoreEntry::inUseCount()); storeAppendPrintf(&output, "Maximum Swap Size : %" PRIu64 " KB\n", maxSize() >> 10); storeAppendPrintf(&output, "Current Store Swap Size: %.2f KB\n", currentSize() / 1024.0); storeAppendPrintf(&output, "Current Capacity : %.2f%% used, %.2f%% free\n", Math::doublePercent(currentSize(), maxSize()), Math::doublePercent((maxSize() - currentSize()), maxSize())); if (sharedMemStore) sharedMemStore->stat(output); disks->stat(output); } /* if needed, this could be taught to cache the result */ uint64_t Store::Controller::maxSize() const { /* TODO: include memory cache ? */ return disks->maxSize(); } uint64_t Store::Controller::minSize() const { /* TODO: include memory cache ? */ return disks->minSize(); } uint64_t Store::Controller::currentSize() const { /* TODO: include memory cache ? */ return disks->currentSize(); } uint64_t Store::Controller::currentCount() const { /* TODO: include memory cache ? */ return disks->currentCount(); } int64_t Store::Controller::maxObjectSize() const { /* TODO: include memory cache ? */ return disks->maxObjectSize(); } void Store::Controller::configure() { disks->configure(); store_swap_high = (long) (((float) maxSize() * (float) Config.Swap.highWaterMark) / (float) 100); store_swap_low = (long) (((float) maxSize() * (float) Config.Swap.lowWaterMark) / (float) 100); store_pages_max = Config.memMaxSize / sizeof(mem_node); // TODO: move this into a memory cache class when we have one const int64_t memMax = static_cast(min(Config.Store.maxInMemObjSize, Config.memMaxSize)); const int64_t disksMax = disks->maxObjectSize(); store_maxobjsize = std::max(disksMax, memMax); } StoreSearch * Store::Controller::search() { // this is the only kind of search we currently support return NewLocalSearch(); } void Store::Controller::sync(void) { if (sharedMemStore) sharedMemStore->sync(); disks->sync(); } /* * handle callbacks all available fs'es */ int Store::Controller::callback() { /* mem cache callbacks ? */ return disks->callback(); } /// update reference counters of the recently touched entry void Store::Controller::referenceBusy(StoreEntry &e) { // special entries do not belong to any specific Store, but are IN_MEMORY if (EBIT_TEST(e.flags, ENTRY_SPECIAL)) return; /* Notify the fs that we're referencing this object again */ if (e.hasDisk()) disks->reference(e); // Notify the memory cache that we're referencing this object again if (sharedMemStore && e.mem_status == IN_MEMORY) sharedMemStore->reference(e); // TODO: move this code to a non-shared memory cache class when we have it if (e.mem_obj) { if (mem_policy->Referenced) mem_policy->Referenced(mem_policy, &e, &e.mem_obj->repl); } } /// dereference()s an idle entry /// \returns false if and only if the entry should be deleted bool Store::Controller::dereferenceIdle(StoreEntry &e, bool wantsLocalMemory) { // special entries do not belong to any specific Store, but are IN_MEMORY if (EBIT_TEST(e.flags, ENTRY_SPECIAL)) return true; // idle private entries cannot be reused if (EBIT_TEST(e.flags, KEY_PRIVATE)) return false; bool keepInStoreTable = false; // keep only if somebody needs it there // Notify the fs that we are not referencing this object any more. This // should be done even if we overwrite keepInStoreTable afterwards. if (e.hasDisk()) keepInStoreTable = disks->dereference(e) || keepInStoreTable; // Notify the memory cache that we're not referencing this object any more if (sharedMemStore && e.mem_status == IN_MEMORY) keepInStoreTable = sharedMemStore->dereference(e) || keepInStoreTable; // TODO: move this code to a non-shared memory cache class when we have it if (e.mem_obj) { if (mem_policy->Dereferenced) mem_policy->Dereferenced(mem_policy, &e, &e.mem_obj->repl); // non-shared memory cache relies on store_table if (localMemStore) keepInStoreTable = wantsLocalMemory || keepInStoreTable; } if (e.hittingRequiresCollapsing()) { // If we were writing this now-locally-idle entry, then we did not // finish and should now destroy an incomplete entry. Otherwise, do not // leave this idle StoreEntry behind because handleIMSReply() lacks // freshness checks when hitting a collapsed revalidation entry. keepInStoreTable = false; // may overrule fs decisions made above } return keepInStoreTable; } bool Store::Controller::markedForDeletion(const cache_key *key) const { // assuming a public key, checking Transients should cover all cases. return transients && transients->markedForDeletion(key); } bool Store::Controller::markedForDeletionAndAbandoned(const StoreEntry &e) const { // The opposite check order could miss a reader that has arrived after the // !readers() and before the markedForDeletion() check. return markedForDeletion(reinterpret_cast(e.key)) && transients && !transients->readers(e); } bool Store::Controller::hasReadableDiskEntry(const StoreEntry &e) const { return disks->hasReadableEntry(e); } /// flags problematic entries before find() commits to finalizing/returning them void Store::Controller::checkFoundCandidate(const StoreEntry &entry) const { checkTransients(entry); // The "hittingRequiresCollapsing() has an active writer" checks below // protect callers from getting stuck and/or from using a stale revalidation // reply. However, these protections are not reliable because the writer may // disappear at any time and/or without a trace. Collapsing adds risks... if (entry.hittingRequiresCollapsing()) { if (entry.hasTransients()) { // Too late to check here because the writer may be gone by now, but // Transients do check when they setCollapsingRequirement(). } else { // a local writer must hold a lock on its writable entry if (!(entry.locked() && entry.isAccepting())) throw TextException("no local writer", Here()); } } } StoreEntry * Store::Controller::find(const cache_key *key) { if (const auto entry = peek(key)) { try { if (!entry->key) allowSharing(*entry, key); checkFoundCandidate(*entry); entry->touch(); referenceBusy(*entry); return entry; } catch (const std::exception &ex) { debugs(20, 2, "failed with " << *entry << ": " << ex.what()); entry->release(); // fall through } } return nullptr; } /// indexes and adds SMP-tracking for an ephemeral peek() result void Store::Controller::allowSharing(StoreEntry &entry, const cache_key *key) { // anchorToCache() below and many find() callers expect a registered entry addReading(&entry, key); if (entry.hasTransients()) { // store hadWriter before computing `found`; \see Transients::get() const auto hadWriter = transients->hasWriter(entry); const auto found = anchorToCache(entry); if (!found) { // !found should imply hittingRequiresCollapsing() regardless of writer presence if (!entry.hittingRequiresCollapsing()) { debugs(20, DBG_IMPORTANT, "ERROR: Squid BUG: missing ENTRY_REQUIRES_COLLAPSING for " << entry); throw TextException("transients entry missing ENTRY_REQUIRES_COLLAPSING", Here()); } if (!hadWriter) { // prevent others from falling into the same trap throw TextException("unattached transients entry missing writer", Here()); } } } } StoreEntry * Store::Controller::findCallbackXXX(const cache_key *key) { // We could check for mem_obj presence (and more), moving and merging some // of the duplicated neighborsUdpAck() and neighborsHtcpReply() code here, // but that would mean polluting Store with HTCP/ICP code. Instead, we // should encapsulate callback-related data in a protocol-neutral MemObject // member or use an HTCP/ICP-specific index rather than store_table. // cannot reuse peekAtLocal() because HTCP/ICP callbacks may use private keys return static_cast(hash_lookup(store_table, key)); } /// \returns either an existing local reusable StoreEntry object or nil /// To treat remotely marked entries specially, /// callers ought to check markedForDeletion() first! StoreEntry * Store::Controller::peekAtLocal(const cache_key *key) { if (StoreEntry *e = static_cast(hash_lookup(store_table, key))) { // callers must only search for public entries assert(!EBIT_TEST(e->flags, KEY_PRIVATE)); assert(e->publicKey()); checkTransients(*e); // TODO: ignore and maybe handleIdleEntry() unlocked intransit entries // because their backing store slot may be gone already. return e; } return nullptr; } StoreEntry * Store::Controller::peek(const cache_key *key) { debugs(20, 3, storeKeyText(key)); if (markedForDeletion(key)) { debugs(20, 3, "ignoring marked in-transit " << storeKeyText(key)); return nullptr; } if (StoreEntry *e = peekAtLocal(key)) { debugs(20, 3, "got local in-transit entry: " << *e); return e; } // Must search transients before caches because we must sync those we find. if (transients) { if (StoreEntry *e = transients->get(key)) { debugs(20, 3, "got shared in-transit entry: " << *e); return e; } } if (sharedMemStore) { if (StoreEntry *e = sharedMemStore->get(key)) { debugs(20, 3, "got mem-cached entry: " << *e); return e; } } if (const auto e = disks->get(key)) { debugs(20, 3, "got disk-cached entry: " << *e); return e; } debugs(20, 4, "cannot locate " << storeKeyText(key)); return nullptr; } bool Store::Controller::transientsReader(const StoreEntry &e) const { return transients && e.hasTransients() && transients->isReader(e); } bool Store::Controller::transientsWriter(const StoreEntry &e) const { return transients && e.hasTransients() && transients->isWriter(e); } int64_t Store::Controller::accumulateMore(StoreEntry &entry) const { return disks->accumulateMore(entry); // The memory cache should not influence for-swapout accumulation decision. } // Must be called from StoreEntry::release() or releaseRequest() because // those methods currently manage local indexing of StoreEntry objects. // TODO: Replace StoreEntry::release*() with Root().evictCached(). void Store::Controller::evictCached(StoreEntry &e) { debugs(20, 7, e); if (transients) transients->evictCached(e); memoryEvictCached(e); disks->evictCached(e); } void Store::Controller::evictIfFound(const cache_key *key) { debugs(20, 7, storeKeyText(key)); if (StoreEntry *entry = peekAtLocal(key)) { debugs(20, 5, "marking local in-transit " << *entry); entry->release(true); return; } if (sharedMemStore) sharedMemStore->evictIfFound(key); disks->evictIfFound(key); if (transients) transients->evictIfFound(key); } /// whether the memory cache is allowed to store that many additional pages bool Store::Controller::memoryCacheHasSpaceFor(const int pagesRequired) const { // XXX: We count mem_nodes but may free shared memory pages instead. const auto fits = mem_node::InUseCount() + pagesRequired <= store_pages_max; debugs(20, 7, fits << ": " << mem_node::InUseCount() << '+' << pagesRequired << '?' << store_pages_max); return fits; } void Store::Controller::freeMemorySpace(const int bytesRequired) { const auto pagesRequired = (bytesRequired + SM_PAGE_SIZE-1) / SM_PAGE_SIZE; if (memoryCacheHasSpaceFor(pagesRequired)) return; // XXX: When store_pages_max is smaller than pagesRequired, we should not // look for more space (but we do because we want to abandon idle entries?). // limit our performance impact to one walk per second static time_t lastWalk = 0; if (lastWalk == squid_curtime) return; lastWalk = squid_curtime; debugs(20, 2, "need " << pagesRequired << " pages"); // let abandon()/handleIdleEntry() know about the impeding memory shortage memoryPagesDebt_ = pagesRequired; // XXX: SMP-unaware: Walkers should iterate memory cache, not store_table. // XXX: Limit iterations by time, not arbitrary count. const auto walker = mem_policy->PurgeInit(mem_policy, 100000); int removed = 0; while (const auto entry = walker->Next(walker)) { // Abandoned memory cache entries are purged during memory shortage. entry->abandon(__func__); // may delete entry ++removed; if (memoryCacheHasSpaceFor(pagesRequired)) break; } // TODO: Move to RemovalPolicyWalker::Done() that has more/better details. debugs(20, 3, "removed " << removed << " out of " << hot_obj_count << " memory-cached entries"); walker->Done(walker); memoryPagesDebt_ = 0; } // move this into [non-shared] memory cache class when we have one /// whether e should be kept in local RAM for possible future caching bool Store::Controller::keepForLocalMemoryCache(StoreEntry &e) const { if (!e.memoryCachable()) return false; // does the current and expected size obey memory caching limits? assert(e.mem_obj); const int64_t loadedSize = e.mem_obj->endOffset(); const int64_t expectedSize = e.mem_obj->expectedReplySize(); // may be < 0 const int64_t ramSize = max(loadedSize, expectedSize); const int64_t ramLimit = min( static_cast(Config.memMaxSize), static_cast(Config.Store.maxInMemObjSize)); return ramSize <= ramLimit; } void Store::Controller::memoryOut(StoreEntry &e, const bool preserveSwappable) { bool keepInLocalMemory = false; if (sharedMemStore) sharedMemStore->write(e); // leave keepInLocalMemory false else if (localMemStore) keepInLocalMemory = keepForLocalMemoryCache(e); debugs(20, 7, "keepInLocalMemory: " << keepInLocalMemory); if (!keepInLocalMemory) e.trimMemory(preserveSwappable); } /// removes the entry from the memory cache /// XXX: Dangerous side effect: Unlocked entries lose their mem_obj. void Store::Controller::memoryEvictCached(StoreEntry &e) { // TODO: Untangle memory caching from mem_obj. if (sharedMemStore) sharedMemStore->evictCached(e); else // TODO: move into [non-shared] memory cache class when we have one if (!e.locked()) e.destroyMemObject(); } void Store::Controller::memoryDisconnect(StoreEntry &e) { if (sharedMemStore) sharedMemStore->disconnect(e); // else nothing to do for non-shared memory cache } void Store::Controller::noteStoppedSharedWriting(StoreEntry &e) { if (transients && e.hasTransients()) // paranoid: the caller should check transients->completeWriting(e); } int Store::Controller::transientReaders(const StoreEntry &e) const { return (transients && e.hasTransients()) ? transients->readers(e) : 0; } void Store::Controller::transientsDisconnect(StoreEntry &e) { if (transients) transients->disconnect(e); } void Store::Controller::handleIdleEntry(StoreEntry &e) { bool keepInLocalMemory = false; if (EBIT_TEST(e.flags, ENTRY_SPECIAL)) { // Icons (and cache digests?) should stay in store_table until we // have a dedicated storage for them (that would not purge them). // They are not managed [well] by any specific Store handled below. keepInLocalMemory = true; } else if (sharedMemStore) { // leave keepInLocalMemory false; sharedMemStore maintains its own cache } else if (localMemStore) { keepInLocalMemory = keepForLocalMemoryCache(e) && // in good shape and // the local memory cache is not overflowing memoryCacheHasSpaceFor(memoryPagesDebt_); } // An idle, unlocked entry that only belongs to a SwapDir which controls // its own index, should not stay in the global store_table. if (!dereferenceIdle(e, keepInLocalMemory)) { debugs(20, 5, "destroying unlocked entry: " << &e << ' ' << e); destroyStoreEntry(static_cast(&e)); return; } debugs(20, 5, "keepInLocalMemory: " << keepInLocalMemory); // formerly known as "WARNING: found KEY_PRIVATE" assert(!EBIT_TEST(e.flags, KEY_PRIVATE)); // TODO: move this into [non-shared] memory cache class when we have one if (keepInLocalMemory) { e.setMemStatus(IN_MEMORY); e.mem_obj->unlinkRequest(); return; } // We know the in-memory data will be gone. Get rid of the entire entry if // it has nothing worth preserving on disk either. if (!e.swappedOut()) { e.release(); // deletes e return; } memoryEvictCached(e); // may already be gone // and keep the entry in store_table for its on-disk data } bool Store::Controller::updateOnNotModified(StoreEntry *old, StoreEntry &e304) { Must(old); Must(old->mem_obj); Must(e304.mem_obj); // updateOnNotModified() may be called many times for the same old entry. // e304.mem_obj->appliedUpdates value distinguishes two cases: // false: Independent store clients revalidating the same old StoreEntry. // Each such update uses its own e304. The old StoreEntry // accumulates such independent updates. // true: Store clients feeding off the same 304 response. Each such update // uses the same e304. For timestamps correctness and performance // sake, it is best to detect and skip such repeated update calls. if (e304.mem_obj->appliedUpdates) { debugs(20, 5, "ignored repeated update of " << *old << " with " << e304); return true; } e304.mem_obj->appliedUpdates = true; try { if (!old->updateOnNotModified(e304)) { debugs(20, 5, "updated nothing in " << *old << " with " << e304); return true; } } catch (...) { debugs(20, DBG_IMPORTANT, "ERROR: Failed to update a cached response: " << CurrentException); return false; } if (sharedMemStore && old->mem_status == IN_MEMORY && !EBIT_TEST(old->flags, ENTRY_SPECIAL)) sharedMemStore->updateHeaders(old); if (old->swap_dirn > -1) disks->updateHeaders(old); return true; } bool Store::Controller::allowCollapsing(StoreEntry *e, const RequestFlags &reqFlags, const HttpRequestMethod &) { const KeyScope keyScope = reqFlags.refresh ? ksRevalidation : ksDefault; // set the flag now so that it gets copied into the Transients entry e->setCollapsingRequirement(true); if (e->makePublic(keyScope)) { // this is needed for both local and SMP collapsing debugs(20, 3, "may " << (transients && e->hasTransients() ? "SMP-" : "locally-") << "collapse " << *e); assert(e->hittingRequiresCollapsing()); return true; } // paranoid cleanup; the flag is meaningless for private entries e->setCollapsingRequirement(false); return false; } void Store::Controller::addReading(StoreEntry *e, const cache_key *key) { if (transients) transients->monitorIo(e, key, Store::ioReading); e->hashInsert(key); } void Store::Controller::addWriting(StoreEntry *e, const cache_key *key) { assert(e); if (EBIT_TEST(e->flags, ENTRY_SPECIAL)) return; // constant memory-resident entries do not need transients if (transients) transients->monitorIo(e, key, Store::ioWriting); // else: non-SMP configurations do not need transients } void Store::Controller::syncCollapsed(const sfileno xitIndex) { assert(transients); StoreEntry *collapsed = transients->findCollapsed(xitIndex); if (!collapsed) { // the entry is no longer active, ignore update debugs(20, 7, "not SMP-syncing not-transient " << xitIndex); return; } if (!collapsed->locked()) { debugs(20, 3, "skipping (and may destroy) unlocked " << *collapsed); handleIdleEntry(*collapsed); return; } assert(collapsed->mem_obj); if (EBIT_TEST(collapsed->flags, ENTRY_ABORTED)) { debugs(20, 3, "skipping already aborted " << *collapsed); return; } debugs(20, 7, "syncing " << *collapsed); Transients::EntryStatus entryStatus; transients->status(*collapsed, entryStatus); if (entryStatus.waitingToBeFreed) { debugs(20, 3, "will release " << *collapsed << " due to waitingToBeFreed"); collapsed->release(true); // may already be marked } if (transients->isWriter(*collapsed)) return; // readers can only change our waitingToBeFreed flag assert(transients->isReader(*collapsed)); bool found = false; bool inSync = false; if (sharedMemStore && collapsed->mem_obj->memCache.io == MemObject::ioDone) { found = true; inSync = true; debugs(20, 7, "already handled by memory store: " << *collapsed); } else if (sharedMemStore && collapsed->hasMemStore()) { found = true; inSync = sharedMemStore->updateAnchored(*collapsed); // TODO: handle entries attached to both memory and disk } else if (collapsed->hasDisk()) { found = true; inSync = disks->updateAnchored(*collapsed); } else { try { found = anchorToCache(*collapsed); inSync = found; } catch (...) { // TODO: Write an exception handler for the entire method. debugs(20, 3, "anchorToCache() failed for " << *collapsed << ": " << CurrentException); collapsed->abort(); return; } } if (entryStatus.waitingToBeFreed && !found) { debugs(20, 3, "aborting unattached " << *collapsed << " because it was marked for deletion before we could attach it"); collapsed->abort(); return; } if (inSync) { debugs(20, 5, "synced " << *collapsed); assert(found); collapsed->setCollapsingRequirement(false); collapsed->invokeHandlers(); return; } if (found) { // unrecoverable problem syncing this entry debugs(20, 3, "aborting unsyncable " << *collapsed); collapsed->abort(); return; } if (!entryStatus.hasWriter) { debugs(20, 3, "aborting abandoned-by-writer " << *collapsed); collapsed->abort(); return; } // the entry is still not in one of the caches debugs(20, 7, "waiting " << *collapsed); collapsed->setCollapsingRequirement(true); } /// If possible and has not been done, associates the entry with its store(s). /// \returns false for not-yet-cached entries that we may attach later /// \returns true for other entries after synchronizing them with their store bool Store::Controller::anchorToCache(StoreEntry &entry) { assert(entry.hasTransients()); assert(transientsReader(entry)); // TODO: Attach entries to both memory and disk // TODO: Reduce code duplication with syncCollapsed() if (sharedMemStore && entry.mem().memCache.io == MemObject::ioDone) { debugs(20, 5, "already handled by memory store: " << entry); return true; } else if (sharedMemStore && entry.hasMemStore()) { debugs(20, 5, "already anchored to memory store: " << entry); return true; } else if (entry.hasDisk()) { debugs(20, 5, "already anchored to disk: " << entry); return true; } debugs(20, 7, "anchoring " << entry); Transients::EntryStatus entryStatus; transients->status(entry, entryStatus); bool found = false; if (sharedMemStore) found = sharedMemStore->anchorToCache(entry); if (!found) found = disks->anchorToCache(entry); if (found) { debugs(20, 7, "anchored " << entry); entry.setCollapsingRequirement(false); return true; } if (entryStatus.waitingToBeFreed) throw TextException("will never be able to anchor to an already marked entry", Here()); if (!entryStatus.hasWriter) throw TextException("will never be able to anchor to an abandoned-by-writer entry", Here()); debugs(20, 7, "skipping not yet cached " << entry); entry.setCollapsingRequirement(true); return false; } bool Store::Controller::SmpAware() { return MemStore::Enabled() || Disks::SmpAware(); } void Store::Controller::checkTransients(const StoreEntry &e) const { if (EBIT_TEST(e.flags, ENTRY_SPECIAL)) return; assert(!transients || e.hasTransients()); } Store::Controller& Store::Root() { static const auto root = new Controller(); return *root; }