Skip to content
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

[mqtt] Avoid parallel streams with common thread pool to avoid deadlocks #13621

Merged
merged 3 commits into from
Dec 6, 2022

Conversation

ssalonen
Copy link
Contributor

@ssalonen ssalonen commented Oct 30, 2022

To mitigate issue #13657 (common thread pool exhaustion when combining parallel streams with synchronization or locks)

All the parallel streams are anyways hitting resource contention / bottleneck since they all leads to MqttBrokerConnection subscribe or unsubscribe which is having synchronized block with subscribers of that broker connection. [*]

On the surface things looks like they are asynchronous (based on CompletableFuture an all) but only the lowest level operations seem to be asynchronous, and in mid-levels we have this synchronization and locking behavour currently.

I would argue typical users have only single MqttBrokerConnection, raising the question whether the parallel streams bring any true benefit really.

[*] It looks like we might be able to remove the whole synchronized block there, using ConcurrentHashMap.compute. But even then the synchronization would happen, just hidden from plain sight, inside java runtime. From official javadocs: Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this Map.... Not worth the trouble, the code would be much more unreadable that way

Fixes #13657

Signed-off-by: Sami Salonen [email protected]

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
Comment on lines 108 to 117
return availabilityStates.values().parallelStream().map(cChannel -> cChannel.start(connection, scheduler, 0))
.collect(FutureCollector.allOf());
@NonNull
Collector<@NonNull CompletableFuture<@Nullable Void>, @NonNull Set<@NonNull CompletableFuture<@Nullable Void>>, @NonNull CompletableFuture<@Nullable Void>> allOfCollector = FutureCollector
.allOf();
return availabilityStates.values().stream().map(cChannel -> {
final @NonNull CompletableFuture<@Nullable Void> fut;
fut = cChannel == null ? CompletableFuture.completedFuture(null) : cChannel.start(connection, scheduler, 0);
return fut;
}).collect(allOfCollector);
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

cChannel.start() i.e. ChannelState.start calls MqttBrokerConnection.subscribe which is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Other changes are to remove null typing warnings

Comment on lines 152 to 157
this.topics.parallelStream().map(t -> connection.subscribe(t, this)).collect(FutureCollector.allOf())
this.topics.stream().map(t -> connection.subscribe(t, this)).collect(allOfCollector)
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

MqttBrokerConnection.subscribe is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Comment on lines -164 to +169
this.topics.parallelStream().forEach(t -> connection.unsubscribe(t, this));
this.topics.stream().forEach(t -> connection.unsubscribe(t, this));
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

MqttBrokerConnection.unsubscribe is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Comment on lines -183 to +188
this.topics.parallelStream().forEach(t -> connection.unsubscribe(t, this));
this.topics.stream().forEach(t -> connection.unsubscribe(t, this));
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

MqttBrokerConnection.unsubscribe is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Comment on lines 123 to 130
return channels.values().parallelStream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
.collect(FutureCollector.allOf());
@NonNull
Collector<@NonNull CompletableFuture<@Nullable Void>, @NonNull Set<@NonNull CompletableFuture<@Nullable Void>>, @NonNull CompletableFuture<@Nullable Void>> allOfCollector = FutureCollector
.allOf();
return channels.values().stream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
.collect(allOfCollector);
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

cChannel.start() i.e. ComponentChannel.start calls ChannelState.start which calls MqttBrokerConnection.subscribe which is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Other changes are to remove null typing warnings

Comment on lines 134 to 143
return channels.values().parallelStream().map(ComponentChannel::stop).collect(FutureCollector.allOf());
@NonNull
Collector<@NonNull CompletableFuture<@Nullable Void>, @NonNull Set<@NonNull CompletableFuture<@Nullable Void>>, @NonNull CompletableFuture<@Nullable Void>> allOfCollector = FutureCollector
.allOf();
return channels.values().stream().map(ComponentChannel::stop).collect(allOfCollector);
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

ComponentChannel.stop i.e. ComponentChannel.start calls ChannelState.stop which calls MqttBrokerConnection.unsubscribe which is internally synchronizing on all subscribers of the connection.

Making this non-parallel stream should reduce resource contention anyways.

Other changes are to remove null typing warnings

Comment on lines -198 to +200
haComponents.values().parallelStream().map(e -> e.start(connection, scheduler, attributeReceiveTimeout))
haComponents.values().stream().map(e -> e.start(connection, scheduler, attributeReceiveTimeout))
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

Eventually the streams synchronizes in MqttBrokerConnection.subscribe (see above comment with AbstractComponent leading to resource contention.

Making this non-parallel should reduce resource contention.

Comment on lines -216 to +221
haComponents.values().parallelStream().map(AbstractComponent::stop) //
haComponents.values().stream().map(AbstractComponent::stop) //
Copy link
Contributor Author

@ssalonen ssalonen Oct 30, 2022

Choose a reason for hiding this comment

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

Eventually the streams synchronizes in MqttBrokerConnection.unsubscribe (see above comment with AbstractComponent leading to resource contention.

Making this non-parallel should reduce resource contention.

@ssalonen ssalonen requested a review from J-N-K November 1, 2022 19:25
@wborn wborn added the bug An unexpected problem or unintended behavior of an add-on label Nov 5, 2022
@lolodomo
Copy link
Contributor

This looks like an important fix.
@wborn @cweitkamp @kaikreuzer @fwolter @jlaur : maybe one of you is more confortable with these changes than me ?

@ssalonen
Copy link
Contributor Author

ssalonen commented Dec 3, 2022

Any chance getting this to official release?

I have been running this version (as prebuilt here) for several weeks without adverse effects

@antroids
Copy link
Contributor

antroids commented Dec 5, 2022

I agree that calling and getting results from asynchronous methods in parallel streams is not a best idea and could be replaced by sequential version.

Copy link
Member

@wborn wborn left a comment

Choose a reason for hiding this comment

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

Thanks! Let's deal with the annotations another time. 🙂

@wborn wborn merged commit 923c0f1 into openhab:main Dec 6, 2022
@wborn wborn added this to the 3.4 milestone Dec 6, 2022
morph166955 pushed a commit to morph166955/openhab-addons that referenced this pull request Dec 18, 2022
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
Signed-off-by: Ben Rosenblum <[email protected]>
andrasU pushed a commit to andrasU/openhab-addons that referenced this pull request Dec 24, 2022
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
Signed-off-by: Andras Uhrin <[email protected]>
borazslo pushed a commit to borazslo/openhab-mideaac-addon that referenced this pull request Jan 8, 2023
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
psmedley pushed a commit to psmedley/openhab-addons that referenced this pull request Feb 23, 2023
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
nemerdaud pushed a commit to nemerdaud/openhab-addons that referenced this pull request Feb 28, 2023
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
andrasU pushed a commit to andrasU/openhab-addons that referenced this pull request Jan 6, 2024
…cks (openhab#13621)

To mitigate issue https://github.com/openhab/openhab-core/issues/3125
(common thread pool exhaustion when combining parallel streams with
synchronization or locks)

Signed-off-by: Sami Salonen <[email protected]>
Signed-off-by: Andras Uhrin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug An unexpected problem or unintended behavior of an add-on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[mqtt] Frequent deadlock, likely caused by parallel streams
4 participants