Skip to content

Commit 419536b

Browse files
authored
Merge pull request #82 from httpsphl/perf/files-outside-store
perf(courier): read and write attachments with the store free
2 parents 9165d9b + 0538644 commit 419536b

2 files changed

Lines changed: 73 additions & 38 deletions

File tree

‎crates/botloftd/src/courier/mod.rs‎

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ mod render;
66
mod settings;
77
mod sleep;
88

9+
use std::path::PathBuf;
910
use std::sync::Arc;
1011
use std::time::Duration;
1112

1213
use botloft_core::ids::BotId;
13-
use botloft_core::protocol::{BotState, Delivery, Message, SenderKind};
14+
use botloft_core::protocol::{BotState, Delivery, Message, SenderKind, Task};
1415
use botloft_store::{DeliveryOutcome, Store, StoreError};
1516
use tokio::sync::Notify;
1617
use tracing::{debug, warn};
@@ -69,36 +70,47 @@ pub async fn run(daemon: Arc<Daemon>) {
6970

7071
/// What to do with a due delivery.
7172
enum Step {
72-
Send(Rendered),
73+
Send(Box<Draft>),
7374
/// The bot cannot take messages yet.
7475
Wait,
7576
/// It can never be delivered.
7677
Drop(&'static str),
7778
}
7879

80+
/// One pass over the queue. The store is locked step by step, never
81+
/// while an attachment is read from disk.
7982
fn cycle(daemon: &Arc<Daemon>) {
8083
let now = daemon.clock.now_ms();
81-
let store = daemon.store();
82-
match store.recover_leases(now) {
83-
Ok(recovered) => {
84-
for delivery in recovered {
85-
debug!(delivery = %delivery.id, "a send was cut off; delivering again");
86-
daemon.emit(Event::DeliveryChanged(delivery));
84+
let due = {
85+
let store = daemon.store();
86+
match store.recover_leases(now) {
87+
Ok(recovered) => {
88+
for delivery in recovered {
89+
debug!(delivery = %delivery.id, "a send was cut off; delivering again");
90+
daemon.emit(Event::DeliveryChanged(delivery));
91+
}
8792
}
93+
Err(err) => warn!("courier could not recover leases: {err}"),
8894
}
89-
Err(err) => warn!("courier could not recover leases: {err}"),
90-
}
91-
tasks::expire_overdue(daemon, &store, now);
92-
let due = match store.due_deliveries(now) {
93-
Ok(due) => due,
94-
Err(err) => {
95-
warn!("courier could not read the queue: {err}");
96-
return;
95+
tasks::expire_overdue(daemon, &store, now);
96+
match store.due_deliveries(now) {
97+
Ok(due) => due,
98+
Err(err) => {
99+
warn!("courier could not read the queue: {err}");
100+
return;
101+
}
97102
}
98103
};
99104
for delivery in due {
100-
let changed = match prepare(daemon, &store, &delivery, now) {
101-
Ok(Step::Send(rendered)) => send(daemon, &store, &delivery, &rendered, now),
105+
let store = daemon.store();
106+
let changed = match prepare(daemon, &store, &delivery) {
107+
Ok(Step::Send(draft)) => {
108+
drop(store);
109+
// Inline images are read here, with the store free.
110+
let rendered = draft.render(now);
111+
// Claiming checks the delivery is still waiting.
112+
send(daemon, &daemon.store(), &delivery, &rendered, now)
113+
}
102114
Ok(Step::Wait) => {
103115
let until = clock::after(now, WAIT_FOR_BOT);
104116
finish(&store, &delivery, DeliveryOutcome::Defer { until }, now)
@@ -170,13 +182,8 @@ fn send(
170182
finish(store, delivery, outcome, now)
171183
}
172184

173-
/// Decides whether the delivery can go now and renders it.
174-
fn prepare(
175-
daemon: &Daemon,
176-
store: &Store,
177-
delivery: &Delivery,
178-
now: i64,
179-
) -> Result<Step, StoreError> {
185+
/// Decides whether the delivery can go now and gathers what it needs.
186+
fn prepare(daemon: &Daemon, store: &Store, delivery: &Delivery) -> Result<Step, StoreError> {
180187
let Some(message) = store.message(&delivery.message_id)? else {
181188
return Ok(Step::Drop("the message no longer exists"));
182189
};
@@ -204,18 +211,42 @@ fn prepare(
204211
(SenderKind::Bot, Some(id)) => store.bot(id)?.map(|sender| sender.handle),
205212
_ => None,
206213
};
207-
let workspace = daemon.paths.bot_workspace(&crew.slug, &bot.slug);
208214
let routine = routine_context(store, &message)?;
209-
let context = Context {
210-
crew_name: &crew.name,
211-
sender_handle: sender.as_deref(),
212-
task: task.as_ref(),
213-
workspace: &workspace,
214-
routine: routine
215-
.as_ref()
216-
.map(|(name, scheduled, zone)| (name.as_str(), scheduled.as_str(), zone.as_str())),
217-
};
218-
Ok(Step::Send(render(&message, &context, now)))
215+
Ok(Step::Send(Box::new(Draft {
216+
workspace: daemon.paths.bot_workspace(&crew.slug, &bot.slug),
217+
message,
218+
crew_name: crew.name,
219+
sender_handle: sender,
220+
task,
221+
routine,
222+
})))
223+
}
224+
225+
/// A delivery about to go, read from the store.
226+
struct Draft {
227+
message: Message,
228+
crew_name: String,
229+
sender_handle: Option<String>,
230+
task: Option<Task>,
231+
workspace: PathBuf,
232+
routine: Option<(String, String, String)>,
233+
}
234+
235+
impl Draft {
236+
/// The line for the bot's stdin; reads the images it inlines.
237+
fn render(&self, now: i64) -> Rendered {
238+
let context = Context {
239+
crew_name: &self.crew_name,
240+
sender_handle: self.sender_handle.as_deref(),
241+
task: self.task.as_ref(),
242+
workspace: &self.workspace,
243+
routine: self
244+
.routine
245+
.as_ref()
246+
.map(|(name, scheduled, zone)| (name.as_str(), scheduled.as_str(), zone.as_str())),
247+
};
248+
render(&self.message, &context, now)
249+
}
219250
}
220251

221252
/// A routine's name, the local time the run is for and the zone.

‎crates/botloftd/src/service/messages.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ pub fn send(daemon: &Daemon, params: MessagesSendParams) -> ApiResult<Message> {
2626
// Files alone are a message too.
2727
String::new()
2828
};
29-
let store = daemon.store();
30-
let (crew, bot) = bots::active(&store, &params.bot_id)?;
29+
let (crew, bot) = bots::active(&daemon.store(), &params.bot_id)?;
3130
let now = daemon.clock.now_ms();
31+
// Decoding and writing the files takes a while: the store waits for
32+
// nobody meanwhile.
3233
let workspace = daemon.paths.bot_workspace(&crew.slug, &bot.slug);
3334
let attachments =
3435
attachments::save(&workspace, uploads, daemon.bots.attachment_max_bytes, now)?;
36+
let store = daemon.store();
37+
// Archived or deleted while the files were saved.
38+
let (crew, bot) = bots::active(&store, &params.bot_id)?;
3539
let message = Message {
3640
id: MessageId::generate(),
3741
crew_id: crew.id,

0 commit comments

Comments
 (0)