Skip to content

Commit 4726609

Browse files
authored
Do not lose wakeups for block device workers (#973)
The QueueMinder's `notify_workers` is the sole indicator that a worker thread is asleep or not, so `take_notifications` transfers the responsibility to handle these bits to the caller, which `flush_notifications` did not fully handle. Return remaining bits indicating idle workers to QueueMinder, so when there's enough work to justify waking them, we actually do. In the process, fix a less exciting bug where attempting to spuriously wake already-woken worker threads would result in waking fewer workers than desired. And, along the way, add a few new probes that were useful in debugging.
1 parent 3f1752e commit 4726609

4 files changed

Lines changed: 120 additions & 35 deletions

File tree

lib/propolis/src/block/attachment.rs

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,27 +74,57 @@ impl QueueSlot {
7474
let _ = self.notify_count.fetch_add(count.get(), Ordering::Release);
7575
}
7676
}
77-
fn take_notifications(&self) -> Option<(NonZeroUsize, Bitmap)> {
78-
let state = self.state.lock().unwrap();
79-
let minder = state.minder.as_ref()?;
80-
81-
if self.notify_count.load(Ordering::Relaxed) == 0 {
82-
return None;
83-
}
84-
let wake_wids = minder.take_notifications()?;
85-
86-
let pending = self.notify_count.swap(0, Ordering::Acquire);
87-
let count =
88-
NonZeroUsize::new(pending).expect("notify count is non-zero");
89-
Some((count, wake_wids))
90-
}
9177
fn flush_notifications(&self) {
9278
let guard = self.workers.lock().unwrap();
9379
let Some(workers) = guard.as_ref() else {
9480
return;
9581
};
96-
if let Some((count, wake_wids)) = self.take_notifications() {
97-
workers.wake(wake_wids, Some(count), Some(self.queue_id));
82+
83+
let state = self.state.lock().unwrap();
84+
let Some(minder) = state.minder.as_ref() else {
85+
// The queue isn't associated with anything yet, so there are no
86+
// interested workers to wake.
87+
return;
88+
};
89+
90+
let pending = self.notify_count.swap(0, Ordering::AcqRel);
91+
92+
let Some(pending) = NonZeroUsize::new(pending) else {
93+
// We have not been asked to wake any workers since the last
94+
// `flush_notifications`. This is relatively unlikely but
95+
// legitimate, such as if this `QueueSlot` paused and resumed (as
96+
// for migrations) repeatedly.
97+
return;
98+
};
99+
100+
// Take the full set of workers that may be idle and interested in this
101+
// queue. At this point we are responsible for either waking workers
102+
// here, or returning the idle-and-interested bit to `minder`.
103+
let Some(wake_wids) = minder.take_notifications() else {
104+
// `notify_count` was non-zero, but between checking the notify
105+
// count and getting idle workers, we started pausing devices.
106+
// Bummer. Request notification of as many workers as we were
107+
// going to, and let a future `flush_notifications()` take care of
108+
// it.
109+
self.request_notify(Some(pending));
110+
return;
111+
};
112+
drop(state);
113+
114+
let remaining_wids =
115+
workers.wake(wake_wids, pending, Some(self.queue_id));
116+
117+
if !remaining_wids.is_empty() {
118+
let state = self.state.lock().unwrap();
119+
let Some(minder) = state.minder.as_ref() else {
120+
// The queue no longer has a minder. This is unfortunate, but it
121+
// is at least OK to discard `remaining_wids` here: if this
122+
// queue is reassociated later, updating the queue collection's
123+
// associations will wake all queues.
124+
return;
125+
};
126+
127+
minder.add_notifications(remaining_wids);
98128
}
99129
}
100130
}
@@ -975,17 +1005,33 @@ impl WorkerCollection {
9751005
fn wake(
9761006
&self,
9771007
wake_wids: Bitmap,
978-
limit: Option<NonZeroUsize>,
1008+
limit: NonZeroUsize,
9791009
qid_hint: Option<QueueId>,
980-
) {
981-
let _num_woke = wake_wids
982-
.iter()
983-
.take(limit.unwrap_or(MAX_WORKERS).get())
984-
.filter_map(|wid| {
985-
let slot = self.workers.get(wid)?;
986-
slot.wake(None, qid_hint).then_some(())
987-
})
988-
.count();
1010+
) -> Bitmap {
1011+
probes::block_worker_collection_wake!(|| (wake_wids.0, limit.get()));
1012+
1013+
let mut num_woken = 0;
1014+
let mut idle_wids = wake_wids.iter();
1015+
1016+
for wid in &mut idle_wids {
1017+
let Some(slot) = self.workers.get(wid) else {
1018+
continue;
1019+
};
1020+
1021+
if slot.wake(None, qid_hint) {
1022+
num_woken += 1;
1023+
}
1024+
1025+
if num_woken == limit.get() {
1026+
break;
1027+
}
1028+
}
1029+
1030+
let remainder = idle_wids.remainder();
1031+
1032+
probes::block_worker_collection_woken!(|| (remainder.0, num_woken));
1033+
1034+
remainder
9891035
}
9901036
fn update_queue_associations(&self, queues_associated: Versioned<Bitmap>) {
9911037
let mut state = self.state.lock().unwrap();
@@ -1314,6 +1360,9 @@ impl Bitmap {
13141360
assert!(idx < Self::TOP_BIT);
13151361
self.0 &= !(1u64 << idx);
13161362
}
1363+
pub fn set_all(&mut self, other: Bitmap) {
1364+
self.0 |= other.0;
1365+
}
13171366
pub fn lowest_set(&self) -> Option<usize> {
13181367
if self.0.count_ones() == 0 {
13191368
None
@@ -1352,6 +1401,11 @@ impl Iterator for BitIter {
13521401
Some(idx)
13531402
}
13541403
}
1404+
impl BitIter {
1405+
fn remainder(self) -> Bitmap {
1406+
self.0
1407+
}
1408+
}
13551409
pub struct LoopIter {
13561410
cur: Bitmap,
13571411
orig: Bitmap,

lib/propolis/src/block/minder.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ impl QueueMinder {
194194
/// that more requests are available.
195195
pub fn next_req(&self, wid: WorkerId) -> Option<DeviceRequest> {
196196
let mut state = self.state.lock().unwrap();
197-
if state.paused || !state.notify_workers.is_empty() {
197+
if state.paused {
198198
state.notify_workers.set(wid);
199199
return None;
200200
}
@@ -315,9 +315,14 @@ impl QueueMinder {
315315
}
316316
}
317317

318-
/// Get a bitmap of the workers which should be notified that this queue may
319-
/// now have requests available.
320-
pub(crate) fn take_notifications(&self) -> Option<Bitmap> {
318+
/// Take the bitmap of the workers which should be notified that this queue
319+
/// may now have requests available.
320+
///
321+
/// Bits in this map correspond to workers that either should be
322+
/// [`WorkerSlot::wake`]'d or returned to this `QueueMinder` via
323+
/// [`add_notifications`]. Failure to do so will result in idle workers
324+
/// never being woken for future work.
325+
pub(in crate::block) fn take_notifications(&self) -> Option<Bitmap> {
321326
let mut state = self.state.lock().unwrap();
322327
if state.paused {
323328
state.notify_workers = Bitmap::ALL;
@@ -327,6 +332,18 @@ impl QueueMinder {
327332
}
328333
}
329334

335+
/// Add a set of workers to be notified when this queue may have requests
336+
/// available.
337+
///
338+
/// This should only be called with the remaining parts of a bitmap obtained
339+
/// from an ealier [`take_notifications`]. Using other bit patterns may
340+
/// result in wakeups to out-of-range worker IDs and subsequent panic.
341+
pub(in crate::block) fn add_notifications(&self, worker_ids: Bitmap) {
342+
let mut state = self.state.lock().unwrap();
343+
344+
state.notify_workers.set_all(worker_ids);
345+
}
346+
330347
/// Associate a [MetricConsumer] with this queue.
331348
///
332349
/// It will be notified about each IO completion as they occur.

lib/propolis/src/block/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ mod probes {
8686
fn block_wake(dev_id: u32, worker_id: u64) {}
8787
fn block_notify(devq_id: u64) {}
8888
fn block_strategy(dev_id: u32, strat: String, generation: u64) {}
89+
90+
fn block_worker_collection_wake(wake_wids: u64, limit: usize) {}
91+
fn block_worker_collection_woken(remaining_wids: u64, num_woken: usize) {}
8992
}
9093

9194
/// Type of operations which may be issued to a virtual block device.

lib/propolis/src/hw/nvme/mod.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod probes {
3737
fn nvme_doorbell_admin_cq(val: u16) {}
3838
fn nvme_doorbell_admin_sq(val: u16) {}
3939
fn nvme_admin_cmd(opcode: u8, prp1: u64, prp2: u64) {}
40+
fn nvme_block_notify(sqid: u16, block_qid: u16, occupied_hint: u16) {}
4041
}
4142

4243
/// The max number of MSI-X interrupts we support
@@ -1186,8 +1187,14 @@ impl PciNvme {
11861187
Some((sqid, sq.num_occupied()))
11871188
})
11881189
{
1190+
let block_qid = queue::sqid_to_block_qid(sqid);
1191+
probes::nvme_block_notify!(|| (
1192+
sqid,
1193+
u16::from(block_qid),
1194+
num_occupied
1195+
));
11891196
self.block_attach.notify(
1190-
queue::sqid_to_block_qid(sqid),
1197+
block_qid,
11911198
NonZeroUsize::new(num_occupied as usize),
11921199
);
11931200
}
@@ -1204,10 +1211,14 @@ impl PciNvme {
12041211
drop(guard);
12051212

12061213
assert_ne!(qid, queue::ADMIN_QUEUE_ID);
1207-
self.block_attach.notify(
1208-
queue::sqid_to_block_qid(qid),
1209-
NonZeroUsize::new(num_occupied as usize),
1210-
);
1214+
let block_qid = queue::sqid_to_block_qid(qid);
1215+
probes::nvme_block_notify!(|| (
1216+
qid,
1217+
u16::from(block_qid),
1218+
num_occupied
1219+
));
1220+
self.block_attach
1221+
.notify(block_qid, NonZeroUsize::new(num_occupied as usize));
12111222
};
12121223
Ok(())
12131224
}

0 commit comments

Comments
 (0)