Skip to content

Commit ebb2e5d

Browse files
committed
Add PaginatedKVStore traits upstreamed from ldk-server
Allows for a paginated KV store for more efficient listing of keys so you don't need to fetch all at once. Uses monotonic counter or timestamp to track the order of keys and allow for pagination. The traits are largely just copy-pasted from ldk-server. Adds some basic tests that were generated using claude code.
1 parent 3fee76b commit ebb2e5d

File tree

2 files changed

+423
-2
lines changed

2 files changed

+423
-2
lines changed

lightning/src/util/persist.rs

Lines changed: 292 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,223 @@ pub trait KVStore {
347347
) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + MaybeSend;
348348
}
349349

350+
/// Represents the response from a paginated `list` operation.
351+
///
352+
/// Contains the list of keys and an optional `next_page_token` that can be used to retrieve the
353+
/// next set of keys.
354+
#[derive(Debug, Clone, PartialEq, Eq)]
355+
pub struct PaginatedListResponse {
356+
/// A vector of keys, ordered in descending order of `creation_time`.
357+
pub keys: Vec<String>,
358+
359+
/// A token that can be used to retrieve the next set of keys.
360+
/// The `String` is the last key from the current page and the `i64` is
361+
/// the associated `creation_time` used for ordering.
362+
///
363+
/// Is `None` if there are no more pages to retrieve.
364+
pub next_page_token: Option<(String, i64)>,
365+
}
366+
367+
/// Provides an interface that allows storage and retrieval of persisted values that are associated
368+
/// with given keys, with support for pagination with time-based ordering.
369+
///
370+
/// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
371+
/// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
372+
/// ways, as long as per-namespace key uniqueness is asserted.
373+
///
374+
/// Keys and namespaces are required to be valid ASCII strings in the range of
375+
/// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
376+
/// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
377+
/// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
378+
/// should always be separated by primary namespace first, before secondary namespaces are used.
379+
/// While the number of primary namespaces will be relatively small and determined at compile time,
380+
/// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
381+
/// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
382+
/// and equally named primary or secondary namespaces must be avoided.
383+
///
384+
/// **Note:** This trait extends the functionality of [`KVStoreSync`] by adding support for
385+
/// paginated listing of keys based on a monotonic counter or logical timestamp. This is useful
386+
/// when dealing with a large number of keys that cannot be efficiently retrieved all at once.
387+
///
388+
/// For an asynchronous version of this trait, see [`PaginatedKVStore`].
389+
pub trait PaginatedKVStoreSync: KVStoreSync {
390+
/// Persists the given data under the given `key`.
391+
///
392+
/// If the key does not exist, it will be created with the given `creation_time`. If the key
393+
/// already exists, the data will be updated but the original `creation_time` will be preserved.
394+
/// This ensures consistent pagination even when entries are updated.
395+
///
396+
/// The `creation_time` parameter is an `i64` representing a monotonic counter or logical
397+
/// timestamp. It is used to track the order of keys for list operations, allowing results to
398+
/// be returned in descending order of creation time. Since `creation_time` is immutable after
399+
/// initial creation, pagination remains consistent even when entries are updated.
400+
///
401+
/// Will create the given `primary_namespace` and `secondary_namespace` if not already present
402+
/// in the store.
403+
fn write_with_time(
404+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, creation_time: i64,
405+
buf: Vec<u8>,
406+
) -> Result<(), io::Error>;
407+
408+
/// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
409+
/// `primary_namespace`, ordered in descending order of `creation_time`.
410+
///
411+
/// The `list_paginated` method returns the latest records first, based on the `creation_time`
412+
/// associated with each key. Pagination is controlled by the `next_page_token`, which is an
413+
/// `Option<(String, i64)>` used to determine the starting point for the next page of results.
414+
/// If `next_page_token` is `None`, the listing starts from the most recent entry. The
415+
/// `next_page_token` in the returned [`PaginatedListResponse`] can be used to fetch the next
416+
/// page of results.
417+
///
418+
/// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
419+
/// there are no more keys to return.
420+
fn list_paginated(
421+
&self, primary_namespace: &str, secondary_namespace: &str,
422+
next_page_token: Option<(String, i64)>,
423+
) -> Result<PaginatedListResponse, io::Error>;
424+
}
425+
426+
/// A wrapper around a [`PaginatedKVStoreSync`] that implements the [`PaginatedKVStore`] trait.
427+
/// It is not necessary to use this type directly.
428+
#[derive(Clone)]
429+
pub struct PaginatedKVStoreSyncWrapper<K: Deref>(pub K)
430+
where
431+
K::Target: PaginatedKVStoreSync;
432+
433+
impl<K: Deref> Deref for PaginatedKVStoreSyncWrapper<K>
434+
where
435+
K::Target: PaginatedKVStoreSync,
436+
{
437+
type Target = Self;
438+
fn deref(&self) -> &Self::Target {
439+
self
440+
}
441+
}
442+
443+
/// This is not exported to bindings users as async is only supported in Rust.
444+
impl<K: Deref> KVStore for PaginatedKVStoreSyncWrapper<K>
445+
where
446+
K::Target: PaginatedKVStoreSync,
447+
{
448+
fn read(
449+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
450+
) -> impl Future<Output = Result<Vec<u8>, io::Error>> + 'static + MaybeSend {
451+
let res = self.0.read(primary_namespace, secondary_namespace, key);
452+
453+
async move { res }
454+
}
455+
456+
fn write(
457+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
458+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend {
459+
let res = self.0.write(primary_namespace, secondary_namespace, key, buf);
460+
461+
async move { res }
462+
}
463+
464+
fn remove(
465+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
466+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend {
467+
let res = self.0.remove(primary_namespace, secondary_namespace, key, lazy);
468+
469+
async move { res }
470+
}
471+
472+
fn list(
473+
&self, primary_namespace: &str, secondary_namespace: &str,
474+
) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + MaybeSend {
475+
let res = self.0.list(primary_namespace, secondary_namespace);
476+
477+
async move { res }
478+
}
479+
}
480+
481+
/// This is not exported to bindings users as async is only supported in Rust.
482+
impl<K: Deref> PaginatedKVStore for PaginatedKVStoreSyncWrapper<K>
483+
where
484+
K::Target: PaginatedKVStoreSync,
485+
{
486+
fn write_with_time(
487+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, creation_time: i64,
488+
buf: Vec<u8>,
489+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend {
490+
let res =
491+
self.0.write_with_time(primary_namespace, secondary_namespace, key, creation_time, buf);
492+
493+
async move { res }
494+
}
495+
496+
fn list_paginated(
497+
&self, primary_namespace: &str, secondary_namespace: &str,
498+
next_page_token: Option<(String, i64)>,
499+
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend {
500+
let res = self.0.list_paginated(primary_namespace, secondary_namespace, next_page_token);
501+
502+
async move { res }
503+
}
504+
}
505+
506+
/// Provides an interface that allows storage and retrieval of persisted values that are associated
507+
/// with given keys, with support for pagination with time-based ordering.
508+
///
509+
/// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
510+
/// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
511+
/// ways, as long as per-namespace key uniqueness is asserted.
512+
///
513+
/// Keys and namespaces are required to be valid ASCII strings in the range of
514+
/// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
515+
/// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
516+
/// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
517+
/// should always be separated by primary namespace first, before secondary namespaces are used.
518+
/// While the number of primary namespaces will be relatively small and determined at compile time,
519+
/// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
520+
/// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
521+
/// and equally named primary or secondary namespaces must be avoided.
522+
///
523+
/// **Note:** This trait extends the functionality of [`KVStore`] by adding support for
524+
/// paginated listing of keys based on a monotonic counter or logical timestamp. This is useful
525+
/// when dealing with a large number of keys that cannot be efficiently retrieved all at once.
526+
///
527+
/// For a synchronous version of this trait, see [`PaginatedKVStoreSync`].
528+
///
529+
/// This is not exported to bindings users as async is only supported in Rust.
530+
pub trait PaginatedKVStore: KVStore {
531+
/// Persists the given data under the given `key`.
532+
///
533+
/// If the key does not exist, it will be created with the given `creation_time`. If the key
534+
/// already exists, the data will be updated but the original `creation_time` will be preserved.
535+
/// This ensures consistent pagination even when entries are updated.
536+
///
537+
/// The `creation_time` parameter is an `i64` representing a monotonic counter or logical
538+
/// timestamp. It is used to track the order of keys for list operations, allowing results to
539+
/// be returned in descending order of creation time. Since `creation_time` is immutable after
540+
/// initial creation, pagination remains consistent even when entries are updated.
541+
///
542+
/// Will create the given `primary_namespace` and `secondary_namespace` if not already present
543+
/// in the store.
544+
fn write_with_time(
545+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, creation_time: i64,
546+
buf: Vec<u8>,
547+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend;
548+
549+
/// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
550+
/// `primary_namespace`, ordered in descending order of `creation_time`.
551+
///
552+
/// The `list_paginated` method returns the latest records first, based on the `creation_time`
553+
/// associated with each key. Pagination is controlled by the `next_page_token`, which is an
554+
/// `Option<(String, i64)>` used to determine the starting point for the next page of results.
555+
/// If `next_page_token` is `None`, the listing starts from the most recent entry. The
556+
/// `next_page_token` in the returned [`PaginatedListResponse`] can be used to fetch the next
557+
/// page of results.
558+
///
559+
/// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
560+
/// there are no more keys to return.
561+
fn list_paginated(
562+
&self, primary_namespace: &str, secondary_namespace: &str,
563+
next_page_token: Option<(String, i64)>,
564+
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend;
565+
}
566+
350567
/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`]
351568
/// data migration.
352569
pub trait MigratableKVStore: KVStoreSync {
@@ -1565,7 +1782,7 @@ mod tests {
15651782
use crate::ln::msgs::BaseMessageHandler;
15661783
use crate::sync::Arc;
15671784
use crate::util::test_channel_signer::TestChannelSigner;
1568-
use crate::util::test_utils::{self, TestStore};
1785+
use crate::util::test_utils::{self, TestPaginatedStore, TestStore};
15691786
use bitcoin::hashes::hex::FromHex;
15701787
use core::cmp;
15711788

@@ -1975,4 +2192,78 @@ mod tests {
19752192
let store: Arc<dyn KVStoreSync + Send + Sync> = Arc::new(TestStore::new(false));
19762193
assert!(persist_fn::<_, TestChannelSigner>(Arc::clone(&store)));
19772194
}
2195+
2196+
#[test]
2197+
fn paginated_store_basic_operations() {
2198+
let store = TestPaginatedStore::new(10);
2199+
2200+
// Write some data
2201+
store.write_with_time("ns1", "ns2", "key1", 100, vec![1, 2, 3]).unwrap();
2202+
store.write_with_time("ns1", "ns2", "key2", 200, vec![4, 5, 6]).unwrap();
2203+
2204+
// Read it back
2205+
assert_eq!(KVStoreSync::read(&store, "ns1", "ns2", "key1").unwrap(), vec![1, 2, 3]);
2206+
assert_eq!(KVStoreSync::read(&store, "ns1", "ns2", "key2").unwrap(), vec![4, 5, 6]);
2207+
2208+
// List should return keys in descending time order
2209+
let response = store.list_paginated("ns1", "ns2", None).unwrap();
2210+
assert_eq!(response.keys, vec!["key2", "key1"]);
2211+
assert!(response.next_page_token.is_none());
2212+
2213+
// Remove a key
2214+
KVStoreSync::remove(&store, "ns1", "ns2", "key1", false).unwrap();
2215+
assert!(KVStoreSync::read(&store, "ns1", "ns2", "key1").is_err());
2216+
}
2217+
2218+
#[test]
2219+
fn paginated_store_pagination() {
2220+
let store = TestPaginatedStore::new(2);
2221+
2222+
// Write 5 items with different times
2223+
for i in 0..5i64 {
2224+
store.write_with_time("ns", "", &format!("key{i}"), i, vec![i as u8]).unwrap();
2225+
}
2226+
2227+
// First page should have 2 items (highest times first: key4, key3)
2228+
let page1 = store.list_paginated("ns", "", None).unwrap();
2229+
assert_eq!(page1.keys.len(), 2);
2230+
assert_eq!(page1.keys, vec!["key4", "key3"]);
2231+
assert!(page1.next_page_token.is_some());
2232+
2233+
// Second page
2234+
let page2 = store.list_paginated("ns", "", page1.next_page_token).unwrap();
2235+
assert_eq!(page2.keys.len(), 2);
2236+
assert_eq!(page2.keys, vec!["key2", "key1"]);
2237+
assert!(page2.next_page_token.is_some());
2238+
2239+
// Third page (last item)
2240+
let page3 = store.list_paginated("ns", "", page2.next_page_token).unwrap();
2241+
assert_eq!(page3.keys.len(), 1);
2242+
assert_eq!(page3.keys, vec!["key0"]);
2243+
assert!(page3.next_page_token.is_none());
2244+
}
2245+
2246+
#[test]
2247+
fn paginated_store_update_preserves_creation_time() {
2248+
let store = TestPaginatedStore::new(10);
2249+
2250+
// Write items with specific creation times
2251+
store.write_with_time("ns", "", "key1", 100, vec![1]).unwrap();
2252+
store.write_with_time("ns", "", "key2", 200, vec![2]).unwrap();
2253+
store.write_with_time("ns", "", "key3", 300, vec![3]).unwrap();
2254+
2255+
// Verify initial order (descending by creation_time)
2256+
let response = store.list_paginated("ns", "", None).unwrap();
2257+
assert_eq!(response.keys, vec!["key3", "key2", "key1"]);
2258+
2259+
// Update key1 with a new creation_time that would put it first if used
2260+
store.write_with_time("ns", "", "key1", 999, vec![1, 1]).unwrap();
2261+
2262+
// Verify data was updated
2263+
assert_eq!(KVStoreSync::read(&store, "ns", "", "key1").unwrap(), vec![1, 1]);
2264+
2265+
// Verify order is unchanged - creation_time should have been preserved
2266+
let response = store.list_paginated("ns", "", None).unwrap();
2267+
assert_eq!(response.keys, vec!["key3", "key2", "key1"]);
2268+
}
19782269
}

0 commit comments

Comments
 (0)