Skip to content

Commit

Permalink
[http] experimental cache store
Browse files Browse the repository at this point in the history
  • Loading branch information
mikz committed May 3, 2017
1 parent bb69f13 commit 0a1a9f7
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 0 deletions.
43 changes: 43 additions & 0 deletions apicast/src/resty/http_ng/backend/cache.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
local setmetatable = setmetatable

------------
--- HTTP
-- HTTP client
-- @module http_ng.backend

local _M = {}

local mt = { __index = _M }

function _M.new(backend, options)
local opts = options or {}
return setmetatable({
backend = backend, cache_store = opts.cache_store
}, mt)
end

--- Send request and return the response
-- @tparam http_ng.request request
-- @treturn http_ng.response
function _M:send(request)
local cache_store = self.cache_store
local backend = self.backend

if cache_store then
local res, err = cache_store:get(request)

if not res then

res, err = backend:send(request)
if res and not err then
cache_store:set(res)
end
end

return res, err
else
return backend:send(request)
end
end

return _M
116 changes: 116 additions & 0 deletions apicast/src/resty/http_ng/cache_store.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
local lrucache = require 'resty.lrucache'
local ngx_re = require 'ngx.re'
local http_headers = require 'resty.http_ng.headers'

local setmetatable = setmetatable
local gsub = string.gsub
local lower = string.lower
local format = string.format

local _M = { default_size = 1000 }

local mt = { __index = _M }

function _M.new(size)
return setmetatable({ cache = lrucache.new(size or _M.default_size) }, mt)
end

local function request_cache_key(request)
-- TODO: verify if this is correct cache key and what are the implications
-- FIXME: missing headers, ...
return format('%s:%s', request.method, request.url)
end

function _M:get(request)
local cache = self.cache
if not cache then
return nil, 'not initialized'
end

-- TODO: verify it is valid request per the RFC: https://tools.ietf.org/html/rfc7234#section-3

local cache_key = request_cache_key(request)

if not cache_key then return end

local res, stale = cache:get(cache_key)

-- TODO: handle stale responses per the RFC: https://tools.ietf.org/html/rfc7234#section-4.2.4
local serve_stale = false

if res then
return res
elseif serve_stale then
-- TODO: generate Warning header per the RFC: https://tools.ietf.org/html/rfc7234#section-4.2.4
return stale
end
end


local function parse_cache_control(value)
if not value then return end

local res, err = ngx_re.split(value, '\\s*,\\s*', 'oj')

local cache_control = {}

local t = {}

for i=1, #res do
local res, err = ngx_re.split(res[i], '=', 'oj', nil, 2, t)

if err then
ngx.log(ngx.WARN, err)
else
-- TODO: selectively handle quoted strings per the RFC: https://tools.ietf.org/html/rfc7234#section-5.2
cache_control[gsub(lower(res[1]), '-', '_')] = tonumber(res[2]) or res[2] or true
end
end

if err then
ngx.log(ngx.WARN, err)
end

return cache_control
end

local function response_cache_key(response)
return request_cache_key(response.request)
end

local function response_ttl(response)
local cache_control = parse_cache_control(response.headers.cache_control)

return cache_control.max_age
end


function _M:set(response)
local cache = self.cache

if not cache then
return nil, 'not initialized'
end

-- TODO: verify it is valid response per the RFC: https://tools.ietf.org/html/rfc7234#section-3
if not response then return end

local cache_key = response_cache_key(response)

if not cache_key then return end

local ttl = response_ttl(response)

if ttl then
local res = {
body = response.body,
headers = http_headers.new(response.headers),
status = response.status
}

cache:set(cache_key, res, ttl)
end
end


return _M
52 changes: 52 additions & 0 deletions spec/resty/http_ng/backend/cache_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
local _M = require 'resty.http_ng.backend.cache'
local fake_backend = require 'fake_backend_helper'
local spy = require 'luassert.spy'
local http_response = require 'resty.http_ng.response'
local cache_store = require 'resty.http_ng.cache_store'

local inspect = require 'inspect'
describe('cache backend', function()
describe('GET method', function()
local function cache(res, options)
local fake = fake_backend.new(res)
return _M.new(fake, options)
end

it('accesses the url', function()
local res = spy.new(function(req) return http_response.new(req, 200, { }, 'ok') end)

local response, err = cache(res):send{method = 'GET', url = 'http://example.com/' }

assert.falsy(err)
assert.truthy(response)

assert.spy(res).was.called(1)

assert.equal('ok', response.body)
end)

it('accesses caches the call', function()
local res = spy.new(function(req)
return http_response.new(req, 200, {
cache_control = 'private, max-age=10'
}, 'ok')
end)

local backend = cache(res, { cache_store = cache_store.new() })

local function check()
local response, err = backend:send{method = 'GET', url = 'http://example.com/' }

assert.spy(res).was.called(1)

assert.truthy(response)
assert.falsy(err)

assert.equal('ok', response.body)
end

check()
check()
end)
end)
end)
19 changes: 19 additions & 0 deletions spec/resty/http_ng/cache_store_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
local _M = require 'resty.http_ng.cache_store'

describe('HTTP cache store', function()

describe('.new', function()
local cache_store = _M.new()

assert.truthy(cache_store.get)
assert.truthy(cache_store.set)
end)

describe(':get', function()
pending('fetches response from cache')
end)

describe(':set', function()
pending('stores response in cache')
end)
end)

0 comments on commit 0a1a9f7

Please sign in to comment.