use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tonic::Status;
use tracing::{debug, info, trace, warn};
use xds_cache::{Cache, ShardedCache};
use xds_core::{NodeHash, ResourceRegistry, TypeUrl, XdsResult};
use crate::stream::StreamContext;
use crate::utils::{generate_nonce, NoncePrefix};
use xds_types::envoy::service::discovery::v3::{
DeltaDiscoveryResponse, Resource as ProtoResource,
};
use xds_types::google::protobuf::Any;
#[derive(Debug, Default)]
pub struct ClientResourceState {
subscribed: HashMap<String, String>,
requested: HashSet<String>,
wildcard: bool,
}
impl ClientResourceState {
pub fn new() -> Self {
Self::default()
}
pub fn subscribe(&mut self, names: impl IntoIterator<Item = String>) {
for name in names {
self.requested.insert(name);
}
}
pub fn unsubscribe(&mut self, names: impl IntoIterator<Item = String>) {
for name in names {
self.requested.remove(&name);
self.subscribed.remove(&name);
}
}
pub fn set_wildcard(&mut self, wildcard: bool) {
self.wildcard = wildcard;
}
pub fn is_subscribed(&self, name: &str) -> bool {
self.wildcard || self.requested.contains(name)
}
pub fn mark_sent(&mut self, name: String, version: String) {
self.subscribed.insert(name, version);
}
pub fn client_version(&self, name: &str) -> Option<&str> {
self.subscribed.get(name).map(|s| s.as_str())
}
pub fn mark_removed(&mut self, names: impl IntoIterator<Item = String>) {
for name in names {
self.subscribed.remove(&name);
}
}
}
#[derive(Debug)]
pub struct DeltaHandler {
cache: Arc<ShardedCache>,
registry: Arc<ResourceRegistry>,
}
impl DeltaHandler {
pub fn new(cache: Arc<ShardedCache>, registry: Arc<ResourceRegistry>) -> Self {
Self { cache, registry }
}
#[inline]
pub fn cache(&self) -> &ShardedCache {
&self.cache
}
#[inline]
pub fn registry(&self) -> &ResourceRegistry {
&self.registry
}
pub fn process_request(
&self,
ctx: &StreamContext,
type_url: TypeUrl,
client_state: &mut ClientResourceState,
subscribe: Vec<String>,
unsubscribe: Vec<String>,
node_hash: NodeHash,
) -> XdsResult<Option<DeltaResponse>> {
ctx.record_request();
trace!(
stream = %ctx.id(),
type_url = %type_url,
subscribe = ?subscribe,
unsubscribe = ?unsubscribe,
"processing Delta request"
);
if subscribe.is_empty() && unsubscribe.is_empty() && client_state.requested.is_empty() {
client_state.set_wildcard(true);
} else {
client_state.subscribe(subscribe.clone());
client_state.unsubscribe(unsubscribe.clone());
}
let snapshot = match self.cache.get_snapshot(node_hash) {
Some(s) => s,
None => {
debug!(
stream = %ctx.id(),
node = %node_hash,
"no snapshot available for node"
);
return Ok(None);
}
};
let resources = match snapshot.get_resources(type_url.clone()) {
Some(r) => r,
None => {
debug!(
stream = %ctx.id(),
type_url = %type_url,
"no resources of type in snapshot"
);
return Ok(None);
}
};
let mut updated = Vec::new();
let mut removed = Vec::new();
for (name, resource) in resources.iter() {
if !client_state.is_subscribed(name) {
continue;
}
let version = resources.version();
let client_version = client_state.client_version(name);
if client_version != Some(version) {
updated.push(DeltaResource {
name: name.to_string(),
version: version.to_string(),
resource: resource.clone(),
});
client_state.mark_sent(name.to_string(), version.to_string());
}
}
let current_names: HashSet<&String> = resources.names().collect();
let removed_names: Vec<String> = client_state
.subscribed
.keys()
.filter(|name| !current_names.contains(name))
.cloned()
.collect();
for name in &removed_names {
removed.push(name.clone());
}
client_state.mark_removed(removed_names);
if updated.is_empty() && removed.is_empty() {
return Ok(None);
}
let response = DeltaResponse {
type_url,
resources: updated,
removed_resources: removed,
nonce: generate_nonce(NoncePrefix::Delta),
system_version_info: snapshot.version().to_string(),
};
info!(
stream = %ctx.id(),
type_url = %response.type_url,
updates = response.resources.len(),
removals = response.removed_resources.len(),
"sending Delta response"
);
ctx.record_response();
Ok(Some(response))
}
pub fn handle_ack(&self, ctx: &StreamContext, type_url: TypeUrl, nonce: &str) {
debug!(
stream = %ctx.id(),
type_url = %type_url,
nonce = %nonce,
"received Delta ACK"
);
}
pub fn handle_nack(&self, ctx: &StreamContext, type_url: TypeUrl, nonce: &str, error: &str) {
warn!(
stream = %ctx.id(),
type_url = %type_url,
nonce = %nonce,
error = %error,
"received Delta NACK"
);
}
}
#[derive(Debug, Clone)]
pub struct DeltaResource {
pub name: String,
pub version: String,
pub resource: xds_core::BoxResource,
}
#[derive(Debug, Clone)]
pub struct DeltaResponse {
pub type_url: TypeUrl,
pub resources: Vec<DeltaResource>,
pub removed_resources: Vec<String>,
pub nonce: String,
pub system_version_info: String,
}
pub fn delta_response_to_proto(response: DeltaResponse) -> Result<DeltaDiscoveryResponse, Status> {
let type_url = response.type_url.as_str().to_string();
let resources: Vec<ProtoResource> = response
.resources
.into_iter()
.map(|r| {
let encoded = r
.resource
.encode()
.map_err(|e| Status::internal(format!("failed to encode resource: {}", e)))?;
Ok(ProtoResource {
name: r.name,
aliases: Vec::new(),
version: r.version,
resource: Some(Any {
type_url: encoded.type_url.clone(),
value: encoded.value.clone(),
}),
ttl: None,
cache_control: None,
resource_name: None,
metadata: None,
})
})
.collect::<Result<Vec<_>, Status>>()?;
Ok(DeltaDiscoveryResponse {
system_version_info: response.system_version_info,
resources,
type_url,
removed_resources: response.removed_resources,
removed_resource_names: Vec::new(),
nonce: response.nonce,
control_plane: None,
resource_errors: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_state_subscribe() {
let mut state = ClientResourceState::new();
state.subscribe(["cluster-1".to_string(), "cluster-2".to_string()]);
assert!(state.is_subscribed("cluster-1"));
assert!(state.is_subscribed("cluster-2"));
assert!(!state.is_subscribed("cluster-3"));
}
#[test]
fn client_state_wildcard() {
let mut state = ClientResourceState::new();
state.set_wildcard(true);
assert!(state.is_subscribed("anything"));
assert!(state.is_subscribed("really-anything"));
}
#[test]
fn client_state_unsubscribe() {
let mut state = ClientResourceState::new();
state.subscribe(["cluster-1".to_string()]);
state.mark_sent("cluster-1".to_string(), "v1".to_string());
assert!(state.is_subscribed("cluster-1"));
assert_eq!(state.client_version("cluster-1"), Some("v1"));
state.unsubscribe(["cluster-1".to_string()]);
assert!(!state.is_subscribed("cluster-1"));
assert!(state.client_version("cluster-1").is_none());
}
}