Skip to content

Replace PostgreSQL advisory locking with fenced leases - #1111

Merged
tnull merged 2 commits into
lightningdevkit:mainfrom
joostjager:postgres-leases
Oct 6, 2026
Merged

tnull merged 2 commits into
lightningdevkit:mainfrom
joostjager:postgres-leases

Conversation

@joostjager

@joostjager joostjager commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Replace the temporary PostgreSQL advisory lock introduced in #1012 with a renewable lease in a companion table. The advisory-lock mechanism has not been released, so no migration is needed.

Acquire the lease atomically with schema setup, before loading persisted state. Validate ownership and expiry and renew the lease within each KV mutation transaction, so stale writes and deletes cannot commit. This replaces the separate lock checks before and after operations.

Renew idle leases in the background and release them by owner ID on drop. Lease loss panics in the calling task for mutations or in the background renewal task; background renewal errors and timeouts also panic. No recovery API or node lifecycle changes are introduced.

Tests cover setup rollback, contention, idle renewal, renewal timeouts, stale mutations, and release after takeover. Also verified contention, takeover after a crash, graceful restart, and stale-owner failure using two real LDK Server instances sharing a database.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 21, 2026 •

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull tnull left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a rebase for CI to run. Let me know when fully ready for review.

@tnull tnull added this to the 0.8 milestone Sep 22, 2026
@joostjager

Copy link
Copy Markdown
Contributor Author

If we indeed add this on the 0.8 milestone, I'll remove the migration because the temp. locking solution won't be in a release.

@tnull

tnull commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

If we indeed add this on the 0.8 milestone, I'll remove the migration because the temp. locking solution won't be in a release.

Do it.

Create the KV table, record its schema version, and create the listing
index in one transaction so failed initialization rolls back schema
changes. Keep database creation outside the transaction and preserve the
session advisory lock for the store's lifetime.

Add a regression test that forces index creation to fail and verifies
the schema version update is rolled back.
@joostjager
joostjager force-pushed the postgres-leases branch 2 times, most recently from 8f19873 to 6532d1b Compare September 23, 2026 12:20
@joostjager
joostjager marked this pull request as ready for review September 23, 2026 12:21
@joostjager
joostjager removed the request for review from TheBlueMatt September 23, 2026 12:23
@joostjager

Copy link
Copy Markdown
Contributor Author

I’ve removed the migration. This now directly replaces advisory locks with the lease table.

I’m particularly happy to see all the scattered checks before and after operations to confirm we still hold the lock disappear. PostgreSQL now checks lease validity within the transaction.

I had a good back-and-forth with AI to minimize the diff. I think it’s looking pretty good now.

@joostjager
joostjager requested a review from tnull September 23, 2026 12:24

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yet to do a very detailed review. Also tagging @benthecarman as a secondary reviewer as he did the first approach.

Comment thread src/io/postgres_store/mod.rs Outdated
.execute(&update_sql, &[&self.lease_owner_id.as_slice(), &lease_duration_secs])
.await?;
if updated != 1 {
panic!("PostgreSQL node lease was lost; continuing may corrupt node state");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should be std::process:abort as panicking the tokio task won't abort the whole process, but just have the task return a JoinError in the end.

@joostjager joostjager Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous advisory-lock implementation used assert! rather than abort, and LDK Server sets panic = "abort" for both dev and release builds. But definitely seems safer to use std::process:abort, will change.

Note that not panicking wouldn't be a data consistency issue, because the consistency is guarded in each transaction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, it seems in other places in ldk-node, it's not an explicit process abort?

@tnull
tnull requested a review from benthecarman September 24, 2026 12:41
Comment thread src/io/postgres_store/mod.rs Outdated
);
}

async fn execute_mutation<F: FnOnce(PgError) -> io::Error>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not the biggest fan of extracting helpers that are less than 5-7 lines of code, as they simply tend to increase clutter and make it harder to see what's going on. Is there a particular benefit from this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the later commit, it is expanded and no longer 5-7 lines.

