Listeners behavior discussion
John Hendrikx
john.hendrikx at gmail.com
Sat Jul 9 11:02:26 UTC 2022
Hi all,
I'm looking into ExpressionHelper as part of my evaluation of adding
Cleaner's to clean up weak listeners stubs a bit more eagerly.
I would like to hear your thoughts on the following:
ExpressionHelper has a locking mechanism so that when an event occurs,
it is fired to the listeners which were active at the moment the event
was triggered. This is achieved by making a copy of the listeners *if*
the list is modified while an event emission is in progress. This uses a
simple boolean for the lock ("locked").
It is however conceivable (although perhaps not good practice) to change
the value again while the event emission is occurring. An easy example I
can think of is that when a text field is modified, a ChangeListener may
change the value to uppercase if the field should only contain uppercase
characters. This will trigger a nested event emission for the same
property. JavaFX does not prohibit this (and probably can't anymore for
backwards compatibility reasons). There are several problems with this:
1) The locking mechanism does not take nesting into account, therefore
when the 2nd nested emission completes, the listener lists are unlocked,
and the first event emission will continue with the lists unlocked. If
there is any modification after the 2nd emission completes, listeners
may be skipped, notified twice or an IndexOutOfBoundsException may occur.
2) The events received by the listeners are inconsistent depending on
where you are in relation to the listener that made the nested change.
Let's say there are three listeners, A, B and C in that order. Listener
B will change any uneven number to the next higher even number. The
events seen by the three listeners when the value changes from 2 to 3 are:
A: 2 -> 3; 3 -> 4
B: 2 -> 3; 3 -> 4
C: 3 -> 4; 2 -> 4
IMHO, we should strive to have the exact same events for all three
listeners: 2 -> 3; 3 -> 4 or prohibit this with an exception.
I think it is possible to change this so all listeners receive the exact
same events; this would be achieved by delaying the 2nd event emission
until the first has finished.
I think the specification for how invalidation/change listener event
notification works should perhaps be documented somewhere. I think it
should be:
When an event occurs, the currently list of registered listeners will
receive this event in the order they were registered, regardless of
whether the list is modified during the event emission. If during
emission, another event occurs for the same property, it will be queued
and emitted immediately after the current emission completes. This
ensures all registered listeners receive the same events irrespective of
registration order. Event emission is serialized, that is to say, no
two emissions for the same property will ever run concurrently.
=====
Implementation ideas
If we can all agree on the above specification, I think an
implementation can be created that never needs to copy a listener list.
The main trick to get this correct is in how events are emitted. First,
when a nested emission occurs (easily detected by the fact that the list
is locked), the change for the nested emission is buffered. The nested
emission is then triggered as soon as the first finishes (when the first
emission is about to unlock, it checks for a nested emission, and does
another emission if one is present, and so on). If there are multiple
emissions after an emission finishes, they probably can and should be
rolled into a single one. Note that nested emissions can easily go
wrong (two listeners that change values in conflicting ways) and cause
an infinite loop -- we're not looking to prevent these kinds of bugs
here, although we could set a limit before "giving up".
Secondly, while the lists are locked, any additions or removals to the
list should be buffered. Just before the list is unlocked (and before a
second emission occurs), all changes are applied. This has the
additional bonus that any mass changes to the listener list (for example
when a large number of stubs were determined to require clean up) can
use mass change functions like #removeAll and #addAll, potentially
saving a lot of copying of the listener list, on top of the copy that is
already avoided due to the lock.
The fireValueChangedEvent method should be synchronized, so only the
same thread could potentially trigger a nested emission. I think we
should steer clear from allowing multiple threads to emit events for the
same property concurrently as this will make the events received
unpredictable and probably even incorrect (the last change received may
not reflect the actual current value!).
Thanks for reading, I look forward to hearing your thoughts!
--John
More information about the openjfx-dev
mailing list