1use fst::automaton::AlwaysMatch;
9use fst::set::Stream as FSTStream;
10use fst::{
11 Automaton, Error as FSTError, IntoStreamer, Set as FSTSet, SetBuilder as FSTSetBuilder,
12 Streamer,
13};
14use fst_levenshtein::Levenshtein;
15use fst_regex::Regex;
16use hashbrown::{HashMap, HashSet};
17use radix::RadixNum;
18use regex_syntax::escape as regex_escape;
19use std::collections::VecDeque;
20use std::fmt;
21use std::fs::{self, File};
22use std::io::{self, BufRead, BufReader, BufWriter, Write};
23use std::iter::FromIterator;
24use std::path::{Path, PathBuf};
25use std::str;
26use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
27use std::thread;
28use std::time::{Duration, SystemTime};
29
30use super::generic::{
31 StoreGeneric, StoreGenericActionBuilder, StoreGenericBuilder, StoreGenericPool,
32};
33use super::keyer::StoreKeyerHasher;
34use crate::lexer::ranges::LexerRegexRange;
35
36#[derive(Clone)]
39pub struct StoreFSTPool {
40 fst_store_config: Arc<crate::config::ConfigStoreFST>,
41 graph_pool: Arc<RwLock<HashMap<StoreFSTKey, StoreFSTBox>>>,
42 graph_acquire_lock: Arc<Mutex<()>>,
43 graph_rebuild_lock: Arc<Mutex<()>>,
44 graph_access_lock: Arc<RwLock<()>>,
45 graph_consolidate: Arc<RwLock<HashSet<StoreFSTKey>>>,
46}
47
48pub struct StoreFSTBuilder<'build> {
49 fst_store_config: &'build crate::config::ConfigStoreFST,
50 graph_consolidate: Arc<RwLock<HashSet<StoreFSTKey>>>,
51}
52
53pub struct StoreFST {
54 graph: FSTSet,
55 target: StoreFSTKey,
56 pending: StoreFSTPending,
57 last_used: Arc<RwLock<SystemTime>>,
58 last_consolidated: Arc<RwLock<SystemTime>>,
59 graph_consolidate: Arc<RwLock<HashSet<StoreFSTKey>>>,
60}
61
62#[derive(Default)]
63pub struct StoreFSTPending {
64 pop: Arc<RwLock<HashSet<Vec<u8>>>>,
65 push: Arc<RwLock<HashSet<Vec<u8>>>>,
66}
67
68pub struct StoreFSTActionBuilder<'build> {
69 pub fst_store_config: &'build crate::config::ConfigStoreFST,
70}
71
72pub struct StoreFSTAction {
73 store: StoreFSTBox,
74}
75
76#[derive(PartialEq, Eq, Hash, Clone, Copy)]
77pub struct StoreFSTKey {
78 collection_hash: StoreFSTAtom,
79 bucket_hash: StoreFSTAtom,
80}
81
82pub struct StoreFSTMisc;
83
84#[derive(Copy, Clone)]
85enum StoreFSTPathMode {
86 Permanent,
87 Temporary,
88 Backup,
89}
90
91type StoreFSTAtom = u32;
92type StoreFSTBox = Arc<StoreFST>;
93
94const WORD_LIMIT_LENGTH: usize = 40;
95const ATOM_HASH_RADIX: usize = 16;
96
97impl StoreFSTPathMode {
98 fn extension(&self) -> &'static str {
99 match self {
100 StoreFSTPathMode::Permanent => ".fst",
101 StoreFSTPathMode::Temporary => ".fst.tmp",
102 StoreFSTPathMode::Backup => ".fst.bck",
103 }
104 }
105}
106
107impl StoreFSTPool {
108 pub fn new(fst_store_config: Arc<crate::config::ConfigStoreFST>) -> Self {
109 Self {
110 fst_store_config,
111 graph_pool: Arc::default(),
112 graph_acquire_lock: Arc::default(),
113 graph_rebuild_lock: Arc::default(),
114 graph_access_lock: Arc::default(),
115 graph_consolidate: Arc::default(),
116 }
117 }
118
119 pub fn count(&self) -> (usize, usize) {
120 (
121 self.graph_pool.read().unwrap().len(),
122 self.graph_consolidate.read().unwrap().len(),
123 )
124 }
125
126 pub fn lock_read_access<'a>(&'a self) -> RwLockReadGuard<'a, ()> {
127 self.graph_access_lock.read().unwrap()
128 }
129
130 pub fn lock_write_access<'a>(&'a self) -> RwLockWriteGuard<'a, ()> {
131 self.graph_access_lock.write().unwrap()
132 }
133
134 pub fn acquire<T: AsRef<str>>(&self, collection: T, bucket: T) -> Result<StoreFSTBox, ()> {
135 let (collection_str, bucket_str) = (collection.as_ref(), bucket.as_ref());
136
137 let pool_key = StoreFSTKey::from_str(collection_str, bucket_str);
138
139 let _acquire = self.graph_acquire_lock.lock().unwrap();
142
143 let graph_pool_read = self.graph_pool.read().unwrap();
145
146 if let Some(store_fst) = graph_pool_read.get(&pool_key) {
147 Self::proceed_acquire_cache("fst", collection_str, pool_key, store_fst)
148 } else {
149 tracing::info!(
150 "fst store not in pool for collection: {} <{:x}> / bucket: {} <{:x}>, opening it",
151 collection_str,
152 pool_key.collection_hash,
153 bucket_str,
154 pool_key.bucket_hash
155 );
156
157 drop(graph_pool_read);
160
161 let builder = StoreFSTBuilder {
162 fst_store_config: &self.fst_store_config,
163 graph_consolidate: Arc::clone(&self.graph_consolidate),
164 };
165
166 Self::proceed_acquire_open("fst", collection_str, pool_key, &self.graph_pool, &builder)
167 }
168 }
169
170 pub fn janitor(&self) {
171 Self::proceed_janitor(
172 "fst",
173 &self.graph_pool,
174 self.fst_store_config.pool.inactive_after,
175 &self.graph_access_lock,
176 )
177 }
178
179 pub fn backup(&self, path: &Path) -> Result<(), io::Error> {
180 tracing::debug!("backing up all fst stores to path: {:?}", path);
181
182 fs::create_dir_all(path)?;
184
185 self.dump_action(
187 "backup",
188 StoreFSTPathMode::Permanent,
189 &self.fst_store_config.path,
190 path,
191 &Self::backup_item,
192 )
193 }
194
195 pub fn restore(&self, path: &Path) -> Result<(), io::Error> {
196 tracing::debug!("restoring all fst stores from path: {:?}", path);
197
198 self.dump_action(
200 "restore",
201 StoreFSTPathMode::Backup,
202 path,
203 &self.fst_store_config.path,
204 &Self::restore_item,
205 )
206 }
207
208 pub fn consolidate(&self, force: bool) {
209 tracing::debug!("scanning for fst store pool items to consolidate");
210
211 let _rebuild = self.graph_rebuild_lock.lock().unwrap();
219
220 if self.graph_consolidate.read().unwrap().is_empty() {
222 tracing::info!("no fst store pool items to consolidate in register");
223
224 return;
225 }
226
227 let mut keys_consolidate: Vec<StoreFSTKey> = Vec::new();
229
230 {
231 let _access = self.graph_access_lock.write().unwrap();
234
235 let (graph_pool_read, graph_consolidate_read) = (
236 self.graph_pool.read().unwrap(),
237 self.graph_consolidate.read().unwrap(),
238 );
239
240 for key in &*graph_consolidate_read {
241 if let Some(store) = graph_pool_read.get(key) {
242 let not_consolidated_for = store
247 .last_consolidated
248 .read()
249 .unwrap()
250 .elapsed()
251 .unwrap_or_else(|err| {
252 tracing::error!(
253 "fst key: {} last consolidated duration clock issue, zeroing: {}",
254 key,
255 err
256 );
257
258 Duration::from_secs(0)
260 })
261 .as_secs();
262
263 if force
264 || not_consolidated_for >= self.fst_store_config.graph.consolidate_after
265 {
266 tracing::info!(
267 "fst key: {} not consolidated for: {} seconds, may consolidate",
268 key,
269 not_consolidated_for
270 );
271
272 keys_consolidate.push(*key);
273 } else {
274 tracing::debug!(
275 "fst key: {} not consolidated for: {} seconds, no consolidate",
276 key,
277 not_consolidated_for
278 );
279 }
280 }
281 }
282 }
283
284 if keys_consolidate.is_empty() {
286 tracing::info!("no fst store pool items need to consolidate at the moment");
287
288 return;
289 }
290
291 {
293 let _access = self.graph_access_lock.write().unwrap();
296
297 let mut graph_consolidate_write = self.graph_consolidate.write().unwrap();
298
299 for key in &keys_consolidate {
300 graph_consolidate_write.remove(key);
301
302 tracing::debug!("fst key: {} cleared from consolidate register", key);
303 }
304 }
305
306 let (mut count_moved, mut count_pushed, mut count_popped) = (0, 0, 0);
308
309 {
310 for key in &keys_consolidate {
311 {
312 let _access = self.graph_access_lock.write().unwrap();
317
318 let do_close = if let Some(store) = self.graph_pool.read().unwrap().get(key) {
319 tracing::debug!("fst key: {} consolidate started", key);
320
321 let consolidate_counts = self.consolidate_item(store);
322
323 count_moved += consolidate_counts.1;
324 count_pushed += consolidate_counts.2;
325 count_popped += consolidate_counts.3;
326
327 tracing::debug!("fst key: {} consolidate complete", key);
328
329 consolidate_counts.0
331 } else {
332 false
333 };
334
335 if do_close {
341 self.graph_pool.write().unwrap().remove(key);
342 }
343 }
344
345 thread::yield_now();
352 }
353 }
354
355 tracing::info!(
356 "done scanning for fst store pool items to consolidate (move: {}, push: {}, pop: {})",
357 count_moved,
358 count_pushed,
359 count_popped
360 );
361 }
362
363 #[allow(clippy::type_complexity)]
364 fn dump_action(
365 &self,
366 action: &str,
367 path_mode: StoreFSTPathMode,
368 read_path: &Path,
369 write_path: &Path,
370 fn_item: &dyn Fn(&Self, &Path, &Path, &str, &str) -> Result<(), io::Error>,
371 ) -> Result<(), io::Error> {
372 let fst_extension = path_mode.extension();
373 let fst_extension_len = fst_extension.len();
374
375 for collection in fs::read_dir(read_path)? {
377 let collection = collection?;
378
379 if let (Ok(collection_file_type), Some(collection_name)) =
381 (collection.file_type(), collection.file_name().to_str())
382 {
383 if collection_file_type.is_dir() {
384 tracing::debug!("fst collection ongoing {}: {}", action, collection_name);
385
386 fs::create_dir_all(write_path.join(collection_name))?;
388
389 for bucket in fs::read_dir(read_path.join(collection_name))? {
391 let bucket = bucket?;
392
393 if let (Ok(bucket_file_type), Some(bucket_file_name)) =
395 (bucket.file_type(), bucket.file_name().to_str())
396 {
397 let bucket_file_name_len = bucket_file_name.len();
398
399 if bucket_file_type.is_file()
400 && bucket_file_name_len > fst_extension_len
401 && bucket_file_name.ends_with(fst_extension)
402 {
403 let bucket_name =
405 &bucket_file_name[..(bucket_file_name_len - fst_extension_len)];
406
407 tracing::debug!(
408 "fst bucket ongoing {}: {}/{}",
409 action,
410 collection_name,
411 bucket_name
412 );
413
414 fn_item(
415 self,
416 write_path,
417 &bucket.path(),
418 collection_name,
419 bucket_name,
420 )?;
421 }
422 }
423 }
424 }
425 }
426 }
427
428 Ok(())
429 }
430
431 fn backup_item(
432 &self,
433 backup_path: &Path,
434 _origin_path: &Path,
435 collection_name: &str,
436 bucket_name: &str,
437 ) -> Result<(), io::Error> {
438 let _access = self.graph_access_lock.write().unwrap();
441
442 let fst_backup_path = backup_path.join(collection_name).join(format!(
444 "{}{}",
445 bucket_name,
446 StoreFSTPathMode::Backup.extension()
447 ));
448
449 tracing::debug!(
450 "fst bucket: {}/{} backing up to path: {:?}",
451 collection_name,
452 bucket_name,
453 fst_backup_path
454 );
455
456 fs::remove_file(&fst_backup_path).ok();
458
459 let backup_fst_file = File::create(&fst_backup_path)?;
461 let mut backup_fst_writer = BufWriter::new(backup_fst_file);
462
463 let mut count_words = 0;
464
465 if let (Ok(collection_radix), Ok(bucket_radix)) = (
468 RadixNum::from_str(collection_name, ATOM_HASH_RADIX),
469 RadixNum::from_str(bucket_name, ATOM_HASH_RADIX),
470 ) {
471 if let (Ok(collection_hash), Ok(bucket_hash)) =
472 (collection_radix.as_decimal(), bucket_radix.as_decimal())
473 {
474 let origin_fst = StoreFSTBuilder::open(
475 collection_hash as StoreFSTAtom,
476 bucket_hash as StoreFSTAtom,
477 &self.fst_store_config,
478 )
479 .map_err(|_| io::Error::other("graph open failure"))?;
480
481 let mut origin_fst_stream = origin_fst.stream();
482
483 while let Some(word) = origin_fst_stream.next() {
484 count_words += 1;
485
486 backup_fst_writer.write_all(word)?;
488 backup_fst_writer.write_all(b"\n")?;
489 }
490
491 tracing::info!(
492 "fst bucket: {}/{} backed up to path: {:?} ({} words)",
493 collection_name,
494 bucket_name,
495 fst_backup_path,
496 count_words
497 );
498 }
499 }
500
501 Ok(())
502 }
503
504 fn restore_item(
505 &self,
506 _backup_path: &Path,
507 origin_path: &Path,
508 collection_name: &str,
509 bucket_name: &str,
510 ) -> Result<(), io::Error> {
511 let _access = self.graph_access_lock.write().unwrap();
514
515 tracing::debug!(
516 "fst bucket: {}/{} restoring from path: {:?}",
517 collection_name,
518 bucket_name,
519 origin_path
520 );
521
522 if let (Ok(collection_radix), Ok(bucket_radix)) = (
525 RadixNum::from_str(collection_name, ATOM_HASH_RADIX),
526 RadixNum::from_str(bucket_name, ATOM_HASH_RADIX),
527 ) {
528 if let (Ok(collection_hash), Ok(bucket_hash)) =
529 (collection_radix.as_decimal(), bucket_radix.as_decimal())
530 {
531 self.close(collection_hash as StoreFSTAtom, bucket_hash as StoreFSTAtom);
533
534 let fst_path = self.fst_store_config.path(
536 StoreFSTPathMode::Permanent,
537 collection_hash as StoreFSTAtom,
538 Some(bucket_hash as StoreFSTAtom),
539 );
540
541 if fst_path.exists() {
543 fs::remove_file(&fst_path)?;
544 }
545
546 let fst_writer = BufWriter::new(File::create(&fst_path)?);
548 let fst_backup_reader = BufReader::new(File::open(&origin_path)?);
549
550 let mut fst_builder = FSTSetBuilder::new(fst_writer)
551 .map_err(|_| io::Error::other("graph restore builder failure"))?;
552
553 for word in fst_backup_reader.lines() {
554 let word = word?;
555
556 fst_builder
557 .insert(word)
558 .map_err(|_| io::Error::other("graph restore word insert failure"))?;
559 }
560
561 fst_builder
562 .finish()
563 .map_err(|_| io::Error::other("graph restore finish failure"))?;
564
565 tracing::info!(
566 "fst bucket: {}/{} restored to path: {:?} from backup: {:?}",
567 collection_name,
568 bucket_name,
569 fst_path,
570 origin_path
571 );
572 }
573 }
574
575 Ok(())
576 }
577
578 fn consolidate_item(&self, store: &StoreFSTBox) -> (bool, usize, usize, usize) {
579 let (mut should_close, mut count_moved, mut count_pushed, mut count_popped) =
580 (false, 0, 0, 0);
581
582 let (mut pending_push_write, mut pending_pop_write) = (
584 store.pending.push.write().unwrap(),
585 store.pending.pop.write().unwrap(),
586 );
587
588 if !(pending_push_write.is_empty() && pending_pop_write.is_empty()) {
592 if let Ok(old_fst) = StoreFSTBuilder::open(
594 store.target.collection_hash,
595 store.target.bucket_hash,
596 &self.fst_store_config,
597 ) {
598 let bucket_tmp_path = self.fst_store_config.path(
600 StoreFSTPathMode::Temporary,
601 store.target.collection_hash,
602 Some(store.target.bucket_hash),
603 );
604
605 let bucket_tmp_path_parent = bucket_tmp_path.parent().unwrap();
606
607 if fs::create_dir_all(&bucket_tmp_path_parent).is_ok() {
608 fs::remove_file(&bucket_tmp_path).ok();
611
612 if let Ok(tmp_fst_file) = File::create(&bucket_tmp_path) {
613 let tmp_fst_writer = BufWriter::new(tmp_fst_file);
614
615 if let Ok(mut tmp_fst_builder) = FSTSetBuilder::new(tmp_fst_writer) {
617 let mut ordered_push_vec: Vec<&[u8]> =
621 Vec::from_iter(pending_push_write.iter().map(|item| item.as_ref()));
622
623 ordered_push_vec.sort();
624
625 let mut ordered_push: VecDeque<&[u8]> =
626 VecDeque::from_iter(ordered_push_vec);
627
628 let mut old_fst_stream = old_fst.stream();
631
632 'old: while let Some(old_fst_word) = old_fst_stream.next() {
633 if let Some(push_first_ref) = ordered_push.front() {
642 if *push_first_ref <= old_fst_word {
644 while let Some(push_front_ref) = ordered_push.front() {
645 if *push_front_ref <= old_fst_word {
646 let push_front = ordered_push.pop_front().unwrap();
650
651 if StoreFSTMisc::check_over_limits(
652 tmp_fst_builder.bytes_written() as usize,
653 count_pushed + count_moved,
654 &self.fst_store_config.graph,
655 ) {
656 tracing::warn!(
658 "limit reached on new from old in fst"
659 );
660
661 break 'old;
663 }
664
665 if let Err(err) = tmp_fst_builder.insert(push_front)
666 {
667 tracing::error!(
669 "failed inserting new from old in fst: {}",
670 err
671 );
672 } else {
673 count_pushed += 1;
675 }
676
677 continue;
680 }
681
682 break;
685 }
686 }
687 }
688
689 if !pending_pop_write.contains(old_fst_word) {
691 if StoreFSTMisc::check_over_limits(
692 tmp_fst_builder.bytes_written() as usize,
693 count_pushed + count_moved,
694 &self.fst_store_config.graph,
695 ) {
696 tracing::warn!("limit reached on old word in fst");
698
699 break 'old;
701 }
702
703 if let Err(err) = tmp_fst_builder.insert(old_fst_word) {
704 tracing::error!(
706 "failed inserting old word in fst: {}",
707 err
708 );
709 } else {
710 count_moved += 1;
712 }
713 } else {
714 count_popped += 1;
715 }
716 }
717
718 while let Some(push_front) = ordered_push.pop_front() {
722 if StoreFSTMisc::check_over_limits(
723 tmp_fst_builder.bytes_written() as usize,
724 count_pushed + count_moved,
725 &self.fst_store_config.graph,
726 ) {
727 tracing::warn!(
729 "limit reached on new word from complete in fst"
730 );
731
732 break;
734 }
735
736 if let Err(err) = tmp_fst_builder.insert(push_front) {
737 tracing::error!(
739 "failed inserting new word from complete in fst: {}",
740 err
741 );
742 } else {
743 count_pushed += 1;
745 }
746 }
747
748 if tmp_fst_builder.finish().is_ok() {
750 should_close = true;
752
753 let bucket_final_path = self.fst_store_config.path(
757 StoreFSTPathMode::Permanent,
758 store.target.collection_hash,
759 Some(store.target.bucket_hash),
760 );
761
762 if fs::rename(&bucket_tmp_path, &bucket_final_path).is_ok() {
764 tracing::info!(
765 "done consolidate fst at path: {:?}",
766 bucket_final_path
767 );
768 } else {
769 tracing::error!(
770 "error consolidating fst at path: {:?}",
771 bucket_final_path
772 );
773 }
774 } else {
775 tracing::error!(
776 "error finishing building temporary fst at path: {:?}",
777 bucket_tmp_path
778 );
779 }
780 } else {
781 tracing::error!(
782 "error starting building temporary fst at path: {:?}",
783 bucket_tmp_path
784 );
785 }
786 } else {
787 tracing::error!(
788 "error initializing temporary fst at path: {:?}",
789 bucket_tmp_path
790 );
791 }
792 } else {
793 tracing::error!(
794 "error initializing temporary fst directory at path: {:?}",
795 bucket_tmp_path_parent
796 );
797 }
798 } else {
799 tracing::error!("error opening old fst");
800 }
801
802 *pending_push_write = HashSet::new();
804 *pending_pop_write = HashSet::new();
805 }
806
807 (should_close, count_moved, count_pushed, count_popped)
808 }
809
810 fn close(&self, collection_hash: StoreFSTAtom, bucket_hash: StoreFSTAtom) {
811 tracing::debug!(
812 "closing finite-state transducer graph for collection: <{:x}> and bucket: <{:x}>",
813 collection_hash,
814 bucket_hash
815 );
816
817 let bucket_target = StoreFSTKey::from_atom(collection_hash, bucket_hash);
818
819 self.graph_pool.write().unwrap().remove(&bucket_target);
820 self.graph_consolidate
821 .write()
822 .unwrap()
823 .remove(&bucket_target);
824 }
825}
826
827impl<'build> StoreGenericPool<StoreFSTKey, StoreFST, StoreFSTBuilder<'build>> for StoreFSTPool {}
828
829impl<'build> StoreFSTBuilder<'build> {
830 fn open(
831 collection_hash: StoreFSTAtom,
832 bucket_hash: StoreFSTAtom,
833 fst_store_config: &crate::config::ConfigStoreFST,
834 ) -> Result<FSTSet, FSTError> {
835 tracing::debug!(
836 "opening finite-state transducer graph for collection: <{:x}> and bucket: <{:x}>",
837 collection_hash,
838 bucket_hash
839 );
840
841 let collection_bucket_path = fst_store_config.path(
842 StoreFSTPathMode::Permanent,
843 collection_hash,
844 Some(bucket_hash),
845 );
846
847 if collection_bucket_path.exists() {
848 unsafe { FSTSet::from_path(collection_bucket_path) }
853 } else {
854 let empty_iter: Vec<&str> = Vec::new();
857
858 FSTSet::from_iter(empty_iter)
859 }
860 }
861}
862
863impl crate::config::ConfigStoreFST {
864 fn path(
865 &self,
866 mode: StoreFSTPathMode,
867 collection_hash: StoreFSTAtom,
868 bucket_hash: Option<StoreFSTAtom>,
869 ) -> PathBuf {
870 let mut final_path = self.path.join(format!("{:x}", collection_hash));
871
872 if let Some(bucket_hash) = bucket_hash {
873 final_path = final_path.join(format!("{:x}{}", bucket_hash, mode.extension()));
874 }
875
876 final_path
877 }
878}
879
880impl<'build> StoreGenericBuilder<StoreFSTKey, StoreFST> for StoreFSTBuilder<'build> {
881 fn build(&self, pool_key: StoreFSTKey) -> Result<StoreFST, ()> {
882 Self::open(
883 pool_key.collection_hash,
884 pool_key.bucket_hash,
885 self.fst_store_config,
886 )
887 .map(|graph| {
888 let now = SystemTime::now();
889
890 StoreFST {
891 graph,
892 target: pool_key,
893 pending: StoreFSTPending::default(),
894 last_used: Arc::new(RwLock::new(now)),
895 last_consolidated: Arc::new(RwLock::new(now)),
896 graph_consolidate: Arc::clone(&self.graph_consolidate),
897 }
898 })
899 .map_err(|err| {
900 tracing::error!("failed opening fst: {}", err);
901 })
902 }
903}
904
905impl StoreFST {
906 pub fn cardinality(&self) -> usize {
907 self.graph.len()
908 }
909
910 pub fn as_stream(&self) -> FSTStream<'_, AlwaysMatch> {
911 self.graph.into_stream()
912 }
913
914 pub fn lookup_begins(&self, word: &str) -> Result<FSTStream<'_, Regex>, ()> {
915 let mut regex_str = regex_escape(word);
921
922 regex_str.push('(');
923
924 let write_result = LexerRegexRange::from(word)
925 .unwrap_or_default()
926 .write_to(&mut regex_str);
927
928 regex_str.push_str("*)");
929
930 if let Err(err) = write_result {
932 tracing::error!(
933 "could not lookup word in fst via 'begins': {} because regex write failed: {}",
934 word,
935 err
936 );
937
938 return Err(());
939 }
940
941 tracing::debug!(
943 "looking-up word in fst via 'begins': {} with regex: {}",
944 word,
945 regex_str
946 );
947
948 if let Ok(regex) = Regex::new(®ex_str) {
949 Ok(self.graph.search(regex).into_stream())
950 } else {
951 Err(())
952 }
953 }
954
955 pub fn lookup_typos(
956 &self,
957 word: &str,
958 max_factor: Option<u32>,
959 ) -> Result<FSTStream<'_, Levenshtein>, ()> {
960 let mut typo_factor = match word.len() {
962 1..=3 => 0,
963 4..=6 => 1,
964 7..=9 => 2,
965 _ => 3,
966 };
967
968 if let Some(max_factor) = max_factor {
970 if typo_factor > max_factor {
971 tracing::debug!(
972 "Capping typo factor from {typo_factor} to {max_factor} \
973 because of max allowed factor"
974 );
975 typo_factor = max_factor;
976 }
977 }
978
979 tracing::debug!(
980 "looking-up word in fst via 'typos': {} with typo factor: {}",
981 word,
982 typo_factor
983 );
984
985 if let Ok(fuzzy) = Levenshtein::new(word, typo_factor) {
986 Ok(self.graph.search(fuzzy).into_stream())
987 } else {
988 Err(())
989 }
990 }
991
992 pub fn should_consolidate(&self) {
993 if !self
995 .graph_consolidate
996 .read()
997 .unwrap()
998 .contains(&self.target)
999 {
1000 self.graph_consolidate.write().unwrap().insert(self.target);
1002
1003 let mut last_consolidated_value = self.last_consolidated.write().unwrap();
1006
1007 *last_consolidated_value = SystemTime::now();
1008
1009 drop(last_consolidated_value);
1011
1012 tracing::info!("graph consolidation scheduled on pool key: {}", self.target);
1013 } else {
1014 tracing::debug!(
1015 "graph consolidation already scheduled on pool key: {}",
1016 self.target
1017 );
1018 }
1019 }
1020}
1021
1022impl StoreGeneric for StoreFST {
1023 fn ref_last_used(&self) -> &RwLock<SystemTime> {
1024 &self.last_used
1025 }
1026}
1027
1028impl<'build> StoreFSTActionBuilder<'build> {
1029 pub fn access(store: StoreFSTBox) -> StoreFSTAction {
1030 Self::build(store)
1031 }
1032
1033 fn build(store: StoreFSTBox) -> StoreFSTAction {
1034 StoreFSTAction { store }
1035 }
1036}
1037
1038impl StoreFSTPool {
1039 pub fn erase<T: AsRef<str>>(&self, collection: T, bucket: Option<T>) -> Result<u32, ()> {
1040 self.dispatch_erase("fst", collection, bucket)
1041 }
1042}
1043
1044impl StoreGenericActionBuilder for StoreFSTPool {
1045 fn proceed_erase_collection(&self, collection_str: &str) -> Result<u32, ()> {
1046 let path_mode = StoreFSTPathMode::Permanent;
1047
1048 let collection_atom = StoreKeyerHasher::to_compact(collection_str);
1049 let collection_path = self.fst_store_config.path(path_mode, collection_atom, None);
1050
1051 let mut bucket_atoms: Vec<StoreFSTAtom> = Vec::new();
1055
1056 {
1057 let graph_pool_read = self.graph_pool.read().unwrap();
1058
1059 for target_key in graph_pool_read.keys() {
1060 if target_key.collection_hash == collection_atom {
1061 bucket_atoms.push(target_key.bucket_hash);
1062 }
1063 }
1064 }
1065
1066 if !bucket_atoms.is_empty() {
1067 tracing::debug!(
1068 "will force-close {} fst buckets for collection: {}",
1069 bucket_atoms.len(),
1070 collection_str
1071 );
1072
1073 let (mut graph_pool_write, mut graph_consolidate_write) = (
1074 self.graph_pool.write().unwrap(),
1075 self.graph_consolidate.write().unwrap(),
1076 );
1077
1078 for bucket_atom in bucket_atoms {
1079 tracing::debug!(
1080 "fst bucket graph force close for bucket: {}/<{:x}>",
1081 collection_str,
1082 bucket_atom
1083 );
1084
1085 let bucket_target = StoreFSTKey::from_atom(collection_atom, bucket_atom);
1086
1087 graph_pool_write.remove(&bucket_target);
1088 graph_consolidate_write.remove(&bucket_target);
1089 }
1090 }
1091
1092 if collection_path.exists() {
1094 tracing::debug!(
1095 "fst collection store exists, erasing: {}/* at path: {:?}",
1096 collection_str,
1097 &collection_path
1098 );
1099
1100 let erase_result = fs::remove_dir_all(&collection_path);
1102
1103 if erase_result.is_ok() {
1104 tracing::debug!("done with fst collection erasure");
1105
1106 Ok(1)
1107 } else {
1108 Err(())
1109 }
1110 } else {
1111 tracing::debug!(
1112 "fst collection store does not exist, consider already erased: {}/* at path: {:?}",
1113 collection_str,
1114 &collection_path
1115 );
1116
1117 Ok(0)
1118 }
1119 }
1120
1121 fn proceed_erase_bucket(&self, collection_str: &str, bucket_str: &str) -> Result<u32, ()> {
1122 tracing::debug!(
1123 "sub-erase on fst bucket: {} for collection: {}",
1124 bucket_str,
1125 collection_str
1126 );
1127
1128 let (collection_atom, bucket_atom) = (
1129 StoreKeyerHasher::to_compact(collection_str),
1130 StoreKeyerHasher::to_compact(bucket_str),
1131 );
1132
1133 let bucket_path = self.fst_store_config.path(
1134 StoreFSTPathMode::Permanent,
1135 collection_atom,
1136 Some(bucket_atom),
1137 );
1138
1139 self.close(collection_atom, bucket_atom);
1141
1142 if bucket_path.exists() {
1144 tracing::debug!(
1145 "fst bucket graph exists, erasing: {}/{} at path: {:?}",
1146 collection_str,
1147 bucket_str,
1148 &bucket_path
1149 );
1150
1151 let erase_result = fs::remove_file(&bucket_path);
1153
1154 if erase_result.is_ok() {
1155 tracing::debug!("done with fst bucket erasure");
1156
1157 Ok(1)
1158 } else {
1159 Err(())
1160 }
1161 } else {
1162 tracing::debug!(
1163 "fst bucket graph does not exist, consider already erased: {}/{} at path: {:?}",
1164 collection_str,
1165 bucket_str,
1166 &bucket_path
1167 );
1168
1169 Ok(0)
1170 }
1171 }
1172}
1173
1174impl StoreFSTAction {
1175 pub fn push_word(&self, word: &str, fst_store_config: &crate::config::ConfigStoreFST) -> bool {
1176 if Self::word_over_limit(word) {
1178 return false;
1179 }
1180
1181 let word_bytes = word.as_bytes();
1182
1183 if self.store.pending.pop.read().unwrap().contains(word_bytes) {
1185 self.store.pending.pop.write().unwrap().remove(word_bytes);
1186 }
1187
1188 let graph_fst = self.store.graph.as_fst();
1192
1193 if !self.store.graph.contains(&word)
1194 && !self.store.pending.push.read().unwrap().contains(word_bytes)
1195 && self.store.pending.push.read().unwrap().len() < fst_store_config.graph.max_words
1196 && !StoreFSTMisc::check_over_limits(
1197 graph_fst.size(),
1198 graph_fst.len(),
1199 &fst_store_config.graph,
1200 )
1201 {
1202 self.store
1203 .pending
1204 .push
1205 .write()
1206 .unwrap()
1207 .insert(word_bytes.to_vec());
1208
1209 self.store.should_consolidate();
1210
1211 true
1213 } else {
1214 false
1216 }
1217 }
1218
1219 pub fn pop_word(&self, word: &str) -> bool {
1220 if Self::word_over_limit(word) {
1222 return false;
1223 }
1224
1225 let word_bytes = word.as_bytes();
1226
1227 if self.store.pending.push.read().unwrap().contains(word_bytes) {
1229 self.store.pending.push.write().unwrap().remove(word_bytes);
1230 }
1231
1232 if self.store.graph.contains(word_bytes)
1234 && !self.store.pending.pop.read().unwrap().contains(word_bytes)
1235 {
1236 self.store
1237 .pending
1238 .pop
1239 .write()
1240 .unwrap()
1241 .insert(word_bytes.to_vec());
1242
1243 self.store.should_consolidate();
1244
1245 true
1247 } else {
1248 false
1250 }
1251 }
1252
1253 pub fn suggest_words(
1254 &self,
1255 from_word: &str,
1256 limit: usize,
1257 max_typo_factor: Option<u32>,
1258 ) -> Option<Vec<String>> {
1259 if Self::word_over_limit(from_word) {
1261 return None;
1262 }
1263
1264 let mut found_words = Vec::with_capacity(limit);
1265
1266 if let Ok(stream) = self.store.lookup_begins(from_word) {
1268 tracing::debug!("looking up for word: {} in 'begins' fst stream", from_word);
1269
1270 Self::find_words_stream(stream, &mut found_words, limit);
1271 }
1272
1273 if found_words.len() < limit {
1275 if let Ok(stream) = self.store.lookup_typos(from_word, max_typo_factor) {
1276 tracing::debug!("looking up for word: {} in 'typos' fst stream", from_word);
1277
1278 Self::find_words_stream(stream, &mut found_words, limit);
1279 }
1280 }
1281
1282 if !found_words.is_empty() {
1283 Some(found_words)
1284 } else {
1285 None
1286 }
1287 }
1288
1289 pub fn list_words(&self, limit: usize, offset: usize) -> Result<Vec<String>, ()> {
1290 let stream = self.store.as_stream();
1291
1292 match stream
1294 .into_strs()
1295 .map(|words| words.into_iter().skip(offset).take(limit).collect())
1296 {
1297 Err(err) => {
1298 tracing::debug!("conversion of stream failed: {}", err);
1299 Err(())
1300 }
1301 Ok(words) => Ok(words),
1302 }
1303 }
1304
1305 pub fn count_words(&self) -> usize {
1306 self.store.cardinality()
1307 }
1308
1309 fn word_over_limit(word: &str) -> bool {
1310 if word.len() > WORD_LIMIT_LENGTH {
1311 tracing::debug!("got over-limit fst word: {}", word);
1312
1313 true
1314 } else {
1315 false
1316 }
1317 }
1318
1319 fn find_words_stream<A: Automaton>(
1320 mut stream: FSTStream<A>,
1321 found_words: &mut Vec<String>,
1322 limit: usize,
1323 ) {
1324 while let Some(word) = stream.next() {
1325 if let Ok(word_str) = str::from_utf8(word) {
1326 let word_string = word_str.to_string();
1327
1328 if !found_words.contains(&word_string) {
1329 found_words.push(word_string);
1330
1331 if found_words.len() >= limit {
1333 break;
1334 }
1335 }
1336 }
1337 }
1338 }
1339}
1340
1341impl StoreFSTMisc {
1342 pub fn count_collection_buckets(
1343 collection: impl AsRef<str>,
1344 fst_store_config: &crate::config::ConfigStoreFST,
1345 ) -> Result<usize, ()> {
1346 let mut count = 0;
1347
1348 let path_mode = StoreFSTPathMode::Permanent;
1349
1350 let collection_atom = StoreKeyerHasher::to_compact(collection.as_ref());
1351 let collection_path = fst_store_config.path(path_mode, collection_atom, None);
1352
1353 if collection_path.exists() {
1354 if let Ok(entries) = fs::read_dir(&collection_path) {
1356 let fst_extension = path_mode.extension();
1357 let fst_extension_len = fst_extension.len();
1358
1359 for entry in entries.flatten() {
1360 if let Some(entry_name) = entry.file_name().to_str() {
1361 let entry_name_len = entry_name.len();
1362
1363 if entry_name_len > fst_extension_len && entry_name.ends_with(fst_extension)
1365 {
1366 count += 1;
1367 }
1368 }
1369 }
1370 } else {
1371 tracing::error!("failed reading directory for count: {:?}", collection_path);
1372
1373 return Err(());
1374 }
1375 }
1376
1377 Ok(count)
1378 }
1379
1380 fn check_over_limits(
1381 bytes_count: usize,
1382 words_count: usize,
1383 fst_graph_config: &crate::config::ConfigStoreFSTGraph,
1384 ) -> bool {
1385 let max_size = fst_graph_config.max_size * 1024;
1387
1388 if bytes_count >= max_size {
1389 tracing::info!(
1390 "fst has exceeded maximum allowed bytes: {} over limit: {}",
1391 bytes_count,
1392 max_size
1393 );
1394
1395 return true;
1396 }
1397
1398 if words_count >= fst_graph_config.max_words {
1400 tracing::info!(
1401 "fst has exceeded maximum allowed words: {} over limit: {}",
1402 words_count,
1403 fst_graph_config.max_words
1404 );
1405
1406 return true;
1407 }
1408
1409 false
1411 }
1412}
1413
1414impl StoreFSTKey {
1415 pub fn from_atom(collection_hash: StoreFSTAtom, bucket_hash: StoreFSTAtom) -> StoreFSTKey {
1416 StoreFSTKey {
1417 collection_hash,
1418 bucket_hash,
1419 }
1420 }
1421
1422 pub fn from_str(collection_str: &str, bucket_str: &str) -> StoreFSTKey {
1423 StoreFSTKey {
1424 collection_hash: StoreKeyerHasher::to_compact(collection_str),
1425 bucket_hash: StoreKeyerHasher::to_compact(bucket_str),
1426 }
1427 }
1428}
1429
1430impl fmt::Display for StoreFSTKey {
1431 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1432 write!(f, "<{:x}>/<{:x}>", self.collection_hash, self.bucket_hash)
1433 }
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use super::*;
1439
1440 #[test]
1441 fn it_acquires_graph() {
1442 let fst_store_config = test_fst_store_config();
1443 let fst_pool = StoreFSTPool::new(fst_store_config);
1444
1445 assert!(fst_pool.acquire("c:test:1", "b:test:1").is_ok());
1446 }
1447
1448 #[test]
1449 fn it_janitors_graph() {
1450 let fst_store_config = test_fst_store_config();
1451 let fst_pool = StoreFSTPool::new(fst_store_config);
1452
1453 fst_pool.janitor();
1454 }
1455
1456 #[test]
1457 fn it_proceeds_primitives() {
1458 let fst_store_config = test_fst_store_config();
1459 let fst_pool = StoreFSTPool::new(fst_store_config);
1460
1461 let store = fst_pool.acquire("c:test:2", "b:test:2").unwrap();
1462
1463 assert!(store.lookup_typos("valerien", None).is_ok());
1464 }
1465
1466 fn test_fst_store_config() -> Arc<crate::config::ConfigStoreFST> {
1467 Arc::new(
1468 config::Config::builder()
1469 .add_source(config::File::from_str(
1470 crate::config::tests::defaults_toml(),
1471 config::FileFormat::Toml,
1472 ))
1473 .build()
1474 .unwrap()
1475 .get::<crate::config::ConfigStoreFST>("store.fst")
1476 .unwrap(),
1477 )
1478 }
1479}
1480
1481impl fmt::Debug for StoreFSTPool {
1484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1485 use crate::util::fmt::{AsPrettyMutex, AsPrettyRwLock};
1486
1487 let Self {
1489 graph_pool,
1490 graph_acquire_lock,
1491 graph_rebuild_lock,
1492 graph_access_lock,
1493 graph_consolidate,
1494 fst_store_config: _fst_store_config,
1497 } = self;
1498
1499 f.debug_struct("StoreFSTPool")
1500 .field("graph_pool", &AsPrettyRwLock(graph_pool))
1501 .field("graph_acquire_lock", &AsPrettyMutex(graph_acquire_lock))
1502 .field("graph_rebuild_lock", &AsPrettyMutex(graph_rebuild_lock))
1503 .field("graph_access_lock", &AsPrettyRwLock(graph_access_lock))
1504 .field("graph_consolidate", &AsPrettyRwLock(graph_consolidate))
1505 .finish_non_exhaustive()
1506 }
1507}
1508
1509impl fmt::Debug for StoreFSTKey {
1510 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1511 fmt::Display::fmt(&self, f)
1512 }
1513}
1514
1515impl fmt::Debug for StoreFST {
1516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1517 use crate::util::fmt::AsPrettyRwLock;
1518
1519 let Self {
1521 graph,
1522 target,
1523 pending,
1524 last_used,
1525 last_consolidated,
1526 graph_consolidate,
1527 } = self;
1528
1529 f.debug_struct("StoreFST")
1530 .field("graph", graph)
1531 .field("target", target)
1532 .field("pending", pending)
1533 .field("last_used", &AsPrettyRwLock(last_used))
1534 .field("last_consolidated", &AsPrettyRwLock(last_consolidated))
1535 .field("graph_consolidate", &AsPrettyRwLock(graph_consolidate))
1536 .finish()
1537 }
1538}
1539
1540impl fmt::Debug for StoreFSTPending {
1541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1542 use crate::util::fmt::AsPrettyRwLock;
1543
1544 let Self { pop, push } = self;
1546
1547 f.debug_struct("StoreFSTPending")
1548 .field("pop", &AsPrettyRwLock(pop))
1549 .field("push", &AsPrettyRwLock(push))
1550 .finish()
1551 }
1552}