racknga 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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