[shelly] Improve stability through better thread safety - #20396
Conversation
|
@Nadahar I think that's all for ShellyHttpClient. |
That's entirely up to you. I think it's fine to do more than one class. |
|
I don't think you can safely do |
|
Now I found where they are called, from |
There was a problem hiding this comment.
Pull request overview
This PR updates the Shelly binding’s ShellyHttpClient to reduce concurrency issues by adding synchronization around configuration updates and reads, and by adjusting several state fields to be volatile.
Changes:
- Synchronize
setConfig(...)and snapshot selectedconfigfields under a lock for request execution. - Change several mutable fields (
thingName, timeout counters,basicAuth) tovolatile. - Use local snapshots (e.g.,
deviceIp,userId,password) to avoid unsynchronized reads ofconfigfields during HTTP calls.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
changes applied |
|
@Nadahar I also added Shelly1HttpClient, please verify. |
|
The "stats counters" looks good. I've started to look at the rest, and I only came to So, either everything that includes any fields from Synchronizing all use of |
|
@markus7017 I pushed a commit here with a proposal to make It's rather messy. The reason is that you use the configuration class for "other purposes". If you only used it for what it's for, there would be no need for synchronization, since In my opinion, these two things should be split, with one class just representing the configuration, and one with the "operational state", where you set the things you set in the configuration itself now. I think this would simplify things - but it should work this way as well. |
+1 for the seperation. |
f890200 to
7c23fdb
Compare
|
I assumed that nobody had checked this out yet, so I amended the last commit. All I did was move the comments in |
|
I took the work of commenting on each config parameter which is used where (I don't mean that the code should be merged with these comments, they are only an aid to disentangle this): // All access must be guarded by "this"
/** IP address of the device */
private String deviceIp = ""; // relay, roller, dimmer, light, rgbw2, battery, basic, relay-gen2, roller-gen2, rgbw-gen2, battery-gen2, dimmer-gen2, blugw
// All access must be guarded by "this"
/** IP address or MAC address for BLU devices */
private String deviceAddress = ""; // blubattery
// All access must be guarded by "this"
/** userid for HTTP basic auth */
private String userId = ""; // relay, roller, dimmer, light, rgbw2, battery, basic
// All access must be guarded by "this"
/** password for HTTP basic auth */
private String password = ""; // relay, roller, dimmer, light, rgbw2, battery, basic, relay-gen2, roller-gen2, rgbw-gen2, battery-gen2, dimmer-gen2
// All access must be guarded by "this"
/** schedule interval for the update job */
private int updateInterval = 60; // relay, roller, dimmer, light, rgbw2, battery, basic, relay-gen2, roller-gen2, rgbw-gen2, battery-gen2, dimmer-gen2
// All access must be guarded by "this"
/** threshold for battery value */
private int lowBattery = 15; // battery, battery-gen2, blubattery
// All access must be guarded by "this"
/** {@code true}: turn on device if brightness > 0 is set */
private boolean brightnessAutoOn = true; // dimmer, light, rgbw2, dimmer-gen2
// All access must be guarded by "this"
/** Roller position favorite when control channel receives ON, 0=none */
private int favoriteUP = 0; // roller, roller-gen2
// All access must be guarded by "this"
/** Roller position favorite when control channel receives ON, 0=none */
private int favoriteDOWN = 0; // roller, roller-gen2
// All access must be guarded by "this"
/** {@code true}: register for Relay btn_xxx events */
private boolean eventsButton = false; // relay, dimmer
// All access must be guarded by "this"
/** {@code true}: register for device out_xxx events */
private boolean eventsSwitch = true; // relay, dimmer, light
// All access must be guarded by "this"
/** {@code true}: register for short/long push events */
private boolean eventsPush = true; // relay, dimmer
// All access must be guarded by "this"
/** {@code true}: register for short/long push events */
private boolean eventsRoller = true; // roller
// All access must be guarded by "this"
/** {@code true}: register for sensor events */
private boolean eventsSensorReport = true; // battery, basic
// All access must be guarded by "this"
/** {@code true}: use CoIoT events (based on COAP) */
private boolean eventsCoIoT = false; // relay, roller, dimmer, light, rgbw2, battery, basic
// All access must be guarded by "this"
/** local IP addresses used to create callback URL */
private String localIp = ""; //
// All access must be guarded by "this"
private String localPort = "8080"; //
// All access must be guarded by "this"
private String realm = ""; //
// All access must be guarded by "this"
private Boolean enableBluGateway = false; // relay-gen2, roller-gen2, rgbw-gen2, dimmer-gen2, blugw
// All access must be guarded by "this"
private Boolean enableRangeExtender = true; // relay-gen2, rgbw-gen2This allows some conclusions to be drawn: |
|
From my understanding those values are initially filled
I need to check on code level, but assume that those are not changed after thing initialization. In additional a copy is passed to the ShellyHttp class, so I don't get why we need to synchronize access to each member through the code. I could make the ShellyThingConfiguration representing the thing config only and copy the fields into something. like ShellyThingDynamicConfig, which then gets passed to the other classes. |
Yes, that what I've been suggesting. RuntimeSettings/State/Config or whatever you want to call it. Then the stuff you do with e.g. the password (where you fetch the "default" password if it's blank etc.) could also only be done there, and the config itself never modified. |
|
There's one thing I need to explain: The way I approach thread-safety is methodical. I generally don't stop to try to evaluate "is this likely to happen or not", things just get way too complicated that way. So, I'm "strict" in the sense that I look at everything that "can go wrong", without trying to do an analysis of the probability, and then I deal with that. I probably end up protecting some things that "in the real world" would work fine without it, but as I see it, I save lots of time that way because the alternative is very complex analysis, and probably simulation, of what can and can't occur. Many people seem to rely on "guessing" instead, or only handing thread-safety when it's "proven" that it can fail. I reject that, since it only leads to endless subtle bugs, because timing sensitive bugs can be very hard to "prove", and guessing is just that - guessing. |
If something is only modified during initialization, I typically make the class immutable (final fields). That way, there's no need for synchronization. When "building" the information, one can either do it using local variables that are figured out before the immutable instance is created, or I make a "builder" class so that you don't need to juggle all the local variables. When the builder builds, it then creates an immutable instance that can safely be shared. |
7c23fdb to
22675f7
Compare
|
@Nadahar I separated the persistent thing config from those values used in addition (ShellyThingBasicConfig vs. ShellyThingConfiguration) and implemented thread-safety by synchronized getter/setter methods. I also move initialization of several config settings centrally to the ShellyThingConfiguration constructor. |
22675f7 to
6f40a7e
Compare
|
@Nadahar Various files are initialized in the constructor, but not being modified later (those having a getter, but not a setter method). From my understanding I could omit synchronized for them and keep it only for those exposing a setter method, correct? |
736865b to
f9f7ea7
Compare
Yes.
I already commented on it - sometimes I think you're a bit too quick when reading 😉 |
Signed-off-by: Markus Michels <markus7017@gmail.com>
|
Additional test cases are pushed |
|
@Nadahar Could you please click "Resolve conversation" on those topics you are fine with. From my side there is nothing more, which should be included in this PR. |
Done. You still seem to have skipped some comments, at least you haven't responded or done anything. |
Signed-off-by: Markus Michels <markus7017@gmail.com>
I miss the change from INFO to DEBUG, now changed |
Have a reply yes, but not to the last things I've said. It's about the TODO that should be removed, that I've now finally made a suggestion for removing, and it's still the comment in |
Signed-off-by: Markus Michels <markus7017@gmail.com>
|
TODO: getDeviceProfile(), I'm aware of this, but will not include it in this PR (has nothing to do with config topics) TODO 3: logic on BD address in initializeThingConfig() looks good. I gave an answer on HandlerFactory. I removed modified() completely and things are re-initialized on on changes to the binding config. For now this is fine with me. |
I'm not sure what you're referring to, the TODO we removed now was the last one I knew off. The point with making them isn't that they should become a part of the merged code, but that they must be somehow "resolved" before we do. Since the logic seems to work well, I wanted the TODO removed before we merge.
You didn't answer my reply. You didn't specify "how it worked", just that "it works without". I don't know what that means - does it mean that the config is updated, or also that the Thing handlers are reinitialized? That wasn't clear to me, but I understand your reply here as if the latter is the case. That would have been enough for me to "resolve" the comment, except that I didn't understand your comment about "native threads" - which I would also like to have clarified, independently of this PR. |
Nope, the other 2 are still in the code
Yes, both works as mentioned dozend comments above. This was already remove before the "partial revert" and I just re-applied. I tested with
I was referring to this code, which create a new thread rather than using schedule(). Nevertheless, it's removed. |
Yes, but I don't think they are from this PR.
Yes, that's what I thought too. But this would only happen occasionally, and the threads would only run briefly (there is no loop keeping them alive). The treads are just as "native" as those you get from Also, this wasn't the code that was previously removed. That was your code, I wrote this one without ever looking at what you had there. I doubt it was the same. This one was "smart", it would only trigger the reinit if one of the relevant configuration parameters had changed, not for any change to the configuration. I made But, if the I can't find the code in @Override
public ThingHandler registerHandler(Thing thing) {
ThingHandler thingHandler = createHandler(thing);
if (thingHandler == null) {
throw new IllegalStateException(this.getClass().getSimpleName()
+ " could not create a handler for the thing '" + thing.getUID() + "'.");
}
if ((thing instanceof Bridge) && !(thingHandler instanceof BridgeHandler)) {
throw new IllegalStateException(
"Created handler of bridge '" + thing.getUID() + "' must implement the BridgeHandler interface.");
}
registerConfigStatusProvider(thing, thingHandler);
registerFirmwareUpdateHandler(thing, thingHandler);
registerServices(thing, thingHandler);
return thingHandler;
}It doesn't store the |
|
@lsiepel As far as I'm concerned, this is ready. I haven't done a full review with all the latest changes, but I've reviewed the details of the changes, so hopefully I haven't missed anything. The issue with handler reinit can be handled outside this PR. Maybe give Copilot a final round? Otherwise, this LGTM. |
lsiepel
left a comment
There was a problem hiding this comment.
Thanks, LGTM
A PR i will remember for some time. Reviews, tests all look good, there seems to be some areas that can be further improved. Hopefully we can do that in smaller chunks.
Anyway, special thanks to @markus7017 and @Nadahar.
It's not my first rodeo, I'm not so sure that this makes it that high on the list of what I've been through 😉 That said, not everything can be done in smaller chunks, which is exactly those things that tend to never be done - because they can't be done in small chunks. But, I agree that I've had enough of this PR now... |
…/api configuration management (openhab#20396) * Improve thread safety for class ShellyHttpClient Signed-off-by: Markus Michels <markus7017@gmail.com> Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
|
This created memories :-) A big thank you, this provides a big improvement. |
…/api configuration management (openhab#20396) * Improve thread safety for class ShellyHttpClient Signed-off-by: Markus Michels <markus7017@gmail.com> Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
…/api configuration management (openhab#20396) * Improve thread safety for class ShellyHttpClient Signed-off-by: Markus Michels <markus7017@gmail.com> Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
…/api configuration management (openhab#20396) * Improve thread safety for class ShellyHttpClient Signed-off-by: Markus Michels <markus7017@gmail.com> Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com> Signed-off-by: olemr <olemr@olemr.com>
…/api configuration management (openhab#20396) * Improve thread safety for class ShellyHttpClient Signed-off-by: Markus Michels <markus7017@gmail.com> Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
The PR implements synchronization for improve thread safety of the class.