accelerator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ logs
6
+ nginx/*_temp
7
+ pkg
8
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ #Nginx Accelerator
2
+
3
+ Drop-in page caching using nginx, lua, and memcached.
4
+
5
+ ##Features
6
+
7
+ * Listens to Cache-Control max-age header
8
+ * The memcached key is the URI (easy to expire on demand)
9
+ * Really, really fast
10
+
11
+ ##Requirements
12
+
13
+ Nginx build with the following modules:
14
+
15
+ * [LuaJIT](http://wiki.nginx.org/HttpLuaModule)
16
+ * [MemcNginxModule](http://wiki.nginx.org/HttpMemcModule)
17
+ * [LuaRestyMemcachedLibrary](https://github.com/agentzh/lua-resty-memcached)
18
+
19
+ See the [Building OpenResty](#building-openresty) section below for instructions.
20
+
21
+ ##Install
22
+
23
+ luarocks install nginx-accelerator
24
+
25
+ ##Nginx config
26
+
27
+ Drop the following line in any `location` directive within `nginx.conf`:
28
+
29
+ access_by_lua "require('accelerator').access()";
30
+
31
+ For example:
32
+
33
+ http {
34
+ server {
35
+ listen 8080;
36
+
37
+ location = / {
38
+ access_by_lua "require('accelerator').access()";
39
+ }
40
+ }
41
+ }
42
+
43
+ The TTL is based on `Cache-Control: max-age`, but defaults to 10 seconds.
44
+
45
+ To configure your memcached connection information:
46
+
47
+ access_by_lua "require('accelerator').access({ host='127.0.0.1', port=11211 })";
48
+
49
+ ## Ruby client
50
+
51
+ ### Install gem
52
+
53
+ gem install accelerator
54
+
55
+ ### Example
56
+
57
+ cache = Accelerator.new("localhost:11211")
58
+ cache.get("/test")
59
+ cache.set("/test", "body")
60
+ cache.delete("/test")
61
+ cache.expire("/test", 10)
62
+
63
+ ## Running specs
64
+
65
+ ###Install Lua
66
+
67
+ brew install lua
68
+ brew install luarocks
69
+
70
+ ###Install PCRE
71
+
72
+ brew update
73
+ brew install pcre
74
+
75
+ ###Install [OpenResty](http://openresty.org) (nginx)
76
+
77
+ curl -O http://agentzh.org/misc/nginx/ngx_openresty-1.2.4.9.tar.gz
78
+ tar xzvf ngx_openresty-1.2.4.9.tar.gz
79
+ cd ngx_openresty-1.2.4.9/
80
+
81
+ Get your PCRE version:
82
+
83
+ brew info pcre
84
+
85
+ Replace **VERSION** below with the PCRE version:
86
+
87
+ ./configure --with-luajit --with-cc-opt="-I/usr/local/Cellar/pcre/VERSION/include" --with-ld-opt="-L/usr/local/Cellar/pcre/VERSION/lib"
88
+ make
89
+ make install
90
+
91
+ ###Start nginx
92
+
93
+ cd nginx-accelerator
94
+ ./nginx/start
95
+
96
+ ### Run specs
97
+
98
+ bundle install
99
+ spec spec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/accelerator ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path("../../lib/accelerator", __FILE__)
data/build ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ cd src
4
+
5
+ if [ "$1" == "watch" ]
6
+ then
7
+ moonc -t ../lib -w .
8
+ else
9
+ moonc -t ../lib .
10
+ fi
@@ -0,0 +1,95 @@
1
+ module("accelerator", package.seeall)
2
+ local json = require("cjson")
3
+ local memcached = require("resty.memcached")
4
+ local debug
5
+ debug = function(kind, msg)
6
+ if msg then
7
+ msg = kind .. ": " .. msg
8
+ else
9
+ msg = kind
10
+ end
11
+ return ngx.log(ngx.DEBUG, msg)
12
+ end
13
+ local memclient
14
+ memclient = function(opts)
15
+ if opts == nil then
16
+ opts = { }
17
+ end
18
+ local client, err = memcached:new()
19
+ if not client or err then
20
+ error(err or "problem creating client")
21
+ end
22
+ client:set_timeout(1000)
23
+ local ok
24
+ ok, err = client:connect((opts.host or "127.0.0.1"), (opts.port or 11211))
25
+ if not ok or err then
26
+ error(err or "problem connecting")
27
+ end
28
+ return client
29
+ end
30
+ local access
31
+ access = function(opts)
32
+ if ngx.var.request_method ~= "GET" then
33
+ return
34
+ end
35
+ if ngx.is_subrequest then
36
+ return
37
+ end
38
+ local fn
39
+ fn = function()
40
+ local memc = memclient(opts)
41
+ local cache, flags, err = memc:get(ngx.var.request_uri)
42
+ if err then
43
+ error(err)
44
+ end
45
+ if cache then
46
+ debug("read cache", cache)
47
+ cache = json.decode(cache)
48
+ if cache.header then
49
+ ngx.header = cache.header
50
+ end
51
+ if cache.body then
52
+ ngx.say(cache.body)
53
+ end
54
+ end
55
+ if not cache or os.time() - cache.time >= cache.ttl then
56
+ local co = coroutine.create(function()
57
+ cache = cache or { }
58
+ cache.time = os.time()
59
+ memc:set(ngx.var.request_uri, json.encode(cache))
60
+ local res = ngx.location.capture(ngx.var.request_uri)
61
+ if not res then
62
+ return
63
+ end
64
+ local ttl = nil
65
+ do
66
+ local cc = res.header["Cache-Control"]
67
+ if cc then
68
+ res.header["Cache-Control"] = nil
69
+ local x, x
70
+ x, x, ttl = string.find(cc, "max%-age=(%d+)")
71
+ end
72
+ end
73
+ if ttl then
74
+ ttl = tonumber(ttl)
75
+ debug("ttl", ttl)
76
+ end
77
+ res.time = os.time()
78
+ res.ttl = ttl or opts.ttl or 10
79
+ memc:set(ngx.var.request_uri, json.encode(res))
80
+ return debug("write cache")
81
+ end)
82
+ coroutine.resume(co)
83
+ end
84
+ if cache and cache.body then
85
+ return ngx.exit(ngx.HTTP_OK)
86
+ end
87
+ end
88
+ local status, err = pcall(fn)
89
+ if err then
90
+ return ngx.log(ngx.ERR, err)
91
+ end
92
+ end
93
+ return {
94
+ access = access
95
+ }
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'cgi'
3
+ require 'json'
4
+ require 'memcached'
5
+
6
+ class Accelerator
7
+
8
+ def initialize(host="localhost:11211")
9
+ @memc = Memcached.new(host)
10
+ end
11
+
12
+ def delete(uri)
13
+ @memc.delete(key(uri))
14
+ end
15
+
16
+ def expire(uri, ttl=nil)
17
+ if data = get_and_set_time(uri)
18
+ data[:ttl] = ttl if ttl
19
+ data[:time] -= data[:ttl] || 10
20
+ @memc.set(key(uri), data.to_json, 604800, false)
21
+ end
22
+ end
23
+
24
+ def get(uri)
25
+ if data = get_and_parse(uri)
26
+ [ data.delete(:body), data ]
27
+ end
28
+ end
29
+
30
+ def set(uri, body)
31
+ data = get_and_set_time(uri) || { :time => Time.now.to_i }
32
+ data[:body] = body
33
+ @memc.set(key(uri), data.to_json, 604800, false)
34
+ end
35
+
36
+ private
37
+
38
+ def key(k)
39
+ CGI.escape(k).gsub(/%../) { |s| s.downcase }
40
+ end
41
+
42
+ def get_and_parse(uri)
43
+ data = @memc.get(key(uri), nil) rescue nil
44
+ if data
45
+ JSON.parse(data, :symbolize_names => true)
46
+ end
47
+ end
48
+
49
+ def get_and_set_time(uri)
50
+ if data = get_and_parse(uri)
51
+ data[:time] = Time.now.to_i
52
+ end
53
+ data
54
+ end
55
+ end
data/nginx/nginx.conf ADDED
@@ -0,0 +1,34 @@
1
+ worker_processes 1;
2
+ error_log logs/error.log debug;
3
+ events {
4
+ worker_connections 1024;
5
+ }
6
+ http {
7
+ server {
8
+ listen 8080;
9
+
10
+ location = /test {
11
+
12
+ access_by_lua "
13
+ ngx.log(ngx.DEBUG, 'ACCESS PHASE')
14
+ require('accelerator').access({ host='127.0.0.1', port=11211 })
15
+ ";
16
+
17
+ proxy_pass http://127.0.0.1:8081/;
18
+ }
19
+ }
20
+
21
+ server {
22
+ listen 8081;
23
+
24
+ location = / {
25
+
26
+ more_set_headers "Cache-Control: max-age=1";
27
+
28
+ content_by_lua "
29
+ ngx.log(ngx.DEBUG, 'CONTENT PHASE')
30
+ ngx.say(os.time())
31
+ ";
32
+ }
33
+ }
34
+ }
data/nginx/start ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+
3
+ ./build
4
+ luarocks make
5
+ PATH=/usr/local/openresty/nginx/sbin:$PATH
6
+ export PATH
7
+ nginx -p `pwd`/nginx/ -c nginx.conf -s stop
8
+ nginx -p `pwd`/nginx/ -c nginx.conf
9
+ tail -f nginx/logs/error.log
@@ -0,0 +1,24 @@
1
+ package = "nginx-accelerator"
2
+ version = "1.0-1"
3
+ source = {
4
+ url = "http://github.com/winton/nginx-accelerator"
5
+ }
6
+ description = {
7
+ summary = "Nginx-level memcaching for fun and profit",
8
+ detailed = [[
9
+ Uses information from Cache-Control headers to memcache responses
10
+ at the nginx level.
11
+ ]],
12
+ homepage = "http://github.com/winton/nginx-accelerator",
13
+ license = "MIT"
14
+ }
15
+ dependencies = {
16
+ "lua >= 5.1",
17
+ "lua-cjson >= 2.1.0-1"
18
+ }
19
+ build = {
20
+ type = "builtin",
21
+ modules = {
22
+ ["accelerator"] = "lib/accelerator.lua"
23
+ }
24
+ }
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ root = File.expand_path('../', __FILE__)
3
+ lib = "#{root}/lib"
4
+
5
+ $:.unshift lib unless $:.include?(lib)
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "accelerator"
9
+ s.version = '0.1.0'
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = [ "Winton Welsh" ]
12
+ s.email = [ "mail@wintoni.us" ]
13
+ s.homepage = "http://github.com/winton/nginx-accelerator"
14
+ s.summary = %q{Drop-in page caching using nginx, lua, and memcached}
15
+ s.description = %q{Drop-in page caching using nginx, lua, and memcached.}
16
+
17
+ s.executables = `cd #{root} && git ls-files bin/*`.split("\n").collect { |f| File.basename(f) }
18
+ s.files = `cd #{root} && git ls-files`.split("\n")
19
+ s.require_paths = %w(lib)
20
+ s.test_files = `cd #{root} && git ls-files -- {features,test,spec}/*`.split("\n")
21
+
22
+ s.add_dependency "memcached", "~> 1.5.0"
23
+ s.add_development_dependency "rspec", "~> 1.0"
24
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Accelerator do
4
+
5
+ before(:all) do
6
+ @accelerator = Accelerator.new
7
+ @accelerator.delete("/test")
8
+ end
9
+
10
+ it "should create cache on first request" do
11
+ time = Time.new.to_i
12
+ request.should == time
13
+ body, options = @accelerator.get("/test")
14
+ response_tests(time)
15
+ end
16
+
17
+ it "should return same cache on next request" do
18
+ $expires_at = Time.now.to_f.ceil
19
+ request.should == $last_time
20
+ body, options = @accelerator.get("/test")
21
+ response_tests($last_time)
22
+ end
23
+
24
+ it "should not expire naturally before 1 second" do
25
+ if Time.now.to_f < $expires_at
26
+ expires_in = $expires_at - Time.now.to_f
27
+ request(0.5 * expires_in).should == $last_time
28
+ end
29
+ end
30
+
31
+ it "should expire naturally after 1 second" do
32
+ if Time.now.to_f < $expires_at
33
+ expires_in = $expires_at - Time.now.to_f
34
+ request(expires_in).should == $last_time
35
+ end
36
+ response_tests(Time.new.to_i)
37
+ end
38
+
39
+ it "should obey client expiration" do
40
+ @accelerator.expire("/test")
41
+ request.should == $last_time
42
+ response_tests($last_time)
43
+ end
44
+
45
+ it "should set cache body from the client" do
46
+ @accelerator.set("/test", "123")
47
+ $expires_at = Time.now.to_f.ceil
48
+ request.should == 123
49
+ end
50
+
51
+ it "should not expire naturally before 1 second" do
52
+ if Time.now.to_f < $expires_at
53
+ expires_in = $expires_at - Time.now.to_f
54
+ request(0.5 * expires_in).should == 123
55
+ end
56
+ end
57
+
58
+ it "should expire naturally after 1 second" do
59
+ if Time.now.to_f < $expires_at
60
+ expires_in = $expires_at - Time.now.to_f
61
+ request(expires_in).should == 123
62
+ end
63
+ response_tests(Time.new.to_i)
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ require "pp"
2
+ require "bundler"
3
+
4
+ Bundler.require(:development)
5
+
6
+ $root = File.expand_path('../../', __FILE__)
7
+
8
+ require "#{$root}/lib/accelerator"
9
+
10
+ def request(wait=nil)
11
+ sleep(wait) if wait
12
+ `curl -s http://localhost:8080/test`.strip.to_i
13
+ end
14
+
15
+ def response_tests(time)
16
+ $last_time = time
17
+ body, options = @accelerator.get("/test")
18
+ body.strip.should == time.to_s
19
+ options.should == {
20
+ :ttl => 1,
21
+ :status => 200,
22
+ :header => {
23
+ :"Content-Length" => 11,
24
+ :"Content-Type" => "text/plain"
25
+ },
26
+ :time => time
27
+ }
28
+ end
@@ -0,0 +1,99 @@
1
+ module "accelerator", package.seeall
2
+
3
+
4
+ -- Dependencies
5
+
6
+ json = require "cjson"
7
+ memcached = require "resty.memcached"
8
+
9
+
10
+ -- Debug log
11
+
12
+ debug = (kind, msg) ->
13
+ msg = if msg then kind .. ": " .. msg else kind
14
+ ngx.log(ngx.DEBUG, msg)
15
+
16
+
17
+ -- Create memcached client
18
+
19
+ memclient = (opts={}) ->
20
+ client, err = memcached\new()
21
+ error(err or "problem creating client") if not client or err
22
+
23
+ -- Set memcached connection timeout to 1 sec
24
+ client\set_timeout(1000)
25
+
26
+ -- Connect to memcached server
27
+ ok, err = client\connect((opts.host or "127.0.0.1"), (opts.port or 11211))
28
+ error(err or "problem connecting") if not ok or err
29
+
30
+ client
31
+
32
+
33
+ -- Execute within access_by_lua:
34
+ -- http://wiki.nginx.org/HttpLuaModule#access_by_lua
35
+
36
+ access = (opts) ->
37
+ return if ngx.var.request_method ~= "GET"
38
+ return if ngx.is_subrequest
39
+
40
+ fn = ->
41
+ memc = memclient(opts)
42
+
43
+ cache, flags, err = memc\get(ngx.var.request_uri)
44
+ error(err) if err
45
+
46
+ if cache
47
+ debug("read cache", cache)
48
+ cache = json.decode(cache)
49
+
50
+ if cache.header
51
+ ngx.header = cache.header
52
+
53
+ if cache.body
54
+ ngx.say(cache.body)
55
+
56
+ -- Rewrite cache if cache does not exist or ttl has expired
57
+ if not cache or os.time() - cache.time >= cache.ttl
58
+ co = coroutine.create ->
59
+
60
+ -- Immediately update the time to prevent multiple writes
61
+ cache = cache or {}
62
+ cache.time = os.time()
63
+ memc\set(ngx.var.request_uri, json.encode(cache))
64
+
65
+ -- Make subrequest
66
+ res = ngx.location.capture(ngx.var.request_uri)
67
+ return if not res
68
+
69
+ -- Parse TTL
70
+ ttl = nil
71
+
72
+ if cc = res.header["Cache-Control"]
73
+ res.header["Cache-Control"] = nil
74
+ x, x, ttl = string.find(cc, "max%-age=(%d+)")
75
+
76
+ if ttl
77
+ ttl = tonumber(ttl)
78
+ debug("ttl", ttl)
79
+
80
+ res.time = os.time()
81
+ res.ttl = ttl or opts.ttl or 10
82
+
83
+ -- Write cache
84
+ memc\set(ngx.var.request_uri, json.encode(res))
85
+ debug("write cache")
86
+
87
+ coroutine.resume(co)
88
+
89
+ -- Prevent further phases from executing if body rendered
90
+ if cache and cache.body
91
+ ngx.exit(ngx.HTTP_OK)
92
+
93
+ status, err = pcall(fn)
94
+ ngx.log(ngx.ERR, err) if err
95
+
96
+
97
+ -- Return
98
+
99
+ return { access: access }
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: accelerator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Winton Welsh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: memcached
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.5.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ description: Drop-in page caching using nginx, lua, and memcached.
47
+ email:
48
+ - mail@wintoni.us
49
+ executables:
50
+ - accelerator
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - bin/accelerator
60
+ - build
61
+ - lib/accelerator.lua
62
+ - lib/accelerator.rb
63
+ - nginx-accelerator-1.0-1.rockspec
64
+ - nginx-accelerator.gemspec
65
+ - nginx/nginx.conf
66
+ - nginx/start
67
+ - spec/accelerator_spec.rb
68
+ - spec/spec_helper.rb
69
+ - src/accelerator.moon
70
+ homepage: http://github.com/winton/nginx-accelerator
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.23
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Drop-in page caching using nginx, lua, and memcached
94
+ test_files:
95
+ - spec/accelerator_spec.rb
96
+ - spec/spec_helper.rb