racknga 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/NEWS.ja.rdoc CHANGED
@@ -1,5 +1,16 @@
1
1
  = お知らせ
2
2
 
3
+ == 0.9.1: 2010-11-11
4
+
5
+ * キャッシュの正当性チェックを強化。
6
+ * User-Agent毎のキャッシュをサポート。
7
+ * JSONPをサポートするミドルウェアを追加。
8
+ * HTTPのRangeをサポートするミドルウェアを追加。
9
+ * groongaデータベースにアクセスログを記録するミドルウェアを追加。
10
+ * Passengerの状態を視覚化するMuninプラグインを追加。
11
+ * ライセンスをLGPLv2.1からLGPLv2.1 or laterに変更。
12
+ * will_paginate対応。
13
+
3
14
  == 0.9.0: 2010-07-04
4
15
 
5
16
  * 最初のリリース!
data/NEWS.rdoc CHANGED
@@ -1,5 +1,9 @@
1
1
  = NEWS
2
2
 
3
+ == 0.9.1: 2010-11-11
4
+
5
+ * Supported will_paginate.
6
+
3
7
  == 0.9.0: 2010-07-04
4
8
 
5
9
  * Initial release!
data/README.ja.rdoc CHANGED
@@ -6,7 +6,8 @@ racknga(らくんが)
6
6
 
7
7
  == 説明
8
8
 
9
- rroongaの機能を利用したRackのミドルウェアを提供します。
9
+ rroongaの機能を利用したRackのミドルウェアとrroongaの機能は利
10
+ 用してませんが役に立つミドルウェアを提供します。
10
11
 
11
12
  * rroonga: http://groonga.rubyforge.org/
12
13
  * Rack: http://rack.rubyforge.org/
@@ -17,7 +18,11 @@ Kouhei Sutou:: <tt><kou@clear-code.com></tt>
17
18
 
18
19
  == ライセンス
19
20
 
20
- LGPL 2.1です。詳しくはlicense/LGPLを見てください。
21
+ LGPL 2.1またはそれ以降のバージョンです。詳しくは
22
+ license/lgpl-2.1.txtを見てください。
23
+
24
+ (コントリビュートされたパッチなども含み、Kouhei Sutouが
25
+ ライセンスを変更する権利を持ちます。)
21
26
 
22
27
  == 依存ソフトウェア
23
28
 
@@ -25,13 +30,14 @@ LGPL 2.1です。詳しくはlicense/LGPLを見てください。
25
30
  * rroonga
26
31
  * Rack
27
32
 
28
- == インストール
33
+ === あるとよいソフトウェア
29
34
 
30
- % sudo gem install racknga
35
+ * jpmobile
36
+ * will_paginate
31
37
 
32
- == ドキュメント
38
+ == インストール
33
39
 
34
- http://groonga.rubyforge.org/racknga/
40
+ % sudo gem install racknga
35
41
 
36
42
  == メーリングリスト
37
43
 
@@ -39,6 +45,36 @@ http://groonga.rubyforge.org/racknga/
39
45
 
40
46
  http://lists.sourceforge.jp/mailman/listinfo/groonga-dev
41
47
 
48
+ == ドキュメント
49
+
50
+ RackngaにはRackアプリケーションに有用なミドルウェアをたくさん
51
+ 提供しています。ここでは、オプションなど詳細な情報は
52
+ http://groonga.rubyforge.org/racknga/ を参照してください。
53
+
54
+ === Racknga::Middleware::Cache
55
+
56
+ ...
57
+
58
+ === Racknga::Middleware::Deflater
59
+
60
+ ...
61
+
62
+ === Racknga::Middleware::ExceptionNotifier
63
+
64
+ ...
65
+
66
+ === Racknga::Middleware::JSONP
67
+
68
+ ...
69
+
70
+ === Racknga::Middleware::Range
71
+
72
+ ...
73
+
74
+ === Racknga::Middleware::Log
75
+
76
+ ...
77
+
42
78
  == 感謝
43
79
 
44
80
  * ...
