-
Notifications
You must be signed in to change notification settings - Fork 152
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
💥 [Breaking] Asyncify slot suppliers #2433
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
import java.time.Duration; | ||
import java.time.Instant; | ||
import java.util.Optional; | ||
import java.util.concurrent.*; | ||
|
||
/** Implements a {@link SlotSupplier} based on resource usage for a particular slot type. */ | ||
@Experimental | ||
|
@@ -32,6 +33,18 @@ public class ResourceBasedSlotSupplier<SI extends SlotInfo> implements SlotSuppl | |
private final ResourceBasedController resourceController; | ||
private final ResourceBasedSlotOptions options; | ||
private Instant lastSlotIssuedAt = Instant.EPOCH; | ||
// For slot reservations that are waiting to re-check resource usage | ||
private static final ScheduledExecutorService scheduler = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should consider accepting this in the constructor. Can have a default, but we should allow people to control thread creation if they want. |
||
Executors.newScheduledThreadPool( | ||
// Two threads seem needed here, so that reading PID decisions doesn't interfere overly | ||
// with firing off scheduled tasks or one another. | ||
2, | ||
r -> { | ||
Thread t = new Thread(r); | ||
t.setName("ResourceBasedSlotSupplier.scheduler"); | ||
t.setDaemon(true); | ||
return t; | ||
}); | ||
|
||
/** | ||
* Construct a slot supplier for workflow tasks with the given resource controller and options. | ||
|
@@ -139,29 +152,43 @@ private ResourceBasedSlotSupplier( | |
} | ||
|
||
@Override | ||
public SlotPermit reserveSlot(SlotReserveContext<SI> ctx) throws InterruptedException { | ||
while (true) { | ||
if (ctx.getNumIssuedSlots() < options.getMinimumSlots()) { | ||
return new SlotPermit(); | ||
} else { | ||
Duration mustWaitFor; | ||
try { | ||
mustWaitFor = options.getRampThrottle().minus(timeSinceLastSlotIssued()); | ||
} catch (ArithmeticException e) { | ||
mustWaitFor = Duration.ZERO; | ||
} | ||
if (mustWaitFor.compareTo(Duration.ZERO) > 0) { | ||
Thread.sleep(mustWaitFor.toMillis()); | ||
} | ||
|
||
Optional<SlotPermit> permit = tryReserveSlot(ctx); | ||
if (permit.isPresent()) { | ||
return permit.get(); | ||
} else { | ||
Thread.sleep(10); | ||
} | ||
} | ||
public CompletableFuture<SlotPermit> reserveSlot(SlotReserveContext<SI> ctx) { | ||
if (ctx.getNumIssuedSlots() < options.getMinimumSlots()) { | ||
return CompletableFuture.completedFuture(new SlotPermit()); | ||
} | ||
return tryReserveSlot(ctx) | ||
.map(CompletableFuture::completedFuture) | ||
.orElseGet(() -> scheduleSlotAcquisition(ctx)); | ||
} | ||
|
||
private CompletableFuture<SlotPermit> scheduleSlotAcquisition(SlotReserveContext<SI> ctx) { | ||
Duration mustWaitFor; | ||
try { | ||
mustWaitFor = options.getRampThrottle().minus(timeSinceLastSlotIssued()); | ||
} catch (ArithmeticException e) { | ||
mustWaitFor = Duration.ZERO; | ||
} | ||
|
||
CompletableFuture<Void> permitFuture; | ||
if (mustWaitFor.compareTo(Duration.ZERO) > 0) { | ||
permitFuture = | ||
CompletableFuture.supplyAsync(() -> null, delayedExecutor(mustWaitFor.toMillis())); | ||
} else { | ||
permitFuture = CompletableFuture.completedFuture(null); | ||
} | ||
|
||
// After the delay, try to reserve the slot | ||
return permitFuture.thenCompose( | ||
ignored -> { | ||
Optional<SlotPermit> permit = tryReserveSlot(ctx); | ||
// If we couldn't get a slot this time, delay for a short period and try again | ||
return permit | ||
.map(CompletableFuture::completedFuture) | ||
.orElseGet( | ||
() -> | ||
CompletableFuture.supplyAsync(() -> null, delayedExecutor(10)) | ||
.thenCompose(ig -> scheduleSlotAcquisition(ctx))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ChatGPT is telling me that cancellation does not propagate across There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this works fine because the returned future is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My concern is that ChatGPT says that a cancel of the outer future does not propagate the cancel to the But I fear reading https://stackoverflow.com/questions/25417881/canceling-a-completablefuture-chain and poking around, there may need to be some other mechanism to make sure the outer completable future cancel propagates to cancelling the delayed executor. I couldn't tell if the test was doing that. I wouldn't be surprised if cancel is not hierarchical. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Canceling a completable future is not a good way to tell the producer the result is no longer needed, it is to tell the consumer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 So I do not know of what a good way in Java to tell the implementer of something async that it is cancelled? I am guessing we use thread interruption for synchronous activity code today and disallow async/future-returning activity code? Any ideas here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the standard library would use |
||
}); | ||
} | ||
|
||
@Override | ||
|
@@ -190,4 +217,9 @@ public ResourceBasedController getResourceController() { | |
private Duration timeSinceLastSlotIssued() { | ||
return Duration.between(lastSlotIssuedAt, Instant.now()); | ||
} | ||
|
||
// Polyfill for Java 9 delayedExecutor | ||
private static Executor delayedExecutor(long delay) { | ||
return r -> scheduler.schedule(() -> scheduler.execute(r), delay, TimeUnit.MILLISECONDS); | ||
} | ||
} |
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.
reserveSlot
is a user defined function so I would except any use case to be under atry
block so exceptions don't leek out and crash the worker or cause undesirable behaviour