From bee26e22edb7b785c9cb2dd919f8f6cb3c16a39b Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:36:32 +1000 Subject: [PATCH 1/3] wip --- addr_cache.go | 17 ------------ caches.go | 62 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ rubbish_cache.go | 29 -------------------- rubbish_cache_test.go | 40 +++++++++++++++++----------- 6 files changed, 90 insertions(+), 61 deletions(-) delete mode 100644 addr_cache.go create mode 100644 caches.go delete mode 100644 rubbish_cache.go diff --git a/addr_cache.go b/addr_cache.go deleted file mode 100644 index 240ef09..0000000 --- a/addr_cache.go +++ /dev/null @@ -1,17 +0,0 @@ -package aklapi - -type addrResponseCache map[string]*AddrResponse - -var addrCache = make(addrResponseCache) - -func (c addrResponseCache) Lookup(searchText string) (resp *AddrResponse, ok bool) { - if NoCache { - return nil, false - } - resp, ok = c[searchText] - return -} - -func (c addrResponseCache) Add(searchText string, ar *AddrResponse) { - c[searchText] = ar -} diff --git a/caches.go b/caches.go new file mode 100644 index 0000000..d603c38 --- /dev/null +++ b/caches.go @@ -0,0 +1,62 @@ +package aklapi + +import ( + "github.com/phuslu/lru" +) + +const defCacheSz = 100 // seems reasonable. +var ( + addrCache = newLRUCache[string, *AddrResponse](defCacheSz) + rubbishCache = rubbishResultCache{lc: newLRUCache[string, *CollectionDayDetailResult](defCacheSz)} +) + +type lruCache[K comparable, V any] struct { + cache *lru.LRUCache[K, V] +} + +func newLRUCache[K comparable, V any](size int) *lruCache[K, V] { + return &lruCache[K, V]{ + cache: lru.NewLRUCache[K, V](size), + } +} + +func (c *lruCache[K, V]) Lookup(key K) (resp V, ok bool) { + var nothing V + if NoCache { + return nothing, false + } + return c.cache.Get(key) +} + +func (c *lruCache[K, V]) Add(key K, value V) { + c.cache.Set(key, value) +} + +func (c *lruCache[K, V]) Delete(key K) { + c.cache.Delete(key) +} + +type rubbishResultCache struct { + lc *lruCache[string, *CollectionDayDetailResult] +} + +func (c *rubbishResultCache) Lookup(searchText string) (result *CollectionDayDetailResult, ok bool) { + result, ok = c.lc.Lookup(searchText) + if !ok { + return nil, false + } + + today := now() + for _, res := range result.Collections { + if today.After(res.Date) || res.Date.IsZero() { + // invalidate from cache. + c.lc.Delete(searchText) + return nil, false + } + } + return +} + +func (c *rubbishResultCache) Add(searchText string, result *CollectionDayDetailResult) { + c.lc.Add(searchText, result) +} diff --git a/go.mod b/go.mod index 7a2888f..1d0a922 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/phuslu/lru v1.0.18 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.39.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d6de205..bfbd52b 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/phuslu/lru v1.0.18 h1:ioKRYLym7nv6UmaKHXSR0Z8s2KCEra+mcWcn9zXQnlM= +github.com/phuslu/lru v1.0.18/go.mod h1:ci5hb8dRIa+2I+KcPl4958OWCg09FxwZCP8InU1L1ME= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rusq/osenv/v2 v2.0.1 h1:1LtNt8VNV/W86wb38Hyu5W3Rwqt/F1JNRGE+8GRu09o= diff --git a/rubbish_cache.go b/rubbish_cache.go deleted file mode 100644 index aa4d816..0000000 --- a/rubbish_cache.go +++ /dev/null @@ -1,29 +0,0 @@ -package aklapi - -type rubbishResultCache map[string]*CollectionDayDetailResult - -var rubbishCache rubbishResultCache = make(rubbishResultCache, 0) - -func (c rubbishResultCache) Lookup(searchText string) (result *CollectionDayDetailResult, ok bool) { - if NoCache { - return nil, false - } - result, ok = c[searchText] - if !ok { - return - } - - today := now() - for _, res := range result.Collections { - if today.After(res.Date) || res.Date.IsZero() { - // invalidate from cache. - delete(c, searchText) - return nil, false - } - } - return -} - -func (c rubbishResultCache) Add(searchText string, result *CollectionDayDetailResult) { - c[searchText] = result -} diff --git a/rubbish_cache_test.go b/rubbish_cache_test.go index c727811..bc4066c 100644 --- a/rubbish_cache_test.go +++ b/rubbish_cache_test.go @@ -39,6 +39,16 @@ var ( } ) +func newRubbishCacheFromMap(m map[string]*CollectionDayDetailResult) *rubbishResultCache { + var res = rubbishResultCache{ + lc: newLRUCache[string, *CollectionDayDetailResult](defCacheSz), + } + for k, v := range m { + res.Add(k, v) + } + return &res +} + func Test_rubbishResultCache_Lookup(t *testing.T) { defer func() { NoCache = false @@ -49,52 +59,52 @@ func Test_rubbishResultCache_Lookup(t *testing.T) { tests := []struct { name string NoCache bool // if true, set NoCache to true before running test - c rubbishResultCache + c *rubbishResultCache args args wantResult *CollectionDayDetailResult wantOk bool }{ - {"not in cache (empty)", false, rubbishResultCache{}, args{"blah"}, nil, false}, + {"not in cache (empty)", false, newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{}), args{"blah"}, nil, false}, {"not in cache", false, - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: tomorrow, - }, + }), args{"blah"}, nil, false}, {"in cache (future)", false, - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: tomorrow, - }, + }), args{testAddr.Address}, tomorrow, true}, {"in cache (today)", false, - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: today, - }, + }), args{testAddr.Address}, today, true}, {"in cache, expired", false, - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: yesterday, - }, + }), args{testAddr.Address}, nil, false}, {"in cache, second entry expired", false, - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: secondYesterday, - }, + }), args{testAddr.Address}, nil, false}, {"no cache", true, - rubbishResultCache{ - testAddr.Address: today, - }, + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ + testAddr.Address: secondYesterday, + }), args{testAddr.Address}, nil, false}, } From dcff2110a48db82367a06cf405e4b9f9588871f2 Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:28:24 +1000 Subject: [PATCH 2/3] context, lru cache, modernise --- .github/workflows/go.yml | 2 +- Dockerfile | 2 +- addr.go | 19 ++-- addr_cache_test.go | 92 ------------------- addr_test.go | 6 +- aklapi.go | 2 - rubbish_cache_test.go => caches_test.go | 114 +++++++++++++++++++++--- cmd/aklapi/assets.go | 2 +- cmd/aklapi/handlers.go | 12 +-- cmd/aklapi/main.go | 17 ++-- go.mod | 4 +- go.sum | 11 --- rubbish.go | 23 +++-- rubbish_test.go | 2 +- 14 files changed, 155 insertions(+), 153 deletions(-) delete mode 100644 addr_cache_test.go rename rubbish_cache_test.go => caches_test.go (59%) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3066f1b..f335dc9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Build run: go build -v ./... diff --git a/Dockerfile b/Dockerfile index 59c6948..26dacd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-alpine AS builder +FROM golang:1.25-alpine AS builder LABEL maintainer="github:@rusq" WORKDIR /build diff --git a/addr.go b/addr.go index 5ba0a5d..a51b69d 100644 --- a/addr.go +++ b/addr.go @@ -1,9 +1,10 @@ package aklapi import ( + "context" "encoding/json" "errors" - "log" + "log/slog" "net/http" "strconv" "time" @@ -36,19 +37,19 @@ func (s Address) String() string { } // AddressLookup is a convenience function to get addresses. -func AddressLookup(addr string) (*AddrResponse, error) { - return MatchingPropertyAddresses(&AddrRequest{SearchText: addr, PageSize: 10}) +func AddressLookup(ctx context.Context, addr string) (*AddrResponse, error) { + return MatchingPropertyAddresses(ctx, &AddrRequest{SearchText: addr, PageSize: 10}) } // MatchingPropertyAddresses wrapper around the AKL Council API. -func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { +func MatchingPropertyAddresses(ctx context.Context, addrReq *AddrRequest) (*AddrResponse, error) { cachedAr, ok := addrCache.Lookup(addrReq.SearchText) if ok { - log.Printf("cached address result: %q", cachedAr) + slog.DebugContext(ctx, "found cached address result", "addr", cachedAr) return cachedAr, nil } - req, err := http.NewRequest("GET", addrURI, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addrURI, nil) if err != nil { return nil, err } @@ -66,7 +67,7 @@ func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { return nil, err } defer resp.Body.Close() - log.Printf("address call complete in %s", time.Since(start)) + slog.DebugContext(ctx, "address call complete", "duration", time.Since(start)) if resp.StatusCode != http.StatusOK { return nil, errors.New("address API returned status code: " + strconv.Itoa(resp.StatusCode)) @@ -82,8 +83,8 @@ func MatchingPropertyAddresses(addrReq *AddrRequest) (*AddrResponse, error) { return &apiResp, nil } -func oneAddress(addr string) (*Address, error) { - resp, err := AddressLookup(addr) +func oneAddress(ctx context.Context, addr string) (*Address, error) { + resp, err := AddressLookup(ctx, addr) if err != nil { return nil, err } diff --git a/addr_cache_test.go b/addr_cache_test.go deleted file mode 100644 index 3b25fe4..0000000 --- a/addr_cache_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package aklapi - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_addrResponseCache_Lookup(t *testing.T) { - defer func() { - NoCache = false - }() - type args struct { - searchText string - } - tests := []struct { - name string - NoCache bool // if true, set NoCache to true before running test - c addrResponseCache - args args - wantResp *AddrResponse - wantOk bool - }{ - {"not in cache", - false, - addrResponseCache{ - "xxx": &AddrResponse{Items: []Address{*testAddr}}, - }, - args{"yyy"}, - nil, - false, - }, - {"cached", - false, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - args{testAddr.Address}, - &AddrResponse{Items: []Address{*testAddr}}, - true, - }, - {"cached, no cache mode", - true, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - args{testAddr.Address}, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - NoCache = tt.NoCache - gotResp, gotOk := tt.c.Lookup(tt.args.searchText) - if !reflect.DeepEqual(gotResp, tt.wantResp) { - t.Errorf("addrResponseCache.Lookup() gotResp = %v, want %v", gotResp, tt.wantResp) - } - if gotOk != tt.wantOk { - t.Errorf("addrResponseCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) - } - }) - } -} - -func Test_addrResponseCache_Add(t *testing.T) { - type args struct { - searchText string - ar *AddrResponse - } - tests := []struct { - name string - c addrResponseCache - args args - wantAddrResponseCache addrResponseCache - }{ - {"add", - addrResponseCache{}, - args{testAddr.Address, &AddrResponse{Items: []Address{*testAddr}}}, - addrResponseCache{ - testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.c.Add(tt.args.searchText, tt.args.ar) - assert.Equal(t, tt.wantAddrResponseCache, tt.c) - }) - } -} diff --git a/addr_test.go b/addr_test.go index ca345aa..3999713 100644 --- a/addr_test.go +++ b/addr_test.go @@ -68,7 +68,7 @@ func TestMatchingPropertyAddresses(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := MatchingPropertyAddresses(tt.args.addrReq) + got, err := MatchingPropertyAddresses(t.Context(), tt.args.addrReq) if (err != nil) != tt.wantErr { t.Errorf("MatchingPropertyAddresses() error = %v, wantErr %v", err, tt.wantErr) return @@ -106,7 +106,7 @@ func TestAddress(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := AddressLookup(tt.args.addr) + got, err := AddressLookup(t.Context(), tt.args.addr) if (err != nil) != tt.wantErr { t.Errorf("Address() error = %v, wantErr %v", err, tt.wantErr) return @@ -160,7 +160,7 @@ func Test_oneAddress(t *testing.T) { oldURI := addrURI defer func() { addrURI = oldURI }() addrURI = tt.testSrv.URL - got, err := oneAddress(tt.args.addr) + got, err := oneAddress(t.Context(), tt.args.addr) if (err != nil) != tt.wantErr { t.Errorf("oneAddress() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/aklapi.go b/aklapi.go index c770167..21988ea 100644 --- a/aklapi.go +++ b/aklapi.go @@ -1,11 +1,9 @@ package aklapi import ( - "regexp" "time" ) var ( defaultLoc, _ = time.LoadLocation("Pacific/Auckland") // Auckland is in NZ. - dow = regexp.MustCompile("Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday") ) diff --git a/rubbish_cache_test.go b/caches_test.go similarity index 59% rename from rubbish_cache_test.go rename to caches_test.go index bc4066c..30b4d65 100644 --- a/rubbish_cache_test.go +++ b/caches_test.go @@ -8,6 +8,99 @@ import ( "github.com/stretchr/testify/assert" ) +func newLRUFromMap[K comparable, V any](m map[K]V) *lruCache[K, V] { + ret := newLRUCache[K, V](defCacheSz) + for k, v := range m { + ret.Add(k, v) + } + return ret +} + +func Test_addrResponseCache_Lookup(t *testing.T) { + defer func() { + NoCache = false + }() + type args struct { + searchText string + } + tests := []struct { + name string + NoCache bool // if true, set NoCache to true before running test + c *lruCache[string, *AddrResponse] + args args + wantResp *AddrResponse + wantOk bool + }{ + {"not in cache", + false, + newLRUFromMap(map[string]*AddrResponse{ + "xxx": &AddrResponse{Items: []Address{*testAddr}}, + }), + args{"yyy"}, + nil, + false, + }, + {"cached", + false, + newLRUFromMap(map[string]*AddrResponse{ + testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, + }), + args{testAddr.Address}, + &AddrResponse{Items: []Address{*testAddr}}, + true, + }, + {"cached, no cache mode", + true, + newLRUFromMap(map[string]*AddrResponse{ + testAddr.Address: &AddrResponse{Items: []Address{*testAddr}}, + }), + args{testAddr.Address}, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + NoCache = tt.NoCache + gotResp, gotOk := tt.c.Lookup(tt.args.searchText) + if !reflect.DeepEqual(gotResp, tt.wantResp) { + t.Errorf("addrResponseCache.Lookup() gotResp = %v, want %v", gotResp, tt.wantResp) + } + if gotOk != tt.wantOk { + t.Errorf("addrResponseCache.Lookup() gotOk = %v, want %v", gotOk, tt.wantOk) + } + }) + } +} + +func Test_addrResponseCache_Add(t *testing.T) { + type args struct { + searchText string + ar *AddrResponse + } + tests := []struct { + name string + c *lruCache[string, *AddrResponse] + args args + want *AddrResponse + }{ + {"add", + newLRUCache[string, *AddrResponse](10), + args{testAddr.Address, &AddrResponse{Items: []Address{*testAddr}}}, + &AddrResponse{Items: []Address{*testAddr}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.c.Add(tt.args.searchText, tt.args.ar) + + got, ok := tt.c.Lookup(tt.args.searchText) + assert.True(t, ok) + assert.Equal(t, got, tt.want) + }) + } +} + var ( tomorrow = &CollectionDayDetailResult{ Collections: []RubbishCollection{ @@ -128,25 +221,26 @@ func Test_rubbishResultCache_Add(t *testing.T) { ar *CollectionDayDetailResult } tests := []struct { - name string - c rubbishResultCache - args args - wantrubbishResultCache rubbishResultCache + name string + c *rubbishResultCache + args args + want *CollectionDayDetailResult }{ {"add", - rubbishResultCache{ + newRubbishCacheFromMap(map[string]*CollectionDayDetailResult{ testAddr.Address: tomorrow, - }, + }), args{testAddr.Address, tomorrow}, - rubbishResultCache{ - testAddr.Address: tomorrow, - }, + tomorrow, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.c.Add(tt.args.searchText, tt.args.ar) - assert.Equal(t, tt.wantrubbishResultCache, tt.c) + + res, ok := tt.c.Lookup(tt.args.searchText) + assert.True(t, ok) + assert.Equal(t, tt.want, res) }) } } diff --git a/cmd/aklapi/assets.go b/cmd/aklapi/assets.go index 9004621..5355356 100644 --- a/cmd/aklapi/assets.go +++ b/cmd/aklapi/assets.go @@ -17,7 +17,7 @@ const rootHTML = `
AT SOME POINT IN TIME, FUTURE WILL BE DIFFERENT FROM THE PRESENT.
+AT SOME POINT, THE FUTURE WILL BE DIFFERENT FROM THE PRESENT.