@@ -176,10 +176,77 @@ impl AuditFilter {
176176 }
177177}
178178
179+ /// How long the ChronDB connection can be idle before the GC closes it.
180+ const AUDIT_DB_IDLE_TIMEOUT : std:: time:: Duration = std:: time:: Duration :: from_secs ( 120 ) ;
181+
182+ /// How often the GC thread checks for idle connections.
183+ const AUDIT_GC_INTERVAL : std:: time:: Duration = std:: time:: Duration :: from_secs ( 30 ) ;
184+
185+ /// Manages the ChronDB connection lifecycle: open on first use, close when idle.
186+ ///
187+ /// ChronDB loads a GraalVM native-image shared library whose internal threads
188+ /// consume CPU even when no operations are in flight. This struct ensures the
189+ /// isolate only exists while the database is actively being used.
190+ ///
191+ /// A background GC thread monitors `last_used` and drops the connection after
192+ /// [`AUDIT_DB_IDLE_TIMEOUT`] of inactivity, tearing down the GraalVM isolate.
193+ /// The next operation transparently reopens it (like Go's `defer` on each use
194+ /// cycle).
195+ pub ( crate ) struct DbPool {
196+ data_path : String ,
197+ index_path : String ,
198+ inner : std:: sync:: Mutex < DbPoolInner > ,
199+ }
200+
201+ struct DbPoolInner {
202+ db : Option < Arc < ChronDB > > ,
203+ last_used : std:: time:: Instant ,
204+ }
205+
206+ impl DbPool {
207+ fn new ( data_path : String , index_path : String ) -> Self {
208+ Self {
209+ data_path,
210+ index_path,
211+ inner : std:: sync:: Mutex :: new ( DbPoolInner {
212+ db : None ,
213+ last_used : std:: time:: Instant :: now ( ) ,
214+ } ) ,
215+ }
216+ }
217+
218+ /// Acquire a handle to ChronDB — opens if not connected.
219+ fn acquire ( & self ) -> Result < Arc < ChronDB > > {
220+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
221+ inner. last_used = std:: time:: Instant :: now ( ) ;
222+ if let Some ( ref db) = inner. db {
223+ return Ok ( db. clone ( ) ) ;
224+ }
225+ let db = ChronDB :: open ( & self . data_path , & self . index_path )
226+ . map_err ( |e| anyhow:: anyhow!( "failed to open audit db: {e:?}" ) ) ?;
227+ let db = Arc :: new ( db) ;
228+ inner. db = Some ( db. clone ( ) ) ;
229+ eprintln ! ( "[audit] database opened" ) ;
230+ Ok ( db)
231+ }
232+
233+ /// GC tick: close if idle longer than `max_idle`.
234+ /// Returns true if the connection was closed.
235+ fn gc ( & self , max_idle : std:: time:: Duration ) -> bool {
236+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
237+ if inner. db . is_some ( ) && inner. last_used . elapsed ( ) >= max_idle {
238+ inner. db = None ; // Drop → SharedWorker::drop → graal_tear_down_isolate
239+ eprintln ! ( "[audit] database closed (idle {:?})" , max_idle) ;
240+ return true ;
241+ }
242+ false
243+ }
244+ }
245+
179246pub enum AuditLogger {
180247 Active {
181248 sender : tokio:: sync:: mpsc:: UnboundedSender < AuditEntry > ,
182- db : Arc < ChronDB > ,
249+ pool : Arc < DbPool > ,
183250 } ,
184251 Disabled ,
185252}
@@ -199,12 +266,11 @@ impl AuditLogger {
199266 std:: fs:: create_dir_all ( & index_path)
200267 . with_context ( || format ! ( "failed to create audit index dir: {index_path}" ) ) ?;
201268
202- let db = ChronDB :: open ( & data_path, & index_path)
203- . map_err ( |e| anyhow:: anyhow!( "failed to open audit db: {e:?}" ) ) ?;
204- let db = Arc :: new ( db) ;
269+ let pool = Arc :: new ( DbPool :: new ( data_path, index_path) ) ;
205270
271+ // Writer thread: receives entries via channel, writes to ChronDB on demand.
206272 let ( tx, mut rx) = tokio:: sync:: mpsc:: unbounded_channel :: < AuditEntry > ( ) ;
207- let db_clone = db . clone ( ) ;
273+ let writer_pool = pool . clone ( ) ;
208274 tokio:: task:: spawn_blocking ( move || {
209275 while let Some ( entry) = rx. blocking_recv ( ) {
210276 let key = format ! (
@@ -213,12 +279,24 @@ impl AuditLogger {
213279 uuid:: Uuid :: new_v4( )
214280 ) ;
215281 if let Ok ( doc) = serde_json:: to_value ( & entry) {
216- let _ = db_clone. put ( & key, & doc, None ) ;
282+ if let Ok ( db) = writer_pool. acquire ( ) {
283+ let _ = db. put ( & key, & doc, None ) ;
284+ }
217285 }
218286 }
219287 } ) ;
220288
221- Ok ( AuditLogger :: Active { sender : tx, db } )
289+ // GC thread: monitors idle time, closes ChronDB when not in use.
290+ let gc_pool = pool. clone ( ) ;
291+ std:: thread:: Builder :: new ( )
292+ . name ( "audit-gc" . to_string ( ) )
293+ . spawn ( move || loop {
294+ std:: thread:: sleep ( AUDIT_GC_INTERVAL ) ;
295+ gc_pool. gc ( AUDIT_DB_IDLE_TIMEOUT ) ;
296+ } )
297+ . ok ( ) ;
298+
299+ Ok ( AuditLogger :: Active { sender : tx, pool } )
222300 }
223301
224302 pub fn log ( & self , entry : AuditEntry ) {
@@ -230,7 +308,8 @@ impl AuditLogger {
230308 pub fn query_recent ( & self , limit : usize ) -> Result < Vec < AuditEntry > > {
231309 match self {
232310 AuditLogger :: Disabled => Ok ( vec ! [ ] ) ,
233- AuditLogger :: Active { db, .. } => {
311+ AuditLogger :: Active { pool, .. } => {
312+ let db = pool. acquire ( ) ?;
234313 let raw = db
235314 . list_by_prefix ( "audit:" , None )
236315 . map_err ( |e| anyhow:: anyhow!( "failed to query audit logs: {e:?}" ) ) ?;
@@ -242,7 +321,8 @@ impl AuditLogger {
242321 pub fn query_filtered ( & self , filter : & AuditFilter ) -> Result < Vec < AuditEntry > > {
243322 match self {
244323 AuditLogger :: Disabled => Ok ( vec ! [ ] ) ,
245- AuditLogger :: Active { db, .. } => {
324+ AuditLogger :: Active { pool, .. } => {
325+ let db = pool. acquire ( ) ?;
246326 let raw = db
247327 . list_by_prefix ( "audit:" , None )
248328 . map_err ( |e| anyhow:: anyhow!( "failed to query audit logs: {e:?}" ) ) ?;
0 commit comments