Skip to content
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

[manager/dispatcher] Synchronization fixes #2495

Merged
merged 1 commit into from
Feb 1, 2018
Merged

Conversation

anshulpundir
Copy link
Contributor

@anshulpundir anshulpundir commented Jan 30, 2018

Fixes for synchronizing the dispatcher shutdown with in-progress rpcs. This addresses the case where the Dispatcher.Register() rpc races with Dispatcher.Stop() and reinserts into the dispatcher node store after it has been cleaned up by Stop().

We'll hold off on changing the grace period for agents until we have tested this fix at scale.

@@ -125,8 +125,18 @@ type clusterUpdate struct {

// Dispatcher is responsible for dispatching tasks and tracking agent health.
type Dispatcher struct {
mu sync.Mutex
wg sync.WaitGroup
// Lock to provide mutually exclusive access to dispatcher fields
Copy link
Contributor

Choose a reason for hiding this comment

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

Start the comment with the name of the fields: // mu blah blah....

mu sync.Mutex
// Flag to indicate shutdown and prevent new operations on the dispatcher.
// Set by calling Stop().
shutdown bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to avoid using a tombstone channel?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Caught up with @stevvooe offline. I agree that using a tombstone is cleaner, but some of the operations don't use a select {} so using a bool saves some code.

@codecov
Copy link

codecov bot commented Jan 30, 2018

Codecov Report

Merging #2495 into master will increase coverage by 1.2%.
The diff coverage is 86.2%.

@@            Coverage Diff             @@
##           master    #2495      +/-   ##
==========================================
+ Coverage   61.17%   62.38%    +1.2%     
==========================================
  Files          49      129      +80     
  Lines        6898    21323   +14425     
==========================================
+ Hits         4220    13302    +9082     
- Misses       2243     6586    +4343     
- Partials      435     1435    +1000

// Set shutdown to true.
// This will prevent RPCs that start after stop() is called
// from making progress and essentially puts the dispatcher in drain.
d.shutdown = true

Choose a reason for hiding this comment

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

don't see any place where the shutdown is set back to false. Remember that the Dispatcher is not recreated but is reused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point! Will address this.

Choose a reason for hiding this comment

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

I'm a little worried by the fact that the CI was green...

Copy link
Contributor Author

@anshulpundir anshulpundir Jan 31, 2018

Choose a reason for hiding this comment

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

Yea, unfortunately the unit-test always creates a new dispatcher. I know.

Looking at adding a unit-test.

@anshulpundir anshulpundir changed the title [manager/dispatcher] Synchronization fixes [WIP] [manager/dispatcher] Synchronization fixes Jan 31, 2018
@dperny
Copy link
Collaborator

dperny commented Jan 31, 2018

use an RWMutex instead of a regular one. There's only one case that writes (the actual shutdown) and everything else is reads. It's an easy win for optimization. In addition, this will allow you to eliminate the waitgroup, because you can just RLock the RWMutex for the duration of the method call, and the Stop method won't be able to acquire a write lock until all readlocks are released. Make sense?

@anshulpundir
Copy link
Contributor Author

Using a RWMutex was the initial approach. Here's why I decided to use a wait group:

  1. Using a WaitGroup alone is perhaps more performant than a RWLock and somewhat simpler (in the stop() function you just need to call wait() as opposed to grabbing a write lock) and somewhat more intuitive.
  2. There was already a waitgroup on the dispatcher struct, so reusing that was an easy win.

I realized that the shutdown flag is not really needed since we can inspect the dispatcher context to signal shutdown. This code is much simpler and also inline with the two points above.

@@ -1137,6 +1157,9 @@ func (d *Dispatcher) getRootCACert() []byte {
// a special boolean field Disconnect which if true indicates that node should
// reconnect to another Manager immediately.
func (d *Dispatcher) Session(r *api.SessionRequest, stream api.Dispatcher_SessionServer) error {
d.shutdownWait.Add(1)
Copy link
Contributor

Choose a reason for hiding this comment

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

@anshulpundir here and in a couple of places above, why isn't the check on d.shutdown required?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually nevermind, after your most recent comment, this is not relevant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're using the Dispatcher context to signal shutdown.

Note the order: we always increment the waitgroup first, followed by isRunningLocked(). If an RPC finds the context not cancelled, it will have +1 the WaitGroup, which will make sure that the Stop() function waits for this rpc to finish.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say that this is slightly clever, so make sure to document it somewhere that the ordering is what does the trick. It'll be hard to read later otherwise.

d.shutdownWait.Add(1)
defer d.shutdownWait.Done()

if !d.isRunning() {

Choose a reason for hiding this comment

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

This is the version with no lock, so can potentially race with the shutdown, is it a problem?
If not can you add a comment saying the reason why it is fine like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its not because Stop() will wait for Heardbeat() to complete since it has already incremented the waitgroup. Added a comment.

Choose a reason for hiding this comment

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

The wait of the WG is not the first operation of the Stop so the check for running here is racing with the beginning of the stop

Choose a reason for hiding this comment

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

as I was saying maybe is not a problem but with this change you would have start the stop function and still process an extra heartbeat because the check isRunning check the bool with no lock

Copy link
Contributor Author

@anshulpundir anshulpundir Feb 1, 2018

Choose a reason for hiding this comment

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

with this change you would have start the stop function and still process an extra heartbeat because the check isRunning check the bool with no lock

True. However is no correctness issue and the win is that we don't need to get a lock in Heartbeat(), which is the most frequent call on the dispatcher.

Signed-off-by: Anshul Pundir <anshul.pundir@docker.com>
Copy link

@fcrisciani fcrisciani left a comment

Choose a reason for hiding this comment

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

LGTM

@dperny
Copy link
Collaborator

dperny commented Feb 1, 2018

Alright, LGTM, we're shipping it.

@nishanttotla
Copy link
Contributor

There are some potential races introduced by this PR causing tests to fail on moby/moby. We may have to fix that: moby/moby#36274

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants