From 7f71e929de2c3ac8e8577b40d0a9dc71d42f679d Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 29 May 2024 15:59:18 -0700 Subject: [PATCH] Add in-memory cache for rwp server --- cmd/rwp/cmd/serve/api.go | 69 ++++++++++++---------- cmd/rwp/cmd/serve/cache/local.go | 90 +++++++++++++++++++++++++++++ cmd/rwp/cmd/serve/cache/pubcache.go | 22 +++++++ cmd/rwp/cmd/serve/server.go | 8 +++ go.mod | 3 + go.sum | 7 +++ 6 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 cmd/rwp/cmd/serve/cache/local.go create mode 100644 cmd/rwp/cmd/serve/cache/pubcache.go diff --git a/cmd/rwp/cmd/serve/api.go b/cmd/rwp/cmd/serve/api.go index 00a0ba8..82db26b 100644 --- a/cmd/rwp/cmd/serve/api.go +++ b/cmd/rwp/cmd/serve/api.go @@ -16,6 +16,7 @@ import ( "github.com/gorilla/mux" httprange "github.com/gotd/contrib/http_range" "github.com/pkg/errors" + "github.com/readium/go-toolkit/cmd/rwp/cmd/serve/cache" "github.com/readium/go-toolkit/pkg/asset" "github.com/readium/go-toolkit/pkg/manifest" "github.com/readium/go-toolkit/pkg/pub" @@ -53,43 +54,49 @@ func (s *Server) getPublication(filename string) (*pub.Publication, error) { return nil, err } - // TODO: cache open publications - cp := filepath.Clean(string(fpath)) - pub, err := streamer.New(streamer.Config{ - InferA11yMetadata: s.config.InferA11yMetadata, - }).Open(asset.File(filepath.Join(s.config.BaseDirectory, cp)), "") - if err != nil { - return nil, errors.Wrap(err, "failed opening "+cp) - } + dat, ok := s.lfu.Get(cp) + if !ok { + pub, err := streamer.New(streamer.Config{ + InferA11yMetadata: s.config.InferA11yMetadata, + }).Open(asset.File(filepath.Join(s.config.BaseDirectory, cp)), "") + if err != nil { + return nil, errors.Wrap(err, "failed opening "+cp) + } - // TODO: Remove this after we make links relative in the go-toolkit - for i, link := range pub.Manifest.Links { - pub.Manifest.Links[i] = makeRelative(link) - } - for i, link := range pub.Manifest.Resources { - pub.Manifest.Resources[i] = makeRelative(link) - } - for i, link := range pub.Manifest.ReadingOrder { - pub.Manifest.ReadingOrder[i] = makeRelative(link) - } - for i, link := range pub.Manifest.TableOfContents { - pub.Manifest.TableOfContents[i] = makeRelative(link) - } - var makeCollectionRelative func(mp manifest.PublicationCollectionMap) - makeCollectionRelative = func(mp manifest.PublicationCollectionMap) { - for i := range mp { - for j := range mp[i] { - for k := range mp[i][j].Links { - mp[i][j].Links[k] = makeRelative(mp[i][j].Links[k]) + // TODO: Remove this after we make links relative in the go-toolkit + for i, link := range pub.Manifest.Links { + pub.Manifest.Links[i] = makeRelative(link) + } + for i, link := range pub.Manifest.Resources { + pub.Manifest.Resources[i] = makeRelative(link) + } + for i, link := range pub.Manifest.ReadingOrder { + pub.Manifest.ReadingOrder[i] = makeRelative(link) + } + for i, link := range pub.Manifest.TableOfContents { + pub.Manifest.TableOfContents[i] = makeRelative(link) + } + var makeCollectionRelative func(mp manifest.PublicationCollectionMap) + makeCollectionRelative = func(mp manifest.PublicationCollectionMap) { + for i := range mp { + for j := range mp[i] { + for k := range mp[i][j].Links { + mp[i][j].Links[k] = makeRelative(mp[i][j].Links[k]) + } + makeCollectionRelative(mp[i][j].Subcollections) } - makeCollectionRelative(mp[i][j].Subcollections) } } - } - makeCollectionRelative(pub.Manifest.Subcollections) + makeCollectionRelative(pub.Manifest.Subcollections) - return pub, nil + // Cache the publication + encPub := &cache.CachedPublication{Publication: pub} + s.lfu.Set(cp, encPub) + + return encPub.Publication, nil + } + return dat.(*cache.CachedPublication).Publication, nil } func (s *Server) getManifest(w http.ResponseWriter, req *http.Request) { diff --git a/cmd/rwp/cmd/serve/cache/local.go b/cmd/rwp/cmd/serve/cache/local.go new file mode 100644 index 0000000..0ddb889 --- /dev/null +++ b/cmd/rwp/cmd/serve/cache/local.go @@ -0,0 +1,90 @@ +package cache + +// Originally from https://github.com/go-redis/cache/blob/v8.4.3/local.go +// Modified to store interface{} instead of []byte + +import ( + "sync" + "time" + + "github.com/vmihailenco/go-tinylfu" + "golang.org/x/exp/rand" +) + +type Evictable interface { + OnEvict() +} + +type LocalCache interface { + Set(key string, data Evictable) + Get(key string) (Evictable, bool) + Del(key string) +} + +type TinyLFU struct { + mu sync.Mutex + rand *rand.Rand + lfu *tinylfu.T + ttl time.Duration + offset time.Duration +} + +var _ LocalCache = (*TinyLFU)(nil) + +func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { + const maxOffset = 10 * time.Second + + offset := ttl / 10 + if offset > maxOffset { + offset = maxOffset + } + + return &TinyLFU{ + rand: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))), + lfu: tinylfu.New(size, 100000), + ttl: ttl, + offset: offset, + } +} + +func (c *TinyLFU) UseRandomizedTTL(offset time.Duration) { + c.offset = offset +} + +func (c *TinyLFU) Set(key string, b Evictable) { + c.mu.Lock() + defer c.mu.Unlock() + + ttl := c.ttl + if c.offset > 0 { + ttl += time.Duration(c.rand.Int63n(int64(c.offset))) + } + + c.lfu.Set(&tinylfu.Item{ + Key: key, + Value: b, + ExpireAt: time.Now().Add(ttl), + OnEvict: func() { + b.OnEvict() + }, + }) +} + +func (c *TinyLFU) Get(key string) (Evictable, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + val, ok := c.lfu.Get(key) + if !ok { + return nil, false + } + + return val.(Evictable), true +} + +func (c *TinyLFU) Del(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + c.lfu.Del(key) +} diff --git a/cmd/rwp/cmd/serve/cache/pubcache.go b/cmd/rwp/cmd/serve/cache/pubcache.go new file mode 100644 index 0000000..52a63f5 --- /dev/null +++ b/cmd/rwp/cmd/serve/cache/pubcache.go @@ -0,0 +1,22 @@ +package cache + +import ( + "github.com/readium/go-toolkit/pkg/pub" +) + +// CachedPublication implements Evictable +type CachedPublication struct { + *pub.Publication +} + +func EncapsulatePublication(pub *pub.Publication) *CachedPublication { + cp := &CachedPublication{pub} + return cp +} + +func (cp *CachedPublication) OnEvict() { + // Cleanup + if cp.Publication != nil { + cp.Publication.Close() + } +} diff --git a/cmd/rwp/cmd/serve/server.go b/cmd/rwp/cmd/serve/server.go index 4fa14c2..20f1b5d 100644 --- a/cmd/rwp/cmd/serve/server.go +++ b/cmd/rwp/cmd/serve/server.go @@ -1,7 +1,10 @@ package serve import ( + "time" + "github.com/gorilla/mux" + "github.com/readium/go-toolkit/cmd/rwp/cmd/serve/cache" "github.com/readium/go-toolkit/pkg/streamer" ) @@ -15,10 +18,15 @@ type ServerConfig struct { type Server struct { config ServerConfig router *mux.Router + lfu *cache.TinyLFU } +const MaxCachedPublicationAmount = 10 +const MaxCachedPublicationTTL = time.Second * time.Duration(600) + func NewServer(config ServerConfig) *Server { return &Server{ config: config, + lfu: cache.NewTinyLFU(MaxCachedPublicationAmount, MaxCachedPublicationTTL), } } diff --git a/go.mod b/go.mod index 7e1be1d..660112c 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,9 @@ require ( github.com/stretchr/testify v1.9.0 github.com/trimmer-io/go-xmp v1.0.0 github.com/urfave/negroni v1.0.0 + github.com/vmihailenco/go-tinylfu v0.2.2 github.com/zeebo/xxh3 v1.0.2 + golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 golang.org/x/net v0.23.0 golang.org/x/text v0.14.0 ) @@ -29,6 +31,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/antchfx/xpath v1.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 5dd01ab..345a44c 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,9 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -292,6 +295,8 @@ github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= +github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= +github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -333,6 +338,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=