New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUG] ObservableCache Connect subscription omits concurrent updates #376
Comments
Like the high amount of research you did submitting this bug it definitely helps We will get back to you in a couple days. |
Thanks Glenn. On reflection, my proposed solution to "Bug 2" is still racy. (A writer checks A revised solution is to pass a delegate |
What you say makes perfect sense and would resolve a whole manner of ills which have crept into the lib. Several years ago I seriously stressed the lib and found it to be perfectly thread safe but over the years there have been tweaks which on the face of are sound but somewhere along the line things changed, and there have been quite a few reports of threading issues which I am sure will be fixed with your suggestion. Additionally while on the subject of the connect method there is an issue #359 where Are you willing to do a PR, also for |
Hi Roland. Thanks for creating a very useful library. I hope to have a PR ready shortly. (Disclaimer - sorry - this is not a commitment.) A few observations:
First, the observable returned yields the initial count current at the time when Second, write methods in
This occurs as follows: assume that change collection is in force; writer thread calls
I think actually the solution to these problems in both classes (which I will present in my PR) is simple: eliminate the Noted also your point about #359. I intend that my change will fix that too. |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Describe the bug
There are two related bugs in
ObservableCache<TObject, TKey>
which cause the changeset sequence emitted byConnect(Func<TObject, bool>)
sometimes to miss updates between the initial changeset and the update changesets which follow.Bug 1:
_changes
escapes lock scope inConnect(Func<TObject, bool>)
https://github.com/reactiveui/DynamicData/blob/1e61faa7e1db28e4ce1004f99c7d26cc59050a34/src/DynamicData/Cache/ObservableCache.cs#L196-L208
First, note that this method uses Observable.Defer, not Observable.Create. Observable.Defer returns an observable that when subscribed to:
In ObservableCache, access to the Subject field
_changes
is (apparently) guarded by_locker
. But the code above violates this constraint: within the callback's lock scope (executed in step 1) we simply read the_changes
field and return this reference within a Concat observable. It is not until sometime after that the calling code subscribes to it (in or after step 2), at which point the lock is no longer held. There is therefore a window where any updates to the cache occurring in another thread will appear neither in the initial changeset nor in any subsequent one, i.e. be lost.An alternative implementation which does not exhibit this problem is as follows:
Here, the initial changeset construction and the subscription to
_changes
occur together atomically within the scope of the lock.Note that this also fixes a relatively minor bug (call it 1b) in the original where if a predicate is given, the already-filtered initial changeset is redundantly filtered a second time.
Bug 2: UpdateFromSource/UpdateFromIntermediate access to
_changes
without lockhttps://github.com/reactiveui/DynamicData/blob/1e61faa7e1db28e4ce1004f99c7d26cc59050a34/src/DynamicData/Cache/ObservableCache.cs#L91
https://github.com/reactiveui/DynamicData/blob/1e61faa7e1db28e4ce1004f99c7d26cc59050a34/src/DynamicData/Cache/ObservableCache.cs#L122
In these excerpts from UpdateFromIntermediate and UpdateFromSource respectively
_readerWriter.Write
's bool parametercollectChanges
is determined by the value of_changes.HasObservers
. If true,Write
returns a non-empty changeset which is propagated to change observers (after correctly acquiring the_locker
lock).HasObservers
internally uses a volatile read to retrieve the very latest state.This mechanism appears to exist to avoid the materialisation of changesets redundantly when no-one is interested.
However, a concurrent caller to
Connect
is interested in any changes that occur after the point of generating the initial changeset, including any that occur before the subscription to_changes
has completed (which will cause HasObservers to become true). There is therefore a window during whichcollectChanges
is erroneously determined to be false, resulting in lost changesets.A simple solution with minimal downside (i.e. no new locking) would be to introduce a volatile flag field:
and to take it into account when determining
collectChanges
:Then it just needs to be set and unset within the
Connect
callback. Thus:Steps To Reproduce
Missing updates can be observed by subscribing to a cache that is undergoing frequent concurrent updates from another thread.
I think it likely that #319 and #333 are manifestations of this problem.
Expected behavior
The changeset sequence emitted by the observable returned by Connect should form a contiguous sequence, with no "gaps".
Environment
The text was updated successfully, but these errors were encountered: