@@ -32,12 +32,12 @@ use crate::utils::{
3232pub struct BotState {
3333 pub config : Config ,
3434 pub database : Database ,
35- pub music_api : MusicApi ,
35+ pub music_api : Arc < MusicApi > ,
3636 inflight_downloads : Arc < InflightDownloads > ,
3737 pub download_semaphore : Arc < tokio:: sync:: Semaphore > ,
3838 pub upload_semaphore : Arc < tokio:: sync:: Semaphore > ,
3939 pub message_task_semaphore : Arc < tokio:: sync:: Semaphore > ,
40- pub maintenance_tx : tokio:: sync:: mpsc:: UnboundedSender < MaintenanceSignal > ,
40+ pub maintenance_tx : tokio:: sync:: mpsc:: Sender < MaintenanceSignal > ,
4141 pub bot_username : String ,
4242 pub upload_client_state : Arc < Mutex < UploadClientState > > ,
4343 pub maintenance_counters : MaintenanceCounters ,
@@ -63,6 +63,8 @@ pub struct UploadCounters {
6363const SPEED_SAMPLE_WINDOW : usize = 20 ;
6464const STATUS_RESOURCE_REFRESH_INTERVAL : std:: time:: Duration = std:: time:: Duration :: from_secs ( 2 ) ;
6565const MIN_DOWNLOAD_CHUNK_BYTES : usize = 64 * 1024 ;
66+ const MAINTENANCE_QUEUE_CAPACITY : usize = 32 ;
67+ const CACHE_PRUNE_INTERVAL_REQUESTS : u32 = 50 ;
6668
6769#[ derive( Debug ) ]
6870pub struct RuntimeMetrics {
@@ -129,12 +131,14 @@ static STATUS_RESOURCE_CACHE: LazyLock<std::sync::Mutex<(System, Instant, Resour
129131pub struct MaintenanceCounters {
130132 pub memory_release_requests : AtomicU32 ,
131133 pub db_analyze_requests : AtomicU32 ,
134+ pub api_cache_prune_requests : AtomicU32 ,
132135}
133136
134137#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
135138pub enum MaintenanceSignal {
136139 AnalyzeDb ,
137140 ReleaseMemory ,
141+ PruneApiCache ,
138142}
139143
140144#[ derive( Debug , Default ) ]
@@ -248,6 +252,7 @@ impl MaintenanceCounters {
248252 Self {
249253 memory_release_requests : AtomicU32 :: new ( 0 ) ,
250254 db_analyze_requests : AtomicU32 :: new ( 0 ) ,
255+ api_cache_prune_requests : AtomicU32 :: new ( 0 ) ,
251256 }
252257 }
253258
@@ -516,16 +521,17 @@ pub async fn run(config: Config) -> Result<()> {
516521 let database = Database :: new ( & config. database ) . await ?;
517522 tracing:: info!( "Database initialized" ) ;
518523
519- let ( maintenance_tx, maintenance_rx) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
524+ // Initialize music API
525+ let music_api = Arc :: new ( MusicApi :: new_with_config ( & config) ) ;
526+ tracing:: info!( "Music API initialized" ) ;
527+
528+ let ( maintenance_tx, maintenance_rx) = tokio:: sync:: mpsc:: channel ( MAINTENANCE_QUEUE_CAPACITY ) ;
520529 let maintenance_database = database. clone ( ) ;
530+ let maintenance_music_api = Arc :: clone ( & music_api) ;
521531 tokio:: spawn ( async move {
522- maintenance_worker ( maintenance_rx, maintenance_database) . await ;
532+ maintenance_worker ( maintenance_rx, maintenance_database, maintenance_music_api ) . await ;
523533 } ) ;
524534
525- // Initialize music API
526- let music_api = MusicApi :: new_with_config ( & config) ;
527- tracing:: info!( "Music API initialized" ) ;
528-
529535 // Initialize bot with custom API URL support
530536 let bot = if !config. bot_api . is_empty ( ) && config. bot_api != "https://api.telegram.org" {
531537 // 使用自定义API URL
@@ -625,7 +631,7 @@ pub async fn run(config: Config) -> Result<()> {
625631 let bot_state = Arc :: new ( BotState {
626632 config,
627633 database,
628- music_api,
634+ music_api : Arc :: clone ( & music_api ) ,
629635 inflight_downloads : Arc :: new ( InflightDownloads :: default ( ) ) ,
630636 download_semaphore : Arc :: new ( tokio:: sync:: Semaphore :: new (
631637 max_concurrent_downloads as usize ,
@@ -1525,8 +1531,14 @@ async fn download_and_send_music(
15251531 } else {
15261532 state. database . save_song_info ( & song_info) . await ?;
15271533 for signal in collect_maintenance_signals ( & state. maintenance_counters , & state. config ) {
1528- if state. maintenance_tx . send ( signal) . is_err ( ) {
1529- tracing:: warn!( "Maintenance worker unavailable; skipping signal" ) ;
1534+ match state. maintenance_tx . try_send ( signal) {
1535+ Ok ( ( ) ) => { }
1536+ Err ( tokio:: sync:: mpsc:: error:: TrySendError :: Full ( _) ) => {
1537+ tracing:: debug!( "Maintenance queue full; dropping signal {:?}" , signal) ;
1538+ }
1539+ Err ( tokio:: sync:: mpsc:: error:: TrySendError :: Closed ( _) ) => {
1540+ tracing:: warn!( "Maintenance worker unavailable; skipping signal" ) ;
1541+ }
15301542 }
15311543 }
15321544 }
@@ -1747,7 +1759,7 @@ fn collect_maintenance_signals(
17471759 counters : & MaintenanceCounters ,
17481760 config : & Config ,
17491761) -> Vec < MaintenanceSignal > {
1750- let mut signals = Vec :: with_capacity ( 2 ) ;
1762+ let mut signals = Vec :: with_capacity ( 3 ) ;
17511763 for ( counter, interval, signal) in [
17521764 (
17531765 & counters. db_analyze_requests ,
@@ -1759,6 +1771,11 @@ fn collect_maintenance_signals(
17591771 config. memory_release_interval_requests ,
17601772 MaintenanceSignal :: ReleaseMemory ,
17611773 ) ,
1774+ (
1775+ & counters. api_cache_prune_requests ,
1776+ CACHE_PRUNE_INTERVAL_REQUESTS ,
1777+ MaintenanceSignal :: PruneApiCache ,
1778+ ) ,
17621779 ] {
17631780 if MaintenanceCounters :: should_run ( counter, interval) {
17641781 signals. push ( signal) ;
@@ -1793,8 +1810,9 @@ async fn acquire_download_leader(
17931810}
17941811
17951812async fn maintenance_worker (
1796- mut rx : tokio:: sync:: mpsc:: UnboundedReceiver < MaintenanceSignal > ,
1813+ mut rx : tokio:: sync:: mpsc:: Receiver < MaintenanceSignal > ,
17971814 database : Database ,
1815+ music_api : Arc < MusicApi > ,
17981816) {
17991817 while let Some ( signal) = rx. recv ( ) . await {
18001818 match signal {
@@ -1813,6 +1831,17 @@ async fn maintenance_worker(
18131831 tracing:: warn!( "Memory release background task failed: {}" , e) ;
18141832 }
18151833 }
1834+ MaintenanceSignal :: PruneApiCache => {
1835+ let stats = music_api. prune_expired_cache_entries ( ) ;
1836+ if stats. total_removed ( ) > 0 {
1837+ tracing:: debug!(
1838+ "Pruned API cache entries: detail={}, url={}, lyric={}" ,
1839+ stats. song_detail_removed,
1840+ stats. song_url_removed,
1841+ stats. song_lyric_removed
1842+ ) ;
1843+ }
1844+ }
18161845 }
18171846 }
18181847}
@@ -2709,6 +2738,21 @@ mod tests {
27092738 assert_eq ! ( third, vec![ super :: MaintenanceSignal :: ReleaseMemory ] ) ;
27102739 }
27112740
2741+ #[ test]
2742+ fn maintenance_scheduler_emits_cache_prune_signal_on_interval ( ) {
2743+ let counters = super :: MaintenanceCounters :: new ( ) ;
2744+ let mut config = crate :: config:: Config :: default ( ) ;
2745+ config. db_analyze_interval_requests = 0 ;
2746+ config. memory_release_interval_requests = 0 ;
2747+
2748+ for _ in 0 ..super :: CACHE_PRUNE_INTERVAL_REQUESTS . saturating_sub ( 1 ) {
2749+ let _ = super :: collect_maintenance_signals ( & counters, & config) ;
2750+ }
2751+
2752+ let signals = super :: collect_maintenance_signals ( & counters, & config) ;
2753+ assert_eq ! ( signals, vec![ super :: MaintenanceSignal :: PruneApiCache ] ) ;
2754+ }
2755+
27122756 #[ test]
27132757 fn upload_pool_idle_timeout_disabled_when_zero ( ) {
27142758 assert ! ( !super :: should_set_upload_pool_idle_timeout( 0 ) ) ;
0 commit comments