From f34056f241c744d20595e4eb2a32235a0399e7e5 Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 09:02:33 +0200 Subject: [PATCH 1/6] add cache file and a unit test --- cache/cache.go | 92 +++++++++++++++++++++++++++++++++++++++++++++ cache/cache_test.go | 34 +++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 cache/cache.go create mode 100644 cache/cache_test.go diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..96cde8b --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,92 @@ +/* +Copyright 2024 Adora Laura Kalb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "net/netip" + "sync" + "time" +) + +var ( + GlobalScoreCache *ScoreCache + cacheExpiredAfter = 5 * time.Minute +) + +type ServerScore struct { + expiresAt time.Time + Score float64 +} + +type ScoreCache struct { + stop chan struct{} + + mu sync.RWMutex + + scores map[netip.Addr]ServerScore +} + +type CacheMissError struct{} + +func (m *CacheMissError) Error() string { + return "User is not in cache!" +} + +func newCacheMissError() *CacheMissError { + return &CacheMissError{} +} + +func NewLocalCache() *ScoreCache { + lc := &ScoreCache{ + scores: make(map[netip.Addr]ServerScore), + stop: make(chan struct{}), + } + + return lc +} + +func (sc *ScoreCache) Add(score float64, ip netip.Addr, ts time.Time) { + ssc := ServerScore{Score: score, expiresAt: ts.Add(cacheExpiredAfter)} + sc.mu.Lock() + sc.scores[ip] = ssc + sc.mu.Unlock() +} + +func (lc *ScoreCache) Get(ip netip.Addr) (ServerScore, error) { + now := time.Now() + lc.mu.RLock() + defer lc.mu.RUnlock() + + cachedScore, ok := lc.scores[ip] + if !ok { + return ServerScore{}, newCacheMissError() + } + + if now.After(cachedScore.expiresAt) { + lc.delete(ip) + return ServerScore{}, newCacheMissError() + } + + return cachedScore, nil +} + +func (lc *ScoreCache) delete(ip netip.Addr) { + lc.mu.Lock() + + delete(lc.scores, ip) + lc.mu.Unlock() +} diff --git a/cache/cache_test.go b/cache/cache_test.go new file mode 100644 index 0000000..38be9a9 --- /dev/null +++ b/cache/cache_test.go @@ -0,0 +1,34 @@ +/* +Copyright 2024 Adora Laura Kalb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "net/netip" + "testing" + "time" +) + +func TestCacheGet(t *testing.T) { + const testFloat = 1.23456 + cache := NewLocalCache() + cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), time.Now().Add(5*time.Minute)) + + score, _ := cache.Get(netip.MustParseAddr("1.2.3.4")) + if score.Score != testFloat { + t.Fatalf("cache.Get(\"1.2.3.4\") = %f, want %f", score.Score, testFloat) + } +} From 3cc1653e11d00cb1d7d37088583f8faf121bfbd6 Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 09:02:38 +0200 Subject: [PATCH 2/6] add makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e329429 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ + +test: + go test ./... From c713e16ed23a2d4f26d25782b24b6258303375c2 Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 10:57:38 +0200 Subject: [PATCH 3/6] add test CI --- .woodpecker/golang-test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .woodpecker/golang-test.yml diff --git a/.woodpecker/golang-test.yml b/.woodpecker/golang-test.yml new file mode 100644 index 0000000..e825a0f --- /dev/null +++ b/.woodpecker/golang-test.yml @@ -0,0 +1,9 @@ +when: + - event: push + +steps: + test: + image: golang:1.22-bullseye + commands: + - go mod download + - go test -v ./... From 36e9aa49f7aa536d9b04189a5f07d22581a73aac Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 11:09:06 +0200 Subject: [PATCH 4/6] fix deadlock with waiting mutex --- cache/cache.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cache/cache.go b/cache/cache.go index 96cde8b..6d0b9bd 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -69,12 +69,13 @@ func (sc *ScoreCache) Add(score float64, ip netip.Addr, ts time.Time) { func (lc *ScoreCache) Get(ip netip.Addr) (ServerScore, error) { now := time.Now() lc.mu.RLock() - defer lc.mu.RUnlock() cachedScore, ok := lc.scores[ip] if !ok { + lc.mu.RUnlock() return ServerScore{}, newCacheMissError() } + lc.mu.RUnlock() if now.After(cachedScore.expiresAt) { lc.delete(ip) From 2a574d6f3b832c332295e9c733b227d8f1ec58e0 Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 11:10:00 +0200 Subject: [PATCH 5/6] add test for cache miss --- cache/cache_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cache/cache_test.go b/cache/cache_test.go index 38be9a9..254b5d3 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -32,3 +32,18 @@ func TestCacheGet(t *testing.T) { t.Fatalf("cache.Get(\"1.2.3.4\") = %f, want %f", score.Score, testFloat) } } + +func TestCacheMiss(t *testing.T) { + now := time.Now() + const testFloat = 1.23456 + + expiredTime := now.Add(-(time.Minute * 10)) + + cache := NewLocalCache() + cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), expiredTime) + + _, err := cache.Get(netip.MustParseAddr("1.2.3.4")) + if err == nil { + t.Fatalf("cache.Get(\"1.2.3.4\") = got nil, want error") + } +} From 265cbd5abfbdd4feb725daace4a0f188c0dd5224 Mon Sep 17 00:00:00 2001 From: Adora Laura Kalb Date: Fri, 19 Apr 2024 11:40:05 +0200 Subject: [PATCH 6/6] add caching functionality --- cache/cache.go | 2 +- cache/cache_test.go | 4 ++-- collector/collector.go | 30 +++++++++++++++++++++++++----- helpers/helpers.go | 25 +++++++++++++++++++++++++ main.go | 3 +++ models/api_response.go | 8 ++++---- 6 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 helpers/helpers.go diff --git a/cache/cache.go b/cache/cache.go index 6d0b9bd..dfd64d8 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -50,7 +50,7 @@ func newCacheMissError() *CacheMissError { return &CacheMissError{} } -func NewLocalCache() *ScoreCache { +func NewScoreCache() *ScoreCache { lc := &ScoreCache{ scores: make(map[netip.Addr]ServerScore), stop: make(chan struct{}), diff --git a/cache/cache_test.go b/cache/cache_test.go index 254b5d3..e5f1e96 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -24,7 +24,7 @@ import ( func TestCacheGet(t *testing.T) { const testFloat = 1.23456 - cache := NewLocalCache() + cache := NewScoreCache() cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), time.Now().Add(5*time.Minute)) score, _ := cache.Get(netip.MustParseAddr("1.2.3.4")) @@ -39,7 +39,7 @@ func TestCacheMiss(t *testing.T) { expiredTime := now.Add(-(time.Minute * 10)) - cache := NewLocalCache() + cache := NewScoreCache() cache.Add(testFloat, netip.MustParseAddr("1.2.3.4"), expiredTime) _, err := cache.Get(netip.MustParseAddr("1.2.3.4")) diff --git a/collector/collector.go b/collector/collector.go index 7a332e4..77ef95d 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -27,6 +27,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" + "golang.adora.codes/ntppool-exporter/cache" "golang.adora.codes/ntppool-exporter/models" ) @@ -61,7 +62,24 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { } func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) { - httpError := false + score, err := cache.GlobalScoreCache.Get(c.target) + if err == nil { + level.Debug(logger).Log("msg", "Serving score from cache", + "server", c.target.String(), "score", score.Score) + + serverScoreMetric := prometheus.NewDesc("ntppool_server_score", + "Shows the server score currently assigned at ntppool.org", + nil, nil) + + m1 := prometheus.MustNewConstMetric(serverScoreMetric, prometheus.GaugeValue, score.Score) + m1 = prometheus.NewMetricWithTimestamp(time.Now(), m1) + ch <- m1 + return + } + + level.Debug(logger).Log("msg", "Cache miss, querying API", + "server", c.target.String()) + var serverApiScore float64 = 0 const apiEndpoint = "https://www.ntppool.org/scores/" const apiQuery = "/json?limit=1&monitor=24" @@ -73,7 +91,7 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) { if err != nil { level.Error(logger).Log("msg", "Error sending HTTP request", "url", url, "message", err) - httpError = true + return } res, err := client.Do(req) if err != nil { @@ -97,9 +115,11 @@ func (c Collector) collect(ch chan<- prometheus.Metric, logger log.Logger) { return } - if !httpError { - serverApiScore = response.History[0].Score - } + serverApiScore = response.History[0].Score + + cache.GlobalScoreCache.Add(serverApiScore, c.target, time.Now()) + level.Debug(logger).Log("msg", "Added score to cache", + "server", c.target.String(), "score", score.Score) // TODO: Test or delete serverScoreMetric := prometheus.NewDesc("ntppool_server_score", diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 0000000..98ccb04 --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,25 @@ +/* +Copyright 2024 Adora Laura Kalb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helpers + +import ( + "time" +) + +func UnixToTime(i int64) time.Time { + return time.Unix(i, 0) +} diff --git a/main.go b/main.go index ac10d7f..07b6f58 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ import ( "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" + "golang.adora.codes/ntppool-exporter/cache" "golang.adora.codes/ntppool-exporter/collector" ) @@ -95,6 +96,8 @@ func main() { kingpin.Parse() logger := promlog.New(promlogConfig) + cache.GlobalScoreCache = cache.NewScoreCache() + level.Info(logger).Log("msg", "Starting ntppool_exporter", "version", version.Info()) level.Info(logger).Log("build_context", version.BuildContext()) diff --git a/models/api_response.go b/models/api_response.go index 3dcd88d..014ee77 100644 --- a/models/api_response.go +++ b/models/api_response.go @@ -34,8 +34,8 @@ type ApiResponse struct { } type ApiResponseHistory struct { - Timestamp int `json:"ts"` - Step int `json:"step"` - Score float64 `json:"score"` - MonitorID int `json:"monitor_id"` + TimestampInt int64 `json:"ts"` + Step int `json:"step"` + Score float64 `json:"score"` + MonitorID int `json:"monitor_id"` }