1use crate::runtime::Runtime;
5use anyhow::{Context, Result};
6
7use async_nats::jetstream::kv;
8use derive_builder::Builder;
9use derive_getters::Dissolve;
10use futures::StreamExt;
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::{RwLock, mpsc};
14use validator::Validate;
15
16use etcd_client::{
17 Certificate, Compare, CompareOp, DeleteOptions, GetOptions, Identity, LockClient, LockOptions,
18 LockResponse, PutOptions, PutResponse, TlsOptions, Txn, TxnOp, TxnOpResponse, WatchOptions,
19 WatchStream, Watcher,
20};
21pub use etcd_client::{ConnectOptions, KeyValue, LeaseClient};
22use tokio::time::{Duration, Instant, interval};
23use tokio_util::sync::CancellationToken;
24
25mod connector;
26mod lease;
27mod lock;
28
29use connector::Connector;
30use lease::*;
31pub use lock::*;
32
33use super::utils::build_in_runtime;
34use crate::config::environment_names::etcd as env_etcd;
35
36const STARTUP_CONNECT_TIMEOUT: Duration = Duration::from_secs(120);
37const STARTUP_CONNECT_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
38const STARTUP_CONNECT_MAX_BACKOFF: Duration = Duration::from_secs(30);
39const WATCH_RETRY_INITIAL_BACKOFF: Duration = Duration::from_millis(250);
40const WATCH_RETRY_MAX_BACKOFF: Duration = Duration::from_secs(5);
41const WATCH_RESYNC_GET_TIMEOUT: Duration = Duration::from_secs(10);
42
43#[derive(Clone)]
45pub struct Client {
46 connector: Arc<Connector>,
47 primary_lease: u64,
48 runtime: Runtime,
49 rt: Arc<tokio::runtime::Runtime>,
53}
54
55impl std::fmt::Debug for Client {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "etcd::Client primary_lease={}", self.primary_lease)
58 }
59}
60
61impl Client {
62 pub fn builder() -> ClientOptionsBuilder {
63 ClientOptionsBuilder::default()
64 }
65
66 pub async fn new(config: ClientOptions, runtime: Runtime) -> Result<Self> {
75 let runtime_for_lease = runtime.clone();
76
77 let ((connector, lease_id), rt) = build_in_runtime(
78 async move { Self::connect_with_startup_retry(&config, runtime_for_lease).await },
79 1,
80 )
81 .await?;
82
83 Ok(Client {
84 connector,
85 primary_lease: lease_id,
86 rt,
87 runtime,
88 })
89 }
90
91 async fn connect_with_startup_retry(
93 config: &ClientOptions,
94 runtime: Runtime,
95 ) -> Result<(Arc<Connector>, u64)> {
96 let token = runtime.primary_token();
97 let deadline = Instant::now() + STARTUP_CONNECT_TIMEOUT;
98 let mut backoff = STARTUP_CONNECT_INITIAL_BACKOFF;
99
100 loop {
101 if token.is_cancelled() {
102 anyhow::bail!("etcd startup connection cancelled");
103 }
104
105 let attempt = Self::connect_startup_attempt(config, &runtime).await;
106
107 match attempt {
108 Ok(connection) => return Ok(connection),
109 Err(err) => {
110 let now = Instant::now();
111 if now >= deadline {
112 return Err(err);
113 }
114
115 let sleep_duration = backoff.min(deadline.saturating_duration_since(now));
116
117 tracing::warn!(
118 error = %err,
119 retry_in = ?sleep_duration,
120 remaining = ?deadline.saturating_duration_since(now),
121 "etcd not reachable yet; retrying startup connection"
122 );
123
124 tokio::select! {
125 biased;
126
127 _ = token.cancelled() => {
128 anyhow::bail!("etcd startup connection cancelled");
129 }
130
131 _ = tokio::time::sleep(sleep_duration) => {}
132 }
133 backoff = backoff.saturating_mul(2).min(STARTUP_CONNECT_MAX_BACKOFF);
134 }
135 }
136 }
137 }
138
139 async fn connect_startup_attempt(
140 config: &ClientOptions,
141 runtime: &Runtime,
142 ) -> Result<(Arc<Connector>, u64)> {
143 let token = runtime.primary_token();
144 let connector =
145 Connector::new(config.etcd_url.clone(), config.etcd_connect_options.clone()).await?;
146
147 let lease_id = if config.attach_lease {
148 create_lease(connector.clone(), config.lease_ttl, runtime.clone())
149 .await
150 .with_context(|| {
151 format!(
152 "Unable to create lease. Check etcd server status at {}",
153 config.etcd_url.join(", ")
154 )
155 })?
156 } else {
157 0
158 };
159
160 if token.is_cancelled() {
161 anyhow::bail!("etcd startup connection cancelled");
162 }
163
164 Ok((connector, lease_id))
165 }
166
167 fn etcd_client(&self) -> etcd_client::Client {
170 self.connector.get_client()
171 }
172
173 pub fn lease_id(&self) -> u64 {
175 self.primary_lease
176 }
177
178 pub async fn kv_create(
190 &self,
191 key: &str,
192 value: Vec<u8>,
193 lease_id: Option<u64>,
194 ) -> Result<Option<u64>> {
195 let id = lease_id.unwrap_or(self.lease_id());
196 let put_options = PutOptions::new().with_lease(id as i64);
197
198 let txn = Txn::new()
200 .when(vec![Compare::version(key, CompareOp::Equal, 0)]) .and_then(vec![
202 TxnOp::put(key, value, Some(put_options)), ])
204 .or_else(vec![
205 TxnOp::get(key, None), ]);
207
208 let result = self.connector.get_client().kv_client().txn(txn).await?;
210
211 if result.succeeded() {
213 return Ok(None);
214 }
215
216 if let Some(etcd_client::TxnOpResponse::Get(get_resp)) =
218 result.op_responses().into_iter().next()
219 && let Some(kv) = get_resp.kvs().first()
220 {
221 let version = kv.version() as u64;
222 return Ok(Some(version));
223 }
224
225 for resp in result.op_responses() {
227 tracing::warn!(response = ?resp, "kv_create etcd op response");
228 }
229 anyhow::bail!("Unable to create key. Check etcd server status")
230 }
231
232 pub async fn kv_create_or_validate(
234 &self,
235 key: String,
236 value: Vec<u8>,
237 lease_id: Option<u64>,
238 ) -> Result<()> {
239 let id = lease_id.unwrap_or(self.lease_id());
240 let put_options = PutOptions::new().with_lease(id as i64);
241
242 let txn = Txn::new()
245 .when(vec![Compare::version(key.as_str(), CompareOp::Equal, 0)]) .and_then(vec![
247 TxnOp::put(key.as_str(), value.clone(), Some(put_options)), ])
249 .or_else(vec![
250 TxnOp::txn(Txn::new().when(vec![Compare::value(
252 key.as_str(),
253 CompareOp::Equal,
254 value.clone(),
255 )])),
256 ]);
257
258 let result = self.connector.get_client().kv_client().txn(txn).await?;
260
261 if result.succeeded() {
263 Ok(())
264 } else {
265 match result.op_responses().first() {
266 Some(response) => match response {
267 TxnOpResponse::Txn(response) => match response.succeeded() {
268 true => Ok(()),
269 false => anyhow::bail!(
270 "Unable to create or validate key. Check etcd server status"
271 ),
272 },
273 _ => {
274 anyhow::bail!("Unable to validate key operation. Check etcd server status")
275 }
276 },
277 None => anyhow::bail!("Unable to create or validate key. Check etcd server status"),
278 }
279 }
280 }
281
282 pub async fn kv_put(
283 &self,
284 key: impl AsRef<str>,
285 value: impl AsRef<[u8]>,
286 lease_id: Option<u64>,
287 ) -> Result<()> {
288 let id = lease_id.unwrap_or(self.lease_id());
289 let put_options = PutOptions::new().with_lease(id as i64);
290 let _ = self
291 .connector
292 .get_client()
293 .kv_client()
294 .put(key.as_ref(), value.as_ref(), Some(put_options))
295 .await?;
296 Ok(())
297 }
298
299 pub async fn kv_put_with_options(
300 &self,
301 key: impl AsRef<str>,
302 value: impl AsRef<[u8]>,
303 options: Option<PutOptions>,
304 ) -> Result<PutResponse> {
305 let options = options
306 .unwrap_or_default()
307 .with_lease(self.lease_id() as i64);
308 self.connector
309 .get_client()
310 .kv_client()
311 .put(key.as_ref(), value.as_ref(), Some(options))
312 .await
313 .map_err(|err| err.into())
314 }
315
316 pub async fn kv_get(
317 &self,
318 key: impl Into<Vec<u8>>,
319 options: Option<GetOptions>,
320 ) -> Result<Vec<KeyValue>> {
321 let mut get_response = self
322 .connector
323 .get_client()
324 .kv_client()
325 .get(key, options)
326 .await?;
327 Ok(get_response.take_kvs())
328 }
329
330 pub async fn kv_delete(
331 &self,
332 key: impl Into<Vec<u8>>,
333 options: Option<DeleteOptions>,
334 ) -> Result<u64> {
335 self.connector
336 .get_client()
337 .kv_client()
338 .delete(key, options)
339 .await
340 .map(|del_response| del_response.deleted() as u64)
341 .map_err(|err| err.into())
342 }
343
344 pub async fn kv_get_prefix(&self, prefix: impl AsRef<str>) -> Result<Vec<KeyValue>> {
345 let mut get_response = self
346 .connector
347 .get_client()
348 .kv_client()
349 .get(prefix.as_ref(), Some(GetOptions::new().with_prefix()))
350 .await?;
351
352 Ok(get_response.take_kvs())
353 }
354
355 pub async fn lock(
358 &self,
359 key: impl Into<Vec<u8>>,
360 lease_id: Option<u64>,
361 ) -> Result<LockResponse> {
362 let mut lock_client = self.connector.get_client().lock_client();
363 let id = lease_id.unwrap_or(self.lease_id());
364 let options = LockOptions::new().with_lease(id as i64);
365 lock_client
366 .lock(key, Some(options))
367 .await
368 .map_err(|err| err.into())
369 }
370
371 pub async fn unlock(&self, lock_key: impl Into<Vec<u8>>) -> Result<()> {
373 let mut lock_client = self.connector.get_client().lock_client();
374 lock_client
375 .unlock(lock_key)
376 .await
377 .map_err(|err: etcd_client::Error| anyhow::anyhow!(err))?;
378 Ok(())
379 }
380
381 pub async fn kv_watch_prefix(
383 &self,
384 prefix: impl AsRef<str> + std::fmt::Display,
385 ) -> Result<PrefixWatcher> {
386 self.watch_internal(prefix, false).await
387 }
388
389 pub async fn kv_get_and_watch_prefix(
390 &self,
391 prefix: impl AsRef<str> + std::fmt::Display,
392 ) -> Result<PrefixWatcher> {
393 self.watch_internal(prefix, true).await
394 }
395
396 async fn watch_internal(
402 &self,
403 prefix: impl AsRef<str> + std::fmt::Display,
404 include_existing: bool,
405 ) -> Result<PrefixWatcher> {
406 let (mut start_revision, existing_kvs) = self
407 .get_start_revision(prefix.as_ref(), include_existing)
408 .await?;
409
410 let existing_count = existing_kvs.as_ref().map_or(0, |kvs| kvs.len());
412 let (tx, rx) = mpsc::channel(existing_count + 32);
413
414 if let Some(kvs) = existing_kvs {
416 tracing::trace!("sending {} existing kvs", kvs.len());
417 for kv in kvs {
418 tx.send(WatchEvent::Put(kv)).await?;
419 }
420 }
421
422 let connector = self.connector.clone();
424 let prefix_str = prefix.as_ref().to_string();
425 let cancel_token = self.runtime.primary_token();
426 self.rt.spawn(async move {
427 let mut first_connect = true;
428 let mut reconnect = true;
429 while reconnect {
430 if !first_connect {
431 let mut retry_attempt = 0u64;
432 let mut retry_backoff = WATCH_RETRY_INITIAL_BACKOFF;
433 while let Err(err) = Self::resync_watch_prefix(
434 &connector,
435 &prefix_str,
436 &mut start_revision,
437 &tx,
438 &cancel_token,
439 )
440 .await
441 {
442 if tx.is_closed() || cancel_token.is_cancelled() {
443 return;
444 }
445
446 retry_attempt = retry_attempt.saturating_add(1);
447 if retry_attempt == 1 {
448 tracing::warn!(
449 error = %err,
450 prefix = %prefix_str,
451 "failed to resync etcd watch prefix after reconnect; retrying"
452 );
453 } else {
454 tracing::info!(
455 error = %err,
456 prefix = %prefix_str,
457 retry_attempt,
458 backoff_ms = retry_backoff.as_millis(),
459 "still failing to resync etcd watch prefix after reconnect; retrying"
460 );
461 }
462
463 if Self::is_etcd_connection_error(&err) {
464 let deadline = std::time::Instant::now() + Duration::from_secs(10);
465 if let Err(err) = connector.reconnect(deadline).await {
466 tracing::warn!(
467 error = %err,
468 prefix = %prefix_str,
469 "failed to reconnect to ETCD before watch resync; retrying"
470 );
471 }
472 }
473
474 tokio::select! {
475 _ = cancel_token.cancelled() => return,
476 _ = tokio::time::sleep(Self::watch_retry_backoff(retry_backoff)) => {}
477 }
478 retry_backoff =
479 retry_backoff.saturating_mul(2).min(WATCH_RETRY_MAX_BACKOFF);
480 }
481 }
482
483 let watch_stream =
485 match Self::new_watch_stream(&connector, &prefix_str, start_revision).await {
486 Ok(stream) => stream,
487 Err(_) => return,
488 };
489
490 first_connect = false;
491
492 reconnect =
494 Self::monitor_watch_stream(watch_stream, &prefix_str, &mut start_revision, &tx)
495 .await;
496 }
497 });
498
499 Ok(PrefixWatcher {
500 prefix: prefix.as_ref().to_string(),
501 rx,
502 })
503 }
504
505 async fn get_start_revision(
507 &self,
508 prefix: impl AsRef<str> + std::fmt::Display,
509 include_existing: bool,
510 ) -> Result<(i64, Option<Vec<KeyValue>>)> {
511 let mut kv_client = self.connector.get_client().kv_client();
512 let mut get_response = kv_client
513 .get(prefix.as_ref(), Some(GetOptions::new().with_prefix()))
514 .await?;
515
516 let mut start_revision = get_response
518 .header()
519 .ok_or(anyhow::anyhow!("missing header; unable to get revision"))?
520 .revision();
521 tracing::trace!("{prefix}: start_revision: {start_revision}");
522 start_revision += 1;
523
524 let existing_kvs = include_existing.then(|| {
526 let kvs = get_response.take_kvs();
527 tracing::trace!("initial kv count: {:?}", kvs.len());
528 kvs
529 });
530
531 Ok((start_revision, existing_kvs))
532 }
533
534 async fn resync_watch_prefix(
536 connector: &Arc<Connector>,
537 prefix: &str,
538 start_revision: &mut i64,
539 tx: &mpsc::Sender<WatchEvent>,
540 cancel_token: &CancellationToken,
541 ) -> Result<()> {
542 let mut kv_client = connector.get_client().kv_client();
543 let get_result = tokio::select! {
544 _ = cancel_token.cancelled() => anyhow::bail!("watch resync cancelled"),
545 result = tokio::time::timeout(
546 WATCH_RESYNC_GET_TIMEOUT,
547 kv_client.get(prefix, Some(GetOptions::new().with_prefix())),
548 ) => result,
549 };
550 let mut response = get_result
551 .with_context(|| format!("timed out fetching etcd prefix snapshot for '{prefix}'"))?
552 .with_context(|| format!("failed to fetch etcd prefix snapshot for '{prefix}'"))?;
553
554 let header = response
555 .header()
556 .ok_or_else(|| anyhow::anyhow!("missing header during watch resync for '{prefix}'"))?;
557 *start_revision = header.revision() + 1;
558
559 let kvs = response.take_kvs();
560 tracing::warn!(
561 prefix,
562 kv_count = kvs.len(),
563 start_revision = *start_revision,
564 "resyncing etcd watch prefix after reconnect"
565 );
566
567 tokio::select! {
568 _ = cancel_token.cancelled() => anyhow::bail!("watch resync cancelled"),
569 result = tx.send(WatchEvent::Resync(kvs)) => {
570 result.context("failed to send WatchEvent::Resync")
571 }
572 }
573 }
574
575 fn is_etcd_connection_error(err: &anyhow::Error) -> bool {
576 if err.chain().any(|cause| {
577 cause
578 .downcast_ref::<tokio::time::error::Elapsed>()
579 .is_some()
580 }) {
581 return true;
582 }
583
584 err.chain().any(|cause| {
585 let Some(err) = cause.downcast_ref::<etcd_client::Error>() else {
586 return false;
587 };
588
589 match err {
590 etcd_client::Error::IoError(_)
591 | etcd_client::Error::TransportError(_)
592 | etcd_client::Error::EndpointError(_) => true,
593 etcd_client::Error::GRpcStatus(status) => matches!(
594 status.code() as i32,
595 1
597 | 2
599 | 4
601 | 14
603 ),
604 _ => false,
605 }
606 })
607 }
608
609 fn watch_retry_backoff(current: Duration) -> Duration {
610 let max_ms = u64::try_from(current.as_millis()).unwrap_or(u64::MAX);
611 let min_ms = (max_ms / 2).max(1);
612 let jitter_range = max_ms.saturating_sub(min_ms).saturating_add(1);
613 Duration::from_millis(min_ms + rand::random::<u64>() % jitter_range)
614 }
615
616 async fn new_watch_stream(
621 connector: &Arc<Connector>,
622 prefix: &String,
623 start_revision: i64,
624 ) -> Result<WatchStream> {
625 loop {
626 match connector
627 .get_client()
628 .watch_client()
629 .watch(
630 prefix.as_str(),
631 Some(
632 WatchOptions::new()
633 .with_prefix()
634 .with_start_revision(start_revision)
635 .with_prev_key(),
636 ),
637 )
638 .await
639 {
640 Ok((_, watch_stream)) => {
641 tracing::debug!("Watch stream established for prefix '{prefix}'");
642 return Ok(watch_stream);
643 }
644 Err(err) => {
645 tracing::debug!(error = %err, "Failed to establish watch stream for prefix '{}'", prefix);
646 let deadline = std::time::Instant::now() + Duration::from_secs(10);
647 if let Err(err) = connector.reconnect(deadline).await {
648 tracing::error!(
649 "Failed to reconnect to ETCD within 10 secs for watching prefix '{}': {}",
650 prefix,
651 err
652 );
653 return Err(err);
654 }
655 }
657 }
658 }
659 }
660
661 async fn monitor_watch_stream(
667 mut watch_stream: WatchStream,
668 prefix: &String,
669 start_revision: &mut i64,
670 tx: &mpsc::Sender<WatchEvent>,
671 ) -> bool {
672 loop {
673 tokio::select! {
674 maybe_resp = watch_stream.next() => {
675 let response = match maybe_resp {
677 Some(Ok(res)) => res,
678 Some(Err(err)) => {
679 tracing::warn!(error = %err, "Error watching stream for prefix '{}'", prefix);
680 return true; }
682 None => {
683 tracing::warn!("Watch stream unexpectedly closed for prefix '{prefix}'");
684 return true; }
686 };
687
688 *start_revision = match response.header() {
690 Some(header) => header.revision() + 1,
691 None => {
692 tracing::error!("Missing header in watch response for prefix '{prefix}'");
693 return false;
694 }
695 };
696
697 if Self::process_watch_events(response.events(), tx).await.is_err() {
699 return false;
700 };
701 }
702 _ = tx.closed() => {
703 tracing::debug!("no more receivers, stopping watcher");
704 return false;
705 }
706 }
707 }
708 }
709
710 async fn process_watch_events(
715 events: &[etcd_client::Event],
716 tx: &mpsc::Sender<WatchEvent>,
717 ) -> Result<()> {
718 for event in events {
719 let Some(kv) = event.kv() else {
721 continue; };
723
724 match event.event_type() {
726 etcd_client::EventType::Put => {
727 if let Err(err) = tx.send(WatchEvent::Put(kv.clone())).await {
728 tracing::error!("kv watcher error forwarding WatchEvent::Put: {err}");
729 return Err(err.into());
730 }
731 }
732 etcd_client::EventType::Delete => {
733 if tx.send(WatchEvent::Delete(kv.clone())).await.is_err() {
734 return Err(anyhow::anyhow!("failed to send WatchEvent::Delete"));
735 }
736 }
737 }
738 }
739 Ok(())
740 }
741}
742
743#[derive(Dissolve)]
744pub struct PrefixWatcher {
745 prefix: String,
746 rx: mpsc::Receiver<WatchEvent>,
747}
748
749#[derive(Debug)]
750pub enum WatchEvent {
751 Put(KeyValue),
752 Delete(KeyValue),
753 Resync(Vec<KeyValue>),
758}
759
760#[derive(Debug, Clone, Builder, Validate)]
762pub struct ClientOptions {
763 #[validate(length(min = 1))]
764 pub etcd_url: Vec<String>,
765
766 #[builder(default)]
767 pub etcd_connect_options: Option<ConnectOptions>,
768
769 #[builder(default = "true")]
771 pub attach_lease: bool,
772
773 #[builder(default = "default_lease_ttl()")]
775 pub lease_ttl: u64,
776}
777
778impl Default for ClientOptions {
779 fn default() -> Self {
780 let mut connect_options = None;
781
782 if let (Ok(username), Ok(password)) = (
783 std::env::var(env_etcd::auth::ETCD_AUTH_USERNAME),
784 std::env::var(env_etcd::auth::ETCD_AUTH_PASSWORD),
785 ) {
786 connect_options = Some(ConnectOptions::new().with_user(username, password));
788 } else if let (Ok(ca), Ok(cert), Ok(key)) = (
789 std::env::var(env_etcd::auth::ETCD_AUTH_CA),
790 std::env::var(env_etcd::auth::ETCD_AUTH_CLIENT_CERT),
791 std::env::var(env_etcd::auth::ETCD_AUTH_CLIENT_KEY),
792 ) {
793 connect_options = Some(
795 ConnectOptions::new().with_tls(
796 TlsOptions::new()
797 .ca_certificate(Certificate::from_pem(ca))
798 .identity(Identity::from_pem(cert, key)),
799 ),
800 );
801 }
802
803 ClientOptions {
804 etcd_url: default_servers(),
805 etcd_connect_options: connect_options,
806 attach_lease: true,
807 lease_ttl: default_lease_ttl(),
808 }
809 }
810}
811
812fn default_servers() -> Vec<String> {
813 match std::env::var(env_etcd::ETCD_ENDPOINTS) {
814 Ok(possible_list_of_urls) => possible_list_of_urls
815 .split(',')
816 .map(|s| s.to_string())
817 .collect(),
818 Err(_) => vec!["http://localhost:2379".to_string()],
819 }
820}
821
822fn default_lease_ttl() -> u64 {
823 match std::env::var(env_etcd::ETCD_LEASE_TTL) {
824 Ok(raw) => match raw.parse::<u64>() {
825 Ok(ttl) if ttl > 0 => ttl,
826 Ok(_) => {
827 tracing::warn!(
828 "{} must be >= 1; got 0. Falling back to 10.",
829 env_etcd::ETCD_LEASE_TTL
830 );
831 10
832 }
833 Err(err) => {
834 tracing::warn!(
835 "Invalid {}='{}' ({err}). Falling back to 10.",
836 env_etcd::ETCD_LEASE_TTL,
837 raw
838 );
839 10
840 }
841 },
842 Err(_) => 10,
843 }
844}
845
846pub struct KvCache {
848 client: Client,
849 pub prefix: String,
850 cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
851 watcher: Option<PrefixWatcher>,
852}
853
854impl KvCache {
855 pub async fn new(
857 client: Client,
858 prefix: String,
859 initial_values: HashMap<String, Vec<u8>>,
860 ) -> Result<Self> {
861 let mut cache = HashMap::new();
862
863 let existing_kvs = client.kv_get_prefix(&prefix).await?;
865 for kv in existing_kvs {
866 let key = String::from_utf8_lossy(kv.key()).to_string();
867 cache.insert(key, kv.value().to_vec());
868 }
869
870 for (key, value) in initial_values.iter() {
875 let full_key = format!("{}{}", prefix, key);
876 if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(full_key.clone()) {
877 client.kv_put(&full_key, value.clone(), None).await?;
878 e.insert(value.clone());
879 }
880 }
881
882 let watcher = client.kv_get_and_watch_prefix(&prefix).await?;
886
887 let cache = Arc::new(RwLock::new(cache));
888 let mut result = Self {
889 client,
890 prefix,
891 cache,
892 watcher: Some(watcher),
893 };
894
895 result.start_watcher().await?;
897
898 Ok(result)
899 }
900
901 async fn start_watcher(&mut self) -> Result<()> {
903 if let Some(watcher) = self.watcher.take() {
904 let cache = self.cache.clone();
905 let prefix = self.prefix.clone();
906
907 tokio::spawn(async move {
908 let mut rx = watcher.rx;
909
910 while let Some(event) = rx.recv().await {
911 match event {
912 WatchEvent::Put(kv) => {
913 let key = String::from_utf8_lossy(kv.key()).to_string();
914 let value = kv.value().to_vec();
915
916 tracing::trace!("KvCache update: {} = {:?}", key, value);
917 let mut cache_write = cache.write().await;
918 cache_write.insert(key, value);
919 }
920 WatchEvent::Delete(kv) => {
921 let key = String::from_utf8_lossy(kv.key()).to_string();
922
923 tracing::trace!("KvCache delete: {key}");
924 let mut cache_write = cache.write().await;
925 cache_write.remove(&key);
926 }
927 WatchEvent::Resync(kvs) => {
928 let mut replacement = HashMap::with_capacity(kvs.len());
929 for kv in kvs {
930 let key = String::from_utf8_lossy(kv.key()).to_string();
931 let value = kv.value().to_vec();
932 replacement.insert(key, value);
933 }
934
935 tracing::warn!(
936 prefix,
937 new_count = replacement.len(),
938 "KvCache replacing state from etcd watch resync"
939 );
940 let mut cache_write = cache.write().await;
941 *cache_write = replacement;
942 }
943 }
944 }
945
946 tracing::debug!("KvCache watcher for prefix '{prefix}' stopped");
947 });
948 }
949
950 Ok(())
951 }
952
953 pub async fn get(&self, key: &str) -> Option<Vec<u8>> {
955 let full_key = format!("{}{}", self.prefix, key);
956 let cache_read = self.cache.read().await;
957 cache_read.get(&full_key).cloned()
958 }
959
960 pub async fn get_all(&self) -> HashMap<String, Vec<u8>> {
962 let cache_read = self.cache.read().await;
963 cache_read.clone()
964 }
965
966 pub async fn put(&self, key: &str, value: Vec<u8>, lease_id: Option<u64>) -> Result<()> {
968 let full_key = format!("{}{}", self.prefix, key);
969
970 self.client
972 .kv_put(&full_key, value.clone(), lease_id)
973 .await?;
974
975 let mut cache_write = self.cache.write().await;
977 cache_write.insert(full_key, value);
978
979 Ok(())
980 }
981
982 pub async fn delete(&self, key: &str) -> Result<()> {
984 let full_key = format!("{}{}", self.prefix, key);
985
986 self.client.kv_delete(full_key.clone(), None).await?;
988
989 let mut cache_write = self.cache.write().await;
991 cache_write.remove(&full_key);
992
993 Ok(())
994 }
995}
996
997#[cfg(test)]
998mod unit_tests {
999 use super::*;
1000
1001 #[test]
1002 fn classifies_etcd_connection_errors() {
1003 let err = anyhow::Error::new(etcd_client::Error::EndpointError(
1004 "endpoint unavailable".to_string(),
1005 ));
1006 assert!(Client::is_etcd_connection_error(&err));
1007
1008 let err = anyhow::Error::new(etcd_client::Error::IoError(std::io::Error::new(
1009 std::io::ErrorKind::ConnectionRefused,
1010 "connection refused",
1011 )));
1012 assert!(Client::is_etcd_connection_error(&err));
1013
1014 let runtime = tokio::runtime::Builder::new_current_thread()
1015 .enable_time()
1016 .build()
1017 .unwrap();
1018 let elapsed = runtime.block_on(async {
1019 tokio::time::timeout(Duration::from_millis(0), std::future::pending::<()>())
1020 .await
1021 .unwrap_err()
1022 });
1023 let err = anyhow::Error::new(elapsed).context("timed out fetching etcd prefix snapshot");
1024 assert!(Client::is_etcd_connection_error(&err));
1025
1026 let err = anyhow::Error::new(etcd_client::Error::InvalidArgs("bad request".to_string()));
1027 assert!(!Client::is_etcd_connection_error(&err));
1028
1029 let err = anyhow::anyhow!("missing header during watch resync");
1030 assert!(!Client::is_etcd_connection_error(&err));
1031 }
1032}
1033
1034#[cfg(feature = "integration")]
1035#[cfg(test)]
1036mod tests {
1037 use crate::{DistributedRuntime, distributed::DistributedConfig};
1038
1039 use super::*;
1040
1041 #[test]
1042 fn test_ectd_client() {
1043 let rt = Runtime::single_threaded().unwrap();
1044 let rt_clone = rt.clone();
1045 let config = DistributedConfig::from_settings();
1046
1047 rt_clone.primary().block_on(async move {
1048 let drt = DistributedRuntime::new(rt, config).await.unwrap();
1049 test_kv_create_or_validate(drt).await.unwrap();
1050 });
1051 }
1052
1053 async fn test_kv_create_or_validate(drt: DistributedRuntime) -> Result<()> {
1054 let key = "__integration_test_key";
1055 let value = b"test_value";
1056
1057 let client = Client::new(ClientOptions::default(), drt.runtime().clone())
1058 .await
1059 .expect("etcd client should be available");
1060 let lease_id = drt.connection_id();
1061
1062 let result = client.kv_create(key, value.to_vec(), Some(lease_id)).await;
1064 assert!(result.is_ok(), "");
1065
1066 let result = client.kv_create(key, value.to_vec(), Some(lease_id)).await;
1073 assert!(
1074 result.is_ok() && result.unwrap().is_some(),
1075 "Expected Ok(Some(version)) when key already exists"
1076 );
1077
1078 let result = client
1080 .kv_create_or_validate(key.to_string(), value.to_vec(), Some(lease_id))
1081 .await;
1082 assert!(result.is_ok());
1083
1084 let different_value = b"different_value";
1086 let result = client
1087 .kv_create_or_validate(key.to_string(), different_value.to_vec(), Some(lease_id))
1088 .await;
1089 assert!(result.is_err(), "");
1090
1091 Ok(())
1092 }
1093
1094 #[test]
1095 fn test_kv_cache() {
1096 let rt = Runtime::single_threaded().unwrap();
1097 let rt_clone = rt.clone();
1098 let config = DistributedConfig::from_settings();
1099
1100 rt_clone.primary().block_on(async move {
1101 let drt = DistributedRuntime::new(rt, config).await.unwrap();
1102 test_kv_cache_operations(drt).await.unwrap();
1103 });
1104 }
1105
1106 async fn test_kv_cache_operations(drt: DistributedRuntime) -> Result<()> {
1107 let client = Client::new(ClientOptions::default(), drt.runtime().clone())
1109 .await
1110 .expect("etcd client should be available");
1111
1112 let test_id = uuid::Uuid::new_v4().to_string();
1114 let prefix = format!("v1/test_kv_cache_{}/", test_id);
1115
1116 let mut initial_values = HashMap::new();
1118 initial_values.insert("key1".to_string(), b"value1".to_vec());
1119 initial_values.insert("key2".to_string(), b"value2".to_vec());
1120
1121 let kv_cache = KvCache::new(client.clone(), prefix.clone(), initial_values).await?;
1123
1124 let value1 = kv_cache.get("key1").await;
1126 assert_eq!(value1, Some(b"value1".to_vec()));
1127
1128 let value2 = kv_cache.get("key2").await;
1129 assert_eq!(value2, Some(b"value2".to_vec()));
1130
1131 let all_values = kv_cache.get_all().await;
1133 assert_eq!(all_values.len(), 2);
1134 assert_eq!(
1135 all_values.get(&format!("{}key1", prefix)),
1136 Some(&b"value1".to_vec())
1137 );
1138 assert_eq!(
1139 all_values.get(&format!("{}key2", prefix)),
1140 Some(&b"value2".to_vec())
1141 );
1142
1143 kv_cache.put("key3", b"value3".to_vec(), None).await?;
1145
1146 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1148
1149 let value3 = kv_cache.get("key3").await;
1151 assert_eq!(value3, Some(b"value3".to_vec()));
1152
1153 kv_cache
1155 .put("key1", b"updated_value1".to_vec(), None)
1156 .await?;
1157
1158 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1160
1161 let updated_value1 = kv_cache.get("key1").await;
1163 assert_eq!(updated_value1, Some(b"updated_value1".to_vec()));
1164
1165 client
1167 .kv_put(
1168 &format!("{}key2", prefix),
1169 b"external_update".to_vec(),
1170 None,
1171 )
1172 .await?;
1173
1174 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1176
1177 let external_update = kv_cache.get("key2").await;
1179 assert_eq!(external_update, Some(b"external_update".to_vec()));
1180
1181 let etcd_client = client.etcd_client();
1183 let _ = etcd_client
1184 .kv_client()
1185 .delete(
1186 prefix,
1187 Some(etcd_client::DeleteOptions::new().with_prefix()),
1188 )
1189 .await?;
1190
1191 Ok(())
1192 }
1193}