Don't delete tasks until they're actually removed by the agent#2446
Don't delete tasks until they're actually removed by the agent#2446nishanttotla wants to merge 3 commits intomoby:masterfrom
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2446 +/- ##
==========================================
- Coverage 63.74% 60.53% -3.21%
==========================================
Files 64 128 +64
Lines 11793 26426 +14633
==========================================
+ Hits 7517 15996 +8479
- Misses 3662 9017 +5355
- Partials 614 1413 +799 |
51a93be to
c70768d
Compare
|
@nishanttotla As I stated in the other issue, adding a new state for removal doesn't make a whole lot of sense. There is already the functionality of removing the task from the assignment set that activates the removal workflow. We can tell that this state is unnecessary, because it is just a noop on the agent and no actual removal is happening. States are meant to set a target for a state and then the agent achieves that target. The main problem that is addressed here is that the orchestrator needs to confirm shutdown (some sort of ack) before releasing the ip address to another container. If a task is reported as What I am not understanding with this PR is why you think you need a removal state. From the changes to the agent, it is clear that the shutdown (or completed, if it is already down) state is sufficient. The correct solution here is to remove these tasks from the assignment set, so they are not sent down to the agent. The agent will proceed with the remove. You can see part of this logic here. If you want acknowledgement of cleanup, you don't need to do this. Simply set the target state to shutdown, and the task will get shutdown and release resources (like ip address). You may also be able to use the existing Before proceeding with a PR, it would be good to fully complete the design discussion so that we can find a proper solution. |
dperny
left a comment
There was a problem hiding this comment.
without yet addressing stephen's comments, here's a rough overview of the PR. haven't reviewed tests, and not solid on the overall design.
| t.DesiredState = state | ||
|
|
||
| if err := store.UpdateTask(tx, t); err != nil { | ||
| log.G(ctx).WithError(err).Errorf("failed to update task desired state to %s", state.String()) |
There was a problem hiding this comment.
why not return an error here, and abort the transaction?
There was a problem hiding this comment.
there might be a good reason not to, but logging an error is functionally the same as ignoring it altogether, except that you have some context when it breaks.
There was a problem hiding this comment.
It seems like we have this pattern elsewhere too, for instance in deleteTask just below this. Same for your comment below.
There was a problem hiding this comment.
I think its OK to revisit existing patterns. I agree with @dperny that we should fail the txn.
manager/orchestrator/service.go
Outdated
| t.DesiredState = api.TaskStateRemove | ||
|
|
||
| if err := store.UpdateTask(tx, t); err != nil { | ||
| log.G(ctx).WithError(err).Errorf("failed to update task desired state to REMOVE") |
There was a problem hiding this comment.
again here, why not return the error and abort the transaction?
| ) | ||
| s.View(func(tx store.ReadTx) { | ||
| tasks, err = store.FindTasks(tx, store.ByServiceID(service.ID)) | ||
| }) |
There was a problem hiding this comment.
there's a possible race here between the view and the batch below, i think. fix might be to move the view into the batch but before the loop below. not sure if we use this pattern elsewhere.
There was a problem hiding this comment.
@dperny same pattern as in DeleteServiceTasks above this code. I see your point, but does this mean we've missed this race condition in the past?
| return | ||
| } | ||
| orchestrator.DeleteServiceTasks(ctx, r.store, v.Service) | ||
| orchestrator.SetServiceTasksRemove(ctx, r.store, v.Service) |
There was a problem hiding this comment.
You need to make the same change in github.com/swarmkit/manager/orchestrator/global/global.go L150.
Then, you can remove the DeleteServiceTasks method, because it has no other uses in the codebase.
| r.updater.Update(ctx, r.cluster, service, sortedSlots[:specifiedSlots]) | ||
| err = r.store.Batch(func(batch *store.Batch) error { | ||
| r.deleteTasksMap(ctx, batch, deadSlots) | ||
| r.deleteTasks(ctx, batch, sortedSlots[specifiedSlots:]) |
There was a problem hiding this comment.
you can remove the (*replicated.Orchestrator).deleteTasks method, because it's no longer used.
agent/exec/controller.go
Outdated
| }() | ||
|
|
||
| if task.DesiredState == api.TaskStateShutdown { | ||
| if task.DesiredState == api.TaskStateShutdown || task.DesiredState == api.TaskStateRemove { |
There was a problem hiding this comment.
why not if task.DesiredState >= api.TaskStateShutdown, to encompass possible future desired states?
There was a problem hiding this comment.
I don't have a solid answer for this yet, I believe I did the exact comparison since the previous condition was an exact comparison itself.
There was a problem hiding this comment.
I think you can go with >= api.TaskStateShutdown here. This branch bounds the largest state achievable in the agent as SHUTDOWN, which is the behavior we want.
anshulpundir
left a comment
There was a problem hiding this comment.
Initial comments. Will review further and add more.
agent/exec/controller.go
Outdated
| }() | ||
|
|
||
| if task.DesiredState == api.TaskStateShutdown { | ||
| if task.DesiredState == api.TaskStateShutdown || task.DesiredState == api.TaskStateRemove { |
There was a problem hiding this comment.
Please add a comment.
api/types.pb.go
Outdated
| // The main purpose of this state is to free up resources associated with service tasks on | ||
| // unresponsive nodes without having to delete those tasks. This state is directly assigned | ||
| // to the task by the orchestrator. | ||
| TaskStateRemove TaskState = 800 |
There was a problem hiding this comment.
Please move this above the comment for TaskStateOrphaned and add a comment for this state.
7975498 to
108aaef
Compare
|
@stevvooe @nishanttotla why do we need to remove from the assignment set? why is it not sufficient to simply set the task state to "shutdown", then let the task reaper clean up the task when it gets reported as shutdown? Here is a quick diff of what I'm talking about. No tests, not sure it even builds, just trying to convey the idea: |
|
Ah, I see, |
|
Updated the diff of my counterproposal. Check if all tasks in a slot are dead, and, if so, delete all the tasks in that slot. |
I am not sure that it is necessary, either. I was trying to communicate that "removal" is proxied through removal from the assignment set. Looking at this closer, having shutdown confirmation seems like the first step (which I hope is already working). It seems like the missing component here is acknowledgement that the allocator has returned the tasks resources to the pool before removing the task. |
|
Based on our offline discussion, I'm going to hold off on updating the state name until we all can agree on it. In the meantime, I'll make sure this PR works right. I'll also make sure I update design docs as an immediate follow up. |
agent/exec/controller_test.go
Outdated
| }) | ||
| } | ||
|
|
||
| func TestRemove(t *testing.T) { |
There was a problem hiding this comment.
TestDesiredStateRemove
Include a comment explaining that we are testing that the agent maintains SHUTDOWN as the maximum state in the agent.
api/types.proto
Outdated
| // The main purpose of the REMOVE state is to correctly handle service deletions | ||
| // and scale downs. This allows us to keep track of tasks that have been marked | ||
| // for deletion, but can't yet be removed because the agent is in the process of | ||
| // shutting them down. |
There was a problem hiding this comment.
We should also mention that tasks marked with this states are removed from the system once they have been shut down by the agent.
| @@ -50,7 +50,7 @@ func (r *Orchestrator) handleServiceEvent(ctx context.Context, event events.Even | |||
| if !orchestrator.IsReplicatedService(v.Service) { | |||
There was a problem hiding this comment.
unrelated by not totally: Please add a comment for this case.
| err = r.store.Batch(func(batch *store.Batch) error { | ||
| r.deleteTasksMap(ctx, batch, deadSlots) | ||
| r.deleteTasks(ctx, batch, sortedSlots[specifiedSlots:]) | ||
| r.setTasksDesiredState(ctx, batch, sortedSlots[specifiedSlots:], api.TaskStateRemove) |
There was a problem hiding this comment.
Please add a comment.
For bonus points, you can also add comments for the reconcile() function :)
|
|
||
| func (r *Orchestrator) deleteTasks(ctx context.Context, batch *store.Batch, slots []orchestrator.Slot) { | ||
| // setTasksDesiredState sets the desired state of all tasks to what | ||
| // is requested. |
There was a problem hiding this comment.
sets the desired state of all tasks to what is requested => sets the desired state for all tasks for the given slots to the given state.
| for _, slot := range slots { | ||
| for _, t := range slot { | ||
| r.deleteTask(ctx, batch, t) | ||
| err := batch.Update(func(tx store.Tx) error { |
There was a problem hiding this comment.
Why not do all the updates in the same txn ?
There was a problem hiding this comment.
Nevermind. I realized that updates to the store are batched using store.Batch.
| assert.Len(t, foundTasks, 4) | ||
| } | ||
|
|
||
| func TestTaskStateRemoveOnScaledown(t *testing.T) { |
There was a problem hiding this comment.
Please add a comment specifying what the test is about and the steps involved.
| assert.Len(t, foundTasks, 1) | ||
| } | ||
|
|
||
| func TestTaskStateRemoveOnServiceRemoval(t *testing.T) { |
| tr.orphaned = append(tr.orphaned, t.ID) | ||
| } | ||
| // add tasks that have been shutdown and marked "remove" | ||
| if t.DesiredState == api.TaskStateRemove && t.Status.State >= api.TaskStateShutdown { |
There was a problem hiding this comment.
nit: Please update comments for Run() above for this change.
| log.G(ctx).WithError(err).Errorf("failed to update task desired state to REMOVE") | ||
| } | ||
| return nil | ||
| }) |
There was a problem hiding this comment.
The log statement here https://github.com/docker/swarmkit/pull/2446/files#diff-93afa918180cbe7ddd6db3b78a89fb8aR64 doesn't seem accurate. Its the failure of task update txn.
| if t.Status.State >= api.TaskStateOrphaned && t.ServiceID == "" { | ||
| tr.orphaned = append(tr.orphaned, t.ID) | ||
| } | ||
| // add tasks that have been shutdown and marked "remove" |
There was a problem hiding this comment.
Please also comment on why we remove these tasks.
6e95d25 to
6e125f6
Compare
1c5356f to
9a9ec6c
Compare
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
9a9ec6c to
618281c
Compare
|
I've address most of the above comments. |
| // for deletion, but can't yet be removed because the agent is in the process of | ||
| // shutting them down. Once the agent has shut down tasks with desired state | ||
| // REMOVE, the task reaper is responsible for removing them. | ||
| REMOVE = 800 [(gogoproto.enumvalue_customname)="TaskStateRemove"]; |
There was a problem hiding this comment.
832 for consistency with the others?
There was a problem hiding this comment.
i think you missed the ORPHANED = 832 below, which makes this correct at halfway between REJECTED and ORPHANED.
|
|
||
| err := store.UpdateTask(tx, t) | ||
| if err != nil { | ||
| log.G(ctx).WithError(err).Errorf("failed to update task %s desired state to %s", t.ID, state.String()) |
There was a problem hiding this comment.
This log statement seems redundant with the one below.
| r.deleteTask(ctx, batch, t) | ||
| err := batch.Update(func(tx store.Tx) error { | ||
| // update desired state | ||
| t.DesiredState = state |
There was a problem hiding this comment.
It's good practice to make sure that the state is being advanced, and never moves backwards. Currently there isn't a state beyond "remove" but we could potentially add one later.
| if err := store.DeleteTask(tx, t.ID); err != nil { | ||
| log.G(ctx).WithError(err).Errorf("failed to delete task") | ||
| // update desired state to REMOVE | ||
| t.DesiredState = api.TaskStateRemove |
There was a problem hiding this comment.
It's good practice to make sure that the state is being advanced, and never moves backwards. Currently there isn't a state beyond "remove" but we could potentially add one later.
|
i'm gonna carry this PR to completion for @nishanttotla because he's not available right now. |
This PR is a proposed way to fix #2407. We may choose to solve the same problem differently, but here's what this PR does:
REMOVE.REMOVE(as opposed to just straight up deleting them like the existing behavior. This existing behavior causes networking issues as detailed in [Proposal] Resolving the IP address contention issue #2407, and also reporting issues such as service ps not showing all instances after scaling down moby#34967).REMOVEand initiates task shutdown.REMOVEand actual state>=SHUTDOWNThings this PR does not deal with
docker service psand see tasks inREMOVEstate until they're deleted? What about scale downs?)