Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/httpcache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# httpcache

An example that caches HTTP server responses based on request path and query parameters. To run the
example, use the following command:

```
go run cmd/main.go
```

This will spin a HTTP server on port `:8080` with the `/reports/{name}` route. The first time you
call this endpoint with a custom name, it will take around 5 seconds to complete. All subsequent calls
within one minute using the same name will use a cached response and will take milliseconds to complete.

The endpoint can be called using `curl`:

```
curl 127.0.0.1:8080/reports/hello
```
47 changes: 47 additions & 0 deletions examples/httpcache/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

import (
"context"
"httpcache/internal/server"
"log/slog"
"os/signal"
"syscall"
)

func main() {
shutdown := runServer()

ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
)
defer cancel()

<-ctx.Done()

shutdown()
}

// runServer starts the HTTP server and returns a shutdown function.
func runServer() func() {
srv := server.NewServer(":8080")

stopCh := make(chan struct{})

go func() {
defer close(stopCh)

if err := srv.Start(); err != nil {
slog.Default().With("error", err).Error("unexpected server closure")
}
}()

return func() {
if err := srv.Stop(); err != nil {
slog.Default().With("error", err).Error("stopping server")
}

<-stopCh
}
}
7 changes: 7 additions & 0 deletions examples/httpcache/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module httpcache

go 1.24.2

require github.com/jellydator/ttlcache/v3 v3.3.0

require golang.org/x/sync v0.8.0 // indirect
14 changes: 14 additions & 0 deletions examples/httpcache/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
119 changes: 119 additions & 0 deletions examples/httpcache/internal/server/respcache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package respcache provides a logic for caching HTTP responses.
package respcache

import (
"bytes"
"log/slog"
"net/http"
"time"

"github.com/jellydator/ttlcache/v3"
)

// Cache contains required information to
// cache HTTP requests.
type Cache struct {
log *slog.Logger
cache *ttlcache.Cache[string, cacheItem]
}

// NewCache creates a new Cache instance with the specified TTL.
func NewCache(ttl time.Duration) *Cache {
c := &Cache{
log: slog.Default().With("component", "cache"),
cache: ttlcache.New(
ttlcache.WithTTL[string, cacheItem](ttl),
),
}

go c.cache.Start()

return c
}

// Stop stops the automatic cleanup process.
// It blocks until the cleanup process exits.
func (c Cache) Stop() {
c.cache.Stop()
}

// Handle is a middleware that caches the response
// based on the request path and query parameters.
func (c Cache) Handle(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.URL.RawPath + r.URL.RawQuery

// We check if the response is already cached
// by looking up the key in the cache.
item := c.cache.Get(key)
if item != nil {
ci := item.Value()

w.WriteHeader(ci.statusCode)
w.Write(ci.body)

return
}

// We create a custom response writer to capture the
// response body so that we can cache it.
rw := &responseWriter{
w: w,
body: &bytes.Buffer{},

// We set a default status code here,
// as using Write without WriteHeader automatically
// sets the status code to http.StatusOK.
statusCode: http.StatusOK,
}

next.ServeHTTP(rw, r)

// After the response is written, we cache it
// using the key we built earlier.
c.cache.Set(
key,
cacheItem{
body: rw.body.Bytes(),
statusCode: rw.statusCode,
},
ttlcache.DefaultTTL,
)
})
}

// cacheItem represents a cached item in the cache.
type cacheItem struct {
body []byte
statusCode int
}

// responseWriter is a helper struct that is used to intercept
// http.ResponseWriter's Write method.
type responseWriter struct {
w http.ResponseWriter
body *bytes.Buffer
statusCode int
}

// Write writes the data to the connection as part of an HTTP reply.
func (wr *responseWriter) Write(buf []byte) (int, error) {
n, err := wr.body.Write(buf)
if err != nil {
// unlikely to happen
return n, err
}

return wr.w.Write(buf)
}

// Header returns the header map that is sent by the WriteHeader.
func (wr *responseWriter) Header() http.Header {
return wr.w.Header()
}

// WriteHeader sends an HTTP response header with the provided status code.
func (wr *responseWriter) WriteHeader(statusCode int) {
wr.statusCode = statusCode
wr.w.WriteHeader(statusCode)
}
77 changes: 77 additions & 0 deletions examples/httpcache/internal/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package server provides functionality to create and run an HTTP server.
package server

import (
"context"
"fmt"
"httpcache/internal/server/respcache"
"log/slog"
"net/http"
"time"
)

// Server contains required information to
// run an HTTP server.
type Server struct {
log *slog.Logger
cache *respcache.Cache
serv *http.Server
}

// NewServer creates a new Server instance with
// the specified address.
func NewServer(addr string) *Server {
s := &Server{
log: slog.Default().With("component", "server"),
cache: respcache.NewCache(time.Minute),
}

s.serv = &http.Server{
Addr: addr,
Handler: s.router(),
}

return s
}

// Start starts the server. It blocks until the server.Stop is called.
func (s *Server) Start() error {
s.log.With("addr", s.serv.Addr).Info("started web server")

return s.serv.ListenAndServe()
}

// Stop shuts down the server.
func (s *Server) Stop() error {
s.cache.Stop()

return s.serv.Shutdown(context.Background())
}

// router sets up the HTTP routes for the server.
func (s Server) router() http.Handler {
mux := http.NewServeMux()

mux.Handle("GET /reports/{name}", s.cache.Handle(s.fetchReport))

return mux
}

// fetchReport is the handler that fetches report information based on
// the report name provided in the URL path.
func (s Server) fetchReport(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")

// For the demonstration purposes, the timer below acts
// as a placeholder for the actual report fetching logic.
select {
case <-time.After(5 * time.Second):
// OK.
case <-r.Context().Done():
w.WriteHeader(http.StatusBadRequest)
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Report %q fetched successfully!", name)))
}
Loading