Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement cache and replay #216

Closed
wants to merge 21 commits into from
Closed

Implement cache and replay #216

wants to merge 21 commits into from

Conversation

hueich
Copy link
Collaborator

@hueich hueich commented Oct 15, 2017

This implementation of cache and replay is backed by boltdb. #75

Features:

  • Optional cache updating
  • Optional cache replaying
  • Optional hermetic mode
  • Basic built-in cache key generator that just hashes the HTTP method and URL
  • Custom cache key support via context value, so any prefix modifier can set the cache key

Future work:

  • Separate keygen modifier that reads an HTTP request header, e.g. X-Martian-Cache-Key, to use as custom cache key
  • Fancier keygen modifiers that:
    • Sort URL query params?
    • Include other HTTP headers?
    • Include the request body?

@hueich hueich self-assigned this Oct 15, 2017
@hueich
Copy link
Collaborator Author

hueich commented Oct 18, 2017

A potential improvement is making the cached []byte value be an Entry struct, which would contain not only the serialized http.Response, but also the http.Request and other meta information, so it can be extended in the future, as well as aid in debugging. Let me know what you think.


const (
// CustomKey is the context key for setting a custom cache key for a request.
CustomKey = "cache.CustomKey"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does CustomKey need to be exported?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This allows other modifiers to to set custom cache keys via setting it in the context. E.g. a modifier that looks for a specific header, or a modifier that looks for a specific URL param, etc. This allows flexibility in controlling how a cache key is generated via modifiers.

// If `replay` is true, the modifier will replay responses from its cache.
// If `hermetic` is true, the modifier will return error if it cannot replay a cached response, e.g. on cache miss or not replaying.
// Argument combinations that don't make sense will return error, e.g. replay=false and hermetic=true.
func NewModifier(filepath, bucket string, update, replay, hermetic bool) (martian.RequestResponseModifier, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have this constructor only take the filepath - the rest should be reasonable defaults that can be modified by setters. By default, the modifier should be replay mode.

func NewModifier(filepath) (martian.RequestResponseModifier, error) {

func (m *modifier) SetBucketName(name string) {

etc

return nil, fmt.Errorf("cache.Modifier: bucket name cannot be empty if updating or replaying")
}

opt := &bolt.Options{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline

}
// TODO: Figure out how to close the db after use.

if bucket != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably have a default bucket name

return nil
}

// ModifyRequest modifies the http.Request.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More descriptive comment

}

// getCacheKey gets a cache key from the request context or generates a new one.
func getCacheKey(req *http.Request) ([]byte, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of holding the key in the request context? Generating the key on the fly is pretty inexpensive, and this would only be helpful if we were generating it several times over the course of the request / response - and I only see us doing it twice (once on request, once on response).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so any upstream request modifiers can set a custom cache key, instead of using the naive default one here. I think storing it in the context is better than in the HTTP header or URL param, so it doesn't pollute the HTTP request.

return nil
}

// ModifyResponse modifies the http.Response.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better godoc comment

ctx := martian.NewContext(res.Request)
cached, ok := ctx.Get(cachedResponseCtxKey)
if ok {
*res = *cached.(*http.Response)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if cached, ok := ctx.Get(cachedResponseCtxKey); ok {
  *res = *cached.(*http.Response)
  return nil
}

and then you don't need else

ctx := martian.NewContext(res.Request)
cached, ok := ctx.Get(cachedResponseCtxKey)
if ok {
*res = *cached.(*http.Response)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should make res.Request the request that we're responding to - not the request associated with the cached response
rather than wholesale swapping out the response, might it be more clear to replace the body and the headers of res?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch about the res.Request. Will update it.

For the response, I swapped it out entirely, since the one we start with is a canned response anyway. Besides the body and headers, there's also the HTTP major/minor versions, and possible trailers. Is there any concern with just swapping it out? The only thing I can think of is if there is a response modifier upstream that had data it wanted to preserve. However, those modifiers should be downstream from the cache modifier, so they can modify the cached response.

@hueich
Copy link
Collaborator Author

hueich commented Apr 15, 2018

PTAL.

@micaelAlastor
Copy link

Is this in usable state atm?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants