From 0e96d56fa72c066f1d7b94cba05981d131adfdb1 Mon Sep 17 00:00:00 2001 From: "i.novik" Date: Sat, 18 Jul 2026 15:32:11 +0200 Subject: [PATCH] refactor: hoist regexes into LazyLock statics Compiles each regex exactly once instead of on every `MentionParameters::default()` / handler invocation, and isolates the remaining `expect`s to single constant-initializer sites (they can only fire on a developer typo, never at runtime), which sets up the no-panic lint in a later iteration. - boot.rs: the five MentionParameters regexes now clone from `LazyLock` statics. - chat_gpt_handler.rs: `BOT_PROFILES` and `CHAT_SUMMARY_REQUEST_REGEX` move from `OnceLock` (+ `get_or_init`) to `LazyLock`. Also fixes a latent bug: `handle_reply` read `BOT_PROFILES.get()` and silently did nothing if a reply arrived before the first `handle_chat_gpt_question` had initialized the profiles. With `LazyLock` the profiles are always populated, so the guard is gone. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/boot.rs | 26 +++++++-- src/chat_gpt_handler.rs | 125 +++++++++++++++++++++------------------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/src/boot.rs b/src/boot.rs index 101996b..5cc48e4 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use chrono::Duration; use redis::aio::ConnectionManager; @@ -27,6 +27,20 @@ const MIN_TIME_DIFF: i64 = 15; pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1/chat/completions"; +// Compile each mention regex exactly once. The patterns are compile-time +// constants, so `expect` here can only fire on a developer typo — never at +// runtime — which is why it is an accepted panic site. +static RUST_RE: LazyLock = + LazyLock::new(|| Regex::new(RUST_REGEX).expect("RUST_REGEX must compile")); +static BLAZING_FAST_RE: LazyLock = + LazyLock::new(|| Regex::new(BLAZING_FAST_REGEX).expect("BLAZING_FAST_REGEX must compile")); +static GAYNESS_RE: LazyLock = + LazyLock::new(|| Regex::new(GAYNESS_REGEX).expect("GAYNESS_REGEX must compile")); +static CHAT_GPT_RE: LazyLock = + LazyLock::new(|| Regex::new(CHAT_GPT_REGEX).expect("CHAT_GPT_REGEX must compile")); +static URL_RE: LazyLock = + LazyLock::new(|| Regex::new(URL_REGEX).expect("URL_REGEX must compile")); + #[derive(Clone)] pub struct GptParameters { pub chat_gpt_api_token: Arc, @@ -48,11 +62,11 @@ pub struct MentionParameters { impl Default for MentionParameters { fn default() -> Self { Self { - rust_regex: Regex::new(RUST_REGEX).expect("Can't compile regex"), - blazing_fast_regex: Regex::new(BLAZING_FAST_REGEX).expect("Can't compile regex"), - gayness_regex: Regex::new(GAYNESS_REGEX).expect("Can't compile regex"), - chat_gpt_regex: Regex::new(CHAT_GPT_REGEX).expect("Can't compile regex"), - url_regex: Regex::new(URL_REGEX).expect("Can't compile regex"), + rust_regex: RUST_RE.clone(), + blazing_fast_regex: BLAZING_FAST_RE.clone(), + gayness_regex: GAYNESS_RE.clone(), + chat_gpt_regex: CHAT_GPT_RE.clone(), + url_regex: URL_RE.clone(), req_time_diff: Duration::minutes(MIN_TIME_DIFF), } } diff --git a/src/chat_gpt_handler.rs b/src/chat_gpt_handler.rs index ec23c35..feb9f70 100644 --- a/src/chat_gpt_handler.rs +++ b/src/chat_gpt_handler.rs @@ -1,5 +1,5 @@ use std::fmt::Debug; -use std::sync::OnceLock; +use std::sync::LazyLock; use crate::chat_gpt_handler::BotProfile::{Fedor, Felix, Ferris}; use crate::chat_gpt_handler::ChatMessageRole::{System, User}; @@ -34,35 +34,43 @@ const FERRIS_CHAT_GPT_SYSTEM_CONTEXT: &str = "Ты чат-бот Rust комью Твоя задача вызвать у собеседника интерес к языку Rust. \ Ты любишь рассказывать забавные факты о языке Rust."; -static BOT_PROFILES: OnceLock>> = OnceLock::new(); +// Bot profiles and the summary-request regex are compile-time constants, so +// these initializers can only panic on a developer typo, never at runtime. +// `LazyLock` (vs the previous `OnceLock`) also guarantees the profiles are +// always populated — `handle_reply` below used to silently no-op if a reply +// arrived before the first `handle_chat_gpt_question` had initialized them. +static BOT_PROFILES: LazyLock>> = LazyLock::new(|| { + vec![ + BotConfiguration { + profile: Fedor, + mention_regex: Regex::new(r"(?i)(fedor|ф[её]дор|федя)") + .expect("Fedor mention regex must compile"), + gpt_system_context: FEDOR_CHAT_GPT_SYSTEM_CONTEXT, + }, + BotConfiguration { + profile: Felix, + mention_regex: Regex::new(r"(?i)(felix|феликс)") + .expect("Felix mention regex must compile"), + gpt_system_context: FELIX_CHAT_GPT_SYSTEM_CONTEXT, + }, + BotConfiguration { + profile: Ferris, + mention_regex: Regex::new(r"(?i)(feris|ferris|ферис|феррис)") + .expect("Ferris mention regex must compile"), + gpt_system_context: FERRIS_CHAT_GPT_SYSTEM_CONTEXT, + }, + ] +}); const SUMMARY_REQUEST_REGEX: &str = r"(?i)([чш].о?\b.*\bпроисходит)"; -static CHAT_SUMMARY_REQUEST_REGEX: OnceLock = OnceLock::new(); +static CHAT_SUMMARY_REQUEST_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(SUMMARY_REQUEST_REGEX).expect("SUMMARY_REQUEST_REGEX must compile") +}); pub async fn handle_chat_gpt_question(bot: Bot, msg: Message, gpt_parameters: &GptParameters) { let chat_id = msg.chat.id; let message = msg.text().expect("can't parse incoming message"); info!("gpt invocation: chat_id: {chat_id}, message: {message}"); - let bot_profiles = BOT_PROFILES.get_or_init(|| { - vec![ - BotConfiguration { - profile: Fedor, - mention_regex: Regex::new(r"(?i)(fedor|ф[её]дор|федя)") - .expect("Can't compile regex"), - gpt_system_context: FEDOR_CHAT_GPT_SYSTEM_CONTEXT, - }, - BotConfiguration { - profile: Felix, - mention_regex: Regex::new(r"(?i)(felix|феликс)").expect("Can't compile regex"), - gpt_system_context: FELIX_CHAT_GPT_SYSTEM_CONTEXT, - }, - BotConfiguration { - profile: Ferris, - mention_regex: Regex::new(r"(?i)(feris|ferris|ферис|феррис)") - .expect("Can't compile regex"), - gpt_system_context: FERRIS_CHAT_GPT_SYSTEM_CONTEXT, - }, - ] - }); + let bot_profiles = &*BOT_PROFILES; let bot_configuration = bot_profiles .iter() .find(|&x| x.is_correct_config(message)) @@ -72,8 +80,7 @@ pub async fn handle_chat_gpt_question(bot: Bot, msg: Message, gpt_parameters: &G role: User, content: message.to_string(), }; - let summary_request_regex = CHAT_SUMMARY_REQUEST_REGEX - .get_or_init(|| Regex::new(SUMMARY_REQUEST_REGEX).expect("Can't compile regex")); + let summary_request_regex = &*CHAT_SUMMARY_REQUEST_REGEX; let mut redis_cm = gpt_parameters.redis_connection_manager.clone(); let context = match summary_request_regex.is_match(message) { true => { @@ -176,42 +183,40 @@ pub async fn handle_reply( "truing to reply chat_id:{chat_id:#?}, msg_id: {:?}, thread_id: {:#?}", msg.id, msg.thread_id ); - if let Some(bot_profiles) = BOT_PROFILES.get() { - let bot_configuration = bot_profiles - .iter() - .find(|&x| x.profile == reply_msg_bot_profile) - .unwrap_or(&bot_profiles[0]); - let bot_context_key = - &format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0); - let user_message = ChatMessage { - role: User, - content: message.to_string(), - }; - let context = fetch_bot_context( - &mut redis_cm, - bot_context_key, - &user_message, - bot_configuration.gpt_system_context, - ) - .await; + let bot_profiles = &*BOT_PROFILES; + let bot_configuration = bot_profiles + .iter() + .find(|&x| x.profile == reply_msg_bot_profile) + .unwrap_or(&bot_profiles[0]); + let bot_context_key = &format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0); + let user_message = ChatMessage { + role: User, + content: message.to_string(), + }; + let context = fetch_bot_context( + &mut redis_cm, + bot_context_key, + &user_message, + bot_configuration.gpt_system_context, + ) + .await; - let gpt_response_message = - gpt_service::chat_gpt_call(gpt_parameters, chat_id, context).await; - let bot_reply_msg_response = bot - .send_message(chat_id, &gpt_response_message.content) - .reply_parameters(ReplyParameters::new(msg.id)) - .await; - - update_bot_context_and_identifiers( - &mut redis_cm, - bot_configuration.profile, - bot_context_key, - &user_message, - &gpt_response_message, - bot_reply_msg_response, - ) + let gpt_response_message = + gpt_service::chat_gpt_call(gpt_parameters, chat_id, context).await; + let bot_reply_msg_response = bot + .send_message(chat_id, &gpt_response_message.content) + .reply_parameters(ReplyParameters::new(msg.id)) .await; - } + + update_bot_context_and_identifiers( + &mut redis_cm, + bot_configuration.profile, + bot_context_key, + &user_message, + &gpt_response_message, + bot_reply_msg_response, + ) + .await; } }