Repository navigation
fix: implement discovery interface with KeyValueStoreManager #3944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ use std::sync::Arc; | |
| use tokio::net::unix::pipe::Receiver; | ||
|
|
||
| use crate::{ | ||
| discovery::{DiscoveryClient, DiscoveryEvent, DiscoveryKey}, | ||
| pipeline::async_trait, | ||
| transports::etcd::{Client as EtcdClient, WatchEvent}, | ||
| }; | ||
|
|
@@ -65,17 +66,15 @@ impl Client { | |
| }) | ||
| } | ||
|
|
||
| // Client with auto-discover instances using etcd | ||
| // Client with auto-discover instances using discovery client | ||
| pub(crate) async fn new_dynamic(endpoint: Endpoint) -> Result<Self> { | ||
| const INSTANCE_REFRESH_PERIOD: Duration = Duration::from_secs(1); | ||
|
|
||
| // create live endpoint watcher | ||
| let Some(etcd_client) = &endpoint.component.drt.etcd_client else { | ||
| anyhow::bail!("Attempt to create a dynamic client on a static endpoint"); | ||
| }; | ||
| // Get the discovery client from DRT | ||
| let discovery_client = endpoint.component.drt.discovery_client().await?; | ||
|
|
||
| let instance_source = | ||
| Self::get_or_create_dynamic_instance_source(etcd_client, &endpoint).await?; | ||
| Self::get_or_create_dynamic_instance_source(discovery_client, &endpoint).await?; | ||
|
|
||
| let client = Client { | ||
| endpoint, | ||
|
|
@@ -194,7 +193,7 @@ impl Client { | |
| } | ||
|
|
||
| async fn get_or_create_dynamic_instance_source( | ||
| etcd_client: &EtcdClient, | ||
| discovery_client: Arc<dyn DiscoveryClient>, | ||
| endpoint: &Endpoint, | ||
| ) -> Result<Arc<InstanceSource>> { | ||
| let drt = endpoint.drt(); | ||
|
|
@@ -209,72 +208,108 @@ impl Client { | |
| } | ||
| } | ||
|
|
||
| let prefix_watcher = etcd_client | ||
| .kv_get_and_watch_prefix(endpoint.etcd_root()) | ||
| .await?; | ||
| // Create discovery key from endpoint info | ||
| let discovery_key = DiscoveryKey::Endpoint { | ||
| namespace: endpoint.component.namespace.name.clone(), | ||
| component: endpoint.component.name.clone(), | ||
| endpoint: endpoint.name.clone(), | ||
| }; | ||
|
|
||
| let (prefix, _watcher, mut kv_event_rx) = prefix_watcher.dissolve(); | ||
| // Start list_and_watch on the discovery client | ||
| let mut discovery_stream = discovery_client | ||
| .list_and_watch(discovery_key.clone()) | ||
| .await?; | ||
|
|
||
| let (watch_tx, watch_rx) = tokio::sync::watch::channel(vec![]); | ||
|
|
||
| let secondary = endpoint.component.drt.runtime.secondary().clone(); | ||
| let endpoint_path = endpoint.path(); | ||
|
|
||
| // this task should be included in the registry | ||
| // currently this is created once per client, but this object/task should only be instantiated | ||
| // once per worker/instance | ||
| // Spawn task to handle discovery events | ||
| secondary.spawn(async move { | ||
| tracing::debug!("Starting endpoint watcher for prefix: {}", prefix); | ||
| tracing::debug!("Starting discovery watcher for endpoint: {}", endpoint_path); | ||
| let mut map = HashMap::new(); | ||
|
|
||
| // Use StreamExt to iterate over the discovery stream | ||
| use futures::StreamExt; | ||
|
|
||
| loop { | ||
| let kv_event = tokio::select! { | ||
| let discovery_event = tokio::select! { | ||
| _ = watch_tx.closed() => { | ||
| tracing::debug!("all watchers have closed; shutting down endpoint watcher for prefix: {prefix}"); | ||
| tracing::debug!("all watchers have closed; shutting down discovery watcher for endpoint: {endpoint_path}"); | ||
| break; | ||
| } | ||
| kv_event = kv_event_rx.recv() => { | ||
| match kv_event { | ||
| Some(kv_event) => kv_event, | ||
| event = discovery_stream.next() => { | ||
| match event { | ||
| Some(Ok(event)) => event, | ||
| Some(Err(e)) => { | ||
| tracing::error!("discovery stream error: {}; shutting down discovery watcher for endpoint: {endpoint_path}", e); | ||
| break; | ||
| } | ||
| None => { | ||
| tracing::debug!("watch stream has closed; shutting down endpoint watcher for prefix: {prefix}"); | ||
| tracing::debug!("discovery stream closed; shutting down discovery watcher for endpoint: {endpoint_path}"); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
Comment on lines
225
to
255
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add runtime cancellation to the discovery watcher to avoid stranded tasks. The spawned watcher exits only on receiver close or stream end. It should also listen to the runtime’s primary cancellation token. Apply: let secondary = endpoint.component.drt.runtime.secondary().clone();
+ let cancel_token = endpoint.drt().primary_token();
let endpoint_path = endpoint.path();
// Spawn task to handle discovery events
secondary.spawn(async move {
tracing::debug!("Starting discovery watcher for endpoint: {}", endpoint_path);
let mut map = HashMap::new();
// Use StreamExt to iterate over the discovery stream
use futures::StreamExt;
loop {
- let discovery_event = tokio::select! {
+ let discovery_event = tokio::select! {
+ _ = cancel_token.cancelled() => {
+ tracing::debug!("runtime cancelled; shutting down discovery watcher for endpoint: {endpoint_path}");
+ break;
+ }
_ = watch_tx.closed() => {
tracing::debug!("all watchers have closed; shutting down discovery watcher for endpoint: {endpoint_path}");
break;
}
event = discovery_stream.next() => {
🤖 Prompt for AI Agents |
||
|
|
||
| match kv_event { | ||
| WatchEvent::Put(kv) => { | ||
| let key = String::from_utf8(kv.key().to_vec()); | ||
| let val = serde_json::from_slice::<Instance>(kv.value()); | ||
| if let (Ok(key), Ok(val)) = (key, val) { | ||
| map.insert(key.clone(), val); | ||
| } else { | ||
| tracing::error!("Unable to parse put endpoint event; shutting down endpoint watcher for prefix: {prefix}"); | ||
| break; | ||
| } | ||
| match discovery_event { | ||
| DiscoveryEvent::Added(discovery_instance) => { | ||
| // Extract fields from discovery instance | ||
| let (namespace, component, endpoint_name, instance_id) = match &discovery_instance { | ||
| crate::discovery::DiscoveryInstance::Endpoint { | ||
| namespace, | ||
| component, | ||
| endpoint, | ||
| instance_id | ||
| } => ( | ||
| namespace.clone(), | ||
| component.clone(), | ||
| endpoint.clone(), | ||
| *instance_id | ||
| ), | ||
| }; | ||
|
|
||
| // For now, create a test Instance with NatsTcp transport | ||
| // TODO: Discovery should store full Instance details, not just metadata | ||
| let instance = Instance { | ||
| component, | ||
| endpoint: endpoint_name, | ||
| namespace, | ||
| instance_id, | ||
| transport: TransportType::NatsTcp("".to_string()), | ||
| }; | ||
|
|
||
| tracing::debug!( | ||
| instance_id = %instance.id(), | ||
| endpoint = %endpoint_path, | ||
| "Discovery: Added instance" | ||
| ); | ||
|
|
||
| // Use instance_id as the key in the HashMap | ||
| map.insert(instance_id.to_string(), instance); | ||
| } | ||
| WatchEvent::Delete(kv) => { | ||
| match String::from_utf8(kv.key().to_vec()) { | ||
| Ok(key) => { map.remove(&key); } | ||
| Err(_) => { | ||
| tracing::error!("Unable to parse delete endpoint event; shutting down endpoint watcher for prefix: {}", prefix); | ||
| break; | ||
| } | ||
| } | ||
| DiscoveryEvent::Removed(instance_id) => { | ||
| tracing::debug!( | ||
| instance_id = %instance_id, | ||
| endpoint = %endpoint_path, | ||
| "Discovery: Removed instance" | ||
| ); | ||
| map.remove(&instance_id.to_string()); | ||
| } | ||
| } | ||
|
|
||
| // Send complete snapshot of current instances | ||
| let instances: Vec<Instance> = map.values().cloned().collect(); | ||
|
|
||
| if watch_tx.send(instances).is_err() { | ||
| tracing::debug!("Unable to send watch updates; shutting down endpoint watcher for prefix: {}", prefix); | ||
| tracing::debug!("Unable to send watch updates; shutting down discovery watcher for endpoint: {}", endpoint_path); | ||
| break; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| tracing::debug!("Completed endpoint watcher for prefix: {prefix}"); | ||
| tracing::debug!("Completed discovery watcher for endpoint: {endpoint_path}"); | ||
| let _ = watch_tx.send(vec![]); | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,8 +118,6 @@ impl EndpointConfigBuilder { | |
| let endpoint_name = endpoint.name.clone(); | ||
| let system_health = endpoint.drt().system_health.clone(); | ||
| let subject = endpoint.subject_to(connection_id); | ||
| let etcd_path = endpoint.etcd_path_with_lease_id(connection_id); | ||
| let etcd_client = endpoint.component.drt.etcd_client.clone(); | ||
|
|
||
| // Register health check target in SystemHealth if provided | ||
| if let Some(health_check_payload) = &health_check_payload { | ||
|
|
@@ -193,35 +191,52 @@ impl EndpointConfigBuilder { | |
| result | ||
| }); | ||
|
|
||
| // make the components service endpoint discovery in etcd | ||
|
|
||
| // client.register_service() | ||
| let info = Instance { | ||
| // Register the endpoint with the discovery service | ||
| let discovery_key = crate::discovery::DiscoveryKey::Endpoint { | ||
| namespace: namespace_name.clone(), | ||
| component: component_name.clone(), | ||
| endpoint: endpoint_name.clone(), | ||
| namespace: namespace_name.clone(), | ||
| instance_id: connection_id, | ||
| transport: TransportType::NatsTcp(subject), | ||
| }; | ||
|
|
||
| let info = serde_json::to_vec_pretty(&info)?; | ||
| // Attempt to get discovery client and register | ||
| let discovery_client = match endpoint.component.drt.discovery_client().await { | ||
| Ok(client) => client, | ||
| Err(e) => { | ||
| tracing::error!( | ||
| component_name, | ||
| endpoint_name, | ||
| error = %e, | ||
| "Unable to get discovery client" | ||
| ); | ||
| endpoint_shutdown_token.cancel(); | ||
| return Err(error!( | ||
| "Unable to get discovery client. Check discovery service configuration" | ||
| )); | ||
| } | ||
| }; | ||
|
|
||
| if let Some(etcd_client) = &etcd_client | ||
| && let Err(e) = etcd_client | ||
| .kv_create(&etcd_path, info, Some(connection_id)) | ||
| .await | ||
| { | ||
| // Use the discovery client's instance_id (which is the connection_id) | ||
| let instance_id = discovery_client.instance_id(); | ||
|
|
||
| if let Err(e) = discovery_client.serve(discovery_key).await { | ||
| tracing::error!( | ||
| component_name, | ||
| endpoint_name, | ||
| error = %e, | ||
| "Unable to register service for discovery" | ||
| "Unable to register endpoint with discovery service" | ||
| ); | ||
| endpoint_shutdown_token.cancel(); | ||
| return Err(error!( | ||
| "Unable to register service for discovery. Check discovery service status" | ||
| "Unable to register endpoint with discovery service. Check discovery service status" | ||
| )); | ||
| } | ||
|
|
||
| tracing::debug!( | ||
| component_name, | ||
| endpoint_name, | ||
| instance_id = %instance_id, | ||
| "Successfully registered endpoint with discovery service" | ||
| ); | ||
|
Comment on lines
+194
to
+239
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainDeregister on shutdown and gate registration on readiness
Recommendations:
I'll verify the core concerns by examining the codebase structure and implementation details. Length of output: 5706 🏁 Script executed: #!/bin/bash
# Search for DiscoveryClient trait definition and deregister methods
rg -nP "trait\s+DiscoveryClient|async\s+fn\s+deregister|async\s+fn\s+serve" --type=rs -A5Length of output: 26 🏁 Script executed: #!/bin/bash
# Search for endpoint shutdown/cleanup patterns and drop implementations
rg -nP "endpoint_shutdown_token|impl.*Drop|cleanup|deregister" --type=rs -C3 lib/runtime/src/component/endpoint.rsLength of output: 26 🏁 Script executed: #!/bin/bash
# Check discovery service implementation for TTL and Delete event handling
fd -e rs -path "*discovery*" -type f | head -20Length of output: 168 🏁 Script executed: #!/bin/bash
# Search for readiness gates or checks before discovery registration
rg -nP "ready|readiness|Ready" lib/runtime/src/component/endpoint.rs -B5 -A5Length of output: 0 Length of output: 955 🏁 Script executed: #!/bin/bash
# Search for serve and deregister methods in discovery client implementations
rg "async fn (serve|deregister)" -B2 -A8Length of output: 2111 🏁 Script executed: #!/bin/bash
# Look for PushEndpoint and its start method
rg "impl PushEndpoint|fn start\(" -B2 -A15 lib/runtime/src/component/endpoint.rsLength of output: 470 🏁 Script executed: #!/bin/bash
# Find discovery module files
find . -path "*/discovery*" -type f -name "*.rs" | head -15Length of output: 256 🏁 Script executed: #!/bin/bash
# Search for endpoint shutdown behavior and where discovery deregistration might occur
rg "endpoint_shutdown_token|\.cancel\(\)" -B2 -A2Length of output: 38482 Length of output: 4320 🏁 Script executed: #!/bin/bash
# Look at DiscoveryInstance structure and lifecycle
rg "pub enum DiscoveryInstance|pub struct DiscoveryInstance" lib/runtime/src/discovery/ -A15Length of output: 949 🏁 Script executed: #!/bin/bash
# Check PushEndpoint.start() return type and whether it signals readiness
rg "pub async fn start" lib/runtime/src/component/endpoint.rs -B2 -A30 | head -60Length of output: 1099 🏁 Script executed: #!/bin/bash
# Search for any deregister or cleanup calls in discovery or component shutdown
rg "deregister|Remove|Delete.*discovery|discovery.*Remove" lib/runtime/src/ -B3 -A3Length of output: 8388 Add deregister() trait method and call on shutdown; gate discovery registration on endpoint readiness The Recommendations:
🤖 Prompt for AI Agents |
||
| task.await??; | ||
|
|
||
| Ok(()) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compilation blocker:
Durationnot in scope and const is unused.This const won’t compile (missing
use std::time::Duration;) and it’s unused. Remove it or import and use it.Apply this minimal fix (remove the unused const):
- const INSTANCE_REFRESH_PERIOD: Duration = Duration::from_secs(1);📝 Committable suggestion
🤖 Prompt for AI Agents