-
Notifications
You must be signed in to change notification settings - Fork 41.2k
Introduce HealthIndicatorRegistry #4965
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
Conversation
9c87e41
to
dbf7d54
Compare
@Override | ||
public Map<String, HealthIndicator> getAll() { | ||
return Collections.unmodifiableMap( | ||
new HashMap<String, HealthIndicator>(this.healthIndicators)); |
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.
Unfortunately, and somewhat surprisingly, this isn't thread-safe. It's doing a putAll
under the covers, the documentation for which says:
The behavior of this operation is undefined if the specified map is modified while the operation is in progress.
With the current implementation of HashMap
it should work – it iterates over the concurrent hash map's entry set and doing so is thread-safe – but that's an implementation detail that we shouldn't rely on. As a result, everything needs to be synchronized
which means there's no longer any point in using ConcurrentHashMap
.
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 see, somewhat suprisingly indeed.
So you're suggesting we should ditch the ConcurrentHashMap
and just synchronize all implemented operations on the healthIndicators Map
itself?
Wouldn't this do the job with less code:
@Override
public Map<String, HealthIndicator> getAll() {
synchronized (this.healthIndicators) {
return Collections.unmodifiableMap(
new HashMap<String, HealthIndicator>(this.healthIndicators));
}
}
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.
A single synchronized
block doesn't stop other threads from mutating the map via register
or unregister
.
dbf7d54
to
c166a8a
Compare
@wilkinsona I've updated the PR, please review the changes. |
Looks good now. Thanks. |
Any chance this makes is it into 1.4? |
+1 for 1.4 |
public void register(String name, HealthIndicator healthIndicator) { | ||
Assert.notNull(healthIndicator, "HealthIndicator must not be null"); | ||
synchronized (this.healthIndicators) { | ||
if (this.healthIndicators.get(name) != null) { |
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.
Why not Assert.state(!this.healthIndicators.containsKey(name), text)
?
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.
Using Assert.state
would require constructing exception message string on each invocation of register
operation, vs just in scenarios where IllegalStateException
indeed is thrown, which presents a certain overhead.
Other than that, IMO current code is more readable as well.
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.
Sounds reasonable. But why wouldn't you want to use containsKey
?
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.
Even though there is a check for null
values on HealthIndicator
registration, Map
API itself permits null
values so this approach IMO expresses better how we're using healthIndicators
Map
.
*/ | ||
public class DefaultHealthIndicatorRegistry implements HealthIndicatorRegistry { | ||
|
||
private final Map<String, HealthIndicator> healthIndicators = |
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.
Why not use ConcurrentHashMap?
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.
That's how it was in a previous revision, but then getAll
isn't thread-safe without using synchronized
. At that point, there's no benefit to using ConcurrentHashMap
.
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.
Since operations on healthIndicators
map require external synchronization anyway (see register
operation as the most obvious example), using a ConcurrentHashMap
would introduce an unnecessary overhead.
See comments on outdated diffs for more background.
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.
Hmm... Can't we just use putIfAbsent in register method? There is such method in ConcurrentHashMap, which is thread safe. Method getAll will be thread safe as well and will return snapshot of the map, of course, some of the updates will not be seen immediately. Is it critical?
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.
That's true, I've forgot about that one. However, there's still @wilkinsona's comment on getAll
, which was the original reason to drop ConcurrentHashMap
.
As already mentioned, refer to the older comments on this PR.
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.
Why would getAll be not thread-safe? It is absolutely legal to iterate concurrent map. :) Updates are not guaranteed to be seen while iteration, but on next iteration they will be seen -- is that an issue you are talking about?
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.
@Aloren Have you read the comments on the outdated diff from January 18? It's explained there. I'd link to it, but GitHub doesn't make it easy. You should be able to find it from here: #4965 (comment).
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.
@wilkinsona Looks like implementation in java 8 changed for HashMap. There should not be such issue now. Was not aware about that. Thanks for the link. Now it is clear.
6023907
to
5b9a69a
Compare
I'm afraid we're probably not going to have time to get his in 1.4. We'll revisit it again for 1.5. Thanks! |
f11c11c
to
289d6e5
Compare
289d6e5
to
8364464
Compare
8364464
to
2ca7acb
Compare
I've adapted this PR to the new Actuator infra. Any chance this gets a review/consideration for 2.0? |
Thanks a lot @vpavic! I am not sure we'll have time to review this one for 2.0 still. I am flagging it now to get more feedback. |
This commit introduces HealthIndicatorRegistry which handles registration of HealthIndicator instances. Registering new HealthIndicator instances is now possible in runtime.
2ca7acb
to
a161f14
Compare
I've rebased the proposed changes onto the current Regarding the reactive support:
I didn't take on this yet since |
This commit introduces HealthIndicatorRegistry which handles registration of HealthIndicator instances. Registering new HealthIndicator instances is now possible in runtime. See spring-projectsgh-4965
This commit is work in progress and polishes the initial submission by making sure that the CompositeHealthIndicator is also a registry. The current infrastructure prevents the composite to be registered as a bean as it would be an extra HealthIndicator in the context and therefore may be taken into account as a regular indicator. The registry on the other hand must be registered in the context so that users can manipulate its content. Ultimately, both those objects share common base features that we should not duplicate. This commit makes sure that the registry has a `health()` method but does not implement HealthIndicator. The composite now extends from the registry and implements HealthIndicator, simply delegating to that parent method. It's unclear at this point if we want to keep this arrangement as the duplication of factories is a bit annoying. See spring-projectsgh-4965
Alright, I've had an extensive review and a good chunk of polishing in 23585d2. Paging @wilkinsona for a review. The main problem here is that The other reason I went ahead with this approach is that we have a composite for the reactive support and we'll need a reactive registry as well. Once we figure out what the best arrangement for this is, we can easily adapt that to the reactive bits. |
This commit introduces HealthIndicatorRegistry which handles registration of HealthIndicator instances. Registering new HealthIndicator instances is now possible in runtime. See spring-projectsgh-4965
This commit is work in progress and polishes the initial submission by making sure that the CompositeHealthIndicator is also a registry. The current infrastructure prevents the composite to be registered as a bean as it would be an extra HealthIndicator in the context and therefore may be taken into account as a regular indicator. The registry on the other hand must be registered in the context so that users can manipulate its content. Ultimately, both those objects share common base features that we should not duplicate. This commit makes sure that the registry has a `health()` method but does not implement HealthIndicator. The composite now extends from the registry and implements HealthIndicator, simply delegating to that parent method. It's unclear at this point if we want to keep this arrangement as the duplication of factories is a bit annoying. See spring-projectsgh-4965
* pr/4965: Polish "Introduce HealthIndicatorRegistry" Introduce HealthIndicatorRegistry
This commit introduces HealthIndicatorRegistry which handles registration of HealthIndicator instances. Registering new HealthIndicator instances is now possible in runtime. See gh-4965
See gh-4965 Co-authored-by: Andy Wilkinson <[email protected]>
Thank you very much @vpavic and @wilkinsona - This is now merged. There is a |
Thanks for merging this @snicoll, I've finally managed to take a closer look at the subsequent changes to With the current arrangement, some scenarios that were previously simple to setup are now more complicated. For example, if one wants to use Previously, this was possible by simply creating Additionally, with |
It is as simple as
I am not sure I am following. You'd previously get the list of
Sorry I am not following neither the argument nor the actual problem that it causes. Can you please explain that in more details? |
Yes, either way to begin with there needs to be a
When some component is considered a registry, to me that sounds like a central place that manages instances of some lower level component. To have the managed component be aware of the registry is awkward and confusing, at least to me. |
I disagree with that but either way the current arrangement does not enforce it one way or the other. I read your feedback as a serious issue on your side and the only thing I can think of would be to remove the deprecation on the constructor that takes the |
Not sure what was that supposed to mean, but just to clarify - the feedback I've provided is solely about my observations on changes done vs the original state of this PR. I didn't suggest that something was broken or not usable, but rather that I found some things to be confusing, and that some use cases require more code and involve more components than they did before. To go back to
I wouldn't remove the deprecation, as there should be a clearly preferred way to construct |
I disagree with the unusual part. Having said that, I get it now. Even if we have deprecated the method that allows you to register additional indicators, you can still do so by accessing the registry. Would that help if the composite returned a unmodifiable The Thanks for bearing with me :) |
@vpavic I had a call with @wilkinsona to discuss this and we believe the current design is the best compromise at this point. Having a intermediate API that the registry implements will not fix the problem that someone may accidentally pass the registry from the context (as it would implement this contract). I have, however, remove the deprecation on the constructor that takes a As for the link between the registry and the composite, it is inevitable. Previously the composite was responsible of two things: 1. getting a list of health indicators with the ability to add more, and 2. providing a health indicator on the composite. The first responsibility has been moved to the registry but the registry is then responsible to provide the list of indicators since it's managing that state. We've also extensively discussed the reason we have a |
Thanks for looking into this.
Right, that's why I referred to it as middle ground solution when I originally brought it up. My preference has always been for Regarding |
This PR introduces
HealthIndicatorRegistry
which handles registration ofHealthIndicator
instances. Registering newHealthIndicator
instances (as well as unregistering) is now possible in runtime (see #4894).I've signed the CLA.