1use anyhow::Result;
5use serde::Deserialize as _;
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use super::{
10 DiscoveryInstance, DiscoveryInstanceId, DiscoveryQuery, ModelCardInstanceId,
11 model_with_updated_taints, validate_event_source_reregistration, validate_model_reregistration,
12};
13
14fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
28where
29 D: serde::Deserializer<'de>,
30 T: Default + serde::Deserialize<'de>,
31{
32 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
33}
34
35#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct DiscoveryMetadata {
39 #[serde(default, deserialize_with = "deserialize_null_default")]
41 endpoints: HashMap<String, DiscoveryInstance>,
42 #[serde(default, deserialize_with = "deserialize_null_default")]
44 model_cards: HashMap<String, DiscoveryInstance>,
45 #[serde(default, deserialize_with = "deserialize_null_default")]
47 event_channels: HashMap<String, DiscoveryInstance>,
48 #[serde(default, deserialize_with = "deserialize_null_default")]
50 event_sources: HashMap<String, DiscoveryInstance>,
51}
52
53impl DiscoveryMetadata {
54 pub fn new() -> Self {
56 Self {
57 endpoints: HashMap::new(),
58 model_cards: HashMap::new(),
59 event_channels: HashMap::new(),
60 event_sources: HashMap::new(),
61 }
62 }
63
64 pub fn register_endpoint(&mut self, instance: DiscoveryInstance) -> Result<()> {
66 match instance.id() {
67 DiscoveryInstanceId::Endpoint(key) => {
68 self.endpoints.insert(key.to_path(), instance);
69 Ok(())
70 }
71 DiscoveryInstanceId::Model(_) => {
72 anyhow::bail!("Cannot register non-endpoint instance as endpoint")
73 }
74 DiscoveryInstanceId::EventChannel(_) => {
75 anyhow::bail!("Cannot register EventChannel instance as endpoint")
76 }
77 DiscoveryInstanceId::EventSource(_) => {
78 anyhow::bail!("Cannot register EventSource instance as endpoint")
79 }
80 }
81 }
82
83 pub fn register_model_card(
85 &mut self,
86 instance: DiscoveryInstance,
87 ) -> Result<DiscoveryInstance> {
88 match instance.id() {
89 DiscoveryInstanceId::Model(key) => match self.model_cards.entry(key.to_path()) {
90 std::collections::hash_map::Entry::Vacant(entry) => {
91 entry.insert(instance.clone());
92 Ok(instance)
93 }
94 std::collections::hash_map::Entry::Occupied(entry) => {
95 validate_model_reregistration(entry.get(), &instance)?;
96 Ok(entry.get().clone())
97 }
98 },
99 DiscoveryInstanceId::Endpoint(_) => {
100 anyhow::bail!("Cannot register non-model-card instance as model card")
101 }
102 DiscoveryInstanceId::EventChannel(_) => {
103 anyhow::bail!("Cannot register EventChannel instance as model card")
104 }
105 DiscoveryInstanceId::EventSource(_) => {
106 anyhow::bail!("Cannot register EventSource instance as model card")
107 }
108 }
109 }
110
111 pub fn update_model_taints(
113 &mut self,
114 id: &ModelCardInstanceId,
115 taints: HashSet<String>,
116 ) -> Result<bool> {
117 let path = id.to_path();
118 let existing = self
119 .model_cards
120 .get_mut(&path)
121 .ok_or_else(|| anyhow::anyhow!("model discovery record {path} is not registered"))?;
122 let candidate = model_with_updated_taints(existing, taints)?;
123 if candidate == *existing {
124 return Ok(false);
125 }
126 *existing = candidate;
127 Ok(true)
128 }
129
130 pub fn unregister_endpoint(&mut self, instance: &DiscoveryInstance) -> Result<()> {
132 match instance.id() {
133 DiscoveryInstanceId::Endpoint(key) => {
134 self.endpoints.remove(&key.to_path());
135 Ok(())
136 }
137 DiscoveryInstanceId::Model(_) => {
138 anyhow::bail!("Cannot unregister non-endpoint instance as endpoint")
139 }
140 DiscoveryInstanceId::EventChannel(_) => {
141 anyhow::bail!("Cannot unregister EventChannel instance as endpoint")
142 }
143 DiscoveryInstanceId::EventSource(_) => {
144 anyhow::bail!("Cannot unregister EventSource instance as endpoint")
145 }
146 }
147 }
148
149 pub fn unregister_model_card(&mut self, instance: &DiscoveryInstance) -> Result<()> {
151 match instance.id() {
152 DiscoveryInstanceId::Model(key) => {
153 self.model_cards.remove(&key.to_path());
154 Ok(())
155 }
156 DiscoveryInstanceId::Endpoint(_) => {
157 anyhow::bail!("Cannot unregister non-model-card instance as model card")
158 }
159 DiscoveryInstanceId::EventChannel(_) => {
160 anyhow::bail!("Cannot unregister EventChannel instance as model card")
161 }
162 DiscoveryInstanceId::EventSource(_) => {
163 anyhow::bail!("Cannot unregister EventSource instance as model card")
164 }
165 }
166 }
167
168 pub fn register_event_channel(&mut self, instance: DiscoveryInstance) -> Result<()> {
170 match instance.id() {
171 DiscoveryInstanceId::EventChannel(key) => {
172 self.event_channels.insert(key.to_path(), instance);
173 Ok(())
174 }
175 DiscoveryInstanceId::Endpoint(_) => {
176 anyhow::bail!("Cannot register Endpoint instance as event channel")
177 }
178 DiscoveryInstanceId::Model(_) => {
179 anyhow::bail!("Cannot register Model instance as event channel")
180 }
181 DiscoveryInstanceId::EventSource(_) => {
182 anyhow::bail!("Cannot register EventSource instance as event channel")
183 }
184 }
185 }
186
187 pub fn unregister_event_channel(&mut self, instance: &DiscoveryInstance) -> Result<()> {
189 match instance.id() {
190 DiscoveryInstanceId::EventChannel(key) => {
191 self.event_channels.remove(&key.to_path());
192 Ok(())
193 }
194 DiscoveryInstanceId::Endpoint(_) => {
195 anyhow::bail!("Cannot unregister Endpoint instance as event channel")
196 }
197 DiscoveryInstanceId::Model(_) => {
198 anyhow::bail!("Cannot unregister Model instance as event channel")
199 }
200 DiscoveryInstanceId::EventSource(_) => {
201 anyhow::bail!("Cannot unregister EventSource instance as event channel")
202 }
203 }
204 }
205
206 pub fn register_event_source(&mut self, instance: DiscoveryInstance) -> Result<()> {
208 match instance.id() {
209 DiscoveryInstanceId::EventSource(key) => {
210 let path = key.to_path();
211 match self.event_sources.entry(path) {
212 std::collections::hash_map::Entry::Vacant(entry) => {
213 entry.insert(instance);
214 Ok(())
215 }
216 std::collections::hash_map::Entry::Occupied(entry) => {
217 validate_event_source_reregistration(entry.get(), &instance)
218 }
219 }
220 }
221 DiscoveryInstanceId::Endpoint(_) => {
222 anyhow::bail!("Cannot register Endpoint instance as event source")
223 }
224 DiscoveryInstanceId::Model(_) => {
225 anyhow::bail!("Cannot register Model instance as event source")
226 }
227 DiscoveryInstanceId::EventChannel(_) => {
228 anyhow::bail!("Cannot register EventChannel instance as event source")
229 }
230 }
231 }
232
233 pub fn unregister_event_source(&mut self, instance: &DiscoveryInstance) -> Result<()> {
235 match instance.id() {
236 DiscoveryInstanceId::EventSource(key) => {
237 self.event_sources.remove(&key.to_path());
238 Ok(())
239 }
240 DiscoveryInstanceId::Endpoint(_) => {
241 anyhow::bail!("Cannot unregister Endpoint instance as event source")
242 }
243 DiscoveryInstanceId::Model(_) => {
244 anyhow::bail!("Cannot unregister Model instance as event source")
245 }
246 DiscoveryInstanceId::EventChannel(_) => {
247 anyhow::bail!("Cannot unregister EventChannel instance as event source")
248 }
249 }
250 }
251
252 pub fn get_all_endpoints(&self) -> Vec<DiscoveryInstance> {
254 self.endpoints.values().cloned().collect()
255 }
256
257 pub fn get_all_model_cards(&self) -> Vec<DiscoveryInstance> {
259 self.model_cards.values().cloned().collect()
260 }
261
262 pub fn get_all_event_channels(&self) -> Vec<DiscoveryInstance> {
264 self.event_channels.values().cloned().collect()
265 }
266
267 pub fn get_all_event_sources(&self) -> Vec<DiscoveryInstance> {
269 self.event_sources.values().cloned().collect()
270 }
271
272 pub fn get_all(&self) -> Vec<DiscoveryInstance> {
274 self.endpoints
275 .values()
276 .chain(self.model_cards.values())
277 .chain(self.event_channels.values())
278 .chain(self.event_sources.values())
279 .cloned()
280 .collect()
281 }
282
283 pub fn filter(&self, query: &DiscoveryQuery) -> Vec<DiscoveryInstance> {
285 let all_instances = match query {
286 DiscoveryQuery::AllEndpoints
287 | DiscoveryQuery::NamespacedEndpoints { .. }
288 | DiscoveryQuery::ComponentEndpoints { .. }
289 | DiscoveryQuery::Endpoint { .. } => self.get_all_endpoints(),
290
291 DiscoveryQuery::AllModels
292 | DiscoveryQuery::NamespacedModels { .. }
293 | DiscoveryQuery::ComponentModels { .. }
294 | DiscoveryQuery::EndpointModels { .. } => self.get_all_model_cards(),
295
296 DiscoveryQuery::EventChannels(_) => self.get_all_event_channels(),
298 DiscoveryQuery::EventSources(_) => self.get_all_event_sources(),
299 };
300
301 filter_instances(all_instances, query)
302 }
303}
304
305impl Default for DiscoveryMetadata {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311fn filter_instances(
313 instances: Vec<DiscoveryInstance>,
314 query: &DiscoveryQuery,
315) -> Vec<DiscoveryInstance> {
316 instances.into_iter().filter(|i| i.matches(query)).collect()
317}
318
319#[derive(Clone, Debug)]
321pub struct MetadataSnapshot {
322 pub instances: HashMap<u64, Arc<DiscoveryMetadata>>,
324 pub generations: HashMap<u64, i64>,
327 pub sequence: u64,
329 pub timestamp: std::time::Instant,
331}
332
333impl MetadataSnapshot {
334 pub fn empty() -> Self {
335 Self {
336 instances: HashMap::new(),
337 generations: HashMap::new(),
338 sequence: 0,
339 timestamp: std::time::Instant::now(),
340 }
341 }
342
343 pub fn has_changes_from(&self, prev: &MetadataSnapshot) -> bool {
347 if self.generations == prev.generations {
348 tracing::trace!(
349 "Snapshot (seq={}): no changes, {} instances",
350 self.sequence,
351 self.instances.len()
352 );
353 return false;
354 }
355
356 let curr_ids: HashSet<u64> = self.generations.keys().copied().collect();
358 let prev_ids: HashSet<u64> = prev.generations.keys().copied().collect();
359
360 let added: Vec<_> = curr_ids
361 .difference(&prev_ids)
362 .map(|id| format!("{:x}", id))
363 .collect();
364 let removed: Vec<_> = prev_ids
365 .difference(&curr_ids)
366 .map(|id| format!("{:x}", id))
367 .collect();
368 let updated: Vec<_> = self
369 .generations
370 .iter()
371 .filter(|(k, v)| prev.generations.get(*k).is_some_and(|pv| pv != *v))
372 .map(|(k, _)| format!("{:x}", k))
373 .collect();
374
375 tracing::info!(
376 "Snapshot (seq={}): {} instances, added={:?}, removed={:?}, updated={:?}",
377 self.sequence,
378 self.instances.len(),
379 added,
380 removed,
381 updated
382 );
383
384 true
385 }
386
387 pub fn filter(&self, query: &DiscoveryQuery) -> Vec<DiscoveryInstance> {
389 self.instances
390 .values()
391 .flat_map(|metadata| metadata.filter(query))
392 .collect()
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::component::{Instance, TransportType};
400 use crate::discovery::{EventChannelQuery, EventSourceQuery};
401
402 #[test]
403 fn authoritative_model_taint_updates_do_not_read_stale_snapshots() {
404 let mut metadata = DiscoveryMetadata::new();
405 let model = DiscoveryInstance::Model {
406 namespace: "ns".to_string(),
407 component: "worker".to_string(),
408 endpoint: "generate".to_string(),
409 instance_id: 7,
410 card_json: serde_json::json!({
411 "runtime_config": {"taints": ["a"]}
412 }),
413 model_suffix: None,
414 };
415 let DiscoveryInstanceId::Model(id) = model.id() else {
416 unreachable!()
417 };
418 metadata.register_model_card(model.clone()).unwrap();
419
420 assert!(
421 metadata
422 .update_model_taints(&id, HashSet::from(["b".to_string()]))
423 .unwrap()
424 );
425 assert!(
426 metadata
427 .update_model_taints(&id, HashSet::from(["a".to_string()]))
428 .unwrap()
429 );
430
431 let stored = metadata.get_all_model_cards().pop().unwrap();
432 let DiscoveryInstance::Model { card_json, .. } = stored else {
433 unreachable!()
434 };
435 assert_eq!(
436 card_json["runtime_config"]["taints"],
437 serde_json::json!(["a"])
438 );
439
440 metadata.unregister_model_card(&model).unwrap();
441 let error = metadata
442 .update_model_taints(&id, HashSet::from(["b".to_string()]))
443 .unwrap_err();
444 assert!(error.to_string().contains("is not registered"));
445 }
446
447 #[test]
448 fn same_id_model_registration_preserves_authoritative_taints() {
449 let mut metadata = DiscoveryMetadata::new();
450 let model = DiscoveryInstance::Model {
451 namespace: "ns".to_string(),
452 component: "worker".to_string(),
453 endpoint: "generate".to_string(),
454 instance_id: 7,
455 card_json: serde_json::json!({
456 "display_name": "model",
457 "runtime_config": {"taints": ["initial"]}
458 }),
459 model_suffix: None,
460 };
461 let DiscoveryInstanceId::Model(id) = model.id() else {
462 unreachable!()
463 };
464 metadata.register_model_card(model.clone()).unwrap();
465 metadata
466 .update_model_taints(&id, HashSet::from(["updated".to_string()]))
467 .unwrap();
468
469 let replayed = metadata.register_model_card(model).unwrap();
470
471 let DiscoveryInstance::Model { card_json, .. } = replayed else {
472 unreachable!()
473 };
474 assert_eq!(
475 card_json["runtime_config"]["taints"],
476 serde_json::json!(["updated"])
477 );
478 assert_eq!(metadata.get_all_model_cards().len(), 1);
479 }
480
481 #[test]
482 fn test_metadata_serde() {
483 let mut metadata = DiscoveryMetadata::new();
484
485 let instance = DiscoveryInstance::Endpoint(Instance {
487 namespace: "test".to_string(),
488 component: "comp1".to_string(),
489 endpoint: "ep1".to_string(),
490 instance_id: 123,
491 transport: TransportType::Nats("nats://localhost:4222".to_string()),
492 device_type: None,
493 request_plane_codec: None,
494 });
495
496 metadata.register_endpoint(instance).unwrap();
497
498 let json = serde_json::to_string(&metadata).unwrap();
500
501 let deserialized: DiscoveryMetadata = serde_json::from_str(&json).unwrap();
503
504 assert_eq!(deserialized.endpoints.len(), 1);
505 assert_eq!(deserialized.model_cards.len(), 0);
506 }
507
508 #[tokio::test]
509 async fn test_metadata_accessors() {
510 let mut metadata = DiscoveryMetadata::new();
511
512 for i in 0..3 {
514 let instance = DiscoveryInstance::Endpoint(Instance {
515 namespace: "test".to_string(),
516 component: "comp1".to_string(),
517 endpoint: format!("ep{}", i),
518 instance_id: i,
519 transport: TransportType::Nats("nats://localhost:4222".to_string()),
520 device_type: None,
521 request_plane_codec: None,
522 });
523 metadata.register_endpoint(instance).unwrap();
524 }
525
526 for i in 0..2 {
528 let instance = DiscoveryInstance::Model {
529 namespace: "test".to_string(),
530 component: "comp1".to_string(),
531 endpoint: format!("ep{}", i),
532 instance_id: i,
533 card_json: serde_json::json!({"model": "test"}),
534 model_suffix: None,
535 };
536 metadata.register_model_card(instance).unwrap();
537 }
538
539 assert_eq!(metadata.get_all_endpoints().len(), 3);
540 assert_eq!(metadata.get_all_model_cards().len(), 2);
541 assert_eq!(metadata.get_all().len(), 5);
542 }
543
544 #[test]
545 fn event_source_registration_filters_and_removes_exact_incarnation() {
546 use crate::discovery::EventScope;
547 use crate::protocols::EndpointId;
548
549 let mut metadata = DiscoveryMetadata::new();
550 let endpoint = EndpointId {
551 namespace: "test".to_string(),
552 component: "worker".to_string(),
553 name: "decode".to_string(),
554 };
555 let source = |publisher_id| DiscoveryInstance::EventSource {
556 scope: EventScope::Endpoint {
557 endpoint: endpoint.clone(),
558 },
559 topic: "kv-events".to_string(),
560 publisher_id,
561 metadata: serde_json::json!({"worker_id": 7, "dp_rank": 0}),
562 };
563
564 let old = source(100);
565 let current = source(205);
566 metadata.register_event_source(old.clone()).unwrap();
567 metadata.register_event_source(old.clone()).unwrap();
568 let mut conflicting = old.clone();
569 let DiscoveryInstance::EventSource {
570 metadata: descriptor,
571 ..
572 } = &mut conflicting
573 else {
574 unreachable!()
575 };
576 *descriptor = serde_json::json!({"worker_id": 7, "dp_rank": 0, "changed": true});
577 assert!(
578 metadata
579 .register_event_source(conflicting)
580 .unwrap_err()
581 .to_string()
582 .contains("cannot change its descriptor")
583 );
584 metadata.register_event_source(current.clone()).unwrap();
585
586 let query =
587 DiscoveryQuery::EventSources(EventSourceQuery::endpoint_topic(endpoint, "kv-events"));
588 assert_eq!(metadata.filter(&query).len(), 2);
589
590 metadata.unregister_event_source(&old).unwrap();
591 assert_eq!(metadata.filter(&query), vec![current]);
592 }
593
594 #[test]
595 fn event_channel_queries_match_exact_scope() {
596 use crate::discovery::{EventScope, EventTransport};
597 use crate::protocols::EndpointId;
598
599 let mut metadata = DiscoveryMetadata::new();
600 let component_scope = EventScope::Component {
601 namespace: "test".to_string(),
602 component: "worker".to_string(),
603 };
604 let endpoint_a = EndpointId {
605 namespace: "test".to_string(),
606 component: "worker".to_string(),
607 name: "a".to_string(),
608 };
609 let endpoint_b = EndpointId {
610 name: "b".to_string(),
611 ..endpoint_a.clone()
612 };
613
614 for (instance_id, scope) in [
615 (1, component_scope),
616 (
617 2,
618 EventScope::Endpoint {
619 endpoint: endpoint_a.clone(),
620 },
621 ),
622 (
623 3,
624 EventScope::Endpoint {
625 endpoint: endpoint_b.clone(),
626 },
627 ),
628 ] {
629 metadata
630 .register_event_channel(DiscoveryInstance::EventChannel {
631 scope,
632 topic: "kv-events".to_string(),
633 instance_id,
634 transport: EventTransport::zmq(format!("tcp://localhost:{instance_id}")),
635 })
636 .unwrap();
637 }
638
639 assert_eq!(metadata.get_all_event_channels().len(), 3);
640 assert_eq!(metadata.get_all().len(), 3);
641 assert_eq!(
642 metadata
643 .filter(&DiscoveryQuery::EventChannels(EventChannelQuery::all()))
644 .len(),
645 3
646 );
647
648 let component = metadata.filter(&DiscoveryQuery::EventChannels(EventChannelQuery::topic(
649 "test",
650 "worker",
651 "kv-events",
652 )));
653 assert_eq!(component.len(), 1);
654
655 let endpoint_a_instances = metadata.filter(&DiscoveryQuery::EventChannels(
656 EventChannelQuery::endpoint_topic(endpoint_a, "kv-events"),
657 ));
658 assert_eq!(endpoint_a_instances.len(), 1);
659 assert_eq!(endpoint_a_instances[0].instance_id(), 2);
660
661 let endpoint_b_instances = metadata.filter(&DiscoveryQuery::EventChannels(
662 EventChannelQuery::endpoint_topic(endpoint_b, "kv-events"),
663 ));
664 assert_eq!(endpoint_b_instances.len(), 1);
665 assert_eq!(endpoint_b_instances[0].instance_id(), 3);
666
667 metadata
668 .unregister_event_channel(&endpoint_a_instances[0])
669 .unwrap();
670 assert_eq!(metadata.get_all_event_channels().len(), 2);
671 assert!(
672 metadata
673 .filter(&DiscoveryQuery::EventChannels(
674 EventChannelQuery::component("other", "worker")
675 ))
676 .is_empty()
677 );
678 }
679
680 #[tokio::test]
681 async fn test_mixed_instances() {
682 use crate::discovery::{EventScope, EventTransport};
683
684 let mut metadata = DiscoveryMetadata::new();
685
686 let endpoint = DiscoveryInstance::Endpoint(Instance {
688 namespace: "test".to_string(),
689 component: "comp1".to_string(),
690 endpoint: "ep1".to_string(),
691 instance_id: 1,
692 transport: TransportType::Nats("nats://localhost:4222".to_string()),
693 device_type: None,
694 request_plane_codec: None,
695 });
696 metadata.register_endpoint(endpoint).unwrap();
697
698 let model = DiscoveryInstance::Model {
699 namespace: "test".to_string(),
700 component: "comp1".to_string(),
701 endpoint: "ep1".to_string(),
702 instance_id: 2,
703 card_json: serde_json::json!({"model": "test"}),
704 model_suffix: None,
705 };
706 metadata.register_model_card(model).unwrap();
707
708 let event_channel = DiscoveryInstance::EventChannel {
709 scope: EventScope::Component {
710 namespace: "test".to_string(),
711 component: "comp1".to_string(),
712 },
713 topic: "test-topic".to_string(),
714 instance_id: 3,
715 transport: EventTransport::zmq("tcp://localhost:5000"),
716 };
717 metadata.register_event_channel(event_channel).unwrap();
718
719 assert_eq!(metadata.get_all().len(), 3);
721 assert_eq!(metadata.get_all_endpoints().len(), 1);
722 assert_eq!(metadata.get_all_model_cards().len(), 1);
723 assert_eq!(metadata.get_all_event_channels().len(), 1);
724 }
725}