On Sun, Jan 29, 2023 at 7:34 PM Diego Augusto Molina <
diegoaugustomol...@gmail.com> wrote:

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


So how about using sync.Once:

type CacheEntry struct {
  Auth AuthInfo
  once sync.Once
}

Cache would be a map[CacheKey]*CacheEntry. When AuthInfo expires, you
simply replace it with a new one:

func (c *Cache) Expire(key CacheKey) {
  c.Lock()
  defer c.Unlock()
  c.cache[key]=&CacheEntry{}
}

func (c *Cache) Get(key CacheKey) Auth {
   c.RLock()
   entry:=c.cache[key]
    c.RUnlock()
    if entry==nil {
          c.Lock()
          entry=c.cache[key]
          if entry==nil {
             c.cache[key]=&CacheEntry{}
          }
          c.UnLock()
   }
    entry.Do(func() {
            // Authenticate
    })
    return entry.Auth
}



>
> *<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
> <https://groups.google.com/d/msgid/golang-nuts/0fe9c0f3-a703-45f3-88b7-bbfc29111b2en%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>

-- 
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/CAMV2RqqorOvzFsFKObrBbRwEzrns-p6CrKhyn5_M8_THN-cvTQ%40mail.gmail.com.

Reply via email to