raindrops-maintained 0.21.0

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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.document +7 -0
  3. data/.gitattributes +4 -0
  4. data/.gitignore +16 -0
  5. data/.manifest +62 -0
  6. data/.olddoc.yml +16 -0
  7. data/COPYING +165 -0
  8. data/GIT-VERSION-FILE +1 -0
  9. data/GIT-VERSION-GEN +40 -0
  10. data/GNUmakefile +4 -0
  11. data/LATEST +9 -0
  12. data/LICENSE +16 -0
  13. data/NEWS +384 -0
  14. data/README +101 -0
  15. data/TODO +3 -0
  16. data/archive/.gitignore +3 -0
  17. data/archive/slrnpull.conf +4 -0
  18. data/examples/linux-listener-stats.rb +122 -0
  19. data/examples/middleware.ru +5 -0
  20. data/examples/watcher.ru +4 -0
  21. data/examples/watcher_demo.ru +13 -0
  22. data/examples/yahns.conf.rb +30 -0
  23. data/examples/zbatery.conf.rb +16 -0
  24. data/ext/raindrops/extconf.rb +163 -0
  25. data/ext/raindrops/linux_inet_diag.c +713 -0
  26. data/ext/raindrops/my_fileno.h +16 -0
  27. data/ext/raindrops/raindrops.c +487 -0
  28. data/ext/raindrops/raindrops_atomic.h +23 -0
  29. data/ext/raindrops/tcp_info.c +245 -0
  30. data/lib/raindrops/aggregate/last_data_recv.rb +94 -0
  31. data/lib/raindrops/aggregate/pmq.rb +245 -0
  32. data/lib/raindrops/aggregate.rb +8 -0
  33. data/lib/raindrops/last_data_recv.rb +102 -0
  34. data/lib/raindrops/linux.rb +77 -0
  35. data/lib/raindrops/middleware/proxy.rb +40 -0
  36. data/lib/raindrops/middleware.rb +153 -0
  37. data/lib/raindrops/struct.rb +62 -0
  38. data/lib/raindrops/watcher.rb +428 -0
  39. data/lib/raindrops.rb +72 -0
  40. data/pkg.mk +151 -0
  41. data/raindrops-maintained.gemspec +1 -0
  42. data/raindrops.gemspec +26 -0
  43. data/setup.rb +1586 -0
  44. data/test/ipv6_enabled.rb +9 -0
  45. data/test/rack_unicorn.rb +11 -0
  46. data/test/test_aggregate_pmq.rb +65 -0
  47. data/test/test_inet_diag_socket.rb +16 -0
  48. data/test/test_last_data_recv.rb +57 -0
  49. data/test/test_last_data_recv_unicorn.rb +69 -0
  50. data/test/test_linux.rb +281 -0
  51. data/test/test_linux_all_tcp_listen_stats.rb +66 -0
  52. data/test/test_linux_all_tcp_listen_stats_leak.rb +43 -0
  53. data/test/test_linux_ipv6.rb +166 -0
  54. data/test/test_linux_middleware.rb +64 -0
  55. data/test/test_linux_reuseport_tcp_listen_stats.rb +51 -0
  56. data/test/test_middleware.rb +128 -0
  57. data/test/test_middleware_unicorn.rb +37 -0
  58. data/test/test_middleware_unicorn_ipv6.rb +37 -0
  59. data/test/test_raindrops.rb +207 -0
  60. data/test/test_raindrops_gc.rb +38 -0
  61. data/test/test_struct.rb +54 -0
  62. data/test/test_tcp_info.rb +88 -0
  63. data/test/test_watcher.rb +186 -0
  64. 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='&quot;tail&quot; 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