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
55#[derive(Debug, Copy, Clone, Eq, PartialEq)]
56pub enum CompareAndPutOutcome {
57 Updated,
58 Missing,
59 Conflict,
60}
61
62impl std::fmt::Debug for Client {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "etcd::Client primary_lease={}", self.primary_lease)
65 }
66}
67
68impl Client {
69 pub fn builder() -> ClientOptionsBuilder {
70 ClientOptionsBuilder::default()
71 }
72
73 pub async fn new(config: ClientOptions, runtime: Runtime) -> Result<Self> {
82 let runtime_for_lease = runtime.clone();
83
84 let ((connector, lease_id), rt) = build_in_runtime(
85 async move { Self::connect_with_startup_retry(&config, runtime_for_lease).await },
86 1,
87 )
88 .await?;
89
90 Ok(Client {
91 connector,
92 primary_lease: lease_id,
93 rt,
94 runtime,
95 })
96 }
97
98 async fn connect_with_startup_retry(
100 config: &ClientOptions,
101 runtime: Runtime,
102 ) -> Result<(Arc<Connector>, u64)> {
103 let token = runtime.primary_token();
104 let deadline = Instant::now() + STARTUP_CONNECT_TIMEOUT;
105 let mut backoff = STARTUP_CONNECT_INITIAL_BACKOFF;
106
107 loop {
108 if token.is_cancelled() {
109 anyhow::bail!("etcd startup connection cancelled");
110 }
111
112 let attempt = Self::connect_startup_attempt(config, &runtime).await;
113
114 match attempt {
115 Ok(connection) => return Ok(connection),
116 Err(err) => {
117 let now = Instant::now();
118 if now >= deadline {
119 return Err(err);
120 }
121
122 let sleep_duration = backoff.min(deadline.saturating_duration_since(now));
123
124 tracing::warn!(
125 error = %err,
126 retry_in = ?sleep_duration,
127 remaining = ?deadline.saturating_duration_since(now),
128 "etcd not reachable yet; retrying startup connection"
129 );
130
131 tokio::select! {
132 biased;
133
134 _ = token.cancelled() => {
135 anyhow::bail!("etcd startup connection cancelled");
136 }
137
138 _ = tokio::time::sleep(sleep_duration) => {}
139 }
140 backoff = backoff.saturating_mul(2).min(STARTUP_CONNECT_MAX_BACKOFF);
141 }
142 }
143 }
144 }
145
146 async fn connect_startup_attempt(
147 config: &ClientOptions,
148 runtime: &Runtime,
149 ) -> Result<(Arc<Connector>, u64)> {
150 let token = runtime.primary_token();
151 let connector =
152 Connector::new(config.etcd_url.clone(), config.etcd_connect_options.clone()).await?;
153
154 let lease_id = if config.attach_lease {
155 create_lease(connector.clone(), config.lease_ttl, runtime.clone())
156 .await
157 .with_context(|| {
158 format!(
159 "Unable to create lease. Check etcd server status at {}",
160 config.etcd_url.join(", ")
161 )
162 })?
163 } else {
164 0
165 };
166
167 if token.is_cancelled() {
168 anyhow::bail!("etcd startup connection cancelled");
169 }
170
171 Ok((connector, lease_id))
172 }
173
174 fn etcd_client(&self) -> etcd_client::Client {
177 self.connector.get_client()
178 }
179
180 pub fn lease_id(&self) -> u64 {
182 self.primary_lease
183 }
184
185 pub async fn kv_create(
197 &self,
198 key: &str,
199 value: Vec<u8>,
200 lease_id: Option<u64>,
201 ) -> Result<Option<u64>> {
202 let id = lease_id.unwrap_or(self.lease_id());
203 let put_options = PutOptions::new().with_lease(id as i64);
204
205 let txn = Txn::new()
207 .when(vec![Compare::version(key, CompareOp::Equal, 0)]) .and_then(vec![
209 TxnOp::put(key, value, Some(put_options)), ])
211 .or_else(vec![
212 TxnOp::get(key, None), ]);
214
215 let result = self.connector.get_client().kv_client().txn(txn).await?;
217
218 if result.succeeded() {
220 return Ok(None);
221 }
222
223 if let Some(etcd_client::TxnOpResponse::Get(get_resp)) =
225 result.op_responses().into_iter().next()
226 && let Some(kv) = get_resp.kvs().first()
227 {
228 let version = kv.version() as u64;
229 return Ok(Some(version));
230 }
231
232 for resp in result.op_responses() {
234 tracing::warn!(response = ?resp, "kv_create etcd op response");
235 }
236 anyhow::bail!("Unable to create key. Check etcd server status")
237 }
238
239 pub async fn kv_create_or_validate(
241 &self,
242 key: String,
243 value: Vec<u8>,
244 lease_id: Option<u64>,
245 ) -> Result<()> {
246 let id = lease_id.unwrap_or(self.lease_id());
247 let put_options = PutOptions::new().with_lease(id as i64);
248
249 let txn = Txn::new()
252 .when(vec![Compare::version(key.as_str(), CompareOp::Equal, 0)]) .and_then(vec![
254 TxnOp::put(key.as_str(), value.clone(), Some(put_options)), ])
256 .or_else(vec![
257 TxnOp::txn(Txn::new().when(vec![Compare::value(
259 key.as_str(),
260 CompareOp::Equal,
261 value.clone(),
262 )])),
263 ]);
264
265 let result = self.connector.get_client().kv_client().txn(txn).await?;
267
268 if result.succeeded() {
270 Ok(())
271 } else {
272 match result.op_responses().first() {
273 Some(response) => match response {
274 TxnOpResponse::Txn(response) => match response.succeeded() {
275 true => Ok(()),
276 false => anyhow::bail!(
277 "Unable to create or validate key. Check etcd server status"
278 ),
279 },
280 _ => {
281 anyhow::bail!("Unable to validate key operation. Check etcd server status")
282 }
283 },
284 None => anyhow::bail!("Unable to create or validate key. Check etcd server status"),
285 }
286 }
287 }
288
289 pub async fn kv_put(
290 &self,
291 key: impl AsRef<str>,
292 value: impl AsRef<[u8]>,
293 lease_id: Option<u64>,
294 ) -> Result<()> {
295 let id = lease_id.unwrap_or(self.lease_id());
296 let put_options = PutOptions::new().with_lease(id as i64);
297 let _ = self
298 .connector
299 .get_client()
300 .kv_client()
301 .put(key.as_ref(), value.as_ref(), Some(put_options))
302 .await?;
303 Ok(())
304 }
305
306 pub async fn kv_put_with_options(
307 &self,
308 key: impl AsRef<str>,
309 value: impl AsRef<[u8]>,
310 options: Option<PutOptions>,
311 ) -> Result<PutResponse> {
312 let options = options
313 .unwrap_or_default()
314 .with_lease(self.lease_id() as i64);
315 self.connector
316 .get_client()
317 .kv_client()
318 .put(key.as_ref(), value.as_ref(), Some(options))
319 .await
320 .map_err(|err| err.into())
321 }
322
323 pub async fn kv_compare_and_put(
325 &self,
326 key: impl AsRef<str>,
327 expected: impl AsRef<[u8]>,
328 value: impl AsRef<[u8]>,
329 lease_id: Option<u64>,
330 ) -> Result<CompareAndPutOutcome> {
331 let key = key.as_ref();
332 let current = self
333 .connector
334 .get_client()
335 .kv_client()
336 .get(key, None)
337 .await?;
338 let Some(current) = current.kvs().first() else {
339 return Ok(CompareAndPutOutcome::Missing);
340 };
341 if current.value() != expected.as_ref() {
342 return Ok(CompareAndPutOutcome::Conflict);
343 }
344 let expected_mod_revision = current.mod_revision();
345
346 let put_options = PutOptions::new().with_lease(lease_id.unwrap_or(self.lease_id()) as i64);
347 let txn = Txn::new()
348 .when(vec![Compare::mod_revision(
349 key,
350 CompareOp::Equal,
351 expected_mod_revision,
352 )])
353 .and_then(vec![TxnOp::put(
354 key,
355 value.as_ref().to_vec(),
356 Some(put_options),
357 )])
358 .or_else(vec![TxnOp::get(key, None)]);
359
360 let result = self.connector.get_client().kv_client().txn(txn).await?;
361 if result.succeeded() {
362 return Ok(CompareAndPutOutcome::Updated);
363 }
364
365 match result.op_responses().into_iter().next() {
366 Some(TxnOpResponse::Get(response)) if response.kvs().is_empty() => {
367 Ok(CompareAndPutOutcome::Missing)
368 }
369 Some(TxnOpResponse::Get(_)) => Ok(CompareAndPutOutcome::Conflict),
370 response => {
371 tracing::warn!(?response, "unexpected compare-and-put response");
372 anyhow::bail!("Unable to compare and replace key. Check etcd server status")
373 }
374 }
375 }
376
377 pub async fn kv_get(
378 &self,
379 key: impl Into<Vec<u8>>,
380 options: Option<GetOptions>,
381 ) -> Result<Vec<KeyValue>> {
382 let mut get_response = self
383 .connector
384 .get_client()
385 .kv_client()
386 .get(key, options)
387 .await?;
388 Ok(get_response.take_kvs())
389 }
390
391 pub async fn kv_delete(
392 &self,
393 key: impl Into<Vec<u8>>,
394 options: Option<DeleteOptions>,
395 ) -> Result<u64> {
396 self.connector
397 .get_client()
398 .kv_client()
399 .delete(key, options)
400 .await
401 .map(|del_response| del_response.deleted() as u64)
402 .map_err(|err| err.into())
403 }
404
405 pub async fn kv_get_prefix(&self, prefix: impl AsRef<str>) -> Result<Vec<KeyValue>> {
406 let mut get_response = self
407 .connector
408 .get_client()
409 .kv_client()
410 .get(prefix.as_ref(), Some(GetOptions::new().with_prefix()))
411 .await?;
412
413 Ok(get_response.take_kvs())
414 }
415
416 pub async fn lock(
419 &self,
420 key: impl Into<Vec<u8>>,
421 lease_id: Option<u64>,
422 ) -> Result<LockResponse> {
423 let mut lock_client = self.connector.get_client().lock_client();
424 let id = lease_id.unwrap_or(self.lease_id());
425 let options = LockOptions::new().with_lease(id as i64);
426 lock_client
427 .lock(key, Some(options))
428 .await
429 .map_err(|err| err.into())
430 }
431
432 pub async fn unlock(&self, lock_key: impl Into<Vec<u8>>) -> Result<()> {
434 let mut lock_client = self.connector.get_client().lock_client();
435 lock_client
436 .unlock(lock_key)
437 .await
438 .map_err(|err: etcd_client::Error| anyhow::anyhow!(err))?;
439 Ok(())
440 }
441
442 pub async fn kv_watch_prefix(
444 &self,
445 prefix: impl AsRef<str> + std::fmt::Display,
446 ) -> Result<PrefixWatcher> {
447 self.watch_internal(prefix, false).await
448 }
449
450 pub async fn kv_get_and_watch_prefix(
451 &self,
452 prefix: impl AsRef<str> + std::fmt::Display,
453 ) -> Result<PrefixWatcher> {
454 self.watch_internal(prefix, true).await
455 }
456
457 async fn watch_internal(
463 &self,
464 prefix: impl AsRef<str> + std::fmt::Display,
465 include_existing: bool,
466 ) -> Result<PrefixWatcher> {
467 let (mut start_revision, existing_kvs) = self
468 .get_start_revision(prefix.as_ref(), include_existing)
469 .await?;
470
471 let existing_count = existing_kvs.as_ref().map_or(0, |kvs| kvs.len());
473 let (tx, rx) = mpsc::channel(existing_count + 32);
474
475 if let Some(kvs) = existing_kvs {
477 tracing::trace!("sending {} existing kvs", kvs.len());
478 for kv in kvs {
479 tx.send(WatchEvent::Put(kv)).await?;
480 }
481 }
482
483 let connector = self.connector.clone();
485 let prefix_str = prefix.as_ref().to_string();
486 let cancel_token = self.runtime.primary_token();
487 self.rt.spawn(async move {
488 let mut first_connect = true;
489 let mut reconnect = true;
490 while reconnect {
491 if !first_connect {
492 let mut retry_attempt = 0u64;
493 let mut retry_backoff = WATCH_RETRY_INITIAL_BACKOFF;
494 while let Err(err) = Self::resync_watch_prefix(
495 &connector,
496 &prefix_str,
497 &mut start_revision,
498 &tx,
499 &cancel_token,
500 )
501 .await
502 {
503 if tx.is_closed() || cancel_token.is_cancelled() {
504 return;
505 }
506
507 retry_attempt = retry_attempt.saturating_add(1);
508 if retry_attempt == 1 {
509 tracing::warn!(
510 error = %err,
511 prefix = %prefix_str,
512 "failed to resync etcd watch prefix after reconnect; retrying"
513 );
514 } else {
515 tracing::info!(
516 error = %err,
517 prefix = %prefix_str,
518 retry_attempt,
519 backoff_ms = retry_backoff.as_millis(),
520 "still failing to resync etcd watch prefix after reconnect; retrying"
521 );
522 }
523
524 if Self::is_etcd_connection_error(&err) {
525 let deadline = std::time::Instant::now() + Duration::from_secs(10);
526 if let Err(err) = connector.reconnect(deadline).await {
527 tracing::warn!(
528 error = %err,
529 prefix = %prefix_str,
530 "failed to reconnect to ETCD before watch resync; retrying"
531 );
532 }
533 }
534
535 tokio::select! {
536 _ = cancel_token.cancelled() => return,
537 _ = tokio::time::sleep(Self::watch_retry_backoff(retry_backoff)) => {}
538 }
539 retry_backoff =
540 retry_backoff.saturating_mul(2).min(WATCH_RETRY_MAX_BACKOFF);
541 }
542 }
543
544 let watch_stream =
546 match Self::new_watch_stream(&connector, &prefix_str, start_revision).await {
547 Ok(stream) => stream,
548 Err(_) => return,
549 };
550
551 first_connect = false;
552
553 reconnect =
555 Self::monitor_watch_stream(watch_stream, &prefix_str, &mut start_revision, &tx)
556 .await;
557 }
558 });
559
560 Ok(PrefixWatcher {
561 prefix: prefix.as_ref().to_string(),
562 rx,
563 })
564 }
565
566 async fn get_start_revision(
568 &self,
569 prefix: impl AsRef<str> + std::fmt::Display,
570 include_existing: bool,
571 ) -> Result<(i64, Option<Vec<KeyValue>>)> {
572 let mut kv_client = self.connector.get_client().kv_client();
573 let mut get_response = kv_client
574 .get(prefix.as_ref(), Some(GetOptions::new().with_prefix()))
575 .await?;
576
577 let mut start_revision = get_response
579 .header()
580 .ok_or(anyhow::anyhow!("missing header; unable to get revision"))?
581 .revision();
582 tracing::trace!("{prefix}: start_revision: {start_revision}");
583 start_revision += 1;
584
585 let existing_kvs = include_existing.then(|| {
587 let kvs = get_response.take_kvs();
588 tracing::trace!("initial kv count: {:?}", kvs.len());
589 kvs
590 });
591
592 Ok((start_revision, existing_kvs))
593 }
594
595 async fn resync_watch_prefix(
597 connector: &Arc<Connector>,
598 prefix: &str,
599 start_revision: &mut i64,
600 tx: &mpsc::Sender<WatchEvent>,
601 cancel_token: &CancellationToken,
602 ) -> Result<()> {
603 let mut kv_client = connector.get_client().kv_client();
604 let get_result = tokio::select! {
605 _ = cancel_token.cancelled() => anyhow::bail!("watch resync cancelled"),
606 result = tokio::time::timeout(
607 WATCH_RESYNC_GET_TIMEOUT,
608 kv_client.get(prefix, Some(GetOptions::new().with_prefix())),
609 ) => result,
610 };
611 let mut response = get_result
612 .with_context(|| format!("timed out fetching etcd prefix snapshot for '{prefix}'"))?
613 .with_context(|| format!("failed to fetch etcd prefix snapshot for '{prefix}'"))?;
614
615 let header = response
616 .header()
617 .ok_or_else(|| anyhow::anyhow!("missing header during watch resync for '{prefix}'"))?;
618 *start_revision = header.revision() + 1;
619
620 let kvs = response.take_kvs();
621 tracing::warn!(
622 prefix,
623 kv_count = kvs.len(),
624 start_revision = *start_revision,
625 "resyncing etcd watch prefix after reconnect"
626 );
627
628 tokio::select! {
629 _ = cancel_token.cancelled() => anyhow::bail!("watch resync cancelled"),
630 result = tx.send(WatchEvent::Resync(kvs)) => {
631 result.context("failed to send WatchEvent::Resync")
632 }
633 }
634 }
635
636 fn is_etcd_connection_error(err: &anyhow::Error) -> bool {
637 if err.chain().any(|cause| {
638 cause
639 .downcast_ref::<tokio::time::error::Elapsed>()
640 .is_some()
641 }) {
642 return true;
643 }
644
645 err.chain().any(|cause| {
646 let Some(err) = cause.downcast_ref::<etcd_client::Error>() else {
647 return false;
648 };
649
650 match err {
651 etcd_client::Error::IoError(_)
652 | etcd_client::Error::TransportError(_)
653 | etcd_client::Error::EndpointError(_) => true,
654 etcd_client::Error::GRpcStatus(status) => matches!(
655 status.code() as i32,
656 1
658 | 2
660 | 4
662 | 14
664 ),
665 _ => false,
666 }
667 })
668 }
669
670 fn watch_retry_backoff(current: Duration) -> Duration {
671 let max_ms = u64::try_from(current.as_millis()).unwrap_or(u64::MAX);
672 let min_ms = (max_ms / 2).max(1);
673 let jitter_range = max_ms.saturating_sub(min_ms).saturating_add(1);
674 Duration::from_millis(min_ms + rand::random::<u64>() % jitter_range)
675 }
676
677 async fn new_watch_stream(
682 connector: &Arc<Connector>,
683 prefix: &String,
684 start_revision: i64,
685 ) -> Result<WatchStream> {
686 loop {
687 match connector
688 .get_client()
689 .watch_client()
690 .watch(
691 prefix.as_str(),
692 Some(
693 WatchOptions::new()
694 .with_prefix()
695 .with_start_revision(start_revision)
696 .with_prev_key(),
697 ),
698 )
699 .await
700 {
701 Ok((_, watch_stream)) => {
702 tracing::debug!("Watch stream established for prefix '{prefix}'");
703 return Ok(watch_stream);
704 }
705 Err(err) => {
706 tracing::debug!(error = %err, "Failed to establish watch stream for prefix '{}'", prefix);
707 let deadline = std::time::Instant::now() + Duration::from_secs(10);
708 if let Err(err) = connector.reconnect(deadline).await {
709 tracing::error!(
710 "Failed to reconnect to ETCD within 10 secs for watching prefix '{}': {}",
711 prefix,
712 err
713 );
714 return Err(err);
715 }
716 }
718 }
719 }
720 }
721
722 async fn monitor_watch_stream(
728 mut watch_stream: WatchStream,
729 prefix: &String,
730 start_revision: &mut i64,
731 tx: &mpsc::Sender<WatchEvent>,
732 ) -> bool {
733 loop {
734 tokio::select! {
735 maybe_resp = watch_stream.next() => {
736 let response = match maybe_resp {
738 Some(Ok(res)) => res,
739 Some(Err(err)) => {
740 tracing::warn!(error = %err, "Error watching stream for prefix '{}'", prefix);
741 return true; }
743 None => {
744 tracing::warn!("Watch stream unexpectedly closed for prefix '{prefix}'");
745 return true; }
747 };
748
749 *start_revision = match response.header() {
751 Some(header) => header.revision() + 1,
752 None => {
753 tracing::error!("Missing header in watch response for prefix '{prefix}'");
754 return false;
755 }
756 };
757
758 if Self::process_watch_events(response.events(), tx).await.is_err() {
760 return false;
761 };
762 }
763 _ = tx.closed() => {
764 tracing::debug!("no more receivers, stopping watcher");
765 return false;
766 }
767 }
768 }
769 }
770
771 async fn process_watch_events(
776 events: &[etcd_client::Event],
777 tx: &mpsc::Sender<WatchEvent>,
778 ) -> Result<()> {
779 for event in events {
780 let Some(kv) = event.kv() else {
782 continue; };
784
785 match event.event_type() {
787 etcd_client::EventType::Put => {
788 if let Err(err) = tx.send(WatchEvent::Put(kv.clone())).await {
789 tracing::error!("kv watcher error forwarding WatchEvent::Put: {err}");
790 return Err(err.into());
791 }
792 }
793 etcd_client::EventType::Delete => {
794 if tx.send(WatchEvent::Delete(kv.clone())).await.is_err() {
795 return Err(anyhow::anyhow!("failed to send WatchEvent::Delete"));
796 }
797 }
798 }
799 }
800 Ok(())
801 }
802}
803
804#[derive(Dissolve)]
805pub struct PrefixWatcher {
806 prefix: String,
807 rx: mpsc::Receiver<WatchEvent>,
808}
809
810#[derive(Debug)]
811pub enum WatchEvent {
812 Put(KeyValue),
813 Delete(KeyValue),
814 Resync(Vec<KeyValue>),
819}
820
821#[derive(Debug, Clone, Builder, Validate)]
823pub struct ClientOptions {
824 #[validate(length(min = 1))]
825 pub etcd_url: Vec<String>,
826
827 #[builder(default)]
828 pub etcd_connect_options: Option<ConnectOptions>,
829
830 #[builder(default = "true")]
832 pub attach_lease: bool,
833
834 #[builder(default = "default_lease_ttl()")]
836 pub lease_ttl: u64,
837}
838
839impl Default for ClientOptions {
840 fn default() -> Self {
841 let mut connect_options = None;
842
843 if let (Ok(username), Ok(password)) = (
844 std::env::var(env_etcd::auth::ETCD_AUTH_USERNAME),
845 std::env::var(env_etcd::auth::ETCD_AUTH_PASSWORD),
846 ) {
847 connect_options = Some(ConnectOptions::new().with_user(username, password));
849 } else if let (Ok(ca), Ok(cert), Ok(key)) = (
850 std::env::var(env_etcd::auth::ETCD_AUTH_CA),
851 std::env::var(env_etcd::auth::ETCD_AUTH_CLIENT_CERT),
852 std::env::var(env_etcd::auth::ETCD_AUTH_CLIENT_KEY),
853 ) {
854 connect_options = Some(
856 ConnectOptions::new().with_tls(
857 TlsOptions::new()
858 .ca_certificate(Certificate::from_pem(ca))
859 .identity(Identity::from_pem(cert, key)),
860 ),
861 );
862 }
863
864 ClientOptions {
865 etcd_url: default_servers(),
866 etcd_connect_options: connect_options,
867 attach_lease: true,
868 lease_ttl: default_lease_ttl(),
869 }
870 }
871}
872
873fn default_servers() -> Vec<String> {
874 match std::env::var(env_etcd::ETCD_ENDPOINTS) {
875 Ok(possible_list_of_urls) => possible_list_of_urls
876 .split(',')
877 .map(|s| s.to_string())
878 .collect(),
879 Err(_) => vec!["http://localhost:2379".to_string()],
880 }
881}
882
883fn default_lease_ttl() -> u64 {
884 match std::env::var(env_etcd::ETCD_LEASE_TTL) {
885 Ok(raw) => match raw.parse::<u64>() {
886 Ok(ttl) if ttl > 0 => ttl,
887 Ok(_) => {
888 tracing::warn!(
889 "{} must be >= 1; got 0. Falling back to 10.",
890 env_etcd::ETCD_LEASE_TTL
891 );
892 10
893 }
894 Err(err) => {
895 tracing::warn!(
896 "Invalid {}='{}' ({err}). Falling back to 10.",
897 env_etcd::ETCD_LEASE_TTL,
898 raw
899 );
900 10
901 }
902 },
903 Err(_) => 10,
904 }
905}
906
907pub struct KvCache {
909 client: Client,
910 pub prefix: String,
911 cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
912 watcher: Option<PrefixWatcher>,
913}
914
915impl KvCache {
916 pub async fn new(
918 client: Client,
919 prefix: String,
920 initial_values: HashMap<String, Vec<u8>>,
921 ) -> Result<Self> {
922 let mut cache = HashMap::new();
923
924 let existing_kvs = client.kv_get_prefix(&prefix).await?;
926 for kv in existing_kvs {
927 let key = String::from_utf8_lossy(kv.key()).to_string();
928 cache.insert(key, kv.value().to_vec());
929 }
930
931 for (key, value) in initial_values.iter() {
936 let full_key = format!("{}{}", prefix, key);
937 if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(full_key.clone()) {
938 client.kv_put(&full_key, value.clone(), None).await?;
939 e.insert(value.clone());
940 }
941 }
942
943 let watcher = client.kv_get_and_watch_prefix(&prefix).await?;
947
948 let cache = Arc::new(RwLock::new(cache));
949 let mut result = Self {
950 client,
951 prefix,
952 cache,
953 watcher: Some(watcher),
954 };
955
956 result.start_watcher().await?;
958
959 Ok(result)
960 }
961
962 async fn start_watcher(&mut self) -> Result<()> {
964 if let Some(watcher) = self.watcher.take() {
965 let cache = self.cache.clone();
966 let prefix = self.prefix.clone();
967
968 tokio::spawn(async move {
969 let mut rx = watcher.rx;
970
971 while let Some(event) = rx.recv().await {
972 match event {
973 WatchEvent::Put(kv) => {
974 let key = String::from_utf8_lossy(kv.key()).to_string();
975 let value = kv.value().to_vec();
976
977 tracing::trace!("KvCache update: {} = {:?}", key, value);
978 let mut cache_write = cache.write().await;
979 cache_write.insert(key, value);
980 }
981 WatchEvent::Delete(kv) => {
982 let key = String::from_utf8_lossy(kv.key()).to_string();
983
984 tracing::trace!("KvCache delete: {key}");
985 let mut cache_write = cache.write().await;
986 cache_write.remove(&key);
987 }
988 WatchEvent::Resync(kvs) => {
989 let mut replacement = HashMap::with_capacity(kvs.len());
990 for kv in kvs {
991 let key = String::from_utf8_lossy(kv.key()).to_string();
992 let value = kv.value().to_vec();
993 replacement.insert(key, value);
994 }
995
996 tracing::warn!(
997 prefix,
998 new_count = replacement.len(),
999 "KvCache replacing state from etcd watch resync"
1000 );
1001 let mut cache_write = cache.write().await;
1002 *cache_write = replacement;
1003 }
1004 }
1005 }
1006
1007 tracing::debug!("KvCache watcher for prefix '{prefix}' stopped");
1008 });
1009 }
1010
1011 Ok(())
1012 }
1013
1014 pub async fn get(&self, key: &str) -> Option<Vec<u8>> {
1016 let full_key = format!("{}{}", self.prefix, key);
1017 let cache_read = self.cache.read().await;
1018 cache_read.get(&full_key).cloned()
1019 }
1020
1021 pub async fn get_all(&self) -> HashMap<String, Vec<u8>> {
1023 let cache_read = self.cache.read().await;
1024 cache_read.clone()
1025 }
1026
1027 pub async fn put(&self, key: &str, value: Vec<u8>, lease_id: Option<u64>) -> Result<()> {
1029 let full_key = format!("{}{}", self.prefix, key);
1030
1031 self.client
1033 .kv_put(&full_key, value.clone(), lease_id)
1034 .await?;
1035
1036 let mut cache_write = self.cache.write().await;
1038 cache_write.insert(full_key, value);
1039
1040 Ok(())
1041 }
1042
1043 pub async fn delete(&self, key: &str) -> Result<()> {
1045 let full_key = format!("{}{}", self.prefix, key);
1046
1047 self.client.kv_delete(full_key.clone(), None).await?;
1049
1050 let mut cache_write = self.cache.write().await;
1052 cache_write.remove(&full_key);
1053
1054 Ok(())
1055 }
1056}
1057
1058#[cfg(test)]
1059mod unit_tests {
1060 use super::*;
1061
1062 #[test]
1063 fn classifies_etcd_connection_errors() {
1064 let err = anyhow::Error::new(etcd_client::Error::EndpointError(
1065 "endpoint unavailable".to_string(),
1066 ));
1067 assert!(Client::is_etcd_connection_error(&err));
1068
1069 let err = anyhow::Error::new(etcd_client::Error::IoError(std::io::Error::new(
1070 std::io::ErrorKind::ConnectionRefused,
1071 "connection refused",
1072 )));
1073 assert!(Client::is_etcd_connection_error(&err));
1074
1075 let runtime = tokio::runtime::Builder::new_current_thread()
1076 .enable_time()
1077 .build()
1078 .unwrap();
1079 let elapsed = runtime.block_on(async {
1080 tokio::time::timeout(Duration::from_millis(0), std::future::pending::<()>())
1081 .await
1082 .unwrap_err()
1083 });
1084 let err = anyhow::Error::new(elapsed).context("timed out fetching etcd prefix snapshot");
1085 assert!(Client::is_etcd_connection_error(&err));
1086
1087 let err = anyhow::Error::new(etcd_client::Error::InvalidArgs("bad request".to_string()));
1088 assert!(!Client::is_etcd_connection_error(&err));
1089
1090 let err = anyhow::anyhow!("missing header during watch resync");
1091 assert!(!Client::is_etcd_connection_error(&err));
1092 }
1093}
1094
1095#[cfg(feature = "integration")]
1096#[cfg(test)]
1097mod tests {
1098 use crate::{DistributedRuntime, distributed::DistributedConfig};
1099
1100 use super::*;
1101
1102 #[test]
1103 fn test_ectd_client() {
1104 let rt = Runtime::single_threaded().unwrap();
1105 let rt_clone = rt.clone();
1106 let config = DistributedConfig::from_settings();
1107
1108 rt_clone.primary().block_on(async move {
1109 let drt = DistributedRuntime::new(rt, config).await.unwrap();
1110 test_kv_create_or_validate(drt).await.unwrap();
1111 });
1112 }
1113
1114 async fn test_kv_create_or_validate(drt: DistributedRuntime) -> Result<()> {
1115 let key = "__integration_test_key";
1116 let value = b"test_value";
1117
1118 let client = Client::new(ClientOptions::default(), drt.runtime().clone())
1119 .await
1120 .expect("etcd client should be available");
1121 let lease_id = drt.connection_id();
1122
1123 let result = client.kv_create(key, value.to_vec(), Some(lease_id)).await;
1125 assert!(result.is_ok(), "");
1126
1127 let result = client.kv_create(key, value.to_vec(), Some(lease_id)).await;
1134 assert!(
1135 result.is_ok() && result.unwrap().is_some(),
1136 "Expected Ok(Some(version)) when key already exists"
1137 );
1138
1139 let result = client
1141 .kv_create_or_validate(key.to_string(), value.to_vec(), Some(lease_id))
1142 .await;
1143 assert!(result.is_ok());
1144
1145 let different_value = b"different_value";
1147 let result = client
1148 .kv_create_or_validate(key.to_string(), different_value.to_vec(), Some(lease_id))
1149 .await;
1150 assert!(result.is_err(), "");
1151
1152 Ok(())
1153 }
1154
1155 #[test]
1156 fn test_kv_cache() {
1157 let rt = Runtime::single_threaded().unwrap();
1158 let rt_clone = rt.clone();
1159 let config = DistributedConfig::from_settings();
1160
1161 rt_clone.primary().block_on(async move {
1162 let drt = DistributedRuntime::new(rt, config).await.unwrap();
1163 test_kv_cache_operations(drt).await.unwrap();
1164 });
1165 }
1166
1167 async fn test_kv_cache_operations(drt: DistributedRuntime) -> Result<()> {
1168 let client = Client::new(ClientOptions::default(), drt.runtime().clone())
1170 .await
1171 .expect("etcd client should be available");
1172
1173 let test_id = uuid::Uuid::new_v4().to_string();
1175 let prefix = format!("v1/test_kv_cache_{}/", test_id);
1176
1177 let mut initial_values = HashMap::new();
1179 initial_values.insert("key1".to_string(), b"value1".to_vec());
1180 initial_values.insert("key2".to_string(), b"value2".to_vec());
1181
1182 let kv_cache = KvCache::new(client.clone(), prefix.clone(), initial_values).await?;
1184
1185 let value1 = kv_cache.get("key1").await;
1187 assert_eq!(value1, Some(b"value1".to_vec()));
1188
1189 let value2 = kv_cache.get("key2").await;
1190 assert_eq!(value2, Some(b"value2".to_vec()));
1191
1192 let all_values = kv_cache.get_all().await;
1194 assert_eq!(all_values.len(), 2);
1195 assert_eq!(
1196 all_values.get(&format!("{}key1", prefix)),
1197 Some(&b"value1".to_vec())
1198 );
1199 assert_eq!(
1200 all_values.get(&format!("{}key2", prefix)),
1201 Some(&b"value2".to_vec())
1202 );
1203
1204 kv_cache.put("key3", b"value3".to_vec(), None).await?;
1206
1207 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1209
1210 let value3 = kv_cache.get("key3").await;
1212 assert_eq!(value3, Some(b"value3".to_vec()));
1213
1214 kv_cache
1216 .put("key1", b"updated_value1".to_vec(), None)
1217 .await?;
1218
1219 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1221
1222 let updated_value1 = kv_cache.get("key1").await;
1224 assert_eq!(updated_value1, Some(b"updated_value1".to_vec()));
1225
1226 client
1228 .kv_put(
1229 &format!("{}key2", prefix),
1230 b"external_update".to_vec(),
1231 None,
1232 )
1233 .await?;
1234
1235 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1237
1238 let external_update = kv_cache.get("key2").await;
1240 assert_eq!(external_update, Some(b"external_update".to_vec()));
1241
1242 let etcd_client = client.etcd_client();
1244 let _ = etcd_client
1245 .kv_client()
1246 .delete(
1247 prefix,
1248 Some(etcd_client::DeleteOptions::new().with_prefix()),
1249 )
1250 .await?;
1251
1252 Ok(())
1253 }
1254}