data/README.rdoc CHANGED
@@ -17,7 +17,10 @@ Kouhei Sutou:: <tt><kou@clear-code.com></tt>
17
17
 
18
18
  == License
19
19
 
20
- LGPL 2.1. See license/LGPL for details.
20
+ LGPL 2.1 or later. See license/lgpl-2.1.txt for details.
21
+
22
+ (Kouhei Sutou has a right to change the license
23
+ inclidng contributed patches.)
21
24
 
22
25
  == Dependencies
23
26
 
@@ -25,6 +28,10 @@ LGPL 2.1. See license/LGPL for details.
25
28
  * rroonga
26
29
  * Rack
27
30
 
31
+ == Suggested libraries
32
+
33
+ * will_paginate
34
+
28
35
  == Install
29
36
 
30
37
  % sudo gem install racknga
data/Rakefile CHANGED
@@ -105,7 +105,7 @@ Hoe.spec('racknga') do
105
105
  ["rack"]]
106
106
  project.spec_extras = {
107
107
  :extra_rdoc_files => Dir.glob("**/*.rdoc"),
108
- :licenses => ["LGPL 2.1"]
108
+ :licenses => ["LGPL 2.1 or later"]
109
109
  }
110
110
  project.readme_file = "README.ja.rdoc"
111
111
 
data/html/footer.html.erb CHANGED
@@ -6,6 +6,11 @@
6
6
  <img src="/rubyforge.png" width="120" height="24" border="0" alt="Ruby/groongaプロジェクトはRubyForge.orgにホスティングしてもらっています。" />
7
7
  </a>
8
8
  </p>
9
+ <p id="sponsor-github">
10
+ <a href="http://github.com/ranguba/">
11
+ ラングバプロジェクトはGitHubにホスティングしてもらっています。
12
+ </a>
13
+ </p>
9
14
  <p id="sponsor-tango">
10
15
  <a href="http://tango.freedesktop.org/">
11
16
  <img width="120" height="53" border="0" alt="Tango Desktop Projectのアイコンを利用しています。" src="/tango-logo.png" />
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
8
9
  #
9
10
  # This library is distributed in the hope that it will be useful,
10
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -15,6 +16,10 @@
15
16
  # License along with this library; if not, write to the Free Software
16
17
  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18
 
19
+ require 'fileutils'
20
+
21
+ require 'groonga'
22
+
18
23
  module Racknga
19
24
  class CacheDatabase
20
25
  def initialize(database_path)
@@ -27,10 +32,22 @@ module Racknga
27
32
  @context["Responses"]
28
33
  end
29
34
 
30
- def purge_old_responses(threshold_time_stamp=nil)
31
- threshold_time_stamp ||= Time.now
35
+ def configurations
36
+ @context["Configurations"]
37
+ end
38
+
39
+ def configuration
40
+ configurations["default"]
41
+ end
42
+
43
+ def purge_old_responses
44
+ age_modulo = 2 ** 32
45
+ age = configuration.age
46
+ previous_age = (age - 1).modulo(age_modulo)
47
+ configuration.age = (age + 1).modulo(age_modulo)
48
+
32
49
  responses.each do |response|
33
- response.delete if response.created_at < threshold_time_stamp
50
+ response.delete if response.age == previous_age
34
51
  end
35
52
  end
36
53
 
@@ -41,6 +58,7 @@ module Racknga
41
58
  create_database
42
59
  end
43
60
  ensure_tables
61
+ ensure_default_configuration
44
62
  end
45
63
 
46
64
  def close_database
@@ -55,12 +73,24 @@ module Racknga
55
73
  :key_type => "ShortText") do |table|
56
74
  table.uint32("status")
57
75
  table.short_text("headers")
58
- table.text("body")
76
+ table.text("body", :compress => :zlib)
77
+ table.short_text("checksum")
78
+ table.uint32("age")
59
79
  table.time("created_at")
60
80
  end
61
81
  end
62
82
  end
63
83
 
