Skip to content

Don't delete tasks until they're actually removed by the agent#2446

Closed
nishanttotla wants to merge 3 commits intomoby:masterfrom
nishanttotla:task-management-improvements
Closed

Don't delete tasks until they're actually removed by the agent#2446
nishanttotla wants to merge 3 commits intomoby:masterfrom
nishanttotla:task-management-improvements

Conversation

@nishanttotla
Copy link
Copy Markdown
Contributor

@nishanttotla nishanttotla commented Nov 15, 2017

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:

  1. Add a new task state called REMOVE.
  2. On service removals or scale downs, tasks that are due to be removed get updated to have desired state 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).
  3. The agent detects desired state REMOVE and initiates task shutdown.
  4. The task reaper deletes tasks that have desired state REMOVE and actual state >=SHUTDOWN

Things this PR does not deal with

  • Changing API output (do we want to be able to do docker service ps and see tasks in REMOVE state until they're deleted? What about scale downs?)
  • Updating the design docs

@codecov
Copy link
Copy Markdown

codecov bot commented Nov 15, 2017

Codecov Report

Merging #2446 into master will decrease coverage by 3.2%.
The diff coverage is 71.42%.

@@            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

@nishanttotla nishanttotla force-pushed the task-management-improvements branch 9 times, most recently from 51a93be to c70768d Compare November 16, 2017 21:27
@nishanttotla nishanttotla changed the title [WIP] Don't delete tasks until they're actually removed by the agent [Don't delete tasks until they're actually removed by the agent Nov 16, 2017
@nishanttotla nishanttotla changed the title [Don't delete tasks until they're actually removed by the agent Don't delete tasks until they're actually removed by the agent Nov 16, 2017
@stevvooe
Copy link
Copy Markdown
Contributor

@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 COMPLETED or SHUTDOWN, the ip address should be released. However, it seems we remove tasks before confirming their final state and that is likely the root cause of the bug.

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 ORPHANED or DEAD desired state to signal the task reaper to actually remove the task.

Before proceeding with a PR, it would be good to fully complete the design discussion so that we can find a proper solution.

Copy link
Copy Markdown
Collaborator

@dperny dperny left a comment

Choose a reason for hiding this comment

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

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())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not return an error here, and abort the transaction?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

It seems like we have this pattern elsewhere too, for instance in deleteTask just below this. Same for your comment below.

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.

I think its OK to revisit existing patterns. I agree with @dperny that we should fail the txn.

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")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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))
})
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

@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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

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.

Done.

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:])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you can remove the (*replicated.Orchestrator).deleteTasks method, because it's no longer used.

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.

Done.

}()

if task.DesiredState == api.TaskStateShutdown {
if task.DesiredState == api.TaskStateShutdown || task.DesiredState == api.TaskStateRemove {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not if task.DesiredState >= api.TaskStateShutdown, to encompass possible future desired states?

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.

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.

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.

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.

Copy link
Copy Markdown
Contributor

@anshulpundir anshulpundir left a comment

Choose a reason for hiding this comment

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

Initial comments. Will review further and add more.

}()

if task.DesiredState == api.TaskStateShutdown {
if task.DesiredState == api.TaskStateShutdown || task.DesiredState == api.TaskStateRemove {
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.

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
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.

Please move this above the comment for TaskStateOrphaned and add a comment for this state.

@nishanttotla nishanttotla force-pushed the task-management-improvements branch from 7975498 to 108aaef Compare November 20, 2017 23:37
@dperny
Copy link
Copy Markdown
Collaborator

dperny commented Nov 21, 2017

@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:
master...dperny:sequential-task-removal

@dperny
Copy link
Copy Markdown
Collaborator

dperny commented Nov 21, 2017

Ah, I see, Slot doesn't work like that.

@dperny
Copy link
Copy Markdown
Collaborator

dperny commented Nov 21, 2017

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.

@stevvooe
Copy link
Copy Markdown
Contributor

why do we need to remove from the assignment set?

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.

@nishanttotla
Copy link
Copy Markdown
Contributor Author

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.

})
}

func TestRemove(t *testing.T) {
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.

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.
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.

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) {
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.

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)
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.

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.
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.

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 {
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.

Why not do all the updates in the same txn ?

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.

@anshulpundir how do you mean?

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.

Nevermind. I realized that updates to the store are batched using store.Batch.

assert.Len(t, foundTasks, 4)
}

func TestTaskStateRemoveOnScaledown(t *testing.T) {
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.

Please add a comment specifying what the test is about and the steps involved.

assert.Len(t, foundTasks, 1)
}

func TestTaskStateRemoveOnServiceRemoval(t *testing.T) {
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.

Same as above

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 {
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.

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
})
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.

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"
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.

Please also comment on why we remove these tasks.

@nishanttotla nishanttotla force-pushed the task-management-improvements branch 4 times, most recently from 6e95d25 to 6e125f6 Compare November 22, 2017 21:59
@nishanttotla nishanttotla force-pushed the task-management-improvements branch 8 times, most recently from 1c5356f to 9a9ec6c Compare November 23, 2017 00:59
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
@nishanttotla nishanttotla force-pushed the task-management-improvements branch from 9a9ec6c to 618281c Compare November 23, 2017 01:00
@nishanttotla
Copy link
Copy Markdown
Contributor Author

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"];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

832 for consistency with the others?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think you missed the ORPHANED = 832 below, which makes this correct at halfway between REJECTED and ORPHANED.

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.

// Remove ...


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())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@dperny
Copy link
Copy Markdown
Collaborator

dperny commented Nov 27, 2017

i'm gonna carry this PR to completion for @nishanttotla because he's not available right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] Resolving the IP address contention issue

5 participants