1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//! What writers and the flush installer tell each other.
//!
//! A writer whose batch fills a newest in-memory table (each holds its keyspace's size) asks for a
//! switch; the installer clears the request when it looks, so requests made while it is busy fold
//! into one. A writer is admitted while the newest tables have room or another epoch may still be
//! frozen; otherwise it waits for the next install, or is refused when it asked not to wait. The
//! installer publishes what the levels ask of writers after every change to them (see
//! [`super::pressure`]): pacing writes to the delayed-write rate, each waiting for its own bytes,
//! so compaction catches up, or holding them until an install relieves the keyspace concerned. A
//! failed flush or compaction is published once and refuses every later write.
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use arc_swap::ArcSwap;
use tokio::sync::Notify;
use tokio::sync::futures::Notified;
use super::pressure::{Load, Pressure};
use super::rate_limiter::RateLimiter;
use super::view::View;
use crate::memtable::Memtable;
use crate::{Error, Result};
/// Requests, wake-ups and the failure flag shared by writers and the installer.
pub(super) struct FlushSignals {
/// A switch is asked for; later requests fold into it until the installer looks.
pending: AtomicBool,
/// Wakes the installer for a switch.
wake: Notify,
/// Wakes a flush when the batches it waits for have completed.
pub(super) visible: Notify,
/// Wakes stalled writers after every install, and on failure.
installed: Notify,
/// The highest sequence a caller of `Db::flush` wants on table files.
forced: AtomicU64,
/// The highest sequence on table files, raised after each install.
flushed: AtomicU64,
/// What failed, as one of the `FAILED_` codes: writes are refused until the database is reopened.
failed: AtomicU8,
/// Frozen epochs allowed to wait for flushes.
max_frozen: usize,
/// The most bytes the newest tables may hold together; zero sets no ceiling.
total_bytes: u64,
/// Paces writes while the levels ask for it; none paces nothing.
delay: Option<RateLimiter>,
/// What the levels ask of writers, as a [`Pressure`] code, published by the installer.
pressure: AtomicU8,
/// The most compaction has owed, for tests to read.
#[cfg(test)]
peak_owed: AtomicU64,
}
/// How far ahead of the delayed-write rate a slowed write may go: a millisecond's worth.
const SLOWDOWN_BURST: Duration = Duration::from_millis(1);
/// No failure; a flush failed; a compaction failed.
const FAILED_NONE: u8 = 0;
const FAILED_FLUSH: u8 = 1;
const FAILED_COMPACTION: u8 = 2;
impl FlushSignals {
/// Signals letting `max_frozen` epochs wait for flushes, pacing writes to `delayed_writes`
/// bytes a second, and treating the newest tables as full once they hold `total_bytes`
/// together (zero: never).
pub(super) fn new(max_frozen: u32, delayed_writes: u64, total_bytes: u64) -> Self {
Self {
pending: AtomicBool::new(false),
wake: Notify::new(),
visible: Notify::new(),
installed: Notify::new(),
forced: AtomicU64::new(0),
flushed: AtomicU64::new(0),
failed: AtomicU8::new(FAILED_NONE),
max_frozen: usize::try_from(max_frozen).unwrap_or(usize::MAX),
total_bytes,
delay: RateLimiter::new(delayed_writes, SLOWDOWN_BURST),
pressure: AtomicU8::new(Pressure::None.code()),
#[cfg(test)]
peak_owed: AtomicU64::new(0),
}
}
/// Asks for a switch when a newest table of `view` has reached its size.
pub(super) fn check(&self, view: &View) {
if self.newest_is_full(view) {
self.request();
}
}
/// Asks the installer to look for a switch.
fn request(&self) {
// AcqRel: one requester wins; pairs with the installer clearing the flag, which then
// sees what the requester did before (a forced target included).
if !self.pending.swap(true, Ordering::AcqRel) {
self.wake.notify_one();
}
}
/// Returns once every sequence up to `target` is on table files, asking for the in-memory
/// table holding it to be frozen and flushed.
///
/// # Errors
///
/// [`Error::FlushFailed`] or [`Error::CompactionFailed`] after one failed.
pub(super) async fn flush_through(&self, target: u64) -> Result<()> {
loop {
let installed = self.installed.notified();
tokio::pin!(installed);
// Registered before the checks, so an install between them still wakes this wait.
installed.as_mut().enable();
if let Some(error) = self.failure() {
return Err(error);
}
// Acquire: pairs with the Release in `set_flushed`.
if self.flushed.load(Ordering::Acquire) >= target {
return Ok(());
}
// Relaxed: published to the installer by the request's read-modify-write.
self.forced.fetch_max(target, Ordering::Relaxed);
self.request();
installed.await;
}
}
/// Records that every sequence up to `flushed` is on table files and wakes waiters.
pub(super) fn set_flushed(&self, flushed: u64) {
// Release: a waiter that reads it sees the install that wrote the file.
self.flushed.fetch_max(flushed, Ordering::Release);
self.installed.notify_waiters();
}
/// The error that stopped the installer, if any: writes must stop.
pub(super) fn failure(&self) -> Option<Error> {
// Acquire: pairs with the Release store when the installer gives up.
match self.failed.load(Ordering::Acquire) {
FAILED_NONE => None,
FAILED_COMPACTION => Some(Error::CompactionFailed),
_ => Some(Error::FlushFailed),
}
}
/// Returns once a write of `bytes` may go ahead: the newest tables have room or another epoch
/// may be frozen, and the levels do not hold writes; when they ask for pacing the write first
/// waits for its bytes at the delayed-write rate.
///
/// # Errors
///
/// [`Error::WriteStalled`] instead of waiting or being delayed when `no_slowdown`, and
/// [`Error::FlushFailed`] or [`Error::CompactionFailed`] after one failed.
pub(super) async fn admit(
&self,
view: &ArcSwap<View>,
no_slowdown: bool,
bytes: u64,
) -> Result<()> {
loop {
// Created before the check: a later `notify_waiters` reaches it even unpolled.
let installed = self.installed.notified();
if let Some(failure) = self.failure() {
return Err(failure);
}
let stalled = self.stalls(&view.load());
// Relaxed: read after `installed` is created. A store the installer made before
// waking waiters is ordered before this read by the wake-up's read-modify-write
// whenever this waiter missed that wake-up; otherwise the wake-up reaches it.
let pressure = Pressure::from_code(self.pressure.load(Ordering::Relaxed));
let reason = if stalled {
"the in-memory table is full and the frozen ones wait for flushes"
} else if pressure >= Pressure::StopOwed {
pressure.reason()
} else if pressure > Pressure::None {
if no_slowdown {
return Err(Error::WriteStalled {
reason: pressure.reason(),
});
}
let wait = self
.delay
.as_ref()
.map_or(Duration::ZERO, |delay| delay.reserve(bytes, Instant::now()));
if !wait.is_zero() {
tokio::time::sleep(wait).await;
}
return Ok(());
} else {
return Ok(());
};
if no_slowdown {
return Err(Error::WriteStalled { reason });
}
installed.await;
}
}
/// Publishes what the levels ask of writers; when that changed, held writers look again.
pub(super) fn set_load(&self, load: Load) {
// Relaxed: see `admit`.
let code = load.pressure.code();
let before = self.pressure.swap(code, Ordering::Relaxed);
if before != code {
self.installed.notify_waiters();
}
#[cfg(test)]
self.peak_owed.fetch_max(load.owed, Ordering::Relaxed);
}
/// The most compaction has owed, for tests to read.
#[cfg(test)]
pub(super) fn peak_owed(&self) -> u64 {
self.peak_owed.load(Ordering::Relaxed)
}
/// The delayed-write rate, for tests to read.
#[cfg(test)]
pub(super) fn delayed_rate(&self) -> Option<u64> {
self.delay.as_ref().map(RateLimiter::rate)
}
/// Resolves when a writer asks for a switch.
pub(super) fn requested(&self) -> Notified<'_> {
self.wake.notified()
}
/// Marks the request seen; a writer that found it set before this asked for nothing, so the
/// installer checks the size itself afterwards.
pub(super) fn take_request(&self) {
// AcqRel RMW, not a store: it reads the last requester's swap, so the size that writer
// saw is visible to the check that follows; a plain store leaves that store→load pair
// unordered and the request lost until the next write.
self.pending.swap(false, Ordering::AcqRel);
}
/// Wakes stalled writers after an install.
pub(super) fn installed(&self) {
self.installed.notify_waiters();
}
/// Publishes the installer's failure, `error`, and wakes stalled writers to see it.
pub(super) fn fail(&self, error: &Error) {
let failure = match error {
Error::CompactionFailed => FAILED_COMPACTION,
_ => FAILED_FLUSH,
};
// Release: writers that see the flag see the failure's effects.
self.failed.store(failure, Ordering::Release);
self.installed.notify_waiters();
}
/// Whether the newest table of `view` should be frozen, with `frozen` tables already waiting
/// and every sequence up to `switched` in older tables: it is full, or a flush wants a
/// sequence it holds.
pub(super) fn should_switch(&self, view: &View, frozen: usize, switched: u64) -> bool {
// Relaxed: read after the installer's read-modify-write of the request flag.
let forced = self.forced.load(Ordering::Relaxed) > switched;
frozen < self.max_frozen && (forced || self.newest_is_full(view))
}
/// Whether `view` has no room for another write: its newest table is full and no more
/// tables may be frozen.
fn stalls(&self, view: &View) -> bool {
let frozen = view.epochs().saturating_sub(1);
frozen >= self.max_frozen && self.newest_is_full(view)
}
/// Whether a newest table of `view` is full, or the newest tables together have reached
/// the database's ceiling.
fn newest_is_full(&self, view: &View) -> bool {
view.newest_tables().any(Memtable::is_full)
|| (self.total_bytes != 0
&& view
.newest_tables()
.map(Memtable::approximate_bytes)
.fold(0_u64, u64::saturating_add)
>= self.total_bytes)
}
}