diff options
-rw-r--r-- | pkg/api/api0/client.go | 21 | ||||
-rw-r--r-- | pkg/api/api0/serverlist.go | 55 |
2 files changed, 75 insertions, 1 deletions
diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go index d3097b1..5a8aaf6 100644 --- a/pkg/api/api0/client.go +++ b/pkg/api/api0/client.go @@ -7,6 +7,7 @@ import ( "net/http" "net/netip" "strconv" + "strings" "time" "github.com/pg9182/atlas/pkg/origin" @@ -455,7 +456,25 @@ func (h *Handler) handleClientServers(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json; charset=utf-8") - respMaybeCompress(w, r, http.StatusOK, h.ServerList.csGetJSON()) + + buf := h.ServerList.csGetJSON() + for _, e := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { + if t, _, _ := strings.Cut(e, ";"); strings.TrimSpace(t) == "gzip" { + if zbuf, ok := h.ServerList.csGetJSONGzip(); ok { + buf = zbuf + w.Header().Set("Content-Encoding", "gzip") + } else { + hlog.FromRequest(r).Error().Msg("failed to gzip server list") + } + break + } + } + + w.Header().Set("Content-Length", strconv.Itoa(len(buf))) + w.WriteHeader(http.StatusOK) + if r.Method != http.MethodHead { + w.Write(buf) + } } /* diff --git a/pkg/api/api0/serverlist.go b/pkg/api/api0/serverlist.go index de48ae0..9c9d04e 100644 --- a/pkg/api/api0/serverlist.go +++ b/pkg/api/api0/serverlist.go @@ -2,6 +2,7 @@ package api0 import ( "bytes" + "compress/gzip" "errors" "fmt" "net/netip" @@ -35,6 +36,12 @@ type ServerList struct { csUpdateWg sync.WaitGroup // allows other goroutines to wait for that update to complete csBytes atomic.Pointer[[]byte] // contents of buffer must not be modified; only swapped + // /client/servers gzipped json + csgzUpdate atomic.Pointer[*byte] // pointer to the first byte of the last known json (works because it must be swapped, not modified) + csgzUpdateMu sync.Mutex // ensures only one update runs at a time + csgzUpdateWg sync.WaitGroup // allows other goroutines to wait for that update to complete + csgzBytes atomic.Pointer[[]byte] // gzipped + // for unit tests __clock func() time.Time } @@ -239,6 +246,54 @@ func (s *ServerList) csGetJSON() []byte { return b.Bytes() } +// csGetJSONGzip is like csGetJSON, but returns it gzipped with true, or false +// if an error occurs. +func (s *ServerList) csGetJSONGzip() ([]byte, bool) { + buf := s.csGetJSON() + if len(buf) == 0 { + return nil, false + } + cur := &buf[0] + + last := s.csgzUpdate.Load() + if last != nil && *last == cur { + if zbuf := s.csgzBytes.Load(); zbuf != nil && *zbuf != nil { + return *zbuf, true + } + } + + if !s.csgzUpdateMu.TryLock() { + s.csgzUpdateWg.Wait() + if zbuf := s.csgzBytes.Load(); zbuf != nil && *zbuf != nil { + return *zbuf, true + } + return nil, false + } else { + defer s.csgzUpdateMu.Unlock() + s.csgzUpdateWg.Add(1) + defer s.csgzUpdateWg.Done() + } + + var b bytes.Buffer + zw := gzip.NewWriter(&b) + if _, err := zw.Write(buf); err != nil { + s.csgzBytes.Store(nil) + s.csgzUpdate.Store(&cur) + return nil, false + } + if err := zw.Close(); err != nil { + s.csgzBytes.Store(nil) + s.csgzUpdate.Store(&cur) + return nil, false + } + + zbuf := b.Bytes() + s.csgzBytes.Store(&zbuf) + s.csgzUpdate.Store(&cur) + + return zbuf, true +} + // csUpdateNextUpdateTime updates the next update time for the cached // /client/servers response. It must be called after any time updates while // holding a write lock on s.mu. |