1use std::sync::Arc;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::time::{Duration, Instant};
4
5use async_trait::async_trait;
6use camel_bridge::download::{default_cache_dir_for_spec, ensure_binary_for_spec};
7use camel_bridge::health::wait_for_health;
8use camel_bridge::process::{BridgeError, BridgeProcess, BridgeProcessConfig};
9use camel_bridge::reconnect::BridgeReconnectHandler;
10use camel_bridge::spec::XML_BRIDGE;
11use camel_component_api::RuntimeObservability;
12use dashmap::DashMap;
13use prost::Message;
14use sha2::{Digest, Sha256};
15use std::sync::OnceLock;
16use tokio::sync::{Mutex, RwLock, watch};
17use tonic::transport::Channel;
18use tracing::error;
19
20use crate::error::ValidatorError;
21use crate::proto;
22use crate::proto::{
23 HealthCheckRequest, RegisterSchemaRequest, RegisterSchemaResponse, ValidateResponse,
24 ValidateWithRequest,
25};
26use crate::schema_set::SchemaSet;
27
28pub type SchemaId = String;
29
30impl RegisterSchemaRequest {
31 pub(crate) fn new(schema_id: SchemaId, set: &SchemaSet) -> Self {
37 Self {
38 schema_id,
39 schema: set.root_bytes().to_vec(),
40 root_system_id: set.root_system_id.clone(),
41 resources: set
42 .resources
43 .iter()
44 .map(|(key, bytes)| (key.clone(), bytes.clone()))
45 .collect(),
46 aliases: set
47 .aliases
48 .iter()
49 .map(|(key, value)| (key.clone(), value.clone()))
50 .collect(),
51 }
52 }
53
54 fn check_encoded_size(&self) -> Result<(), ValidatorError> {
55 if self.encoded_len() > XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE {
56 return Err(ValidatorError::Transport {
57 message: "SCHEMA_SET_REQUEST_SIZE_CAP: encoded registration exceeds 17 MiB"
58 .to_string(),
59 source: None,
60 });
61 }
62 Ok(())
63 }
64}
65
66pub(crate) const XSD_BRIDGE_MAX_DECODING_MESSAGE_SIZE: usize = 17 * 1024 * 1024;
78
79pub(crate) const XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE: usize = XSD_BRIDGE_MAX_DECODING_MESSAGE_SIZE;
83
84pub(crate) fn xsd_bridge_decode_limit() -> usize {
85 XSD_BRIDGE_MAX_DECODING_MESSAGE_SIZE
86}
87
88pub(crate) fn xsd_bridge_client(
95 channel: Channel,
96) -> proto::xsd_validator_client::XsdValidatorClient<Channel> {
97 proto::xsd_validator_client::XsdValidatorClient::new(channel)
98 .max_decoding_message_size(xsd_bridge_decode_limit())
99}
100
101#[derive(Debug, Clone)]
102pub enum BridgeState {
103 Starting,
104 Ready { channel: Channel },
105 Degraded(String),
106 Restarting { attempt: u32, next_at: Instant },
107 Stopped,
108}
109
110pub struct XmlBridgeSlot {
111 pub state_rx: watch::Receiver<BridgeState>,
112 pub(crate) state_tx: watch::Sender<BridgeState>,
113 pub process: Arc<Mutex<Option<BridgeProcess>>>,
114}
115
116impl std::fmt::Debug for XmlBridgeSlot {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("XmlBridgeSlot").finish()
119 }
120}
121
122#[async_trait]
123pub trait XsdBridge: Send + Sync {
124 async fn register(&self, schema_id: &str, set: &SchemaSet) -> Result<SchemaId, ValidatorError>;
129 async fn validate(&self, schema_id: &str, doc_bytes: Vec<u8>) -> Result<(), ValidatorError>;
130}
131
132#[async_trait]
133trait XsdBridgeRpc: Send + Sync {
134 async fn register_schema(
135 &self,
136 channel: Channel,
137 request: RegisterSchemaRequest,
138 ) -> Result<RegisterSchemaResponse, ValidatorError>;
139
140 async fn validate_with(
141 &self,
142 channel: Channel,
143 request: ValidateWithRequest,
144 ) -> Result<ValidateResponse, ValidatorError>;
145}
146
147#[derive(Debug)]
148struct GrpcXsdBridgeRpc;
149
150#[async_trait]
151impl XsdBridgeRpc for GrpcXsdBridgeRpc {
152 async fn register_schema(
153 &self,
154 channel: Channel,
155 request: RegisterSchemaRequest,
156 ) -> Result<RegisterSchemaResponse, ValidatorError> {
157 let mut client = xsd_bridge_client(channel);
158 let response = client.register_schema(request).await.map_err(|e| {
159 ValidatorError::transport_with_source("xml-bridge register_schema RPC failed", e)
160 })?;
161 Ok(response.into_inner())
162 }
163
164 async fn validate_with(
165 &self,
166 channel: Channel,
167 request: ValidateWithRequest,
168 ) -> Result<ValidateResponse, ValidatorError> {
169 let mut client = xsd_bridge_client(channel);
170 let response = client.validate_with(request).await.map_err(|e| {
171 ValidatorError::transport_with_source("xml-bridge validate_with RPC failed", e)
172 })?;
173 Ok(response.into_inner())
174 }
175}
176
177#[async_trait]
178trait XsdBridgeLauncher: Send + Sync {
179 async fn start(
180 &self,
181 version: &str,
182 cache_dir: &std::path::Path,
183 timeout_ms: u64,
184 ) -> Result<(Option<BridgeProcess>, Channel), ValidatorError>;
185
186 async fn stop(&self, process: Option<BridgeProcess>) {
187 if let Some(process) = process {
188 let _ = process.stop().await;
189 }
190 }
191}
192
193struct ProcessXsdBridgeLauncher;
194
195#[async_trait]
196impl XsdBridgeLauncher for ProcessXsdBridgeLauncher {
197 async fn start(
198 &self,
199 version: &str,
200 cache_dir: &std::path::Path,
201 timeout_ms: u64,
202 ) -> Result<(Option<BridgeProcess>, Channel), ValidatorError> {
203 let binary_path = ensure_binary_for_spec(&XML_BRIDGE, version, cache_dir)
204 .await
205 .map_err(|e| ValidatorError::endpoint(format!("XML bridge binary unavailable: {e}")))?;
206
207 let process_config = BridgeProcessConfig::xml(binary_path, timeout_ms);
208 let (process, channel) = BridgeProcess::start_and_connect(&process_config)
209 .await
210 .map_err(|e| ValidatorError::endpoint(format!("XML bridge start failed: {e}")))?;
211
212 wait_for_health(&channel, Duration::from_secs(10), |ch| {
213 let mut client = proto::health_client::HealthClient::new(ch);
214 async move {
215 let resp = client.check(HealthCheckRequest {}).await?;
216 Ok(resp.into_inner().status == "SERVING")
217 }
218 })
219 .await
220 .map_err(|e| ValidatorError::endpoint(format!("XML bridge health check failed: {e}")))?;
221
222 Ok((Some(process), channel))
223 }
224}
225
226#[derive(Debug, Clone)]
227struct ReadyBridge {
228 channel: Channel,
229 generation: usize,
230}
231
232#[derive(Clone)]
233pub struct XsdBridgeBackend {
234 channel: Arc<RwLock<Option<ReadyBridge>>>,
238 generation: Arc<AtomicUsize>,
239 schemas: Arc<DashMap<SchemaId, Arc<SchemaSet>>>,
242 schema_cache_max_entries: Arc<AtomicUsize>,
243 slot: Arc<XmlBridgeSlot>,
244 rpc: Arc<dyn XsdBridgeRpc>,
245 launcher: Arc<dyn XsdBridgeLauncher>,
246 start_lock: Arc<Mutex<()>>,
247 bridge_version: String,
248 bridge_cache_dir: std::path::PathBuf,
249 bridge_start_timeout_ms: u64,
250 observability: Arc<OnceLock<(Arc<dyn RuntimeObservability>, String)>>,
251}
252
253impl std::fmt::Debug for XsdBridgeBackend {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("XsdBridgeBackend")
256 .field("bridge_version", &self.bridge_version)
257 .field("bridge_cache_dir", &self.bridge_cache_dir)
258 .finish()
259 }
260}
261
262impl XsdBridgeBackend {
263 pub fn new() -> Self {
264 let (state_tx, state_rx) = watch::channel(BridgeState::Stopped);
265 let slot = Arc::new(XmlBridgeSlot {
266 state_rx,
267 state_tx,
268 process: Arc::new(Mutex::new(None)),
269 });
270
271 Self {
272 channel: Arc::new(RwLock::new(None)),
273 generation: Arc::new(AtomicUsize::new(0)),
274 schemas: Arc::new(DashMap::new()),
275 schema_cache_max_entries: Arc::new(AtomicUsize::new(
276 crate::config::DEFAULT_SCHEMA_CACHE_MAX_ENTRIES,
277 )),
278 slot,
279 rpc: Arc::new(GrpcXsdBridgeRpc),
280 launcher: Arc::new(ProcessXsdBridgeLauncher),
281 start_lock: Arc::new(Mutex::new(())),
282 bridge_version: crate::BRIDGE_VERSION.to_string(),
283 bridge_cache_dir: default_cache_dir_for_spec(&XML_BRIDGE),
284 bridge_start_timeout_ms: 30_000,
285 observability: Arc::new(OnceLock::new()),
286 }
287 }
288
289 #[cfg(test)]
290 fn for_test(rpc: Arc<dyn XsdBridgeRpc>, channel: Channel) -> Self {
291 let (state_tx, state_rx) = watch::channel(BridgeState::Ready {
292 channel: channel.clone(),
293 });
294 let slot = Arc::new(XmlBridgeSlot {
295 state_rx,
296 state_tx,
297 process: Arc::new(Mutex::new(None)),
298 });
299 Self {
300 channel: Arc::new(RwLock::new(Some(ReadyBridge {
301 channel,
302 generation: 1,
303 }))),
304 generation: Arc::new(AtomicUsize::new(1)),
305 schemas: Arc::new(DashMap::new()),
306 schema_cache_max_entries: Arc::new(AtomicUsize::new(
307 crate::config::DEFAULT_SCHEMA_CACHE_MAX_ENTRIES,
308 )),
309 slot,
310 rpc,
311 launcher: Arc::new(ProcessXsdBridgeLauncher),
312 start_lock: Arc::new(Mutex::new(())),
313 bridge_version: crate::BRIDGE_VERSION.to_string(),
314 bridge_cache_dir: default_cache_dir_for_spec(&XML_BRIDGE),
315 bridge_start_timeout_ms: 30_000,
316 observability: Arc::new(OnceLock::new()),
317 }
318 }
319
320 pub fn set_observability(&self, runtime: Arc<dyn RuntimeObservability>, route_id: String) {
321 self.observability.set((runtime, route_id)).ok();
322 }
323
324 pub fn schema_id_for(xsd_bytes: &[u8]) -> SchemaId {
325 let mut hasher = Sha256::new();
326 hasher.update(xsd_bytes);
327 format!("xsd-{}", hex::encode(hasher.finalize()))
328 }
329
330 pub fn set_schema_cache_max_entries(&self, max_entries: usize) {
333 if self.schemas.len() > max_entries {
334 self.schemas.clear();
335 }
336 self.schema_cache_max_entries
337 .store(max_entries, Ordering::Relaxed);
338 }
339
340 async fn ensure_bridge_ready(&self) -> Result<ReadyBridge, ValidatorError> {
341 if let Some(ch) = self.channel.read().await.clone() {
342 return Ok(ch);
343 }
344
345 let _guard = self.start_lock.lock().await;
346 if let Some(ch) = self.channel.read().await.clone() {
347 return Ok(ch);
348 }
349
350 let _ = self.slot.state_tx.send(BridgeState::Starting);
351 self.launch_and_seed().await
352 }
353
354 async fn restart_bridge(
355 &self,
356 failed_generation: usize,
357 ) -> Result<ReadyBridge, ValidatorError> {
358 let _guard = self.start_lock.lock().await;
359 if let Some(ready) = self.channel.read().await.as_ref()
360 && ready.generation > failed_generation
361 {
362 return Ok(ready.clone());
363 }
364
365 let _ = self.slot.state_tx.send(BridgeState::Restarting {
366 attempt: 0,
367 next_at: Instant::now(),
368 });
369 self.launch_and_seed().await
370 }
371
372 async fn launch_and_seed(&self) -> Result<ReadyBridge, ValidatorError> {
376 self.channel.write().await.take();
377 let old_process = {
378 let mut process_guard = self.slot.process.lock().await;
379 process_guard.take()
380 };
381 self.launcher.stop(old_process).await;
382
383 let (process, channel) = match self.start_bridge_process().await {
384 Ok(launched) => launched,
385 Err(e) => {
386 let _ = self
387 .slot
388 .state_tx
389 .send(BridgeState::Degraded(e.to_string()));
390 return Err(e);
391 }
392 };
393
394 if let Err(e) = self.reseed_with_channel(&channel).await {
395 self.launcher.stop(process).await;
396 let _ = self
397 .slot
398 .state_tx
399 .send(BridgeState::Degraded(e.to_string()));
400 return Err(e);
401 }
402 let ready = ReadyBridge {
403 channel: channel.clone(),
404 generation: self.generation.fetch_add(1, Ordering::Relaxed) + 1,
405 };
406 {
407 let mut published = self.channel.write().await;
410 *self.slot.process.lock().await = process;
411 *published = Some(ready.clone());
412 }
413 let _ = self.slot.state_tx.send(BridgeState::Ready {
414 channel: channel.clone(),
415 });
416
417 Ok(ready)
418 }
419
420 async fn start_bridge_process(
421 &self,
422 ) -> Result<(Option<BridgeProcess>, Channel), ValidatorError> {
423 self.launcher
424 .start(
425 &self.bridge_version,
426 &self.bridge_cache_dir,
427 self.bridge_start_timeout_ms,
428 )
429 .await
430 }
431
432 fn is_transport_retryable(err: &ValidatorError) -> bool {
439 matches!(
440 err,
441 ValidatorError::Transport {
442 source: Some(source),
443 ..
444 } if source.downcast_ref::<tonic::Status>()
445 .is_some_and(|status| status.code() == tonic::Code::Unavailable)
446 )
447 }
448
449 pub async fn shutdown(&self) {
450 let mut guard = self.slot.process.lock().await;
451 if let Some(p) = guard.take()
452 && let Err(e) = p.stop().await
453 {
454 tracing::warn!("Failed to stop XSD bridge process: {}", e);
455 }
456 }
457
458 async fn register_with_channel(
459 &self,
460 ready: &ReadyBridge,
461 schema_id: &str,
462 set: &SchemaSet,
463 ) -> Result<Option<SchemaId>, ValidatorError> {
464 let request = RegisterSchemaRequest::new(schema_id.to_string(), set);
465 request.check_encoded_size()?;
466 let response = self
467 .rpc
468 .register_schema(ready.channel.clone(), request)
469 .await?;
470
471 if let Some(err) = response.error {
472 return Err(ValidatorError::from_bridge_error(&err));
473 }
474
475 let published = self.channel.read().await;
476 if !published
477 .as_ref()
478 .is_some_and(|current| current.generation == ready.generation)
479 {
480 return Ok(None);
483 }
484
485 let max_entries = self.schema_cache_max_entries.load(Ordering::Relaxed);
489 if self.schemas.len() >= max_entries && !self.schemas.contains_key(schema_id) {
490 self.schemas.clear();
491 }
492
493 self.schemas
496 .insert(schema_id.to_string(), Arc::new(set.clone()));
497 Ok(Some(schema_id.to_string()))
498 }
499
500 async fn reseed_with_channel(&self, channel: &Channel) -> Result<(), ValidatorError> {
509 let schemas: Vec<(SchemaId, Arc<SchemaSet>)> = self
513 .schemas
514 .iter()
515 .map(|entry| (entry.key().clone(), Arc::clone(entry.value())))
516 .collect();
517
518 let observability = self
519 .observability
520 .get()
521 .map(|(rt, rid)| (Arc::clone(rt), rid.clone()));
522
523 for (schema_id, set) in schemas {
524 let outcome = async {
525 let request = RegisterSchemaRequest::new(schema_id.clone(), &set);
526 request.check_encoded_size()?;
527 let response = self.rpc.register_schema(channel.clone(), request).await?;
528 match response.error {
529 Some(err) => Err(ValidatorError::from_bridge_error(&err)),
533 None => Ok(()),
534 }
535 }
536 .await;
537 if let Err(e) = outcome {
538 if let Some((ref rt, ref rid)) = observability {
539 rt.metrics()
540 .increment_errors(rid, "e:validator:reconnect-reseed");
541 }
542 error!(schema_id = %schema_id, error = %e, "re-seed schema failed after reconnect");
544 return Err(e);
545 }
546 }
547 Ok(())
548 }
549}
550
551impl BridgeReconnectHandler for XsdBridgeBackend {
552 fn on_reconnect(&self, channel: &Channel) -> Result<(), BridgeError> {
553 let this = self.clone();
554 let channel = channel.clone();
555 tokio::spawn(async move {
556 let _ = this.reseed_with_channel(&channel).await;
560 });
561 Ok(())
562 }
563}
564
565#[async_trait]
566impl XsdBridge for XsdBridgeBackend {
567 async fn register(&self, schema_id: &str, set: &SchemaSet) -> Result<SchemaId, ValidatorError> {
568 let mut ready = self.ensure_bridge_ready().await?;
569 let mut transport_retried = false;
570 loop {
571 {
572 let published = self.channel.read().await;
573 if published
574 .as_ref()
575 .is_some_and(|current| current.generation == ready.generation)
576 && self.schemas.contains_key(schema_id)
577 {
578 return Ok(schema_id.to_string());
579 }
580 }
581 match self.register_with_channel(&ready, schema_id, set).await {
582 Ok(Some(id)) => return Ok(id),
583 Ok(None) => ready = self.ensure_bridge_ready().await?,
584 Err(e) if !transport_retried && Self::is_transport_retryable(&e) => {
585 transport_retried = true;
586 ready = self.restart_bridge(ready.generation).await?;
587 }
588 Err(e) => return Err(e),
589 }
590 }
591 }
592
593 async fn validate(&self, schema_id: &str, doc_bytes: Vec<u8>) -> Result<(), ValidatorError> {
594 let req = ValidateWithRequest {
595 schema_id: schema_id.to_string(),
596 document: doc_bytes,
597 };
598 if req.encoded_len() > XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE {
599 return Err(ValidatorError::Transport {
600 message: "VALIDATION_REQUEST_SIZE_CAP: encoded validation exceeds 17 MiB"
601 .to_string(),
602 source: None,
603 });
604 }
605 let channel = self.ensure_bridge_ready().await?;
606
607 let response = match self
608 .rpc
609 .validate_with(channel.channel.clone(), req.clone())
610 .await
611 {
612 Ok(resp) => resp,
613 Err(e) if Self::is_transport_retryable(&e) => {
614 let restarted = self.restart_bridge(channel.generation).await?;
615 self.rpc.validate_with(restarted.channel, req).await?
616 }
617 Err(e) => return Err(e),
618 };
619
620 if let Some(err) = response.error {
621 return Err(ValidatorError::from_bridge_error(&err));
622 }
623 if response.valid {
624 return Ok(());
625 }
626
627 let details = response
628 .errors
629 .iter()
630 .map(|e| format!("{}:{} {}", e.line, e.column, e.message))
631 .collect::<Vec<_>>()
632 .join("\n");
633 Err(ValidatorError::validation(format!(
634 "XSD validation failed:\n{details}"
635 )))
636 }
637}
638
639impl Default for XsdBridgeBackend {
640 fn default() -> Self {
641 Self::new()
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use std::collections::{BTreeMap, HashMap};
649 use std::sync::Mutex as StdMutex;
650 use std::sync::atomic::{AtomicUsize, Ordering};
651 use tokio::sync::Notify;
652 use tonic::transport::Endpoint;
653
654 #[derive(Debug)]
655 struct MockRpc {
656 register_calls: Arc<AtomicUsize>,
657 validate_ok: bool,
658 captured: Arc<StdMutex<Vec<RegisterSchemaRequest>>>,
659 signal: Arc<Notify>,
660 }
661
662 impl MockRpc {
663 fn new(validate_ok: bool) -> Self {
664 Self {
665 register_calls: Arc::new(AtomicUsize::new(0)),
666 validate_ok,
667 captured: Arc::new(StdMutex::new(Vec::new())),
668 signal: Arc::new(Notify::new()),
669 }
670 }
671 }
672
673 #[async_trait]
674 impl XsdBridgeRpc for MockRpc {
675 async fn register_schema(
676 &self,
677 _channel: Channel,
678 request: RegisterSchemaRequest,
679 ) -> Result<RegisterSchemaResponse, ValidatorError> {
680 self.register_calls.fetch_add(1, Ordering::SeqCst);
681 self.captured
682 .lock()
683 .expect("capture lock")
684 .push(request.clone());
685 self.signal.notify_one();
686 Ok(RegisterSchemaResponse {
687 schema_id: request.schema_id,
688 error: None,
689 })
690 }
691
692 async fn validate_with(
693 &self,
694 _channel: Channel,
695 _request: ValidateWithRequest,
696 ) -> Result<ValidateResponse, ValidatorError> {
697 Ok(ValidateResponse {
698 valid: self.validate_ok,
699 errors: Vec::new(),
700 error: None,
701 })
702 }
703 }
704
705 fn lazy_channel() -> Channel {
706 Endpoint::from_static("http://127.0.0.1:65535").connect_lazy()
707 }
708
709 fn test_backend(rpc: &Arc<MockRpc>) -> XsdBridgeBackend {
710 XsdBridgeBackend::for_test(Arc::clone(rpc) as Arc<dyn XsdBridgeRpc>, lazy_channel())
711 }
712
713 async fn wait_for_register_calls(rpc: &MockRpc, min: usize) {
718 let deadline = Instant::now() + Duration::from_secs(2);
719 loop {
720 if rpc.captured.lock().expect("capture lock").len() >= min {
721 return;
722 }
723 let remaining = deadline.saturating_duration_since(Instant::now());
724 assert!(
725 !remaining.is_zero(),
726 "timed out waiting for {min} register calls"
727 );
728 if tokio::time::timeout(remaining, rpc.signal.notified())
729 .await
730 .is_err()
731 {
732 assert!(
733 rpc.captured.lock().expect("capture lock").len() >= min,
734 "timed out waiting for {min} register calls"
735 );
736 return;
737 }
738 }
739 }
740
741 fn sample_set(root_bytes: &[u8]) -> SchemaSet {
742 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
743 resources.insert("root.xsd".to_string(), root_bytes.to_vec());
744 SchemaSet {
745 root_system_id: "root.xsd".to_string(),
746 resources,
747 aliases: BTreeMap::new(),
748 }
749 }
750
751 fn xml_ingress_limit() -> usize {
752 include_str!("../../../../bridges/xml/src/main/resources/application.yml")
753 .lines()
754 .find_map(|line| line.trim().strip_prefix("max-inbound-message-size:"))
755 .expect("XML gRPC ingress configuration")
756 .trim()
757 .parse()
758 .expect("numeric ingress limit")
759 }
760
761 #[test]
762 fn register_request_at_byte_budget_fits_ingress() {
763 use crate::schema_set::MAX_SCHEMA_BYTES;
764
765 let prefix = br#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><!--"#;
766 let suffix = b"--></xs:schema>";
767 let mut root = prefix.to_vec();
768 root.resize(MAX_SCHEMA_BYTES - suffix.len(), b' ');
769 root.extend_from_slice(suffix);
770 let set = sample_set(&root);
771 assert_eq!(set.root_bytes().len(), MAX_SCHEMA_BYTES);
772 let request = RegisterSchemaRequest::new(set.cache_key(), &set);
773 assert_eq!(request.schema, root, "legacy root must remain populated");
774 let encoded = request.encode_to_vec();
775 assert_eq!(encoded.len(), request.encoded_len());
776 assert!(encoded.len() > 16 * 1024 * 1024);
777 assert!(encoded.len() <= XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE);
778 assert!(
779 encoded.len() <= xml_ingress_limit(),
780 "encoded request {} exceeds configured ingress {}",
781 encoded.len(),
782 xml_ingress_limit()
783 );
784 }
785
786 #[test]
787 fn xml_ingress_matches_request_limit() {
788 assert_eq!(xml_ingress_limit(), XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE);
789 assert_eq!(
790 XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE,
791 xsd_bridge_decode_limit()
792 );
793 }
794
795 mod generations {
796 use super::*;
797 use std::future::{Future, poll_fn};
798 use std::pin::Pin;
799 use std::task::Poll;
800
801 type Events = Arc<StdMutex<Vec<String>>>;
802
803 struct CountingLauncher {
804 launches: AtomicUsize,
805 stops: AtomicUsize,
806 events: Events,
807 }
808
809 #[async_trait]
810 impl XsdBridgeLauncher for CountingLauncher {
811 async fn start(
812 &self,
813 _version: &str,
814 _cache_dir: &std::path::Path,
815 _timeout_ms: u64,
816 ) -> Result<(Option<BridgeProcess>, Channel), ValidatorError> {
817 let launch = self.launches.fetch_add(1, Ordering::SeqCst) + 1;
818 self.events.lock().unwrap().push(format!("launch:{launch}"));
819 Ok((None, lazy_channel()))
820 }
821
822 async fn stop(&self, process: Option<BridgeProcess>) {
823 assert!(process.is_none(), "tests must not launch a real process");
824 self.stops.fetch_add(1, Ordering::SeqCst);
825 self.events.lock().unwrap().push("stop".to_string());
826 }
827 }
828
829 struct GatedRpc {
830 calls: AtomicUsize,
831 gated_calls: Vec<usize>,
832 fail_call: usize,
833 entered: Notify,
834 release: Notify,
835 events: Events,
836 }
837
838 impl GatedRpc {
839 fn new(gated_calls: Vec<usize>, fail_call: usize) -> Arc<Self> {
840 Arc::new(Self {
841 calls: AtomicUsize::new(0),
842 gated_calls,
843 fail_call,
844 entered: Notify::new(),
845 release: Notify::new(),
846 events: Arc::new(StdMutex::new(Vec::new())),
847 })
848 }
849 }
850
851 #[async_trait]
852 impl XsdBridgeRpc for GatedRpc {
853 async fn register_schema(
854 &self,
855 _channel: Channel,
856 request: RegisterSchemaRequest,
857 ) -> Result<RegisterSchemaResponse, ValidatorError> {
858 let call = self.calls.fetch_add(1, Ordering::SeqCst) + 1;
859 self.events
860 .lock()
861 .unwrap()
862 .push(format!("register:{call}:{}", request.schema_id));
863 if self.gated_calls.contains(&call) {
864 self.entered.notify_one();
865 self.release.notified().await;
866 }
867 if call == self.fail_call {
868 self.events.lock().unwrap().push(format!("fail:{call}"));
869 return Err(ValidatorError::transport_with_source(
870 "test reseed transport failure",
871 tonic::Status::unavailable("test connection lost"),
872 ));
873 }
874 self.events
875 .lock()
876 .unwrap()
877 .push(format!("ack:{call}:{}", request.schema_id));
878 Ok(RegisterSchemaResponse {
879 schema_id: request.schema_id,
880 error: None,
881 })
882 }
883
884 async fn validate_with(
885 &self,
886 _channel: Channel,
887 _request: ValidateWithRequest,
888 ) -> Result<ValidateResponse, ValidatorError> {
889 unreachable!("generation tests exercise readiness and registration")
890 }
891 }
892
893 fn fixture(rpc: &Arc<GatedRpc>) -> (XsdBridgeBackend, Arc<CountingLauncher>) {
894 let mut backend = XsdBridgeBackend::for_test(rpc.clone(), lazy_channel());
895 let launcher = Arc::new(CountingLauncher {
896 launches: AtomicUsize::new(0),
897 stops: AtomicUsize::new(0),
898 events: rpc.events.clone(),
899 });
900 backend.launcher = launcher.clone();
901 (backend, launcher)
902 }
903
904 fn retain(backend: &XsdBridgeBackend, id: &str) {
905 backend
906 .schemas
907 .insert(id.to_string(), Arc::new(sample_set(id.as_bytes())));
908 }
909
910 async fn poll_once<F: Future>(mut future: Pin<&mut F>) -> Poll<F::Output> {
911 poll_fn(|cx| Poll::Ready(future.as_mut().poll(cx))).await
912 }
913
914 mod n1 {
915 use super::*;
916
917 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
918 enum Operation {
919 Register,
920 Validate,
921 }
922
923 impl Operation {
924 async fn run(self, backend: &XsdBridgeBackend) -> Result<(), ValidatorError> {
925 match self {
926 Self::Register => backend
927 .register("xsd-target", &sample_set(b"<target/>"))
928 .await
929 .map(|_| ()),
930 Self::Validate => {
931 backend.validate("xsd-target", b"<target/>".to_vec()).await
932 }
933 }
934 }
935 }
936
937 struct ScriptedRpc {
938 operation: Operation,
939 code: tonic::Code,
940 failures: usize,
941 attempts: AtomicUsize,
942 registrations: StdMutex<Vec<RegisterSchemaRequest>>,
943 validations: AtomicUsize,
944 events: Events,
945 }
946
947 impl ScriptedRpc {
948 fn target(&self, operation: Operation, id: &str) -> Result<(), ValidatorError> {
949 if operation == self.operation && id == "xsd-target" {
950 let attempt = self.attempts.fetch_add(1, Ordering::SeqCst) + 1;
951 self.events
952 .lock()
953 .unwrap()
954 .push(format!("target:{attempt}"));
955 if attempt <= self.failures {
956 return Err(ValidatorError::transport_with_source(
957 "xml-bridge RPC failed",
958 tonic::Status::new(
959 self.code,
960 format!("connection unavailable: attempt {attempt}"),
961 ),
962 ));
963 }
964 }
965 Ok(())
966 }
967 }
968
969 #[async_trait]
970 impl XsdBridgeRpc for ScriptedRpc {
971 async fn register_schema(
972 &self,
973 _channel: Channel,
974 request: RegisterSchemaRequest,
975 ) -> Result<RegisterSchemaResponse, ValidatorError> {
976 self.registrations.lock().unwrap().push(request.clone());
977 self.target(Operation::Register, &request.schema_id)?;
978 self.events
979 .lock()
980 .unwrap()
981 .push(format!("ack:{}", request.schema_id));
982 Ok(RegisterSchemaResponse {
983 schema_id: request.schema_id,
984 error: None,
985 })
986 }
987
988 async fn validate_with(
989 &self,
990 _channel: Channel,
991 request: ValidateWithRequest,
992 ) -> Result<ValidateResponse, ValidatorError> {
993 self.validations.fetch_add(1, Ordering::SeqCst);
994 self.target(Operation::Validate, &request.schema_id)?;
995 Ok(ValidateResponse {
996 valid: true,
997 errors: vec![],
998 error: None,
999 })
1000 }
1001 }
1002
1003 fn fixture(
1004 operation: Operation,
1005 code: tonic::Code,
1006 failures: usize,
1007 ) -> (XsdBridgeBackend, Arc<ScriptedRpc>, Arc<CountingLauncher>) {
1008 let events = Arc::new(StdMutex::new(vec![]));
1009 let rpc = Arc::new(ScriptedRpc {
1010 operation,
1011 code,
1012 failures,
1013 attempts: AtomicUsize::new(0),
1014 registrations: StdMutex::new(vec![]),
1015 validations: AtomicUsize::new(0),
1016 events: events.clone(),
1017 });
1018 let launcher = Arc::new(CountingLauncher {
1019 launches: AtomicUsize::new(0),
1020 stops: AtomicUsize::new(0),
1021 events,
1022 });
1023 let mut backend = XsdBridgeBackend::for_test(rpc.clone(), lazy_channel());
1024 backend.launcher = launcher.clone();
1025 (backend, rpc, launcher)
1026 }
1027
1028 fn assert_size_refusal(error: &ValidatorError) {
1029 assert!(matches!(
1030 error,
1031 ValidatorError::Transport { source: None, .. }
1032 ));
1033 assert!(!XsdBridgeBackend::is_transport_retryable(error));
1034 let message = error.to_string();
1035 assert!(message.len() <= 512);
1036 assert!(!message.contains("private-schema-id"));
1037 assert!(!message.contains("PRIVATE_DOCUMENT"));
1038 }
1039
1040 fn assert_status(error: &ValidatorError, code: tonic::Code, attempt: usize) {
1041 let ValidatorError::Transport {
1042 source: Some(source),
1043 ..
1044 } = error
1045 else {
1046 panic!("expected typed RPC status: {error:?}");
1047 };
1048 let status = source
1049 .downcast_ref::<tonic::Status>()
1050 .expect("tonic source");
1051 assert_eq!(status.code(), code);
1052 assert_eq!(
1053 status.message(),
1054 format!("connection unavailable: attempt {attempt}")
1055 );
1056 }
1057
1058 #[tokio::test]
1059 async fn oversized_validation_is_refused_before_readiness_or_rpc() {
1060 tokio::time::timeout(Duration::from_secs(2), async {
1061 let mut outcomes = vec![];
1062 for cold in [true, false] {
1063 let (backend, rpc, launcher) = fixture(Operation::Validate, tonic::Code::Ok, 0);
1064 if cold {
1065 backend.channel.write().await.take();
1066 backend.slot.state_tx.send(BridgeState::Stopped).unwrap();
1067 } else {
1068 backend.register("private-schema-id", &sample_set(b"<first/>")).await.unwrap();
1069 backend.register("xsd-other", &sample_set(b"<other/>")).await.unwrap();
1070 rpc.registrations.lock().unwrap().clear();
1071 }
1072 let retained: Vec<_> = backend.schemas.iter()
1073 .map(|entry| (entry.key().clone(), entry.value().clone())).collect();
1074 let mut state = backend.slot.state_rx.clone();
1075 state.borrow_and_update();
1076 let mut document = vec![b'x'; XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE + 1];
1077 document[..16].copy_from_slice(b"PRIVATE_DOCUMENT");
1078 let result = backend.validate("private-schema-id", document).await;
1079 let validations = rpc.validations.load(Ordering::SeqCst);
1080 let unchanged = !state.has_changed().unwrap()
1081 && backend.generation.load(Ordering::SeqCst) == 1
1082 && backend.channel.read().await.as_ref().map(|ready| ready.generation)
1083 == if cold { None } else { Some(1) }
1084 && backend.schemas.len() == retained.len()
1085 && retained.iter().all(|(id, set)| Arc::ptr_eq(&backend.schemas.get(id).unwrap(), set));
1086 if !cold {
1087 backend.validate("xsd-other", b"<other/>".to_vec()).await.unwrap();
1088 }
1089 outcomes.push((cold, result, validations, unchanged,
1090 launcher.launches.load(Ordering::SeqCst),
1091 launcher.stops.load(Ordering::SeqCst),
1092 rpc.registrations.lock().unwrap().len()));
1093 }
1094 for (cold, result, validations, unchanged, launches, stops, registrations) in outcomes {
1095 assert_size_refusal(&result.expect_err("oversized validation must be refused"));
1096 assert_eq!((validations, launches, stops, registrations), (0, 0, 0, 0), "cold={cold}");
1097 assert!(unchanged, "refusal must preserve channel, generation, state and cache; cold={cold}");
1098 }
1099 }).await.expect("validation size preflight must finish within deadline");
1100 }
1101
1102 #[tokio::test]
1103 async fn validation_encoded_limit_includes_id_and_overhead() {
1104 tokio::time::timeout(Duration::from_secs(2), async {
1105 let limit = XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE;
1106 assert_eq!(limit, xml_ingress_limit());
1107 assert_eq!(limit, xsd_bridge_decode_limit());
1108 let (backend, rpc, launcher) = fixture(Operation::Validate, tonic::Code::Ok, 0);
1109 for extra in [0, 1] {
1110 let mut request = ValidateWithRequest {
1111 schema_id: "private-schema-id".to_string(),
1112 document: vec![b'x'; limit - 128],
1113 };
1114 let overhead = request.encoded_len() - request.document.len();
1115 request.document.resize(limit - overhead + extra, b'x');
1116 assert!(request.document.len() < limit);
1117 assert_eq!(request.encoded_len(), limit + extra);
1118 let result = backend.validate(&request.schema_id, request.document).await;
1119 if extra == 0 {
1120 result.expect("exact encoded ceiling is admitted");
1121 } else {
1122 assert_size_refusal(
1123 &result.expect_err("ID and protobuf overhead cross ceiling"),
1124 );
1125 }
1126 }
1127 assert_eq!(rpc.validations.load(Ordering::SeqCst), 1);
1128 assert_eq!(launcher.launches.load(Ordering::SeqCst), 0);
1129 assert_eq!(launcher.stops.load(Ordering::SeqCst), 0);
1130 })
1131 .await
1132 .expect("encoded validation boundary must finish within deadline");
1133 }
1134
1135 #[tokio::test]
1136 async fn limit_statuses_do_not_restart_register_or_validate() {
1137 tokio::time::timeout(Duration::from_secs(2), async {
1138 let mut outcomes = vec![];
1139 for operation in [Operation::Register, Operation::Validate] {
1140 for code in [
1141 tonic::Code::ResourceExhausted,
1142 tonic::Code::OutOfRange,
1143 tonic::Code::InvalidArgument,
1144 tonic::Code::FailedPrecondition,
1145 ] {
1146 let (backend, rpc, launcher) = fixture(operation, code, 2);
1147 retain(&backend, "xsd-other");
1148 if operation == Operation::Validate {
1149 retain(&backend, "xsd-target");
1150 }
1151 let mut state = backend.slot.state_rx.clone();
1152 state.borrow_and_update();
1153 let result = operation.run(&backend).await;
1154 backend
1155 .validate("xsd-other", b"<other/>".to_vec())
1156 .await
1157 .unwrap();
1158 let registrations = rpc.registrations.lock().unwrap().len();
1159 outcomes.push((
1160 operation,
1161 code,
1162 result,
1163 rpc.attempts.load(Ordering::SeqCst),
1164 launcher.launches.load(Ordering::SeqCst),
1165 launcher.stops.load(Ordering::SeqCst),
1166 registrations,
1167 backend.channel.read().await.as_ref().unwrap().generation,
1168 !state.has_changed().unwrap(),
1169 ));
1170 }
1171 }
1172 for (
1173 operation,
1174 code,
1175 result,
1176 attempts,
1177 launches,
1178 stops,
1179 registrations,
1180 generation,
1181 unchanged,
1182 ) in outcomes
1183 {
1184 assert_status(&result.expect_err("limit status must surface"), code, 1);
1185 assert_eq!(
1186 (attempts, launches, stops, generation),
1187 (1, 0, 0, 1),
1188 "{operation:?} {code:?}"
1189 );
1190 assert_eq!(registrations, usize::from(operation == Operation::Register));
1191 assert!(unchanged);
1192 }
1193 })
1194 .await
1195 .expect("limit statuses must finish within deadline");
1196 }
1197
1198 #[test]
1199 fn typed_retry_classifier_only_accepts_unavailable() {
1200 let mut mismatches = vec![];
1201 for value in 0..=16 {
1202 let code = tonic::Code::from_i32(value);
1203 for cause in [false, true] {
1204 for message in ["connection unavailable", "document size limit exceeded"] {
1205 let mut status = tonic::Status::new(code, message);
1206 if cause {
1207 status.set_source(Arc::new(std::io::Error::other(
1208 std::io::Error::new(
1209 std::io::ErrorKind::TimedOut,
1210 "h2 cancelled: connection unavailable",
1211 ),
1212 )));
1213 }
1214 let error = ValidatorError::transport_with_source("RPC failed", status);
1215 if XsdBridgeBackend::is_transport_retryable(&error)
1216 != (code == tonic::Code::Unavailable)
1217 {
1218 mismatches.push((code, cause, message));
1219 }
1220 }
1221 }
1222 }
1223 let non_tonic = ValidatorError::transport_with_source(
1224 "connection unavailable",
1225 std::io::Error::other(tonic::Status::unavailable(
1226 "nested status is not direct source",
1227 )),
1228 );
1229 let domain = ValidatorError::Transport {
1230 message: "connection unavailable".into(),
1231 source: None,
1232 };
1233 assert!(
1234 !XsdBridgeBackend::is_transport_retryable(&non_tonic),
1235 "non-tonic source is not process loss"
1236 );
1237 assert!(!XsdBridgeBackend::is_transport_retryable(&domain));
1238 assert!(
1239 mismatches.is_empty(),
1240 "only Unavailable may restart: {mismatches:?}"
1241 );
1242 }
1243
1244 async fn assert_unavailable_retry(failures: usize) {
1245 for operation in [Operation::Register, Operation::Validate] {
1246 let (backend, rpc, launcher) =
1247 fixture(operation, tonic::Code::Unavailable, failures);
1248 let mut set = sample_set(b"<root/>");
1249 set.resources
1250 .insert("child.xsd".into(), b"<child/>".to_vec());
1251 set.aliases.insert("urn:child".into(), "child.xsd".into());
1252 backend.schemas.insert("xsd-other".into(), Arc::new(set));
1253 retain(
1254 &backend,
1255 if operation == Operation::Validate {
1256 "xsd-target"
1257 } else {
1258 "xsd-cached"
1259 },
1260 );
1261 let retained: Vec<_> = backend
1262 .schemas
1263 .iter()
1264 .map(|entry| RegisterSchemaRequest::new(entry.key().clone(), entry.value()))
1265 .collect();
1266 let result = operation.run(&backend).await;
1267 if failures == 1 {
1268 result.unwrap();
1269 } else {
1270 assert_status(
1271 &result.expect_err("second failure must surface"),
1272 tonic::Code::Unavailable,
1273 2,
1274 );
1275 }
1276 assert_eq!(
1277 rpc.attempts.load(Ordering::SeqCst),
1278 2,
1279 "{operation:?}: no third target attempt"
1280 );
1281 assert_eq!(launcher.launches.load(Ordering::SeqCst), 1);
1282 assert_eq!(launcher.stops.load(Ordering::SeqCst), 1);
1283 assert_eq!(backend.generation.load(Ordering::SeqCst), 2);
1284 assert_eq!(backend.channel.read().await.as_ref().unwrap().generation, 2);
1285 let events = rpc.events.lock().unwrap();
1286 assert_eq!(&events[..3], ["target:1", "stop", "launch:1"]);
1287 assert_eq!(
1288 &events[3..5]
1289 .iter()
1290 .cloned()
1291 .collect::<std::collections::BTreeSet<_>>(),
1292 &retained
1293 .iter()
1294 .map(|request| format!("ack:{}", request.schema_id))
1295 .collect()
1296 );
1297 assert_eq!(events[5], "target:2", "all full-set replays precede retry");
1298 let registrations = rpc.registrations.lock().unwrap();
1299 let replay = if operation == Operation::Register {
1300 ®istrations[1..3]
1301 } else {
1302 ®istrations[..]
1303 };
1304 assert_eq!(
1305 registrations.len(),
1306 if operation == Operation::Register {
1307 4
1308 } else {
1309 2
1310 }
1311 );
1312 for expected in retained {
1313 assert!(replay.contains(&expected), "replay preserves full set");
1314 }
1315 }
1316 }
1317
1318 #[tokio::test]
1319 async fn unavailable_retries_register_and_validate_once() {
1320 tokio::time::timeout(Duration::from_secs(2), assert_unavailable_retry(1))
1321 .await
1322 .expect("single retry must finish within deadline");
1323 }
1324
1325 #[tokio::test]
1326 async fn second_unavailable_is_propagated_without_another_restart() {
1327 tokio::time::timeout(Duration::from_secs(2), assert_unavailable_retry(2))
1328 .await
1329 .expect("second failure must finish within deadline");
1330 }
1331 }
1332
1333 #[tokio::test]
1334 async fn oversized_encoded_request_is_non_retryable() {
1335 tokio::time::timeout(Duration::from_secs(5), async {
1336 let rpc = GatedRpc::new(vec![], 0);
1337 let (backend, launcher) = fixture(&rpc);
1338 let mut set = sample_set(b"<xs:schema/>");
1339 let long_key = "k".repeat(XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE);
1340 set.resources.insert(long_key.clone(), vec![1]);
1341 assert!(
1342 RegisterSchemaRequest::new("xsd-large".to_string(), &set).encoded_len()
1343 > XSD_BRIDGE_MAX_REGISTER_REQUEST_SIZE
1344 );
1345 let initial = backend.register("xsd-large", &set).await.err();
1346 let initial_launches = launcher.launches.load(Ordering::SeqCst);
1347 let initial_stops = launcher.stops.load(Ordering::SeqCst);
1348 backend
1351 .schemas
1352 .insert("xsd-large".to_string(), Arc::new(set));
1353 let replay = backend.restart_bridge(1).await.err();
1354 assert_eq!(rpc.calls.load(Ordering::SeqCst), 0, "no registration RPCs");
1355 assert_eq!(initial_launches, 0, "refusal must not restart the bridge");
1356 assert_eq!(initial_stops, 0);
1357 assert_eq!(launcher.launches.load(Ordering::SeqCst), 1);
1358 assert_eq!(launcher.stops.load(Ordering::SeqCst), 2);
1359 assert!(backend.channel.read().await.is_none());
1360 for error in [initial, replay] {
1361 let error = error.expect("oversized registration must be refused");
1362 assert!(matches!(
1363 error,
1364 ValidatorError::Transport { source: None, .. }
1365 ));
1366 assert!(!XsdBridgeBackend::is_transport_retryable(&error));
1367 let message = error.to_string();
1368 assert!(message.len() <= 512);
1369 assert!(message.contains("SCHEMA_SET_REQUEST_SIZE_CAP"));
1370 assert!(!message.contains(&long_key));
1371 }
1372 })
1373 .await
1374 .expect("oversized registration and replay must finish within deadline");
1375 }
1376
1377 #[tokio::test]
1378 async fn concurrent_caller_waits_for_restart_reseed() {
1379 tokio::time::timeout(Duration::from_secs(2), async {
1380 let rpc = GatedRpc::new(vec![1], 0);
1381 let (backend, launcher) = fixture(&rpc);
1382 retain(&backend, "xsd-a");
1383 retain(&backend, "xsd-b");
1384 let mut restart = Box::pin(backend.restart_bridge(1));
1385 assert!(poll_once(restart.as_mut()).await.is_pending());
1386 rpc.entered.notified().await;
1387
1388 let mut caller = Box::pin(backend.ensure_bridge_ready());
1389 assert!(
1390 poll_once(caller.as_mut()).await.is_pending(),
1391 "readiness must stay pending until every reseed acknowledgement"
1392 );
1393 rpc.release.notify_one();
1394 let (restarted, ready) = tokio::join!(restart, caller);
1395 assert_eq!(restarted.unwrap().generation, 2);
1396 assert_eq!(ready.unwrap().generation, 2);
1397 assert_eq!(launcher.launches.load(Ordering::SeqCst), 1);
1398 assert_eq!(rpc.calls.load(Ordering::SeqCst), 2);
1399 assert_eq!(
1400 rpc.events
1401 .lock()
1402 .unwrap()
1403 .iter()
1404 .filter(|e| e.starts_with("ack:"))
1405 .count(),
1406 2
1407 );
1408 })
1409 .await
1410 .expect("restart/readiness must finish within deadline");
1411 }
1412
1413 #[tokio::test]
1414 async fn failed_reseed_unpublishes_and_next_call_reseeds() {
1415 tokio::time::timeout(Duration::from_secs(2), async {
1416 let rpc = GatedRpc::new(vec![], 1);
1417 let (backend, launcher) = fixture(&rpc);
1418 retain(&backend, "xsd-a");
1419 retain(&backend, "xsd-b");
1420 let err = backend
1421 .restart_bridge(1)
1422 .await
1423 .expect_err("first reseed fails");
1424 assert!(XsdBridgeBackend::is_transport_retryable(&err));
1425 assert!(matches!(
1426 *backend.slot.state_rx.borrow(),
1427 BridgeState::Degraded(_)
1428 ));
1429 assert!(
1430 backend.channel.read().await.is_none(),
1431 "failed generation must not remain published"
1432 );
1433 assert!(backend.slot.process.lock().await.is_none());
1434 assert_eq!(
1435 launcher.stops.load(Ordering::SeqCst),
1436 2,
1437 "old and failed replacement must be stopped"
1438 );
1439 assert_eq!(
1440 backend.schemas.len(),
1441 2,
1442 "failure must retain the full replay set"
1443 );
1444
1445 let ready = backend.ensure_bridge_ready().await.unwrap();
1446 assert!(ready.generation > 1);
1447 assert_eq!(
1448 backend.channel.read().await.as_ref().unwrap().generation,
1449 ready.generation
1450 );
1451 assert!(matches!(
1452 *backend.slot.state_rx.borrow(),
1453 BridgeState::Ready { .. }
1454 ));
1455 assert_eq!(launcher.launches.load(Ordering::SeqCst), 2);
1456 assert_eq!(rpc.calls.load(Ordering::SeqCst), 3);
1457 let mut acknowledged: Vec<_> = rpc
1458 .events
1459 .lock()
1460 .unwrap()
1461 .iter()
1462 .filter(|e| e.starts_with("ack:"))
1463 .map(|e| e.rsplit(':').next().unwrap().to_string())
1464 .collect();
1465 acknowledged.sort();
1466 assert_eq!(acknowledged, ["xsd-a", "xsd-b"]);
1467 })
1468 .await
1469 .expect("failed replay and subsequent startup must finish within deadline");
1470 }
1471
1472 #[tokio::test]
1473 async fn concurrent_transport_retries_restart_once() {
1474 tokio::time::timeout(Duration::from_secs(2), async {
1475 let rpc = GatedRpc::new(vec![1], 0);
1476 let (backend, launcher) = fixture(&rpc);
1477 retain(&backend, "xsd-a");
1478 let mut first = Box::pin(backend.restart_bridge(1));
1479 assert!(poll_once(first.as_mut()).await.is_pending());
1480 rpc.entered.notified().await;
1481 let mut second = Box::pin(backend.restart_bridge(1));
1482 assert!(poll_once(second.as_mut()).await.is_pending());
1483 rpc.release.notify_one();
1484 let (first, second) = tokio::join!(first, second);
1485 assert_eq!(
1486 launcher.launches.load(Ordering::SeqCst),
1487 1,
1488 "two failures from one generation need one replacement"
1489 );
1490 assert_eq!(first.unwrap().generation, 2);
1491 assert_eq!(second.unwrap().generation, 2);
1492 assert_eq!(rpc.calls.load(Ordering::SeqCst), 1);
1493 })
1494 .await
1495 .expect("concurrent restarts must finish within deadline");
1496 }
1497
1498 #[tokio::test]
1499 async fn stale_generation_restart_reuses_current() {
1500 tokio::time::timeout(Duration::from_secs(2), async {
1501 let rpc = GatedRpc::new(vec![], 0);
1502 let (backend, launcher) = fixture(&rpc);
1503 assert_eq!(backend.restart_bridge(1).await.unwrap().generation, 2);
1505 launcher.launches.store(0, Ordering::SeqCst);
1506 launcher.stops.store(0, Ordering::SeqCst);
1507 rpc.events.lock().unwrap().clear();
1508
1509 let ready = backend.restart_bridge(1).await.unwrap();
1510 assert_eq!(
1511 launcher.launches.load(Ordering::SeqCst),
1512 0,
1513 "a late generation-1 failure must reuse generation 2"
1514 );
1515 assert_eq!(launcher.stops.load(Ordering::SeqCst), 0);
1516 assert_eq!(ready.generation, 2);
1517 assert!(rpc.events.lock().unwrap().is_empty());
1518 })
1519 .await
1520 .expect("stale restart must finish within deadline");
1521 }
1522
1523 #[tokio::test]
1524 async fn stale_generation_ack_is_reregistered() {
1525 tokio::time::timeout(Duration::from_secs(2), async {
1526 let rpc = GatedRpc::new(vec![1, 2], 0);
1527 let (backend, launcher) = fixture(&rpc);
1528 let set = sample_set(b"<stale/>");
1529 let mut registration = Box::pin(backend.register("xsd-stale", &set));
1530 assert!(poll_once(registration.as_mut()).await.is_pending());
1531 rpc.entered.notified().await;
1532 assert_eq!(backend.restart_bridge(1).await.unwrap().generation, 2);
1533 assert!(!backend.schemas.contains_key("xsd-stale"));
1534
1535 rpc.release.notify_one();
1536 assert!(
1537 poll_once(registration.as_mut()).await.is_pending(),
1538 "old acknowledgement must not complete registration on the current generation"
1539 );
1540 rpc.entered.notified().await;
1541 assert!(
1542 !backend.schemas.contains_key("xsd-stale"),
1543 "old acknowledgement must not become a local hit"
1544 );
1545 assert_eq!(
1546 *rpc.events.lock().unwrap(),
1547 [
1548 "register:1:xsd-stale",
1549 "stop",
1550 "launch:1",
1551 "ack:1:xsd-stale",
1552 "register:2:xsd-stale",
1553 ]
1554 );
1555 rpc.release.notify_one();
1556 assert_eq!(registration.await.unwrap(), "xsd-stale");
1557 assert_eq!(
1558 backend.register("xsd-stale", &set).await.unwrap(),
1559 "xsd-stale"
1560 );
1561 assert_eq!(
1562 rpc.calls.load(Ordering::SeqCst),
1563 2,
1564 "local reuse is allowed only after the current acknowledgement"
1565 );
1566 assert_eq!(launcher.launches.load(Ordering::SeqCst), 1);
1567 assert_eq!(
1568 *rpc.events.lock().unwrap(),
1569 [
1570 "register:1:xsd-stale",
1571 "stop",
1572 "launch:1",
1573 "ack:1:xsd-stale",
1574 "register:2:xsd-stale",
1575 "ack:2:xsd-stale",
1576 ]
1577 );
1578 })
1579 .await
1580 .expect("stale acknowledgement recovery must finish within deadline");
1581 }
1582
1583 #[tokio::test]
1584 async fn cached_register_waits_for_successful_generation() {
1585 tokio::time::timeout(Duration::from_secs(2), async {
1586 let mut outcomes = Vec::new();
1587 for failed_reseed in [false, true] {
1588 let rpc = if failed_reseed {
1589 GatedRpc::new(vec![2], 1)
1590 } else {
1591 GatedRpc::new(vec![1], 0)
1592 };
1593 let (backend, launcher) = fixture(&rpc);
1594 retain(&backend, "xsd-cached");
1595 let set = sample_set(b"xsd-cached");
1596 let mut restart = Box::pin(backend.restart_bridge(1));
1597 if failed_reseed {
1598 let err = restart.as_mut().await.expect_err("first reseed fails");
1599 assert!(XsdBridgeBackend::is_transport_retryable(&err));
1600 assert!(matches!(
1601 *backend.slot.state_rx.borrow(),
1602 BridgeState::Degraded(_)
1603 ));
1604 assert!(backend.channel.read().await.is_none());
1605 assert_eq!(launcher.stops.load(Ordering::SeqCst), 2);
1606 } else {
1607 assert!(poll_once(restart.as_mut()).await.is_pending());
1608 rpc.entered.notified().await;
1609 }
1610
1611 let mut cached = Box::pin(backend.register("xsd-cached", &set));
1612 let observed = poll_once(cached.as_mut()).await;
1613 let pending = observed.is_pending();
1614 if failed_reseed && pending {
1615 rpc.entered.notified().await;
1616 }
1617 let launches = launcher.launches.load(Ordering::SeqCst);
1618 let registrations = rpc.calls.load(Ordering::SeqCst);
1619 assert!(backend.channel.read().await.is_none());
1620 rpc.release.notify_one();
1621
1622 if !failed_reseed {
1623 assert_eq!(restart.await.unwrap().generation, 2);
1624 }
1625 let registered = match observed {
1626 Poll::Ready(result) => result,
1627 Poll::Pending => cached.await,
1628 };
1629 assert_eq!(registered.unwrap(), "xsd-cached");
1630 let current_acknowledged = backend
1631 .channel
1632 .read()
1633 .await
1634 .as_ref()
1635 .is_some_and(|ready| ready.generation > 1);
1636 outcomes.push((
1637 failed_reseed,
1638 pending,
1639 launches,
1640 registrations,
1641 current_acknowledged,
1642 ));
1643 assert_eq!(rpc.calls.load(Ordering::SeqCst), registrations);
1644 }
1645 assert_eq!(
1648 outcomes,
1649 [(false, true, 1, 1, true), (true, true, 2, 2, true)],
1650 "cached register must wait for reseed and recover after failed reseed: \
1651 (failed reseed, pending, launches, registrations, current acknowledged)"
1652 );
1653 })
1654 .await
1655 .expect("cached registration and recovery must finish within deadline");
1656 }
1657 }
1658
1659 #[tokio::test]
1660 async fn xsd_bridge_reconnect_reseeds() {
1661 let rpc = Arc::new(MockRpc::new(true));
1662 let calls = Arc::clone(&rpc.register_calls);
1663 let backend = test_backend(&rpc);
1664
1665 let set_a = sample_set(b"<xsd:a/>");
1666 let set_b = sample_set(b"<xsd:b/>");
1667 let _id_a = backend.register("xsd-a", &set_a).await.unwrap();
1668 let _id_b = backend.register("xsd-b", &set_b).await.unwrap();
1669
1670 backend.on_reconnect(&lazy_channel()).unwrap();
1671 wait_for_register_calls(&rpc, 4).await;
1672
1673 assert!(calls.load(Ordering::SeqCst) >= 4);
1674 }
1675
1676 #[tokio::test]
1677 async fn register_request_carries_root_system_id_and_resources() {
1678 let rpc = Arc::new(MockRpc::new(true));
1679 let backend = test_backend(&rpc);
1680
1681 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1682 resources.insert("root.xsd".to_string(), b"<xs:schema/>".to_vec());
1683 resources.insert("a/child.xsd".to_string(), b"<xs:schema/>".to_vec());
1684 let set = SchemaSet {
1685 root_system_id: "root.xsd".to_string(),
1686 resources,
1687 aliases: BTreeMap::new(),
1688 };
1689
1690 backend.register("xsd-root", &set).await.unwrap();
1691
1692 let captured = rpc.captured.lock().unwrap();
1693 assert_eq!(captured.len(), 1);
1694 let req = &captured[0];
1695 assert_eq!(req.schema_id, "xsd-root");
1696 assert_eq!(req.root_system_id, "root.xsd");
1697 assert_eq!(req.schema, b"<xs:schema/>".to_vec());
1698 assert_eq!(req.resources.len(), 2);
1699 assert_eq!(
1700 req.resources.get("a/child.xsd").map(Vec::as_slice),
1701 Some(b"<xs:schema/>".as_slice())
1702 );
1703 }
1704
1705 #[tokio::test]
1706 async fn register_request_carries_alias_map() {
1707 let rpc = Arc::new(MockRpc::new(true));
1708 let backend = test_backend(&rpc);
1709
1710 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1711 resources.insert("root.xsd".to_string(), b"<xs:schema/>".to_vec());
1712 resources.insert("a/parent.xsd".to_string(), b"<xs:schema/>".to_vec());
1713 resources.insert(
1715 "http://example.com/parent.xsd".to_string(),
1716 b"<xs:schema/>".to_vec(),
1717 );
1718 let mut aliases: BTreeMap<String, String> = BTreeMap::new();
1719 aliases.insert(
1720 "http://example.com/parent.xsd".to_string(),
1721 "a/parent.xsd".to_string(),
1722 );
1723 let set = SchemaSet {
1724 root_system_id: "root.xsd".to_string(),
1725 resources,
1726 aliases,
1727 };
1728
1729 backend.register("xsd-alias", &set).await.unwrap();
1730
1731 let captured = rpc.captured.lock().unwrap();
1732 let req = &captured[0];
1733 assert_eq!(
1734 req.aliases
1735 .get("http://example.com/parent.xsd")
1736 .map(String::as_str),
1737 Some("a/parent.xsd")
1738 );
1739 assert!(req.resources.contains_key("http://example.com/parent.xsd"));
1741 }
1742
1743 #[tokio::test]
1744 async fn register_preserves_schema_aliases() {
1745 let rpc = Arc::new(MockRpc::new(true));
1746 let backend = test_backend(&rpc);
1747
1748 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1751 resources.insert("root.xsd".to_string(), b"<root/>".to_vec());
1752 resources.insert("a/parent.xsd".to_string(), b"<child/>".to_vec());
1753 resources.insert(
1754 "http://example.com/parent.xsd".to_string(),
1755 b"<child/>".to_vec(),
1756 );
1757 let mut aliases: BTreeMap<String, String> = BTreeMap::new();
1758 aliases.insert(
1759 "http://example.com/parent.xsd".to_string(),
1760 "a/parent.xsd".to_string(),
1761 );
1762 let set = SchemaSet {
1763 root_system_id: "root.xsd".to_string(),
1764 resources,
1765 aliases,
1766 };
1767
1768 let expected_resources: HashMap<String, Vec<u8>> = [
1769 ("root.xsd".to_string(), b"<root/>".to_vec()),
1770 ("a/parent.xsd".to_string(), b"<child/>".to_vec()),
1771 (
1772 "http://example.com/parent.xsd".to_string(),
1773 b"<child/>".to_vec(),
1774 ),
1775 ]
1776 .into_iter()
1777 .collect();
1778 let expected_aliases: HashMap<String, String> = [(
1779 "http://example.com/parent.xsd".to_string(),
1780 "a/parent.xsd".to_string(),
1781 )]
1782 .into_iter()
1783 .collect();
1784
1785 backend.register("xsd-keep", &set).await.unwrap();
1786
1787 backend.on_reconnect(&lazy_channel()).unwrap();
1788 wait_for_register_calls(&rpc, 2).await;
1789
1790 let captured = rpc.captured.lock().unwrap();
1791 assert_eq!(captured.len(), 2, "exactly one register and one replay");
1792
1793 for (label, req) in [("initial", &captured[0]), ("replay", &captured[1])] {
1796 assert_eq!(req.schema_id, "xsd-keep", "{label}: schema_id");
1797 assert_eq!(req.root_system_id, "root.xsd", "{label}: root_system_id");
1798 assert_eq!(
1799 req.schema,
1800 b"<root/>".to_vec(),
1801 "{label}: legacy schema field must carry the root bytes"
1802 );
1803 assert_eq!(
1804 req.resources, expected_resources,
1805 "{label}: full resources map must be preserved"
1806 );
1807 assert_eq!(
1808 req.aliases, expected_aliases,
1809 "{label}: full alias map must be preserved"
1810 );
1811 assert_eq!(
1812 req.resources.get("http://example.com/parent.xsd"),
1813 req.resources.get("a/parent.xsd"),
1814 "{label}: alias-URI resource must duplicate its mapped child bytes"
1815 );
1816 }
1817 }
1818
1819 #[tokio::test]
1820 async fn register_does_not_read_filesystem() {
1821 let rpc = Arc::new(MockRpc::new(true));
1822 let backend = test_backend(&rpc);
1823
1824 let dir = tempfile::tempdir().unwrap();
1826 let root = dir.path().join("root.xsd");
1827 std::fs::write(
1828 &root,
1829 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>"#,
1830 )
1831 .unwrap();
1832 let set = crate::schema_set::discover_schema_set(&root, &BTreeMap::new()).unwrap();
1833 drop(dir); backend.register("xsd-nofs", &set).await.unwrap();
1836 backend.on_reconnect(&lazy_channel()).unwrap();
1837 wait_for_register_calls(&rpc, 2).await;
1838
1839 let captured = rpc.captured.lock().unwrap();
1840 assert!(
1841 captured.len() >= 2,
1842 "register and replay must succeed without the files on disk"
1843 );
1844 for req in captured.iter() {
1845 assert!(
1846 req.resources.contains_key(&req.root_system_id),
1847 "root bytes must be retained from the discovered snapshot"
1848 );
1849 }
1850 }
1851
1852 fn write_root_with_child(dir: &std::path::Path, child_bytes: &[u8]) -> std::path::PathBuf {
1856 let root = dir.join("root.xsd");
1857 let child = dir.join("a").join("child.xsd");
1858 std::fs::create_dir_all(child.parent().expect("child parent")).unwrap();
1859 std::fs::write(
1860 &root,
1861 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:include schemaLocation="a/child.xsd"/></xs:schema>"#,
1862 )
1863 .unwrap();
1864 std::fs::write(&child, child_bytes).unwrap();
1865 root
1866 }
1867
1868 #[tokio::test]
1869 async fn changed_child_yields_new_cache_key() {
1870 let dir = tempfile::tempdir().unwrap();
1871 let root = write_root_with_child(
1872 dir.path(),
1873 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element name="a" type="xs:string"/></xs:schema>"#,
1874 );
1875
1876 let no_aliases = BTreeMap::new();
1877 let set1 = crate::schema_set::discover_schema_set(&root, &no_aliases).unwrap();
1878 let id1 = set1.cache_key();
1879 let root_bytes = set1.root_bytes().to_vec();
1880
1881 std::fs::write(
1883 dir.path().join("a").join("child.xsd"),
1884 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element name="b" type="xs:int"/></xs:schema>"#,
1885 )
1886 .unwrap();
1887 let set2 = crate::schema_set::discover_schema_set(&root, &no_aliases).unwrap();
1888 let id2 = set2.cache_key();
1889
1890 assert_ne!(id1, id2, "a changed child must rotate the cache key");
1891 assert_eq!(
1892 root_bytes.as_slice(),
1893 set2.root_bytes(),
1894 "only the child changed; the legacy root bytes must be unchanged"
1895 );
1896
1897 assert_ne!(
1899 id1,
1900 XsdBridgeBackend::schema_id_for(&root_bytes),
1901 "the id must be the full-set cache key, not the root-only helper"
1902 );
1903
1904 let rpc = Arc::new(MockRpc::new(true));
1906 let backend = test_backend(&rpc);
1907 let recorded1 = backend.register(&id1, &set1).await.unwrap();
1908 let recorded2 = backend.register(&id2, &set2).await.unwrap();
1909 assert_ne!(recorded1, recorded2);
1910 {
1911 let captured = rpc.captured.lock().unwrap();
1912 assert_eq!(captured.len(), 2, "one request per distinct cache key");
1913 assert_eq!(captured[0].schema_id, id1);
1914 assert_eq!(captured[1].schema_id, id2);
1915 }
1916
1917 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1922 resources.insert("root.xsd".to_string(), b"<root/>".to_vec());
1923 resources.insert("a/parent.xsd".to_string(), b"<child/>".to_vec());
1924 resources.insert("b/parent.xsd".to_string(), b"<child/>".to_vec());
1925 resources.insert(
1926 "http://example.com/parent.xsd".to_string(),
1927 b"<child/>".to_vec(),
1928 );
1929 let mut aliases_a: BTreeMap<String, String> = BTreeMap::new();
1930 aliases_a.insert(
1931 "http://example.com/parent.xsd".to_string(),
1932 "a/parent.xsd".to_string(),
1933 );
1934 let mut aliases_b = aliases_a.clone();
1935 aliases_b.insert(
1936 "http://example.com/parent.xsd".to_string(),
1937 "b/parent.xsd".to_string(),
1938 );
1939 let set_alias_a = SchemaSet {
1940 root_system_id: "root.xsd".to_string(),
1941 resources: resources.clone(),
1942 aliases: aliases_a,
1943 };
1944 let set_alias_b = SchemaSet {
1945 root_system_id: "root.xsd".to_string(),
1946 resources,
1947 aliases: aliases_b,
1948 };
1949 assert_ne!(
1950 set_alias_a.cache_key(),
1951 set_alias_b.cache_key(),
1952 "an alias-mapping change alone must rotate the cache key"
1953 );
1954 }
1955
1956 #[test]
1957 fn cache_key_is_stable_for_unchanged_set() {
1958 let mut resources: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1959 resources.insert("root.xsd".to_string(), b"<root/>".to_vec());
1960 resources.insert("a/child.xsd".to_string(), b"<child/>".to_vec());
1961 let mut aliases: BTreeMap<String, String> = BTreeMap::new();
1962 aliases.insert(
1963 "http://example.com/child.xsd".to_string(),
1964 "a/child.xsd".to_string(),
1965 );
1966 let set = SchemaSet {
1967 root_system_id: "root.xsd".to_string(),
1968 resources,
1969 aliases,
1970 };
1971 assert_eq!(set.cache_key(), set.cache_key(), "key is deterministic");
1972
1973 let mut reordered: BTreeMap<String, Vec<u8>> = BTreeMap::new();
1976 reordered.insert("a/child.xsd".to_string(), b"<child/>".to_vec());
1977 reordered.insert("root.xsd".to_string(), b"<root/>".to_vec());
1978 let clone = SchemaSet {
1979 root_system_id: "root.xsd".to_string(),
1980 resources: reordered,
1981 aliases: set.aliases.clone(),
1982 };
1983 assert_eq!(
1984 set.cache_key(),
1985 clone.cache_key(),
1986 "insertion order must not affect the key"
1987 );
1988 }
1989
1990 #[tokio::test]
1991 async fn reconnect_reseeds_every_virtual_path() {
1992 let dir = tempfile::tempdir().unwrap();
1993 let root = write_root_with_child(
1994 dir.path(),
1995 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>"#,
1996 );
1997
1998 let mut aliases: BTreeMap<String, String> = BTreeMap::new();
1999 aliases.insert(
2000 "http://example.com/child.xsd".to_string(),
2001 "a/child.xsd".to_string(),
2002 );
2003 let set = crate::schema_set::discover_schema_set(&root, &aliases).unwrap();
2004
2005 assert_eq!(
2008 set.resources.len(),
2009 3,
2010 "expected root, child, and alias-URI keys, got {:?}",
2011 set.resources.keys().collect::<Vec<_>>()
2012 );
2013 let expected_resources: HashMap<String, Vec<u8>> = set
2014 .resources
2015 .iter()
2016 .map(|(key, bytes)| (key.clone(), bytes.clone()))
2017 .collect();
2018 let expected_aliases: HashMap<String, String> = set
2019 .aliases
2020 .iter()
2021 .map(|(key, value)| (key.clone(), value.clone()))
2022 .collect();
2023 let root_system_id = set.root_system_id.clone();
2024 let root_bytes = set.root_bytes().to_vec();
2025 let schema_id = set.cache_key();
2026
2027 let rpc = Arc::new(MockRpc::new(true));
2028 let backend = test_backend(&rpc);
2029 backend.register(&schema_id, &set).await.unwrap();
2030
2031 drop(dir);
2033 backend.on_reconnect(&lazy_channel()).unwrap();
2034 wait_for_register_calls(&rpc, 2).await;
2035
2036 let captured = rpc.captured.lock().unwrap();
2037 assert_eq!(captured.len(), 2, "exactly one register and one replay");
2038 let replay = &captured[1];
2039 assert_eq!(replay.schema_id, schema_id, "schema id");
2040 assert_eq!(replay.root_system_id, root_system_id, "root systemId");
2041 assert_eq!(
2042 replay.schema, root_bytes,
2043 "legacy schema field must carry the root bytes"
2044 );
2045 assert_eq!(
2046 replay.resources, expected_resources,
2047 "replay must carry every virtual path, not just a count"
2048 );
2049 assert_eq!(
2050 replay.aliases, expected_aliases,
2051 "replay must carry the alias map"
2052 );
2053 }
2054
2055 #[tokio::test]
2056 async fn root_without_imports_registers_one_entry() {
2057 let dir = tempfile::tempdir().unwrap();
2058 let root = dir.path().join("root.xsd");
2059 std::fs::write(
2060 &root,
2061 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>"#,
2062 )
2063 .unwrap();
2064
2065 let set = crate::schema_set::discover_schema_set(&root, &BTreeMap::new()).unwrap();
2066 assert_eq!(set.resources.len(), 1, "a single file is a one-entry set");
2067 assert_eq!(set.root_system_id, "root.xsd");
2068 assert!(set.aliases.is_empty());
2069
2070 let rpc = Arc::new(MockRpc::new(true));
2071 let backend = test_backend(&rpc);
2072 let schema_id = set.cache_key();
2073 backend.register(&schema_id, &set).await.unwrap();
2074
2075 let captured = rpc.captured.lock().unwrap();
2076 assert_eq!(captured.len(), 1);
2077 let req = &captured[0];
2078 assert_eq!(req.schema_id, schema_id);
2079 assert_eq!(req.root_system_id, "root.xsd");
2080 assert_eq!(req.resources.len(), 1);
2081 assert_eq!(
2082 req.resources.get("root.xsd").map(Vec::as_slice),
2083 Some(set.root_bytes()),
2084 "the one resource must be the root's bytes"
2085 );
2086 assert!(req.aliases.is_empty());
2087 assert_eq!(
2089 req.schema,
2090 set.root_bytes().to_vec(),
2091 "legacy schema field must stay the root bytes"
2092 );
2093 }
2094
2095 #[test]
2102 fn transport_error_with_tonic_source_classifies_retryable() {
2103 let err = ValidatorError::transport_with_source(
2104 "xml-bridge validate_with RPC failed",
2105 tonic::Status::unavailable("tcp connect error: ConnectionRefused"),
2106 );
2107 assert!(
2108 XsdBridgeBackend::is_transport_retryable(&err),
2109 "a Transport wrapping a tonic Status must classify retryable: {err}"
2110 );
2111 }
2112
2113 #[test]
2118 fn sourceless_domain_transport_is_not_retryable() {
2119 let domain = crate::proto::BridgeError {
2120 kind: crate::proto::bridge_error::Kind::Internal as i32,
2121 message: "upstream transport unavailable".to_string(),
2122 line: 0,
2123 column: 0,
2124 detail: String::new(),
2125 };
2126 let err = ValidatorError::from_bridge_error(&domain);
2127 assert!(
2128 !XsdBridgeBackend::is_transport_retryable(&err),
2129 "a source-less domain Transport must not classify retryable: {err}"
2130 );
2131 }
2132
2133 struct RejectingRpc;
2138
2139 #[async_trait]
2140 impl XsdBridgeRpc for RejectingRpc {
2141 async fn register_schema(
2142 &self,
2143 _channel: Channel,
2144 _request: RegisterSchemaRequest,
2145 ) -> Result<RegisterSchemaResponse, ValidatorError> {
2146 Err(ValidatorError::transport_with_source(
2147 "xml-bridge register_schema RPC failed",
2148 tonic::Status::unavailable("connection refused"),
2149 ))
2150 }
2151
2152 async fn validate_with(
2153 &self,
2154 _channel: Channel,
2155 _request: ValidateWithRequest,
2156 ) -> Result<ValidateResponse, ValidatorError> {
2157 Ok(ValidateResponse {
2158 valid: true,
2159 errors: Vec::new(),
2160 error: None,
2161 })
2162 }
2163 }
2164
2165 #[tokio::test]
2171 async fn reseed_completes_all_registrations_before_returning() {
2172 let rpc = Arc::new(MockRpc::new(true));
2173 let backend = test_backend(&rpc);
2174
2175 let set_a = sample_set(b"<xsd:a/>");
2176 let set_b = sample_set(b"<xsd:b/>");
2177 let _id_a = backend.register("xsd-a", &set_a).await.unwrap();
2178 let _id_b = backend.register("xsd-b", &set_b).await.unwrap();
2179 assert_eq!(rpc.captured.lock().expect("capture lock").len(), 2);
2180
2181 backend.reseed_with_channel(&lazy_channel()).await.unwrap();
2182
2183 let captured = rpc.captured.lock().expect("capture lock");
2184 assert_eq!(
2185 captured.len(),
2186 4,
2187 "both retained sets must be re-registered before the reseed returns"
2188 );
2189 let mut replayed: Vec<&str> = captured[2..]
2190 .iter()
2191 .map(|req| req.schema_id.as_str())
2192 .collect();
2193 replayed.sort_unstable();
2194 assert_eq!(
2195 replayed,
2196 ["xsd-a", "xsd-b"],
2197 "the replay must carry every retained schema id"
2198 );
2199 }
2200
2201 #[tokio::test]
2205 async fn reseed_failure_surfaces_as_error() {
2206 let rpc = Arc::new(RejectingRpc);
2207 let backend =
2208 XsdBridgeBackend::for_test(Arc::clone(&rpc) as Arc<dyn XsdBridgeRpc>, lazy_channel());
2209 backend
2211 .schemas
2212 .insert("xsd-a".to_string(), Arc::new(sample_set(b"<xsd:a/>")));
2213
2214 let err = backend
2215 .reseed_with_channel(&lazy_channel())
2216 .await
2217 .expect_err("a failed re-registration must surface as an error");
2218 assert!(
2219 XsdBridgeBackend::is_transport_retryable(&err),
2220 "the surfaced error keeps its transport classification: {err}"
2221 );
2222 }
2223}