Compare commits

..

3 Commits

Author SHA1 Message Date
Xe Iaso
4b3ee0d4d4 test(nginx): remove docker compose calls here
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-28 18:47:36 -05:00
Xe Iaso
dd58fd64c3 test(nginx): remove -it here
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-28 18:33:59 -05:00
Xe Iaso
264187f90b feat: expose pprof endpoints over metrics listener
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-12-28 18:24:05 -05:00
7 changed files with 38 additions and 26 deletions

View File

@@ -17,6 +17,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/http/pprof"
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
@@ -542,6 +543,12 @@ func metricsServer(ctx context.Context, lg slog.Logger, done func()) {
} }
}) })
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()} srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind) listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
lg.Debug("listening for metrics", "url", metricsUrl) lg.Debug("listening for metrics", "url", metricsUrl)

View File

@@ -19,7 +19,7 @@ type Impl[K comparable, V any] struct {
// stopCh stops the background cleanup worker. // stopCh stops the background cleanup worker.
stopCh chan struct{} stopCh chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
lock sync.Mutex lock sync.RWMutex
} }
type decayMapEntry[V any] struct { type decayMapEntry[V any] struct {
@@ -64,28 +64,26 @@ func (m *Impl[K, V]) expire(key K) bool {
// Delete a value from the DecayMap by key. // Delete a value from the DecayMap by key.
// //
// This defers deletions to a background thread for performance reasons.
//
// If the value does not exist, return false. Return true after // If the value does not exist, return false. Return true after
// deletion. // deletion.
func (m *Impl[K, V]) Delete(key K) bool { func (m *Impl[K, V]) Delete(key K) bool {
select { // Use a single write lock to avoid RUnlock->Lock convoy.
// Defer decay deletion to the background worker to avoid convoy. m.lock.Lock()
case m.deleteCh <- deleteReq[K]{key: key, expiry: time.Now().Add(-1 * time.Second)}: defer m.lock.Unlock()
return m.expire(key) _, ok := m.data[key]
default: if ok {
// Channel full: drop request; a future Cleanup() or Get will retry. delete(m.data, key)
return true
} }
return ok
} }
// Get gets a value from the DecayMap by key. // Get gets a value from the DecayMap by key.
// //
// If a value has expired, forcibly delete it if it was not updated. // If a value has expired, forcibly delete it if it was not updated.
func (m *Impl[K, V]) Get(key K) (V, bool) { func (m *Impl[K, V]) Get(key K) (V, bool) {
m.lock.Lock() m.lock.RLock()
defer m.lock.Unlock()
value, ok := m.data[key] value, ok := m.data[key]
m.lock.RUnlock()
if !ok { if !ok {
return Zilch[V](), false return Zilch[V](), false
@@ -131,8 +129,8 @@ func (m *Impl[K, V]) Cleanup() {
// Len returns the number of entries in the DecayMap. // Len returns the number of entries in the DecayMap.
func (m *Impl[K, V]) Len() int { func (m *Impl[K, V]) Len() int {
m.lock.Lock() m.lock.RLock()
defer m.lock.Unlock() defer m.lock.RUnlock()
return len(m.data) return len(m.data)
} }
@@ -148,7 +146,7 @@ func (m *Impl[K, V]) Close() {
func (m *Impl[K, V]) cleanupWorker() { func (m *Impl[K, V]) cleanupWorker() {
defer m.wg.Done() defer m.wg.Done()
batch := make([]deleteReq[K], 0, 64) batch := make([]deleteReq[K], 0, 64)
ticker := time.NewTicker(15 * time.Minute) ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
flush := func() { flush := func() {

View File

@@ -30,7 +30,15 @@ func TestImpl(t *testing.T) {
t.Error("got value even though it was supposed to be expired") t.Error("got value even though it was supposed to be expired")
} }
dm.Cleanup() // Deletion of expired entries after Get is deferred to a background worker.
// Assert it eventually disappears from the map.
deadline := time.Now().Add(200 * time.Millisecond)
for time.Now().Before(deadline) {
if dm.Len() == 0 {
break
}
time.Sleep(5 * time.Millisecond)
}
if dm.Len() != 0 { if dm.Len() != 0 {
t.Fatalf("expected background cleanup to remove expired key; len=%d", dm.Len()) t.Fatalf("expected background cleanup to remove expired key; len=%d", dm.Len())
} }

View File

@@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- The memory store now decays values every 15 minutes instead of every 10 milliseconds. - Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309)).
- Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309)) - Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
<!-- This changes the project to: --> <!-- This changes the project to: -->

View File

@@ -27,7 +27,7 @@ type impl struct {
} }
func (i *impl) Delete(_ context.Context, key string) error { func (i *impl) Delete(_ context.Context, key string) error {
if _, ok := i.store.Get(key); !ok { if !i.store.Delete(key) {
return fmt.Errorf("%w: %q", store.ErrNotFound, key) return fmt.Errorf("%w: %q", store.ErrNotFound, key)
} }

View File

@@ -57,6 +57,10 @@ func Common(t *testing.T, f store.Factory, config json.RawMessage) {
t.Error("wanted test to not exist in store but it exists anyways") t.Error("wanted test to not exist in store but it exists anyways")
} }
if err := s.Delete(t.Context(), t.Name()); err == nil {
t.Errorf("key %q does not exist and Delete did not return non-nil", t.Name())
}
return nil return nil
}, },
}, },
@@ -79,6 +83,7 @@ func Common(t *testing.T, f store.Factory, config json.RawMessage) {
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if err := tt.doer(t, s); !errors.Is(err, tt.err) { if err := tt.doer(t, s); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err) t.Logf("want: %v", tt.err)
t.Logf("got: %v", err) t.Logf("got: %v", err)

View File

@@ -7,18 +7,12 @@ source ../lib/lib.sh
set -euo pipefail set -euo pipefail
build_anubis_ko
mint_cert mimi.techaro.lol mint_cert mimi.techaro.lol
docker run --rm -it \ docker run --rm \
-v ./conf/nginx:/etc/nginx:ro \ -v ./conf/nginx:/etc/nginx:ro \
-v ../pki:/techaro/pki:ro \ -v ../pki:/techaro/pki:ro \
nginx \ nginx \
nginx -t nginx -t
docker compose up -d
docker compose down -t 1 || :
docker compose rm -f || :
exit 0 exit 0