Files
scylladb/utils/atomic_vector.hh
Michael Litvak 55f4a2b754 migration_listener: fix deadlock in nested notifications
When calling a migration notification from the context of a notification
callback, this could lead to a deadlock with unregistering a listener:
A: the parent notification is called. it calls thread_for_each, where it
   acquires a read lock on the vector of listeners, and calls the
   callback function for each listener while holding the lock.
B: a listener is unregistered. it calls `remove` and tries to acquire a
   write lock on the vector of listeners. it waits because the lock is
   held.
A: the callback function calls another notification and calls
   thread_for_each which tries to acquire the read lock again. but it
   waits since there is a waiter.

Currently we have such concrete scenario when creating a table, where
the callback of `before_create_column_family` in the tablet allocator
calls `before_allocate_tablet_map`, and this could deadlock with node
shutdown where we unregister listeners.

Fix this by not acquiring the read lock again in the nested
notification. There is no need because the read lock is already held by
the parent notification while the child notification is running. We add
a function `thread_for_each_nested` that is similar to `thread_for_each`
except it assumes the read lock is already held and doesn't acquire it,
and it should be used for nested notifications instead of
`thread_for_each`.

Fixes scylladb/scylladb#27364

Closes scylladb/scylladb#27637
2025-12-17 14:00:28 +01:00

90 lines
3.2 KiB
C++

/*
* Copyright (C) 2020-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#pragma once
#include "utils/on_internal_error.hh"
#include <seastar/core/rwlock.hh>
#include <seastar/util/defer.hh>
#include <seastar/util/noncopyable_function.hh>
#include <vector>
// This class supports atomic removes (by using a lock and returning a
// future) and non atomic insert and iteration (by using indexes).
template <typename T>
class atomic_vector {
std::vector<T> _vec;
mutable seastar::rwlock _vec_lock;
public:
void add(const T& value) {
_vec.push_back(value);
}
seastar::future<> remove(const T& value) {
return with_lock(_vec_lock.for_write(), [this, value] {
_vec.erase(std::remove(_vec.begin(), _vec.end(), value), _vec.end());
});
}
// This must be called on a thread. The callback function must not
// call remove or thread_for_each. If the callback function needs to
// call this, use thread_for_each_nested instead.
//
// We would take callbacks that take a T&, but we had bugs in the
// past with some of those callbacks holding that reference past a
// preemption.
void thread_for_each(seastar::noncopyable_function<void(T)> func) const {
_vec_lock.for_read().lock().get();
auto unlock = seastar::defer([this] {
_vec_lock.for_read().unlock();
});
// We grab a lock in remove(), but not in add(), so we
// iterate using indexes to guard against the vector being
// reallocated.
for (size_t i = 0, n = _vec.size(); i < n; ++i) {
func(_vec[i]);
}
}
// This must be called on a thread. This should be used only from
// the context of a thread_for_each callback, when the read lock is
// already held. The callback function must not call remove or
// thread_for_each.
void thread_for_each_nested(seastar::noncopyable_function<void(T)> func) const {
// When called in the context of thread_for_each, the read lock is
// already held, so we don't need to acquire it again. Acquiring it
// again could lead to a deadlock.
if (!_vec_lock.locked()) {
utils::on_internal_error("thread_for_each_nested called without holding the vector lock");
}
// We grab a lock in remove(), but not in add(), so we
// iterate using indexes to guard against the vector being
// reallocated.
for (size_t i = 0, n = _vec.size(); i < n; ++i) {
func(_vec[i]);
}
}
// The callback function must not call remove.
//
// We would take callbacks that take a T&, but we had bugs in the
// past with some of those callbacks holding that reference past a
// preemption.
seastar::future<> for_each(seastar::noncopyable_function<seastar::future<>(T)> func) const {
auto holder = co_await _vec_lock.hold_read_lock();
// We grab a lock in remove(), but not in add(), so we
// iterate using indexes to guard against the vector being
// reallocated.
for (size_t i = 0, n = _vec.size(); i < n; ++i) {
co_await func(_vec[i]);
}
}
};