@@ -52,6 +52,8 @@ use ladybug::core::simd::{self, hamming_distance};
5252use ladybug:: nars:: TruthValue ;
5353use ladybug:: storage:: service:: { CognitiveService , CpuFeatures , ServiceConfig } ;
5454use ladybug:: storage:: { Addr , BindSpace , CogRedis , FINGERPRINT_WORDS , RedisResult } ;
55+ #[ cfg( feature = "lancedb" ) ]
56+ use ladybug:: storage:: LancePersistence ;
5557use ladybug:: { FINGERPRINT_BITS , FINGERPRINT_BYTES , VERSION } ;
5658
5759// =============================================================================
@@ -482,6 +484,9 @@ struct DbState {
482484 cpu : CpuFeatures ,
483485 /// Start time
484486 start_time : Instant ,
487+ /// Lance persistence layer
488+ #[ cfg( feature = "lancedb" ) ]
489+ persistence : Arc < LancePersistence > ,
485490}
486491
487492impl DbState {
@@ -492,14 +497,71 @@ impl DbState {
492497 } ;
493498
494499 let service = CognitiveService :: new ( svc_config) . expect ( "Failed to create CognitiveService" ) ;
500+ let mut cog_redis = CogRedis :: new ( ) ;
501+ let mut fingerprints = Vec :: new ( ) ;
502+
503+ #[ cfg( feature = "lancedb" ) ]
504+ let persistence = {
505+ let lance_dir = format ! ( "{}/lance" , config. data_dir) ;
506+ let persistence = Arc :: new ( LancePersistence :: new ( & lance_dir) ) ;
507+ let rt = tokio:: runtime:: Runtime :: new ( ) . expect ( "Failed to create tokio runtime" ) ;
508+
509+ if persistence. has_data ( ) {
510+ eprintln ! ( "[server] Lance data found, hydrating..." ) ;
511+
512+ if let Err ( e) = rt. block_on ( persistence. ensure_tables ( ) ) {
513+ eprintln ! ( "[server] WARNING: ensure_tables failed: {}" , e) ;
514+ }
515+
516+ match rt. block_on ( persistence. hydrate ( ) ) {
517+ Ok ( Some ( space) ) => {
518+ let node_count = space. nodes_iter ( )
519+ . filter ( |( a, _) | a. prefix ( ) > 0x0F )
520+ . count ( ) ;
521+ let edge_count = space. edges_iter ( ) . count ( ) ;
522+ cog_redis. replace_bind_space ( space) ;
523+ eprintln ! (
524+ "[server] Hydrated BindSpace: {} nodes, {} edges" ,
525+ node_count, edge_count
526+ ) ;
527+ }
528+ Ok ( None ) => {
529+ eprintln ! ( "[server] No BindSpace data to hydrate" ) ;
530+ }
531+ Err ( e) => {
532+ eprintln ! ( "[server] WARNING: BindSpace hydration failed: {}" , e) ;
533+ }
534+ }
535+
536+ match rt. block_on ( persistence. hydrate_index ( ) ) {
537+ Ok ( fps) if !fps. is_empty ( ) => {
538+ eprintln ! ( "[server] Hydrated {} index fingerprints" , fps. len( ) ) ;
539+ fingerprints = fps;
540+ }
541+ Ok ( _) => { }
542+ Err ( e) => {
543+ eprintln ! ( "[server] WARNING: Index hydration failed: {}" , e) ;
544+ }
545+ }
546+ } else {
547+ eprintln ! ( "[server] No Lance data found, starting fresh" ) ;
548+ if let Err ( e) = rt. block_on ( persistence. ensure_tables ( ) ) {
549+ eprintln ! ( "[server] WARNING: ensure_tables failed: {}" , e) ;
550+ }
551+ }
552+
553+ persistence
554+ } ;
495555
496556 Self {
497- fingerprints : Vec :: new ( ) ,
557+ fingerprints,
498558 kv : HashMap :: new ( ) ,
499- cog_redis : CogRedis :: new ( ) ,
559+ cog_redis,
500560 service,
501561 cpu : CpuFeatures :: detect ( ) ,
502562 start_time : Instant :: now ( ) ,
563+ #[ cfg( feature = "lancedb" ) ]
564+ persistence,
503565 }
504566 }
505567}
@@ -1062,7 +1124,7 @@ fn handle_xor(body: &str, format: ResponseFormat) -> Vec<u8> {
10621124 // XOR: one pass over 256 u64 words — nanoseconds
10631125 let mut result = Fingerprint :: zero ( ) ;
10641126 for i in 0 ..FINGERPRINT_WORDS {
1065- result. words_mut ( ) [ i] = fp_a. words ( ) [ i] ^ fp_b. words ( ) [ i] ;
1127+ result. as_raw_mut ( ) [ i] = fp_a. as_raw ( ) [ i] ^ fp_b. as_raw ( ) [ i] ;
10661128 }
10671129
10681130 match format {
@@ -1105,7 +1167,7 @@ fn handle_xor_verify(body: &str, format: ResponseFormat) -> Vec<u8> {
11051167 // XOR: identical fingerprints produce all zeros
11061168 let mut check = Fingerprint :: zero ( ) ;
11071169 for i in 0 ..FINGERPRINT_WORDS {
1108- check. words_mut ( ) [ i] = fp_a. words ( ) [ i] ^ fp_b. words ( ) [ i] ;
1170+ check. as_raw_mut ( ) [ i] = fp_a. as_raw ( ) [ i] ^ fp_b. as_raw ( ) [ i] ;
11091171 }
11101172 let residual = check. popcount ( ) ;
11111173 let valid = residual == 0 ;
@@ -3555,6 +3617,79 @@ fn main() {
35553617
35563618 let state: SharedState = Arc :: new ( RwLock :: new ( DbState :: new ( & config) ) ) ;
35573619
3620+ // Spawn background persistence thread (flush every 30 seconds)
3621+ #[ cfg( feature = "lancedb" ) ]
3622+ {
3623+ let persist_state = Arc :: clone ( & state) ;
3624+ std:: thread:: spawn ( move || {
3625+ let rt = tokio:: runtime:: Runtime :: new ( ) . expect ( "persist runtime" ) ;
3626+ loop {
3627+ std:: thread:: sleep ( std:: time:: Duration :: from_secs ( 30 ) ) ;
3628+
3629+ // Check if there are dirty addresses (quick read lock)
3630+ let has_dirty = {
3631+ let db = persist_state. read ( ) . unwrap ( ) ;
3632+ db. cog_redis . bind_space ( ) . dirty_addrs ( ) . next ( ) . is_some ( )
3633+ } ;
3634+
3635+ if has_dirty {
3636+ // Persist under read lock
3637+ let persist_result = {
3638+ let db = persist_state. read ( ) . unwrap ( ) ;
3639+ let persistence = Arc :: clone ( & db. persistence ) ;
3640+ let bs = db. cog_redis . bind_space ( ) ;
3641+ rt. block_on ( persistence. persist_full ( bs) )
3642+ } ;
3643+
3644+ match persist_result {
3645+ Ok ( ( ) ) => {
3646+ if let Ok ( mut db) = persist_state. write ( ) {
3647+ db. cog_redis . bind_space_mut ( ) . clear_dirty ( ) ;
3648+ }
3649+ eprintln ! ( "[persist] BindSpace flushed to Lance" ) ;
3650+ }
3651+ Err ( e) => {
3652+ eprintln ! ( "[persist] BindSpace flush failed: {}" , e) ;
3653+ }
3654+ }
3655+ }
3656+
3657+ // Persist index fingerprints
3658+ {
3659+ let db = persist_state. read ( ) . unwrap ( ) ;
3660+ if !db. fingerprints . is_empty ( ) {
3661+ let persistence = Arc :: clone ( & db. persistence ) ;
3662+ if let Err ( e) = rt. block_on ( persistence. persist_index ( & db. fingerprints ) ) {
3663+ eprintln ! ( "[persist] Index flush failed: {}" , e) ;
3664+ }
3665+ }
3666+ }
3667+ }
3668+ } ) ;
3669+ }
3670+
3671+ // Register SIGTERM/SIGINT handler for graceful shutdown
3672+ #[ cfg( feature = "lancedb" ) ]
3673+ {
3674+ let shutdown_state = Arc :: clone ( & state) ;
3675+ ctrlc:: set_handler ( move || {
3676+ eprintln ! ( "\n [shutdown] Flushing to Lance before exit..." ) ;
3677+ let rt = tokio:: runtime:: Runtime :: new ( ) . expect ( "shutdown runtime" ) ;
3678+ let db = shutdown_state. read ( ) . unwrap ( ) ;
3679+ let persistence = Arc :: clone ( & db. persistence ) ;
3680+
3681+ if let Err ( e) = rt. block_on ( persistence. persist_full ( db. cog_redis . bind_space ( ) ) ) {
3682+ eprintln ! ( "[shutdown] BindSpace flush failed: {}" , e) ;
3683+ }
3684+ if let Err ( e) = rt. block_on ( persistence. persist_index ( & db. fingerprints ) ) {
3685+ eprintln ! ( "[shutdown] Index flush failed: {}" , e) ;
3686+ }
3687+ eprintln ! ( "[shutdown] Done. Exiting." ) ;
3688+ std:: process:: exit ( 0 ) ;
3689+ } )
3690+ . expect ( "Failed to set Ctrl-C handler" ) ;
3691+ }
3692+
35583693 // Spawn UDP bitpacked Hamming listener
35593694 spawn_udp_listener ( & config. host , udp_port, Arc :: clone ( & state) ) ;
35603695
@@ -3565,6 +3700,7 @@ fn main() {
35653700
35663701 println ! ( "Listening on http://{}" , addr) ;
35673702 println ! ( "UDP Hamming on udp://{}:{}" , config. host, udp_port) ;
3703+ println ! ( "Lance persistence: {}/lance" , config. data_dir) ;
35683704
35693705 // Accept connections
35703706 for stream in listener. incoming ( ) {
0 commit comments