/*
* This file is part of Scylla.
*
* Scylla is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Scylla is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Scylla. If not, see .
*/
/*
* Copyright (C) 2017 ScyllaDB
*/
#pragma once
#include
#include
#include "db/timeout_clock.hh"
/// Specific semaphore for controlling reader concurrency
///
/// Before creating a reader one should obtain a permit by calling
/// `wait_admission()`. This permit can then be used for tracking the
/// reader's memory consumption via `reader_resource_tracker`.
/// The permit should be held onto for the lifetime of the reader
/// and/or any buffer its tracking.
/// Reader concurrency is dual limited by count and memory.
/// The semaphore can be configured with the desired limits on
/// construction. New readers will only be admitted when there is both
/// enough count and memory units available. Readers are admitted in
/// FIFO order.
/// It's possible to specify the maximum allowed number of waiting
/// readers by the `max_queue_length` constructor parameter. When the
/// number waiting readers would be equal or greater than this number
/// (when calling `wait_admission()`) an exception will be thrown.
/// The type of the exception and optionally some additional code
/// that should be executed when this happens can be customized by the
/// `raise_queue_overloaded_exception` constructor parameter. This
/// function will be called every time the queue limit is surpassed.
/// It is expected to return an `std::exception_ptr` that will be
/// injected into the future.
class reader_concurrency_semaphore {
public:
struct resources {
int count = 0;
ssize_t memory = 0;
resources() = default;
resources(int count, ssize_t memory)
: count(count)
, memory(memory) {
}
bool operator>=(const resources& other) const {
return count >= other.count && memory >= other.memory;
}
resources& operator-=(const resources& other) {
count -= other.count;
memory -= other.memory;
return *this;
}
resources& operator+=(const resources& other) {
count += other.count;
memory += other.memory;
return *this;
}
explicit operator bool() const {
return count >= 0 && memory >= 0;
}
};
class reader_permit {
reader_concurrency_semaphore& _semaphore;
const resources _base_cost;
public:
reader_permit(reader_concurrency_semaphore& semaphore, resources base_cost)
: _semaphore(semaphore)
, _base_cost(base_cost) {
}
~reader_permit() {
_semaphore.signal(_base_cost);
}
reader_permit(const reader_permit&) = delete;
reader_permit& operator=(const reader_permit&) = delete;
reader_permit(reader_permit&& other) = delete;
reader_permit& operator=(reader_permit&& other) = delete;
void consume_memory(size_t memory) {
_semaphore.consume_memory(memory);
}
void signal_memory(size_t memory) {
_semaphore.signal_memory(memory);
}
};
private:
static std::exception_ptr default_make_queue_overloaded_exception() {
return std::make_exception_ptr(std::runtime_error("restricted mutation reader queue overload"));
}
resources _resources;
struct entry {
promise> pr;
resources res;
entry(promise>&& pr, resources r) : pr(std::move(pr)), res(r) {}
};
struct expiry_handler {
void operator()(entry& e) noexcept {
e.pr.set_exception(semaphore_timed_out());
}
};
expiring_fifo _wait_list;
size_t _max_queue_length = std::numeric_limits::max();
std::function _make_queue_overloaded_exception = default_make_queue_overloaded_exception;
std::function _evict_an_inactive_reader;
bool has_available_units(const resources& r) const {
return bool(_resources) && _resources >= r;
}
bool may_proceed(const resources& r) const {
return has_available_units(r) && _wait_list.empty();
}
void consume_memory(size_t memory) {
_resources.memory -= memory;
}
void signal(const resources& r);
void signal_memory(size_t memory) {
signal(resources(0, static_cast(memory)));
}
public:
reader_concurrency_semaphore(unsigned count,
size_t memory,
size_t max_queue_length = std::numeric_limits::max(),
std::function raise_queue_overloaded_exception = default_make_queue_overloaded_exception,
std::function evict_an_inactive_reader = {})
: _resources(count, memory)
, _max_queue_length(max_queue_length)
, _make_queue_overloaded_exception(raise_queue_overloaded_exception)
, _evict_an_inactive_reader(std::move(evict_an_inactive_reader)) {
}
reader_concurrency_semaphore(const reader_concurrency_semaphore&) = delete;
reader_concurrency_semaphore& operator=(const reader_concurrency_semaphore&) = delete;
reader_concurrency_semaphore(reader_concurrency_semaphore&&) = delete;
reader_concurrency_semaphore& operator=(reader_concurrency_semaphore&&) = delete;
future> wait_admission(size_t memory, db::timeout_clock::time_point timeout = db::no_timeout);
const resources available_resources() const {
return _resources;
}
size_t waiters() const {
return _wait_list.size();
}
};
class reader_resource_tracker {
lw_shared_ptr _permit;
public:
reader_resource_tracker() = default;
explicit reader_resource_tracker(lw_shared_ptr permit)
: _permit(std::move(permit)) {
}
bool operator==(const reader_resource_tracker& other) const {
return _permit == other._permit;
}
file track(file f) const;
lw_shared_ptr get_permit() const {
return _permit;
}
};
inline reader_resource_tracker no_resource_tracking() {
return {};
}