dynamo_runtime/storage/kv/
etcd.rs1use std::collections::HashMap;
5use std::pin::Pin;
6use std::time::Duration;
7
8use crate::transports::etcd;
9use async_stream::stream;
10use async_trait::async_trait;
11use etcd_client::PutOptions;
12
13use super::{Bucket, Key, KeyValue, Store, StoreError, StoreOutcome, WatchEvent};
14
15#[derive(Clone)]
16pub struct EtcdStore {
17 client: etcd::Client,
18}
19
20impl EtcdStore {
21 pub fn new(client: etcd::Client) -> Self {
22 Self { client }
23 }
24}
25
26#[async_trait]
27impl Store for EtcdStore {
28 type Bucket = EtcdBucket;
29
30 async fn get_or_create_bucket(
32 &self,
33 bucket_name: &str,
34 _ttl: Option<Duration>, ) -> Result<Self::Bucket, StoreError> {
36 Ok(EtcdBucket {
37 client: self.client.clone(),
38 bucket_name: bucket_name.to_string(),
39 })
40 }
41
42 async fn get_bucket(&self, bucket_name: &str) -> Result<Option<Self::Bucket>, StoreError> {
45 Ok(Some(EtcdBucket {
46 client: self.client.clone(),
47 bucket_name: bucket_name.to_string(),
48 }))
49 }
50
51 fn connection_id(&self) -> u64 {
52 self.client.lease_id()
53 }
54
55 fn shutdown(&self) {
56 }
58
59 async fn check_connection(&self) -> Result<(), StoreError> {
60 self.client
61 .check_connection()
62 .await
63 .map_err(|e| StoreError::EtcdError(e.to_string()))
64 }
65}
66
67pub struct EtcdBucket {
68 client: etcd::Client,
69 bucket_name: String,
70}
71
72#[async_trait]
73impl Bucket for EtcdBucket {
74 async fn insert(
75 &self,
76 key: &Key,
77 value: bytes::Bytes,
78 revision: u64,
80 ) -> Result<StoreOutcome, StoreError> {
81 let version = revision;
82 if version == 0 {
83 self.create(key, value).await
84 } else {
85 self.update(key, value, version).await
86 }
87 }
88
89 async fn compare_and_replace(
90 &self,
91 key: &Key,
92 expected: bytes::Bytes,
93 value: bytes::Bytes,
94 ) -> Result<StoreOutcome, StoreError> {
95 let k = make_key(&self.bucket_name, key);
96 match self
97 .client
98 .kv_compare_and_put(k, expected, value, None)
99 .await
100 .map_err(|error| StoreError::EtcdError(error.to_string()))?
101 {
102 etcd::CompareAndPutOutcome::Updated => Ok(StoreOutcome::Created(0)),
103 etcd::CompareAndPutOutcome::Missing => Err(StoreError::MissingKey(key.to_string())),
104 etcd::CompareAndPutOutcome::Conflict => Err(StoreError::Retry),
105 }
106 }
107
108 async fn get(&self, key: &Key) -> Result<Option<bytes::Bytes>, StoreError> {
109 let k = make_key(&self.bucket_name, key);
110 tracing::trace!("etcd get: {k}");
111
112 let mut kvs = self
113 .client
114 .kv_get(k, None)
115 .await
116 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
117 if kvs.is_empty() {
118 return Ok(None);
119 }
120 let (_, val) = kvs.swap_remove(0).into_key_value();
121 Ok(Some(val.into()))
122 }
123
124 async fn delete(&self, key: &Key) -> Result<(), StoreError> {
125 let k = make_key(&self.bucket_name, key);
126 tracing::trace!("etcd delete: {k}");
127 let _ = self
128 .client
129 .kv_delete(k, None)
130 .await
131 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
132 Ok(())
133 }
134
135 async fn watch(
136 &self,
137 ) -> Result<Pin<Box<dyn futures::Stream<Item = WatchEvent> + Send + 'life0>>, StoreError> {
138 let prefix = make_key(&self.bucket_name, &"".into());
139 tracing::trace!("etcd watch: {prefix}");
140 let watcher = self
141 .client
142 .kv_get_and_watch_prefix(&prefix)
143 .await
144 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
145 let (_, mut watch_stream) = watcher.dissolve();
146 let output = stream! {
147 while let Some(event) = watch_stream.recv().await {
148 match event {
149 etcd::WatchEvent::Put(kv) => {
150 let (k, v) = kv.into_key_value();
151 let key = match String::from_utf8(k) {
152 Ok(k) => Key::new(k),
153 Err(err) => {
154 tracing::error!(%err, prefix, "Invalid UTF8 in etcd key");
155 continue;
156 }
157 };
158 let item = KeyValue::new(key, v.into());
159 yield WatchEvent::Put(item);
160 }
161 etcd::WatchEvent::Delete(kv) => {
162 let (k, _) = kv.into_key_value();
163 let key = match String::from_utf8(k) {
164 Ok(k) => Key::new(k),
165 Err(err) => {
166 tracing::error!(%err, prefix, "Invalid UTF8 in etcd key");
167 continue;
168 }
169 };
170 yield WatchEvent::Delete(key);
171 }
172 etcd::WatchEvent::Resync(kvs) => {
173 let mut snapshot = HashMap::with_capacity(kvs.len());
174 for kv in kvs {
175 let (k, v) = kv.into_key_value();
176 let key = match String::from_utf8(k) {
177 Ok(k) => Key::new(k),
178 Err(err) => {
179 tracing::error!(%err, prefix, "Invalid UTF8 in etcd resync key");
180 continue;
181 }
182 };
183 snapshot.insert(key, v.into());
184 }
185 yield WatchEvent::Resync(snapshot);
186 }
187 }
188 }
189 };
190 Ok(Box::pin(output))
191 }
192
193 async fn entries(&self) -> Result<HashMap<Key, bytes::Bytes>, StoreError> {
194 let k = make_key(&self.bucket_name, &"".into());
195 tracing::trace!("etcd entries: {k}");
196
197 let resp = self
198 .client
199 .kv_get_prefix(k)
200 .await
201 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
202 let out: HashMap<Key, bytes::Bytes> = resp
203 .into_iter()
204 .map(|kv| {
205 let (k, v) = kv.into_key_value();
206 (Key::new(String::from_utf8_lossy(&k).to_string()), v.into())
207 })
208 .collect();
209
210 Ok(out)
211 }
212}
213
214impl EtcdBucket {
215 async fn create(
216 &self,
217 key: &Key,
218 value: impl Into<Vec<u8>>,
219 ) -> Result<StoreOutcome, StoreError> {
220 let k = make_key(&self.bucket_name, key);
221 tracing::trace!("etcd create: {k}");
222
223 match self
224 .client
225 .kv_create(k.as_str(), value.into(), None)
226 .await
227 .map_err(|e| StoreError::EtcdError(e.to_string()))?
228 {
229 None => {
230 Ok(StoreOutcome::Created(1)) }
233 Some(revision) => Ok(StoreOutcome::Exists(revision)),
234 }
235 }
236
237 async fn update(
238 &self,
239 key: &Key,
240 value: impl AsRef<[u8]>,
241 revision: u64,
242 ) -> Result<StoreOutcome, StoreError> {
243 let version = revision;
244 let k = make_key(&self.bucket_name, key);
245 tracing::trace!("etcd update: {k}");
246
247 let kvs = self
248 .client
249 .kv_get(k.clone(), None)
250 .await
251 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
252 if kvs.is_empty() {
253 return Err(StoreError::MissingKey(key.to_string()));
254 }
255 let current_version = kvs.first().unwrap().version() as u64;
256 if current_version != version + 1 {
257 tracing::warn!(
258 current_version,
259 attempted_next_version = version,
260 %key,
261 "update: Wrong revision"
262 );
263 }
266
267 let put_options = PutOptions::new()
268 .with_lease(self.client.lease_id() as i64)
269 .with_prev_key();
270 let mut put_resp = self
271 .client
272 .kv_put_with_options(k, value, Some(put_options))
273 .await
274 .map_err(|e| StoreError::EtcdError(e.to_string()))?;
275 Ok(match put_resp.take_prev_key() {
276 None => StoreOutcome::Created(1),
281 Some(kv) if kv.version() as u64 == version + 1 => StoreOutcome::Created(version),
283 Some(kv) => StoreOutcome::Created(kv.version() as u64 + 1),
285 })
286 }
287}
288
289fn make_key(bucket_name: &str, key: &Key) -> String {
290 [bucket_name.to_string(), key.to_string()].join("/")
291}
292
293#[cfg(feature = "integration")]
294#[cfg(test)]
295mod concurrent_create_tests {
296 use super::*;
297 use crate::Runtime;
298 use crate::transports::etcd as etcd_transport;
299 use futures::StreamExt;
300 use std::sync::Arc;
301 use tokio::sync::Barrier;
302
303 #[test]
304 fn test_concurrent_etcd_create_race_condition() {
305 let rt = Runtime::single_threaded().unwrap();
306 let rt_clone = rt.clone();
307
308 rt_clone.primary().block_on(async move {
309 let etcd_client =
310 etcd_transport::Client::new(etcd_transport::ClientOptions::default(), rt)
311 .await
312 .unwrap();
313 let storage = crate::storage::kv::Manager::etcd(etcd_client);
314 test_concurrent_create(&storage).await.unwrap();
315 });
316 }
317
318 #[test]
319 fn delete_wins_race_with_compare_and_replace() {
320 let rt = Runtime::single_threaded().unwrap();
321 let rt_clone = rt.clone();
322
323 rt_clone.primary().block_on(async move {
324 let etcd_client =
325 etcd_transport::Client::new(etcd_transport::ClientOptions::default(), rt)
326 .await
327 .unwrap();
328 let storage = crate::storage::kv::Manager::etcd(etcd_client);
329 let bucket = Arc::new(
330 storage
331 .get_or_create_bucket("test_compare_and_replace_bucket", None)
332 .await
333 .unwrap(),
334 );
335 let key = Key::new(format!("model_{}", uuid::Uuid::new_v4()));
336 bucket.insert(&key, "old".into(), 0).await.unwrap();
337
338 let barrier = Arc::new(Barrier::new(3));
339 let task_bucket = bucket.clone();
340 let task_key = key.clone();
341 let task_barrier = barrier.clone();
342 let update = tokio::spawn(async move {
343 task_barrier.wait().await;
344 task_bucket
345 .compare_and_replace(&task_key, "old".into(), "new".into())
346 .await
347 });
348 let task_bucket = bucket.clone();
349 let task_key = key.clone();
350 let task_barrier = barrier.clone();
351 let delete = tokio::spawn(async move {
352 task_barrier.wait().await;
353 task_bucket.delete(&task_key).await
354 });
355
356 barrier.wait().await;
357 let update_result = update.await.unwrap();
358 delete.await.unwrap().unwrap();
359 assert!(
360 update_result.is_ok() || matches!(update_result, Err(StoreError::MissingKey(_)))
361 );
362 assert_eq!(bucket.get(&key).await.unwrap(), None);
363 });
364 }
365
366 #[test]
367 fn watch_starts_with_one_resync_of_the_prefix() {
368 let rt = Runtime::single_threaded().unwrap();
369 let rt_clone = rt.clone();
370
371 rt_clone.primary().block_on(async move {
372 let etcd_client =
373 etcd_transport::Client::new(etcd_transport::ClientOptions::default(), rt)
374 .await
375 .unwrap();
376 let storage = crate::storage::kv::Manager::etcd(etcd_client);
377 let bucket_name = format!("test_watch_resync_{}", uuid::Uuid::new_v4());
378 let bucket = storage
379 .get_or_create_bucket(&bucket_name, None)
380 .await
381 .unwrap();
382 let existing = Key::new("existing".to_string());
383 bucket.insert(&existing, "1".into(), 0).await.unwrap();
384
385 let mut events = bucket.watch().await.unwrap();
386 let first = events.next().await.unwrap();
387 let WatchEvent::Resync(snapshot) = first else {
388 panic!("expected the initial resync, got {first:?}");
389 };
390 assert_eq!(
391 snapshot
392 .get(&Key::new(make_key(&bucket_name, &existing)))
393 .map(|value| value.as_ref()),
394 Some(b"1".as_slice())
395 );
396
397 let later = Key::new("later".to_string());
398 bucket.insert(&later, "2".into(), 0).await.unwrap();
399 let second = events.next().await.unwrap();
400 let WatchEvent::Put(item) = second else {
401 panic!("expected a put after the snapshot, got {second:?}");
402 };
403 assert_eq!(item.key_str(), make_key(&bucket_name, &later));
404
405 bucket.delete(&existing).await.unwrap();
406 bucket.delete(&later).await.unwrap();
407 });
408 }
409
410 async fn test_concurrent_create(
411 storage: &crate::storage::kv::Manager,
412 ) -> Result<(), StoreError> {
413 let bucket = Arc::new(tokio::sync::Mutex::new(
415 storage
416 .get_or_create_bucket("test_concurrent_bucket", None)
417 .await?,
418 ));
419
420 let num_workers = 10;
422 let barrier = Arc::new(Barrier::new(num_workers));
423
424 let test_key: Key = Key::new(format!("concurrent_test_key_{}", uuid::Uuid::new_v4()));
426 let test_value = "test_value";
427
428 let mut handles = Vec::new();
430 let success_count = Arc::new(tokio::sync::Mutex::new(0));
431 let exists_count = Arc::new(tokio::sync::Mutex::new(0));
432
433 for worker_id in 0..num_workers {
434 let bucket_clone = bucket.clone();
435 let barrier_clone = barrier.clone();
436 let key_clone = test_key.clone();
437 let value_clone = format!("{}_from_worker_{}", test_value, worker_id);
438 let success_count_clone = success_count.clone();
439 let exists_count_clone = exists_count.clone();
440
441 let handle = tokio::spawn(async move {
442 barrier_clone.wait().await;
444
445 let result = bucket_clone
447 .lock()
448 .await
449 .insert(&key_clone, value_clone.into(), 0)
450 .await;
451
452 match result {
453 Ok(StoreOutcome::Created(version)) => {
454 println!(
455 "Worker {} successfully created key with version {}",
456 worker_id, version
457 );
458 let mut count = success_count_clone.lock().await;
459 *count += 1;
460 Ok(version)
461 }
462 Ok(StoreOutcome::Exists(version)) => {
463 println!(
464 "Worker {} found key already exists with version {}",
465 worker_id, version
466 );
467 let mut count = exists_count_clone.lock().await;
468 *count += 1;
469 Ok(version)
470 }
471 Err(e) => {
472 println!("Worker {} got error: {:?}", worker_id, e);
473 Err(e)
474 }
475 }
476 });
477
478 handles.push(handle);
479 }
480
481 let mut results = Vec::new();
483 for handle in handles {
484 let result = handle.await.unwrap();
485 if let Ok(version) = result {
486 results.push(version);
487 }
488 }
489
490 let final_success_count = *success_count.lock().await;
492 let final_exists_count = *exists_count.lock().await;
493
494 println!(
495 "Final counts - Created: {}, Exists: {}",
496 final_success_count, final_exists_count
497 );
498
499 assert_eq!(
502 final_success_count, 1,
503 "Exactly one worker should create the key"
504 );
505
506 assert_eq!(
508 final_exists_count,
509 num_workers - 1,
510 "All other workers should see key exists"
511 );
512
513 assert_eq!(
515 results.len(),
516 num_workers,
517 "All workers should complete successfully"
518 );
519
520 let stored_value = bucket.lock().await.get(&test_key).await?;
522 assert!(stored_value.is_some(), "Key should exist in etcd");
523
524 let stored_str = String::from_utf8(stored_value.unwrap().to_vec()).unwrap();
526 assert!(
527 stored_str.starts_with(test_value),
528 "Stored value should match expected prefix"
529 );
530
531 bucket.lock().await.delete(&test_key).await?;
533
534 Ok(())
535 }
536}