Comment thread src/builder.rs Outdated
/// This acquires an exclusive lease for the selected KV table before reading persisted node
/// state. Nodes may share a database when each node identity uses a distinct `kv_table_name`.
/// Mutations panic on detected lease loss. Failed or timed-out renewals panic in the background
/// renewal task. Node recovery is not handled automatically.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to mention it's not handled automatically, but should we at least give the user half a sentence of guidance what this effectively means, i.e., what they are supposed to do if it panics?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Comment thread tests/common/mod.rs Outdated

/// Drops the given table from the `ldk_db` database, ignoring the case where the database doesn't
/// exist yet. Used to ensure a clean slate before and after Postgres-backed tests.
/// Also drops the companion lease table.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want/need these docs in test-only code to begin with?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduced and made comment more generic

Comment thread src/io/postgres_store/mod.rs
Ok(Self { inner, next_write_version, internal_runtime: Some(internal_runtime) })

let inner_ref = Arc::clone(&inner);
let lease_renewal_task = internal_runtime.spawn(async move {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we need to make sure we hard panic here, too:

Codex:

  1. High — Renewal failure leaves the node running. The renewal task (/home/tnull/workspace/ldk-node-pr-1111-20260928-cPRuYZ/src/io/postgres_store/mod.rs:216) panics on failure, but nothing observes its handle during normal operation. Renewal stops while the node continues running; the
    process wide abort discussed earlier is still absent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside of hard panic is that we can't cleanly unit test this behavior anymore, and need to spawn a child process to let it panic. I tend towards just documenting to set panic=abort. ldk-server already has that.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can, see for example

async fn catch_future_unwind<F: Future>(future: F) -> std::thread::Result<F::Output> {
let mut future = std::pin::pin!(future);
std::future::poll_fn(|cx| {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| future.as_mut().poll(cx))) {
Ok(std::task::Poll::Ready(output)) => std::task::Poll::Ready(Ok(output)),
Ok(std::task::Poll::Pending) => std::task::Poll::Pending,
Err(panic) => std::task::Poll::Ready(Err(panic)),
}
})
.await
}
async fn assert_invalid_write_fails<K: KVStore + RefUnwindSafe>(
kv_store: &K, primary_namespace: &str, secondary_namespace: &str, key: &str, data: Vec<u8>,
) {
let res = std::panic::catch_unwind(|| {
KVStore::write(kv_store, primary_namespace, secondary_namespace, key, data)
});
if let Ok(fut) = res {
if let Ok(write_res) = catch_future_unwind(fut).await {
assert!(write_res.is_err());
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can this catch a process abort? The docs say:

This function only catches unwinding panics, not those that abort the process.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it a panic! because of the unit tests, and also consistency with panics elsewhere in ldk-node. But open to different trade offs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so it should def. be consistent between the different cases, and it seems we want to enforce process abortion consistently. You're indeed correct about the catch_unwind behavior, forgot about that, but that still doesn't change the core issue, IMO. So either we just leave this behavior untested or could do something where we switch to a catchable panic! under cfg(test) while doing a 'real' process exit in production?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the benefit, but I’d prefer to keep normal panics for this PR. LDK Server already uses panic = "abort", and we document that requirement for direct LDK Node users. Without it, lease checks still prevent stale writes, though the process may remain partially running.

I’d rather address library-enforced process termination consistently in a separate change than introduce test-specific behavior just for lease loss.

Comment thread src/io/postgres_store/mod.rs Outdated
let mut interval = tokio::time::interval(NODE_LEASE_RENEWAL_INTERVAL);
loop {
// The first tick is immediate; later attempts follow the renewal interval.
interval.tick().await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default behavior is Burst - should we set something like:

 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread src/io/postgres_store/mod.rs Outdated
let inner_ref = Arc::clone(&inner);
let lease_renewal_task = internal_runtime.spawn(async move {
let mut interval = tokio::time::interval(NODE_LEASE_RENEWAL_INTERVAL);
loop {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a graceful shutdown path like:

 let mut interval = tokio::time::interval(NODE_LEASE_RENEWAL_INTERVAL);
 loop {
-    interval.tick().await;
+    tokio::select! {
+        biased;
+        _ = &mut shutdown_rx => break,
+        _ = interval.tick() => {}
+    }
+
     let renewal = async {
         let mut locked = inner_ref.locked_client().await?;
         // Existing renewal body...
     };
-    tokio::time::timeout(NODE_LEASE_RENEWAL_TIMEOUT, renewal)
-        .await 
-        .expect("PostgreSQL node lease renewal timed out")
-        .expect("Failed to renew PostgreSQL node lease");
+    tokio::select! {
+        biased;
+        _ = &mut shutdown_rx => break,
+        result = tokio::time::timeout(NODE_LEASE_RENEWAL_TIMEOUT, renewal) => {
+            result
+                .expect("PostgreSQL node lease renewal timed out")
+                .expect("Failed to renew PostgreSQL node lease");
+        }
+    }
 }
+
+let release_result =
+    tokio::time::timeout(NODE_LEASE_RELEASE_TIMEOUT, inner_ref.release_node_lease()).await;
+// Send release_result to a synchronous completion channel observed by Drop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored the shutdown path

Comment thread src/io/postgres_store/mod.rs Outdated

let runtime_handle = internal_runtime.handle().clone();
let inner = Arc::clone(&self.inner);
let _ = std::thread::spawn(move || {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of spawning yet another thread here we'll rather want to first send a stop signal and handle graceful shutdown as one arm in the lease task loop, something like:

 impl Drop for PostgresStore {
     fn drop(&mut self) {
-        if let Some(internal_runtime) = self.internal_runtime.as_ref() {
-            let renewal_task = self.lease_renewal_task.take();
-            if let Some(task) = renewal_task.as_ref() {
-                task.abort();
-            }
-
-            let runtime_handle = internal_runtime.handle().clone();
-            let inner = Arc::clone(&self.inner);
-            let _ = std::thread::spawn(move || {
-                runtime_handle.block_on(async move {
-                    if let Some(task) = renewal_task {
-                        let _ = task.await;
-                    }
-
-                    let _ = tokio::time::timeout(
-                        NODE_LEASE_RELEASE_TIMEOUT,
-                        inner.release_node_lease(),
-                    )
-                    .await;
-                });
-            })
-            .join();
+        if let Some(sender) = self.lease_shutdown_sender.take() {
+            let _ = sender.send(());
+        }
+        if let Some(receiver) = self.lease_shutdown_complete.take() {
+            let wait = NODE_LEASE_RELEASE_TIMEOUT + Duration::from_secs(1);
+            let result = receiver.recv_timeout(wait);
+            if let Some(logger) = self.inner.logger.as_ref() {
+                match result {
+                    Ok(Ok(())) => {},
+                    Ok(Err(e)) => log_error!(logger, "Failed to release PostgreSQL node lease: {e}"),
+                    Err(e) => log_error!(logger, "PostgreSQL lease shutdown did not complete: {e}"),
+                }
+            }
+        }
+        if let Some(task) = self.lease_renewal_task.take() {
+            task.abort(); // Only matters if the completion wait failed or timed out.
         }

         if let Some(internal_runtime) = self.internal_runtime.take() {
             if let Ok(internal_runtime) = Arc::try_unwrap(internal_runtime) {
                 internal_runtime.shutdown_background();
             }
         }
     }
 }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@joostjager
joostjager requested a review from tnull September 29, 2026 06:23
@joostjager

Copy link
Copy Markdown
Contributor Author

I plan to follow up with an LDK Server PR adding a small retry loop when the lease is already held. A deployment supervisor could also handle this by restarting the server after failed acquisition, but keeping the standby running and waiting for the lease inside LDK Server seems cleaner.

@tnull

tnull commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

I plan to follow up with an LDK Server PR adding a small retry loop when the lease is already held.

Hmm, I think conceptually stuff like that (which easily will get bigger/more complicated) should really live in LDK Node. IMO, we'll want to keep LDK Server as close to a simple API wrapper as possible.

@joostjager

Copy link
Copy Markdown
Contributor Author

Hmm, I think conceptually stuff like that (which easily will get bigger/more complicated) should really live in LDK Node. IMO, we'll want to keep LDK Server as close to a simple API wrapper as possible.

Yes, we can also put a small retry loop in LDK Node. The trade-off is that the synchronous build call would remain blocked while waiting for the lease. Keeping the retry outside that call makes it easier for the application to expose its standby status and control cancellation. We don’t currently have a way to cancel an ongoing build.

A broader node lifecycle API could change that, but my immediate focus is failover for server deployments, which seems achievable with a small change today. I’m open to either location, provided we can keep that scope narrow.

@joostjager

Copy link
Copy Markdown
Contributor Author

Regardless of where we put the retry loop, I don’t think it needs to block this PR. The priority here is replacing the temporary advisory locks with fenced leases. We can settle the retry behavior and its location in a follow-up.

@benthecarman benthecarman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude review

Comment thread src/io/postgres_store/mod.rs Outdated
transaction.execute(sql, params).await.map_err(err_map)?;
// Renew after the mutation to give the lease a later expiry. Rejection rolls back the
// mutation; success holds the lease row lock until commit.
self.renew_node_lease(&transaction).await.map_err(|e| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This UPDATE holds the lease row lock until COMMIT, so every write across the store is serialized, even with the 10-connection pool. Could we use SELECT 1 FROM <lease> WHERE id = 1 AND owner_id = $1 AND expires_at > clock_timestamp() FOR SHARE here instead? Takeover and renewal still conflict with the share lock, so it fences the same way, and writes to different keys can run in parallel again. Keep it after the mutation so the lock order matches setup. The trade-off is that writes stop extending the lease, so only the background renewal keeps it alive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good idea: less writing and less locking, leaving renewal to the background task. I've pushed a fixup for that.

I also inlined renew_node_lease now that it only had one caller, which allowed some further simplification of the renewal and shutdown code.

@benthecarman benthecarman Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One catch with claude's FOR SHARE suggestion: PostgreSQL lets new share lockers through even while an UPDATE is waiting, so continuous writes can starve the renewal past its 10s timeout. (The new test aborts the renewal task for this reason.) Adding UNIQUE (owner_id) to the lease table and using FOR KEY SHARE here fixes it. The renewal only touches expires_at, so it no longer conflicts, but a takeover changes owner_id, which is now a key column, so it still blocks until our commit. I checked this in psql and with an updated test_postgres_store_lease.

One gotcha: CREATE TABLE IF NOT EXISTS won't add UNIQUE to an existing lease table, and without it FOR KEY SHARE doesn't block takeover. So drop any *_node_lease tables created by earlier revisions of this PR. cleanup_store should drop the lease table too, since leftover tables from earlier runs hit exactly this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one! Fixed. We’re getting deeper into PostgreSQL’s locking behavior here, so I hope we’ve got it right.

Since this code hasn’t been released, we don’t need a migration for existing tables. Test cleanup now drops the lease table too.

Diff for re-review

@joostjager
joostjager force-pushed the postgres-leases branch 2 times, most recently from 49188c8 to 9bb4ab9 Compare October 2, 2026 07:38
@benthecarman

Copy link
Copy Markdown
Contributor

Tried benching and this is much slower:

Benchmarked this on db-bench with ~1.2ms RTT to Postgres. Writes are ~3x slower than main (2.8ms → 8.6ms), because each write is now 6 round trips instead of 2. Folding the lease check into the mutation as a single statement brings it back to ~3.0ms with the same fencing:

WITH lease AS (SELECT 1 FROM <lease> WHERE id = 1 AND owner_id = $1 AND expires_at > clock_timestamp() FOR KEY SHARE),
mutation AS (INSERT ... SELECT ... WHERE EXISTS (SELECT 1 FROM lease) ON CONFLICT ...)
SELECT EXISTS (SELECT 1 FROM lease)

Panic on false. Happy to share the diff.

@joostjager

joostjager commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Good find! I've made the move to the combined statements. That also made the prep commit with the mutation helper extraction redundant. There's still some potential for reuse, but passing query fragments into the helper got a bit messy, so I chose to duplicate a few lines for readability.

Rewritten stack and squashed fixups. Diff for re-review.


impl Drop for PostgresStore {
fn drop(&mut self) {
if let Some(sender) = self.lease_shutdown_sender.take() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI:

• [P2] Outstanding mutations still lose their lease on store drop — /home/tnull/workspace/ldk-node-review-pr-1111-2026-10-05-r2/src/io/postgres_store/mod.rs:332. write and remove return futures that can outlive the store, but Drop releases their lease immediately. Creating a write future,
dropping the store, then awaiting it therefore panics. Keep lease ownership alive through outstanding operations and add coverage for this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny that I raised this same question in #1126, and now it's back on my own plate. I've added a fixup commit that keeps the lease alive through outstanding mutations.

I do wonder whether continuing operations after dropping the store is really the semantics we want. The worker-thread juggling needed to support that isn't particularly clear, so I'll leave it to you which way we go. Alternatively, we could make all stores consistent in allowing outstanding operations to be cancelled on drop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder whether continuing operations after dropping the store is really the semantics we want. The worker-thread juggling needed to support that isn't particularly clear, so I'll leave it to you which way we go. Alternatively, we could make all stores consistent in allowing outstanding operations to be cancelled on drop.

Yeah, I agree that might be worth revisiting, esp. to avoid strange side-effects from zombie tasks after shutdown. However, if we do that we at the very least would need to add a stop/flush method to all stores to be able to wait for persistence to finish in a clearly defined manner.

I think generally an fsync: bool parameter in the KVStore::write method could be a big performance win, coupled with a KVStore::flush method that allows callers finer-grained control over when we block on the persistence to finish and when to more lazily continue? Anyways, that brings us back to the KVStore refactor/one-shot persist topic, ofc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what do you want to do for this PR? Keep the fix-up or drop it again? Either option is safe, because lease enforcement is in the queries themselves.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you said squash. I assume that means keep the fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'd err on the side of consistency for now, to then make a consistent change across the board if we decide to go that way.

@joostjager
joostjager requested a review from tnull October 6, 2026 07:17

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash

@joostjager
joostjager requested review from tnull and removed request for tnull October 6, 2026 09:33
Replace the temporary session advisory lock with an expiring lease in a
companion table. Acquire the lease atomically with schema setup before
loading persisted state.

Fence each write and delete with a single statement that validates lease
ownership and expiry before applying the mutation. Use FOR KEY SHARE and
a unique owner_id so mutations and background renewal can run
concurrently while takeover waits for the statement to commit. Retry
only preparation to avoid replaying a mutation whose commit outcome is
unknown.

Renew leases only in the background and skip missed ticks. Keep renewal
and the store runtime alive through outstanding mutation futures and
their spawned tasks. Release by owner ID after the final shared
resources reference is dropped, with bounded shutdown that allows Tokio
workers to continue driving lease release. Panic on lease rejection and
renewal failure or timeout, and document the application panic-abort
requirement.

Cover setup rollback, renewal and takeover lock compatibility, stale
writes and deletes, missing-key removal after expiry, renewal timeouts,
shutdown with a blocked pool, and mutation futures outliving the store.
Verify immediate lease acquisition after those mutations finish and
clean up both KV and lease tables after tests.
@joostjager

Copy link
Copy Markdown
Contributor Author

Squashed

@joostjager
joostjager requested a review from tnull October 6, 2026 09:42

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Holding off until @benthecarman is happy too.

@benthecarman benthecarman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, only thing that we might want to change is giving this a new error type different than the current kv store error.

@tnull

tnull commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

This looks good to me, only thing that we might want to change is giving this a new error type different than the current kv store error.

Good idea, let's do it in a follow-up (cc @joostjager, wdyt?)

@tnull
tnull merged commit 904ef71 into lightningdevkit:main Oct 6, 2026
17 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants