Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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
finagle-base-http: add MapBackedHeaderMap #805
finagle-base-http: add MapBackedHeaderMap #805
Changes from 7 commits
7c009f6
00280d9
2f1d9d3
801ac60
69864fd
c141122
90efed3
f1d0fc1
de48cd9
File filter
Filter by extension
Conversations
Jump to
There are no files selected for viewing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was talking to @bryce-anderson and we think this might open us to ConcurrentModificationException issues, since we don't synchronize while iterating, and it iterates over the underlying map directly. it seems like we might have a few options here:
The risk of synchronizing foreach is that we're going to lock on someone else's lambda, but it might be worth just doing it for now, since it will still let us use your optimization at very little cost in the common case (single-threaded access). On the other hand, it would be pretty cool to use a ConcurrentSkipListMap 😈. how do you feel about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I wish I never removed the abstraction to swap out the underlying Map implementation.
But let's take a step back. How often do people really concurrently iterate over headers and also mutate the header map and don't synchronize that access because they don't care whether or not the modified data should or shouldn't be part of the iteration? Is that a use-case you want to facilitate in the first place?
Thinking about those questions, my conclusion is that throwing a ConcurrentModificationException that makes the user aware of the situation/race condition/potential bug is better than synchronizing and not being deterministic in what happens first. If the user really wants that for some unforeseen, unspoken and unspeakable reason, they can always still externally synchronize (maybe with a ReadWriteLock or something).
I think in terms of common usage patterns, that's the fastest and most reasonable thing to do too. Even if a ConcurrentSkipListMap would be pretty cool.
If we do want to get overboard and fancy, it should be validated with benchmarks that reflect real-world usage patterns, including the real-world concurrent patterns, because otherwise we're not really measuring anything close to reality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the good outcome is that they get a ConcurrentModificationException–a bad outcome might be an infinite loop or worse. See https://ivoanjo.me/blog/2018/07/21/writing-to-a-java-treemap-concurrently-can-lead-to-an-infinite-loop-during-reads/
I think correctness needs to be our #1 concern. I agree with you that it's extremely unlikely–that's one of the reasons I'm willing to just wrap it in a synchronized block and call it a day.
I'm not sure I understand your concern about not being deterministic about what happens first–that's always going to be the case with concurrent use of a data structure. My concern is that HeaderMap used to be threadsafe, so people may have been already using it assuming it was threadsafe, and then one day we merge this in and their application breaks in an extremely subtle way. If this was a new abstraction, I would be open to considering, "HeaderMap is not threadsafe, all concurrent access must be synchronized by the user" but I don't think that's the right way forward given the circumstances.
My bet is the same as yours, that it's extremely unusual to access it concurrently, so I don't think we need to benchmark it. But I do think that we should protect against it. Locking is pretty cheap when there's no concurrent access, so I don't think it should be that painful to just lock it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jikes! I didn't realize the concurrent case could lead to non-termination. That makes not synchronizing a non-starter, and the below just an aside for academic curiosity.
The concern about concurrent iteration and modification (specifically those two) is that the concept of iteration of all headers and concurrently modifying those headers makes no sense -- either you want those in or out, but you always care, or you wouldn't iteratate them. You must synchronise externally for the operation to be meaningful and correct.
That's not always the case for two concurrent modifications that will never modify the same key, or concurrently getting a key, and modifying some keys that you know to be different to the keys you're concurrently getting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've run into this non-termination before... It was a nightmare, especially considering it was happening on a Netty thread.
Normally I'd agree that we'd prefer a
ConcurrentModificationException
and we've tried that in the past but many users are doing the wrong thing in benign places (think reusing a request to do warmup requests or something) and it just wasn't worth the hassle to introduce the behavior change.I think the solution you've already committed of simply synchronizing the
foreach
call is fine since you're right: this shouldn't be happening very often, if at all. 🤞There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe add a note that since it's not synchronized, it must be synchronized by the calling method? I think it might be safe to always synchronize here, since java synchronized does reentrancy pretty cheaply (I think)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the note -- I also think that re-entrance is pretty cheap, but I wasn't entirely sure, and it's called only a handful of times, and only
private[this]
I'm happy to change it if you see those trade-offs differently.