Listeners behavior discussion

John Hendrikx john.hendrikx at gmail.com
Sun Jul 10 13:06:41 UTC 2022


Hey, thanks for commenting, I have added my comments inline.

>  From what I understand, you are essentially suggesting to solve an
> existing bug in ExpressionHelper, where inconsistent change
> notifications can be produced when a listener modifies its
> ObservableValue.
Well, and the fact that it will prematurely release the lock when a 
nested emission finishes (and there is another bug in there which I 
didn't mention in my previous post, while the lock is held, each change 
of the listener list as part of the same emission triggers another 
unnecessary copy).
> At first, it seems like a good idea to defer a nested change
> notification until after all listeners have been invoked for the
> current change. But upon closer inspection, even with the proposed
> fix, there are more problems:
> Let's consider three listeners A, B, and C that are invoked in this
> order. If B changes the value, then C would seem to receive a correct
> change notification. However, if C tried to get() the value, it would
> not see the value that's reported in the change notification, but the
> value that B set. That's not good.

I think relying on #get when you're using a ChangeListener is a bit odd 
(since we're allowing threading, #get could report anything anyway). For 
an InvalidationListener it wouldn't matter, since it's allowed to get 2 
invalidations where nothing changed. What I think is not good is that 
the ChangeListener is receiving events that never happened, happened in 
a different order or events where the current value didn't actually 
change. This is not documented anywhere: "the old value can be 
incorrect, don't rely on it" or "the change the event reports may never 
have actually happened" or "you may receive two changes with no change 
in the current value reported".  All I would like to see is to formalize 
this somewhat, so we know what the implementation *should* be doing, 
instead of having to guess at what is a bug and what is intended.

Also, I'm like 99% sure nested changes are already happening inside 
JavaFX code itself, for example, during layout.  The more complicated 
layouts can take a few passes to "settle", and even though there may not 
be a direct change of the same property, it can also be triggered by 
changing another property which in turn changes the original property, 
all occurring during the first emission still of the original property.

I can do an experiment where I disallow this (by throwing an exception 
if the list is already locked), but I'm pretty sure that tests are going 
to fail.

> We might be tempted to just add a similar deferred update mechanism to
> the value itself, so that C can still get() the old value even after B
> already set a new value. But that would create another problem: if the
> value is only applied after all listeners have run, B could observe
> that get() == "foo", then call set("bar"), and after that be
> astonished to learn that get() == "foo" (because the value is not yet
> applied).
Agreed, I don't think this would work at all, especially not for 
invalidation listeners.
> I don't think there is a way to support nested value changes, deliver
> the same events to all listeners, and be consistent at the same time.
> Something has to give. The easiest option, quite obviously, is to
> disallow nested value changes entirely.

I think what I described still stands -- it's not an uncommon problem in 
event listening frameworks. The only argument against it at the moment 
is that a direct #get could report something that is not the same as the 
current value, which I think is not really a concern. You want to act on 
the *change*, a historical event that should reflect what actually 
happened so you can look at the old value and the new value and perhaps 
even derive something from those two (like the difference).  A 
ChangeListener might even have logic to ignore the current change if it 
notices that #get reports something different as another change will 
follow soon after (guaranteed by the framework).

> Another option may be the following algorithm:
> 1. When B changes the value, the scheduled change notification for C
> is updated to reflect the new value (but it still needs to retain the
> old value from the perspective of C).
> 2. After A, B and C have run, C is already up to date with the newest
> value. In the deferred phase, only A and B receive additional change
> notifications for the new value.

It's not a bad idea, but I think it would be even harder to implement. 
The problem here is that the listener list can also be modified during 
emission, and so it would be very hard to track which listeners were 
notified already, which weren't notified yet, etc.

It also removes some of the guarantees that a listener should receive 
all change events as they occurred (if we want to guarantee that). 
Basically, the order of listeners becomes a serious influence here, 
something that I think should be avoided. I mean, we are making an 
effort to notify all *original* listeners that were part of the list, 
but then are not concerned with giving all of them the same event 
information?

> As for your proposed specification: ExpressionHelper is an
> implementation detail and not public API. As far as I know, we don't
> specify the invocation order of listeners, and I see no compelling
> reason why we should. Listeners should not depend on their order of
> invocation with respect to other listeners. Of course, users can
> currently depend on that (because it's implemented as such), but we
> probably shouldn't invite them to write even more of such code.

I may not have been clear, I'm not proposing to make ExpressionHelper 
public.  However, since it is used in the majority of our listener 
registration and notification system, and there is no formal 
specification as to how listeners should work in the face of concurrent 
changes, nested changes and listener add/removal, the only place to find 
what the system is currently doing is in ExpressionHelper. 
Unfortunately, what ExpressionHelper is currently doing would not make 
for a very good specification that users can rely and build complicated 
logic on. Primitives which are hard to predict or are just plain 
misrepresenting events are not a solid foundation to build upon.

I know that currently we don't specific invocation order, but this is 
another thing that probably would break JavaFX if changed. It is sort of 
implied though; adding duplicate listeners is allowed, which will be 
notified twice, and the docs mention that removal would remove the first 
instance, implying there is somekind of ordering going on. The docs 
could have stated that duplicates would only be notified once and that 
removal would remove all matches, but it doesn't.

I'll see what happens when I swap out ExpressionHelper for one that 
ignores order and disallowes nested changes (one at a time) :)

> Similarly, I see no compelling reason to include language about
> concurrency in ObservableValue's specification. ObservableValue
> doesn't have any thread-safety guarantee at all; any attempt to use it
> from multiple threads should be assumed to potentially result in
> corrupted state.

I'm not that concerned with this part, although in light of perhaps 
having background cleaning of listener stubs, it could be worthwhile to 
just specify it. If JavaFX itself has a need to do background listener 
removal, then so could users. All it requires is synchronization per 
property while events are emitted.

--John



More information about the openjfx-dev mailing list