Skip to content

Commit

Permalink
beatlabs#128 first iteration of server cache functionality
Browse files Browse the repository at this point in the history
Signed-off-by: Vangelis Katikaridis <[email protected]>
  • Loading branch information
drakos74 committed Apr 1, 2020
1 parent 14bdf80 commit 7e3c6b9
Show file tree
Hide file tree
Showing 8 changed files with 1,676 additions and 259 deletions.
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,82 @@ route := NewRoute("/index", "GET" ProcessorFunc, true, ...MiddlewareFunc)
routeWithAuth := NewAuthRoute("/index", "GET" ProcessorFunc, true, Authendicator, ...MiddlewareFunc)
```

### HTTP Caching

The caching layer for HTTP routes is specified per Route.

```go
type routeCache struct {
// path is the route path, which the cache is enabled for
path string
// processor is the processor function for the route
processor sync.ProcessorFunc
// cache is the cache implementation to be used
cache cache.Cache
// ttl is the time to live for all cached objects
ttl time.Duration
// instant is the timing function for the cache expiry calculations
instant TimeInstant
// minAge specifies the minimum amount of max-age header value for client cache-control requests
minAge uint
// max-fresh specifies the maximum amount of min-fresh header value for client cache-control requests
maxFresh uint
// staleResponse specifies if the server is willing to send stale responses
// if a new response could not be generated for any reason
staleResponse bool
}
```

#### server cache
- The **cache key** is based on the route path and the url request parameters.
- The server caches only **GET requests**.
- The server implementation must specify a **Time to Live policy** upon construction.
- The route should return always the most fresh object instance.
- An **ETag header** must be always in responses that are part of the cache, representing the hash of the response.
- Requests within the time-to-live threshold, will be served from the cache.
Otherwise the request will be handled as usual by the route processor function.
The resulting response will be cached for future requests.
- Requests that cannot be processed due to any kind of error, but are found in the cache,
will be returned to the client with a `Warning` header present in the response.
ONLY IF : this option is specified in the server with the `staleResponse` parameter set to `true`

```
Note : The server is unaware of the cache time-to-live policy itself.
The cache might evict entries based on it's internal configuration.
This is transparent to the server. As long as a key cannot be found in the cache,
the server will execute the route processor function and fill the corresponding cache entry
```

#### client cache-control
The client can control the cache with the appropriate Headers
- `max-age=?`

returns the cached instance only if the age of the instance is lower than the max-age parameter.
This parameter is bounded from below by the server option `minAge`.
This is to avoid chatty clients with no cache control policy (or very aggressive max-age policy) to effectively disable the cache
- `min-fesh=?`

returns the cached instance if the time left for expiration is lower than the provided parameter.
This parameter is bounded from above by the server option `maxFresh`.
This is to avoid chatty clients with no cache control policy (or very aggressive min-fresh policy) to effectively disable the cache
- `max-stale=?`

returns the cached instance, even if it has expired within the provided bound by the client.
This response should always be accompanied by a `must-revalidate` response header.
- `no-cache` / `no-store`

returns a new response to the client by executing the route processing function.
NOTE : Except for cases where a `minAge` or `maxFresh` parameter has been specified in the server.
This is again a safety mechanism to avoid 'aggressive' clients put unexpected load on the server.
The server is responsible to cap the refresh time, BUT must respond with a `Warning` header in such a case.
- `only-if-cached`

expects any response that is found in the cache, otherwise returns an empty response

#### cache design reference
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9

### Asynchronous

The implementation of the async processor follows exactly the same principle as the sync processor.
Expand Down Expand Up @@ -370,4 +446,4 @@ GET /ready

Both can return either a `200 OK` or a `503 Service Unavailable` status code (default: `200 OK`).

It is possible to customize their behaviour by injecting an `http.AliveCheck` and/or an `http.ReadyCheck` `OptionFunc` to the HTTP component constructor.
It is possible to customize their behaviour by injecting an `http.AliveCheck` and/or an `http.ReadyCheck` `OptionFunc` to the HTTP component constructor.
2 changes: 2 additions & 0 deletions component/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ func handler(hnd ProcessorFunc) http.HandlerFunc {

h := extractHeaders(r)

// TODO : pass url to the Request
req := NewRequest(f, r.Body, h, dec)
// TODO : manage warning error type by adding warning to headers
rsp, err := hnd(ctx, req)
if err != nil {
handleError(logger, w, enc, err)
Expand Down
1 change: 1 addition & 0 deletions component/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func (r *Request) Decode(v interface{}) error {
// Response definition of the sync response model.
type Response struct {
Payload interface{}
Headers map[string]string
}

// NewResponse creates a new response.
Expand Down
109 changes: 89 additions & 20 deletions component/http/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,38 +214,107 @@ func NewRoutesBuilder() *RoutesBuilder {
}

type CachedRouteBuilder struct {
path string
processor sync.ProcessorFunc
cache cache.Cache
instant TimeInstant
errors []error
path string
processor sync.ProcessorFunc
cache cache.Cache
instant TimeInstant
ttl time.Duration
minAge uint
maxFresh uint
staleResponse bool
errors []error
}

// WithTimeInstant specifies a time instant function for checking expiry.
func (cb *CachedRouteBuilder) WithTimeInstant(instant TimeInstant) *CachedRouteBuilder {
if instant == nil {
cb.errors = append(cb.errors, errors.New("time instant is nil"))
}
cb.instant = instant
return cb
}

// WithTimeInstant adds a time to live parameter to control the cache expiry policy.
func (cb *CachedRouteBuilder) WithTimeToLive(ttl time.Duration) *CachedRouteBuilder {
if ttl <= 0 {
cb.errors = append(cb.errors, errors.New("time to live must be greater than `0`"))
}
cb.ttl = ttl
return cb
}

// WithMinAge adds a minimum age for the cache responses.
// This will avoid cases where a single client with high request rate and no cache control headers might effectively disable the cache
// This means that if this parameter is missing (e.g. is equal to '0' , the cache can effectively be made obsolete in the above scenario)
func (cb *CachedRouteBuilder) WithMinAge(minAge uint) *CachedRouteBuilder {
cb.minAge = minAge
return cb
}

func NewCachedRouteBuilder(path string, processor sync.ProcessorFunc) *CachedRouteBuilder {
// WithMinFresh adds a minimum age for the cache responses.
// This will avoid cases where a single client with high request rate and no cache control headers might effectively disable the cache
// This means that if this parameter is missing (e.g. is equal to '0' , the cache can effectively be made obsolete in the above scenario)
func (cb *CachedRouteBuilder) WithMaxFresh(maxFresh uint) *CachedRouteBuilder {
cb.maxFresh = maxFresh
return cb
}

// WithStaleResponse allows the cache to return stale responses.
func (cb *CachedRouteBuilder) WithStaleResponse(staleResponse bool) *CachedRouteBuilder {
cb.staleResponse = staleResponse
return cb
}

return &CachedRouteBuilder{
func (cb *CachedRouteBuilder) Create() (*routeCache, error) {
//if len(cb.errors) > 0 {
//ttl > 0
//maxfresh < ttl
return &routeCache{}, nil
//}
}

func NewRouteCache(path string, processor sync.ProcessorFunc, cache cache.Cache) *routeCache {
if strings.ReplaceAll(path, " ", "") == "" {

}
return &routeCache{
path: path,
processor: processor,
cache: cache,
instant: func() int64 {
return time.Now().Unix()
},
}
}

// WithTimeInstant adds authenticator.
func (cb *CachedRouteBuilder) WithTimeInstant(instant TimeInstant) *CachedRouteBuilder {
if instant == nil {
cb.errors = append(cb.errors, errors.New("time instant is nil"))
// ToGetRouteBuilder transforms the cached builder to a GET endpoint builder
// while propagating any errors
func (cb *CachedRouteBuilder) ToGetRouteBuilder() *RouteBuilder {
routeCache, err := cb.Create()
if err == nil {

}
cb.instant = instant
return cb
rb := NewRouteBuilder(cb.path, cacheHandler(cb.processor, routeCache)).MethodGet()
rb.errors = append(rb.errors, cb.errors...)
return rb
}

// WithCache adds authenticator.
func (cb *CachedRouteBuilder) WithCache(cache cache.Cache) *RouteBuilder {
if cache == nil {
// let it break later
return NewRouteBuilder(cb.path, nil)
}
return NewRouteBuilder(cb.path, cacheHandler(cb.processor, cb.cache, cb.instant))
type routeCache struct {
// path is the route path, which the cache is enabled for
path string
// processor is the processor function for the route
processor sync.ProcessorFunc
// cache is the cache implementation to be used
cache cache.Cache
// ttl is the time to live for all cached objects
ttl time.Duration
// instant is the timing function for the cache expiry calculations
instant TimeInstant
// minAge specifies the minimum amount of max-age header value for client cache-control requests
minAge uint
// max-fresh specifies the maximum amount of min-fresh header value for client cache-control requests
maxFresh uint
// staleResponse specifies if the server is willing to send stale responses
// if a new response could not be generated for any reason
staleResponse bool
}
2 changes: 1 addition & 1 deletion examples/sixth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func main() {
if err != nil {
log.Fatalf("failed to init the cache %v", err)
}
cachedRoute := patronhttp.NewCachedRouteBuilder("/", sixth).WithCache(cache).MethodGet()
cachedRoute := patronhttp.NewCachedRouteBuilder("/", sixth, cache).ToGetRouteBuilder()

ctx := context.Background()
err = patron.New(name, version).WithRoutesBuilder(patronhttp.NewRoutesBuilder().Append(cachedRoute)).Run(ctx)
Expand Down
Loading

0 comments on commit 7e3c6b9

Please sign in to comment.