Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ public interface ClaimObserver<PositionT> {
* RestrictionTracker}.
*/
@ThreadSafe
private static class RestrictionTrackerObserver<RestrictionT, PositionT>
static class RestrictionTrackerObserver<RestrictionT, PositionT>
extends RestrictionTracker<RestrictionT, PositionT> {
private static final int SPLIT_TIMEOUT_SEC = 300;
protected final RestrictionTracker<RestrictionT, PositionT> delegate;
protected ReentrantLock lock = new ReentrantLock();
protected volatile Progress lastProgress = Progress.NONE;
Expand Down Expand Up @@ -98,14 +99,42 @@ public RestrictionT currentRestriction() {

@Override
public SplitResult<RestrictionT> trySplit(double fractionOfRemainder) {
lock.lock();
return trySplit(fractionOfRemainder, SPLIT_TIMEOUT_SEC);
}

@VisibleForTesting
SplitResult<RestrictionT> trySplit(double fractionOfRemainder, int timeOutSec) {
// When fractionOfRemainder == 0 (a checkpoint), returning null has a special meaning in the
// RestrictionTracker contract: it MUST imply that the restriction tracker is done and there
// is no more work left to do. Therefore, we cannot time out and return null when
// fractionOfRemainder == 0, and must block until the lock is acquired.
if (fractionOfRemainder == 0) {
lock.lock();
try {
SplitResult<RestrictionT> result = delegate.trySplit(fractionOfRemainder);
needsProgressUpdate = true;
return result;
} finally {
updateProgressAndUnlock();
}
}
try {
SplitResult<RestrictionT> result = delegate.trySplit(fractionOfRemainder);
needsProgressUpdate = true;
return result;
} finally {
updateProgressAndUnlock();
// For dynamic splits (fractionOfRemainder > 0), lock can be held long by a long-running
// tryClaim. We tolerate this scenario by returning null (declining to split) when lock
// timeout occurs.
if (lock.tryLock(timeOutSec, TimeUnit.SECONDS)) {
try {
SplitResult<RestrictionT> result = delegate.trySplit(fractionOfRemainder);
needsProgressUpdate = true;
return result;
} finally {
updateProgressAndUnlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

trySplit has this comment:

  • @return a {@link SplitResult} if a split was possible, otherwise returns {@code null}. If the
  • {@code fractionOfRemainder == 0}, a {@code null} result MUST imply that the restriction
    
  • tracker is done and there is no more work left to do.
    

So it seems like data loss to return null if fractionOfRemainder == 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good catch. I think in that case, we have 2 options:

  1. Raise a runtime exception
  2. Time out (keep the current behavior).

I lean towards keeping the current behavior for 2 reasons:

  1. It is less invasive
  2. It allows jobs to succeed if acquiring the split lock takes e.g. 8 minutes (so no potential regression)

I updated to do this, let me know what you think. It does mean we could fail a work item on a runner-initiated checkpoint, but at least the odds of doing that 4 times in a row should be low. I guess its also more motivation to move forward with your fix.

Btw, I think that the null protection is mostly for this code path -

trySplitForElementAndRestriction(0, continuation.resumeDelay(), false);
- which would actually be ok since the lock would no longer be held after processElement returned. But I do think we should honor this to protect against data loss here nonetheless.

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public synchronized SplitResult<Object> trySplit(double fractionOfRemainder) {
}
}
isBlocked = false;
return null;
return SplitResult.of("primary", "residual");
}

@Override
Expand Down Expand Up @@ -262,4 +262,59 @@ public void testClaimObserversProgressNonBlockingOnTrySplit() throws Interrupted
progress = ((HasProgress) tracker).getProgress();
assertEquals(RestrictionTrackerWithProgress.UPDATED_PROGRESS, progress);
}

@Test
public void testClaimObserversTrySplitNonBlockingOnTryClaim() throws InterruptedException {
RestrictionTrackerWithProgress withProgress = new RestrictionTrackerWithProgress(true, false);
RestrictionTracker<Object, Object> tracker =
RestrictionTrackers.observe(withProgress, new RestrictionTrackers.NoopClaimObserver<>());
Thread blocking = new Thread(() -> tracker.tryClaim(new Object()));
blocking.start();
withProgress.waitUntilBlocking(true);

// While tryClaim holds the lock, trySplit times out and returns null instead of blocking
// indefinitely.
SplitResult<Object> splitResult =
((RestrictionTrackers.RestrictionTrackerObserver<Object, Object>) tracker).trySplit(0.5, 1);
assertEquals(null, splitResult);

withProgress.releaseLock();
withProgress.waitUntilBlocking(false);
blocking.join();

// Once tryClaim releases the lock, trySplit succeeds.
splitResult =
((RestrictionTrackers.RestrictionTrackerObserver<Object, Object>) tracker).trySplit(0.5, 1);
assertEquals(SplitResult.of("primary", "residual"), splitResult);
}

@Test
public void testClaimObserversTrySplitZeroFractionBlocksOnTryClaim() throws InterruptedException {
RestrictionTrackerWithProgress withProgress = new RestrictionTrackerWithProgress(true, false);
RestrictionTracker<Object, Object> tracker =
RestrictionTrackers.observe(withProgress, new RestrictionTrackers.NoopClaimObserver<>());
Thread blocking = new Thread(() -> tracker.tryClaim(new Object()));
blocking.start();
withProgress.waitUntilBlocking(true);

// When fractionOfRemainder == 0, trySplit must block until the lock is released rather than
// timing out and returning null (since null implies the restriction tracker is done).
Thread releaseLater =
new Thread(
() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
withProgress.releaseLock();
});
releaseLater.start();

SplitResult<Object> splitResult =
((RestrictionTrackers.RestrictionTrackerObserver<Object, Object>) tracker).trySplit(0.0, 1);
assertEquals(SplitResult.of("primary", "residual"), splitResult);
blocking.join();
releaseLater.join();
}
}
Loading