84
+ def create_configurations_table
85
+ Groonga::Schema.define(:context => @context) do |schema|
86
+ schema.create_table("Configurations",
87
+ :type => :hash,
88
+ :key_type => "ShortText") do |table|
89
+ table.uint32("age")
90
+ end
91
+ end
92
+ end
93
+
64
94
  def create_database
65
95
  FileUtils.mkdir_p(File.dirname(@database_path))
66
96
  @database = Groonga::Database.create(:path => @database_path,
@@ -68,8 +98,12 @@ module Racknga
68
98
  end
69
99
 
70
100
  def ensure_tables
71
- return if responses
101
+ create_configurations_table
72
102
  create_responses_table
73
103
  end
104
+
105
+ def ensure_default_configuration
106
+ configurations.add("default")
107
+ end
74
108
  end
75
109
  end
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
8
9
  #
9
10
  # This library is distributed in the hope that it will be useful,
10
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -19,6 +20,7 @@ require 'time'
19
20
  require 'net/smtp'
20
21
  require 'etc'
21
22
  require 'socket'
23
+ require 'nkf'
22
24
 
23
25
  require 'racknga/utils'
24
26
 
@@ -0,0 +1,108 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
+
19
+ require 'fileutils'
20
+
21
+ require 'groonga'
22
+
23
+ module Racknga
24
+ class LogDatabase
25
+ def initialize(database_path)
26
+ @database_path = database_path
27
+ @context = Groonga::Context.new(:encoding => :none)
28
+ ensure_database
29
+ end
30
+
31
+ def entries
32
+ @entries ||= @context["Entries"]
33
+ end
34
+
35
+ def ensure_database
36
+ if File.exist?(@database_path)
37
+ @database = Groonga::Database.open(@database_path, :context => @context)
38
+ else
39
+ create_database
40
+ end
41
+ ensure_tables
42
+ end
43
+
44
+ def close_database
45
+ @database.close
46
+ end
47
+
48
+ def purge_old_entries(base_time=nil)
49
+ base_time ||= Time.now - 60 * 60 * 24
50
+ entries.select do |record|
51
+ record.time_stamp < base_time
52
+ end.each do |record|
53
+ record.key.delete
54
+ end
55
+ end
56
+
57
+ private
58
+ def create_tables
59
+ Groonga::Schema.define(:context => @context) do |schema|
60
+ schema.create_table("Tags",
61
+ :type => :patricia_trie,
62
+ :key_type => "ShortText") do |table|
63
+ end
64
+
65
+ schema.create_table("Paths",
66
+ :type => :patricia_trie,
67
+ :key_type => "ShortText") do |table|
68
+ end
69
+
70
+ schema.create_table("UserAgents",
71
+ :type => :hash,
72
+ :key_type => "ShortText") do |table|
73
+ end
74
+
75
+ schema.create_table("Entries") do |table|
76
+ table.time("time_stamp")
77
+ table.reference("tag", "Tags")
78
+ table.reference("path", "Paths")
79
+ table.reference("user_agent", "UserAgents")
80
+ table.float("runtime")
81
+ table.short_text("message", :compress => :zlib)
82
+ end
83
+
84
+ schema.change_table("Tags") do |table|
85
+ table.index("Entries.tag")
86
+ end
87
+
88
+ schema.change_table("Paths") do |table|
89
+ table.index("Entries.path")
90
+ end
91
+
92
+ schema.change_table("UserAgents") do |table|
93
+ table.index("Entries.user_agent")
94
+ end
95
+ end
96
+ end
97
+
98
+ def create_database
99
+ FileUtils.mkdir_p(File.dirname(@database_path))
100
+ @database = Groonga::Database.create(:path => @database_path,
101
+ :context => @context)
102
+ end
103
+
104
+ def ensure_tables
105
+ create_tables
106
+ end
107
+ end
108
+ end
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
8
9
  #
9
10
  # This library is distributed in the hope that it will be useful,
10
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -15,12 +16,35 @@
15
16
  # License along with this library; if not, write to the Free Software
16
17
  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18
 
19
+ require 'digest/md5'
18
20
  require 'yaml'
19
21
  require 'racknga/cache_database'
20
22
 
21
23
  module Racknga
22
24
  module Middleware
25
+ class PerUserAgentCache
26
+ def initialize(application)
27
+ @application = application
28
+ end
29
+
30
+ def call(environment)
31
+ mobile = environment["rack.jpmobile"]
32
+ if mobile
33
+ last_component = mobile.class.name.split(/::/).last
34
+ user_agent_key = "mobile:#{last_component.downcase}"
35
+ else
36
+ user_agent_key = "pc"
37
+ end
38
+ key = environment[Cache::KEY_KEY]
39
+ environment[Cache::KEY_KEY] = [key, user_agent_key].join(":")
40
+ @application.call(environment)
41
+ end
42
+ end
43
+
23
44
  class Cache
45
+ KEY_KEY = "racknga.cache.key"
46
+ START_TIME_KEY = "racknga.cache.start_time"
47
+
24
48
  def initialize(application, options={})
25
49
  @application = application
26
50
  @options = Utils.normalize_options(options || {})
@@ -32,13 +56,15 @@ module Racknga
32
56
  def call(environment)
33
57
  request = Rack::Request.new(environment)
34
58
  return @application.call(environment) unless use_cache?(request)
35
- key = "#{key_prefix(request)}:#{normalize_path(request.fullpath)}"
59
+ age = @database.configuration.age
60
+ key = environment[KEY_KEY] || request.fullpath
61
+ environment[START_TIME_KEY] = Time.now
36
62
  cache = @database.responses
37
63
  record = cache[key]
38
- if record
39
- handle_request_with_cache(cache, key, record, request)
64
+ if record and record.age == age
65
+ handle_request_with_cache(cache, key, age, record, request)
40
66
  else
41
- handle_request(cache, key, request)
67
+ handle_request(cache, key, age, request)
42
68
  end
43
69
  end
44
70
 
@@ -55,22 +81,8 @@ module Racknga
55
81
  requeust.get? or requeust.head?
56
82
  end
57
83
 
58
- def key_prefix(request)
59
- if request.respond_to?(:mobile?) and request.mobile?
60
- last_component = request.mobile.class.name.split(/::/).last
61
- "mobile:#{last_component.downcase}"
62
- else
63
- "pc"
64
- end
65
- end
66
-
67
- def normalize_path(path)
68
- path.gsub(/&callback=jsonp\d+&_=\d+\z/, '')
69
- end
70
-
71
84
  def skip_caching_response?(status, headers, body)
72
85
  return true if status != 200
73
- return true if status != 200
74
86
 
75
87
  headers = Rack::Utils::HeaderHash.new(headers)
76
88
  content_type = headers["Content-Type"]
@@ -85,9 +97,10 @@ module Racknga
85
97
  true
86
98
  end
87
99
 
88
- def handle_request(cache, key, request)
100
+ def handle_request(cache, key, age, request)
89
101
  status, headers, body = @application.call(request.env)
90
102
  if skip_caching_response?(status, headers, body)
103
+ log("skip", request)
91
104
  return [status, headers, body]
92
105
  end
93
106
 
@@ -99,23 +112,59 @@ module Racknga
99
112
  stringified_body << data
100
113
  end
101
114
  headers = headers.to_hash
115
+ encoded_headers = headers.to_yaml
116
+ encoded_body = stringified_body.force_encoding("ASCII-8BIT")
102
117
  cache[key] = {
103
118
  :status => status,
104
- :headers => headers.to_yaml,
105
- :body => stringified_body.force_encoding("ASCII-8BIT"),
119
+ :headers => encoded_headers,
120
+ :body => encoded_body,
121
+ :checksum => compute_checksum(status, encoded_headers, encoded_body),
122
+ :age => age,
106
123
  :created_at => now,
107
124
  }
108
125
  body = [stringified_body]
126
+ log("store", request)
109
127
  [status, headers, body]
110
128
  end
111
129
 
112
- def handle_request_with_cache(cache, key, record, request)
113
- body = record["body"]
114
- return handle_request(cache, key, request) if body.nil?
130
+ def handle_request_with_cache(cache, key, age, record, request)
131
+ status = record.status
132
+ headers = record.headers
133
+ body = record.body
134
+ checksum = record.checksum
135
+ unless valid_cache?(status, headers, body, checksum)
136
+ log("invalid", request)
137
+ return handle_request(cache, key, age, request)
138
+ end
139
+
140
+ log("hit", request)
141
+ [status, YAML.load(headers), [body]]
142
+ end
143
+
144
+ def compute_checksum(status, encoded_headers, encoded_body)
145
+ md5 = Digest::MD5.new
146
+ md5 << status.to_s
147
+ md5 << ":"
148
+ md5 << encoded_headers
149
+ md5 << ":"
150
+ md5 << encoded_body
151
+ md5.hexdigest.force_encoding("ASCII-8BIT")
152
+ end
153
+
154
+ def valid_cache?(status, encoded_headers, encoded_body, checksum)
155
+ return false if status.nil? or encoded_headers.nil? or encoded_body.nil?
156
+ return false if checksum.nil?
157
+ compute_checksum(status, encoded_headers, encoded_body) == checksum
158
+ end
115
159
 
116
- status = record["status"]
117
- headers = YAML.load(record["headers"])
118
- [status, headers, [body]]
160
+ def log(tag, request)
161
+ return unless Middleware.const_defined?(:Log)
162
+ env = request.env
163
+ logger = env[Middleware::Log::LOGGER_KEY]
164
+ return if logger.nil?
165
+ start_time = env[START_TIME_KEY]
166
+ runtime = Time.now - start_time
167
+ logger.log("cache-#{tag}", request.fullpath, :runtime => runtime)
119
168
  end
120
169
  end
121
170
  end
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
8
9
  #
9
10
  # This library is distributed in the hope that it will be useful,
10
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -4,7 +4,8 @@
4
4
  #
5
5
  # This library is free software; you can redistribute it and/or
6
6
  # modify it under the terms of the GNU Lesser General Public
7
- # License version 2.1 as published by the Free Software Foundation.
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
8
9
  #
9
10
  # This library is distributed in the hope that it will be useful,
10
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation; either
8
+ # version 2.1 of the License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
+
19
+ module Racknga
20
+ module Middleware
21
+ class JSONP
22
+ def initialize(application)
23
+ @application = application
24
+ end
25
+
26
+ def call(environment)
27
+ request = Rack::Request.new(environment)
28
+ callback = request["callback"]
29
+ update_cache_key(request) if callback
30
+ status, headers, body = @application.call(environment)
31
+ return [status, headers, body] unless callback
32
+ return [status, headers, body] unless json_response?(headers)
33
+ body = Writer.new(callback, body)
34
+ [status, headers, body]
35
+ end
36
+
37
+ private
38
+ def update_cache_key(request)
39
+ return unless Middleware.const_defined?(:Cache)
40
+ cache_key_key = Cache::KEY_KEY
41
+
42
+ path = request.fullpath
43
+ path, parameters = path.split(/\?/, 2)
44
+ if parameters
45
+ parameters = parameters.split(/[&;]/).reject do |parameter|
46
+ key, value = parameter.split(/\=/, 2)
47
+ key == "callback" or (key == "_" and value = /\A\d+\z/)
48
+ end.join("&")
49
+ path << "?" << parameters unless parameters.empty?
50
+ end
51
+
52
+ key = request.env[cache_key_key]
53
+ request.env[cache_key_key] = [key, path].compact.join(":")
54
+ end
55
+
56
+ def json_response?(headers)
57
+ content_type = Rack::Utils::HeaderHash.new(headers)["Content-Type"]
58
+ content_type == "application/json" or
59
+ content_type == "application/javascript" or
60
+ content_type == "text/javascript"
61
+ end
62
+
63
+ class Writer
64
+ def initialize(callback, body)
65
+ @callback = callback
66
+ @body = body
67
+ end
68
+
69
+ def each(&block)
70
+ block.call("#{@callback}(")
71
+ @body.each(&block)
72
+ block.call(");")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end