dynamo_runtime/storage/kv/
nats.rs1use std::{collections::HashMap, pin::Pin, time::Duration};
5
6use crate::{protocols::EndpointId, slug::Slug, storage::kv, transports::nats::Client};
7use async_nats::jetstream::kv::Operation;
8use async_trait::async_trait;
9use futures::StreamExt;
10
11use super::{Bucket, Store, StoreError, StoreOutcome};
12
13#[derive(Clone)]
14pub struct NATSStore {
15 client: Client,
16 endpoint: EndpointId,
17}
18
19pub struct NATSBucket {
20 nats_store: async_nats::jetstream::kv::Store,
21}
22
23#[async_trait]
24impl Store for NATSStore {
25 type Bucket = NATSBucket;
26
27 async fn get_or_create_bucket(
28 &self,
29 bucket_name: &str,
30 ttl: Option<Duration>,
31 ) -> Result<Self::Bucket, StoreError> {
32 let name = Slug::slugify(bucket_name);
33 let nats_store = self
34 .get_or_create_key_value(&self.endpoint.namespace, &name, ttl)
35 .await?;
36 Ok(NATSBucket { nats_store })
37 }
38
39 async fn get_bucket(&self, bucket_name: &str) -> Result<Option<Self::Bucket>, StoreError> {
40 let name = Slug::slugify(bucket_name);
41 match self.get_key_value(&self.endpoint.namespace, &name).await? {
42 Some(nats_store) => Ok(Some(NATSBucket { nats_store })),
43 None => Ok(None),
44 }
45 }
46
47 fn connection_id(&self) -> u64 {
48 self.client.client().server_info().client_id
49 }
50
51 fn shutdown(&self) {
52 }
55
56 async fn check_connection(&self) -> Result<(), StoreError> {
57 if self.client.client().connection_state() != async_nats::connection::State::Connected {
58 return Err(StoreError::NATSError("NATS is disconnected".into()));
59 }
60 Ok(())
61 }
62}
63
64impl NATSStore {
65 pub fn new(client: Client, endpoint: EndpointId) -> Self {
66 NATSStore { client, endpoint }
67 }
68
69 async fn get_or_create_key_value(
74 &self,
75 namespace: &str,
76 bucket_name: &Slug,
77 ttl: Option<Duration>,
79 ) -> Result<async_nats::jetstream::kv::Store, StoreError> {
80 if let Ok(Some(kv)) = self.get_key_value(namespace, bucket_name).await {
81 return Ok(kv);
82 }
83
84 let bucket_name = single_name(namespace, bucket_name);
87 let js = self.client.jetstream();
88 let create_result = js
89 .create_key_value(
90 async_nats::jetstream::kv::Config {
92 bucket: bucket_name.clone(),
93 max_age: ttl.unwrap_or_default(),
94 ..Default::default()
95 },
96 )
97 .await;
98 let nats_store = create_result
99 .map_err(|err| StoreError::KeyValueError(err.to_string(), bucket_name.clone()))?;
100 tracing::debug!("Created bucket {bucket_name}");
101 Ok(nats_store)
102 }
103
104 async fn get_key_value(
105 &self,
106 namespace: &str,
107 bucket_name: &Slug,
108 ) -> Result<Option<async_nats::jetstream::kv::Store>, StoreError> {
109 let bucket_name = single_name(namespace, bucket_name);
110 let js = self.client.jetstream();
111
112 use async_nats::jetstream::context::KeyValueErrorKind;
113 match js.get_key_value(&bucket_name).await {
114 Ok(store) => Ok(Some(store)),
115 Err(err) if err.kind() == KeyValueErrorKind::GetBucket => {
116 Ok(None)
118 }
119 Err(err) => Err(StoreError::KeyValueError(err.to_string(), bucket_name)),
120 }
121 }
122}
123
124#[async_trait]
125impl Bucket for NATSBucket {
126 async fn insert(
127 &self,
128 key: &kv::Key,
129 value: bytes::Bytes,
130 revision: u64,
131 ) -> Result<StoreOutcome, StoreError> {
132 if revision == 0 {
133 self.create(key, value).await
134 } else {
135 self.update(key, value, revision).await
136 }
137 }
138
139 async fn compare_and_replace(
140 &self,
141 key: &kv::Key,
142 expected: bytes::Bytes,
143 value: bytes::Bytes,
144 ) -> Result<StoreOutcome, StoreError> {
145 let entry = self
146 .nats_store
147 .entry(key)
148 .await
149 .map_err(|error| StoreError::NATSError(error.to_string()))?
150 .ok_or_else(|| StoreError::MissingKey(key.to_string()))?;
151 if matches!(entry.operation, Operation::Delete | Operation::Purge) {
152 return Err(StoreError::MissingKey(key.to_string()));
153 }
154 if entry.value != expected {
155 return Err(StoreError::Retry);
156 }
157
158 match self.nats_store.update(key, value, entry.revision).await {
159 Ok(revision) => Ok(StoreOutcome::Created(revision)),
160 Err(error)
161 if error.kind()
162 == async_nats::jetstream::kv::UpdateErrorKind::WrongLastRevision =>
163 {
164 match self.nats_store.entry(key).await {
165 Ok(None) => Err(StoreError::MissingKey(key.to_string())),
166 Ok(Some(entry))
167 if matches!(entry.operation, Operation::Delete | Operation::Purge) =>
168 {
169 Err(StoreError::MissingKey(key.to_string()))
170 }
171 Ok(Some(_)) => Err(StoreError::Retry),
172 Err(error) => Err(StoreError::NATSError(error.to_string())),
173 }
174 }
175 Err(error) => Err(StoreError::NATSError(error.to_string())),
176 }
177 }
178
179 async fn get(&self, key: &kv::Key) -> Result<Option<bytes::Bytes>, StoreError> {
180 self.nats_store
181 .get(key)
182 .await
183 .map_err(|e| StoreError::NATSError(e.to_string()))
184 }
185
186 async fn delete(&self, key: &kv::Key) -> Result<(), StoreError> {
187 self.nats_store
188 .delete(key)
189 .await
190 .map_err(|e| StoreError::NATSError(e.to_string()))
191 }
192
193 async fn watch(
197 &self,
198 ) -> Result<Pin<Box<dyn futures::Stream<Item = kv::WatchEvent> + Send + 'life0>>, StoreError>
199 {
200 let watch_stream = self
201 .nats_store
202 .watch_with_history(">")
203 .await
204 .map_err(|e| StoreError::NATSError(e.to_string()))?;
205 Ok(Box::pin(
207 watch_stream.filter_map(
208 |maybe_entry: Result<
209 async_nats::jetstream::kv::Entry,
210 async_nats::error::Error<_>,
211 >| async move {
212 match maybe_entry {
213 Ok(entry) => {
214 let key = kv::Key::new(entry.key);
215 Some(match entry.operation {
216 Operation::Put => {
217 let item = kv::KeyValue::new(key, entry.value);
218 kv::WatchEvent::Put(item)
219 }
220 Operation::Delete => kv::WatchEvent::Delete(key),
221 Operation::Purge => kv::WatchEvent::Delete(key),
223 })
224 }
225 Err(e) => {
226 tracing::error!(error=%e, "watch fatal err");
227 None
228 }
229 }
230 },
231 ),
232 ))
233 }
234
235 async fn entries(&self) -> Result<HashMap<kv::Key, bytes::Bytes>, StoreError> {
236 let mut key_stream = self
237 .nats_store
238 .keys()
239 .await
240 .map_err(|e| StoreError::NATSError(e.to_string()))?;
241 let mut out = HashMap::new();
242 while let Some(Ok(key)) = key_stream.next().await {
243 if let Ok(Some(entry)) = self.nats_store.entry(&key).await {
244 out.insert(kv::Key::new(key), entry.value);
245 }
246 }
247 Ok(out)
248 }
249}
250
251impl NATSBucket {
252 async fn create(&self, key: &kv::Key, value: bytes::Bytes) -> Result<StoreOutcome, StoreError> {
253 match self.nats_store.create(&key, value).await {
254 Ok(revision) => Ok(StoreOutcome::Created(revision)),
255 Err(err) if err.kind() == async_nats::jetstream::kv::CreateErrorKind::AlreadyExists => {
256 match self.nats_store.entry(key).await {
258 Ok(Some(entry)) => Ok(StoreOutcome::Exists(entry.revision)),
259 Ok(None) => {
260 tracing::error!(
261 %key,
262 "Race condition, key deleted between create and fetch. Retry."
263 );
264 Err(StoreError::Retry)
265 }
266 Err(err) => Err(StoreError::NATSError(err.to_string())),
267 }
268 }
269 Err(err) => Err(StoreError::NATSError(err.to_string())),
270 }
271 }
272
273 async fn update(
274 &self,
275 key: &kv::Key,
276 value: bytes::Bytes,
277 revision: u64,
278 ) -> Result<StoreOutcome, StoreError> {
279 match self.nats_store.update(key, value.clone(), revision).await {
280 Ok(revision) => Ok(StoreOutcome::Created(revision)),
281 Err(err)
282 if err.kind() == async_nats::jetstream::kv::UpdateErrorKind::WrongLastRevision =>
283 {
284 tracing::warn!(revision, %key, "Update WrongLastRevision, resync");
285 self.resync_update(key, value).await
286 }
287 Err(err) => Err(StoreError::NATSError(err.to_string())),
288 }
289 }
290
291 async fn resync_update(
294 &self,
295 key: &kv::Key,
296 value: bytes::Bytes,
297 ) -> Result<StoreOutcome, StoreError> {
298 match self.nats_store.entry(key).await {
299 Ok(Some(entry)) => {
300 let next_rev = entry.revision + 1;
302 match self.nats_store.update(key, value, next_rev).await {
303 Ok(correct_revision) => Ok(StoreOutcome::Created(correct_revision)),
304 Err(err) => Err(StoreError::NATSError(format!(
305 "Error during update of key {key} after resync: {err}"
306 ))),
307 }
308 }
309 Ok(None) => {
310 tracing::warn!(%key, "Entry does not exist during resync, creating.");
311 self.create(key, value).await
312 }
313 Err(err) => {
314 tracing::error!(%key, %err, "Failed fetching entry during resync");
315 Err(StoreError::NATSError(err.to_string()))
316 }
317 }
318 }
319}
320
321fn single_name(namespace: &str, name: &Slug) -> String {
324 format!("{namespace}_{name}")
325}
326
327#[cfg(feature = "integration")]
328#[cfg(test)]
329mod compare_and_replace_tests {
330 use std::sync::Arc;
331
332 use tokio::sync::Barrier;
333
334 use super::*;
335 use crate::storage::kv::{Bucket as _, Key, Store as _};
336
337 #[tokio::test]
338 async fn delete_wins_race_with_compare_and_replace() {
339 let client = crate::transports::nats::ClientOptions::default()
340 .connect()
341 .await
342 .unwrap();
343 let endpoint = EndpointId {
344 namespace: "test".to_string(),
345 component: "storage".to_string(),
346 name: "compare-and-replace".to_string(),
347 };
348 let store = NATSStore::new(client, endpoint);
349 let bucket_name = format!("compare_and_replace_{}", uuid::Uuid::new_v4());
350 let jetstream_bucket_name = single_name("test", &Slug::slugify(&bucket_name));
351 let bucket = Arc::new(
352 store
353 .get_or_create_bucket(&bucket_name, None)
354 .await
355 .unwrap(),
356 );
357 let key = Key::new("model".to_string());
358 bucket.insert(&key, "old".into(), 0).await.unwrap();
359
360 let barrier = Arc::new(Barrier::new(3));
361 let task_bucket = bucket.clone();
362 let task_key = key.clone();
363 let task_barrier = barrier.clone();
364 let update = tokio::spawn(async move {
365 task_barrier.wait().await;
366 task_bucket
367 .compare_and_replace(&task_key, "old".into(), "new".into())
368 .await
369 });
370 let task_bucket = bucket.clone();
371 let task_key = key.clone();
372 let task_barrier = barrier.clone();
373 let delete = tokio::spawn(async move {
374 task_barrier.wait().await;
375 task_bucket.delete(&task_key).await
376 });
377
378 barrier.wait().await;
379 let update_result = update.await.unwrap();
380 delete.await.unwrap().unwrap();
381 assert!(update_result.is_ok() || matches!(update_result, Err(StoreError::MissingKey(_))));
382 assert_eq!(bucket.get(&key).await.unwrap(), None);
383
384 drop(bucket);
385 store
386 .client
387 .jetstream()
388 .delete_key_value(&jetstream_bucket_name)
389 .await
390 .unwrap();
391 }
392}