Skip to main content

dynamo_runtime/storage/kv/
nats.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // TODO: Track and delete any owned keys
53        // The TTL should ensure NATS does it, but best we do it immediately
54    }
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    /// Get or create a key-value store (aka bucket) in NATS.
70    ///
71    /// ttl is only used if we are creating the bucket, so if that has
72    /// changed first delete the bucket.
73    async fn get_or_create_key_value(
74        &self,
75        namespace: &str,
76        bucket_name: &Slug,
77        // Delete entries older than this
78        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        // It doesn't exist, create it
85
86        let bucket_name = single_name(namespace, bucket_name);
87        let js = self.client.jetstream();
88        let create_result = js
89            .create_key_value(
90                // TODO: configure the bucket, probably need to pass some of these values in
91                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                // bucket doesn't exist
117                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    /// Replays the existing entries as individual [`kv::WatchEvent::Put`] events, not as the
194    /// initial [`kv::WatchEvent::Resync`] the [`kv::Bucket::watch`] contract asks for. Discovery
195    /// does not select this store, so nothing depends on that order here.
196    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        // Map the `Entry` to `Entry.value` which is Bytes of the stored value.
206        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                                // TODO: What is Purge? Not urgent, NATS impl not used
222                                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                // key exists, get the revsion
257                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    /// We have the wrong revision for a key. Fetch it's entry to get the correct revision,
292    /// and try the update again.
293    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                // Re-try the update with new version number
301                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
321/// async-nats won't let us use a multi-part subject to create KV buckets (and probably many other
322/// things).
323fn 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}