From 63fc2fb2c0f3f82d55722e94cd92f2c8bdeb9b68 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 16 Jul 2026 18:40:30 +0000 Subject: [PATCH] grpc: run authenticate's PBKDF2 verify off the engine lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Authenticate RPC held the engine Mutex across scram verify_plain, which at production iteration counts (1.2M) is ~0.8s of PBKDF2 on the async reactor — freezing the whole daemon (IRC link, gossip, every RPC) for the duration and serializing all logins. Fetch the verifier under the lock (cheap), then run verify_plain on spawn_blocking, lock released. 6 concurrent logins: 823ms (was ~4460ms serialized). set_password/register already offload this way; drop the now-unused authority_authenticate. --- src/engine/register.rs | 9 +++++++-- src/grpc.rs | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/engine/register.rs b/src/engine/register.rs index 390f113..0802b9c 100644 --- a/src/engine/register.rs +++ b/src/engine/register.rs @@ -47,8 +47,13 @@ impl Engine { status } - pub fn authority_authenticate(&self, name: &str, password: &str) -> Option { - self.db.authenticate(name, password).map(str::to_string) + /// The account's canonical name and its SHA-256 verifier (owned), so a caller + /// can run the PBKDF2 verify OFF the engine lock. At production iteration + /// counts that derivation is ~1s of CPU — never run it while holding the lock + /// (it would freeze the whole daemon); fetch here, then `scram::verify_plain` + /// on a blocking thread. + pub fn scram_verifier(&self, name: &str) -> Option<(String, String)> { + self.db.scram_lookup(name, "SCRAM-SHA-256").map(|(a, v)| (a.to_string(), v.to_string())) } pub fn authority_set_password(&mut self, account: &str, creds: Option) -> AuthorityStatus { diff --git a/src/grpc.rs b/src/grpc.rs index 6323e65..084c2ba 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -291,9 +291,22 @@ impl Accounts for AccountsService { async fn authenticate(&self, req: Request) -> Result, Status> { authorize(&req, &self.token)?; let msg = req.into_inner(); - match self.engine.lock().await.authority_authenticate(&msg.name, &msg.password) { - Some(account) => Ok(Response::new(AuthenticateReply { status: PbStatus::Ok as i32, account })), - None => Ok(Response::new(AuthenticateReply { status: PbStatus::Invalid as i32, account: String::new() })), + // Fetch the verifier under the lock (cheap), then run the ~1s PBKDF2 on a + // blocking thread — never hold the engine lock across it, or every login + // freezes the whole daemon (IRC link, gossip, all RPCs). + let Some((account, verifier)) = self.engine.lock().await.scram_verifier(&msg.name) else { + return Ok(Response::new(AuthenticateReply { status: PbStatus::Invalid as i32, account: String::new() })); + }; + let password = msg.password; + let ok = tokio::task::spawn_blocking(move || { + crate::engine::scram::verify_plain(crate::engine::scram::Hash::Sha256, &verifier, &password) + }) + .await + .unwrap_or(false); + if ok { + Ok(Response::new(AuthenticateReply { status: PbStatus::Ok as i32, account })) + } else { + Ok(Response::new(AuthenticateReply { status: PbStatus::Invalid as i32, account: String::new() })) } }