Skip to content

Commit ffdf5c9

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 ffdf5c9

File tree

2 files changed

+437
-2
lines changed

2 files changed

+437
-2
lines changed

lightning/src/util/persist.rs

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

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

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

0 commit comments

Comments
 (0)