mirror of
https://github.com/facebook/rocksdb.git
synced 2026-10-23 08:09:02 +03:00
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15387 ## Summary RocksDB's `.clang-tidy` enables `cppcoreguidelines-special-member-functions`, `cppcoreguidelines-virtual-class-destructor` and the noexcept-move checks, and clang warns `-Wdeprecated-copy-with-user-provided-dtor`. A full scan reports 476 findings. In most of them, a class declares a destructor (or copy operations) but leaves the rest of copy/move implicit. That has two costs. Readers can't tell whether a class may be copied or moved. And resource-owning classes keep an implicit copy that would double-free if anyone ever used it. This diff resolves 453 of the 476. Approach, picked per class from least to most invasive: 1. An empty user-provided destructor becomes `= default`. This covers most interface classes, such as `virtual ~X() {}` -> `= default`. If the class is still copied, that would only rename the deprecated-copy warning, so the destructor is removed instead when nothing needs it (`Customizable`, `TableFactory` and `InternalKeyComparator` inherit a virtual one; `IteratorWrapperBase` isn't polymorphic), and `MemTableRep::KeyComparator` gets defaulted copy and move. 2. Classes whose copy is already deleted get explicitly deleted move operations too. Move was already unavailable, so this only documents it. 3. Resource-owning classes that are never copied get deleted copy and move operations. Examples: the C API wrapper structs, which run a `destructor_` callback (edited in `tools/c_api_gen/c_base.cc` and regenerated into `db/c.cc`); `ShardedCache`, which frees its shards; iterator wrappers; and RAII handles. 4. Value types that are copied or moved get the missing operations as `= default`, which keeps their current behavior. 5. Exception specifications of existing functions are unchanged, except for `WriteBatch`'s move assignment (below): no other `noexcept` is added or removed. Only the move operations this diff newly declares (`Comparator`, `FileSampledStats` and the thread-local `Entry`) are `noexcept`; they copy only scalars or atomics. `WriteBatch`'s copy and move assignment destroyed `*this` and placement-new'ed a new `WriteBatch` over it. That is undefined behavior when `*this` is a subclass, and if the copy constructor threw, `*this` was left destroyed and was later destroyed again. Move assignment now assigns member-wise and ends in the same state as the move constructor, which leaves `has_key_with_ts_`, the timestamp-size tracking and the other fields it doesn't copy at their defaults. `rep_` is swapped through a temporary, because a plain string move-assignment can keep the old buffer (e.g. a large batch reset with `batch = WriteBatch()` would keep its memory). Nothing in it can throw, so it is now `noexcept`. Copy assignment copies into a temporary and moves it in, so a throwing copy leaves `*this` unchanged. Bugs this exposed and fixed: - `RestoreAfterCopyOrCreateWorkItem` (backup engine): the hand-written move constructor and assignment moved only some fields, so `from_file` and `to_file` were empty after `push_back(std::move(...))`. A restore that hit a checksum mismatch then reported "While restoring -> : ..." without file names. It now uses the compiler-generated moves, which are still `noexcept`. - `ExternalSstFileIngestionJob` lived in a `std::vector`, and its destructor frees raw-owned pointers, so the implicit copy used on vector reallocation would double-free. Its file batches also point into its own inline file storage, so neither a copy nor a move is safe after `Prepare()`. Copy and move are now deleted, and `DBImpl` holds jobs through `std::unique_ptr`. Compatibility: no public class loses an operation users can call. Public interfaces mostly only get `virtual ~X() = default;`. `WriteBatch`'s move assignment becomes `noexcept`, which is not part of the mangled name, so the ABI doesn't change. `SubcompactionJobInfo` and `CompactionJobInfo` intentionally stay C++20 aggregates, so users' aggregate and designated initializers keep compiling. `Configurable`, `CompareInterface`, `WriteBatchBase`, `ObjectLibrary::Entry` and `MemTableRep::KeyComparator` declare defaulted copy and move, and `Customizable` no longer declares a destructor. As a result, `Configurable` and `Customizable` subclasses that declare no destructor or copy operations of their own now move their registered-options list instead of copying it. In-tree, these are the mutable-options adapters, the table factories, the memory allocators and test classes; all are held through smart pointers or used in place, and `Clone()` copies, so none is moved by value. Not changed (23): `IODebugContext` (custom deep copy plus a `std::shared_mutex`), `DBWithColumnFamilies` in db_bench (stored in a `std::vector` with non-movable members), `Snapshot`'s protected virtual destructor (intended), `ManagedSnapshot`, the `SubcompactionJobInfo` and `CompactionJobInfo` aggregates, and the 16 noexcept-move-constructor and noexcept-swap findings on existing functions, including `autovector`'s move assignment (adding `noexcept` would change their exception specifications). Copying `ManagedSnapshot` already double-releases the snapshot. Fixing that needs a real move implementation and is a behavior change, so it is left for a separate diff. Reviewed By: rban1 Differential Revision: D123186309 fbshipit-source-id: 6f8e57e233e12199a007407130bb9a1f6d52e8d1
234 lines
7.3 KiB
C++
234 lines
7.3 KiB
C++
// Portions Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
//
|
|
// Borrowed from
|
|
// http://www.crazygaze.com/blog/2016/03/24/portable-c-timer-queue/
|
|
// Timer Queue
|
|
//
|
|
// License
|
|
//
|
|
// The source code in this article is licensed under the CC0 license, so feel
|
|
// free to copy, modify, share, do whatever you want with it.
|
|
// No attribution is required, but Ill be happy if you do.
|
|
// CC0 license
|
|
|
|
// The person who associated a work with this deed has dedicated the work to the
|
|
// public domain by waiving all of his or her rights to the work worldwide
|
|
// under copyright law, including all related and neighboring rights, to the
|
|
// extent allowed by law. You can copy, modify, distribute and perform the
|
|
// work, even for commercial purposes, all without asking permission.
|
|
|
|
#pragma once
|
|
|
|
#include <assert.h>
|
|
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <functional>
|
|
#include <queue>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "port/port.h"
|
|
#include "test_util/sync_point.h"
|
|
|
|
// Allows execution of handlers at a specified time in the future
|
|
// Guarantees:
|
|
// - All handlers are executed ONCE, even if cancelled (aborted parameter will
|
|
// be set to true)
|
|
// - If TimerQueue is destroyed, it will cancel all handlers.
|
|
// - Handlers are ALWAYS executed in the Timer Queue worker thread.
|
|
// - Handlers execution order is NOT guaranteed
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// borrowed from
|
|
// http://www.crazygaze.com/blog/2016/03/24/portable-c-timer-queue/
|
|
class TimerQueue {
|
|
public:
|
|
TimerQueue() : m_th(&TimerQueue::run, this) {}
|
|
|
|
~TimerQueue() { shutdown(); }
|
|
|
|
// This function is not thread-safe.
|
|
void shutdown() {
|
|
if (closed_) {
|
|
return;
|
|
}
|
|
cancelAll();
|
|
// Abusing the timer queue to trigger the shutdown.
|
|
add(0, [this](bool) {
|
|
m_finish = true;
|
|
return std::make_pair(false, 0);
|
|
});
|
|
m_th.join();
|
|
closed_ = true;
|
|
}
|
|
|
|
// Adds a new timer
|
|
// \return
|
|
// Returns the ID of the new timer. You can use this ID to cancel the
|
|
// timer
|
|
uint64_t add(int64_t milliseconds,
|
|
std::function<std::pair<bool, int64_t>(bool)> handler) {
|
|
WorkItem item;
|
|
Clock::time_point tp = Clock::now();
|
|
item.end = tp + std::chrono::milliseconds(milliseconds);
|
|
TEST_SYNC_POINT_CALLBACK("TimeQueue::Add:item.end", &item.end);
|
|
item.period = milliseconds;
|
|
item.handler = std::move(handler);
|
|
|
|
std::unique_lock<std::mutex> lk(m_mtx);
|
|
uint64_t id = ++m_idcounter;
|
|
item.id = id;
|
|
m_items.push(std::move(item));
|
|
|
|
// Something changed, so wake up timer thread
|
|
m_checkWork.notify_one();
|
|
return id;
|
|
}
|
|
|
|
// Cancels the specified timer
|
|
// \return
|
|
// 1 if the timer was cancelled.
|
|
// 0 if you were too late to cancel (or the timer ID was never valid to
|
|
// start with)
|
|
size_t cancel(uint64_t id) {
|
|
// Instead of removing the item from the container (thus breaking the
|
|
// heap integrity), we set the item as having no handler, and put
|
|
// that handler on a new item at the top for immediate execution
|
|
// The timer thread will then ignore the original item, since it has no
|
|
// handler.
|
|
std::unique_lock<std::mutex> lk(m_mtx);
|
|
for (auto&& item : m_items.getContainer()) {
|
|
if (item.id == id && item.handler) {
|
|
WorkItem newItem;
|
|
// Zero time, so it stays at the top for immediate execution
|
|
newItem.end = Clock::time_point();
|
|
newItem.id = 0; // Means it is a canceled item
|
|
// Move the handler from item to newitem (thus clearing item)
|
|
newItem.handler = std::move(item.handler);
|
|
m_items.push(std::move(newItem));
|
|
|
|
// Something changed, so wake up timer thread
|
|
m_checkWork.notify_one();
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Cancels all timers
|
|
// \return
|
|
// The number of timers cancelled
|
|
size_t cancelAll() {
|
|
// Setting all "end" to 0 (for immediate execution) is ok,
|
|
// since it maintains the heap integrity
|
|
std::unique_lock<std::mutex> lk(m_mtx);
|
|
m_cancel = true;
|
|
for (auto&& item : m_items.getContainer()) {
|
|
if (item.id && item.handler) {
|
|
item.end = Clock::time_point();
|
|
item.id = 0;
|
|
}
|
|
}
|
|
auto ret = m_items.size();
|
|
|
|
m_checkWork.notify_one();
|
|
return ret;
|
|
}
|
|
|
|
private:
|
|
using Clock = std::chrono::steady_clock;
|
|
TimerQueue(const TimerQueue&) = delete;
|
|
TimerQueue& operator=(const TimerQueue&) = delete;
|
|
TimerQueue(TimerQueue&&) = delete;
|
|
TimerQueue& operator=(TimerQueue&&) = delete;
|
|
|
|
void run() {
|
|
std::unique_lock<std::mutex> lk(m_mtx);
|
|
while (!m_finish) {
|
|
auto end = calcWaitTime_lock();
|
|
if (end.first) {
|
|
// Timers found, so wait until it expires (or something else
|
|
// changes)
|
|
m_checkWork.wait_until(lk, end.second);
|
|
} else {
|
|
// No timers exist, so wait forever until something changes
|
|
m_checkWork.wait(lk);
|
|
}
|
|
|
|
// Check and execute as much work as possible, such as, all expired
|
|
// timers
|
|
checkWork(&lk);
|
|
}
|
|
|
|
// If we are shutting down, we should not have any items left,
|
|
// since the shutdown cancels all items
|
|
assert(m_items.size() == 0);
|
|
}
|
|
|
|
std::pair<bool, Clock::time_point> calcWaitTime_lock() {
|
|
while (m_items.size()) {
|
|
if (m_items.top().handler) {
|
|
// Item present, so return the new wait time
|
|
return std::make_pair(true, m_items.top().end);
|
|
} else {
|
|
// Discard empty handlers (they were cancelled)
|
|
m_items.pop();
|
|
}
|
|
}
|
|
|
|
// No items found, so return no wait time (causes the thread to wait
|
|
// indefinitely)
|
|
return std::make_pair(false, Clock::time_point());
|
|
}
|
|
|
|
void checkWork(std::unique_lock<std::mutex>* lk) {
|
|
while (m_items.size() && m_items.top().end <= Clock::now()) {
|
|
WorkItem item(m_items.top());
|
|
m_items.pop();
|
|
|
|
if (item.handler) {
|
|
(*lk).unlock();
|
|
auto reschedule_pair = item.handler(item.id == 0);
|
|
(*lk).lock();
|
|
if (!m_cancel && reschedule_pair.first) {
|
|
int64_t new_period = (reschedule_pair.second == -1)
|
|
? item.period
|
|
: reschedule_pair.second;
|
|
|
|
item.period = new_period;
|
|
item.end = Clock::now() + std::chrono::milliseconds(new_period);
|
|
m_items.push(std::move(item));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool m_finish = false;
|
|
bool m_cancel = false;
|
|
uint64_t m_idcounter = 0;
|
|
std::condition_variable m_checkWork;
|
|
|
|
struct WorkItem {
|
|
Clock::time_point end;
|
|
int64_t period;
|
|
uint64_t id; // id==0 means it was cancelled
|
|
std::function<std::pair<bool, int64_t>(bool)> handler;
|
|
bool operator>(const WorkItem& other) const { return end > other.end; }
|
|
};
|
|
|
|
std::mutex m_mtx;
|
|
// Inheriting from priority_queue, so we can access the internal container
|
|
class Queue : public std::priority_queue<WorkItem, std::vector<WorkItem>,
|
|
std::greater<WorkItem>> {
|
|
public:
|
|
std::vector<WorkItem>& getContainer() { return this->c; }
|
|
} m_items;
|
|
ROCKSDB_NAMESPACE::port::Thread m_th;
|
|
bool closed_ = false;
|
|
};
|