>From times to times I write a scraper or some other tool that would authenticate to a service and then use the auth result to do stuff concurrently. But when auth expires, I need to synchronize all my goroutines and have a single one do the re-auth process, check the status, etc. and then arrange for all goroutines to go back to work using the new auth result.
To generalize the problem: multiple goroutines read a cached value that expires at some point. When it does, they all should block and some I/O operation has to be performed by a single goroutine to renew the cached value, then unblock all other goroutines and have them use the new value. I solved this in the past in a number of ways: having a single goroutine that handles the cache by asking it for the value through a channel, using sync.Cond (which btw every time I decide to use I need to carefully re-read its docs and do lots of tests because I never get it right at first). But what I came to do lately is to implement an upgradable lock and have every goroutine do: *<code>* func (x implem) getProtectedValue() (someType, error) { // acquires a read lock that can be upgraded lock := x.upgradableLock.UpgradableRLock() // the Unlock method of the returned lock does the right thing // even if we later upgrade the lock defer lock.Unlock() // here we have read access to x.protectedValue // if the cached value is no longer valid, upgrade the lock // and update it if !isValid(x.protectedValue) && lock.TryUpgrade() { // within this if, we know we *do* have write access // to x.protectedValue x.protectedValue, x.protectedValueError = newProtectedValue() } // here we can only say everyone has read access to x.protectedValue // (the goroutine that got the lock upgrade could still write // here but as this code is shared, we should check the result // of the previous lock.TryUpgrade() again) return x.protectedValue, x.protectedValueError } *</code>* The implementation is quite simple: *<code>* // Upgradable implements all methods of sync.RWMutex, plus a new // method to acquire a read lock that can optionally be upgraded // to a write lock. type Upgradable struct { readers sync.RWMutex writers sync.RWMutex } func (m *Upgradable) RLock() { m.readers.RLock() } func (m *Upgradable) TryRLock() bool { return m.readers.TryRLock() } func (m *Upgradable) RUnlock() { m.readers.RUnlock() } func (m *Upgradable) RLocker() sync.Locker { return m.readers.RLocker() } func (m *Upgradable) Lock() { m.writers.Lock() m.readers.Lock() } func (m *Upgradable) TryLock() bool { if m.writers.TryLock() { if m.readers.TryLock() { return true } m.writers.Unlock() } return false } func (m *Upgradable) Unlock() { m.readers.Unlock() m.writers.Unlock() } // UpgradableRLock returns a read lock that can optionally be // upgraded to a write lock. func (m *Upgradable) UpgradableRLock() *UpgradableRLock { m.readers.RLock() return &UpgradableRLock{ m: m, unlockFunc: m.RUnlock, } } // UpgradableRLock is a read lock that can be upgraded to a write // lock. This is acquired by calling (*Upgradable). // UpgradableRLock(). type UpgradableRLock struct { mu sync.Mutex m *Upgradable unlockFunc func() } // TryUpgrade will attempt to upgrade the acquired read lock to // a write lock, and return whether it succeeded. If it didn't // succeed then it will block until the goroutine that succeeded // calls Unlock(). After unblocking, the read lock will still be // valid until calling Unblock(). // // TryUpgrade panics if called more than once or if called after // Unlock. func (u *UpgradableRLock) TryUpgrade() (ok bool) { u.mu.Lock() defer u.mu.Unlock() if u.m == nil { panic("TryUpgrade can only be called once and not after Unlock") } if ok = u.m.writers.TryLock(); ok { u.m.readers.RUnlock() u.m.readers.Lock() u.unlockFunc = u.m.Unlock } else { u.m.readers.RUnlock() u.m.writers.RLock() u.unlockFunc = u.m.writers.RUnlock } u.m = nil return } // Unlock releases the lock, whether it was a read lock or a write // lock acquired by calling Upgrade. // // Unlock panics if called more than once. func (u *UpgradableRLock) Unlock() { u.mu.Lock() defer u.mu.Unlock() if u.unlockFunc == nil { panic("Unlock can only be called once") } u.unlockFunc() u.unlockFunc = nil u.m = nil } *</code>* I obviously try to avoid using it for other than protecting values that require potentially long I/O operations (like in my case re-authenticating to a web service) and also having a lot of interested goroutines. The good thing is that most of the time this pattern only requires one RLock, but the (*UpgradableRLock).Unlock requires an additional lock/unlock to prevent misusage of the upgradable lock (I could potentially get rid of it but preferred to keep it in the side of caution). Another thing I don't like is that I need to allocate for each UpgradableRLock. I'm thinking to re-define UpgradableRLock to be a defined type of Upgradable and convert the pointer type of the receiver to *Upgradable whenever needed (also getting rid of the lock that prevents misusage). I wanted to ask the community what do you think of this pattern, pros and cons, and whether this is overkill and could be better solved with something obvious I'm missing (even though I did my best searching). The reason why I decide to use this pattern is that all other options seem to make my code more complex and less readable. I generally just implement one method that does all the value-getting and it ends up being quite readable and easy to follow: 1. Get an upgradable lock 2. If the value is stale and I get to upgrade the lock, then update the value and store the error as well. The "update" part is generally implemented as a separate method or func called "updateThingLocked". 3. Return the value and the error. Kind regards. -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/0fe9c0f3-a703-45f3-88b7-bbfc29111b2en%40googlegroups.com.