Mint: Add Acquired<T> type for compile-time row lock enforcement #1446
No reviewers
Labels
No labels
DB & Storage
Deployment
Error Handling & Logging
Maintenance
Payment Backend
backport
backport v0.13.x
backport v0.14.x
backport v0.15.x
bindings
blocked
bug
cdk-sql
ci
cli
deps
documentation
duplicate
enhancement
good first issue
help wanted
invalid
keep-open
ldk-node-ui
migrations
mint
mutation-testing
needs rebase
needs review
new nut
nut change
question
ready
rust-version
rustfmt
stacked hold
stale
testing
wallet
weekly-report
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cashubtc/cdk!1446
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/improve-database-layer-and-expectations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Description
Fixes #1404 and #1066
Add a LockedRows tracking system to enforce that resources must be read (and locked) before being modified within a transaction. This prevents race conditions and ensures data consistency.
Key changes:
This ensures the database layer can reject modifications to resources that weren't properly read first, forcing callers to always work with fresh data from the database.
Notes to the reviewers
Suggested CHANGELOG Updates
CHANGED
ADDED
REMOVED
FIXED
Checklist
just final-checkbefore committingThis seems to introduce unnecessary complexity by implementing locking at the application level when we should rely on the locking mechanisms the database already provides. I also think this introduces redundant database calls that could have a performance impact. We would be better served by having more test coverage to ensure we are using the database locking primitives correctly and that there are no race conditions, rather than implementing our own locking in addition to the database locking.
I do like the API change to pass quotes by value rather than by ID.
That is not happening in this PR. Instead, I introduce a mechanism for the database to track locked resources. This PR tracks local locked resources.
The entire code could be ignored in release builds. This code primarily serves as a tool to let developers know whenever they are modifying a potentially stale resource. That's why it forces the developer to read and lock it before updating it.
Great!
Then it should be in tests instead of adding complexity to the code base.
Why was this test removed and there a similar test elsewhere, as this is something that should be tests?
What about now, @thesimplekid ? In 3ed143c0573229c665e9f8af845ee76f6c3f0a1f, I introduced a Rust generic type for reserved-for-update records from the database.
I will create a follow-up PR if this gets accepted, which would do the same, but for proofs. That should eliminate all race conditions, since it would shift responsibility for synchronization and concurrency management to the storage layer.
I don't think we currently have any race conditions that this eliminates. It's to protect us in the future from introducing race conditions by making the api more strict.
This is already the case.
I think this api change is much better then the runtime check before. I'm traveling for the holidays so might take a few days to really review but think the overall approach is sound.
I think the increment should not happen here but in the mint and we change the db fn from increment to set amount paid.
@ -1000,3 +1060,2 @@#[instrument(skip(self))]async fn increment_mint_quote_amount_paid(async fn update_mint_quote(Same comment as amount paid
Another option would be just to remove these two fns for a more general update_mint_quote fn that can do both.
I think we test in the mint codebase, and we agreed to move this test off the database.
I don't think the change-set pattern is appropriate here. The database must be the synchronization point for duplicate detection in multi-instance deployments. In-memory validation creates:
@ -1357,2 +1239,4 @@let current_time = unix_time();quote.paid_time = Some(current_time);quote.payment_preimage = payment_proof.clone();query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?Removing this check and relying on the in-memory duplicate check in add_payment() breaks multi-instance deployments. If two instances load the same quote, both will pass the in-memory check, but one will fail here with a generic DB constraint error instead of DuplicatePaymentId. The callers in ln.rs:92 and mod.rs:761 explicitly catch DuplicatePaymentId to handle this race gracefully that no longer works.
This DuplicatePaymentId catch only works for single-instance races (caught by add_payment() in-memory check). In multi-instance deployments, both instances pass add_payment(), then one fails at update_mint_quote() with a DB constraint error which is not DuplicatePaymentId.
This has the same multi instance issue.
@ -1357,2 +1239,4 @@let current_time = unix_time();quote.paid_time = Some(current_time);quote.payment_preimage = payment_proof.clone();query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?We are not, remember the quote is read as exclusive, signaling any other instance of this intent to update.
Also, the INSERT would fail on duplicate entry.
Keep in mind this is for Acquired not MintQuote.
@ -1357,2 +1239,4 @@let current_time = unix_time();quote.paid_time = Some(current_time);quote.payment_preimage = payment_proof.clone();query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?With a different error. But yeah you're right using the for update we shouldn't hit this.
This comment also is incorrect and using for update makes it work.
@ -1357,2 +1239,4 @@let current_time = unix_time();quote.paid_time = Some(current_time);quote.payment_preimage = payment_proof.clone();query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?I would make it use the same error
Can you link it? When removing tests especially an important one we should be careful that is intentional and there is the expected test converge and we know where it is.
@ -241,6 +244,79 @@ pub async fn process_melt_change(Ok((Some(change_sigs), tx))We should log that its locked.
I think we're over using inline in general and here specifically its an async fn with multiple db calls I don't think there is a benefit to inline that.
You are right, I will add unit tests for
check_state_transitionincrates/cdk-common/src/state.rs@ -241,6 +244,79 @@ pub async fn process_melt_change(Ok((Some(change_sigs), tx))Fixed in
9f5b58eb957ca6c92cce