pax_global_header00006660000000000000000000000064131147771470014526gustar00rootroot0000000000000052 comment=f30b893aa41460885e674be9f82fd79190e0759a golang-github-karrick-goswarm-1.4.7/000077500000000000000000000000001311477714700173675ustar00rootroot00000000000000golang-github-karrick-goswarm-1.4.7/.gitignore000066400000000000000000000004121311477714700213540ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof golang-github-karrick-goswarm-1.4.7/LICENSE000066400000000000000000000020621311477714700203740ustar00rootroot00000000000000MIT License Copyright (c) 2016 Karrick McDermott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-karrick-goswarm-1.4.7/README.md000066400000000000000000000133711311477714700206530ustar00rootroot00000000000000# goswarm Go Stale While Asynchronously Revalidate Memoization ## DESCRIPTION goswarm is a library written in Go for storing the results of expensive function calls and returning the cached result when the same input key occurs again. In addition to the examples provided below, documentation can be found at [![GoDoc](https://godoc.org/github.com/karrick/goswarm?status.svg)](https://godoc.org/github.com/karrick/goswarm). ```Go simple, err := goswarm.NewSimple(nil) if err != nil { log.Fatal(err) } // you can store any Go type in a Swarm simple.Store("someKeyString", 42) simple.Store("anotherKey", struct{}{}) simple.Store("yetAnotherKey", make(chan interface{})) // but when you retrieve it, you are responsible to perform type assertions key := "yetAnotherKey" value, ok := simple.Load(key) if !ok { panic(fmt.Errorf("cannot find %q", key)) } value = value.(chan interface{}) simple.Delete("anotherKey") ``` As seen above, goswarm's API is similar to that of any associative array, also known as a map in Go terminology: It allows storing a value to be retrieved using a specified key, and overwriting any value that might already be present in the map. It allows loading the value associated with the specified key already stored in the map. It allows deleting a specified key and its associated value stored in the map. goswarm differs from most traditional associative array APIs in that it provides a method to load the value associated with a specified key, and if that key is not found in the map, to invoke a specified lookup function to fetch the value for that key. ```Go simple, err := goswarm.NewSimple(&goswarm.Config{ Lookup: func(key string) (interface{}, error) { // TODO: do slow calculation or make a network call result := key // example return result, nil }, }) if err != nil { log.Fatal(err) } defer func() { _ = simple.Close() }() value, err := simple.Query("%version") if !err { panic(fmt.Errorf("cannot retrieve value for key %q: %s", key, err)) } fmt.Printf("The value is: %v\n", value) ``` ## Stale-While-Revalidate and Stale-If-Error In addition, goswarm provides stale-while-revalidate and stale-if-error compatible features in its simple API. ### Stale-While-Revalidate When the user requests the value associated with a particular key, goswarm determines whether that key-value pair is present, and if so, whether that value has become stale. If the value is stale, goswarm will return the previously stored value to the client, but spawn an asynchronous routine to fetch a new value for that key and store that value in the map to be used for future queries. When the user requests the value associated with a particular key that has expired, goswarm will not return that value, but rather synchronously fetch an updated value to store in the map and return to the user. ### Stale-If-Error When fetching a new value to replace a value that has become stale, the lookup callback funciton might return an error. Perhaps the remote network resource used to fetch responses is offline. In these cases, goswarm will not overwrite the stale value with the error, but continue to serve the stale value until the lookup callback function returns a new value rather than an error, or the value expires, in which case, the error is returned to the user. ## Periodic Removal Of Expired Keys If `GCPeriodicty` configuration value is greater than the zero-value for time.Duration, goswarm spawns a separate go-routine that invokes the `GC` method periodically, removing all key-value pairs from the data map that have an expired time. When this feature is used, the `Close` method must be invoked to stop and release that go-routine. ```Go simple, err := goswarm.NewSimple(&goswarm.Config{ GoodExpiryDuration: 24 * time.Hour, BadExpiryDuration: 5 * time.Minute, GCPeriodicity: time.Hour, Lookup: func(key string) (interface{}, error) { // TODO: do slow calculation or make a network call result := key // example return result, nil }, }) if err != nil { log.Fatal(err) } defer func() { _ = simple.Close() }() ``` ## Note About Choosing Stale and Expiry Durations Different applications may require different logic, however if your application needs to continue processing data and serving requests even when a downstream dependency is subject to frequent high latency periods or faults, it is recommended to set the GoodStaleDuration period to a low enough value to ensure data is reasonably up-to-date, but extend the GoodExpiryDuration to be long enough that your application can still operate using possibly stale data. The goswarm library will repeatedly attempt to fetch a new value from the downstream service, but until a defined very long period of time transpires, your service will be relatively insulated from these type of downstream faults and latencies. ```Go simple, err := goswarm.NewSimple(&goswarm.Config{ GoodStaleDuration: time.Minute, GoodExpiryDuration: 24 * time.Hour, BadStaleDuration: time.Minute, BadExpiryDuration: 5 * time.Minute, GCPeriodicity: time.Hour, Lookup: func(key string) (interface{}, error) { // TODO: do slow calculation or make a network call result := key // example return result, nil }, }) if err != nil { log.Fatal(err) } defer func() { _ = simple.Close() }() value, err := simple.Query("%version") if !err { panic(fmt.Errorf("cannot retrieve value for key %q: %s", key, err)) } fmt.Printf("The value is: %v\n", value) ``` golang-github-karrick-goswarm-1.4.7/goswarm.go000066400000000000000000000036041311477714700214000ustar00rootroot00000000000000package goswarm import "time" // Querier specifies a type that provides memoizing the results of expensive function calls and // returning the cached result when the same input key occurs again. type Querier interface { Query(string) (interface{}, error) } // Config specifies the configuration parameters for a Swarm instance. type Config struct { // GoodStaleDuration specifies how long a value remains fresh in the data map. A zero-value // time.Duration value implies the value never stales, and updates will only be fetched // after the value expires. GoodStaleDuration time.Duration // GoodExpiryDuration specifies how long a value is allowed to be served from the data // map. A zero-value time.Duration value implies the value never expires and can always be // served. GoodExpiryDuration time.Duration // BadStaleDuration specifies how long an error result remains fresh in the data map. A // zero-value time.Duration value implies that the error result is stale as soon as it's // inserted into the data map, and Querying the key again will trigger an asynchronous // lookup of the key to try to replace the error value with a good value. BadStaleDuration time.Duration // BadExpiryDuration specifies how long an error result is allowed to be served from the // data map. A zero-value time.Duration value implies the value never expires and can always // be served. BadExpiryDuration time.Duration // Lookup specifies the user callback function to invoke when looking up the value to be // associated with a stale key, expired key, or a key that has yet to be loaded into the // data map. Lookup func(string) (interface{}, error) // GCPeriodicity specifies how frequently the data map purges expired entries. GCPeriodicity time.Duration // GCTimeout specifies how long the GC should wait for values to unlock. GCTimeout time.Duration } const defaultGCTimeout = 10 * time.Second golang-github-karrick-goswarm-1.4.7/simple.go000066400000000000000000000311101311477714700212030ustar00rootroot00000000000000package goswarm import ( "errors" "fmt" "sync" "sync/atomic" "time" ) // Simple memoizes responses from a Querier, providing very low-level // time-based control of how values go stale or expire. When a new value is // stored in the Simple instance, if it is a TimedValue item--or a pointer to a // TimedValue item)--the data map will use the provided Stale and Expiry // values. If the new value is not a TimedValue instance or pointer to a // TimedValue instance, then the Simple instance wraps the value in a // TimedValue struct, and adds the Simple instance's stale and expiry durations // to the current time and stores the resultant TimedValue instance. type Simple struct { config *Config data map[string]*atomicTimedValue lock sync.RWMutex halt chan struct{} closeError chan error gcFlag int32 } // NewSimple returns Swarm that attempts to respond to Query methods by // consulting its TTL cache, then directing the call to the underlying Querier // if a valid response is not stored. Note this function accepts a pointer so // creating an instance with defaults can be done by passing a nil value rather // than a pointer to a Config instance. // // simple, err := goswarm.NewSimple(&goswarm.Simple{ // GoodStaleDuration: time.Minute, // GoodExpiryDuration: 24 * time.Hour, // BadStaleDuration: time.Minute, // BadExpiryDuration: 5 * time.Minute, // Lookup: func(key string) (interface{}, error) { // // TODO: do slow calculation or make a network call // result := key // example // return result, nil // }, // }) // if err != nil { // log.Fatal(err) // } // defer func() { _ = simple.Close() }() func NewSimple(config *Config) (*Simple, error) { if config == nil { config = &Config{} } if config.GoodStaleDuration < 0 { return nil, fmt.Errorf("cannot create Swarm with negative good stale duration: %v", config.GoodStaleDuration) } if config.GoodExpiryDuration < 0 { return nil, fmt.Errorf("cannot create Swarm with negative good expiry duration: %v", config.GoodExpiryDuration) } if config.GoodStaleDuration > 0 && config.GoodExpiryDuration > 0 && config.GoodStaleDuration >= config.GoodExpiryDuration { return nil, fmt.Errorf("cannot create Swarm with good stale duration not less than good expiry duration: %v; %v", config.GoodStaleDuration, config.GoodExpiryDuration) } if config.BadStaleDuration < 0 { return nil, fmt.Errorf("cannot create Swarm with negative bad stale duration: %v", config.BadStaleDuration) } if config.BadExpiryDuration < 0 { return nil, fmt.Errorf("cannot create Swarm with negative bad expiry duration: %v", config.BadExpiryDuration) } if config.BadStaleDuration > 0 && config.BadExpiryDuration > 0 && config.BadStaleDuration >= config.BadExpiryDuration { return nil, fmt.Errorf("cannot create Swarm with bad stale duration not less than bad expiry duration: %v; %v", config.BadStaleDuration, config.BadExpiryDuration) } if config.GCPeriodicity < 0 { return nil, fmt.Errorf("cannot create Swarm with negative GCPeriodicity duration: %v", config.GCPeriodicity) } if config.GCTimeout < 0 { return nil, fmt.Errorf("cannot create Swarm with negative GCTimeout duration: %v", config.GCTimeout) } if config.GCTimeout == 0 { config.GCTimeout = defaultGCTimeout } if config.Lookup == nil { config.Lookup = func(_ string) (interface{}, error) { return nil, errors.New("no lookup defined") } } s := &Simple{ config: config, data: make(map[string]*atomicTimedValue), } if config.GCPeriodicity > 0 { s.halt = make(chan struct{}) s.closeError = make(chan error) go s.run() } return s, nil } // Close releases all memory and go-routines used by the Simple swarm. If // during instantiation, GCPeriodicity was greater than the zero-value for // time.Duration, this method may block while completing any in progress GC run. func (s *Simple) Close() error { if s.config.GCPeriodicity > 0 { close(s.halt) return <-s.closeError } return nil } // Delete removes the key and associated value from the data map. func (s *Simple) Delete(key string) { s.lock.RLock() _, ok := s.data[key] s.lock.RUnlock() if !ok { // If key is not in the data map then there is nothing to delete. return } // Element is in data map, but need to acquire exclusive map lock before we // delete it. s.lock.Lock() delete(s.data, key) s.lock.Unlock() } type gcPair struct { key string doomed bool } // GC examines all key value pairs in the Simple swarm and deletes those whose // values have expired. func (s *Simple) GC() { // Bail if another GC thread is already running. This may happen // automatically when GCPeriodicity is shorter than GCTimeout, or when user // manually invokes GC method. if !atomic.CompareAndSwapInt32(&s.gcFlag, 0, 1) { return } defer atomic.StoreInt32(&s.gcFlag, 0) // MARK PHASE s.lock.RLock() // Create asynchronous goroutines to collect each key-value pair // individually, so overall mark phase task does not block waiting for any // of the key locks. We use a channel to collect key-value pair results in // order to serialize the parallel collection of pairs. // Ultimately, however, we do not desire to spend more than a specified // duration of time collecting key-value pairs during the mark phase, so a // context is created with a deadline to allow for early termination of the // mark phase. This logic does allow some key-value pairs to remain expired // after their eviction time, but with a long enough GCTimeout it is likely // that those evicted key-value pairs will be eventually collected during a // future GC run. // Although context.WithTimeout or context.WithDeadline _could_ be used // below, we do not desire to require Go 1.7 or above when a simple // time.Timer channel will do the job. now := time.Now() timeoutC := time.After(s.config.GCTimeout) // Create a buffered channel large enough to receive all key-value pairs so // that in the event of early mark phase termination due to timeout, the // goroutines created below will not block on sending to a full channel that // is no longer consumed after mark phase has ended. totalCount := len(s.data) allPairs := make(chan gcPair, totalCount) // Loop through all existing key-value pairs in the cache, creating // goroutines for each pair to individually wait for the respective key // lock, test the eviction logic, and send the result to the results // channel. for key, atv := range s.data { go func(key string, atv *atomicTimedValue, allPairs chan<- gcPair) { if av := atv.av.Load(); av != nil { allPairs <- gcPair{ key: key, doomed: av.(*TimedValue).isExpired(now), } } }(key, atv, allPairs) } // After looping through all key-value pairs, we no longer need to hold the // read lock for the cache while waiting for the results to arrive. s.lock.RUnlock() // COLLECT PHASE: Spawn goroutine to collect locked key-value pairs. var doomed []string var receivedCount int loop: for { select { case <-timeoutC: // The above channel is closed when either the timeout has expired // or when the number of received pairs equals the number of // key-value pairs in the cache, done manually below by calling // `cancel()`. break loop case pair := <-allPairs: if pair.doomed { doomed = append(doomed, pair.key) } // Once all key-value pairs have been received we can terminate the // collection phase. if receivedCount++; receivedCount == totalCount { break loop } } } // SWEEP PHASE: Grab the write lock and delete all doomed key-value pairs // from the cache. s.lock.Lock() for _, key := range doomed { delete(s.data, key) } s.lock.Unlock() } // Load returns the value associated with the specified key, and a boolean value // indicating whether or not the key was found in the map. func (s *Simple) Load(key string) (interface{}, bool) { // Do not want to use getOrCreateLockingTimeValue, because there's no reason // to create ATV if key is not present in data map. s.lock.RLock() atv, ok := s.data[key] s.lock.RUnlock() if !ok { return nil, false } av := atv.av.Load() if av == nil { // Element either recently erased by another routine while this method // was waiting for element lock above, or has not been populated by // fetch, in which case the value is not really there yet. return nil, false } return av.(*TimedValue).Value, true } // Query loads the value associated with the specified key from the data // map. When a stale value is found on Query, at most one asynchronous lookup of // a new value is triggered, and the current value is returned from the data // map. When no value or an expired value is found on Query, a synchronous // lookup of a new value is triggered, then the new value is stored and // returned. func (s *Simple) Query(key string) (interface{}, error) { atv := s.getOrCreateAtomicTimedValue(key) av := atv.av.Load() if av == nil { tv := s.update(key, atv) return tv.Value, tv.Err } else { now := time.Now() tv := av.(*TimedValue) if tv.isExpired(now) { tv = s.update(key, atv) } else if tv.isStale(now) { // If no other goroutine is looking up this value, spin one off if atomic.CompareAndSwapInt32(&atv.pending, 0, 1) { go func() { defer atomic.StoreInt32(&atv.pending, 0) _ = s.update(key, atv) }() } } return tv.Value, tv.Err } } // Range invokes specified callback function for each non-expired key in the // data map. Each key-value pair is independently locked until the callback // function invoked with the specified key returns. This method does not block // access to the Simple instance, allowing keys to be added and removed like // normal even while the callbacks are running. func (s *Simple) Range(callback func(key string, value *TimedValue)) { // Need to have read lock while enumerating key-value pairs from map s.lock.RLock() for key, atv := range s.data { // Now that we have a key-value pair from the map, we can release the // map's lock to prevent blocking other routines that need it. s.lock.RUnlock() if av := atv.av.Load(); av != nil { // We have an element. If it's not yet expired, invoke the user's // callback with the key and value. if tv := av.(*TimedValue); !tv.IsExpired() { callback(key, tv) } } // After callback is done with element, re-acquire map-level lock before // we grab the next key-value pair from the map. s.lock.RLock() } s.lock.RUnlock() } // Store saves the key-value pair to the cache, overwriting whatever was // previously stored. func (s *Simple) Store(key string, value interface{}) { atv := s.getOrCreateAtomicTimedValue(key) tv := newTimedValue(value, nil, s.config.GoodStaleDuration, s.config.GoodExpiryDuration) atv.av.Store(tv) } // Update forces an update of the value associated with the specified key. func (s *Simple) Update(key string) { atv := s.getOrCreateAtomicTimedValue(key) s.update(key, atv) } //////////////////////////////////////// func (s *Simple) run() { for { select { case <-time.After(s.config.GCPeriodicity): s.GC() case <-s.halt: s.closeError <- nil // there is no cleanup required, so we just return return } } } func (s *Simple) getOrCreateAtomicTimedValue(key string) *atomicTimedValue { s.lock.RLock() atv, ok := s.data[key] s.lock.RUnlock() if !ok { s.lock.Lock() // check whether value filled while waiting for exclusive access to map // lock atv, ok = s.data[key] if !ok { atv = new(atomicTimedValue) s.data[key] = atv } s.lock.Unlock() } return atv } // The update method attempts to update a new value for the specified key. If // the update is successful, it stores the value in the TimedValue associated // with the key. func (s *Simple) update(key string, atv *atomicTimedValue) *TimedValue { staleDuration := s.config.GoodStaleDuration expiryDuration := s.config.GoodExpiryDuration value, err := s.config.Lookup(key) if err == nil { tv := newTimedValue(value, err, staleDuration, expiryDuration) atv.av.Store(tv) return tv } // lookup gave us an error staleDuration = s.config.BadStaleDuration expiryDuration = s.config.BadExpiryDuration // new error overwrites previous error, and also used when initial value av := atv.av.Load() if av == nil || av.(*TimedValue).Err != nil { tv := newTimedValue(value, err, staleDuration, expiryDuration) atv.av.Store(tv) return tv } // received error this time, but still have old value, and we only replace a // good value with an error if the good value has expired tv := av.(*TimedValue) if tv.IsExpired() { tv = newTimedValue(value, err, staleDuration, expiryDuration) atv.av.Store(tv) } return tv } golang-github-karrick-goswarm-1.4.7/simple_test.go000066400000000000000000000263471311477714700222620ustar00rootroot00000000000000package goswarm import ( "errors" "fmt" "math/rand" "strconv" "strings" "sync" "sync/atomic" "testing" "time" ) func ensureErrorL(t *testing.T, swr *Simple, key, expectedError string) { value, err := swr.Query(key) if value != nil { t.Errorf("Actual: %v; Expected: %v", value, nil) } if err == nil || !strings.Contains(err.Error(), expectedError) { t.Errorf("Actual: %v; Expected: %s", err, expectedError) } } func ensureValueL(t *testing.T, swr *Simple, key string, expectedValue uint64) { value, err := swr.Query(key) if value.(uint64) != expectedValue { t.Errorf("Actual: %d; Expected: %d", value, expectedValue) } if err != nil { t.Errorf("Actual: %#v; Expected: %#v", err, nil) } } //////////////////////////////////////// func TestSimpleSynchronousLookupWhenMiss(t *testing.T) { var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) return uint64(42), nil }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() ensureValueL(t, swr, "miss", 42) if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleNoStaleNoExpireNoLookupWhenHit(t *testing.T) { swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { t.Fatal("lookup ought not to have been invoked") return nil, errors.New("lookup ought not to have been invoked") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() swr.Store("hit", uint64(13)) ensureValueL(t, swr, "hit", 13) } func TestSimpleNoStaleExpireNoLookupWhenBeforeExpire(t *testing.T) { swr, err := NewSimple(&Config{ Lookup: func(_ string) (interface{}, error) { t.Fatal("lookup ought not to have been invoked") return nil, errors.New("lookup ought not to have been invoked") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that expires one minute in the future swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Expiry: time.Now().Add(time.Minute)}) ensureValueL(t, swr, "hit", 13) } func TestSimpleNoStaleExpireSynchronousLookupWhenAfterExpire(t *testing.T) { var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) return uint64(42), nil }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that expired one minute ago swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Expiry: time.Now().Add(-time.Minute)}) ensureValueL(t, swr, "hit", 42) if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleStaleNoExpireNoLookupWhenBeforeStale(t *testing.T) { swr, err := NewSimple(&Config{ Lookup: func(_ string) (interface{}, error) { t.Fatal("lookup ought not to have been invoked") return nil, errors.New("lookup ought not to have been invoked") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that goes stale one minute in the future swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(time.Minute)}) ensureValueL(t, swr, "hit", 13) } func TestSimpleStaleNoExpireSynchronousLookupOnlyOnceWhenAfterStale(t *testing.T) { var wg sync.WaitGroup var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { time.Sleep(5 * time.Millisecond) atomic.AddUint64(&invoked, 1) wg.Done() return uint64(42), nil }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that went stale one minute ago swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute)}) wg.Add(1) ensureValueL(t, swr, "hit", 13) ensureValueL(t, swr, "hit", 13) ensureValueL(t, swr, "hit", 13) wg.Wait() time.Sleep(5 * time.Millisecond) ensureValueL(t, swr, "hit", 42) if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleStaleExpireNoLookupWhenBeforeStale(t *testing.T) { swr, err := NewSimple(&Config{ Lookup: func(_ string) (interface{}, error) { t.Fatal("lookup ought not to have been invoked") return nil, errors.New("lookup ought not to have been invoked") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that goes stale one minute in the future and expires one hour in the future swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(time.Minute), Expiry: time.Now().Add(time.Hour)}) ensureValueL(t, swr, "hit", 13) } func TestSimpleStaleExpireSynchronousLookupWhenAfterStaleAndBeforeExpire(t *testing.T) { var wg sync.WaitGroup var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) wg.Done() return uint64(42), nil }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that went stale one minute ago and expires one minute in the future swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute), Expiry: time.Now().Add(time.Minute)}) // expect to receive the old value back immediately, then expect lookup to be asynchronously invoked wg.Add(1) ensureValueL(t, swr, "hit", 13) wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } ensureValueL(t, swr, "hit", 42) if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleStaleExpireSynchronousLookupWhenAfterExpire(t *testing.T) { var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) return uint64(42), nil }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that went stale one hour ago and expired one minute ago swr.Store("hit", &TimedValue{Value: uint64(42), Err: nil, Stale: time.Now().Add(-time.Hour), Expiry: time.Now().Add(-time.Minute)}) ensureValueL(t, swr, "hit", 42) if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleErrDoesNotReplaceStaleValue(t *testing.T) { var wg sync.WaitGroup var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) wg.Done() return nil, errors.New("fetch error") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that went stale one minute ago swr.Store("hit", &TimedValue{Value: uint64(13), Err: nil, Stale: time.Now().Add(-time.Minute)}) wg.Add(1) ensureValueL(t, swr, "hit", 13) wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } wg.Add(1) ensureValueL(t, swr, "hit", 13) wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(2); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleNewErrReplacesOldError(t *testing.T) { var wg sync.WaitGroup var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { atomic.AddUint64(&invoked, 1) wg.Done() return nil, errors.New("new error") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value that went stale one minute ago swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Stale: time.Now().Add(-time.Minute)}) wg.Add(1) ensureErrorL(t, swr, "hit", "new error") wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleErrReplacesExpiredValue(t *testing.T) { // make stale value, but fetch duration ought cause it to expire var wg sync.WaitGroup var invoked uint64 swr, err := NewSimple(&Config{Lookup: func(_ string) (interface{}, error) { time.Sleep(5 * time.Millisecond) atomic.AddUint64(&invoked, 1) wg.Done() return nil, errors.New("new error") }}) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() // NOTE: storing a value is already stale, but will expire during the fetch swr.Store("hit", &TimedValue{Value: nil, Err: errors.New("original error"), Stale: time.Now().Add(-time.Hour), Expiry: time.Now().Add(5 * time.Millisecond)}) wg.Add(1) ensureErrorL(t, swr, "hit", "original error") wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(1); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } wg.Add(1) ensureErrorL(t, swr, "hit", "new error") wg.Wait() if actual, expected := atomic.AddUint64(&invoked, 0), uint64(2); actual != expected { t.Errorf("Actual: %d; Expected: %d", actual, expected) } } func TestSimpleRange(t *testing.T) { swr, err := NewSimple(nil) if err != nil { t.Fatal(err) } defer func() { _ = swr.Close() }() swr.Store("no expiry", "shall not expire") swr.Store("stale value", TimedValue{Value: "stale value", Stale: time.Now().Add(-time.Minute)}) swr.Store("expired value", TimedValue{Value: "expired value", Expiry: time.Now().Add(-time.Minute)}) called := make(map[string]struct{}) swr.Range(func(key string, value *TimedValue) { called[key] = struct{}{} swr.Store(strconv.Itoa(rand.Intn(50)), "make sure we can invoke methods that require locking") }) if _, ok := called["no expiry"]; !ok { t.Errorf("Actual: %#v; Expected: %#v", ok, true) } if _, ok := called["stale value"]; !ok { t.Errorf("Actual: %#v; Expected: %#v", ok, true) } if _, ok := called["expired value"]; ok { t.Errorf("Actual: %#v; Expected: %#v", ok, false) } swr.Store("ensure range released top level lock", struct{}{}) } func TestSimpleGC(t *testing.T) { swr, err := NewSimple(&Config{ GCPeriodicity: 10 * time.Millisecond, GCTimeout: 10 * time.Millisecond, Lookup: func(key string) (interface{}, error) { time.Sleep(10 * time.Millisecond) return key, nil }, }) if err != nil { t.Fatal(err) } now := time.Now() // populate swr with lots of data, some expired, some stale, some good const itemCount = 10000 for i := 0; i < itemCount; i++ { key := fmt.Sprintf("key%d", i) if rand.Intn(2) < 1 { go func() { _, _ = swr.Query(key) }() } else { var value interface{} switch rand.Intn(4) { case 0: value = TimedValue{Value: "expired", Expiry: now.Add(-time.Minute)} case 1: value = TimedValue{Value: "stale", Stale: now.Add(-time.Minute)} case 2: value = TimedValue{Value: "future stale", Stale: now.Add(time.Minute)} case 3: value = TimedValue{Value: "future expiry", Expiry: now.Add(time.Minute)} case 4: value = "good" } swr.Store(key, value) } } time.Sleep(25 * time.Millisecond) if actual, expected := swr.Close(), error(nil); actual != expected { t.Errorf("Actual: %s; Expected: %s", actual, expected) } } golang-github-karrick-goswarm-1.4.7/timedValue.go000066400000000000000000000065461311477714700220300ustar00rootroot00000000000000package goswarm import ( "sync/atomic" "time" ) // TimedValue couples a value or the error with both a stale and expiry time for the value and // error. type TimedValue struct { // Value stores the datum returned by the lookup function. Value interface{} // Err stores the error returned by the lookup function. Err error // Stale stores the time at which the value becomes stale. On Query, a stale value will // trigger an asynchronous lookup of a replacement value, and the original value is // returned. A zero-value for Stale implies the value never goes stale, and querying the key // associated for this value will never trigger an asynchronous lookup of a replacement // value. Stale time.Time // Expiry stores the time at which the value expires. On Query, an expired value will block // until a synchronous lookup of a replacement value is attempted. Once the lookup returns, // the Query method will return with the new value or the error returned by the lookup // function. Expiry time.Time } // IsExpired returns true if and only if value is expired. A value is expired when its non-zero // expiry time is before the current time, or when the value represents an error and expiry time is // the time.Time zero-value. func (tv *TimedValue) IsExpired() bool { return tv.isExpired(time.Now()) } // provided for internal use so we don't need to repeatedly get the current time func (tv *TimedValue) isExpired(when time.Time) bool { if tv.Err == nil { return !tv.Expiry.IsZero() && when.After(tv.Expiry) } // NOTE: When a TimedValue stores an error result, then Expiry and Expiry zero-values imply // the value is immediately expired. return tv.Expiry.IsZero() || when.After(tv.Expiry) } // IsStale returns true if and only if value is stale. A value is stale when its non-zero stale time // is before the current time, or when the value represents an error and stale time is the time.Time // zero-value. func (tv *TimedValue) IsStale() bool { return tv.isStale(time.Now()) } // provided for internal use so we don't need to repeatedly get the current time func (tv *TimedValue) isStale(when time.Time) bool { if tv.Err == nil { return !tv.Stale.IsZero() && when.After(tv.Stale) } // NOTE: When a TimedValue stores an error result, then Stale and Expiry zero-values imply // the value is immediately stale. return tv.Stale.IsZero() || when.After(tv.Stale) } // helper function to wrap non TimedValue items as TimedValue items. func newTimedValue(value interface{}, err error, staleDuration, expiryDuration time.Duration) *TimedValue { switch val := value.(type) { case TimedValue: return &val case *TimedValue: return val default: if staleDuration == 0 && expiryDuration == 0 { return &TimedValue{Value: value, Err: err} } var stale, expiry time.Time now := time.Now() if staleDuration > 0 { stale = now.Add(staleDuration) } if expiryDuration > 0 { expiry = now.Add(expiryDuration) } return &TimedValue{Value: value, Err: err, Stale: stale, Expiry: expiry} } } type atomicTimedValue struct { // av is accessed with atomic.Value's Load() and Store() methods to // atomically access the underlying *TimedValue. av atomic.Value // pending is accessed with sync/atomic primitives to control whether an // asynchronous lookup ought to be spawned to update av. 1 when a go routine // is waiting on Lookup return; 0 otherwise pending int32 }