/*
 * Copyright (C) 1996-2026 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    Storage Manager */

#include "squid.h"
#include "base/RunnersRegistry.h"
#include "CollapsedForwarding.h"
#include "HttpReply.h"
#include "ipc/mem/Page.h"
#include "ipc/mem/Pages.h"
#include "MemObject.h"
#include "mime_header.h"
#include "SquidConfig.h"
#include "SquidMath.h"
#include "StoreStats.h"
#include "tools.h"
#include "Transients.h"

#include <limits>

/// shared memory segment path to use for Transients map
static const auto &
MapLabel()
{
    static const auto label = new SBuf("transients_map");
    return *label;
}

Transients::Transients(): map(nullptr), locals(nullptr)
{
}

Transients::~Transients()
{
    delete map;
    delete locals;
}

void
Transients::init()
{
    assert(Enabled());
    const int64_t entryLimit = EntryLimit();
    assert(entryLimit > 0);

    Must(!map);
    map = new TransientsMap(MapLabel());
    map->cleaner = this;
    map->disableHitValidation(); // Transients lacks slices to validate

    locals = new Locals(entryLimit, nullptr);
}

void
Transients::getStats(StoreInfoStats &stats) const
{
#if TRANSIENT_STATS_SUPPORTED
    const size_t pageSize = Ipc::Mem::PageSize();

    stats.mem.shared = true;
    stats.mem.capacity =
        Ipc::Mem::PageLimit(Ipc::Mem::PageId::cachePage) * pageSize;
    stats.mem.size =
        Ipc::Mem::PageLevel(Ipc::Mem::PageId::cachePage) * pageSize;
    stats.mem.count = currentCount();
#else
    (void)stats;
#endif
}

void
Transients::stat(StoreEntry &e) const
{
    storeAppendPrintf(&e, "\n\nTransient Objects\n");

    storeAppendPrintf(&e, "Maximum Size: %.0f KB\n", maxSize()/1024.0);
    storeAppendPrintf(&e, "Current Size: %.2f KB %.2f%%\n",
                      currentSize() / 1024.0,
                      Math::doublePercent(currentSize(), maxSize()));

    if (map) {
        const int limit = map->entryLimit();
        storeAppendPrintf(&e, "Maximum entries: %9d\n", limit);
        if (limit > 0) {
            storeAppendPrintf(&e, "Current entries: %" PRId64 " %.2f%%\n",
                              currentCount(), (100.0 * currentCount() / limit));
        }
    }
}

void
Transients::maintain()
{
    // no lazy garbage collection needed
}

uint64_t
Transients::minSize() const
{
    return 0; // XXX: irrelevant, but Store parent forces us to implement this
}

uint64_t
Transients::maxSize() const
{
    // Squid currently does not limit the total size of all transient objects
    return std::numeric_limits<uint64_t>::max();
}

uint64_t
Transients::currentSize() const
{
    // TODO: we do not get enough information to calculate this
    // StoreEntry should update associated stores when its size changes
    return 0;
}

uint64_t
Transients::currentCount() const
{
    return map ? map->entryCount() : 0;
}

int64_t
Transients::maxObjectSize() const
{
    // Squid currently does not limit the size of a transient object
    return std::numeric_limits<uint64_t>::max();
}

void
Transients::reference(StoreEntry &)
{
    // no replacement policy (but the cache(s) storing the entry may have one)
}

bool
Transients::dereference(StoreEntry &)
{
    // no need to keep e in the global store_table for us; we have our own map
    return false;
}

StoreEntry *
Transients::get(const cache_key *key)
{
    if (!map)
        return nullptr;

    sfileno index;
    const Ipc::StoreMapAnchor *anchor = map->openForReading(key, index);
    if (!anchor)
        return nullptr;

    // If we already have a local entry, the store_table should have found it.
    // Since it did not, the local entry key must have changed from public to
    // private. We still need to keep the private entry around for syncing as
    // its clients depend on it, but we should not allow new clients to join.
    if (StoreEntry *oldE = locals->at(index)) {
        debugs(20, 3, "not joining private " << *oldE);
        assert(EBIT_TEST(oldE->flags, KEY_PRIVATE));
        map->closeForReadingAndFreeIdle(index);
        return nullptr;
    }

    StoreEntry *e = new StoreEntry();
    e->createMemObject();
    e->mem_obj->xitTable.open(index, Store::ioReading);

    // keep read lock to receive updates from others
    return e;
}

StoreEntry *
Transients::findCollapsed(const sfileno index)
{
    if (!map)
        return nullptr;

    if (StoreEntry *oldE = locals->at(index)) {
        debugs(20, 5, "found " << *oldE << " at " << index << " in " << MapLabel());
        assert(oldE->mem_obj && oldE->mem_obj->xitTable.index == index);
        return oldE;
    }

    debugs(20, 3, "no entry at " << index << " in " << MapLabel());
    return nullptr;
}

void
Transients::monitorIo(StoreEntry *e, const cache_key *key, const Store::IoStatus direction)
{
    if (!e->hasTransients()) {
        addEntry(e, key, direction);
        assert(e->hasTransients());
    }

    const auto index = e->mem_obj->xitTable.index;
    if (const auto old = locals->at(index)) {
        assert(old == e);
    } else {
        // We do not lock e because we do not want to prevent its destruction;
        // e is tied to us via mem_obj so we will know when it is destructed.
        locals->at(index) = e;
    }
}

/// creates a new Transients entry
void
Transients::addEntry(StoreEntry *e, const cache_key *key, const Store::IoStatus direction)
{
    assert(e);
    assert(e->mem_obj);
    assert(!e->hasTransients());

    Must(map); // configured to track transients

    if (direction == Store::ioWriting)
        return addWriterEntry(*e, key);

    assert(direction == Store::ioReading);
    addReaderEntry(*e, key);
}

