JavaFX Application Thread is recursively re-entrant into Eventhandler handle() method under some circumstances

javafx at use.startmail.com javafx at use.startmail.com
Sun Sep 9 17:05:15 UTC 2018


Hi All,
I spent some time refactoring the program which displays this bug. It's 
now small enough to share the source in an email, but it is very very 
dense and the proof of bug, one  specific line to standard I/O, 
requires the source code to be read and understood in order to see the 
bug. As I said previously, more comprehensible and user-friendly 
versions of this program are  available at . The javadoc is more 
copious, the bug is manifested as an exception and the side effect of 
the bug are more consequential.

This brief version cnosists of just two classes. Here is the JavaFX 
Application class:

***************************************************************************

package bareBonesJavaFXBugExample;


import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;



/**
 * An {@link Application} with one {@link Pane} containing one {@link 
Label}. The {@link Label} has a single {@link 
javafx.event.EventHandler}, {@link LabelEventHandler} which processes 
all {@link MouseEvent}s the {@link Label} receives.
 * <p></p>
 * To trigger the bug, run the application, then spend a second mouse 
over the little label in the upper left hand corner of the scrren. You 
will see output to standard I/O. Then, click the label, which will then 
disppear. Check the I/O for Strings ending in debugCounter is 1. 
<p></p>
 * What that String means and how it proves that the JavaFX Application 
Thread has become reentrant is explained in the javadoc of {@link 
LabelEventHandler}.
 */
public class JavaFXAnomalyBareBonesApplication extends Application
{
    public void start(Stage primaryStage)
    {

      Pane mainPane = new Pane();
      mainPane.setMinHeight(800);
      mainPane.setMinWidth(800);

      Label label = new Label(" this is quite a bug !!!!");

      LabelEventHandler labelEventHandler = new 
LabelEventHandler(mainPane, label);
      label.addEventHandler(MouseEvent.ANY, labelEventHandler);

      mainPane.getChildren().add(label);


      Scene scene = new Scene(mainPane);
      primaryStage.setScene(scene);
      primaryStage.show();

    }



    /**
     * The entry point of application.
     *
     * @param args
     *         the input arguments
     */
    public static void main(String[] args)
    {

        launch(args);
    }



}


***************************************************************************

and here is its only dependency, the EventListener class. I included 
enough javadoc to have the program makes sense. :

***************************************************************************

package bareBonesJavaFXBugExample;



import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

import java.util.Collection;
import java.util.ConcurrentModificationException;

/**
 * An {@link EventHandler} implementation for {@link MouseEvent}s. 
<p></p> This implementation's {@link EventHandler#handle(Event)} shows 
the relevant debug information to standard output before and after 
removing the member {@link #label} from the {@link #pane}.
 * <p></p>
 * <b>discussion</b><br></br>
 * <P></P>
 * Users should first satisfy themselves that the value of {@link 
LabelEventHandler#debugCounter} can only be non-zero, in fact 1 (one) 
in the method {@link LabelEventHandler#showDebugInformation(String)} if 
the method {@link LabelEventHandler#handle(MouseEvent)}  has been 
re-entered recursively, that is, before a previous invocation of {@link 
LabelEventHandler#handle(MouseEvent)} has returned.
 * <p></p>
 * Proof: <p></p> 1) <code>debugCounter</code> starts at value 0 
(zero). <p></p> 2) <code>debugCounter</code> is only incremented once, 
by 1 (one), and that is after the first call to {@link 
LabelEventHandler#showDebugInformation(String)} has returned.<p></p> 3) 
<code>debugCounter</code> is only decremented once, by 1 (one) and that 
is before the last call to {@link 
LabelEventHandler#showDebugInformation(String)}.<p></p> 4) however, 
because
 * <code>debugCounter</code> is a class variable ( it's static), if 
handle() is recurvsively re-entered then it's value can be 1 (one) when 
the re-entrant Thread executes {@link 
LabelEventHandler#showDebugInformation(String)}
 * <p></p>
 * End proof.
 * <p></p>
 * <p></p>
 * <p>
 * The output of this method to standard I/O is volumnious but 
searching the output for the exact String "debugCounter is 1" will 
immediately show the {@link LabelEventHandler#handle(MouseEvent)} 
method to have been recursively entered. <p></p>
 * Some other possibilities other than the JavaFX Application Thread 
recursing into {@code handle()} need to be addressed. <p></p> One is 
the fact that the compiler is free to reorder statements if it can
 * prove that such a reordering would have no effect on the program's 
correctness.
 * <br></br>
 * So somehow the compiler is reordering the increment/decrement of 
{@code  debugCounter} and the calls to {@code   showDebugInformation}. 
<br></br> But this would alter the correctness of the program, so this 
cannot be the case, or the compiler is making an error.<P></P>
 * <p>
 * <p>
 * Another is the fact that I/O is not instantaneous and can appear to 
standard output later than it actually was executed. <br></br> This is 
something often seen in debug stack traces, where the output is broken 
up  or interleaved by the output of the stack trace even though the two 
sets of statments, i/o and stack trace i/o, were strictly ordered in 
execution. <br></br> But this can't account for the value of {@code  
 debugCounter}, so it can't
 * be the reason "debugCounter is 1" appears in output.<p></p> In fact 
we can make this recursive behaviour more obviously consequential to 
the correctness of the program. <p></p> If {@code   handle() } is being 
recursively re-entered, then we can force a {@link 
ConcurrentModificationException} on a {@link Collection}.  <br></br> If 
we try to invoke {@link Collection#add(Object)} to a {@link Collection} 
while it is being iterated through, then a
 * {@link ConcurrentModificationException} will be thrown.<p></p> If we 
re-write this program slightly to first add or remove to or from a 
{@link Collection} then iterate through that {@link Collection} within 
the scope of  execution of {@code   handle()}, <em>and</em> {@code  
 handle()} is being recursively invoked, then we may see a {@link 
ConcurrentModificationException}.
 * <p></p>
 * Two other instances of this same basic program exist at the link 
provided. They are named {@link JavaFXAnomalySimpleVersionApplication} 
and {@link JavaFXAnomalyComplexVersionApplication} which is written to 
throw a {@link ConcurrentModificationException} when the JavaFX 
Application Thread becomes reentrant.
 * <p></p>
 * I also have a screen grab (not included here) of the stack trace at 
a specific moment <code>handle()/code> is being invoked, and it can 
clearly be seen that the previous executing line was within the scope 
of execution of the previous invocation of <code>handle()</code>.
 * <p></p>
 * In the .zip file at the link there is a readme.txt. In that file. I 
present the two lines of code which need to be added, and where they 
need to be added,  so as to generate the same stack trace showing the 
same thing.
 *
 *
 * </p>
 */
