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

internal/ethapi: add mutex around signing + nonce assignment #14516

Merged
merged 1 commit into from
May 30, 2017

Conversation

holiman
Copy link
Contributor

@holiman holiman commented May 25, 2017

Follow-up PR to #14483 .
This PR adds per-sender-account specific mutexes, and removes the pollution of global by placing it inside the AccountManager.

Also enables it for the personal.sendTransaction part of the api

func (am *Manager) LockNonce(address common.Address) {
if _, exist := am.nonceLock.locks[address]; !exist {
am.nonceLock.mut.Lock()
if _, exist := am.nonceLock.locks[address]; !exist {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can't be right, why are you doing this twice? The am.nonceLock.mut.Lock() should go on top with a defer ...Unlock().

Copy link
Contributor Author

@holiman holiman May 26, 2017

Choose a reason for hiding this comment

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

So, it's done twice so that in 99% of all cases, there's no need to even obtain the lock (if the noncelock for that address already exists, it will exit at the first check).

If the noncelock does not exist, it needs to lock the mut. And at that point, it needs to check again, because there may have been two threads doing that unsynchronized first check and now about to create the address-lock.

I don't want to use defer, because there's a tricky thing here, regarding scoping. I'm not sure when the deferred method is executed, but if it's at the end, I don't think it's very nice.

This is how I imagine a deferred will look like without syntactic sugar:

func (am *Manager) LockNonce(address common.Address) {
	if _, exist := am.nonceLock.locks[address]; !exist {
		am.nonceLock.mut.Lock()
		if _, exist := am.nonceLock.locks[address]; !exist {
			am.nonceLock.locks[address] = new(sync.Mutex)
		}
		
	}
	am.nonceLock.locks[address].Lock()
	am.nonceLock.mut.Unlock()
	return
}

The problem with that is that it will try to obtain the addresslock before having released the mut. So if some other process already has the specific addresslock locked, this will stall other callers trying to lock the nonce for totally separate addresses, since they will be unable to obtain mut.
Course, I may be wrong about how defer works (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tested a bit with defer and I think I'm right. But yeah, it could be written as

func (am *Manager) LockNonce(address common.Address) {
	am.nonceLock.mut.Lock()
	if _, exist := am.nonceLock.locks[address]; !exist {
		am.nonceLock.locks[address] = new(sync.Mutex)
	}
	am.nonceLock.mut.Unlock()
	am.nonceLock.locks[address].Lock()
	return
}

That's less code, but all callers will compete on that central lock.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're kind of going against Go's principles here though and I'm not sure whether that's a good idea.

defer is executed at the end of the function's scope, including return statement but excluding the actual returning (i.e. setting of return variables, both named and unnamed) and the nicety is that it will be executed regardless of where the return is.

The dual-check is really, really odd and there's an argument for infinite amount of these checks. I strongly suggest this is written as:

func (am *Manager) LockNonce(address common.Address) {
	am.nonceLock.mut.Lock()
        defer am.nonceLock.mut.Unlock()

	if _, exist := am.nonceLock.locks[address]; !exist {
		am.nonceLock.locks[address] = new(sync.Mutex)
	}
	am.nonceLock.locks[address].Lock()
}

IMO this is better code and less cause for confusion.

Copy link
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 like that example, for the precise reason that it keeps holding the mut lock needlessly, while trying to get the address-specific lock. But since you really want me to use the defer pattern, I'll refactor it into two methods instead, so we can avoid that.

Copy link
Contributor Author

@holiman holiman May 28, 2017

Choose a reason for hiding this comment

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

The dual-check is really, really odd and there's an argument for infinite amount of these checks.

No? Just two.

  1. Check existence (unsynched). If exists, goto 6.
  2. sync
  3. Check existence (synched). If exists, goto 6.
  4. create
  5. unsync
  6. xxx...

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI the check -> lock -> check -> create pattern is used in the recently merged sync.Map implementation (see here).

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Thanks.

@holiman
Copy link
Contributor Author

holiman commented May 28, 2017

I've changed it now, according to @obscuren 's comments. However, I still think that it is suboptimal the way it is right now, since it will lock the mut regardless if the account-specific lock exists or not (it strictly only needs to sync if it doesn't exist) . But it's not a big deal, I guess.

@obscuren
Copy link
Contributor

We can settle the argument if you can provide proof that your version is significantly more performant :-)

@holiman
Copy link
Contributor Author

holiman commented May 28, 2017

As long as I got away with releasing the outer lock before obtaining the inner lock, I'm can live with the way it is :)

@karalabe karalabe added this to the 1.6.2 milestone May 29, 2017
This prevents concurrent assignment of identical nonces when automatic
assignment is used.
@fjl fjl changed the title api/accounts: Implement per-account nonce mutex internal/ethapi: add mutex around signing + nonce assignment May 30, 2017
@fjl
Copy link
Contributor

fjl commented May 30, 2017

@holiman force-pushed the change we discussed off GitHub. Code changes are now almost entirely local to package ethapi, no new API surface is added.

@holiman
Copy link
Contributor Author

holiman commented May 30, 2017

LGTM!

@karalabe karalabe merged commit 41bdf49 into ethereum:master May 30, 2017
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.

None yet

5 participants