SQL pools time out; task locks are a renewed five-minute lease
A 3-node rehearsal (PostgreSQL + NATS + Garage) found two ways a crash leaves work stuck: Pool hangs. The PostgreSQL pool (deadpool) was built with no timeouts, so a request waited for a free connection, and for one to be opened or recycled, for as long as it took: forever when the server stopped answering. MySQL's pool (mysql_async) has no wait timeout at all. - PostgreSQL: wait 30 s (or the store's timeout if longer), create the store's timeout or 15 s (it bounds the whole handshake, where tokio-postgres's connect_timeout covers only the TCP connect), recycle 10 s. The pool config is now always set, not only with poolMaxConnections. - MySQL: every connection is taken through MysqlStore::conn(), which gives up after 30 s. - Both: TCP keepalive after 60 s idle, so a server that vanished without closing the connection is noticed in minutes rather than the two-hour system default. The DataStore schema has no pool timeout settings, so these are fixed defaults; the store's own timeout bounds connecting on PostgreSQL. Task locks. A task lock lasted an hour, so after a hard crash the dead node's tasks waited up to an hour and five minutes. The lock is now a five-minute lease: while this node runs a task, the task manager renews its lock every third of the lifetime (InMemoryStore::renew_lock, a compare-and-set on the store backends and SET XX EX on Redis, which leaves a lock that already expired alone). A killed node's tasks run elsewhere within about five minutes plus the claim recheck. A task this node holds isn't handed to a worker again by the scan. store::pool_timeout (new): a local listener that accepts connections and never answers plays a hung server; a PostgreSQL store with a 2 s timeout returns an error in about 4 s, and a MySQL store in 30 s. Without the timeouts both wait for good. store::task_locks gains a task held for 1.5 lock lifetimes: its lease is still held, and released when the task ends.
This commit is contained in:
@@ -79,7 +79,33 @@ pub async fn task_lock_tests() {
|
||||
"ran before the other node's locks expired: {elapsed:?}"
|
||||
);
|
||||
|
||||
// 3. A graceful stop releases the locks this node holds: another node
|
||||
// 3. A task that runs longer than a lock lifetime keeps its claim: the
|
||||
// task manager renews the lease while this node holds it, and the claim
|
||||
// ends when the task does. (Before, a lock simply lasted an hour.)
|
||||
let [id] = new_task_ids(1)[..] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(server.try_lock_task(id).await, "claim {id}");
|
||||
tokio::time::sleep(Duration::from_secs(LOCK_EXPIRY + LOCK_EXPIRY / 2)).await;
|
||||
assert!(
|
||||
!foreign_lock(&server, id, LOCK_EXPIRY).await,
|
||||
"lease lapsed while the task ran"
|
||||
);
|
||||
server.remove_index_lock(id).await;
|
||||
assert!(
|
||||
foreign_lock(&server, id, LOCK_EXPIRY).await,
|
||||
"released when the task ended"
|
||||
);
|
||||
let _ = server
|
||||
.in_memory_store()
|
||||
.remove_lock(KV_LOCK_TASK, &id.to_be_bytes())
|
||||
.await;
|
||||
assert!(
|
||||
common::ipc::TaskLocks::DEFAULT_EXPIRY <= 5 * 60,
|
||||
"a dead node's tasks wait no more than a few minutes"
|
||||
);
|
||||
|
||||
// 4. A graceful stop releases the locks this node holds: another node
|
||||
// can claim those tasks at once, and this one claims nothing more
|
||||
let ids = new_task_ids(3);
|
||||
for id in &ids {
|
||||
|
||||
Reference in New Issue
Block a user