/*
 * 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 54    Interprocess Communication */

#include "squid.h"
#include "ipc/ReadWriteLock.h"
#include "Store.h"

void Ipc::AssertFlagIsSet(std::atomic_flag &flag)
{
    // If the flag was false, then we set it to true and assert. A true flag
    // may help keep other processes away from this broken entry.
    // Otherwise, we just set an already set flag, which is probably a no-op.
    assert(flag.test_and_set(std::memory_order_relaxed));
}

/// common lockExclusive() and unlockSharedAndSwitchToExclusive() logic:
/// either finish exclusive locking or bail properly
/// \pre The caller must (be the first to) increment writeLevel.
/// \returns whether we got the exclusive lock
bool
Ipc::ReadWriteLock::finalizeExclusive()
{
    assert(writeLevel); // "new" readers are locked out by the caller
    assert(!appending); // nobody can be appending without an exclusive lock
    if (!readLevel) { // no old readers and nobody is becoming a reader
        writing = true;
        return true;
    }
    --writeLevel;
    return false;
}

bool
Ipc::ReadWriteLock::lockShared()
{
    ++readLevel; // this locks "new" writers out
    if (!writeLevel || appending) { // nobody is writing, or sharing is OK
        ++readers;
        return true;
    }
    --readLevel;
    return false;
}

bool
Ipc::ReadWriteLock::lockExclusive()
{
    if (!writeLevel++) // we are the first writer + lock "new" readers out
        return finalizeExclusive(); // decrements writeLevel on failures

    --writeLevel;
    return false;
}

bool
Ipc::ReadWriteLock::lockHeaders()
{
    if (lockShared()) {
        if (!updating.test_and_set(std::memory_order_acquire))
            return true; // we got here first
        // the updating lock was already set by somebody else
        unlockShared();
    }
    return false;
}

void
Ipc::ReadWriteLock::unlockShared()
{
    assert(readers > 0);
    --readers;
    --readLevel;
}

void
Ipc::ReadWriteLock::unlockExclusive()
{
    assert(writing);
    appending = false;
    writing = false;
    --writeLevel;
}

void
Ipc::ReadWriteLock::unlockHeaders()
{
    AssertFlagIsSet(updating);
    updating.clear(std::memory_order_release);
    unlockShared();
}

void
Ipc::ReadWriteLock::switchExclusiveToShared()
{
    assert(writing);
    ++readLevel; // must be done before we release exclusive control
    ++readers;
    unlockExclusive();
}

bool
Ipc::ReadWriteLock::unlockSharedAndSwitchToExclusive()
{
    assert(readers > 0);
    if (!writeLevel++) { // we are the first writer + lock "new" readers out
        unlockShared();
        return finalizeExclusive(); // decrements writeLevel on failures
    }

    // somebody is still writing, so we just stop reading
    unlockShared();
    --writeLevel;
    return false;
}

void
Ipc::ReadWriteLock::startAppending()
{
    assert(writing);
    appending = true;
}

bool
Ipc::ReadWriteLock::stopAppendingAndRestoreExclusive()
{
    assert(writing);
    assert(appending);

    appending = false;

    // Checking `readers` here would mishandle a lockShared() call that started
    // before we banned appending above, saw still true `appending`, got on a
    // "success" code path, but had not incremented the `readers` counter yet.
    // Checking `readLevel` mishandles lockShared() that saw false `appending`,
    // got on a "failure" code path, but had not decremented `readLevel` yet.
    // Our callers prefer the wrong "false" to the wrong "true" result.
    return !readLevel;
}

void
Ipc::ReadWriteLock::updateStats(ReadWriteLockStats &stats) const
{
    if (readers) {
        ++stats.readable;
        stats.readers += readers;
    } else if (writing) {
        ++stats.writeable;
        ++stats.writers;
        stats.appenders += appending;
    } else {
        ++stats.idle;
    }
    ++stats.count;
}

/* Ipc::ReadWriteLockStats */

Ipc::ReadWriteLockStats::ReadWriteLockStats()
{
    memset(this, 0, sizeof(*this));
}

void
Ipc::ReadWriteLockStats::dump(StoreEntry &e) const
{
    storeAppendPrintf(&e, "Available locks: %9d\n", count);

    if (!count)
        return;

    storeAppendPrintf(&e, "Reading: %9d %6.2f%%\n",
                      readable, (100.0 * readable / count));
    storeAppendPrintf(&e, "Writing: %9d %6.2f%%\n",
                      writeable, (100.0 * writeable / count));
    storeAppendPrintf(&e, "Idle:    %9d %6.2f%%\n",
                      idle, (100.0 * idle / count));

    if (readers || writers) {
        const int locked = readers + writers;
        storeAppendPrintf(&e, "Readers:         %9d %6.2f%%\n",
                          readers, (100.0 * readers / locked));
        const double appPerc = writers ? (100.0 * appenders / writers) : 0.0;
        storeAppendPrintf(&e, "Writers:         %9d %6.2f%% including Appenders: %9d %6.2f%%\n",
                          writers, (100.0 * writers / locked),
                          appenders, appPerc);
    }
}

std::ostream &
Ipc::operator <<(std::ostream &os, const Ipc::ReadWriteLock &lock)
{
    return os << lock.readers << 'R' <<
           (lock.writing ? "W" : "") <<
           (lock.appending ? "A" : "");
    // impossible to report lock.updating without setting/clearing that flag
}

