Enforce single-owner contract on InMemoryPersister#1534
Conversation
Coverage Report for CI Build 26154765024Coverage decreased (-0.01%) to 85.284%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
| @@ -838,40 +835,33 @@ where | |||
| type SessionEvent = V; | |||
|
|
|||
| fn save_event(&self, event: Self::SessionEvent) -> Result<(), Self::InternalStorageError> { | |||
There was a problem hiding this comment.
| fn save_event(&self, event: Self::SessionEvent) -> Result<(), Self::InternalStorageError> { | |
| fn save_event(&mut self, event: Self::SessionEvent) -> Result<(), Self::InternalStorageError> { |
maybe it makes sense to change the trait definition... @arminsabouri ? thoughts?
There was a problem hiding this comment.
Tested fn save(&mut self, …) on JsonReceiverSessionPersister with clankers. It fails to compile:
E0596: cannot borrow data in an Arc as mutable, both in the uniffi::export(with_foreign) macro
expansion and CallbackPersisterAdapter::save_event. Arc<dyn Trait> only implements
Deref, not DerefMut — not uniffi-specific.
Workarounds (split native vs FFI traits , or Arc<Mutex<dyn Trait>> callbacks) just
relocate the lock rather than remove it. Curious what @arminsabouri thinks — for this
PR I'd like to land the footgun fix and treat trait shape as its own discussion.
There was a problem hiding this comment.
IIRC we decided to go with &self bc it simplified stuff at the FFI level. Otherwise you would have to wrap the callbacks with a mutex.
struct CallbackPersisterAdapter {
callback_persister: Arc<dyn JsonReceiverSessionPersister>,
}
Regardless I agree with Dan and we should ticket this up and revisit this.
There was a problem hiding this comment.
Do we even need to ticket it? As both of our comments suggest, this was a deliberate decision already compared to known alternatives, not tech-debt. Please ticket if I'm missing something.
caarloshenriq
left a comment
There was a problem hiding this comment.
ACK, agree with the nit pointed by @spacebear21, the missing Clone derive is self-documenting
PR payjoin#1528 review feedback flagged that `Arc<RwLock<InnerStorage>>` inside `Clone`-able `InMemoryPersister` invites concurrent-write footgun: cloning the persister and sharing write access across actors is a logic bug against the SessionPersister contract. Drop `#[derive(Clone)]` and collapse internals to `Mutex<...>`. Callers that genuinely need sharing must opt in via `Arc<InMemoryPersister<V>>` and own the locking discipline. Same change applies to `InMemoryAsyncPersister` (still cfg(test)).
235867f to
7319e22
Compare
|
Nit addressed, ready to go, tagging @chavic because of timezones and the triviality of the change. |
| @@ -809,25 +809,21 @@ pub trait AsyncSessionPersister: Send + Sync { | |||
| } | |||
|
|
|||
| /// In-memory session persister for replaying sessions and introspecting events. | |||
There was a problem hiding this comment.
cACk. I haven't found any functional issues; I was gonna recommend clearer documentation on intent, but I've seen the previous thread now already touched on this
There was a problem hiding this comment.
Do you feel it's still not clear for follow up? I addressed prior thread to remove the single-owner part. What would you like to see?
Follow-up to #1528 addressing review feedback from @nothingmuch in
#1528 (comment).
more net deletes 🤗
Summary
The previous
InMemoryPersister(andInMemoryAsyncPersister) wasCloneand usedArc<RwLock<InnerStorage<V>>>internally. That shape invites the concurrent-write footgun nothingmuch flagged: a caller can clone the persister and share write access across actors, which is a logic bug against theSessionPersistercontract — sessions are conceptually single-actor.This PR makes the single-owner intent type-level enforced:
#[derive(Clone)]from bothInMemoryPersisterandInMemoryAsyncPersister. Callers that need shared access wrap inArc<InMemoryPersister<V>>.Arc<RwLock<InnerStorage<V>>>→Mutex<InnerStorage<V>>(sync usesstd::sync::Mutex, async usestokio::sync::Mutex). The innerArconly existed to makeClonecheap; withoutCloneit's dead weight.RwLocksemantics never mattered here — both reads and writes serialize through the lock.InnerStorage<V>::eventsfromArc<Vec<V>>to plainVec<V>. TheArc::make_mut+try_unwrappattern always fell through to deep clone in practice (≥2 Arc holders at load time), so the indirection added zero value.In-scope side effects (mechanical, no behavior change):
inner.read()/inner.write()→inner.lock()at everypub(crate)access site acrosspayjoin/src/core/receive/v2/{mod,session}.rs,payjoin/src/core/send/v2/session.rs, and persist test helpers.+ Clonetrait bound on thedo_v2_to_v2<R, S>test helper inintegration.rs— the function body only takes&persister, never clones.No callers in-tree clone an
InMemoryPersister(the only.clone()s inpayjoin-ffiare onArc<dyn JsonReceiverSessionPersister>— unrelated). Compatible with #1533, which already wrapsInMemoryPersister<String>in newtypeArc<Self>(single producer, sharing viaArc<Newtype>notArc<InMemoryPersister>) — same architectural intent.Disclosure: co-authored by claude-opus-4-7-1m