-
Notifications
You must be signed in to change notification settings - Fork 7.6k
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
2.x: distinguish between sync and async dispose in ScheduledRunnable #5715
Conversation
Codecov Report
@@ Coverage Diff @@
## 2.x #5715 +/- ##
============================================
- Coverage 96.28% 96.24% -0.05%
- Complexity 5822 5825 +3
============================================
Files 634 634
Lines 41604 41609 +5
Branches 5761 5761
============================================
- Hits 40060 40048 -12
- Misses 616 622 +6
- Partials 928 939 +11
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but comments
/** Indicates the dispose() was called from within the run/call method. */ | ||
static final Object SYNC_DISPOSED = new Object(); | ||
/** Indicates the dispose() was called from another thread. */ | ||
static final Object ASYNC_DISPOSED = new Object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supernit: ASYNC
doesn't mean another thread :)
But for name conciseness I guess it's fine
@@ -66,13 +71,13 @@ public void run() { | |||
} finally { | |||
lazySet(THREAD_INDEX, null); | |||
Object o = get(PARENT_INDEX); | |||
if (o != DISPOSED && o != null && compareAndSet(PARENT_INDEX, o, DONE)) { | |||
if (o != PARENT_DISPOSED && compareAndSet(PARENT_INDEX, o, DONE) && o != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change of compareAndSet
position in if
statement seems to be a behavior change…
Though it doesn't affect ScheduledRunnableTest
on my machine
Do we need that change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. With the previous order, if o
was null
because of lack of a parent, the CAS didn't happen and the task would partially appear to be still active. With the sync-async marker changes, isDisposed
was switched to check the parent reference for indication of being disposed instead of the future reference, which required now 3 comparisons instead of 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, can you please add a test to cover that then?
When I changed order back to original one on your branch, whole ScheduledRunnableTest
passed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unit tests added.
break; | ||
} | ||
if (compareAndSet(FUTURE_INDEX, o, DISPOSED)) { | ||
boolean async = get(THREAD_INDEX) != Thread.currentThread(); | ||
if (compareAndSet(FUTURE_INDEX, o, async ? ASYNC_DISPOSED : SYNC_DISPOSED)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, what worries me here is that we base boolean async
on get(THREAD_INDEX) != Thread.currentThread()
, but in finally
of run()
we set Thread
to null which always gonna give async == false
lazySet(THREAD_INDEX, null); |
Yes, it's lazySet()
, but aren't we creating unwanted race condition here between run()
and dispose()
?
The worst scenario is that if dispose()
indeed was called from another thread but lazySet()
memory write becomes visible to it, we'll have async == false
which is actually wrong in this scenario…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cancelling from the current thread could only happen while the run
callback is executing. Any other cancellation that comes after this can only be asynchronous. It doesn't matter what value is in THREAD_INDEX
at this time because that will not be the same thread as the caller's.
Since we are running with single-threaded thread pools, any subsequent task on the same pool having the same Thread will find the references marked as DONE
and not cancel the previous task. This is guaranteed by the sequential task processing of the thread pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't matter what value is in THREAD_INDEX at this time because that will not be the same thread as the caller's.
Aha, I see now, 👍
This PR adds logic to distinguish between synchronous and asynchronous dispose calls when
setFuture
is executing. It should prevent interrupting the currently running task body if it requested cancellation indirectly before thesetFuture
was executed by the Thread which scheduled the task.Fixes #5711