Skip to content

Avoid using iterator to access ArrayBuffer that can be modified during iteration#356

Merged
lbulej merged 4 commits intomasterfrom
issue/354
Apr 4, 2022
Merged

Avoid using iterator to access ArrayBuffer that can be modified during iteration#356
lbulej merged 4 commits intomasterfrom
issue/354

Conversation

@lbulej
Copy link
Member

@lbulej lbulej commented Apr 3, 2022

The switch to Scala 2.13 apparently unearthed an issue that has been present in finagle-chirper all the time. The code changes in finagle-chirper between Renaissance 0.13 and Renaissance 0.14 were only meant to "port" it to Scala 2.13 and required changing the signature of various methods that compute feed stats to accept a SeqView instead of Seq (because in Scala 2.13, a non-strict SeqView is no longer a subtype of a strict Seq), but even in Scala 2.12, the view method of an ArrayBuffer returned a SeqView.

The main cause appears to be the fact that the analyze() method accesses an ArrayBuffer (representing a per-user feed) without a lock. The feed is appended to under a lock in the Master, but no lock is held when calculating stats on individual feeds (in a Future). When processing a request for feed stats, the lock is held while creating a read only snapshot of the trie that holds all user feeds and a view is created for each feed (which essentially snapshots the number of items in the feed), but the view is processed later.

The analyze() function running as part of the future evaluation uses an iterator to go over chirps in a feed, but the underlying ArrayBuffer can be modified (appended to) in the meantime. In Scala 2.13 this results in the ConcurrentModificationException. I tested the same code in Scala 2.12 (with the same libraries) and I was not able to reproduce the problem.

Now, I believe that the race is intentional and should be benign (similar to updating profiling counters in hotspot), so to keep it the way it was, I just changed the analyze() method to avoid using an iterator and instead iterate over a Range and then access the ArrayBuffer elements by index. This should be "safe" even if the ArrayBuffer is expanded (and the backing array changed) because the ArrayBuffer is only appended to. Using a direct index should either fetch the element from an old array or the new array, but the returned item should be the same — assuming the implementation updates the reference to the backing array after it has copied the contents of the old array to the new array.

  • Strictly speaking, I'm not sure whether even this would hold under the memory model, but I understand the desire to avoid locking for these computations — the whole point of the stats is to provide some reasonable numbers at some point in time and if some updates come in between, they will be included in the next stats call.

Fixes #354 (as long as keeping the race can be considered a fix)

lbulej added 4 commits April 2, 2022 15:42
The automatic main class discovery in renaissance-core has been very
unreliable lately, so we set it explicitly to avoid producing a
bundle without Main-Class attribute in the manifest.
This communicates the need for efficient indexing into a SeqView
in the functions computing feed stats.
The underlying buffer can be modified during stats computation and
trigger ConcurrentModificationException. This uses a Range iterator
(which will not change) and accesses the buffer elements using an
explicit index. While still racy by design (they are just stats), any
races should be benign.
Copy link
Collaborator

@farquet farquet left a comment

Choose a reason for hiding this comment

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

Very nice analysis!
I agree that it makes sense not to add locking for a stats feature. Your fix is probably how one would want to solve this in an actual production workload.
I've tested your branch locally and numbers are comparable with renaissance 0.14 as expected.

@lbulej lbulej merged commit 214e83f into master Apr 4, 2022
@lbulej lbulej deleted the issue/354 branch April 4, 2022 18:05
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.

ConcurrentModificationException on finagle-chirper

2 participants