raindrops-maintained 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +7 -0
- data/.gitattributes +4 -0
- data/.gitignore +16 -0
- data/.manifest +62 -0
- data/.olddoc.yml +16 -0
- data/COPYING +165 -0
- data/GIT-VERSION-FILE +1 -0
- data/GIT-VERSION-GEN +40 -0
- data/GNUmakefile +4 -0
- data/LATEST +9 -0
- data/LICENSE +16 -0
- data/NEWS +384 -0
- data/README +101 -0
- data/TODO +3 -0
- data/archive/.gitignore +3 -0
- data/archive/slrnpull.conf +4 -0
- data/examples/linux-listener-stats.rb +122 -0
- data/examples/middleware.ru +5 -0
- data/examples/watcher.ru +4 -0
- data/examples/watcher_demo.ru +13 -0
- data/examples/yahns.conf.rb +30 -0
- data/examples/zbatery.conf.rb +16 -0
- data/ext/raindrops/extconf.rb +163 -0
- data/ext/raindrops/linux_inet_diag.c +713 -0
- data/ext/raindrops/my_fileno.h +16 -0
- data/ext/raindrops/raindrops.c +487 -0
- data/ext/raindrops/raindrops_atomic.h +23 -0
- data/ext/raindrops/tcp_info.c +245 -0
- data/lib/raindrops/aggregate/last_data_recv.rb +94 -0
- data/lib/raindrops/aggregate/pmq.rb +245 -0
- data/lib/raindrops/aggregate.rb +8 -0
- data/lib/raindrops/last_data_recv.rb +102 -0
- data/lib/raindrops/linux.rb +77 -0
- data/lib/raindrops/middleware/proxy.rb +40 -0
- data/lib/raindrops/middleware.rb +153 -0
- data/lib/raindrops/struct.rb +62 -0
- data/lib/raindrops/watcher.rb +428 -0
- data/lib/raindrops.rb +72 -0
- data/pkg.mk +151 -0
- data/raindrops-maintained.gemspec +1 -0
- data/raindrops.gemspec +26 -0
- data/setup.rb +1586 -0
- data/test/ipv6_enabled.rb +9 -0
- data/test/rack_unicorn.rb +11 -0
- data/test/test_aggregate_pmq.rb +65 -0
- data/test/test_inet_diag_socket.rb +16 -0
- data/test/test_last_data_recv.rb +57 -0
- data/test/test_last_data_recv_unicorn.rb +69 -0
- data/test/test_linux.rb +281 -0
- data/test/test_linux_all_tcp_listen_stats.rb +66 -0
- data/test/test_linux_all_tcp_listen_stats_leak.rb +43 -0
- data/test/test_linux_ipv6.rb +166 -0
- data/test/test_linux_middleware.rb +64 -0
- data/test/test_linux_reuseport_tcp_listen_stats.rb +51 -0
- data/test/test_middleware.rb +128 -0
- data/test/test_middleware_unicorn.rb +37 -0
- data/test/test_middleware_unicorn_ipv6.rb +37 -0
- data/test/test_raindrops.rb +207 -0
- data/test/test_raindrops_gc.rb +38 -0
- data/test/test_struct.rb +54 -0
- data/test/test_tcp_info.rb +88 -0
- data/test/test_watcher.rb +186 -0
- metadata +193 -0
@@ -0,0 +1,428 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require "thread"
|
3
|
+
require "time"
|
4
|
+
require "socket"
|
5
|
+
require "rack"
|
6
|
+
require "aggregate"
|
7
|
+
|
8
|
+
# Raindrops::Watcher is a stand-alone Rack application for watching
|
9
|
+
# any number of TCP and UNIX listeners (all of them by default).
|
10
|
+
#
|
11
|
+
# It depends on the {Aggregate RubyGem}[https://rubygems.org/gems/aggregate]
|
12
|
+
#
|
13
|
+
# In your Rack config.ru:
|
14
|
+
#
|
15
|
+
# run Raindrops::Watcher(options = {})
|
16
|
+
#
|
17
|
+
# It takes the following options hash:
|
18
|
+
#
|
19
|
+
# - :listeners - an array of listener names, (e.g. %w(0.0.0.0:80 /tmp/sock))
|
20
|
+
# - :delay - interval between stats updates in seconds (default: 1)
|
21
|
+
#
|
22
|
+
# Raindrops::Watcher is compatible any thread-safe/thread-aware Rack
|
23
|
+
# middleware. It does not work well with multi-process web servers
|
24
|
+
# but can be used to monitor them. It consumes minimal resources
|
25
|
+
# with the default :delay.
|
26
|
+
#
|
27
|
+
# == HTTP endpoints
|
28
|
+
#
|
29
|
+
# === GET /
|
30
|
+
#
|
31
|
+
# Returns an HTML summary listing of all listen interfaces watched on
|
32
|
+
#
|
33
|
+
# === GET /active/$LISTENER.txt
|
34
|
+
#
|
35
|
+
# Returns a plain text summary + histogram with X-* HTTP headers for
|
36
|
+
# active connections.
|
37
|
+
#
|
38
|
+
# e.g.: curl https://yhbt.net/raindrops-demo/active/0.0.0.0%3A80.txt
|
39
|
+
#
|
40
|
+
# === GET /active/$LISTENER.html
|
41
|
+
#
|
42
|
+
# Returns an HTML summary + histogram with X-* HTTP headers for
|
43
|
+
# active connections.
|
44
|
+
#
|
45
|
+
# e.g.: curl https://yhbt.net/raindrops-demo/active/0.0.0.0%3A80.html
|
46
|
+
#
|
47
|
+
# === GET /queued/$LISTENER.txt
|
48
|
+
#
|
49
|
+
# Returns a plain text summary + histogram with X-* HTTP headers for
|
50
|
+
# queued connections.
|
51
|
+
#
|
52
|
+
# e.g.: curl https://yhbt.net/raindrops-demo/queued/0.0.0.0%3A80.txt
|
53
|
+
#
|
54
|
+
# === GET /queued/$LISTENER.html
|
55
|
+
#
|
56
|
+
# Returns an HTML summary + histogram with X-* HTTP headers for
|
57
|
+
# queued connections.
|
58
|
+
#
|
59
|
+
# e.g.: curl https://yhbt.net/raindrops-demo/queued/0.0.0.0%3A80.html
|
60
|
+
#
|
61
|
+
# === POST /reset/$LISTENER
|
62
|
+
#
|
63
|
+
# Resets the active and queued statistics for the given listener.
|
64
|
+
#
|
65
|
+
# === GET /tail/$LISTENER.txt?active_min=1&queued_min=1
|
66
|
+
#
|
67
|
+
# Streams chunked a response to the client.
|
68
|
+
# Interval is the preconfigured +:delay+ of the application (default 1 second)
|
69
|
+
#
|
70
|
+
# The response is plain text in the following format:
|
71
|
+
#
|
72
|
+
# ISO8601_TIMESTAMP LISTENER_NAME ACTIVE_COUNT QUEUED_COUNT LINEFEED
|
73
|
+
#
|
74
|
+
# Query parameters:
|
75
|
+
#
|
76
|
+
# - active_min - do not stream a line until this active count is reached
|
77
|
+
# - queued_min - do not stream a line until this queued count is reached
|
78
|
+
#
|
79
|
+
# == Response headers (mostly the same names as Raindrops::LastDataRecv)
|
80
|
+
#
|
81
|
+
# - X-Count - number of samples polled
|
82
|
+
# - X-Last-Reset - date since the last reset
|
83
|
+
#
|
84
|
+
# The following headers are only present if X-Count is greater than one.
|
85
|
+
#
|
86
|
+
# - X-Min - lowest number of connections recorded
|
87
|
+
# - X-Max - highest number of connections recorded
|
88
|
+
# - X-Mean - mean number of connections recorded
|
89
|
+
# - X-Std-Dev - standard deviation of connection count
|
90
|
+
# - X-Outliers-Low - number of low outliers (hopefully many for queued)
|
91
|
+
# - X-Outliers-High - number of high outliers (hopefully zero for queued)
|
92
|
+
# - X-Current - current number of connections
|
93
|
+
# - X-First-Peak-At - date of when X-Max was first reached
|
94
|
+
# - X-Last-Peak-At - date of when X-Max was last reached
|
95
|
+
#
|
96
|
+
# = Demo Server
|
97
|
+
#
|
98
|
+
# There is a server running this app at https://yhbt.net/raindrops-demo/
|
99
|
+
# The Raindrops::Middleware demo is also accessible at
|
100
|
+
# https://yhbt.net/raindrops-demo/_raindrops
|
101
|
+
#
|
102
|
+
# The demo server is only limited to 30 users, so be sure not to abuse it
|
103
|
+
# by using the /tail/ endpoint too much.
|
104
|
+
class Raindrops::Watcher
|
105
|
+
# :stopdoc:
|
106
|
+
attr_reader :snapshot
|
107
|
+
include Rack::Utils
|
108
|
+
include Raindrops::Linux
|
109
|
+
DOC_URL = "https://yhbt.net/raindrops/Raindrops/Watcher.html"
|
110
|
+
Peak = Struct.new(:first, :last)
|
111
|
+
|
112
|
+
def initialize(opts = {})
|
113
|
+
@tcp_listeners = @unix_listeners = nil
|
114
|
+
if l = opts[:listeners]
|
115
|
+
tcp, unix = [], []
|
116
|
+
Array(l).each { |addr| (addr =~ %r{\A/} ? unix : tcp) << addr }
|
117
|
+
unless tcp.empty? && unix.empty?
|
118
|
+
@tcp_listeners = tcp
|
119
|
+
@unix_listeners = unix
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@agg_class = opts[:agg_class] || Aggregate
|
124
|
+
@start_time = Time.now.utc
|
125
|
+
@active = Hash.new { |h,k| h[k] = @agg_class.new }
|
126
|
+
@queued = Hash.new { |h,k| h[k] = @agg_class.new }
|
127
|
+
@resets = Hash.new { |h,k| h[k] = @start_time }
|
128
|
+
@peak_active = Hash.new { |h,k| h[k] = Peak.new(@start_time, @start_time) }
|
129
|
+
@peak_queued = Hash.new { |h,k| h[k] = Peak.new(@start_time, @start_time) }
|
130
|
+
@snapshot = [ @start_time, {} ]
|
131
|
+
@delay = opts[:delay] || 1
|
132
|
+
@lock = Mutex.new
|
133
|
+
@start = Mutex.new
|
134
|
+
@cond = ConditionVariable.new
|
135
|
+
@thr = nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def hostname
|
139
|
+
Socket.gethostname
|
140
|
+
end
|
141
|
+
|
142
|
+
# rack endpoint
|
143
|
+
def call(env)
|
144
|
+
@start.synchronize { @thr ||= aggregator_thread(env["rack.logger"]) }
|
145
|
+
case env["REQUEST_METHOD"]
|
146
|
+
when "GET"
|
147
|
+
get env
|
148
|
+
when "HEAD"
|
149
|
+
r = get(env)
|
150
|
+
r[2] = []
|
151
|
+
r
|
152
|
+
when "POST"
|
153
|
+
post env
|
154
|
+
else
|
155
|
+
Rack::Response.new(["Method Not Allowed"], 405).finish
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def aggregate!(agg_hash, peak_hash, addr, number, now)
|
160
|
+
agg = agg_hash[addr]
|
161
|
+
if (max = agg.max) && number > 0 && number >= max
|
162
|
+
peak = peak_hash[addr]
|
163
|
+
peak.first = now if number > max
|
164
|
+
peak.last = now
|
165
|
+
end
|
166
|
+
agg << number
|
167
|
+
end
|
168
|
+
|
169
|
+
def aggregator_thread(logger) # :nodoc:
|
170
|
+
@socket = sock = Raindrops::InetDiagSocket.new
|
171
|
+
thr = Thread.new do
|
172
|
+
begin
|
173
|
+
combined = tcp_listener_stats(@tcp_listeners, sock)
|
174
|
+
combined.merge!(unix_listener_stats(@unix_listeners))
|
175
|
+
@lock.synchronize do
|
176
|
+
now = Time.now.utc
|
177
|
+
combined.each do |addr,stats|
|
178
|
+
aggregate!(@active, @peak_active, addr, stats.active, now)
|
179
|
+
aggregate!(@queued, @peak_queued, addr, stats.queued, now)
|
180
|
+
end
|
181
|
+
@snapshot = [ now, combined ]
|
182
|
+
@cond.broadcast
|
183
|
+
end
|
184
|
+
rescue => e
|
185
|
+
logger.error "#{e.class} #{e.inspect}"
|
186
|
+
end while sleep(@delay) && @socket
|
187
|
+
sock.close
|
188
|
+
end
|
189
|
+
wait_snapshot
|
190
|
+
thr
|
191
|
+
end
|
192
|
+
|
193
|
+
def non_existent_stats(time)
|
194
|
+
[ time, @start_time, @agg_class.new, 0, Peak.new(@start_time, @start_time) ]
|
195
|
+
end
|
196
|
+
|
197
|
+
def active_stats(addr) # :nodoc:
|
198
|
+
@lock.synchronize do
|
199
|
+
time, combined = @snapshot
|
200
|
+
stats = combined[addr] or return non_existent_stats(time)
|
201
|
+
tmp, peak = @active[addr], @peak_active[addr]
|
202
|
+
[ time, @resets[addr], tmp.dup, stats.active, peak ]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def queued_stats(addr) # :nodoc:
|
207
|
+
@lock.synchronize do
|
208
|
+
time, combined = @snapshot
|
209
|
+
stats = combined[addr] or return non_existent_stats(time)
|
210
|
+
tmp, peak = @queued[addr], @peak_queued[addr]
|
211
|
+
[ time, @resets[addr], tmp.dup, stats.queued, peak ]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def wait_snapshot
|
216
|
+
@lock.synchronize do
|
217
|
+
@cond.wait @lock
|
218
|
+
@snapshot
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def std_dev(agg)
|
223
|
+
agg.std_dev.to_s
|
224
|
+
rescue Errno::EDOM
|
225
|
+
"NaN"
|
226
|
+
end
|
227
|
+
|
228
|
+
def agg_to_hash(reset_at, agg, current, peak)
|
229
|
+
{
|
230
|
+
"X-Count" => agg.count.to_s,
|
231
|
+
"X-Min" => agg.min.to_s,
|
232
|
+
"X-Max" => agg.max.to_s,
|
233
|
+
"X-Mean" => agg.mean.to_s,
|
234
|
+
"X-Std-Dev" => std_dev(agg),
|
235
|
+
"X-Outliers-Low" => agg.outliers_low.to_s,
|
236
|
+
"X-Outliers-High" => agg.outliers_high.to_s,
|
237
|
+
"X-Last-Reset" => reset_at.httpdate,
|
238
|
+
"X-Current" => current.to_s,
|
239
|
+
"X-First-Peak-At" => peak.first.httpdate,
|
240
|
+
"X-Last-Peak-At" => peak.last.httpdate,
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
def histogram_txt(agg)
|
245
|
+
updated_at, reset_at, agg, current, peak = *agg
|
246
|
+
headers = agg_to_hash(reset_at, agg, current, peak)
|
247
|
+
body = agg.to_s # 7-bit ASCII-clean
|
248
|
+
headers["Content-Type"] = "text/plain"
|
249
|
+
headers["Expires"] = (updated_at + @delay).httpdate
|
250
|
+
headers["Content-Length"] = body.size.to_s
|
251
|
+
[ 200, headers, [ body ] ]
|
252
|
+
end
|
253
|
+
|
254
|
+
def histogram_html(agg, addr)
|
255
|
+
updated_at, reset_at, agg, current, peak = *agg
|
256
|
+
headers = agg_to_hash(reset_at, agg, current, peak)
|
257
|
+
body = "<html>" \
|
258
|
+
"<head><title>#{hostname} - #{escape_html addr}</title></head>" \
|
259
|
+
"<body><table>" <<
|
260
|
+
headers.map { |k,v|
|
261
|
+
"<tr><td>#{k.gsub(/^X-/, '')}</td><td>#{v}</td></tr>"
|
262
|
+
}.join << "</table><pre>#{escape_html agg}</pre>" \
|
263
|
+
"<form action='../reset/#{escape addr}' method='post'>" \
|
264
|
+
"<input type='submit' name='x' value='reset' /></form>" \
|
265
|
+
"</body>"
|
266
|
+
headers["Content-Type"] = "text/html"
|
267
|
+
headers["Expires"] = (updated_at + @delay).httpdate
|
268
|
+
headers["Content-Length"] = body.size.to_s
|
269
|
+
[ 200, headers, [ body ] ]
|
270
|
+
end
|
271
|
+
|
272
|
+
def get(env)
|
273
|
+
retried = false
|
274
|
+
begin
|
275
|
+
case env["PATH_INFO"]
|
276
|
+
when "/"
|
277
|
+
index
|
278
|
+
when %r{\A/active/(.+)\.txt\z}
|
279
|
+
histogram_txt(active_stats(unescape($1)))
|
280
|
+
when %r{\A/active/(.+)\.html\z}
|
281
|
+
addr = unescape $1
|
282
|
+
histogram_html(active_stats(addr), addr)
|
283
|
+
when %r{\A/queued/(.+)\.txt\z}
|
284
|
+
histogram_txt(queued_stats(unescape($1)))
|
285
|
+
when %r{\A/queued/(.+)\.html\z}
|
286
|
+
addr = unescape $1
|
287
|
+
histogram_html(queued_stats(addr), addr)
|
288
|
+
when %r{\A/tail/(.+)\.txt\z}
|
289
|
+
tail(unescape($1), env)
|
290
|
+
else
|
291
|
+
not_found
|
292
|
+
end
|
293
|
+
rescue Errno::EDOM
|
294
|
+
raise if retried
|
295
|
+
retried = true
|
296
|
+
wait_snapshot
|
297
|
+
retry
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def not_found
|
302
|
+
Rack::Response.new(["Not Found"], 404).finish
|
303
|
+
end
|
304
|
+
|
305
|
+
def post(env)
|
306
|
+
case env["PATH_INFO"]
|
307
|
+
when %r{\A/reset/(.+)\z}
|
308
|
+
reset!(env, unescape($1))
|
309
|
+
else
|
310
|
+
not_found
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def reset!(env, addr)
|
315
|
+
@lock.synchronize do
|
316
|
+
@active.include?(addr) or return not_found
|
317
|
+
@active.delete addr
|
318
|
+
@queued.delete addr
|
319
|
+
@resets[addr] = Time.now.utc
|
320
|
+
@cond.wait @lock
|
321
|
+
end
|
322
|
+
req = Rack::Request.new(env)
|
323
|
+
res = Rack::Response.new
|
324
|
+
url = req.referer || "#{req.host_with_port}/"
|
325
|
+
res.redirect(url)
|
326
|
+
res["Content-Type"] = "text/plain"
|
327
|
+
res.write "Redirecting to #{url}"
|
328
|
+
res.finish
|
329
|
+
end
|
330
|
+
|
331
|
+
def index
|
332
|
+
updated_at, all = snapshot
|
333
|
+
headers = {
|
334
|
+
"Content-Type" => "text/html",
|
335
|
+
"Last-Modified" => updated_at.httpdate,
|
336
|
+
"Expires" => (updated_at + @delay).httpdate,
|
337
|
+
}
|
338
|
+
body = "<html><head>" \
|
339
|
+
"<title>#{hostname} - all interfaces</title>" \
|
340
|
+
"</head><body><h3>Updated at #{updated_at.iso8601}</h3>" \
|
341
|
+
"<table><tr>" \
|
342
|
+
"<th>address</th><th>active</th><th>queued</th><th>reset</th>" \
|
343
|
+
"</tr>" <<
|
344
|
+
all.sort do |a,b|
|
345
|
+
a[0] <=> b[0] # sort by addr
|
346
|
+
end.map do |addr,stats|
|
347
|
+
e_addr = escape addr
|
348
|
+
"<tr>" \
|
349
|
+
"<td><a href='tail/#{e_addr}.txt' " \
|
350
|
+
"title='"tail" output in real time'" \
|
351
|
+
">#{escape_html addr}</a></td>" \
|
352
|
+
"<td><a href='active/#{e_addr}.html' " \
|
353
|
+
"title='show active connection stats'>#{stats.active}</a></td>" \
|
354
|
+
"<td><a href='queued/#{e_addr}.html' " \
|
355
|
+
"title='show queued connection stats'>#{stats.queued}</a></td>" \
|
356
|
+
"<td><form action='reset/#{e_addr}' method='post'>" \
|
357
|
+
"<input title='reset statistics' " \
|
358
|
+
"type='submit' name='x' value='x' /></form></td>" \
|
359
|
+
"</tr>" \
|
360
|
+
end.join << "</table>" \
|
361
|
+
"<p>" \
|
362
|
+
"This is running the #{self.class}</a> service, see " \
|
363
|
+
"<a href='#{DOC_URL}'>#{DOC_URL}</a> " \
|
364
|
+
"for more information and options." \
|
365
|
+
"</p>" \
|
366
|
+
"</body></html>"
|
367
|
+
headers["Content-Length"] = body.size.to_s
|
368
|
+
[ 200, headers, [ body ] ]
|
369
|
+
end
|
370
|
+
|
371
|
+
def tail(addr, env)
|
372
|
+
Tailer.new(self, addr, env).finish
|
373
|
+
end
|
374
|
+
|
375
|
+
# This is the response body returned for "/tail/$ADDRESS.txt". This
|
376
|
+
# must use a multi-threaded Rack server with streaming response support.
|
377
|
+
# It is an internal class and not expected to be used directly
|
378
|
+
class Tailer
|
379
|
+
def initialize(rdmon, addr, env) # :nodoc:
|
380
|
+
@rdmon = rdmon
|
381
|
+
@addr = addr
|
382
|
+
q = Rack::Utils.parse_query env["QUERY_STRING"]
|
383
|
+
@active_min = q["active_min"].to_i
|
384
|
+
@queued_min = q["queued_min"].to_i
|
385
|
+
len = addr.size
|
386
|
+
len = 35 if len > 35
|
387
|
+
@fmt = "%20s % #{len}s % 10u % 10u\n"
|
388
|
+
case env["HTTP_VERSION"]
|
389
|
+
when "HTTP/1.0", nil
|
390
|
+
@chunk = false
|
391
|
+
else
|
392
|
+
@chunk = true
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def finish
|
397
|
+
headers = {
|
398
|
+
"Content-Type" => "text/plain",
|
399
|
+
"Cache-Control" => "no-transform",
|
400
|
+
"Expires" => Time.at(0).httpdate,
|
401
|
+
}
|
402
|
+
headers["Transfer-Encoding"] = "chunked" if @chunk
|
403
|
+
[ 200, headers, self ]
|
404
|
+
end
|
405
|
+
|
406
|
+
# called by the Rack server
|
407
|
+
def each # :nodoc:
|
408
|
+
begin
|
409
|
+
time, all = @rdmon.wait_snapshot
|
410
|
+
stats = all[@addr] or next
|
411
|
+
stats.queued >= @queued_min or next
|
412
|
+
stats.active >= @active_min or next
|
413
|
+
body = sprintf(@fmt, time.iso8601, @addr, stats.active, stats.queued)
|
414
|
+
body = "#{body.size.to_s(16)}\r\n#{body}\r\n" if @chunk
|
415
|
+
yield body
|
416
|
+
end while true
|
417
|
+
yield "0\r\n\r\n" if @chunk
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# shuts down the background thread, only for tests
|
422
|
+
def shutdown
|
423
|
+
@socket = nil
|
424
|
+
@thr.join if @thr
|
425
|
+
@thr = nil
|
426
|
+
end
|
427
|
+
# :startdoc:
|
428
|
+
end
|
data/lib/raindrops.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
#
|
3
|
+
# Each Raindrops object is a container that holds several counters.
|
4
|
+
# It is internally a page-aligned, shared memory area that allows
|
5
|
+
# atomic increments, decrements, assignments and reads without any
|
6
|
+
# locking.
|
7
|
+
#
|
8
|
+
# rd = Raindrops.new 4
|
9
|
+
# rd.incr(0, 1) -> 1
|
10
|
+
# rd.to_ary -> [ 1, 0, 0, 0 ]
|
11
|
+
#
|
12
|
+
# Unlike many classes in this package, the core Raindrops class is
|
13
|
+
# intended to be portable to all reasonably modern *nix systems
|
14
|
+
# supporting mmap(). Please let us know if you have portability
|
15
|
+
# issues, patches or pull requests at mailto:raindrops-public@yhbt.net
|
16
|
+
class Raindrops
|
17
|
+
|
18
|
+
# Used to represent the number of +active+ and +queued+ sockets for
|
19
|
+
# a single listen socket across all threads and processes on a
|
20
|
+
# machine.
|
21
|
+
#
|
22
|
+
# For TCP listeners, only sockets in the TCP_ESTABLISHED state are
|
23
|
+
# accounted for. For Unix domain listeners, only CONNECTING and
|
24
|
+
# CONNECTED Unix domain sockets are accounted for.
|
25
|
+
#
|
26
|
+
# +active+ connections is the number of accept()-ed but not-yet-closed
|
27
|
+
# sockets in all threads/processes sharing the given listener.
|
28
|
+
#
|
29
|
+
# +queued+ connections is the number of un-accept()-ed sockets in the
|
30
|
+
# queue of a given listen socket.
|
31
|
+
#
|
32
|
+
# These stats are currently only available under \Linux
|
33
|
+
class ListenStats < Struct.new(:active, :queued)
|
34
|
+
|
35
|
+
# the sum of +active+ and +queued+ sockets
|
36
|
+
def total
|
37
|
+
active + queued
|
38
|
+
end
|
39
|
+
end unless defined? ListenStats
|
40
|
+
|
41
|
+
# call-seq:
|
42
|
+
# Raindrops.new(size, io: nil) -> raindrops object
|
43
|
+
#
|
44
|
+
# Initializes a Raindrops object to hold +size+ counters. +size+ is
|
45
|
+
# only a hint and the actual number of counters the object has is
|
46
|
+
# dependent on the CPU model, number of cores, and page size of
|
47
|
+
# the machine. The actual size of the object will always be equal
|
48
|
+
# or greater than the specified +size+.
|
49
|
+
# If +io+ is provided, then the Raindrops memory will be backed by
|
50
|
+
# the specified file; otherwise, it will allocate anonymous memory.
|
51
|
+
# The IO object must respond to +truncate+, as this is used to set
|
52
|
+
# the size of the file.
|
53
|
+
# If +zero+ is provided, then the memory region is zeroed prior to
|
54
|
+
# returning. This is only meaningful if +io+ is also provided; in
|
55
|
+
# that case it controls whether any existing counter values in +io+
|
56
|
+
# are retained (false) or whether it is entirely zeroed (true).
|
57
|
+
def initialize(size, io: nil, zero: false)
|
58
|
+
# This ruby wrapper exists to handle the keyword-argument handling,
|
59
|
+
# which is otherwise kind of awkward in C. We delegate the keyword
|
60
|
+
# arguments to the _actual_ initialize implementation as positional
|
61
|
+
# args.
|
62
|
+
initialize_cimpl(size, io, zero)
|
63
|
+
end
|
64
|
+
|
65
|
+
autoload :Linux, 'raindrops/linux'
|
66
|
+
autoload :Struct, 'raindrops/struct'
|
67
|
+
autoload :Middleware, 'raindrops/middleware'
|
68
|
+
autoload :Aggregate, 'raindrops/aggregate'
|
69
|
+
autoload :LastDataRecv, 'raindrops/last_data_recv'
|
70
|
+
autoload :Watcher, 'raindrops/watcher'
|
71
|
+
end
|
72
|
+
require 'raindrops_ext'
|
data/pkg.mk
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
RUBY = ruby
|
2
|
+
RAKE = rake
|
3
|
+
RSYNC = rsync
|
4
|
+
OLDDOC = olddoc
|
5
|
+
RDOC = rdoc
|
6
|
+
|
7
|
+
GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
|
8
|
+
@./GIT-VERSION-GEN
|
9
|
+
-include GIT-VERSION-FILE
|
10
|
+
-include local.mk
|
11
|
+
DLEXT := $(shell $(RUBY) -rrbconfig -e 'puts RbConfig::CONFIG["DLEXT"]')
|
12
|
+
RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
|
13
|
+
RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
|
14
|
+
lib := lib
|
15
|
+
|
16
|
+
ext := $(firstword $(wildcard ext/*))
|
17
|
+
ifneq ($(ext),)
|
18
|
+
ext_pfx := tmp/ext/$(RUBY_ENGINE)-$(RUBY_VERSION)
|
19
|
+
ext_h := $(wildcard $(ext)/*/*.h $(ext)/*.h)
|
20
|
+
ext_src := $(wildcard $(ext)/*.c $(ext_h))
|
21
|
+
ext_pfx_src := $(addprefix $(ext_pfx)/,$(ext_src))
|
22
|
+
ext_d := $(ext_pfx)/$(ext)/.d
|
23
|
+
$(ext)/extconf.rb: $(wildcard $(ext)/*.h)
|
24
|
+
@>> $@
|
25
|
+
$(ext_d):
|
26
|
+
@mkdir -p $(@D)
|
27
|
+
@> $@
|
28
|
+
$(ext_pfx)/$(ext)/%: $(ext)/% $(ext_d)
|
29
|
+
install -m 644 $< $@
|
30
|
+
$(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb $(ext_d) $(ext_h)
|
31
|
+
$(RM) -f $(@D)/*.o
|
32
|
+
cd $(@D) && $(RUBY) $(CURDIR)/$(ext)/extconf.rb $(EXTCONF_ARGS)
|
33
|
+
ext_sfx := _ext.$(DLEXT)
|
34
|
+
ext_dl := $(ext_pfx)/$(ext)/$(notdir $(ext)_ext.$(DLEXT))
|
35
|
+
$(ext_dl): $(ext_src) $(ext_pfx_src) $(ext_pfx)/$(ext)/Makefile
|
36
|
+
@echo $^ == $@
|
37
|
+
$(MAKE) -C $(@D)
|
38
|
+
lib := $(lib):$(ext_pfx)/$(ext)
|
39
|
+
build: $(ext_dl)
|
40
|
+
else
|
41
|
+
build:
|
42
|
+
endif
|
43
|
+
|
44
|
+
pkg_extra += GIT-VERSION-FILE NEWS LATEST
|
45
|
+
NEWS: GIT-VERSION-FILE .olddoc.yml
|
46
|
+
$(OLDDOC) prepare
|
47
|
+
LATEST: NEWS
|
48
|
+
|
49
|
+
manifest:
|
50
|
+
$(RM) .manifest
|
51
|
+
$(MAKE) .manifest
|
52
|
+
|
53
|
+
.manifest: $(pkg_extra)
|
54
|
+
(git ls-files && for i in $@ $(pkg_extra); do echo $$i; done) | \
|
55
|
+
LC_ALL=C sort > $@+
|
56
|
+
cmp $@+ $@ || mv $@+ $@
|
57
|
+
$(RM) $@+
|
58
|
+
|
59
|
+
doc:: .document .olddoc.yml $(pkg_extra) $(PLACEHOLDERS)
|
60
|
+
-find lib -type f -name '*.rbc' -exec rm -f '{}' ';'
|
61
|
+
-find ext -type f -name '*.rbc' -exec rm -f '{}' ';'
|
62
|
+
$(RM) -r doc
|
63
|
+
$(RDOC) -f dark216
|
64
|
+
$(OLDDOC) merge
|
65
|
+
install -m644 COPYING doc/COPYING
|
66
|
+
install -m644 NEWS doc/NEWS
|
67
|
+
install -m644 NEWS.atom.xml doc/NEWS.atom.xml
|
68
|
+
install -m644 $(shell LC_ALL=C grep '^[A-Z]' .document) doc/
|
69
|
+
|
70
|
+
ifneq ($(VERSION),)
|
71
|
+
pkggem := pkg/$(rfpackage)-$(VERSION).gem
|
72
|
+
pkgtgz := pkg/$(rfpackage)-$(VERSION).tgz
|
73
|
+
|
74
|
+
# ensures we're actually on the tagged $(VERSION), only used for release
|
75
|
+
verify:
|
76
|
+
test x"$(shell umask)" = x0022
|
77
|
+
git rev-parse --verify refs/tags/v$(VERSION)^{}
|
78
|
+
git diff-index --quiet HEAD^0
|
79
|
+
test $$(git rev-parse --verify HEAD^0) = \
|
80
|
+
$$(git rev-parse --verify refs/tags/v$(VERSION)^{})
|
81
|
+
|
82
|
+
fix-perms:
|
83
|
+
-git ls-tree -r HEAD | awk '/^100644 / {print $$NF}' | xargs chmod 644
|
84
|
+
-git ls-tree -r HEAD | awk '/^100755 / {print $$NF}' | xargs chmod 755
|
85
|
+
|
86
|
+
gem: $(pkggem)
|
87
|
+
|
88
|
+
install-gem: $(pkggem)
|
89
|
+
gem install --local $(CURDIR)/$<
|
90
|
+
|
91
|
+
$(pkggem): manifest fix-perms
|
92
|
+
gem build $(rfpackage).gemspec
|
93
|
+
mkdir -p pkg
|
94
|
+
mv $(@F) $@
|
95
|
+
|
96
|
+
$(pkgtgz): distdir = $(basename $@)
|
97
|
+
$(pkgtgz): HEAD = v$(VERSION)
|
98
|
+
$(pkgtgz): manifest fix-perms
|
99
|
+
@test -n "$(distdir)"
|
100
|
+
$(RM) -r $(distdir)
|
101
|
+
mkdir -p $(distdir)
|
102
|
+
tar cf - $$(cat .manifest) | (cd $(distdir) && tar xf -)
|
103
|
+
cd pkg && tar cf - $(basename $(@F)) | gzip -9 > $(@F)+
|
104
|
+
mv $@+ $@
|
105
|
+
|
106
|
+
package: $(pkgtgz) $(pkggem)
|
107
|
+
|
108
|
+
release:: verify package
|
109
|
+
# push gem to RubyGems.org
|
110
|
+
gem push $(pkggem)
|
111
|
+
else
|
112
|
+
gem install-gem: GIT-VERSION-FILE
|
113
|
+
$(MAKE) $@ VERSION=$(GIT_VERSION)
|
114
|
+
endif
|
115
|
+
|
116
|
+
all:: check
|
117
|
+
test_units := $(wildcard test/test_*.rb)
|
118
|
+
test: check
|
119
|
+
check: test-unit
|
120
|
+
test-unit: $(test_units)
|
121
|
+
$(test_units): build
|
122
|
+
$(RUBY) -I $(lib) $@ $(RUBY_TEST_OPTS)
|
123
|
+
|
124
|
+
# this requires GNU coreutils variants
|
125
|
+
ifneq ($(RSYNC_DEST),)
|
126
|
+
publish_doc:
|
127
|
+
-git set-file-times
|
128
|
+
$(MAKE) doc
|
129
|
+
$(MAKE) doc_gz
|
130
|
+
$(RSYNC) -av doc/ $(RSYNC_DEST)/ \
|
131
|
+
--exclude index.html* --exclude created.rid*
|
132
|
+
git ls-files | xargs touch
|
133
|
+
endif
|
134
|
+
|
135
|
+
# Create gzip variants of the same timestamp as the original so nginx
|
136
|
+
# "gzip_static on" can serve the gzipped versions directly.
|
137
|
+
doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.gz$$')
|
138
|
+
doc_gz:
|
139
|
+
for i in $(docs); do \
|
140
|
+
gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done
|
141
|
+
check-warnings:
|
142
|
+
@(for i in $$(git ls-files '*.rb'| grep -v '^setup\.rb$$'); \
|
143
|
+
do $(RUBY) -d -W2 -c $$i; done) | grep -v '^Syntax OK$$' || :
|
144
|
+
|
145
|
+
ifneq ($(PLACEHOLDERS),)
|
146
|
+
$(PLACEHOLDERS):
|
147
|
+
echo olddoc_placeholder > $@
|
148
|
+
endif
|
149
|
+
|
150
|
+
.PHONY: all .FORCE-GIT-VERSION-FILE doc check test $(test_units) manifest
|
151
|
+
.PHONY: check-warnings
|
@@ -0,0 +1 @@
|
|
1
|
+
raindrops.gemspec
|
data/raindrops.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
manifest = File.exist?('.manifest') ?
|
3
|
+
IO.readlines('.manifest').map!(&:chomp!) : `git ls-files`.split("\n")
|
4
|
+
test_files = manifest.grep(%r{\Atest/test_.*\.rb\z})
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "raindrops-maintained"
|
8
|
+
s.version = (ENV["VERSION"] ||= '0.21.0').dup
|
9
|
+
s.authors = ["raindrops hackers"]
|
10
|
+
s.description = File.read('README').split("\n\n")[1]
|
11
|
+
s.email = %q{raindrops-public@yhbt.net}
|
12
|
+
s.extensions = %w(ext/raindrops/extconf.rb)
|
13
|
+
s.extra_rdoc_files = IO.readlines('.document').map!(&:chomp!).keep_if do |f|
|
14
|
+
File.exist?(f)
|
15
|
+
end
|
16
|
+
s.files = manifest
|
17
|
+
s.homepage = 'https://yhbt.net/raindrops/'
|
18
|
+
s.summary = 'real-time stats for preforking Rack servers'
|
19
|
+
s.required_ruby_version = '>= 1.9.3'
|
20
|
+
s.test_files = test_files
|
21
|
+
s.add_development_dependency('aggregate', '~> 0.2')
|
22
|
+
s.add_development_dependency('test-unit', '~> 3.0')
|
23
|
+
s.add_development_dependency('posix_mq', '~> 2.0')
|
24
|
+
s.add_development_dependency('rack', [ '>= 1.2', '< 4' ])
|
25
|
+
s.licenses = %w(LGPL-2.1+)
|
26
|
+
end
|