/// addEntry() helper used for cache entry creators/writers
void
Transients::addWriterEntry(StoreEntry &e, const cache_key *key)
{
    sfileno index = 0;
    const auto anchor = map->openForWriting(key, index);
    if (!anchor)
        throw TextException("writer collision", Here());

    // set ASAP in hope to unlock the slot if something throws
    // and to provide index to such methods as hasWriter()
    e.mem_obj->xitTable.open(index, Store::ioWriting);

    anchor->setKey(key);
    // allow reading and receive remote DELETE events, but do not switch to
    // the reading lock because transientReaders() callers want true readers
    map->startAppending(index);
}

/// addEntry() helper used for cache readers
/// readers do not modify the cache, but they must create a Transients entry
void
Transients::addReaderEntry(StoreEntry &e, const cache_key *key)
{
    sfileno index = 0;
    const auto anchor = map->openOrCreateForReading(key, index);
    if (!anchor)
        throw TextException("reader collision", Here());

    e.mem_obj->xitTable.open(index, Store::ioReading);
    // keep the entry locked (for reading) to receive remote DELETE events
}

bool
Transients::hasWriter(const StoreEntry &e)
{
    if (!e.hasTransients())
        return false;
    return map->peekAtWriter(e.mem_obj->xitTable.index);
}

void
Transients::noteFreeMapSlice(const Ipc::StoreMapSliceId)
{
    // TODO: we should probably find the entry being deleted and abort it
}

void
Transients::status(const StoreEntry &entry, Transients::EntryStatus &entryStatus) const
{
    assert(map);
    assert(entry.hasTransients());
    const auto idx = entry.mem_obj->xitTable.index;
    const auto &anchor = isWriter(entry) ?
                         map->writeableEntry(idx) : map->readableEntry(idx);
    entryStatus.hasWriter = anchor.writing();
    entryStatus.waitingToBeFreed = anchor.waitingToBeFreed;
}

void
Transients::completeWriting(const StoreEntry &e)
{
    debugs(20, 5, e);
    assert(e.hasTransients());
    assert(isWriter(e));
    map->switchWritingToReading(e.mem_obj->xitTable.index);
    e.mem_obj->xitTable.io = Store::ioReading;
    CollapsedForwarding::Broadcast(e);
}

int
Transients::readers(const StoreEntry &e) const
{
    if (e.hasTransients()) {
        assert(map);
        return map->peekAtEntry(e.mem_obj->xitTable.index).lock.readers;
    }
    return 0;
}

void
Transients::evictCached(StoreEntry &e)
{
    debugs(20, 5, e);
    if (e.hasTransients()) {
        const auto index = e.mem_obj->xitTable.index;
        if (map->freeEntry(index)) {
            // Delay syncCollapsed(index) which may end `e` wait for updates.
            // Calling it directly/here creates complex reentrant call chains.
            CollapsedForwarding::Broadcast(e, true);
        }
    } // else nothing to do because e must be private
}

void
Transients::evictIfFound(const cache_key *key)
{
    if (!map)
        return;

    const sfileno index = map->fileNoByKey(key);
    if (map->freeEntry(index))
        CollapsedForwarding::Broadcast(index, true);
}

void
Transients::disconnect(StoreEntry &entry)
{
    debugs(20, 5, entry);
    if (entry.hasTransients()) {
        auto &xitTable = entry.mem_obj->xitTable;
        assert(map);
        if (isWriter(entry)) {
            // completeWriting() was not called, so there could be an active
            // Store writer out there, but we should not abortWriting() here
            // because another writer may have succeeded, making readers happy.
            // If none succeeded, the readers will notice the lack of writers.
            map->closeForWriting(xitTable.index);
            CollapsedForwarding::Broadcast(entry);
        } else {
            assert(isReader(entry));
            map->closeForReadingAndFreeIdle(xitTable.index);
        }
        locals->at(xitTable.index) = nullptr;
        xitTable.close();
    }
}

/// calculates maximum number of entries we need to store and map
int64_t
Transients::EntryLimit()
{
    return (UsingSmp() && Store::Controller::SmpAware()) ?
           Config.shared_transient_entries_limit : 0;
}

bool
Transients::markedForDeletion(const cache_key *key) const
{
    assert(map);
    return map->markedForDeletion(key);
}

bool
Transients::isReader(const StoreEntry &e) const
{
    return e.mem_obj && e.mem_obj->xitTable.io == Store::ioReading;
}

bool
Transients::isWriter(const StoreEntry &e) const
{
    return e.mem_obj && e.mem_obj->xitTable.io == Store::ioWriting;
}

/// initializes shared memory segment used by Transients
class TransientsRr: public Ipc::Mem::RegisteredRunner
{
public:
    /* RegisteredRunner API */
    void useConfig() override;
    ~TransientsRr() override;

protected:
    void create() override;

private:
    TransientsMap::Owner *mapOwner = nullptr;
};

DefineRunnerRegistrator(TransientsRr);

void
TransientsRr::useConfig()
{
    assert(Config.memShared.configured());
    Ipc::Mem::RegisteredRunner::useConfig();
}

void
TransientsRr::create()
{
    const int64_t entryLimit = Transients::EntryLimit();
    if (entryLimit <= 0)
        return; // no SMP configured or a misconfiguration

    Must(!mapOwner);
    mapOwner = TransientsMap::Init(MapLabel(), entryLimit);
}

TransientsRr::~TransientsRr()
{
    delete mapOwner;
}

