Expire old cache entries
This commit is contained in:
2
justfile
2
justfile
@@ -11,7 +11,7 @@ tidy:
|
|||||||
test:
|
test:
|
||||||
{{go}} vet ./...
|
{{go}} vet ./...
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
{{go}} test -race -coverprofile=cover.out -timeout=60s -parallel=10 ./...
|
{{go}} test -race -coverprofile=cover.out -timeout=60s ./...
|
||||||
{{go}} tool cover -html=cover.out -o=cover.html
|
{{go}} tool cover -html=cover.out -o=cover.html
|
||||||
|
|
||||||
todo:
|
todo:
|
||||||
|
|||||||
61
potency.go
61
potency.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gopatchy/jsrest"
|
"github.com/gopatchy/jsrest"
|
||||||
)
|
)
|
||||||
@@ -16,15 +17,20 @@ import (
|
|||||||
type Potency struct {
|
type Potency struct {
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
|
||||||
// TODO: Expire based on time; probably use age-based linked list and delete at write time
|
lifetime time.Duration
|
||||||
cache map[string]*savedResult
|
|
||||||
cacheMu sync.RWMutex
|
cache map[string]*savedResult
|
||||||
|
cacheOldest *savedResult
|
||||||
|
cacheNewest *savedResult
|
||||||
|
cacheMu sync.RWMutex
|
||||||
|
|
||||||
inProgress map[string]bool
|
inProgress map[string]bool
|
||||||
inProgressMu sync.Mutex
|
inProgressMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type savedResult struct {
|
type savedResult struct {
|
||||||
|
key string
|
||||||
|
|
||||||
method string
|
method string
|
||||||
url string
|
url string
|
||||||
requestHeader http.Header
|
requestHeader http.Header
|
||||||
@@ -33,6 +39,9 @@ type savedResult struct {
|
|||||||
statusCode int
|
statusCode int
|
||||||
responseHeader http.Header
|
responseHeader http.Header
|
||||||
responseBody []byte
|
responseBody []byte
|
||||||
|
|
||||||
|
added time.Time
|
||||||
|
newer *savedResult
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -53,6 +62,7 @@ var (
|
|||||||
func NewPotency(handler http.Handler) *Potency {
|
func NewPotency(handler http.Handler) *Potency {
|
||||||
return &Potency{
|
return &Potency{
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
lifetime: 6 * time.Hour,
|
||||||
cache: map[string]*savedResult{},
|
cache: map[string]*savedResult{},
|
||||||
inProgress: map[string]bool{},
|
inProgress: map[string]bool{},
|
||||||
}
|
}
|
||||||
@@ -71,6 +81,20 @@ func (p *Potency) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Potency) SetLifetime(lifetime time.Duration) {
|
||||||
|
p.cacheMu.Lock()
|
||||||
|
defer p.cacheMu.Unlock()
|
||||||
|
|
||||||
|
p.lifetime = lifetime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Potency) NumCached() int {
|
||||||
|
p.cacheMu.RLock()
|
||||||
|
defer p.cacheMu.RUnlock()
|
||||||
|
|
||||||
|
return len(p.cache)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Potency) serveHTTP(w http.ResponseWriter, r *http.Request, val string) error {
|
func (p *Potency) serveHTTP(w http.ResponseWriter, r *http.Request, val string) error {
|
||||||
if len(val) < 2 || !strings.HasPrefix(val, `"`) || !strings.HasSuffix(val, `"`) {
|
if len(val) < 2 || !strings.HasPrefix(val, `"`) || !strings.HasSuffix(val, `"`) {
|
||||||
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", val, ErrInvalidKey)
|
return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", val, ErrInvalidKey)
|
||||||
@@ -139,6 +163,8 @@ func (p *Potency) serveHTTP(w http.ResponseWriter, r *http.Request, val string)
|
|||||||
p.handler.ServeHTTP(w, r)
|
p.handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
save := &savedResult{
|
save := &savedResult{
|
||||||
|
key: key,
|
||||||
|
|
||||||
method: r.Method,
|
method: r.Method,
|
||||||
url: r.URL.String(),
|
url: r.URL.String(),
|
||||||
requestHeader: requestHeader,
|
requestHeader: requestHeader,
|
||||||
@@ -149,7 +175,7 @@ func (p *Potency) serveHTTP(w http.ResponseWriter, r *http.Request, val string)
|
|||||||
responseBody: rwi.buf.Bytes(),
|
responseBody: rwi.buf.Bytes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
p.write(key, save)
|
p.write(save)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -181,9 +207,32 @@ func (p *Potency) read(key string) *savedResult {
|
|||||||
return p.cache[key]
|
return p.cache[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Potency) write(key string, sr *savedResult) {
|
func (p *Potency) write(sr *savedResult) {
|
||||||
p.cacheMu.Lock()
|
p.cacheMu.Lock()
|
||||||
defer p.cacheMu.Unlock()
|
defer p.cacheMu.Unlock()
|
||||||
|
|
||||||
p.cache[key] = sr
|
sr.added = time.Now()
|
||||||
|
|
||||||
|
p.cache[sr.key] = sr
|
||||||
|
|
||||||
|
if p.cacheNewest != nil {
|
||||||
|
p.cacheNewest.newer = sr
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cacheNewest = sr
|
||||||
|
|
||||||
|
if p.cacheOldest == nil {
|
||||||
|
p.cacheOldest = sr
|
||||||
|
}
|
||||||
|
|
||||||
|
p.removeExpired()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Potency) removeExpired() {
|
||||||
|
cutoff := time.Now().Add(-1 * p.lifetime)
|
||||||
|
|
||||||
|
for iter := p.cacheOldest; iter != nil && iter.added.Before(cutoff); iter = iter.newer {
|
||||||
|
delete(p.cache, iter.key)
|
||||||
|
p.cacheOldest = iter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,55 @@ func TestPOST(t *testing.T) {
|
|||||||
require.True(t, resp.IsError())
|
require.True(t, resp.IsError())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpire(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := newTestServer(t)
|
||||||
|
defer ts.shutdown(t)
|
||||||
|
|
||||||
|
ts.pot.SetLifetime(1 * time.Second)
|
||||||
|
|
||||||
|
require.Equal(t, 0, ts.pot.NumCached())
|
||||||
|
|
||||||
|
key1 := uniuri.New()
|
||||||
|
|
||||||
|
resp, err := ts.r().
|
||||||
|
SetHeader("Idempotency-Key", fmt.Sprintf(`"%s"`, key1)).
|
||||||
|
SetBody("test1").
|
||||||
|
Post("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, resp.IsError())
|
||||||
|
|
||||||
|
require.Equal(t, 1, ts.pot.NumCached())
|
||||||
|
|
||||||
|
key2 := uniuri.New()
|
||||||
|
|
||||||
|
resp, err = ts.r().
|
||||||
|
SetHeader("Idempotency-Key", fmt.Sprintf(`"%s"`, key2)).
|
||||||
|
SetBody("test2").
|
||||||
|
Post("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, resp.IsError())
|
||||||
|
|
||||||
|
require.Equal(t, 2, ts.pot.NumCached())
|
||||||
|
|
||||||
|
time.Sleep(1100 * time.Millisecond)
|
||||||
|
|
||||||
|
key3 := uniuri.New()
|
||||||
|
|
||||||
|
resp, err = ts.r().
|
||||||
|
SetHeader("Idempotency-Key", fmt.Sprintf(`"%s"`, key3)).
|
||||||
|
SetBody("test3").
|
||||||
|
Post("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, resp.IsError())
|
||||||
|
|
||||||
|
require.Equal(t, 1, ts.pot.NumCached())
|
||||||
|
}
|
||||||
|
|
||||||
type testServer struct {
|
type testServer struct {
|
||||||
dir string
|
dir string
|
||||||
|
pot *potency.Potency
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
rst *resty.Client
|
rst *resty.Client
|
||||||
}
|
}
|
||||||
@@ -166,6 +213,7 @@ func newTestServer(t *testing.T) *testServer {
|
|||||||
|
|
||||||
return &testServer{
|
return &testServer{
|
||||||
dir: dir,
|
dir: dir,
|
||||||
|
pot: p,
|
||||||
srv: srv,
|
srv: srv,
|
||||||
rst: rst,
|
rst: rst,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user