1use std::collections::HashMap;
7use std::collections::HashSet;
8use std::collections::VecDeque;
9use std::net::{IpAddr, SocketAddr};
10use std::time::{Duration, Instant};
11use tracing::debug;
12
13use crate::spvd_decode::StructureDesc;
14
15#[derive(Debug, Clone)]
17pub struct PvaStateConfig {
18 pub max_channels: usize,
20 pub channel_ttl: Duration,
22 pub max_operations: usize,
24 pub max_update_rate: usize,
26 pub max_memory_bytes: usize,
33 pub max_search_cache_entries: usize,
36}
37
38impl Default for PvaStateConfig {
39 fn default() -> Self {
40 Self {
41 max_channels: 40_000,
42 channel_ttl: Duration::from_secs(5 * 60), max_operations: 10_000,
44 max_update_rate: 10_000,
45 max_memory_bytes: DEFAULT_MAX_MEMORY_BYTES,
46 max_search_cache_entries: DEFAULT_MAX_SEARCH_CACHE_ENTRIES,
47 }
48 }
49}
50
51pub const DEFAULT_MAX_MEMORY_BYTES: usize = 1024 * 1024 * 1024;
54pub const DEFAULT_MAX_SEARCH_CACHE_ENTRIES: usize = 200_000;
56
57impl PvaStateConfig {
58 pub fn new(max_channels: usize, ttl_secs: u64) -> Self {
59 Self {
60 max_channels,
61 channel_ttl: Duration::from_secs(ttl_secs),
62 max_operations: 10_000,
63 max_update_rate: 10_000,
64 max_memory_bytes: DEFAULT_MAX_MEMORY_BYTES,
65 max_search_cache_entries: DEFAULT_MAX_SEARCH_CACHE_ENTRIES,
66 }
67 }
68
69 pub fn with_max_update_rate(mut self, max_update_rate: usize) -> Self {
70 self.max_update_rate = max_update_rate;
71 self
72 }
73
74 pub fn with_max_memory_bytes(mut self, max_memory_bytes: usize) -> Self {
76 self.max_memory_bytes = max_memory_bytes;
77 self
78 }
79
80 pub fn with_max_operations(mut self, max_operations: usize) -> Self {
82 self.max_operations = max_operations;
83 self
84 }
85}
86
87fn ring_bytes(ring: &VecDeque<String>) -> usize {
89 ring.capacity() * std::mem::size_of::<String>()
90 + ring.iter().map(String::capacity).sum::<usize>()
91}
92
93fn instants_bytes(times: &VecDeque<Instant>) -> usize {
95 times.capacity() * std::mem::size_of::<Instant>()
96}
97
98fn table_bytes<K, V>(capacity: usize) -> usize {
101 capacity * (std::mem::size_of::<K>() + std::mem::size_of::<V>() + 1)
102}
103
104#[derive(Debug, Clone, Hash, PartialEq, Eq)]
106pub struct ConnectionKey {
107 pub addr_a: SocketAddr,
109 pub addr_b: SocketAddr,
111}
112
113impl ConnectionKey {
114 pub fn new(addr1: SocketAddr, addr2: SocketAddr) -> Self {
116 if addr1 <= addr2 {
118 Self {
119 addr_a: addr1,
120 addr_b: addr2,
121 }
122 } else {
123 Self {
124 addr_a: addr2,
125 addr_b: addr1,
126 }
127 }
128 }
129
130 pub fn from_parts(ip1: &str, port1: u16, ip2: &str, port2: u16) -> Option<Self> {
133 let addr1: SocketAddr = format!("{}:{}", ip1, port1).parse().ok()?;
134 let addr2: SocketAddr = format!("{}:{}", ip2, port2).parse().ok()?;
135 Some(Self::new(addr1, addr2))
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct ChannelInfo {
142 pub pv_name: String,
144 pub cid: u32,
146 pub sid: Option<u32>,
148 pub last_seen: Instant,
150 pub fully_established: bool,
152 pub update_times: VecDeque<Instant>,
153 pub recent_messages: VecDeque<String>,
154}
155
156impl ChannelInfo {
157 pub fn new_pending(cid: u32, pv_name: String) -> Self {
158 Self {
159 pv_name,
160 cid,
161 sid: None,
162 last_seen: Instant::now(),
163 fully_established: false,
164 update_times: VecDeque::new(),
165 recent_messages: VecDeque::new(),
166 }
167 }
168
169 pub fn touch(&mut self) {
170 self.last_seen = Instant::now();
171 }
172
173 pub fn is_expired(&self, ttl: Duration) -> bool {
174 self.last_seen.elapsed() > ttl
175 }
176
177 pub fn heap_bytes(&self) -> usize {
180 self.pv_name.capacity()
181 + instants_bytes(&self.update_times)
182 + ring_bytes(&self.recent_messages)
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct OperationState {
189 pub sid: u32,
191 pub ioid: u32,
193 pub command: u8,
195 pub pv_name: Option<String>,
197 pub field_desc: Option<StructureDesc>,
199 field_desc_bytes: usize,
205 pub initialized: bool,
207 pub last_seen: Instant,
209 pub update_times: VecDeque<Instant>,
210 pub recent_messages: VecDeque<String>,
211}
212
213impl OperationState {
214 pub fn new(sid: u32, ioid: u32, command: u8, pv_name: Option<String>) -> Self {
215 Self {
216 sid,
217 ioid,
218 command,
219 pv_name,
220 field_desc: None,
221 field_desc_bytes: 0,
222 initialized: false,
223 last_seen: Instant::now(),
224 update_times: VecDeque::new(),
225 recent_messages: VecDeque::new(),
226 }
227 }
228
229 pub fn touch(&mut self) {
230 self.last_seen = Instant::now();
231 }
232
233 pub fn is_expired(&self, ttl: Duration) -> bool {
234 self.last_seen.elapsed() > ttl
235 }
236
237 pub fn set_field_desc(&mut self, field_desc: Option<StructureDesc>) {
239 self.field_desc_bytes = field_desc.as_ref().map_or(0, |d| d.heap_size());
240 self.field_desc = field_desc;
241 }
242
243 pub fn field_desc_bytes(&self) -> usize {
245 self.field_desc_bytes
246 }
247
248 pub fn is_placeholder(&self) -> bool {
252 !self.initialized && self.field_desc.is_none()
253 }
254
255 pub fn heap_bytes(&self) -> usize {
257 self.pv_name.as_ref().map_or(0, |s| s.capacity())
258 + self.field_desc_bytes
259 + instants_bytes(&self.update_times)
260 + ring_bytes(&self.recent_messages)
261 }
262}
263
264#[derive(Debug)]
266pub struct ConnectionState {
267 pub channels_by_cid: HashMap<u32, ChannelInfo>,
269 pub sid_to_cid: HashMap<u32, u32>,
271 pub operations: HashMap<u32, OperationState>,
273 pub is_be: bool,
275 pub last_seen: Instant,
277 pub update_times: VecDeque<Instant>,
278 pub recent_messages: VecDeque<String>,
279}
280
281impl ConnectionState {
282 pub fn new() -> Self {
283 Self {
284 channels_by_cid: HashMap::new(),
285 sid_to_cid: HashMap::new(),
286 operations: HashMap::new(),
287 is_be: false, last_seen: Instant::now(),
289 update_times: VecDeque::new(),
290 recent_messages: VecDeque::new(),
291 }
292 }
293
294 pub fn touch(&mut self) {
295 self.last_seen = Instant::now();
296 }
297
298 pub fn get_channel_by_sid(&self, sid: u32) -> Option<&ChannelInfo> {
300 self.sid_to_cid
301 .get(&sid)
302 .and_then(|cid| self.channels_by_cid.get(cid))
303 }
304
305 pub fn get_channel_by_sid_mut(&mut self, sid: u32) -> Option<&mut ChannelInfo> {
307 if let Some(&cid) = self.sid_to_cid.get(&sid) {
308 self.channels_by_cid.get_mut(&cid)
309 } else {
310 None
311 }
312 }
313
314 pub fn get_pv_name_by_sid(&self, sid: u32) -> Option<&str> {
316 self.get_channel_by_sid(sid).map(|ch| ch.pv_name.as_str())
317 }
318
319 pub fn get_pv_name_by_ioid(&self, ioid: u32) -> Option<&str> {
321 self.operations
322 .get(&ioid)
323 .and_then(|op| op.pv_name.as_deref())
324 }
325
326 pub fn heap_bytes(&self) -> usize {
329 table_bytes::<u32, ChannelInfo>(self.channels_by_cid.capacity())
330 + table_bytes::<u32, OperationState>(self.operations.capacity())
331 + table_bytes::<u32, u32>(self.sid_to_cid.capacity())
332 + self
333 .channels_by_cid
334 .values()
335 .map(ChannelInfo::heap_bytes)
336 .sum::<usize>()
337 + self
338 .operations
339 .values()
340 .map(OperationState::heap_bytes)
341 .sum::<usize>()
342 + instants_bytes(&self.update_times)
343 + ring_bytes(&self.recent_messages)
344 }
345}
346
347impl Default for ConnectionState {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353#[derive(Debug)]
355pub struct PvaStateTracker {
356 config: PvaStateConfig,
358 connections: HashMap<ConnectionKey, ConnectionState>,
360 total_channels: usize,
362 pub stats: PvaStateStats,
364 search_cache: HashMap<(IpAddr, u32), String>,
367 search_cache_flat: HashMap<u32, String>,
369}
370
371#[derive(Debug, Default, Clone)]
373pub struct PvaStateStats {
374 pub channels_created: u64,
375 pub channels_destroyed: u64,
376 pub channels_expired: u64,
377 pub channels_evicted: u64,
378 pub operations_created: u64,
379 pub operations_completed: u64,
380 pub create_channel_requests: u64,
381 pub create_channel_responses: u64,
382 pub search_responses_resolved: u64,
383 pub search_cache_entries: u64,
384 pub search_retroactive_resolves: u64,
385 pub client_messages: u64,
387 pub server_messages: u64,
389 pub memory_bytes: u64,
392 pub operations_expired: u64,
395 pub shed_placeholder_ops: u64,
397 pub shed_message_rings: u64,
399 pub shed_search_entries: u64,
401 pub shed_channels: u64,
404}
405
406#[derive(Debug, Clone)]
407pub struct ConnectionSnapshot {
408 pub addr_a: SocketAddr,
409 pub addr_b: SocketAddr,
410 pub channel_count: usize,
411 pub operation_count: usize,
412 pub last_seen: Duration,
413 pub pv_names: Vec<String>,
414 pub updates_per_sec: f64,
415 pub recent_messages: Vec<String>,
416 pub mid_stream: bool,
417 pub is_beacon: bool,
418 pub is_broadcast: bool,
419}
420
421#[derive(Debug, Clone)]
422pub struct ChannelSnapshot {
423 pub addr_a: SocketAddr,
424 pub addr_b: SocketAddr,
425 pub cid: u32,
426 pub sid: Option<u32>,
427 pub pv_name: String,
428 pub last_seen: Duration,
429 pub updates_per_sec: f64,
430 pub recent_messages: Vec<String>,
431 pub mid_stream: bool,
432 pub is_beacon: bool,
433 pub is_broadcast: bool,
434}
435
436impl PvaStateTracker {
437 fn is_broadcast_addr(addr: &SocketAddr) -> bool {
438 match addr.ip() {
439 std::net::IpAddr::V4(v4) => {
440 if v4.is_broadcast() {
441 return true;
442 }
443 v4.octets()[3] == 255
444 }
445 std::net::IpAddr::V6(v6) => {
446 v6.is_multicast()
448 }
449 }
450 }
451 pub fn new(config: PvaStateConfig) -> Self {
452 Self {
453 config,
454 connections: HashMap::new(),
455 total_channels: 0,
456 stats: PvaStateStats::default(),
457 search_cache: HashMap::new(),
458 search_cache_flat: HashMap::new(),
459 }
460 }
461
462 pub fn with_defaults() -> Self {
463 Self::new(PvaStateConfig::default())
464 }
465
466 fn get_or_create_connection(&mut self, key: &ConnectionKey) -> &mut ConnectionState {
468 if !self.connections.contains_key(key) {
469 self.connections.insert(key.clone(), ConnectionState::new());
470 }
471 self.connections.get_mut(key).unwrap()
472 }
473
474 pub fn get_connection(&self, key: &ConnectionKey) -> Option<&ConnectionState> {
476 self.connections.get(key)
477 }
478
479 pub fn get_pv_name_by_sid(&self, conn_key: &ConnectionKey, sid: u32) -> Option<String> {
481 self.connections
482 .get(conn_key)
483 .and_then(|conn| conn.get_pv_name_by_sid(sid))
484 .map(|s| s.to_string())
485 }
486
487 pub fn on_create_channel_request(
490 &mut self,
491 conn_key: &ConnectionKey,
492 cid: u32,
493 pv_name: String,
494 ) {
495 self.stats.create_channel_requests += 1;
496
497 let client_ip = conn_key.addr_a.ip(); self.search_cache.insert((client_ip, cid), pv_name.clone());
501 self.search_cache_flat.insert(cid, pv_name.clone());
502
503 if self.total_channels >= self.config.max_channels {
505 self.evict_oldest_channels(100); }
507
508 let conn = self.get_or_create_connection(conn_key);
509 conn.touch();
510
511 if !conn.channels_by_cid.contains_key(&cid) {
513 conn.channels_by_cid
514 .insert(cid, ChannelInfo::new_pending(cid, pv_name));
515 self.total_channels += 1;
516 self.stats.channels_created += 1;
517 debug!("CREATE_CHANNEL request: cid={}", cid);
518 }
519 }
520
521 pub fn on_create_channel_response(&mut self, conn_key: &ConnectionKey, cid: u32, sid: u32) {
524 self.stats.create_channel_responses += 1;
525
526 let cached_pv_name = self
529 .search_cache
530 .get(&(conn_key.addr_a.ip(), cid))
531 .or_else(|| self.search_cache.get(&(conn_key.addr_b.ip(), cid)))
532 .or_else(|| self.search_cache_flat.get(&cid))
533 .cloned();
534
535 let conn = self.get_or_create_connection(conn_key);
536 conn.touch();
537
538 if let Some(channel) = conn.channels_by_cid.get_mut(&cid) {
539 channel.sid = Some(sid);
540 channel.fully_established = true;
541 channel.touch();
542 conn.sid_to_cid.insert(sid, cid);
543 debug!(
544 "CREATE_CHANNEL response: cid={}, sid={}, pv={}",
545 cid, sid, channel.pv_name
546 );
547 } else {
548 let pv_name = cached_pv_name.unwrap_or_else(|| format!("<unknown:cid={}>", cid));
550 let is_resolved = !pv_name.starts_with("<unknown");
551 debug!(
552 "CREATE_CHANNEL response without request: cid={}, sid={}, resolved={}",
553 cid, sid, is_resolved
554 );
555 let mut channel = ChannelInfo::new_pending(cid, pv_name);
556 channel.sid = Some(sid);
557 channel.fully_established = is_resolved;
558 conn.channels_by_cid.insert(cid, channel);
559 conn.sid_to_cid.insert(sid, cid);
560 self.total_channels += 1;
561 }
562 }
563
564 pub fn on_destroy_channel(&mut self, conn_key: &ConnectionKey, cid: u32, sid: u32) {
566 if let Some(conn) = self.connections.get_mut(conn_key) {
567 conn.touch();
568
569 if conn.channels_by_cid.remove(&cid).is_some() {
571 self.total_channels = self.total_channels.saturating_sub(1);
572 self.stats.channels_destroyed += 1;
573 }
574
575 conn.sid_to_cid.remove(&sid);
577
578 conn.operations.retain(|_, op| op.sid != sid);
580
581 debug!("DESTROY_CHANNEL: cid={}, sid={}", cid, sid);
582 }
583 }
584
585 pub fn on_op_init_request(
588 &mut self,
589 conn_key: &ConnectionKey,
590 sid: u32,
591 ioid: u32,
592 command: u8,
593 ) {
594 let max_ops = self.config.max_operations;
595 let conn = self.get_or_create_connection(conn_key);
596 conn.touch();
597
598 let pv_name = conn.get_pv_name_by_sid(sid).map(|s| s.to_string());
599
600 if conn.operations.len() < max_ops {
601 conn.operations
602 .insert(ioid, OperationState::new(sid, ioid, command, pv_name));
603 self.stats.operations_created += 1;
604 debug!(
605 "Operation INIT: sid={}, ioid={}, cmd={}",
606 sid, ioid, command
607 );
608 }
609 }
610
611 pub fn on_op_init_response(
614 &mut self,
615 conn_key: &ConnectionKey,
616 ioid: u32,
617 field_desc: Option<StructureDesc>,
618 ) {
619 if let Some(conn) = self.connections.get_mut(conn_key) {
620 conn.touch();
621
622 if let Some(op) = conn.operations.get_mut(&ioid) {
623 op.set_field_desc(field_desc);
624 op.initialized = true;
625 op.touch();
626 debug!("Operation INIT response: ioid={}", ioid);
627 }
628 }
629 }
630
631 pub fn on_op_destroy(&mut self, conn_key: &ConnectionKey, ioid: u32) {
633 if let Some(conn) = self.connections.get_mut(conn_key) {
634 if conn.operations.remove(&ioid).is_some() {
635 self.stats.operations_completed += 1;
636 }
637 }
638 }
639
640 pub fn on_op_activity(&mut self, conn_key: &ConnectionKey, sid: u32, ioid: u32, command: u8) {
644 let max_update_rate = self.config.max_update_rate;
645 let max_ops = self.config.max_operations;
646 let mut created_placeholder = false;
647
648 let conn = self.get_or_create_connection(conn_key);
649 conn.touch();
650
651 Self::record_update(&mut conn.update_times, max_update_rate);
652
653 let mut channel_sid = if sid != 0 { Some(sid) } else { None };
654 if let Some(op) = conn.operations.get_mut(&ioid) {
655 op.touch();
656 Self::record_update(&mut op.update_times, max_update_rate);
657 if channel_sid.is_none() {
658 channel_sid = Some(op.sid);
659 }
660 } else if conn.operations.len() < max_ops {
661 let pv_name = if sid != 0 {
664 conn.get_pv_name_by_sid(sid).map(|s| s.to_string())
665 } else if conn.channels_by_cid.len() == 1 && conn.operations.is_empty() {
666 conn.channels_by_cid
671 .values()
672 .next()
673 .map(|ch| ch.pv_name.clone())
674 .filter(|n| !n.starts_with("<unknown"))
675 } else {
676 None
677 };
678 conn.operations
679 .insert(ioid, OperationState::new(sid, ioid, command, pv_name));
680 created_placeholder = true;
681 }
682
683 if let Some(sid_val) = channel_sid {
684 if let Some(channel) = conn.get_channel_by_sid_mut(sid_val) {
685 channel.touch();
686 Self::record_update(&mut channel.update_times, max_update_rate);
687 }
688 }
689
690 if created_placeholder {
692 self.stats.operations_created += 1;
693 debug!(
694 "Auto-created placeholder operation for mid-stream traffic: sid={}, ioid={}, cmd={}",
695 sid, ioid, command
696 );
697 }
698 }
699
700 pub fn on_search(&mut self, pv_requests: &[(u32, String)], source_ip: Option<IpAddr>) {
706 let cid_to_pv: HashMap<u32, String> = pv_requests.iter().cloned().collect();
708
709 for (cid, pv_name) in pv_requests {
710 if let Some(ip) = source_ip {
711 self.search_cache.insert((ip, *cid), pv_name.clone());
712 }
713 self.search_cache_flat.insert(*cid, pv_name.clone());
715 }
716
717 let mut retroactive_count: u64 = 0;
721 for conn in self.connections.values_mut() {
722 for (cid, channel) in conn.channels_by_cid.iter_mut() {
723 if channel.pv_name.starts_with("<unknown") {
724 if let Some(pv_name) = cid_to_pv.get(cid) {
725 debug!(
726 "Retroactive PV resolve from SEARCH: cid={} {} -> {}",
727 cid, channel.pv_name, pv_name
728 );
729 channel.pv_name = pv_name.clone();
730 channel.fully_established = true;
731 retroactive_count += 1;
732 }
733 }
734 }
735
736 for op in conn.operations.values_mut() {
739 let needs_update = match &op.pv_name {
740 None => true,
741 Some(name) => name.starts_with("<unknown"),
742 };
743 if needs_update && op.sid != 0 {
744 if let Some(&cid) = conn.sid_to_cid.get(&op.sid) {
745 if let Some(pv_name) = cid_to_pv.get(&cid) {
746 op.pv_name = Some(pv_name.clone());
747 }
748 }
749 }
750 }
751 }
752 if retroactive_count > 0 {
753 self.stats.search_retroactive_resolves += retroactive_count;
754 debug!(
755 "Retroactively resolved {} unknown channels from SEARCH cache",
756 retroactive_count
757 );
758 }
759
760 self.stats.search_cache_entries = self.search_cache_flat.len() as u64;
762
763 while self.search_cache.len() > 50_000 {
765 if let Some(key) = self.search_cache.keys().next().cloned() {
766 self.search_cache.remove(&key);
767 }
768 }
769 while self.search_cache_flat.len() > 50_000 {
770 if let Some(key) = self.search_cache_flat.keys().next().cloned() {
771 self.search_cache_flat.remove(&key);
772 }
773 }
774 }
775
776 pub fn resolve_search_cids(
781 &mut self,
782 cids: &[u32],
783 peer_ip: Option<IpAddr>,
784 ) -> Vec<(u32, String)> {
785 let mut resolved = Vec::new();
786 for &cid in cids {
787 let pv_name = peer_ip
790 .and_then(|ip| self.search_cache.get(&(ip, cid)))
791 .or_else(|| self.search_cache_flat.get(&cid))
792 .cloned();
793 if let Some(name) = pv_name {
794 resolved.push((cid, name));
795 self.stats.search_responses_resolved += 1;
796 }
797 }
798 resolved
799 }
800
801 pub fn count_direction(&mut self, is_server: bool) {
803 if is_server {
804 self.stats.server_messages += 1;
805 } else {
806 self.stats.client_messages += 1;
807 }
808 }
809
810 pub fn on_message(
811 &mut self,
812 conn_key: &ConnectionKey,
813 sid: u32,
814 ioid: u32,
815 request_type: &str,
816 message: String,
817 is_server: bool,
818 ) {
819 let conn = self.get_or_create_connection(conn_key);
820 conn.touch();
821 let dir = if is_server { "S>" } else { "C>" };
822 let full_message = format!("{} {} {}", dir, request_type, message);
823 Self::push_message(&mut conn.recent_messages, full_message.clone());
824
825 let mut channel_sid = if sid != 0 { Some(sid) } else { None };
826 if let Some(op) = conn.operations.get_mut(&ioid) {
827 Self::push_message(&mut op.recent_messages, full_message.clone());
828 if channel_sid.is_none() {
829 channel_sid = Some(op.sid);
830 }
831 }
832 if let Some(sid_val) = channel_sid {
833 if let Some(channel) = conn.get_channel_by_sid_mut(sid_val) {
834 Self::push_message(&mut channel.recent_messages, full_message);
835 }
836 }
837 }
838
839 fn record_update(times: &mut VecDeque<Instant>, max_update_rate: usize) {
840 let now = Instant::now();
841 times.push_back(now);
842 Self::trim_times(times, now);
843 while times.len() > max_update_rate {
844 times.pop_front();
845 }
846 }
847
848 fn trim_times(times: &mut VecDeque<Instant>, now: Instant) {
849 while let Some(front) = times.front() {
850 if now.duration_since(*front) > Duration::from_secs(1) {
851 times.pop_front();
852 } else {
853 break;
854 }
855 }
856 }
857
858 fn updates_per_sec(times: &VecDeque<Instant>) -> f64 {
859 times.len() as f64
860 }
861
862 fn push_message(messages: &mut VecDeque<String>, message: String) {
863 messages.push_back(message);
864 while messages.len() > 30 {
865 messages.pop_front();
866 }
867 }
868
869 pub fn resolve_pv_name(&self, conn_key: &ConnectionKey, sid: u32, ioid: u32) -> Option<String> {
871 let conn = self.connections.get(conn_key)?;
872
873 if let Some(op) = conn.operations.get(&ioid) {
875 if let Some(ref name) = op.pv_name {
876 if !name.starts_with("<unknown") {
877 return Some(name.clone());
878 }
879 }
880 }
881
882 if sid != 0 {
884 if let Some(name) = conn.get_pv_name_by_sid(sid) {
885 return Some(name.to_string());
886 }
887 }
888
889 if conn.channels_by_cid.len() == 1 && conn.operations.len() <= 1 {
899 if let Some(ch) = conn.channels_by_cid.values().next() {
900 if !ch.pv_name.starts_with("<unknown") {
901 return Some(ch.pv_name.clone());
902 }
903 }
904 }
905
906 None
907 }
908
909 pub fn active_channel_count(&self) -> usize {
911 self.total_channels
912 }
913
914 pub fn active_connection_count(&self) -> usize {
916 self.connections.len()
917 }
918
919 pub fn is_connection_mid_stream(&self, conn_key: &ConnectionKey) -> bool {
921 self.connections
922 .get(conn_key)
923 .map(|conn| {
924 if conn.channels_by_cid.is_empty() && !conn.operations.is_empty() {
926 return true;
927 }
928 conn.channels_by_cid
930 .values()
931 .any(|ch| !ch.fully_established)
932 })
933 .unwrap_or(false)
934 }
935
936 pub fn get_operation(&self, conn_key: &ConnectionKey, ioid: u32) -> Option<&OperationState> {
938 self.connections
939 .get(conn_key)
940 .and_then(|conn| conn.operations.get(&ioid))
941 }
942
943 fn evict_oldest_channels(&mut self, count: usize) -> usize {
952 let mut oldest: Vec<(ConnectionKey, u32, Instant)> = Vec::new();
953
954 for (conn_key, conn) in &self.connections {
955 for (cid, channel) in &conn.channels_by_cid {
956 oldest.push((conn_key.clone(), *cid, channel.last_seen));
957 }
958 }
959
960 oldest.sort_by_key(|(_, _, t)| *t);
962
963 let mut evicted = 0;
965 for (conn_key, cid, _) in oldest.into_iter().take(count) {
966 if let Some(conn) = self.connections.get_mut(&conn_key) {
967 if let Some(channel) = conn.channels_by_cid.remove(&cid) {
968 if let Some(sid) = channel.sid {
969 conn.sid_to_cid.remove(&sid);
970 conn.operations.retain(|_, op| op.sid != sid);
971 }
972 self.total_channels = self.total_channels.saturating_sub(1);
973 self.stats.channels_evicted += 1;
974 evicted += 1;
975 }
976 }
977 }
978 evicted
979 }
980
981 pub fn cleanup_expired(&mut self) {
983 let ttl = self.config.channel_ttl;
984 let mut expired_count = 0;
985
986 for conn in self.connections.values_mut() {
987 let expired_cids: Vec<u32> = conn
988 .channels_by_cid
989 .iter()
990 .filter(|(_, ch)| ch.is_expired(ttl))
991 .map(|(cid, _)| *cid)
992 .collect();
993
994 for cid in expired_cids {
995 if let Some(channel) = conn.channels_by_cid.remove(&cid) {
996 if let Some(sid) = channel.sid {
997 conn.sid_to_cid.remove(&sid);
998 conn.operations.retain(|_, op| op.sid != sid);
999 }
1000 expired_count += 1;
1001 }
1002 }
1003 }
1004
1005 let mut expired_ops: u64 = 0;
1013 for conn in self.connections.values_mut() {
1014 let before = conn.operations.len();
1015 conn.operations.retain(|_, op| !op.is_expired(ttl));
1016 expired_ops += (before - conn.operations.len()) as u64;
1017 }
1018 if expired_ops > 0 {
1019 self.stats.operations_expired += expired_ops;
1020 debug!("Cleaned up {} expired operations", expired_ops);
1021 }
1022
1023 if expired_count > 0 {
1024 self.total_channels = self.total_channels.saturating_sub(expired_count);
1025 self.stats.channels_expired += expired_count as u64;
1026 debug!("Cleaned up {} expired channels", expired_count);
1027 }
1028
1029 self.connections
1031 .retain(|_, conn| !conn.channels_by_cid.is_empty() || !conn.operations.is_empty());
1032
1033 self.enforce_search_cache_cap();
1034 self.shed_to_budget();
1035 self.stats.memory_bytes = self.memory_estimate() as u64;
1036 }
1037
1038 pub fn memory_estimate(&self) -> usize {
1046 let conns = table_bytes::<ConnectionKey, ConnectionState>(self.connections.capacity())
1047 + self
1048 .connections
1049 .values()
1050 .map(ConnectionState::heap_bytes)
1051 .sum::<usize>();
1052 let search = table_bytes::<(IpAddr, u32), String>(self.search_cache.capacity())
1053 + self
1054 .search_cache
1055 .values()
1056 .map(String::capacity)
1057 .sum::<usize>()
1058 + table_bytes::<u32, String>(self.search_cache_flat.capacity())
1059 + self
1060 .search_cache_flat
1061 .values()
1062 .map(String::capacity)
1063 .sum::<usize>();
1064 conns + search
1065 }
1066
1067 fn shed_to_budget(&mut self) {
1076 let budget = self.config.max_memory_bytes;
1077 if budget == 0 {
1078 return;
1079 }
1080 let mut est = self.memory_estimate();
1081 if est <= budget {
1082 return;
1083 }
1084
1085 if self.shed_placeholder_ops(est - budget) > 0 {
1087 self.shrink_containers();
1088 est = self.memory_estimate();
1089 if est <= budget {
1090 return;
1091 }
1092 }
1093
1094 if self.shed_message_rings(est - budget) > 0 {
1097 est = self.memory_estimate();
1098 if est <= budget {
1099 return;
1100 }
1101 }
1102
1103 if self.shed_search_caches() > 0 {
1105 est = self.memory_estimate();
1106 if est <= budget {
1107 return;
1108 }
1109 }
1110
1111 let mut guard = 0;
1115 while est > budget && self.total_channels > 0 && guard < 16 {
1116 let per_channel = (est / self.total_channels.max(1)).max(1);
1117 let batch = (((est - budget) / per_channel) + 1).clamp(100, self.total_channels);
1118 let evicted = self.evict_oldest_channels(batch);
1119 if evicted == 0 {
1120 break;
1121 }
1122 self.stats.shed_channels += evicted as u64;
1123 self.shrink_containers();
1124 est = self.memory_estimate();
1125 guard += 1;
1126 }
1127 }
1128
1129 fn shrink_containers(&mut self) {
1139 for conn in self.connections.values_mut() {
1140 conn.operations.shrink_to_fit();
1141 conn.channels_by_cid.shrink_to_fit();
1142 conn.sid_to_cid.shrink_to_fit();
1143 }
1144 self.connections.shrink_to_fit();
1145 }
1146
1147 fn shed_placeholder_ops(&mut self, target: usize) -> usize {
1150 let mut cands: Vec<(ConnectionKey, u32, Instant, usize)> = Vec::new();
1151 for (ck, conn) in &self.connections {
1152 for (ioid, op) in &conn.operations {
1153 if op.is_placeholder() {
1154 cands.push((
1155 ck.clone(),
1156 *ioid,
1157 op.last_seen,
1158 op.heap_bytes() + std::mem::size_of::<OperationState>(),
1159 ));
1160 }
1161 }
1162 }
1163 cands.sort_by_key(|(_, _, t, _)| *t);
1164
1165 let mut freed = 0;
1166 for (ck, ioid, _, bytes) in cands {
1167 if freed >= target {
1168 break;
1169 }
1170 let removed = self
1171 .connections
1172 .get_mut(&ck)
1173 .and_then(|conn| conn.operations.remove(&ioid));
1174 if removed.is_some() {
1175 freed += bytes;
1176 self.stats.shed_placeholder_ops += 1;
1177 }
1178 }
1179 freed
1180 }
1181
1182 fn shed_message_rings(&mut self, target: usize) -> usize {
1185 let mut cands: Vec<(ConnectionKey, u32, Instant, usize)> = Vec::new();
1186 for (ck, conn) in &self.connections {
1187 for (ioid, op) in &conn.operations {
1188 let bytes = ring_bytes(&op.recent_messages);
1189 if bytes > 0 {
1190 cands.push((ck.clone(), *ioid, op.last_seen, bytes));
1191 }
1192 }
1193 }
1194 cands.sort_by_key(|(_, _, t, _)| *t);
1195
1196 let mut freed = 0;
1197 for (ck, ioid, _, bytes) in cands {
1198 if freed >= target {
1199 break;
1200 }
1201 if let Some(op) = self
1202 .connections
1203 .get_mut(&ck)
1204 .and_then(|conn| conn.operations.get_mut(&ioid))
1205 {
1206 op.recent_messages = VecDeque::new();
1209 freed += bytes;
1210 self.stats.shed_message_rings += 1;
1211 }
1212 }
1213 if freed >= target {
1214 return freed;
1215 }
1216
1217 for conn in self.connections.values_mut() {
1218 if freed >= target {
1219 break;
1220 }
1221 let bytes = ring_bytes(&conn.recent_messages);
1222 if bytes > 0 {
1223 conn.recent_messages = VecDeque::new();
1224 freed += bytes;
1225 self.stats.shed_message_rings += 1;
1226 }
1227 }
1228 freed
1229 }
1230
1231 fn shed_search_caches(&mut self) -> usize {
1237 let freed = table_bytes::<(IpAddr, u32), String>(self.search_cache.capacity())
1238 + self
1239 .search_cache
1240 .values()
1241 .map(String::capacity)
1242 .sum::<usize>()
1243 + table_bytes::<u32, String>(self.search_cache_flat.capacity())
1244 + self
1245 .search_cache_flat
1246 .values()
1247 .map(String::capacity)
1248 .sum::<usize>();
1249 let dropped = (self.search_cache.len() + self.search_cache_flat.len()) as u64;
1250 self.search_cache = HashMap::new();
1251 self.search_cache_flat = HashMap::new();
1252 self.stats.shed_search_entries += dropped;
1253 freed
1254 }
1255
1256 fn enforce_search_cache_cap(&mut self) {
1259 let cap = self.config.max_search_cache_entries;
1260 if cap == 0 {
1261 return;
1262 }
1263 if self.search_cache.len() > cap || self.search_cache_flat.len() > cap {
1264 self.shed_search_caches();
1265 }
1266 }
1267
1268 pub fn summary(&self) -> String {
1270 format!(
1271 "PVA State: {} connections, {} channels (created={}, destroyed={}, expired={}, evicted={})",
1272 self.connections.len(),
1273 self.total_channels,
1274 self.stats.channels_created,
1275 self.stats.channels_destroyed,
1276 self.stats.channels_expired,
1277 self.stats.channels_evicted,
1278 )
1279 }
1280
1281 pub fn channel_count(&self) -> usize {
1283 self.total_channels
1284 }
1285
1286 pub fn connection_count(&self) -> usize {
1288 self.connections.len()
1289 }
1290
1291 pub fn connection_snapshots(&self) -> Vec<ConnectionSnapshot> {
1292 let mut snapshots = Vec::new();
1293 let now = Instant::now();
1294 for (conn_key, conn) in &self.connections {
1295 let mut update_times = conn.update_times.clone();
1296 Self::trim_times(&mut update_times, now);
1297 let mut pv_names: Vec<String> = conn
1298 .channels_by_cid
1299 .values()
1300 .map(|ch| ch.pv_name.clone())
1301 .collect();
1302 pv_names.sort();
1303 pv_names.truncate(8);
1304 let mut messages: Vec<String> = conn.recent_messages.iter().cloned().collect();
1305 if messages.len() > 20 {
1306 messages = messages.split_off(messages.len() - 20);
1307 }
1308 let is_beacon = messages.iter().any(|m| m.starts_with("BEACON "));
1309 let is_broadcast = Self::is_broadcast_addr(&conn_key.addr_a)
1310 || Self::is_broadcast_addr(&conn_key.addr_b);
1311 let mut mid_stream = false;
1312 if conn.channels_by_cid.is_empty() && !conn.operations.is_empty() {
1313 mid_stream = true;
1314 }
1315 if conn
1316 .channels_by_cid
1317 .values()
1318 .any(|ch| !ch.fully_established || ch.pv_name.starts_with("<unknown"))
1319 {
1320 mid_stream = true;
1321 }
1322
1323 snapshots.push(ConnectionSnapshot {
1324 addr_a: conn_key.addr_a,
1325 addr_b: conn_key.addr_b,
1326 channel_count: conn.channels_by_cid.len(),
1327 operation_count: conn.operations.len(),
1328 last_seen: conn.last_seen.elapsed(),
1329 pv_names,
1330 updates_per_sec: Self::updates_per_sec(&update_times),
1331 recent_messages: messages,
1332 mid_stream,
1333 is_beacon,
1334 is_broadcast,
1335 });
1336 }
1337 snapshots
1338 }
1339
1340 pub fn channel_snapshots(&self) -> Vec<ChannelSnapshot> {
1341 let mut snapshots = Vec::new();
1342 let now = Instant::now();
1343 for (conn_key, conn) in &self.connections {
1344 for channel in conn.channels_by_cid.values() {
1345 let mut update_times = channel.update_times.clone();
1346 Self::trim_times(&mut update_times, now);
1347 let mut messages: Vec<String> = channel.recent_messages.iter().cloned().collect();
1348 if messages.len() > 20 {
1349 messages = messages.split_off(messages.len() - 20);
1350 }
1351 let is_beacon = messages.iter().any(|m| m.starts_with("BEACON "));
1352 let is_broadcast = Self::is_broadcast_addr(&conn_key.addr_a)
1353 || Self::is_broadcast_addr(&conn_key.addr_b);
1354 snapshots.push(ChannelSnapshot {
1355 addr_a: conn_key.addr_a,
1356 addr_b: conn_key.addr_b,
1357 cid: channel.cid,
1358 sid: channel.sid,
1359 pv_name: channel.pv_name.clone(),
1360 last_seen: channel.last_seen.elapsed(),
1361 updates_per_sec: Self::updates_per_sec(&update_times),
1362 recent_messages: messages,
1363 mid_stream: !channel.fully_established
1364 || channel.pv_name.starts_with("<unknown"),
1365 is_beacon,
1366 is_broadcast,
1367 });
1368 }
1369
1370 let mut seen_virtual = HashSet::new();
1373 for op in conn.operations.values() {
1374 if conn.get_channel_by_sid(op.sid).is_none() {
1375 let mut update_times = op.update_times.clone();
1376 Self::trim_times(&mut update_times, now);
1377 let mut messages: Vec<String> = op.recent_messages.iter().cloned().collect();
1378 if messages.len() > 20 {
1379 messages = messages.split_off(messages.len() - 20);
1380 }
1381 let is_beacon = messages.iter().any(|m| m.starts_with("BEACON "));
1382 let is_broadcast = Self::is_broadcast_addr(&conn_key.addr_a)
1383 || Self::is_broadcast_addr(&conn_key.addr_b);
1384 let pv_name = op
1385 .pv_name
1386 .clone()
1387 .unwrap_or_else(|| format!("<unknown:sid={}>", op.sid));
1388 if !seen_virtual.insert((op.sid, pv_name.clone())) {
1389 continue;
1390 }
1391 snapshots.push(ChannelSnapshot {
1392 addr_a: conn_key.addr_a,
1393 addr_b: conn_key.addr_b,
1394 cid: 0,
1395 sid: Some(op.sid),
1396 pv_name,
1397 last_seen: op.last_seen.elapsed(),
1398 updates_per_sec: Self::updates_per_sec(&update_times),
1399 recent_messages: messages,
1400 mid_stream: true,
1401 is_beacon,
1402 is_broadcast,
1403 });
1404 }
1405 }
1406 }
1407 snapshots
1408 }
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::*;
1414 use crate::{FieldDesc, FieldType};
1415
1416 fn test_conn_key() -> ConnectionKey {
1417 ConnectionKey::from_parts("192.168.1.1", 12345, "192.168.1.2", 5075).unwrap()
1418 }
1419
1420 fn desc_with_fields(nested: usize) -> StructureDesc {
1423 let mut inner = StructureDesc::new();
1424 for i in 0..nested {
1425 inner.fields.push(FieldDesc {
1426 name: format!("nested_field_name_{i}"),
1427 field_type: FieldType::String,
1428 });
1429 }
1430 let mut outer = StructureDesc::new();
1431 outer.struct_id = Some("epics:nt/NTScalar:1.0".to_string());
1432 outer.fields.push(FieldDesc {
1433 name: "value".to_string(),
1434 field_type: FieldType::String,
1435 });
1436 outer.fields.push(FieldDesc {
1437 name: "timeStamp".to_string(),
1438 field_type: FieldType::Structure(inner),
1439 });
1440 outer
1441 }
1442
1443 #[test]
1444 fn test_heap_size_walks_nested_fields() {
1445 let flat = desc_with_fields(0);
1446 let nested = desc_with_fields(20);
1447
1448 assert!(
1449 flat.heap_size() > 0,
1450 "struct_id and two field names are heap"
1451 );
1452 let names_only = 20 * "nested_field_name_0".len();
1455 assert!(
1456 nested.heap_size() > flat.heap_size() + names_only,
1457 "nested={} flat={}",
1458 nested.heap_size(),
1459 flat.heap_size()
1460 );
1461 }
1462
1463 #[test]
1464 fn test_expired_placeholder_operations_are_reclaimed() {
1465 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(100, 0));
1466 let key = test_conn_key();
1467
1468 tracker.on_op_activity(&key, 0, 42, 13);
1472 assert_eq!(tracker.connection_count(), 1);
1473
1474 tracker.cleanup_expired();
1475
1476 assert_eq!(tracker.stats.operations_expired, 1);
1477 assert_eq!(
1478 tracker.connection_count(),
1479 0,
1480 "connection must drop once its last operation ages out"
1481 );
1482 }
1483
1484 #[test]
1485 fn test_active_operation_is_not_aged_out() {
1486 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(100, 3600));
1487 let key = test_conn_key();
1488
1489 tracker.on_op_activity(&key, 0, 42, 13);
1490 tracker.cleanup_expired();
1491
1492 assert_eq!(tracker.stats.operations_expired, 0);
1493 assert_eq!(tracker.connection_count(), 1);
1494 }
1495
1496 #[test]
1497 fn test_eviction_removes_operations_riding_the_channel() {
1498 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(1, 3600));
1499 let key = test_conn_key();
1500
1501 tracker.on_create_channel_request(&key, 1, "PV:ONE".to_string());
1502 tracker.on_create_channel_response(&key, 1, 100);
1503 tracker.on_op_init_request(&key, 100, 7, 13);
1504 assert_eq!(tracker.connections[&key].operations.len(), 1);
1505
1506 tracker.on_create_channel_request(&key, 2, "PV:TWO".to_string());
1508
1509 assert!(tracker.stats.channels_evicted >= 1);
1510 assert!(
1511 tracker.connections[&key].operations.is_empty(),
1512 "an operation whose channel was evicted is unreachable; nothing \
1513 else reclaims it, and it pins its connection forever"
1514 );
1515 }
1516
1517 #[test]
1518 fn test_shedding_drops_placeholders_before_introspection() {
1519 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(10_000, 3600));
1520 let key = test_conn_key();
1521
1522 tracker.on_create_channel_request(&key, 1, "PV:KEEP".to_string());
1524 tracker.on_create_channel_response(&key, 1, 100);
1525 tracker.on_op_init_request(&key, 100, 1, 13);
1526 tracker.on_op_init_response(&key, 1, Some(desc_with_fields(30)));
1527
1528 for ioid in 100..400 {
1530 tracker.on_op_activity(&key, 0, ioid, 13);
1531 }
1532
1533 tracker.config.max_memory_bytes = tracker.memory_estimate() / 2;
1534 tracker.cleanup_expired();
1535
1536 assert!(tracker.stats.shed_placeholder_ops > 0);
1537 assert_eq!(
1538 tracker.stats.shed_channels, 0,
1539 "no channel should be shed while worthless placeholders remain"
1540 );
1541 let op = &tracker.connections[&key].operations[&1];
1542 assert!(
1543 op.field_desc.is_some(),
1544 "introspection is the expensive thing to reacquire; it sheds last"
1545 );
1546 assert!(tracker.memory_estimate() <= tracker.config.max_memory_bytes);
1547 }
1548
1549 #[test]
1550 fn test_cleanup_holds_state_under_the_memory_ceiling() {
1551 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(40_000, 3600));
1552 let key = test_conn_key();
1553
1554 for cid in 0..4_000u32 {
1555 let sid = cid + 1;
1556 tracker.on_create_channel_request(&key, cid, format!("SR:DI:BPM:{cid:04}:X:MEAN"));
1557 tracker.on_create_channel_response(&key, cid, sid);
1558 tracker.on_op_init_request(&key, sid, sid, 13);
1559 tracker.on_op_init_response(&key, sid, Some(desc_with_fields(30)));
1560 }
1561
1562 let unbounded = tracker.memory_estimate();
1563 let budget = unbounded / 4;
1564 tracker.config.max_memory_bytes = budget;
1565 tracker.cleanup_expired();
1566
1567 assert!(
1568 tracker.stats.shed_channels > 0,
1569 "a quarter of the state cannot hold this channel set; coverage \
1570 has to be given up, and shed_channels is what says so"
1571 );
1572 assert!(
1573 tracker.memory_estimate() <= budget,
1574 "estimate {} still over budget {}",
1575 tracker.memory_estimate(),
1576 budget
1577 );
1578 assert_eq!(
1579 tracker.stats.memory_bytes as usize,
1580 tracker.memory_estimate()
1581 );
1582 }
1583
1584 #[test]
1585 fn test_zero_budget_disables_shedding() {
1586 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(10_000, 3600));
1587 let key = test_conn_key();
1588
1589 for ioid in 0..200 {
1590 tracker.on_op_activity(&key, 0, ioid, 13);
1591 }
1592 tracker.config.max_memory_bytes = 0;
1593 tracker.cleanup_expired();
1594
1595 assert_eq!(tracker.stats.shed_placeholder_ops, 0);
1596 assert_eq!(tracker.stats.shed_channels, 0);
1597 assert_eq!(tracker.connections[&key].operations.len(), 200);
1598 }
1599
1600 #[test]
1601 fn test_search_cache_cap_is_enforced() {
1602 let mut tracker = PvaStateTracker::new(PvaStateConfig::new(10_000, 3600));
1603 tracker.config.max_search_cache_entries = 50;
1604
1605 let ip = Some("192.168.1.1".parse::<IpAddr>().unwrap());
1606 let reqs: Vec<(u32, String)> = (0..200u32).map(|i| (i, format!("PV:{i}"))).collect();
1607 tracker.on_search(&reqs, ip);
1608 assert!(tracker.search_cache.len() > 50);
1609
1610 tracker.cleanup_expired();
1611
1612 assert!(tracker.search_cache.is_empty());
1613 assert!(tracker.search_cache_flat.is_empty());
1614 assert!(tracker.stats.shed_search_entries >= 200);
1615 }
1616
1617 #[test]
1618 fn test_create_channel_flow() {
1619 let mut tracker = PvaStateTracker::with_defaults();
1620 let key = test_conn_key();
1621
1622 tracker.on_create_channel_request(&key, 1, "TEST:PV:VALUE".to_string());
1624 assert_eq!(tracker.channel_count(), 1);
1625
1626 tracker.on_create_channel_response(&key, 1, 100);
1628
1629 let pv_name = tracker.resolve_pv_name(&key, 100, 0);
1631 assert_eq!(pv_name, Some("TEST:PV:VALUE".to_string()));
1632 }
1633
1634 #[test]
1635 fn test_channel_limit() {
1636 let config = PvaStateConfig::new(100, 300);
1637 let mut tracker = PvaStateTracker::new(config);
1638 let key = test_conn_key();
1639
1640 for i in 0..150 {
1642 tracker.on_create_channel_request(&key, i, format!("PV:{}", i));
1643 }
1644
1645 assert!(tracker.channel_count() <= 100);
1647 }
1648
1649 #[test]
1650 fn test_destroy_channel() {
1651 let mut tracker = PvaStateTracker::with_defaults();
1652 let key = test_conn_key();
1653
1654 tracker.on_create_channel_request(&key, 1, "TEST:PV".to_string());
1655 tracker.on_create_channel_response(&key, 1, 100);
1656 assert_eq!(tracker.channel_count(), 1);
1657
1658 tracker.on_destroy_channel(&key, 1, 100);
1659 assert_eq!(tracker.channel_count(), 0);
1660 }
1661
1662 #[test]
1663 fn test_channel_snapshots_dedup_unresolved_sid_rows() {
1664 let mut tracker = PvaStateTracker::with_defaults();
1665 let key = test_conn_key();
1666
1667 tracker.on_op_init_request(&key, 777, 1001, 13);
1669 tracker.on_op_init_request(&key, 777, 1002, 13);
1670 tracker.on_op_activity(&key, 777, 1001, 13);
1671 tracker.on_op_activity(&key, 777, 1002, 13);
1672
1673 let snapshots = tracker.channel_snapshots();
1674 assert_eq!(snapshots.len(), 1);
1675 assert_eq!(snapshots[0].sid, Some(777));
1676 }
1677
1678 #[test]
1679 fn test_single_channel_fallback_works_for_simple_connection() {
1680 let mut tracker = PvaStateTracker::with_defaults();
1683 let key = test_conn_key();
1684
1685 tracker.on_create_channel_request(&key, 1, "SIMPLE:PV".to_string());
1686 tracker.on_create_channel_response(&key, 1, 100);
1687
1688 let pv = tracker.resolve_pv_name(&key, 0, 99);
1690 assert_eq!(pv, Some("SIMPLE:PV".to_string()));
1691 }
1692
1693 #[test]
1694 fn test_no_false_attribution_on_multiplexed_connection() {
1695 let mut tracker = PvaStateTracker::with_defaults();
1700 let key = test_conn_key();
1701
1702 tracker.on_create_channel_request(&key, 1, "CAPTURED:PV".to_string());
1704 tracker.on_create_channel_response(&key, 1, 100);
1705
1706 tracker.on_op_init_request(&key, 100, 1, 13); for ioid in 2..=10 {
1712 tracker.on_op_activity(&key, 0, ioid, 13);
1713 }
1714
1715 let pv1 = tracker.resolve_pv_name(&key, 100, 1);
1717 assert_eq!(pv1, Some("CAPTURED:PV".to_string()));
1718
1719 for ioid in 2..=10 {
1721 let pv = tracker.resolve_pv_name(&key, 0, ioid);
1722 assert_eq!(
1723 pv, None,
1724 "ioid={} should not resolve to the single captured channel",
1725 ioid
1726 );
1727 }
1728 }
1729
1730 #[test]
1731 fn test_on_op_activity_placeholder_not_created_for_multiplexed() {
1732 let mut tracker = PvaStateTracker::with_defaults();
1736 let key = test_conn_key();
1737
1738 tracker.on_create_channel_request(&key, 1, "KNOWN:PV".to_string());
1739 tracker.on_create_channel_response(&key, 1, 100);
1740
1741 tracker.on_op_init_request(&key, 100, 1, 13);
1743
1744 tracker.on_op_activity(&key, 0, 2, 13);
1746
1747 let pv = tracker.resolve_pv_name(&key, 0, 2);
1748 assert_eq!(
1749 pv, None,
1750 "placeholder for ioid=2 should not inherit PV from single-channel fallback"
1751 );
1752 }
1753
1754 #[test]
1755 fn test_search_cache_populates_and_resolves() {
1756 let mut tracker = PvaStateTracker::with_defaults();
1757 let client_ip: IpAddr = "192.168.1.10".parse().unwrap();
1758
1759 let pv_requests = vec![
1761 (100, "MOTOR:X:POSITION".to_string()),
1762 (101, "MOTOR:Y:POSITION".to_string()),
1763 (102, "TEMP:SENSOR:1".to_string()),
1764 ];
1765 tracker.on_search(&pv_requests, Some(client_ip));
1766
1767 let resolved = tracker.resolve_search_cids(&[100, 101, 102], Some(client_ip));
1769 assert_eq!(resolved.len(), 3);
1770 assert_eq!(resolved[0], (100, "MOTOR:X:POSITION".to_string()));
1771 assert_eq!(resolved[1], (101, "MOTOR:Y:POSITION".to_string()));
1772 assert_eq!(resolved[2], (102, "TEMP:SENSOR:1".to_string()));
1773 }
1774
1775 #[test]
1776 fn test_search_cache_partial_resolve() {
1777 let mut tracker = PvaStateTracker::with_defaults();
1778 let client_ip: IpAddr = "192.168.1.10".parse().unwrap();
1779
1780 let pv_requests = vec![(100, "MOTOR:X:POSITION".to_string())];
1781 tracker.on_search(&pv_requests, Some(client_ip));
1782
1783 let resolved = tracker.resolve_search_cids(&[100, 999], Some(client_ip));
1785 assert_eq!(resolved.len(), 1);
1786 assert_eq!(resolved[0], (100, "MOTOR:X:POSITION".to_string()));
1787 }
1788
1789 #[test]
1790 fn test_search_cache_scoped_by_ip() {
1791 let mut tracker = PvaStateTracker::with_defaults();
1792 let client_a: IpAddr = "192.168.1.10".parse().unwrap();
1793 let client_b: IpAddr = "192.168.1.20".parse().unwrap();
1794
1795 tracker.on_search(&[(1, "CLIENT_A:PV".to_string())], Some(client_a));
1797 tracker.on_search(&[(1, "CLIENT_B:PV".to_string())], Some(client_b));
1798
1799 let resolved_a = tracker.resolve_search_cids(&[1], Some(client_a));
1801 assert_eq!(resolved_a.len(), 1);
1802 assert_eq!(resolved_a[0].1, "CLIENT_A:PV");
1803
1804 let resolved_b = tracker.resolve_search_cids(&[1], Some(client_b));
1805 assert_eq!(resolved_b.len(), 1);
1806 assert_eq!(resolved_b[0].1, "CLIENT_B:PV");
1807 }
1808
1809 #[test]
1810 fn test_search_cache_flat_fallback() {
1811 let mut tracker = PvaStateTracker::with_defaults();
1812 let client_ip: IpAddr = "192.168.1.10".parse().unwrap();
1813
1814 tracker.on_search(&[(42, "SOME:PV:NAME".to_string())], Some(client_ip));
1816
1817 let resolved = tracker.resolve_search_cids(&[42], None);
1819 assert_eq!(resolved.len(), 1);
1820 assert_eq!(resolved[0].1, "SOME:PV:NAME");
1821 }
1822
1823 #[test]
1824 fn test_search_cache_used_by_create_channel_response_fallback() {
1825 let mut tracker = PvaStateTracker::with_defaults();
1828 let key = test_conn_key();
1829 let client_ip: IpAddr = "192.168.1.1".parse().unwrap();
1830
1831 tracker.on_search(&[(5, "SEARCHED:PV".to_string())], Some(client_ip));
1833
1834 tracker.on_create_channel_response(&key, 5, 200);
1836
1837 let pv = tracker.resolve_pv_name(&key, 200, 0);
1839 assert_eq!(pv, Some("SEARCHED:PV".to_string()));
1840 }
1841
1842 #[test]
1843 fn test_search_responses_resolved_stat() {
1844 let mut tracker = PvaStateTracker::with_defaults();
1845 let client_ip: IpAddr = "192.168.1.10".parse().unwrap();
1846
1847 tracker.on_search(
1848 &[(1, "PV:A".to_string()), (2, "PV:B".to_string())],
1849 Some(client_ip),
1850 );
1851
1852 assert_eq!(tracker.stats.search_responses_resolved, 0);
1853
1854 tracker.resolve_search_cids(&[1, 2], Some(client_ip));
1855 assert_eq!(tracker.stats.search_responses_resolved, 2);
1856
1857 tracker.resolve_search_cids(&[1], Some(client_ip));
1859 assert_eq!(tracker.stats.search_responses_resolved, 3);
1860 }
1861
1862 #[test]
1863 fn test_retroactive_resolve_unknown_channels_from_search() {
1864 let mut tracker = PvaStateTracker::with_defaults();
1869 let key = test_conn_key();
1870
1871 tracker.on_create_channel_response(&key, 100, 500);
1873 tracker.on_create_channel_response(&key, 101, 501);
1874 tracker.on_create_channel_response(&key, 102, 502);
1875
1876 assert_eq!(
1878 tracker.resolve_pv_name(&key, 500, 0),
1879 Some("<unknown:cid=100>".to_string())
1880 );
1881 assert_eq!(
1882 tracker.resolve_pv_name(&key, 501, 0),
1883 Some("<unknown:cid=101>".to_string())
1884 );
1885
1886 let client_ip: IpAddr = "192.168.1.1".parse().unwrap();
1888 tracker.on_search(
1889 &[
1890 (100, "MOTOR:X:POS".to_string()),
1891 (101, "MOTOR:Y:POS".to_string()),
1892 (102, "TEMP:SENSOR:1".to_string()),
1893 ],
1894 Some(client_ip),
1895 );
1896
1897 assert_eq!(
1899 tracker.resolve_pv_name(&key, 500, 0),
1900 Some("MOTOR:X:POS".to_string())
1901 );
1902 assert_eq!(
1903 tracker.resolve_pv_name(&key, 501, 0),
1904 Some("MOTOR:Y:POS".to_string())
1905 );
1906 assert_eq!(
1907 tracker.resolve_pv_name(&key, 502, 0),
1908 Some("TEMP:SENSOR:1".to_string())
1909 );
1910
1911 assert_eq!(tracker.stats.search_retroactive_resolves, 3);
1913 }
1914
1915 #[test]
1916 fn test_retroactive_resolve_also_updates_operations() {
1917 let mut tracker = PvaStateTracker::with_defaults();
1921 let key = test_conn_key();
1922
1923 tracker.on_create_channel_response(&key, 100, 500);
1925
1926 tracker.on_op_init_request(&key, 500, 1, 13); let pv = tracker.resolve_pv_name(&key, 500, 1);
1932 assert!(pv.is_some());
1933 let client_ip: IpAddr = "192.168.1.1".parse().unwrap();
1937 tracker.on_search(&[(100, "RESOLVED:PV".to_string())], Some(client_ip));
1938
1939 assert_eq!(
1941 tracker.resolve_pv_name(&key, 500, 0),
1942 Some("RESOLVED:PV".to_string())
1943 );
1944 let pv = tracker.resolve_pv_name(&key, 500, 1);
1946 assert_eq!(pv, Some("RESOLVED:PV".to_string()));
1947 }
1948}