Skip to content

Commit 690d1be

Browse files
committed
Wait for concurrent writes when reserving Slack work
1 parent d47d39d commit 690d1be

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎src/store.rs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,12 @@ impl Store {
377377
}
378378
pub fn claim(&mut self, a: &Attention) -> Result<Option<Job>> {
379379
let now = now_ms();
380-
let tx = self.db.transaction()?;
380+
// Reserve the write before reading: permission/status connections can
381+
// write concurrently, and a deferred read-to-write upgrade can fail
382+
// immediately with SQLITE_BUSY instead of honoring the busy timeout.
383+
let tx = self
384+
.db
385+
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
381386
let candidate:Option<(String,String,String,String)>=tx.query_row("SELECT id,context,trigger,note FROM jobs j WHERE status='pending' AND available<=?2 AND updated<=?1 AND NOT EXISTS(SELECT 1 FROM jobs x WHERE x.context=j.context AND x.status='running') ORDER BY created LIMIT 1",params![now-a.settle_ms as i64,now],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?))).optional()?;
382387
let Some((id, context, trigger, note)) = candidate else {
383388
return Ok(None);
@@ -470,7 +475,12 @@ impl Store {
470475
Ok(())
471476
}
472477
pub fn claim_outgoing(&mut self, c: &Config) -> Result<Option<Outgoing>> {
473-
let tx = self.db.transaction()?;
478+
// Reserve the write before reading: permission/status connections can
479+
// write concurrently, and a deferred read-to-write upgrade can fail
480+
// immediately with SQLITE_BUSY instead of honoring the busy timeout.
481+
let tx = self
482+
.db
483+
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
474484
let candidate:Option<(Outgoing,String,i64,i64,i64)>=tx.query_row("SELECT o.id,o.channel,o.thread,o.text,j.trigger,j.version,c.human_version,o.media,(SELECT COUNT(*) FROM outbox_parts p WHERE p.outbox_id=o.id AND p.status='sent') FROM outbox o JOIN jobs j ON o.job_id=j.id JOIN contexts c ON c.id=o.context WHERE o.status='ready' AND o.available<=?1 ORDER BY o.created LIMIT 1",[now_ms()],|r| {
475485
let raw:String=r.get(7)?;
476486
let media=serde_json::from_str(&raw).map_err(|e|rusqlite::Error::FromSqlConversionFailure(7,rusqlite::types::Type::Text,Box::new(e)))?;

‎tests/response_delivery.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,43 @@ fn bot_continuation_is_owner_opt_in_and_keeps_hourly_limits() {
297297
s.db.execute("UPDATE outbox SET available=0", []).unwrap();
298298
assert!(s.claim_outgoing(&c).unwrap().is_some());
299299
}
300+
301+
#[test]
302+
fn reservations_wait_for_other_connections_instead_of_aborting_the_connector() {
303+
for reserve_delivery in [false, true] {
304+
let dir = tempfile::tempdir().unwrap();
305+
let mut s = Store::open(dir.path()).unwrap();
306+
let a = attention();
307+
s.ingest("work", &message("1.000001"), true).unwrap();
308+
if reserve_delivery {
309+
let job = s.claim(&a).unwrap().unwrap();
310+
s.finish(&job, speak(), &a).unwrap();
311+
}
312+
// Permissions and status use their own connections. Hold a write open
313+
// while the worker reserves a job or reply against the same database.
314+
let mut other = rusqlite::Connection::open(dir.path().join("state.sqlite3")).unwrap();
315+
let tx = other
316+
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
317+
.unwrap();
318+
tx.execute(
319+
"INSERT INTO settings VALUES('concurrent-check','active')",
320+
[],
321+
)
322+
.unwrap();
323+
let (ready, started) = std::sync::mpsc::channel();
324+
let worker = std::thread::spawn(move || {
325+
ready.send(()).unwrap();
326+
if reserve_delivery {
327+
s.claim_outgoing(&config()).map(|out| out.map(|v| v.id))
328+
} else {
329+
s.claim(&a).map(|job| job.map(|v| v.id))
330+
}
331+
});
332+
started.recv().unwrap();
333+
std::thread::sleep(std::time::Duration::from_millis(200));
334+
tx.commit().unwrap();
335+
let result = worker.join().unwrap();
336+
assert!(result.is_ok(), "reservation {reserve_delivery}: {result:?}");
337+
assert!(result.unwrap().is_some());
338+
}
339+
}

0 commit comments

Comments
 (0)