Skip to content

Commit aef8166

Browse files
committed
fix DurabilityWorker drop order bug
1 parent 4b3757b commit aef8166

1 file changed

Lines changed: 53 additions & 20 deletions

File tree

crates/core/src/db/durability.rs

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::db::persistence::Durability;
2-
use futures::{channel::mpsc, StreamExt};
32
use spacetimedb_commitlog::payload::{
43
txdata::{Mutations, Ops},
54
Txdata,
@@ -9,11 +8,15 @@ use spacetimedb_datastore::{execution_context::ReducerContext, traits::TxData};
98
use spacetimedb_durability::DurableOffset;
109
use spacetimedb_primitives::TableId;
1110
use std::sync::Arc;
12-
13-
/// A request to persist a transaction.
14-
pub struct DurabilityRequest {
15-
reducer_context: Option<ReducerContext>,
16-
tx_data: Arc<TxData>,
11+
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
12+
13+
/// A request to persist a transaction or to terminate the actor.
14+
pub enum DurabilityRequest {
15+
Work {
16+
reducer_context: Option<ReducerContext>,
17+
tx_data: Arc<TxData>,
18+
},
19+
Close,
1720
}
1821

1922
/// Represents a handle to a background task that persists transactions
@@ -23,14 +26,28 @@ pub struct DurabilityRequest {
2326
/// before sending over to the `Durability` layer.
2427
#[derive(Clone)]
2528
pub struct DurabilityWorker {
26-
request_tx: mpsc::UnboundedSender<DurabilityRequest>,
29+
request_tx: UnboundedSender<DurabilityRequest>,
2730
durability: Arc<Durability>,
2831
}
2932

33+
impl Drop for DurabilityWorker {
34+
fn drop(&mut self) {
35+
// Try to close the actor.
36+
// If the actor paniced, or a clone of `self` was `Drop`ped,
37+
// this can return `Err(_)`,
38+
// in which case we need only drop `self.durability`.
39+
if self.request_tx.send(DurabilityRequest::Close).is_ok() {
40+
// Wait until the actor's `Arc<Durability>` has been dropped.
41+
// After that, we drop `self.durability` as normal.
42+
futures::executor::block_on(self.request_tx.closed());
43+
}
44+
}
45+
}
46+
3047
impl DurabilityWorker {
3148
/// Create a new [`DurabilityWorker`] using the given `durability` policy.
3249
pub fn new(durability: Arc<Durability>) -> Self {
33-
let (request_tx, request_rx) = mpsc::unbounded();
50+
let (request_tx, request_rx) = unbounded_channel();
3451

3552
let actor = DurabilityWorkerActor {
3653
request_rx,
@@ -54,14 +71,15 @@ impl DurabilityWorker {
5471
/// and sends the work to an actor that collects data and calls `durability.append_tx`.
5572
///
5673
/// Panics if the durability worker has closed the receive end of its queue(s),
57-
/// which is likely due to it having panicked.
74+
/// which is likely due to it having panicked
75+
/// or because `DurabilityWorker` and thus `RelationalDB` was cloned.
5876
pub fn request_durability(&self, reducer_context: Option<ReducerContext>, tx_data: &Arc<TxData>) {
5977
self.request_tx
60-
.unbounded_send(DurabilityRequest {
78+
.send(DurabilityRequest::Work {
6179
reducer_context,
6280
tx_data: tx_data.clone(),
6381
})
64-
.expect("durability worker panicked");
82+
.expect("durability actor hung up / panicked");
6583
}
6684

6785
/// Get the [`DurableOffset`] of this database.
@@ -70,24 +88,39 @@ impl DurabilityWorker {
7088
}
7189
}
7290

73-
pub struct DurabilityWorkerActor {
74-
request_rx: mpsc::UnboundedReceiver<DurabilityRequest>,
91+
struct DurabilityWorkerActor {
92+
request_rx: UnboundedReceiver<DurabilityRequest>,
7593
durability: Arc<Durability>,
7694
}
7795

7896
impl DurabilityWorkerActor {
7997
/// Processes requests to do durability.
8098
async fn run(mut self) {
81-
while let Some(DurabilityRequest {
82-
reducer_context,
83-
tx_data,
84-
}) = self.request_rx.next().await
85-
{
86-
Self::do_durability(&*self.durability, reducer_context, &tx_data);
99+
while let Some(req) = self.request_rx.recv().await {
100+
match req {
101+
DurabilityRequest::Work {
102+
reducer_context,
103+
tx_data,
104+
} => Self::do_durability(&*self.durability, reducer_context, &tx_data),
105+
106+
// Terminate the actor
107+
// and make sure we drop `self.durability`
108+
// before we drop `self.request_tx`.
109+
//
110+
// After a `Close`,
111+
// there should be no more `Work` incoming or buffered,
112+
// as `Close` hangs up the receiver end of the channel,
113+
// so nothing can be sent to it.
114+
DurabilityRequest::Close => {
115+
drop(self.durability);
116+
drop(self.request_rx);
117+
return;
118+
}
119+
}
87120
}
88121
}
89122

90-
pub fn do_durability(durability: &Durability, reducer_context: Option<ReducerContext>, tx_data: &TxData) {
123+
fn do_durability(durability: &Durability, reducer_context: Option<ReducerContext>, tx_data: &TxData) {
91124
if tx_data.tx_offset().is_none() {
92125
let name = reducer_context.as_ref().map(|rcx| &*rcx.name);
93126
debug_assert!(

0 commit comments

Comments
 (0)