offline_protocol_reliability/
deduplicator.rs1use chrono::{DateTime, Utc};
11use offline_protocol_core::MessageId;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
16pub struct DeduplicatorConfig {
17 pub max_tracked_messages: usize,
19
20 pub retention_time_secs: u64,
22
23 pub use_bloom_filter: bool,
25
26 pub bloom_filter_bits: usize,
28
29 pub bloom_hash_count: usize,
31
32 pub bloom_filter_count: usize,
34
35 pub bloom_rotation_secs: u64,
37}
38
39impl Default for DeduplicatorConfig {
40 fn default() -> Self {
41 Self {
42 max_tracked_messages: 1000,
44 retention_time_secs: 3600, use_bloom_filter: false,
46 bloom_filter_bits: 1 << 20, bloom_hash_count: 7, bloom_filter_count: 4, bloom_rotation_secs: 900, }
51 }
52}
53
54#[derive(Clone)]
56struct BloomFilter {
57 bits: Vec<u64>,
58 bit_count: usize,
59 hash_count: usize,
60 items_added: usize,
61}
62
63impl BloomFilter {
64 fn new(bit_count: usize, hash_count: usize) -> Self {
65 let word_count = bit_count.div_ceil(64);
66 Self {
67 bits: vec![0u64; word_count],
68 bit_count,
69 hash_count,
70 items_added: 0,
71 }
72 }
73
74 fn hashes(&self, item: &str) -> Vec<usize> {
76 let mut hashes = Vec::with_capacity(self.hash_count);
77
78 let h1 = fnv1a_hash(item.as_bytes());
80 let h2 = fnv1a_hash_seeded(item.as_bytes(), 0x5f356495) | 1;
85
86 for i in 0..self.hash_count {
87 let combined = h1.wrapping_add((i as u64).wrapping_mul(h2));
89 hashes.push((combined as usize) % self.bit_count);
90 }
91
92 hashes
93 }
94
95 fn insert(&mut self, item: &str) {
96 for bit_index in self.hashes(item) {
97 let word_index = bit_index / 64;
98 let bit_offset = bit_index % 64;
99 self.bits[word_index] |= 1u64 << bit_offset;
100 }
101 self.items_added += 1;
102 }
103
104 fn contains(&self, item: &str) -> bool {
105 for bit_index in self.hashes(item) {
106 let word_index = bit_index / 64;
107 let bit_offset = bit_index % 64;
108 if (self.bits[word_index] & (1u64 << bit_offset)) == 0 {
109 return false;
110 }
111 }
112 true
113 }
114
115 fn clear(&mut self) {
116 self.bits.fill(0);
117 self.items_added = 0;
118 }
119
120 fn items_count(&self) -> usize {
121 self.items_added
122 }
123
124 fn estimated_false_positive_rate(&self) -> f64 {
126 if self.items_added == 0 {
127 return 0.0;
128 }
129 let m = self.bit_count as f64;
130 let n = self.items_added as f64;
131 let k = self.hash_count as f64;
132 (1.0 - (-k * n / m).exp()).powf(k)
133 }
134}
135
136fn fnv1a_hash(data: &[u8]) -> u64 {
138 fnv1a_hash_seeded(data, 0xcbf29ce484222325)
139}
140
141fn fnv1a_hash_seeded(data: &[u8], seed: u64) -> u64 {
143 const FNV_PRIME: u64 = 0x100000001b3;
144 let mut hash = seed;
145 for byte in data {
146 hash ^= *byte as u64;
147 hash = hash.wrapping_mul(FNV_PRIME);
148 }
149 hash
150}
151
152struct RotatingBloomFilter {
154 filters: Vec<BloomFilter>,
155 current_index: usize,
156 last_rotation: DateTime<Utc>,
157 rotation_interval_secs: u64,
158}
159
160impl RotatingBloomFilter {
161 fn new(filter_count: usize, bit_count: usize, hash_count: usize, rotation_secs: u64) -> Self {
162 let filters = (0..filter_count)
163 .map(|_| BloomFilter::new(bit_count, hash_count))
164 .collect();
165 Self {
166 filters,
167 current_index: 0,
168 last_rotation: Utc::now(),
169 rotation_interval_secs: rotation_secs,
170 }
171 }
172
173 fn maybe_rotate(&mut self, now: DateTime<Utc>) {
174 let elapsed = now.signed_duration_since(self.last_rotation);
175 if elapsed.num_seconds() >= self.rotation_interval_secs as i64 {
176 self.current_index = (self.current_index + 1) % self.filters.len();
178 self.filters[self.current_index].clear();
179 self.last_rotation = now;
180 }
181 }
182
183 fn insert(&mut self, item: &str) {
184 self.maybe_rotate(Utc::now());
185 self.filters[self.current_index].insert(item);
186 }
187
188 fn contains(&mut self, item: &str) -> bool {
189 self.maybe_rotate(Utc::now());
190 self.filters.iter().any(|f| f.contains(item))
192 }
193
194 fn total_items(&self) -> usize {
195 self.filters.iter().map(|f| f.items_count()).sum()
196 }
197
198 fn clear(&mut self) {
199 for filter in &mut self.filters {
200 filter.clear();
201 }
202 self.current_index = 0;
203 self.last_rotation = Utc::now();
204 }
205
206 fn average_false_positive_rate(&self) -> f64 {
207 let rates: Vec<f64> = self
208 .filters
209 .iter()
210 .map(|f| f.estimated_false_positive_rate())
211 .collect();
212 if rates.is_empty() {
213 return 0.0;
214 }
215 rates.iter().sum::<f64>() / rates.len() as f64
216 }
217}
218
219#[derive(Debug, Clone)]
221struct SeenEntry {
222 seen_at: DateTime<Utc>,
224 last_accessed: DateTime<Utc>,
226}
227
228pub struct Deduplicator {
234 config: DeduplicatorConfig,
235 seen_messages: HashMap<String, SeenEntry>,
237 bloom_filter: Option<RotatingBloomFilter>,
239}
240
241impl Deduplicator {
242 pub fn new() -> Self {
244 Self::with_config(DeduplicatorConfig::default())
245 }
246
247 pub fn with_config(config: DeduplicatorConfig) -> Self {
249 let valid_bloom_config = config.bloom_filter_bits > 0
254 && config.bloom_hash_count > 0
255 && config.bloom_filter_count > 0
256 && config.bloom_rotation_secs > 0;
257 let bloom_filter = if config.use_bloom_filter && valid_bloom_config {
258 Some(RotatingBloomFilter::new(
259 config.bloom_filter_count,
260 config.bloom_filter_bits,
261 config.bloom_hash_count,
262 config.bloom_rotation_secs,
263 ))
264 } else {
265 None
266 };
267
268 Self {
269 config,
270 seen_messages: HashMap::new(),
271 bloom_filter,
272 }
273 }
274
275 pub fn is_exact(&self) -> bool {
280 self.bloom_filter.is_none()
281 }
282
283 pub fn is_duplicate(&self, message_id: &MessageId) -> bool {
297 let msg_id_str = message_id.as_str();
298
299 if let Some(ref bloom) = self.bloom_filter {
300 bloom.filters.iter().any(|f| f.contains(&msg_id_str))
304 } else {
305 self.seen_messages.contains_key(&msg_id_str)
306 }
307 }
308
309 pub fn is_duplicate_mut(&mut self, message_id: &MessageId) -> bool {
312 let msg_id_str = message_id.as_str();
313
314 if let Some(ref mut bloom) = self.bloom_filter {
315 bloom.contains(&msg_id_str)
316 } else if let Some(entry) = self.seen_messages.get_mut(&msg_id_str) {
317 entry.last_accessed = Utc::now();
318 true
319 } else {
320 false
321 }
322 }
323
324 pub fn mark_seen(&mut self, message_id: MessageId) -> bool {
334 let msg_id_str = message_id.as_str();
335
336 if let Some(ref mut bloom) = self.bloom_filter {
337 if bloom.contains(&msg_id_str) {
339 return false;
340 }
341 bloom.insert(&msg_id_str);
343 true
344 } else {
345 if self.seen_messages.contains_key(&msg_id_str) {
347 return false;
348 }
349
350 if self.seen_messages.len() >= self.config.max_tracked_messages {
352 self.cleanup_expired();
353
354 if self.seen_messages.len() >= self.config.max_tracked_messages {
355 if let Some((lru_id, _)) = self
357 .seen_messages
358 .iter()
359 .min_by_key(|(_, entry)| entry.last_accessed)
360 .map(|(id, entry)| (id.clone(), entry.clone()))
361 {
362 self.seen_messages.remove(&lru_id);
363 }
364 }
365 }
366
367 let now = Utc::now();
368 self.seen_messages.insert(
369 msg_id_str,
370 SeenEntry {
371 seen_at: now,
372 last_accessed: now,
373 },
374 );
375
376 true
377 }
378 }
379
380 pub fn unmark_seen(&mut self, message_id: &MessageId) -> bool {
396 if self.bloom_filter.is_some() {
397 return false;
398 }
399 self.seen_messages.remove(&message_id.as_str()).is_some()
400 }
401
402 pub fn cleanup_expired(&mut self) -> usize {
411 if self.bloom_filter.is_some() {
412 return 0;
414 }
415
416 let now = Utc::now();
417 let retention = chrono::Duration::seconds(self.config.retention_time_secs as i64);
418 let cutoff = now - retention;
419
420 let before_count = self.seen_messages.len();
421 self.seen_messages.retain(|_, entry| entry.seen_at > cutoff);
422 before_count - self.seen_messages.len()
423 }
424
425 pub fn tracked_count(&self) -> usize {
429 if let Some(ref bloom) = self.bloom_filter {
430 bloom.total_items()
431 } else {
432 self.seen_messages.len()
433 }
434 }
435
436 pub fn clear(&mut self) {
438 if let Some(ref mut bloom) = self.bloom_filter {
439 bloom.clear();
440 }
441 self.seen_messages.clear();
442 }
443
444 pub fn is_tracked(&self, message_id: &MessageId) -> bool {
446 self.is_duplicate(message_id)
447 }
448
449 pub fn is_bloom_filter_mode(&self) -> bool {
451 self.bloom_filter.is_some()
452 }
453
454 pub fn stats(&self) -> DeduplicatorStats {
456 if let Some(ref bloom) = self.bloom_filter {
457 let total = bloom.total_items();
458 let estimated_capacity = self.config.bloom_filter_bits / self.config.bloom_hash_count;
460 let capacity_percent =
461 ((total as f32 / estimated_capacity as f32) * 100.0).min(100.0) as u8;
462
463 DeduplicatorStats {
464 total_tracked: total,
465 recent_tracked: total, capacity_used_percent: capacity_percent,
467 false_positive_rate: Some(bloom.average_false_positive_rate()),
468 mode: DeduplicatorMode::BloomFilter,
469 }
470 } else {
471 let now = Utc::now();
472 let recent_cutoff = now - chrono::Duration::seconds(60);
473
474 let recent_count = self
475 .seen_messages
476 .values()
477 .filter(|entry| entry.seen_at > recent_cutoff)
478 .count();
479
480 DeduplicatorStats {
481 total_tracked: self.seen_messages.len(),
482 recent_tracked: recent_count,
483 capacity_used_percent: (self.seen_messages.len() as f32
484 / self.config.max_tracked_messages as f32
485 * 100.0) as u8,
486 false_positive_rate: None,
487 mode: DeduplicatorMode::HashMap,
488 }
489 }
490 }
491
492 pub fn estimated_memory_bytes(&self) -> usize {
494 if self.bloom_filter.is_some() {
495 let filter_bytes = self.config.bloom_filter_bits.div_ceil(8);
497 filter_bytes * self.config.bloom_filter_count
498 + std::mem::size_of::<RotatingBloomFilter>()
499 } else {
500 self.seen_messages.len() * 50 + std::mem::size_of::<HashMap<String, SeenEntry>>()
502 }
503 }
504}
505
506impl Default for Deduplicator {
507 fn default() -> Self {
508 Self::new()
509 }
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub enum DeduplicatorMode {
515 HashMap,
517 BloomFilter,
519}
520
521#[derive(Debug, Clone)]
523pub struct DeduplicatorStats {
524 pub total_tracked: usize,
526 pub recent_tracked: usize,
528 pub capacity_used_percent: u8,
530 pub false_positive_rate: Option<f64>,
532 pub mode: DeduplicatorMode,
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use std::thread;
540 use std::time::Duration;
541
542 fn hashmap_config() -> DeduplicatorConfig {
543 DeduplicatorConfig {
544 use_bloom_filter: false,
545 ..Default::default()
546 }
547 }
548
549 #[test]
550 fn test_default_config_is_hashmap_and_capacity_1000() {
551 let config = DeduplicatorConfig::default();
552 assert!(
553 !config.use_bloom_filter,
554 "default should be exact-match HashMap to avoid false positives"
555 );
556 assert_eq!(config.max_tracked_messages, 1000);
557
558 let dedup = Deduplicator::new();
559 assert!(!dedup.is_bloom_filter_mode());
560 let stats = dedup.stats();
561 assert_eq!(stats.mode, DeduplicatorMode::HashMap);
562 }
563
564 #[test]
565 fn test_invalid_bloom_config_falls_back_without_panicking() {
566 let config = DeduplicatorConfig {
567 use_bloom_filter: true,
568 bloom_filter_bits: 0,
569 ..Default::default()
570 };
571
572 let mut dedup = Deduplicator::with_config(config);
573 let message_id = MessageId::new();
574
575 assert!(!dedup.is_bloom_filter_mode());
576 assert!(dedup.mark_seen(message_id.clone()));
577 assert!(dedup.is_duplicate(&message_id));
578 }
579
580 #[test]
581 fn test_default_hashmap_no_false_positives() {
582 let mut dedup = Deduplicator::new();
583 let seen = MessageId::new();
584 dedup.mark_seen(seen.clone());
585
586 for _ in 0..20 {
588 let fresh = MessageId::new();
589 assert!(
590 !dedup.is_duplicate(&fresh),
591 "fresh message ID must not be false positive"
592 );
593 }
594 assert!(dedup.is_duplicate(&seen));
595 }
596
597 #[test]
598 fn test_basic_deduplication() {
599 let mut dedup = Deduplicator::new();
600 let msg_id = MessageId::new();
601
602 assert!(!dedup.is_duplicate(&msg_id));
604 assert!(dedup.mark_seen(msg_id.clone()));
605
606 assert!(dedup.is_duplicate(&msg_id));
608 assert!(!dedup.mark_seen(msg_id.clone()));
609 }
610
611 #[test]
612 fn test_unmark_seen_hashmap_mode() {
613 let mut dedup = Deduplicator::with_config(hashmap_config());
614 let msg_id = MessageId::new();
615
616 dedup.mark_seen(msg_id.clone());
617 assert!(dedup.is_duplicate(&msg_id));
618
619 assert!(dedup.unmark_seen(&msg_id), "tracked id must be removed");
620 assert!(
621 !dedup.is_duplicate(&msg_id),
622 "unmarked message must be processed fresh on replay"
623 );
624 assert!(
625 !dedup.unmark_seen(&msg_id),
626 "second unmark of the same id is a no-op"
627 );
628 }
629
630 #[test]
631 fn test_unmark_seen_noop_in_bloom_mode() {
632 let config = DeduplicatorConfig {
633 use_bloom_filter: true,
634 bloom_filter_bits: 1 << 16,
635 bloom_hash_count: 5,
636 bloom_filter_count: 2,
637 bloom_rotation_secs: 60,
638 ..Default::default()
639 };
640 let mut dedup = Deduplicator::with_config(config);
641 let msg_id = MessageId::new();
642
643 dedup.mark_seen(msg_id.clone());
644 assert!(
645 !dedup.unmark_seen(&msg_id),
646 "bloom filters cannot remove entries"
647 );
648 assert!(
649 dedup.is_duplicate(&msg_id),
650 "bloom mode keeps the id until its window rotates"
651 );
652 }
653
654 #[test]
655 fn test_basic_deduplication_hashmap_mode() {
656 let mut dedup = Deduplicator::with_config(hashmap_config());
657 let msg_id = MessageId::new();
658
659 assert!(!dedup.is_duplicate(&msg_id));
660 assert!(dedup.mark_seen(msg_id.clone()));
661
662 assert!(dedup.is_duplicate(&msg_id));
663 assert!(!dedup.mark_seen(msg_id.clone()));
664 }
665
666 #[test]
667 fn test_multiple_messages() {
668 let mut dedup = Deduplicator::new();
669
670 let msg1 = MessageId::new();
671 let msg2 = MessageId::new();
672 let msg3 = MessageId::new();
673
674 assert!(dedup.mark_seen(msg1.clone()));
675 assert!(dedup.mark_seen(msg2.clone()));
676 assert!(dedup.mark_seen(msg3.clone()));
677
678 assert_eq!(dedup.tracked_count(), 3);
679
680 assert!(dedup.is_duplicate(&msg1));
681 assert!(dedup.is_duplicate(&msg2));
682 assert!(dedup.is_duplicate(&msg3));
683 }
684
685 #[test]
686 fn test_capacity_limit() {
687 let config = DeduplicatorConfig {
689 max_tracked_messages: 3,
690 retention_time_secs: 3600,
691 use_bloom_filter: false,
692 ..Default::default()
693 };
694 let mut dedup = Deduplicator::with_config(config);
695
696 let msg1 = MessageId::new();
697 let msg2 = MessageId::new();
698 let msg3 = MessageId::new();
699 let msg4 = MessageId::new();
700
701 dedup.mark_seen(msg1.clone());
702 dedup.mark_seen(msg2.clone());
703 dedup.mark_seen(msg3.clone());
704
705 assert_eq!(dedup.tracked_count(), 3);
706
707 dedup.mark_seen(msg4.clone());
709 assert_eq!(dedup.tracked_count(), 3);
710
711 assert!(!dedup.is_duplicate(&msg1));
713 assert!(dedup.is_duplicate(&msg4));
714 }
715
716 #[test]
717 fn test_cleanup_expired() {
718 let config = DeduplicatorConfig {
720 max_tracked_messages: 100,
721 retention_time_secs: 1,
722 use_bloom_filter: false,
723 ..Default::default()
724 };
725 let mut dedup = Deduplicator::with_config(config);
726
727 let msg1 = MessageId::new();
728 let msg2 = MessageId::new();
729
730 dedup.mark_seen(msg1.clone());
731
732 thread::sleep(Duration::from_millis(500));
733
734 dedup.mark_seen(msg2.clone());
735
736 thread::sleep(Duration::from_millis(600));
737
738 let removed = dedup.cleanup_expired();
739 assert_eq!(removed, 1);
740 assert_eq!(dedup.tracked_count(), 1);
741
742 assert!(!dedup.is_duplicate(&msg1));
743 assert!(dedup.is_duplicate(&msg2));
744 }
745
746 #[test]
747 fn test_clear() {
748 let mut dedup = Deduplicator::new();
749
750 dedup.mark_seen(MessageId::new());
751 dedup.mark_seen(MessageId::new());
752 dedup.mark_seen(MessageId::new());
753
754 assert_eq!(dedup.tracked_count(), 3);
755
756 dedup.clear();
757 assert_eq!(dedup.tracked_count(), 0);
758 }
759
760 #[test]
761 fn test_is_tracked() {
762 let mut dedup = Deduplicator::new();
763 let msg_id = MessageId::new();
764
765 assert!(!dedup.is_tracked(&msg_id));
766
767 dedup.mark_seen(msg_id.clone());
768 assert!(dedup.is_tracked(&msg_id));
769 }
770
771 #[test]
772 fn test_stats() {
773 let mut dedup = Deduplicator::new();
774
775 for _ in 0..5 {
776 dedup.mark_seen(MessageId::new());
777 }
778
779 let stats = dedup.stats();
780 assert_eq!(stats.total_tracked, 5);
781 }
782
783 #[test]
784 fn test_stats_hashmap_mode() {
785 let mut dedup = Deduplicator::with_config(hashmap_config());
786
787 for _ in 0..5 {
788 dedup.mark_seen(MessageId::new());
789 }
790
791 let stats = dedup.stats();
792 assert_eq!(stats.total_tracked, 5);
793 assert_eq!(stats.recent_tracked, 5);
794 assert!(stats.capacity_used_percent < 1);
795 assert_eq!(stats.mode, DeduplicatorMode::HashMap);
796 }
797
798 #[test]
799 fn test_capacity_percent() {
800 let config = DeduplicatorConfig {
801 max_tracked_messages: 10,
802 retention_time_secs: 3600,
803 use_bloom_filter: false,
804 ..Default::default()
805 };
806 let mut dedup = Deduplicator::with_config(config);
807
808 for _ in 0..5 {
809 dedup.mark_seen(MessageId::new());
810 }
811
812 let stats = dedup.stats();
813 assert_eq!(stats.capacity_used_percent, 50);
814 }
815
816 #[test]
817 fn test_bloom_filter_mode() {
818 let config = DeduplicatorConfig {
819 use_bloom_filter: true,
820 bloom_filter_bits: 1 << 16, bloom_hash_count: 5,
822 bloom_filter_count: 2,
823 bloom_rotation_secs: 60,
824 ..Default::default()
825 };
826 let mut dedup = Deduplicator::with_config(config);
827
828 assert!(dedup.is_bloom_filter_mode());
829
830 let msg1 = MessageId::new();
831 let msg2 = MessageId::new();
832
833 assert!(!dedup.is_duplicate(&msg1));
834 dedup.mark_seen(msg1.clone());
835 assert!(dedup.is_duplicate(&msg1));
836
837 assert!(!dedup.is_duplicate(&msg2));
838 dedup.mark_seen(msg2.clone());
839 assert!(dedup.is_duplicate(&msg2));
840
841 let stats = dedup.stats();
842 assert_eq!(stats.mode, DeduplicatorMode::BloomFilter);
843 assert!(stats.false_positive_rate.is_some());
844 }
845
846 #[test]
847 fn test_bloom_filter_constant_memory() {
848 let config = DeduplicatorConfig {
849 use_bloom_filter: true,
850 bloom_filter_bits: 1 << 16,
851 bloom_hash_count: 5,
852 bloom_filter_count: 2,
853 bloom_rotation_secs: 3600,
854 ..Default::default()
855 };
856 let dedup = Deduplicator::with_config(config);
857
858 let initial_memory = dedup.estimated_memory_bytes();
859
860 let expected_filter_bytes = (1 << 16) / 8 * 2; assert!(initial_memory >= expected_filter_bytes);
864 assert!(initial_memory < expected_filter_bytes * 2); }
866
867 #[test]
868 fn test_bloom_filter_high_volume() {
869 let config = DeduplicatorConfig {
870 use_bloom_filter: true,
871 bloom_filter_bits: 1 << 20, bloom_hash_count: 7,
873 bloom_filter_count: 4,
874 bloom_rotation_secs: 3600,
875 ..Default::default()
876 };
877 let mut dedup = Deduplicator::with_config(config);
878
879 let mut inserted = 0usize;
884 for _ in 0..10_000 {
885 let msg = MessageId::new();
886 if dedup.mark_seen(msg) {
887 inserted += 1;
888 }
889 }
890
891 assert!(
892 inserted >= 9_990,
893 "too many bloom false positives: {}",
894 10_000 - inserted
895 );
896 assert_eq!(dedup.tracked_count(), inserted);
897
898 let stats = dedup.stats();
899 assert!(stats.false_positive_rate.unwrap() < 0.02);
901 }
902
903 #[test]
904 fn test_lru_eviction_preserves_recently_accessed() {
905 let config = DeduplicatorConfig {
906 max_tracked_messages: 3,
907 retention_time_secs: 3600,
908 use_bloom_filter: false,
909 ..Default::default()
910 };
911 let mut dedup = Deduplicator::with_config(config);
912
913 let msg_a = MessageId::new();
914 let msg_b = MessageId::new();
915 let msg_c = MessageId::new();
916
917 dedup.mark_seen(msg_a.clone());
918 thread::sleep(Duration::from_millis(10));
919 dedup.mark_seen(msg_b.clone());
920 thread::sleep(Duration::from_millis(10));
921 dedup.mark_seen(msg_c.clone());
922
923 assert!(dedup.is_duplicate_mut(&msg_a));
925
926 let msg_d = MessageId::new();
928 dedup.mark_seen(msg_d.clone());
929
930 assert_eq!(dedup.tracked_count(), 3);
931 assert!(
932 dedup.is_duplicate(&msg_a),
933 "A was recently accessed, should survive"
934 );
935 assert!(
936 !dedup.is_duplicate(&msg_b),
937 "B should have been evicted (LRU)"
938 );
939 assert!(dedup.is_duplicate(&msg_c), "C should survive");
940 assert!(dedup.is_duplicate(&msg_d), "D was just inserted");
941 }
942}