-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
API for stopping streaming tasks early #16310
Conversation
@@ -3132,7 +3153,7 @@ private void checkTaskDuration() throws ExecutionException, InterruptedException | |||
} else { | |||
DateTime earliestTaskStart = computeEarliestTaskStartTime(group); | |||
|
|||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow()) { | |||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow() || group.getShutdownEarly()) { |
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.
This looks like it is still subject to the stopTaskCount limit ... for my use case I would want to be able to force this rollover without any other restrictions. If you have a use case that requires that it be subject to stopTaskCount, then can you add an optional parameter (e.g. "force=true") that will allow us to choose between the modes? Thanks.
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.
can't you just set stopTaskCount=0 to not have that config apply?
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.
Changing stopTaskCount requires a Supervisor change, which defeats the whole purpose ;)
My clusters generally run with stopTaskCount=1 which works very well ... but for high taskCount jobs it is much more likely that one task will be cycling at any given time, which would render this command useless ...
Furthermore I may have to cycle more than one task, depending on how many of the tasks are running on the node being cycled ... that makes it that much more likely that I will go above the stopTaskCount level.
... unless this command queues up the tasks for cycling, but I didn't think that logic is in the code. So what happens now if you are at the stopTaskCount limit? Is the stop-early command ignored?
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.
in this implementation it would respect the stopTaskCount and not stop the task until a "stop slot" is available.
I guess a alternate implementation could be to just short-circuit stopTaskCount and always stop tasks that have been stopped early, let me think about that a bit
i feel like having a config is too complicated imo, it should be one behavior or the other
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.
thinking about this some more i think it should probably just ignore stopTaskCount since the operator has manually requested the stop
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.
Please add a comment in the code explaining why we chose to ignore stopTaskCount when shutdownEarly
is set to true. I think it makes sense to not respect it in this case, since stopTaskCount was meant to reduce task spikes during normal operation, and shutdownEarly is in response to an API call which is not a "normal" operation.
|
||
EasyMock.reset(spec); | ||
EasyMock.expect(spec.isSuspended()).andReturn(false).anyTimes(); | ||
EasyMock.expect(spec.getDataSchema()).andReturn(getDataSchema()).anyTimes(); |
Check notice
Code scanning / CodeQL
Deprecated method or constructor invocation Note test
SeekableStreamSupervisorSpec.getDataSchema
...-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java
Outdated
Show resolved
Hide resolved
@@ -395,6 +395,25 @@ public Response shutdown(@PathParam("id") final String id) | |||
return terminate(id); | |||
} | |||
|
|||
@POST | |||
@Path("/{id}/taskGroups/restart") |
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.
@Path("/{id}/taskGroups/restart") | |
@Path("/{id}/taskGroups/handoff") |
^ maybe this is a good name to match the function name.
Can you also please add javadocs for this function explaining that it is best effort. I think it is worth calling out that if a user passes in a taskGroup that does not exist, it will not error here.
And some user visible docs here https://github.com/apache/druid/blob/master/docs/api-reference/supervisor-api.md
default void handoffTaskGroupEarly(int taskGroupId) | ||
{ | ||
} |
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.
default void handoffTaskGroupEarly(int taskGroupId) | |
{ | |
} | |
default void handoffTaskGroupEarly(int taskGroupId) throws DruidException | |
{ | |
throw DruidException | |
.forPersona(DruidException.Persona.ADMIN) | |
.ofCategory(DruidException.Category.UNSUPPORTED) | |
.build(...); | |
} |
I think the default implementation should indicate in some way that this is not supported. I don't have a strong opinion on if it needs to be via an exception or some other mechanism.
{ | ||
return asLeaderWithSupervisorManager( | ||
manager -> { | ||
if (manager.handoffTaskGroupsEarly(id, taskGroupIds)) { |
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.
Should handle error case when the supervisor does not support handoffTaskGroupsEarly
We probably also need a null or empty check for taskGroupIds.
@@ -657,6 +669,37 @@ public String getType() | |||
} | |||
} | |||
|
|||
private class HandoffTaskGroupNotice implements Notice | |||
{ | |||
final Integer taskGroupId; |
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.
final Integer taskGroupId; | |
final List<Integer> taskGroupId; |
I think accepting a List of integers in the notice will make for nicer dev ergonomics
private static final String TYPE = "handoff_task_group_notice"; | ||
|
||
HandoffTaskGroupNotice( | ||
final Integer taskGroupId |
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.
Add Nonnull since it should be validated elsewhere
final Integer taskGroupId | |
@Nonnull final List<Integer> taskGroupIds |
@@ -3132,7 +3153,7 @@ private void checkTaskDuration() throws ExecutionException, InterruptedException | |||
} else { | |||
DateTime earliestTaskStart = computeEarliestTaskStartTime(group); | |||
|
|||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow()) { | |||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow() || group.getShutdownEarly()) { |
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.
Please add a comment in the code explaining why we chose to ignore stopTaskCount when shutdownEarly
is set to true. I think it makes sense to not respect it in this case, since stopTaskCount was meant to reduce task spikes during normal operation, and shutdownEarly is in response to an API call which is not a "normal" operation.
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.
Looks good to me. Just have one comment about the body of the API.
Also it would be nice to have some sort of integration test for this since this is quite a subtle change. It is ok if that integration test comes in a follow up patch
docs/api-reference/supervisor-api.md
Outdated
```shell | ||
curl --request POST "http://ROUTER_IP:ROUTER_PORT/druid/indexer/v1/supervisor/social_media/taskGroups/handoff" | ||
--header 'Content-Type: application/json' | ||
--data-raw '["1", "2", "3"]' |
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.
--data-raw '["1", "2", "3"]' | |
--data-raw '{"taskGroupIds": [1, 2, 3]}' |
I think this is the expected format for a REST API. Also it looks like taskGroupIds are integers, so I don't think quotes are needed around the numbers
.../main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
Outdated
Show resolved
Hide resolved
.../main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
Outdated
Show resolved
Hide resolved
…blestream/supervisor/SeekableStreamSupervisor.java Co-authored-by: Suneet Saldanha <[email protected]>
…blestream/supervisor/SeekableStreamSupervisor.java Co-authored-by: Suneet Saldanha <[email protected]>
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.
Thanks for this API 🚀 🚀 🚀
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.
Thanks for the feature, @georgew5656 ! This would be very useful in upgrade scenarios and for future work of having streaming tasks running perpetually.
Even though this PR has already been merged, I have left some small suggestions.
manager -> { | ||
try { | ||
if (manager.handoffTaskGroupsEarly(id, taskGroupIds)) { | ||
return Response.ok(ImmutableMap.of("id", id, "taskGroupIds", taskGroupIds)).build(); |
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.
In the success case, why do we return the request parameters back in the response? We might as well just return an empty 200 OK response.
Alternatively, we could return the taskGroupIds
which were actually marked for early hand-off. In the case where some of the requested taskGroupIds
are non-existent or not actively reading, the returned set of taskGroupIds
could differ from the requested one, thus telling the caller which ones will actually be handed off.
/** Handoff the task group with id=taskGroupId the next time the supervisor runs regardless of task run time*/ | ||
default void handoffTaskGroupsEarly(List<Integer> taskGroupIds) | ||
{ | ||
throw new NotImplementedException("Supervisor does not have the feature to handoff task groups early implemented"); |
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.
Nit: Why not use UnsupportedOperationException
instead?
The error message seems to suggest that this is more of an "unsupported" scenario.
@@ -93,4 +94,10 @@ default Boolean isHealthy() | |||
LagStats computeLagStats(); | |||
|
|||
int getActiveTaskGroupsCount(); | |||
|
|||
/** Handoff the task group with id=taskGroupId the next time the supervisor runs regardless of task run time*/ |
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.
Some rephrase + Druid javadoc styling
/** Handoff the task group with id=taskGroupId the next time the supervisor runs regardless of task run time*/ | |
/** | |
* Marks the given task groups as ready for segment hand-off irrespective of the task run times. | |
* In the subsequent run, the supervisor initiates segment publish and hand-off for these task groups and rolls over their tasks. | |
* taskGroupIds that are not valid or not actively reading are simply ignored. | |
*/ |
if (taskGroup == null) { | ||
log.info("Tried to stop task group [%d] for supervisor [%s] that wasn't actively reading.", taskGroupId, supervisorId); | ||
continue; | ||
} | ||
|
||
taskGroup.setShutdownEarly(); |
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.
Nit:
if (taskGroup == null) { | |
log.info("Tried to stop task group [%d] for supervisor [%s] that wasn't actively reading.", taskGroupId, supervisorId); | |
continue; | |
} | |
taskGroup.setShutdownEarly(); | |
if (taskGroup == null) { | |
log.info("Tried to stop task group[%d] for supervisor[%s] but it is not actively reading.", taskGroupId, supervisorId); | |
} else { | |
taskGroup.setShutdownEarly(); | |
} |
@Override | ||
public void handle() | ||
{ | ||
for (Integer taskGroupId : taskGroupIds) { |
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.
We should add an info log line here saying that we are now going to handoff these task groups early. Otherwise, there is no way to know that such a request was even received by the supervisor.
We can probably also add a log line in SupervisorManager
to indicate when the request was received. The log here can be used to identify when the notice is actually handled.
@@ -3143,14 +3194,15 @@ private void checkTaskDuration() throws ExecutionException, InterruptedException | |||
} else { | |||
DateTime earliestTaskStart = computeEarliestTaskStartTime(group); | |||
|
|||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow()) { | |||
if (earliestTaskStart.plus(ioConfig.getTaskDuration()).isBeforeNow() || group.getShutdownEarly()) { |
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.
If the task group is marked for early shutdown, we should log it.
@@ -266,6 +268,16 @@ Set<String> taskIds() | |||
return tasks.keySet(); | |||
} | |||
|
|||
void setShutdownEarly() |
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.
Should these methods be renamed to setHandoffEarly()
to correspond to the API and the method in SupervisorManager
/ Supervisor
classes?
Adding a api on the overlord to order a task group to checkpoint and exit early before taskDuration is met. This is helpful when trying to update old middle managers (by ending the existing tasks on them) and also could potentially let druid run streaming tasks indefinitely (since they can do intermediate checkpointing and can always be shutdown manually when needed).
Description
Fixed the bug ...
Renamed the class ...
Added a forbidden-apis entry ...
I thought integrating this into the checkTaskDuration flow was the best way to to this since it does everything needed during a regular handoff (when taskDuration is exceeded), and the logic we want here is exactly the same as what is done during regular handoff (as opposed to a intermediate handoff or a supervisor pause)
Release note
Add new API for telling streaming task groups to exit early.
Key changed/added classes in this PR
SupervisorManager
SupervisorResource
SeekableStreamSupervisor
This PR has: