Skip to content

Commit cab0b9b

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 cab0b9b

File tree

2 files changed

+392
-2
lines changed

2 files changed

+392
-2
lines changed

lightning/src/util/persist.rs

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

350+
/// An opaque token used for paginated listing operations.
351+
///
352+
/// This token should be treated as an opaque value by callers. Pass the token returned from
353+
/// one `list_paginated` call to the next call to continue pagination. The internal format
354+
/// is implementation-defined and may change between versions.
355+
#[derive(Debug, Clone, PartialEq, Eq)]
356+
pub struct PageToken(pub String);
357+
358+
/// Represents the response from a paginated `list` operation.
359+
///
360+
/// Contains the list of keys and a token for retrieving the next page of results.
361+
#[derive(Debug, Clone, PartialEq, Eq)]
362+
pub struct PaginatedListResponse {
363+
/// A vector of keys, ordered from most recently created to least recently created.
364+
pub keys: Vec<String>,
365+
366+
/// A token that can be passed to the next call to continue pagination.
367+
///
368+
/// Is `None` if there are no more pages to retrieve.
369+
pub next_page_token: Option<PageToken>,
370+
}
371+
372+
/// Provides an interface that allows storage and retrieval of persisted values that are associated
373+
/// with given keys, with support for pagination.
374+
///
375+
/// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
376+
/// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
377+
/// ways, as long as per-namespace key uniqueness is asserted.
378+
///
379+
/// Keys and namespaces are required to be valid ASCII strings in the range of
380+
/// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
381+
/// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
382+
/// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
383+
/// should always be separated by primary namespace first, before secondary namespaces are used.
384+
/// While the number of primary namespaces will be relatively small and determined at compile time,
385+
/// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
386+
/// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
387+
/// and equally named primary or secondary namespaces must be avoided.
388+
///
389+
/// **Note:** This trait extends the functionality of [`KVStoreSync`] by adding support for
390+
/// paginated listing of keys in creation order. This is useful when dealing with a large number
391+
/// of keys that cannot be efficiently retrieved all at once.
392+
///
393+
/// For an asynchronous version of this trait, see [`PaginatedKVStore`].
394+
pub trait PaginatedKVStoreSync: KVStoreSync {
395+
/// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
396+
/// `primary_namespace`, ordered from most recently created to least recently created.
397+
///
398+
/// Implementations must return keys in reverse creation order (newest first). How creation
399+
/// order is tracked is implementation-defined (e.g., storing creation timestamps, using an
400+
/// incrementing ID, or another mechanism). Creation order (not last-updated order) is used
401+
/// to prevent race conditions during pagination: if keys were ordered by update time, a key
402+
/// updated mid-pagination could shift position, causing it to be skipped or returned twice
403+
/// across pages.
404+
///
405+
/// If `page_token` is provided, listing continues from where the previous page left off.
406+
/// If `None`, listing starts from the most recently created entry. The `next_page_token`
407+
/// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch
408+
/// the next page.
409+
///
410+
/// Implementations must generate a [`PageToken`] that encodes enough information to resume
411+
/// listing from the correct position. The token should encode the creation timestamp (or
412+
/// sequence number) and key name of the last returned entry. Tokens must remain valid across
413+
/// multiple calls within a reasonable timeframe. If the entry referenced by a token has been
414+
/// deleted, implementations should resume from the next valid position rather than failing.
415+
/// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should
416+
/// not be used across different namespace pairs.
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, page_token: Option<PageToken>,
422+
) -> Result<PaginatedListResponse, io::Error>;
423+
}
424+
425+
/// A wrapper around a [`PaginatedKVStoreSync`] that implements the [`PaginatedKVStore`] trait.
426+
/// It is not necessary to use this type directly.
427+
#[derive(Clone)]
428+
pub struct PaginatedKVStoreSyncWrapper<K: Deref>(pub K)
429+
where
430+
K::Target: PaginatedKVStoreSync;
431+
432+
impl<K: Deref> Deref for PaginatedKVStoreSyncWrapper<K>
433+
where
434+
K::Target: PaginatedKVStoreSync,
435+
{
436+
type Target = Self;
437+
fn deref(&self) -> &Self::Target {
438+
self
439+
}
440+
}
441+
442+
/// This is not exported to bindings users as async is only supported in Rust.
443+
impl<K: Deref> KVStore for PaginatedKVStoreSyncWrapper<K>
444+
where
445+
K::Target: PaginatedKVStoreSync,
446+
{
447+
fn read(
448+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
449+
) -> impl Future<Output = Result<Vec<u8>, io::Error>> + 'static + MaybeSend {
450+
let res = self.0.read(primary_namespace, secondary_namespace, key);
451+
452+
async move { res }
453+
}
454+
455+
fn write(
456+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>,
457+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend {
458+
let res = self.0.write(primary_namespace, secondary_namespace, key, buf);
459+
460+
async move { res }
461+
}
462+
463+
fn remove(
464+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
465+
) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend {
466+
let res = self.0.remove(primary_namespace, secondary_namespace, key, lazy);
467+
468+
async move { res }
469+
}
470+
471+
fn list(
472+
&self, primary_namespace: &str, secondary_namespace: &str,
473+
) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + MaybeSend {
474+
let res = self.0.list(primary_namespace, secondary_namespace);
475+
476+
async move { res }
477+
}
478+
}
479+
480+
/// This is not exported to bindings users as async is only supported in Rust.
481+
impl<K: Deref> PaginatedKVStore for PaginatedKVStoreSyncWrapper<K>
482+
where
483+
K::Target: PaginatedKVStoreSync,
484+
{
485+
fn list_paginated(
486+
&self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>,
487+
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend {
488+
let res = self.0.list_paginated(primary_namespace, secondary_namespace, page_token);
489+
490+
async move { res }
491+
}
492+
}
493+
494+
/// Provides an interface that allows storage and retrieval of persisted values that are associated
495+
/// with given keys, with support for pagination.
496+
///
497+
/// In order to avoid collisions, the key space is segmented based on the given `primary_namespace`s
498+
/// and `secondary_namespace`s. Implementations of this trait are free to handle them in different
499+
/// ways, as long as per-namespace key uniqueness is asserted.
500+
///
501+
/// Keys and namespaces are required to be valid ASCII strings in the range of
502+
/// [`KVSTORE_NAMESPACE_KEY_ALPHABET`] and no longer than [`KVSTORE_NAMESPACE_KEY_MAX_LEN`]. Empty
503+
/// primary namespaces and secondary namespaces (`""`) are considered valid; however, if
504+
/// `primary_namespace` is empty, `secondary_namespace` must also be empty. This means that concerns
505+
/// should always be separated by primary namespace first, before secondary namespaces are used.
506+
/// While the number of primary namespaces will be relatively small and determined at compile time,
507+
/// there may be many secondary namespaces per primary namespace. Note that per-namespace uniqueness
508+
/// needs to also hold for keys *and* namespaces in any given namespace, i.e., conflicts between keys
509+
/// and equally named primary or secondary namespaces must be avoided.
510+
///
511+
/// **Note:** This trait extends the functionality of [`KVStore`] by adding support for
512+
/// paginated listing of keys in creation order. This is useful when dealing with a large number
513+
/// of keys that cannot be efficiently retrieved all at once.
514+
///
515+
/// For a synchronous version of this trait, see [`PaginatedKVStoreSync`].
516+
///
517+
/// This is not exported to bindings users as async is only supported in Rust.
518+
pub trait PaginatedKVStore: KVStore {
519+
/// Returns a paginated list of keys that are stored under the given `secondary_namespace` in
520+
/// `primary_namespace`, ordered from most recently created to least recently created.
521+
///
522+
/// Implementations must return keys in reverse creation order (newest first). How creation
523+
/// order is tracked is implementation-defined (e.g., storing creation timestamps, using an
524+
/// incrementing ID, or another mechanism). Creation order (not last-updated order) is used
525+
/// to prevent race conditions during pagination: if keys were ordered by update time, a key
526+
/// updated mid-pagination could shift position, causing it to be skipped or returned twice
527+
/// across pages.
528+
///
529+
/// If `page_token` is provided, listing continues from where the previous page left off.
530+
/// If `None`, listing starts from the most recently created entry. The `next_page_token`
531+
/// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch
532+
/// the next page.
533+
///
534+
/// Implementations must generate a [`PageToken`] that encodes enough information to resume
535+
/// listing from the correct position. The token should encode the creation timestamp (or
536+
/// sequence number) and key name of the last returned entry. Tokens must remain valid across
537+
/// multiple calls within a reasonable timeframe. If the entry referenced by a token has been
538+
/// deleted, implementations should resume from the next valid position rather than failing.
539+
/// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should
540+
/// not be used across different namespace pairs.
541+
///
542+
/// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if
543+
/// there are no more keys to return.
544+
fn list_paginated(
545+
&self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>,
546+
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend;
547+
}
548+
350549
/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`]
351550
/// data migration.
352551
pub trait MigratableKVStore: KVStoreSync {
@@ -1565,7 +1764,7 @@ mod tests {
15651764
use crate::ln::msgs::BaseMessageHandler;
15661765
use crate::sync::Arc;
15671766
use crate::util::test_channel_signer::TestChannelSigner;
1568-
use crate::util::test_utils::{self, TestStore};
1767+
use crate::util::test_utils::{self, TestPaginatedStore, TestStore};
15691768
use bitcoin::hashes::hex::FromHex;
15701769
use core::cmp;
15711770

@@ -1975,4 +2174,78 @@ mod tests {
19752174
let store: Arc<dyn KVStoreSync + Send + Sync> = Arc::new(TestStore::new(false));
19762175
assert!(persist_fn::<_, TestChannelSigner>(Arc::clone(&store)));
19772176
}
2177+
2178+
#[test]
2179+
fn paginated_store_basic_operations() {
2180+
let store = TestPaginatedStore::new(10);
2181+
2182+
// Write some data
2183+
store.write("ns1", "ns2", "key1", vec![1, 2, 3]).unwrap();
2184+
store.write("ns1", "ns2", "key2", vec![4, 5, 6]).unwrap();
2185+
2186+
// Read it back
2187+
assert_eq!(KVStoreSync::read(&store, "ns1", "ns2", "key1").unwrap(), vec![1, 2, 3]);
2188+
assert_eq!(KVStoreSync::read(&store, "ns1", "ns2", "key2").unwrap(), vec![4, 5, 6]);
2189+
2190+
// List should return keys in descending order
2191+
let response = store.list_paginated("ns1", "ns2", None).unwrap();
2192+
assert_eq!(response.keys, vec!["key2", "key1"]);
2193+
assert!(response.next_page_token.is_none());
2194+
2195+
// Remove a key
2196+
KVStoreSync::remove(&store, "ns1", "ns2", "key1", false).unwrap();
2197+
assert!(KVStoreSync::read(&store, "ns1", "ns2", "key1").is_err());
2198+
}
2199+
2200+
#[test]
2201+
fn paginated_store_pagination() {
2202+
let store = TestPaginatedStore::new(2);
2203+
2204+
// Write 5 items with different order values
2205+
for i in 0..5i64 {
2206+
store.write("ns", "", &format!("key{i}"), vec![i as u8]).unwrap();
2207+
}
2208+
2209+
// First page should have 2 items (most recently created first: key4, key3)
2210+
let page1 = store.list_paginated("ns", "", None).unwrap();
2211+
assert_eq!(page1.keys.len(), 2);
2212+
assert_eq!(page1.keys, vec!["key4", "key3"]);
2213+
assert!(page1.next_page_token.is_some());
2214+
2215+
// Second page
2216+
let page2 = store.list_paginated("ns", "", page1.next_page_token).unwrap();
2217+
assert_eq!(page2.keys.len(), 2);
2218+
assert_eq!(page2.keys, vec!["key2", "key1"]);
2219+
assert!(page2.next_page_token.is_some());
2220+
2221+
// Third page (last item)
2222+
let page3 = store.list_paginated("ns", "", page2.next_page_token).unwrap();
2223+
assert_eq!(page3.keys.len(), 1);
2224+
assert_eq!(page3.keys, vec!["key0"]);
2225+
assert!(page3.next_page_token.is_none());
2226+
}
2227+
2228+
#[test]
2229+
fn paginated_store_update_preserves_order() {
2230+
let store = TestPaginatedStore::new(10);
2231+
2232+
// Write items with specific order values
2233+
store.write("ns", "", "key1", vec![1]).unwrap();
2234+
store.write("ns", "", "key2", vec![2]).unwrap();
2235+
store.write("ns", "", "key3", vec![3]).unwrap();
2236+
2237+
// Verify initial order (newest first)
2238+
let response = store.list_paginated("ns", "", None).unwrap();
2239+
assert_eq!(response.keys, vec!["key3", "key2", "key1"]);
2240+
2241+
// Update key1 with a new order value that would put it first if used
2242+
store.write("ns", "", "key1", vec![1, 1]).unwrap();
2243+
2244+
// Verify data was updated
2245+
assert_eq!(KVStoreSync::read(&store, "ns", "", "key1").unwrap(), vec![1, 1]);
2246+
2247+
// Verify order is unchanged - creation order should have been preserved
2248+
let response = store.list_paginated("ns", "", None).unwrap();
2249+
assert_eq!(response.keys, vec!["key3", "key2", "key1"]);
2250+
}
19782251
}

0 commit comments

Comments
 (0)