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

Fixed concurrency issues in GradleDaemonExecutor #7030

Merged
merged 2 commits into from
Mar 3, 2024

Conversation

mbien
Copy link
Member

@mbien mbien commented Feb 5, 2024

the internal GradleTask appears to cover s similar usecase as the
CompletableFuture class. The implementation however caused a three
state race condition:

  • executor calls finish(int) before someone calls result(): this
    seems to be the intended usecase to create a fast-path which
    finishes the task early
  • result() is called before finish(int): this will block on the
    wrapped task and finish has no effect. This behavior can be
    reproduced by attempting to delete a gradle project, the delete
    project dialog will stay open for almost a minute.
  • third state is when createTask() is called too late: this will cause
    a NPE since the task is already started via setTask(), the
    executor would try to call finish on a task wrapper which is null.
    Can be easily reproduced by setting a breakpoint in the codepath
    which calls createTask() or the method itself
    example:
    java.lang.NullPointerException: Cannot invoke "org.netbeans.modules.gradle.execute.GradleDaemonExecutor$GradleTask.finish(int)" because "this.gradleTask" is null
    	at org.netbeans.modules.gradle.execute.GradleDaemonExecutor.run(GradleDaemonExecutor.java:263)
    	at org.netbeans.core.execution.RunClassThread.doRun(RunClassThread.java:132)
    	at org.netbeans.modules.openide.util.GlobalLookup.execute(GlobalLookup.java:45)
    	at org.openide.util.lookup.Lookups.executeWith(Lookups.java:287)
    [catch] at org.netbeans.core.execution.RunClassThread.run(RunClassThread.java:81)
    

fixes:

  • block on CountDownLatch instead of delegate
  • create wrapper right when the task starts
  • the other commit fixes an issue where a start() is called from within the thread's constructor

this has also positive performance side effects: e.g project creation is also faster now

@mbien mbien added Gradle [ci] enable "build tools" tests ci:all-tests [ci] enable all tests ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) labels Feb 5, 2024
@mbien mbien added this to the NB22 milestone Feb 5, 2024
@mbien mbien added performance kind:bug Bug report or fix labels Feb 5, 2024
@mbien
Copy link
Member Author

mbien commented Feb 5, 2024

all green, removing all-tests label

@mbien mbien removed the ci:all-tests [ci] enable all tests label Feb 5, 2024
@mbien mbien marked this pull request as ready for review February 5, 2024 19:43
Copy link
Contributor

@lkishalmi lkishalmi left a comment

Choose a reason for hiding this comment

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

Great find!
Thank you!

GradleTask(ExecutorTask delegate) {
super(() -> {});
this.delegate = delegate;
this.delegate.addTaskListener(t -> doneSignal.countDown());
Copy link
Member

Choose a reason for hiding this comment

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

In this case, if called before finish, the result will appear to be 0 == success. Is that OK ? When searching for finish calls, I've found paths in GradleDaemonExecutor::run - the exception handlers, that do not call gradleTask.finish at all, so these all will reach this lambda and produce result 0 ?

Copy link
Member Author

@mbien mbien Feb 6, 2024

Choose a reason for hiding this comment

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

not sure if I understand the question correctly. If someone calls result() and nothing calls the fast-path finish(int) the delegate task will count down the latch once it completes and the blocking result() method will now unblock and return delegate.result().

the second case is that finish(int) is called before the delegate task completes, this would also unblock all waiting threads which wait on the latch, but this time it returns the result field without waiting for the delegate task.

the second case seems to be the intended purpose of the task wrapper to speed up the time-to-result. Git blame msg luckily mentions it too d7d4faf I wished this would be mentioned in code.

Copy link
Member Author

@mbien mbien Feb 10, 2024

Choose a reason for hiding this comment

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

I've found paths in GradleDaemonExecutor::run - the exception handlers, that do not call gradleTask.finish at all, so these all will reach this lambda and produce result 0 ?

@sdedic this was the case before too, no?

            if (result != null) {
                return result;
            }
            return this.delegate.result();

this would have blocked on the delegate task and return the result. I am not 100% sure what you mean by 0 since the current behavior should do the same.

@mbien
Copy link
Member Author

mbien commented Feb 27, 2024

planning to merge this soon unless there are objections

@mbien mbien removed the ci:dev-build [ci] produce a dev-build zip artifact (7 days expiration, see link on workflow summary page) label Mar 3, 2024
 - small cleanup: added missing overrides, made some fields final etc
the internal GradleTask appears to cover s similar usecase as the
CompletableFuture class. The implementation however caused a three
state race condition:

 - executor calls finish(int) before someone calls result(): this
   seems to be the intended usecase to create a fast-path which
   finishes the task early
 - result() is called before finish(int): this will block on the
   wrapped task and finish has no effect. This behavior can be
   reproduced by attempting to delete a gradle project, the delete
   project dialog will stay open for almost a minute.
 - third state is when createTask() is called too late: this will cause
   a NPE since the task is already started via setTask(), the
   executor would try to call finish on a task wrapper which is null.
   Can be easily reproduced by setting a breakpoint in the codepath
   which calls createTask() or the method itself

fixes:

 - block on CountDownLatch instead of delegate
 - create wrapper right when the task starts
@mbien
Copy link
Member Author

mbien commented Mar 3, 2024

no objections after another 5 days -> going to refresh this PR and then merge when green. Thanks for reviewing everyone!

@mbien mbien force-pushed the fix-gradle-daemon-executor-concurrency branch from 386a3c7 to dcb2580 Compare March 3, 2024 16:32
@mbien mbien merged commit 736cf60 into apache:master Mar 3, 2024
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Gradle [ci] enable "build tools" tests kind:bug Bug report or fix performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants