thin 1.0.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of thin might be problematic. Click here for more details.
- data/CHANGELOG +26 -0
- data/example/async_app.ru +126 -0
- data/example/async_chat.ru +247 -0
- data/example/async_tailer.ru +100 -0
- data/example/config.ru +1 -1
- data/ext/thin_parser/parser.c +299 -1065
- data/ext/thin_parser/parser.rl +7 -9
- data/ext/thin_parser/thin.c +36 -36
- data/lib/rack/adapter/rails.rb +31 -23
- data/lib/thin.rb +2 -3
- data/lib/thin/command.rb +4 -3
- data/lib/thin/connection.rb +51 -15
- data/lib/thin/controllers/cluster.rb +7 -1
- data/lib/thin/controllers/controller.rb +1 -0
- data/lib/thin/request.rb +16 -2
- data/lib/thin/response.rb +4 -2
- data/lib/thin/runner.rb +1 -1
- data/lib/thin/version.rb +5 -5
- data/lib/thin_parser.bundle +0 -0
- data/spec/command_spec.rb +6 -1
- data/spec/controllers/cluster_spec.rb +33 -0
- data/spec/controllers/controller_spec.rb +11 -3
- data/spec/request/parser_spec.rb +28 -4
- data/spec/response_spec.rb +8 -0
- data/spec/runner_spec.rb +3 -2
- data/spec/server/pipelining_spec.rb +1 -0
- data/spec/server/stopping_spec.rb +13 -3
- data/spec/spec_helper.rb +2 -2
- data/tasks/gem.rake +9 -3
- metadata +9 -8
- data/COMMITTERS +0 -3
- data/lib/rack/handler/thin.rb +0 -18
data/CHANGELOG
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
== 1.2.1 Asynctilicious Ultra Supreme release
|
2
|
+
* Require Rack 1.0.0
|
3
|
+
* Require EventMachine 0.12.6
|
4
|
+
* Use Rails Rack based dispatcher when available
|
5
|
+
* Allow String for response body
|
6
|
+
* Require openssl before eventmachine to prevent crash in 1.9
|
7
|
+
|
8
|
+
== 1.2.0 Asynctilicious Supreme release
|
9
|
+
* Add support for Windows mingw Ruby distro [Juan C. Rodriguez]
|
10
|
+
* Add async response support, see example/async_*.ru [raggi]
|
11
|
+
|
12
|
+
== 1.1.1 Super Disco Power Plus release
|
13
|
+
* Fix bug when running with only options [hasimo]
|
14
|
+
|
15
|
+
== 1.1.0 Super Disco Power release
|
16
|
+
* Require EventMachine 0.12.4
|
17
|
+
* Remove Thin handler, now part of Rack 0.9.1
|
18
|
+
* Fix Rack protocol version to 0.1 in environment hash.
|
19
|
+
* Fix error when passing no_epoll option to a cluster.
|
20
|
+
* Omit parsing #defined strings [Jérémy Zurcher]
|
21
|
+
* Defaults SERVER_NAME to localhost like webrick does [#87 state:resolved]
|
22
|
+
* Namespace parser to prevent error when mongrel is required [cliffmoon]
|
23
|
+
* Set RACK_ENV based on environment option when loading rackup file [Curtis Summers] [#83 state:resolved]
|
24
|
+
* Fixes a warning RE relative_url_root when using a prefix with Rails 2.1.1 [seriph] [#85 state:resolved]
|
25
|
+
* --only can work as a sequence number (if < 80) or a port number (if >= 80) [jmay] [#81 state:resolved]
|
26
|
+
|
1
27
|
== 1.0.0 That's What She Said release
|
2
28
|
* Fixed vlad.rake to allow TCP or socket [hellekin]
|
3
29
|
* Updated Mack adapter to handle both <0.8.0 and >0.8.0 [Mark Bates]
|
@@ -0,0 +1,126 @@
|
|
1
|
+
#!/usr/bin/env rackup -s thin
|
2
|
+
#
|
3
|
+
# async_app.ru
|
4
|
+
# raggi/thin
|
5
|
+
#
|
6
|
+
# A second demo app for async rack + thin app processing!
|
7
|
+
# Now using http status code 100 instead.
|
8
|
+
#
|
9
|
+
# Created by James Tucker on 2008-06-17.
|
10
|
+
# Copyright 2008 James Tucker <raggi@rubyforge.org>.
|
11
|
+
#
|
12
|
+
#--
|
13
|
+
# Benchmark Results:
|
14
|
+
#
|
15
|
+
# raggi@mbk:~$ ab -c 100 -n 500 http://127.0.0.1:3000/
|
16
|
+
# This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
|
17
|
+
# Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
18
|
+
# Copyright 2006 The Apache Software Foundation, http://www.apache.org/
|
19
|
+
#
|
20
|
+
# Benchmarking 127.0.0.1 (be patient)
|
21
|
+
# Completed 100 requests
|
22
|
+
# Completed 200 requests
|
23
|
+
# Completed 300 requests
|
24
|
+
# Completed 400 requests
|
25
|
+
# Finished 500 requests
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# Server Software: thin
|
29
|
+
# Server Hostname: 127.0.0.1
|
30
|
+
# Server Port: 3000
|
31
|
+
#
|
32
|
+
# Document Path: /
|
33
|
+
# Document Length: 12 bytes
|
34
|
+
#
|
35
|
+
# Concurrency Level: 100
|
36
|
+
# Time taken for tests: 5.263089 seconds
|
37
|
+
# Complete requests: 500
|
38
|
+
# Failed requests: 0
|
39
|
+
# Write errors: 0
|
40
|
+
# Total transferred: 47000 bytes
|
41
|
+
# HTML transferred: 6000 bytes
|
42
|
+
# Requests per second: 95.00 [#/sec] (mean)
|
43
|
+
# Time per request: 1052.618 [ms] (mean)
|
44
|
+
# Time per request: 10.526 [ms] (mean, across all concurrent requests)
|
45
|
+
# Transfer rate: 8.55 [Kbytes/sec] received
|
46
|
+
#
|
47
|
+
# Connection Times (ms)
|
48
|
+
# min mean[+/-sd] median max
|
49
|
+
# Connect: 0 3 2.2 3 8
|
50
|
+
# Processing: 1042 1046 3.1 1046 1053
|
51
|
+
# Waiting: 1037 1042 3.6 1041 1050
|
52
|
+
# Total: 1045 1049 3.1 1049 1057
|
53
|
+
#
|
54
|
+
# Percentage of the requests served within a certain time (ms)
|
55
|
+
# 50% 1049
|
56
|
+
# 66% 1051
|
57
|
+
# 75% 1053
|
58
|
+
# 80% 1053
|
59
|
+
# 90% 1054
|
60
|
+
# 95% 1054
|
61
|
+
# 98% 1056
|
62
|
+
# 99% 1057
|
63
|
+
# 100% 1057 (longest request)
|
64
|
+
|
65
|
+
class DeferrableBody
|
66
|
+
include EventMachine::Deferrable
|
67
|
+
|
68
|
+
def call(body)
|
69
|
+
body.each do |chunk|
|
70
|
+
@body_callback.call(chunk)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def each &blk
|
75
|
+
@body_callback = blk
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class AsyncApp
|
81
|
+
|
82
|
+
# This is a template async response. N.B. Can't use string for body on 1.9
|
83
|
+
AsyncResponse = [-1, {}, []].freeze
|
84
|
+
|
85
|
+
def call(env)
|
86
|
+
|
87
|
+
body = DeferrableBody.new
|
88
|
+
|
89
|
+
# Get the headers out there asap, let the client know we're alive...
|
90
|
+
EventMachine::next_tick { env['async.callback'].call [200, {'Content-Type' => 'text/plain'}, body] }
|
91
|
+
|
92
|
+
# Semi-emulate a long db request, instead of a timer, in reality we'd be
|
93
|
+
# waiting for the response data. Whilst this happens, other connections
|
94
|
+
# can be serviced.
|
95
|
+
# This could be any callback based thing though, a deferrable waiting on
|
96
|
+
# IO data, a db request, an http request, an smtp send, whatever.
|
97
|
+
EventMachine::add_timer(1) {
|
98
|
+
body.call ["Woah, async!\n"]
|
99
|
+
|
100
|
+
EventMachine::next_tick {
|
101
|
+
# This could actually happen any time, you could spawn off to new
|
102
|
+
# threads, pause as a good looking lady walks by, whatever.
|
103
|
+
# Just shows off how we can defer chunks of data in the body, you can
|
104
|
+
# even call this many times.
|
105
|
+
body.call ["Cheers then!"]
|
106
|
+
body.succeed
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
# throw :async # Still works for supporting non-async frameworks...
|
111
|
+
|
112
|
+
AsyncResponse # May end up in Rack :-)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# The additions to env for async.connection and async.callback absolutely
|
118
|
+
# destroy the speed of the request if Lint is doing it's checks on env.
|
119
|
+
# It is also important to note that an async response will not pass through
|
120
|
+
# any further middleware, as the async response notification has been passed
|
121
|
+
# right up to the webserver, and the callback goes directly there too.
|
122
|
+
# Middleware could possibly catch :async, and also provide a different
|
123
|
+
# async.connection and async.callback.
|
124
|
+
|
125
|
+
# use Rack::Lint
|
126
|
+
run AsyncApp.new
|
@@ -0,0 +1,247 @@
|
|
1
|
+
#!/usr/bin/env rackup -s thin
|
2
|
+
#
|
3
|
+
# async_chat.ru
|
4
|
+
# raggi/thin
|
5
|
+
#
|
6
|
+
# Created by James Tucker on 2008-06-19.
|
7
|
+
# Copyright 2008 James Tucker <raggi@rubyforge.org>.
|
8
|
+
|
9
|
+
# Uncomment if appropriate for you..
|
10
|
+
EM.epoll
|
11
|
+
# EM.kqueue # bug on OS X in 0.12?
|
12
|
+
|
13
|
+
class DeferrableBody
|
14
|
+
include EventMachine::Deferrable
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@queue = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def schedule_dequeue
|
21
|
+
return unless @body_callback
|
22
|
+
EventMachine::next_tick do
|
23
|
+
next unless body = @queue.shift
|
24
|
+
body.each do |chunk|
|
25
|
+
@body_callback.call(chunk)
|
26
|
+
end
|
27
|
+
schedule_dequeue unless @queue.empty?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(body)
|
32
|
+
@queue << body
|
33
|
+
schedule_dequeue
|
34
|
+
end
|
35
|
+
|
36
|
+
def each &blk
|
37
|
+
@body_callback = blk
|
38
|
+
schedule_dequeue
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class Chat
|
44
|
+
|
45
|
+
module UserBody
|
46
|
+
attr_accessor :username
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize
|
50
|
+
@users = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def render_page
|
54
|
+
[] << <<-EOPAGE
|
55
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
56
|
+
<html>
|
57
|
+
<head>
|
58
|
+
<style>
|
59
|
+
body {
|
60
|
+
font-family: sans-serif;
|
61
|
+
margin: 0;
|
62
|
+
padding: 0;
|
63
|
+
margin-top: 4em;
|
64
|
+
margin-bottom: 1em;
|
65
|
+
}
|
66
|
+
#header {
|
67
|
+
background: silver;
|
68
|
+
height: 4em;
|
69
|
+
width: 100%;
|
70
|
+
position: fixed;
|
71
|
+
top: 0px;
|
72
|
+
border-bottom: 1px solid black;
|
73
|
+
padding-left: 0.5em;
|
74
|
+
}
|
75
|
+
#messages {
|
76
|
+
width: 100%;
|
77
|
+
height: 100%;
|
78
|
+
}
|
79
|
+
.message {
|
80
|
+
margin-left: 1em;
|
81
|
+
}
|
82
|
+
#send_form {
|
83
|
+
position: fixed;
|
84
|
+
bottom: 0px;
|
85
|
+
height: 1em;
|
86
|
+
width: 100%;
|
87
|
+
}
|
88
|
+
#message_box {
|
89
|
+
background: silver;
|
90
|
+
width: 100%;
|
91
|
+
border: 0px;
|
92
|
+
border-top: 1px solid black;
|
93
|
+
}
|
94
|
+
.gray {
|
95
|
+
color: gray;
|
96
|
+
}
|
97
|
+
</style>
|
98
|
+
<script type="text/javascript" src="http://ra66i.org/tmp/jquery-1.2.6.min.js"></script>
|
99
|
+
<script type="text/javascript">
|
100
|
+
XHR = function() {
|
101
|
+
var request = false;
|
102
|
+
try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {
|
103
|
+
try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch(e1) {
|
104
|
+
try { request = new XMLHttpRequest(); } catch(e2) {
|
105
|
+
return false;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
return request;
|
110
|
+
}
|
111
|
+
scroll = function() {
|
112
|
+
window.scrollBy(0,50);
|
113
|
+
setTimeout('scroll()',100);
|
114
|
+
}
|
115
|
+
focus = function() {
|
116
|
+
$('#message_box').focus();
|
117
|
+
}
|
118
|
+
send_message = function(message_box) {
|
119
|
+
xhr = XHR();
|
120
|
+
xhr.open("POST", "/", true);
|
121
|
+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
122
|
+
xhr.setRequestHeader("X_REQUESTED_WITH", "XMLHttpRequest");
|
123
|
+
xhr.send("message="+escape(message_box.value));
|
124
|
+
scroll();
|
125
|
+
message_box.value = '';
|
126
|
+
focus();
|
127
|
+
return false;
|
128
|
+
}
|
129
|
+
new_message = function(username, message) {
|
130
|
+
// TODO html escape message
|
131
|
+
formatted_message = "<div class='message'>" + username + ": " + message + "</div>";
|
132
|
+
messages_div = $('#messages');
|
133
|
+
$(formatted_message).appendTo(messages_div);
|
134
|
+
scroll();
|
135
|
+
return true;
|
136
|
+
}
|
137
|
+
</script>
|
138
|
+
<title>Async Chat</title>
|
139
|
+
</head>
|
140
|
+
<body>
|
141
|
+
<div id="header">
|
142
|
+
<h1>Async Chat</h1>
|
143
|
+
</div>
|
144
|
+
<div id="messages" onclick="focus();">
|
145
|
+
<span class="gray">Your first message will become your nickname!</span>
|
146
|
+
<span>Users: #{@users.map{|k,u|u.username}.join(', ')}</span>
|
147
|
+
</div>
|
148
|
+
<form id="send_form" onSubmit="return send_message(this.message)">
|
149
|
+
<input type="text" id="message_box" name="message"></input>
|
150
|
+
</form>
|
151
|
+
<script type="text/javascript">focus();</script>
|
152
|
+
</body>
|
153
|
+
</html>
|
154
|
+
EOPAGE
|
155
|
+
end
|
156
|
+
|
157
|
+
def register_user(user_id, renderer)
|
158
|
+
body = create_user(user_id)
|
159
|
+
body.call render_page
|
160
|
+
body.errback { delete_user user_id }
|
161
|
+
body.callback { delete_user user_id }
|
162
|
+
|
163
|
+
EventMachine::next_tick do
|
164
|
+
renderer.call [200, {'Content-Type' => 'text/html'}, body]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def new_message(user_id, message)
|
169
|
+
return unless @users[user_id]
|
170
|
+
if @users[user_id].username == :anonymous
|
171
|
+
username = unique_username(message)
|
172
|
+
log "User: #{user_id} is #{username}"
|
173
|
+
@users[user_id].username = message
|
174
|
+
message = "<span class='gray'>-> #{username} signed on.</span>"
|
175
|
+
end
|
176
|
+
username ||= @users[user_id].username
|
177
|
+
log "User: #{username} sent: #{message}"
|
178
|
+
@users.each do |id, body|
|
179
|
+
EventMachine::next_tick { body.call [js_message(username, message)] }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
def unique_username(name)
|
185
|
+
name.concat('_') while @users.any? { |id,u| name == u.username }
|
186
|
+
name
|
187
|
+
end
|
188
|
+
|
189
|
+
def log(str)
|
190
|
+
print str, "\n"
|
191
|
+
end
|
192
|
+
|
193
|
+
def add_user(id, body)
|
194
|
+
@users[id] = body
|
195
|
+
end
|
196
|
+
|
197
|
+
def delete_user(id)
|
198
|
+
message = "User: #{id} - #{@users[id].username if @users[id]} disconnected."
|
199
|
+
log message
|
200
|
+
new_message(id, message)
|
201
|
+
@users.delete id
|
202
|
+
end
|
203
|
+
|
204
|
+
def js_message(username, message)
|
205
|
+
%(<script type="text/javascript">new_message("#{username}","#{message}");</script>)
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_user(id)
|
209
|
+
message = "User: #{id} connected."
|
210
|
+
log message
|
211
|
+
new_message(id, message)
|
212
|
+
body = DeferrableBody.new
|
213
|
+
body.extend UserBody
|
214
|
+
body.username = :anonymous
|
215
|
+
add_user(id, body)
|
216
|
+
body
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
class AsyncChat
|
222
|
+
|
223
|
+
AsyncResponse = [-1, {}, []].freeze
|
224
|
+
AjaxResponse = [200, {}, []].freeze
|
225
|
+
|
226
|
+
def initialize
|
227
|
+
@chat = Chat.new
|
228
|
+
end
|
229
|
+
|
230
|
+
def call(env)
|
231
|
+
request = Rack::Request.new(env)
|
232
|
+
# TODO - cookie me, baby
|
233
|
+
user_id = request.env['REMOTE_ADDR']
|
234
|
+
if request.xhr?
|
235
|
+
message = request['message']
|
236
|
+
@chat.new_message(user_id, Rack::Utils.escape_html(message))
|
237
|
+
AjaxResponse
|
238
|
+
else
|
239
|
+
renderer = request.env['async.callback']
|
240
|
+
@chat.register_user(user_id, renderer)
|
241
|
+
AsyncResponse
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
run AsyncChat.new
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env rackup -s thin
|
2
|
+
#
|
3
|
+
# async_tailer.ru
|
4
|
+
# raggi/thin
|
5
|
+
#
|
6
|
+
# Tested with 150 spawned tails on OS X
|
7
|
+
#
|
8
|
+
# Created by James Tucker on 2008-06-18.
|
9
|
+
# Copyright 2008 James Tucker <raggi@rubyforge.org>.
|
10
|
+
|
11
|
+
# Uncomment if appropriate for you..
|
12
|
+
# EM.epoll
|
13
|
+
# EM.kqueue
|
14
|
+
|
15
|
+
class DeferrableBody
|
16
|
+
include EventMachine::Deferrable
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@queue = []
|
20
|
+
# make sure to flush out the queue before closing the connection
|
21
|
+
callback{
|
22
|
+
until @queue.empty?
|
23
|
+
@queue.shift.each{|chunk| @body_callback.call(chunk) }
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def schedule_dequeue
|
29
|
+
return unless @body_callback
|
30
|
+
EventMachine::next_tick do
|
31
|
+
next unless body = @queue.shift
|
32
|
+
body.each do |chunk|
|
33
|
+
@body_callback.call(chunk)
|
34
|
+
end
|
35
|
+
schedule_dequeue unless @queue.empty?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(body)
|
40
|
+
@queue << body
|
41
|
+
schedule_dequeue
|
42
|
+
end
|
43
|
+
|
44
|
+
def each &blk
|
45
|
+
@body_callback = blk
|
46
|
+
schedule_dequeue
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
module TailRenderer
|
52
|
+
attr_accessor :callback
|
53
|
+
|
54
|
+
def receive_data(data)
|
55
|
+
@callback.call([data])
|
56
|
+
end
|
57
|
+
|
58
|
+
def unbind
|
59
|
+
@callback.succeed
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class AsyncTailer
|
64
|
+
|
65
|
+
AsyncResponse = [-1, {}, []].freeze
|
66
|
+
|
67
|
+
def call(env)
|
68
|
+
|
69
|
+
body = DeferrableBody.new
|
70
|
+
|
71
|
+
EventMachine::next_tick do
|
72
|
+
|
73
|
+
env['async.callback'].call [200, {'Content-Type' => 'text/html'}, body]
|
74
|
+
|
75
|
+
body.call ["<h1>Async Tailer</h1><pre>"]
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
EventMachine::popen('tail -f /var/log/system.log', TailRenderer) do |t|
|
80
|
+
|
81
|
+
t.callback = body
|
82
|
+
|
83
|
+
# If for some reason we 'complete' body, close the tail.
|
84
|
+
body.callback do
|
85
|
+
t.close_connection
|
86
|
+
end
|
87
|
+
|
88
|
+
# If for some reason the client disconnects, close the tail.
|
89
|
+
body.errback do
|
90
|
+
t.close_connection
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
AsyncResponse
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
run AsyncTailer.new
|