Skip to main content

prolly_store_redis/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub use prolly::{
4    RemoteBatchOp, RemoteManifestUpdate, RemoteNamedRoot, RemoteProllyStore, RemoteRootCondition,
5    RemoteRootWrite, RemoteStoreBackend, RemoteTransactionConflict, RemoteTransactionUpdate,
6};
7
8/// Redis adapter entry point.
9pub mod redis {
10    use redis_client::{ErrorKind, RedisError, Script, Value};
11
12    use crate::{
13        RemoteBatchOp, RemoteManifestUpdate, RemoteNamedRoot, RemoteRootCondition, RemoteRootWrite,
14        RemoteStoreBackend, RemoteTransactionConflict, RemoteTransactionUpdate,
15    };
16
17    /// Store adapter for Redis-backed prolly nodes and roots.
18    ///
19    /// Redis should be treated as a cache or edge store unless persistence and
20    /// durability are explicitly configured for the Redis deployment.
21    pub type RedisStore = crate::RemoteProllyStore<RedisBackend>;
22
23    /// Redis-backed prolly node/root backend.
24    #[derive(Clone)]
25    pub struct RedisBackend {
26        connection: redis_client::aio::ConnectionManager,
27        key_prefix: Vec<u8>,
28        read_parallelism: usize,
29    }
30
31    impl std::fmt::Debug for RedisBackend {
32        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33            f.debug_struct("RedisBackend")
34                .field("key_prefix", &self.key_prefix)
35                .field("read_parallelism", &self.read_parallelism)
36                .finish_non_exhaustive()
37        }
38    }
39
40    impl RedisBackend {
41        /// Create a backend from an existing Redis connection manager.
42        pub fn new(connection: redis_client::aio::ConnectionManager) -> Self {
43            Self {
44                connection,
45                key_prefix: DEFAULT_KEY_PREFIX.to_vec(),
46                read_parallelism: DEFAULT_READ_PARALLELISM,
47            }
48        }
49
50        /// Connect to Redis using `redis_url`.
51        pub async fn connect(redis_url: &str) -> Result<Self, RedisError> {
52            let client = redis_client::Client::open(redis_url)?;
53            Self::from_client(client).await
54        }
55
56        /// Create a backend from an existing Redis client.
57        pub async fn from_client(client: redis_client::Client) -> Result<Self, RedisError> {
58            Ok(Self::new(client.get_connection_manager().await?))
59        }
60
61        /// Borrow the underlying connection manager.
62        pub fn connection(&self) -> &redis_client::aio::ConnectionManager {
63            &self.connection
64        }
65
66        /// Return the namespace prefix prepended to all Redis keys.
67        pub fn key_prefix(&self) -> &[u8] {
68            &self.key_prefix
69        }
70
71        /// Set the namespace prefix prepended to all Redis keys.
72        ///
73        /// Use a unique prefix when running tests or sharing a Redis database.
74        pub fn with_key_prefix(mut self, key_prefix: impl Into<Vec<u8>>) -> Self {
75            self.key_prefix = key_prefix.into();
76            self
77        }
78
79        /// Set the read parallelism advertised to async prolly traversals.
80        pub fn with_read_parallelism(mut self, read_parallelism: usize) -> Self {
81            self.read_parallelism = read_parallelism.max(1);
82            self
83        }
84
85        /// Delete every key under this backend's namespace prefix.
86        ///
87        /// This is primarily intended for isolated integration tests.
88        pub async fn clear_namespace(&self) -> Result<(), RedisError> {
89            if self.key_prefix.is_empty() {
90                return Err(redis_type_error(
91                    "refusing to clear an empty Redis key prefix",
92                ));
93            }
94
95            let mut pattern = self.key_prefix.clone();
96            pattern.push(b'*');
97            let keys = self.scan_keys(&pattern).await?;
98            self.delete_keys(&keys).await
99        }
100
101        fn node_key(&self, key: &[u8]) -> Vec<u8> {
102            self.family_key(NODE_FAMILY, key)
103        }
104
105        fn root_key(&self, name: &[u8]) -> Vec<u8> {
106            self.family_key(ROOT_FAMILY, name)
107        }
108
109        fn hint_key(&self, namespace: &[u8], key: &[u8]) -> Vec<u8> {
110            let mut redis_key = self.family_key(HINT_FAMILY, &[]);
111            redis_key.extend_from_slice(&(namespace.len() as u64).to_be_bytes());
112            redis_key.extend_from_slice(namespace);
113            redis_key.extend_from_slice(key);
114            redis_key
115        }
116
117        fn family_key(&self, family: &[u8], suffix: &[u8]) -> Vec<u8> {
118            let mut key = Vec::with_capacity(self.key_prefix.len() + family.len() + suffix.len());
119            key.extend_from_slice(&self.key_prefix);
120            key.extend_from_slice(family);
121            key.extend_from_slice(suffix);
122            key
123        }
124
125        fn family_prefix(&self, family: &[u8]) -> Vec<u8> {
126            self.family_key(family, &[])
127        }
128
129        fn family_pattern(&self, family: &[u8]) -> Vec<u8> {
130            let mut pattern = self.family_prefix(family);
131            pattern.push(b'*');
132            pattern
133        }
134
135        async fn scan_keys(&self, pattern: &[u8]) -> Result<Vec<Vec<u8>>, RedisError> {
136            let mut connection = self.connection.clone();
137            let mut cursor = 0_u64;
138            let mut keys = Vec::new();
139
140            loop {
141                let (next_cursor, batch): (u64, Vec<Vec<u8>>) = redis_client::cmd("SCAN")
142                    .arg(cursor)
143                    .arg("MATCH")
144                    .arg(pattern)
145                    .arg("COUNT")
146                    .arg(SCAN_COUNT)
147                    .query_async(&mut connection)
148                    .await?;
149                keys.extend(batch);
150                if next_cursor == 0 {
151                    break;
152                }
153                cursor = next_cursor;
154            }
155
156            Ok(keys)
157        }
158
159        async fn delete_keys(&self, keys: &[Vec<u8>]) -> Result<(), RedisError> {
160            if keys.is_empty() {
161                return Ok(());
162            }
163
164            let mut connection = self.connection.clone();
165            for chunk in keys.chunks(DELETE_CHUNK_SIZE) {
166                let mut command = redis_client::cmd("DEL");
167                for key in chunk {
168                    command.arg(key.as_slice());
169                }
170                command.query_async::<()>(&mut connection).await?;
171            }
172            Ok(())
173        }
174    }
175
176    impl RemoteStoreBackend for RedisBackend {
177        type Error = RedisError;
178
179        async fn get_node(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
180            let mut connection = self.connection.clone();
181            redis_client::cmd("GET")
182                .arg(self.node_key(key))
183                .query_async(&mut connection)
184                .await
185        }
186
187        async fn put_node(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
188            let mut connection = self.connection.clone();
189            redis_client::cmd("SET")
190                .arg(self.node_key(key))
191                .arg(value)
192                .query_async::<()>(&mut connection)
193                .await
194        }
195
196        async fn delete_node(&self, key: &[u8]) -> Result<(), Self::Error> {
197            let mut connection = self.connection.clone();
198            redis_client::cmd("DEL")
199                .arg(self.node_key(key))
200                .query_async::<()>(&mut connection)
201                .await
202        }
203
204        async fn batch_nodes(&self, ops: &[RemoteBatchOp<'_>]) -> Result<(), Self::Error> {
205            if ops.is_empty() {
206                return Ok(());
207            }
208
209            let mut pipeline = redis_client::pipe();
210            pipeline.atomic();
211            for op in ops {
212                match op {
213                    RemoteBatchOp::Upsert { key, value } => {
214                        pipeline
215                            .cmd("SET")
216                            .arg(self.node_key(key))
217                            .arg(*value)
218                            .ignore();
219                    }
220                    RemoteBatchOp::Delete { key } => {
221                        pipeline.cmd("DEL").arg(self.node_key(key)).ignore();
222                    }
223                }
224            }
225
226            let mut connection = self.connection.clone();
227            pipeline.query_async::<()>(&mut connection).await
228        }
229
230        async fn batch_get_nodes_ordered(
231            &self,
232            keys: &[&[u8]],
233        ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
234            if keys.is_empty() {
235                return Ok(Vec::new());
236            }
237
238            let mut command = redis_client::cmd("MGET");
239            for key in keys {
240                command.arg(self.node_key(key));
241            }
242
243            let mut connection = self.connection.clone();
244            command.query_async(&mut connection).await
245        }
246
247        async fn batch_put_nodes(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
248            if entries.is_empty() {
249                return Ok(());
250            }
251
252            let mut pipeline = redis_client::pipe();
253            pipeline.atomic();
254            for (key, value) in entries {
255                pipeline
256                    .cmd("SET")
257                    .arg(self.node_key(key))
258                    .arg(*value)
259                    .ignore();
260            }
261
262            let mut connection = self.connection.clone();
263            pipeline.query_async::<()>(&mut connection).await
264        }
265
266        async fn list_node_cids(&self) -> Result<Vec<Vec<u8>>, Self::Error> {
267            let prefix = self.family_prefix(NODE_FAMILY);
268            let pattern = self.family_pattern(NODE_FAMILY);
269            let mut cids = self
270                .scan_keys(&pattern)
271                .await?
272                .into_iter()
273                .filter_map(|key| {
274                    key.strip_prefix(prefix.as_slice())
275                        .filter(|cid| cid.len() == 32)
276                        .map(<[u8]>::to_vec)
277                })
278                .collect::<Vec<_>>();
279            cids.sort();
280            Ok(cids)
281        }
282
283        fn prefers_batch_reads(&self) -> bool {
284            true
285        }
286
287        fn read_parallelism(&self) -> usize {
288            self.read_parallelism
289        }
290
291        fn supports_hints(&self) -> bool {
292            true
293        }
294
295        async fn get_hint(
296            &self,
297            namespace: &[u8],
298            key: &[u8],
299        ) -> Result<Option<Vec<u8>>, Self::Error> {
300            let mut connection = self.connection.clone();
301            redis_client::cmd("GET")
302                .arg(self.hint_key(namespace, key))
303                .query_async(&mut connection)
304                .await
305        }
306
307        async fn put_hint(
308            &self,
309            namespace: &[u8],
310            key: &[u8],
311            value: &[u8],
312        ) -> Result<(), Self::Error> {
313            let mut connection = self.connection.clone();
314            redis_client::cmd("SET")
315                .arg(self.hint_key(namespace, key))
316                .arg(value)
317                .query_async::<()>(&mut connection)
318                .await
319        }
320
321        async fn batch_put_nodes_with_hint(
322            &self,
323            entries: &[(&[u8], &[u8])],
324            namespace: &[u8],
325            key: &[u8],
326            value: &[u8],
327        ) -> Result<(), Self::Error> {
328            let mut pipeline = redis_client::pipe();
329            pipeline.atomic();
330            for (key, value) in entries {
331                pipeline
332                    .cmd("SET")
333                    .arg(self.node_key(key))
334                    .arg(*value)
335                    .ignore();
336            }
337            pipeline
338                .cmd("SET")
339                .arg(self.hint_key(namespace, key))
340                .arg(value)
341                .ignore();
342
343            let mut connection = self.connection.clone();
344            pipeline.query_async::<()>(&mut connection).await
345        }
346
347        async fn get_root_manifest(&self, name: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
348            let mut connection = self.connection.clone();
349            redis_client::cmd("GET")
350                .arg(self.root_key(name))
351                .query_async(&mut connection)
352                .await
353        }
354
355        async fn put_root_manifest(&self, name: &[u8], manifest: &[u8]) -> Result<(), Self::Error> {
356            let mut connection = self.connection.clone();
357            redis_client::cmd("SET")
358                .arg(self.root_key(name))
359                .arg(manifest)
360                .query_async::<()>(&mut connection)
361                .await
362        }
363
364        async fn delete_root_manifest(&self, name: &[u8]) -> Result<(), Self::Error> {
365            let mut connection = self.connection.clone();
366            redis_client::cmd("DEL")
367                .arg(self.root_key(name))
368                .query_async::<()>(&mut connection)
369                .await
370        }
371
372        async fn compare_and_swap_root_manifest(
373            &self,
374            name: &[u8],
375            expected: Option<&[u8]>,
376            new: Option<&[u8]>,
377        ) -> Result<RemoteManifestUpdate, Self::Error> {
378            let script = Script::new(ROOT_CAS_LUA);
379            let mut invocation = script.prepare_invoke();
380            invocation
381                .key(self.root_key(name))
382                .arg(if expected.is_some() { b"1" } else { b"0" }.as_slice())
383                .arg(expected.unwrap_or_default())
384                .arg(if new.is_some() { b"1" } else { b"0" }.as_slice())
385                .arg(new.unwrap_or_default());
386
387            let mut connection = self.connection.clone();
388            let response: Value = invocation.invoke_async(&mut connection).await?;
389            parse_root_cas_response(response)
390        }
391
392        async fn list_root_manifests(&self) -> Result<Vec<RemoteNamedRoot>, Self::Error> {
393            let prefix = self.family_prefix(ROOT_FAMILY);
394            let pattern = self.family_pattern(ROOT_FAMILY);
395            let mut names = self
396                .scan_keys(&pattern)
397                .await?
398                .into_iter()
399                .filter_map(|key| key.strip_prefix(prefix.as_slice()).map(<[u8]>::to_vec))
400                .collect::<Vec<_>>();
401            names.sort();
402
403            let mut roots = Vec::with_capacity(names.len());
404            for name in names {
405                if let Some(manifest) = self.get_root_manifest(&name).await? {
406                    roots.push(RemoteNamedRoot::new(name, manifest));
407                }
408            }
409            Ok(roots)
410        }
411
412        fn supports_transactions(&self) -> bool {
413            true
414        }
415
416        async fn commit_transaction(
417            &self,
418            node_writes: &[RemoteBatchOp<'_>],
419            root_conditions: &[RemoteRootCondition],
420            root_writes: &[RemoteRootWrite],
421        ) -> Result<RemoteTransactionUpdate, Self::Error> {
422            let script = Script::new(TRANSACTION_COMMIT_LUA);
423            let mut invocation = script.prepare_invoke();
424            for condition in root_conditions {
425                invocation.key(self.root_key(&condition.name));
426            }
427            for write in node_writes {
428                match write {
429                    RemoteBatchOp::Upsert { key, .. } | RemoteBatchOp::Delete { key } => {
430                        invocation.key(self.node_key(key));
431                    }
432                }
433            }
434            for write in root_writes {
435                match write {
436                    RemoteRootWrite::Put { name, .. } | RemoteRootWrite::Delete { name } => {
437                        invocation.key(self.root_key(name));
438                    }
439                }
440            }
441
442            invocation
443                .arg(root_conditions.len())
444                .arg(node_writes.len())
445                .arg(root_writes.len());
446            for condition in root_conditions {
447                invocation
448                    .arg(
449                        if condition.expected.is_some() {
450                            b"1"
451                        } else {
452                            b"0"
453                        }
454                        .as_slice(),
455                    )
456                    .arg(condition.expected.as_deref().unwrap_or_default());
457            }
458            for write in node_writes {
459                match write {
460                    RemoteBatchOp::Upsert { value, .. } => {
461                        invocation.arg("upsert").arg(*value);
462                    }
463                    RemoteBatchOp::Delete { .. } => {
464                        invocation.arg("delete");
465                    }
466                }
467            }
468            for write in root_writes {
469                match write {
470                    RemoteRootWrite::Put { manifest, .. } => {
471                        invocation.arg("put").arg(manifest);
472                    }
473                    RemoteRootWrite::Delete { .. } => {
474                        invocation.arg("delete");
475                    }
476                }
477            }
478
479            let mut connection = self.connection.clone();
480            let response: Value = invocation.invoke_async(&mut connection).await?;
481            parse_transaction_response(response, root_conditions)
482        }
483    }
484
485    fn parse_root_cas_response(response: Value) -> Result<RemoteManifestUpdate, RedisError> {
486        let Value::Array(values) = response else {
487            return Err(redis_type_error("root CAS script returned a non-array"));
488        };
489        let [applied, current] = values
490            .try_into()
491            .map_err(|_| redis_type_error("root CAS script returned wrong arity"))?;
492
493        if value_to_bool(applied)? {
494            return Ok(RemoteManifestUpdate::Applied);
495        }
496
497        Ok(RemoteManifestUpdate::Conflict {
498            current: value_to_optional_bytes(current)?,
499        })
500    }
501
502    fn value_to_bool(value: Value) -> Result<bool, RedisError> {
503        match value {
504            Value::Int(0) => Ok(false),
505            Value::Int(1) => Ok(true),
506            Value::Boolean(value) => Ok(value),
507            other => Err(redis_type_error(format!(
508                "root CAS script returned invalid applied flag: {other:?}"
509            ))),
510        }
511    }
512
513    fn value_to_usize(value: Value) -> Result<usize, RedisError> {
514        match value {
515            Value::Int(value) if value >= 0 => Ok(value as usize),
516            other => Err(redis_type_error(format!(
517                "transaction script returned invalid conflict index: {other:?}"
518            ))),
519        }
520    }
521
522    fn value_to_optional_bytes(value: Value) -> Result<Option<Vec<u8>>, RedisError> {
523        match value {
524            Value::Nil => Ok(None),
525            Value::Boolean(false) => Ok(None),
526            Value::BulkString(bytes) => Ok(Some(bytes)),
527            other => Err(redis_type_error(format!(
528                "root CAS script returned invalid current manifest: {other:?}"
529            ))),
530        }
531    }
532
533    fn parse_transaction_response(
534        response: Value,
535        root_conditions: &[RemoteRootCondition],
536    ) -> Result<RemoteTransactionUpdate, RedisError> {
537        let Value::Array(values) = response else {
538            return Err(redis_type_error("transaction script returned a non-array"));
539        };
540        let [applied, conflict_index, current] = values
541            .try_into()
542            .map_err(|_| redis_type_error("transaction script returned wrong arity"))?;
543
544        if value_to_bool(applied)? {
545            return Ok(RemoteTransactionUpdate::Applied);
546        }
547
548        let index = value_to_usize(conflict_index)?;
549        if index == 0 || index > root_conditions.len() {
550            return Err(redis_type_error(format!(
551                "transaction script returned out-of-range conflict index: {index}"
552            )));
553        }
554        let condition = &root_conditions[index - 1];
555        Ok(RemoteTransactionUpdate::Conflict(
556            RemoteTransactionConflict::new(
557                condition.name.clone(),
558                condition.expected.clone(),
559                value_to_optional_bytes(current)?,
560            ),
561        ))
562    }
563
564    fn redis_type_error(detail: impl Into<String>) -> RedisError {
565        (
566            ErrorKind::TypeError,
567            "unexpected Redis adapter response",
568            detail.into(),
569        )
570            .into()
571    }
572
573    const DEFAULT_KEY_PREFIX: &[u8] = b"prolly:";
574    const DEFAULT_READ_PARALLELISM: usize = 16;
575    const SCAN_COUNT: usize = 1024;
576    const DELETE_CHUNK_SIZE: usize = 512;
577
578    const NODE_FAMILY: &[u8] = b"node:";
579    const ROOT_FAMILY: &[u8] = b"root:";
580    const HINT_FAMILY: &[u8] = b"hint:";
581
582    /// Recommended key prefix for immutable node values.
583    pub const NODE_KEY_PREFIX: &str = "prolly:node:";
584    /// Recommended key prefix for named root manifests.
585    pub const ROOT_KEY_PREFIX: &str = "prolly:root:";
586    /// Recommended key prefix for hints.
587    pub const HINT_KEY_PREFIX: &str = "prolly:hint:";
588
589    const ROOT_CAS_LUA: &str = r#"
590local current = redis.call('GET', KEYS[1])
591local has_expected = ARGV[1]
592local expected = ARGV[2]
593local has_new = ARGV[3]
594local new_value = ARGV[4]
595
596if has_expected == '1' then
597  if current == false or current ~= expected then
598    return {0, current}
599  end
600else
601  if current ~= false then
602    return {0, current}
603  end
604end
605
606if has_new == '1' then
607  redis.call('SET', KEYS[1], new_value)
608else
609  redis.call('DEL', KEYS[1])
610end
611
612return {1, false}
613"#;
614
615    const TRANSACTION_COMMIT_LUA: &str = r#"
616local condition_count = tonumber(ARGV[1])
617local node_write_count = tonumber(ARGV[2])
618local root_write_count = tonumber(ARGV[3])
619local arg_index = 4
620
621for i = 1, condition_count do
622  local current = redis.call('GET', KEYS[i])
623  local has_expected = ARGV[arg_index]
624  local expected = ARGV[arg_index + 1]
625  arg_index = arg_index + 2
626
627  if has_expected == '1' then
628    if current == false or current ~= expected then
629      return {0, i, current}
630    end
631  else
632    if current ~= false then
633      return {0, i, current}
634    end
635  end
636end
637
638local node_key_offset = condition_count
639for i = 1, node_write_count do
640  local kind = ARGV[arg_index]
641  arg_index = arg_index + 1
642  local key = KEYS[node_key_offset + i]
643
644  if kind == 'upsert' then
645    redis.call('SET', key, ARGV[arg_index])
646    arg_index = arg_index + 1
647  elseif kind == 'delete' then
648    redis.call('DEL', key)
649  else
650    error('unknown transaction node op: ' .. tostring(kind))
651  end
652end
653
654local root_key_offset = condition_count + node_write_count
655for i = 1, root_write_count do
656  local kind = ARGV[arg_index]
657  arg_index = arg_index + 1
658  local key = KEYS[root_key_offset + i]
659
660  if kind == 'put' then
661    redis.call('SET', key, ARGV[arg_index])
662    arg_index = arg_index + 1
663  elseif kind == 'delete' then
664    redis.call('DEL', key)
665  else
666    error('unknown transaction root op: ' .. tostring(kind))
667  end
668end
669
670return {1, 0, false}
671"#;
672}
673
674pub use redis::*;