public class LabelEventHandler implements EventHandler<MouseEvent>
{
  /**
   * a counter which acts as a recursion detector. If {@link 
#handle(MouseEvent)} is never recursively invoked by the JavaFX 
Application Thread, then it's value will never be other than 0 (zero) 
in {@link #showDebugInformation(String)}.
   */
  private static int debugCounter;

  /**
   * The {@link Label} which will disappear when clicked. This causes a 
MOUSE_EXITED_TARGET event top be fired and that in turn causes the 
JavaFX Event Dispatch Thread to recurse into this class's {@link 
#handle(MouseEvent)}
   */
  private Label label;
  /**
   * The {@link Pane} which contains the {@link Label}. The {@link 
Label} is removed from this {@link Pane}.
   */
  private final Pane pane;



  /**
   * Assign the values to the members {@link Pane} and {@link Label}
   */
  public LabelEventHandler(Pane pane, Label label)
  {

    this.pane = pane;
    this.label = label;
  }



  /**
   * Causes the member {@link #label} to be removed as a child of the 
member {@link #pane}.
   *
   *
   * @param mouseEvent
   *         the {@link MouseEvent} received from the JavaFX 
Application Thread from the {@link Label} which this {@link 
EventHandler} is listening to.
   */
  @Override
  public void handle(MouseEvent mouseEvent)
  {

    showDebugInformation("ENTERING");// debug can only every be 0 
(zero) at this point
    debugCounter++;


    if (mouseEvent.getEventType().equals(MouseEvent.MOUSE_PRESSED) && 
mouseEvent.isPrimaryButtonDown())
    {
      pane.getChildren().remove(label);
    }

    debugCounter--;
    showDebugInformation("EXITING");// debug can only every be 0 (zero) 
at this point
  }



  /**
   * Displays two values to standard output. The first is a {@link 
String}  indicating whether the {@link 
LabelEventHandler#handle(MouseEvent)} method is being entered or exited 
and the second is the value of {@link LabelEventHandler#debugCounter} 
at the time this method is executed.
   *
   * @param enterOrExit
   *         the string ENTERING or EXITING reflecting the point  at 
which this method was invoked by {@link 
LabelEventHandler#handle(MouseEvent)}.
   */
  private void showDebugInformation(String enterOrExit)
  {

    System.out.println();
    System.out.print(enterOrExit + " method handle");
    System.out.print(" and debugCounter is " + debugCounter);
    System.out.println();
  }





}



*******************************************************************

Just cut and pasting these two into files named by  their  respective 
Java class names, then  placing those  files into a folder 
named bareBonesJavaFXBugExample is all it should  take to make this 
work.

Unless I get contrary  feedback, I will file this as a bug after I run 
it against the most recent releases of the JavaFX. 

Either way I am interested in feedback from the community.  

Cheers !



 
On Saturday, September 8, 2018 at 8:02 AM, Kevin Rushforth 
<kevin.rushforth at oracle.com> wrote:
 
> I am not aware of such a bug. If you have a test program, then you 
> can
> file a bug here:
> 
> https://bugreport.java.com/
> 
> -- Kevin
> 
> 
> On 9/7/2018 5:37 PM, javafx at use.startmail.com wrote:
>> Hi,
>> 
>> I have a couple of very small apps (3 small classes in one case and 
>> 5
>> in another)  which demonstrate that, under some circumstances, the
>> JavaFX Application Thread will recursively re-enter
>> EventHandler#handle().
>> 
>> So this means that it is already in handle (and calls therefrom) and
>> will, in some situations not complete that  processing (thus exiting
>> handle) before it reappears in the same instance of EventHandler's
>> handle method again. So this is true recursion.
>> 
>> I actually don't know if this is expected behavior or not. No one 
>> I've
>> talked to expected it; the general understanding is the JavaFX
>> Application Thread (processing) is specifically single-threaded and
>> also that it will defintily complete one invocation of handle() 
>> before
>> beginning another one.
>> 
>> I have to say that there is NO other Thread  in play here, at least 
>> no
>> other Thread my applications create (what's going on QuantumToolKit
>> may be a different story.)
>> 
>> The material upshot of this is it can lead to apparent  program
>> incorrectness if the dev believes that it's not the case, and 100% 
>> of
>> devs I've talked to think it's not possible.
>> 
>> I am happy to post or attach the classes or modules as requested but
>> first I wanted to check to see if in fact this is already known to 
>> be
>> true and is in fact  expected behavior, in which case it's a 
>> non-issue
>> and just a subtlety people are not aware of.
>> 
>> Thank you so much !


More information about the openjfx-dev mailing list