raindrops 0.4.1 → 0.5.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 (49) hide show
  1. data/.document +2 -1
  2. data/.gitignore +4 -0
  3. data/.wrongdoc.yml +4 -0
  4. data/GIT-VERSION-GEN +1 -1
  5. data/GNUmakefile +2 -196
  6. data/Gemfile +7 -0
  7. data/LICENSE +1 -1
  8. data/README +17 -47
  9. data/Rakefile +0 -104
  10. data/examples/linux-listener-stats.rb +123 -0
  11. data/examples/{config.ru → middleware.ru} +1 -1
  12. data/examples/watcher.ru +4 -0
  13. data/examples/watcher_demo.ru +13 -0
  14. data/examples/zbatery.conf.rb +13 -0
  15. data/ext/raindrops/extconf.rb +5 -0
  16. data/ext/raindrops/linux_inet_diag.c +449 -151
  17. data/ext/raindrops/linux_tcp_info.c +170 -0
  18. data/ext/raindrops/my_fileno.h +36 -0
  19. data/ext/raindrops/raindrops.c +232 -20
  20. data/lib/raindrops.rb +20 -7
  21. data/lib/raindrops/aggregate.rb +8 -0
  22. data/lib/raindrops/aggregate/last_data_recv.rb +86 -0
  23. data/lib/raindrops/aggregate/pmq.rb +239 -0
  24. data/lib/raindrops/last_data_recv.rb +100 -0
  25. data/lib/raindrops/linux.rb +26 -16
  26. data/lib/raindrops/middleware.rb +112 -41
  27. data/lib/raindrops/middleware/proxy.rb +34 -0
  28. data/lib/raindrops/struct.rb +15 -0
  29. data/lib/raindrops/watcher.rb +362 -0
  30. data/pkg.mk +171 -0
  31. data/raindrops.gemspec +10 -20
  32. data/test/ipv6_enabled.rb +10 -0
  33. data/test/rack_unicorn.rb +12 -0
  34. data/test/test_aggregate_pmq.rb +65 -0
  35. data/test/test_inet_diag_socket.rb +13 -0
  36. data/test/test_last_data_recv_unicorn.rb +69 -0
  37. data/test/test_linux.rb +55 -57
  38. data/test/test_linux_all_tcp_listen_stats.rb +66 -0
  39. data/test/test_linux_all_tcp_listen_stats_leak.rb +43 -0
  40. data/test/test_linux_ipv6.rb +158 -0
  41. data/test/test_linux_tcp_info.rb +61 -0
  42. data/test/test_middleware.rb +15 -2
  43. data/test/test_middleware_unicorn.rb +37 -0
  44. data/test/test_middleware_unicorn_ipv6.rb +37 -0
  45. data/test/test_raindrops.rb +65 -1
  46. data/test/test_raindrops_gc.rb +23 -1
  47. data/test/test_watcher.rb +85 -0
  48. metadata +69 -22
  49. data/examples/linux-tcp-listener-stats.rb +0 -44
@@ -1,6 +1,14 @@
1
1
  # -*- encoding: binary -*-
2
- class Raindrops
3
- module Linux
2
+
3
+ # For reporting TCP ListenStats, users of older \Linux kernels need to ensure
4
+ # that the the "inet_diag" and "tcp_diag" kernel modules are loaded as they do
5
+ # not autoload correctly. The inet_diag facilities of \Raindrops is useful
6
+ # for periodic snapshot reporting of listen queue sizes.
7
+ #
8
+ # Instead of snapshotting, Raindrops::Aggregate::LastDataRecv may be used
9
+ # to aggregate statistics from +all+ accepted sockets as they arrive
10
+ # based on the +last_data_recv+ field in Raindrops::TCP_Info
11
+ module Raindrops::Linux
4
12
 
5
13
  # The standard proc path for active UNIX domain sockets, feel free to call
6
14
  # String#replace on this if your /proc is mounted in a non-standard location
@@ -11,7 +19,7 @@ module Linux
11
19
  # Get ListenStats from an array of +paths+
12
20
  #
13
21
  # Socket state mapping from integer => symbol, based on socket_state
14
- # enum from include/linux/net.h in the Linux kernel:
22
+ # enum from include/linux/net.h in the \Linux kernel:
15
23
  # typedef enum {
16
24
  # SS_FREE = 0, /* not allocated */
17
25
  # SS_UNCONNECTED, /* unconnected to any socket */
@@ -26,20 +34,24 @@ module Linux
26
34
  # counterpart due to the latter being able to use inet_diag via netlink.
27
35
  # This parses /proc/net/unix as there is no other (known) way
28
36
  # to expose Unix domain socket statistics over netlink.
29
- def unix_listener_stats(paths)
30
- rv = Hash.new { |h,k| h[k.freeze] = ListenStats.new(0, 0) }
31
- paths = paths.map do |path|
32
- path = path.dup
33
- path.force_encoding(Encoding::BINARY) if defined?(Encoding)
34
- rv[path]
35
- Regexp.escape(path)
37
+ def unix_listener_stats(paths = nil)
38
+ rv = Hash.new { |h,k| h[k.freeze] = Raindrops::ListenStats.new(0, 0) }
39
+ if nil == paths
40
+ paths = [ '[^\n]+' ]
41
+ else
42
+ paths = paths.map do |path|
43
+ path = path.dup
44
+ path.force_encoding(Encoding::BINARY) if defined?(Encoding)
45
+ rv[path]
46
+ Regexp.escape(path)
47
+ end
36
48
  end
37
- paths = / 00000000 \d+ (\d+)\s+\d+ (#{paths.join('|')})$/n
49
+ paths = /^\w+: \d+ \d+ 00000000 \d+ (\d+)\s+\d+ (#{paths.join('|')})$/n
38
50
 
39
51
  # no point in pread since we can't stat for size on this file
40
52
  File.read(*PROC_NET_UNIX_ARGS).scan(paths) do |s|
41
- path = s.last
42
- case s.first.to_i
53
+ path = s[-1]
54
+ case s[0].to_i
43
55
  when 2 then rv[path].queued += 1
44
56
  when 3 then rv[path].active += 1
45
57
  end
@@ -47,8 +59,6 @@ module Linux
47
59
 
48
60
  rv
49
61
  end
50
-
51
62
  module_function :unix_listener_stats
52
63
 
53
- end # Linux
54
- end # Raindrops
64
+ end # Raindrops::Linux
@@ -1,71 +1,144 @@
1
1
  # -*- encoding: binary -*-
2
2
  require 'raindrops'
3
3
 
4
- # Raindrops middleware should be loaded at the top of Rack
5
- # middleware stack before other middlewares for maximum accuracy.
6
- class Raindrops
7
- class Middleware < ::Struct.new(:app, :stats, :path, :tcp, :unix)
4
+ # Raindrops::Middleware is Rack middleware that allows snapshotting
5
+ # current activity from an HTTP request. For all operating systems,
6
+ # it returns at least the following fields:
7
+ #
8
+ # * calling - the number of application dispatchers on your machine
9
+ # * writing - the number of clients being written to on your machine
10
+ #
11
+ # Additional fields are available for \Linux users.
12
+ #
13
+ # It should be loaded at the top of Rack middleware stack before other
14
+ # middlewares for maximum accuracy.
15
+ #
16
+ # === Usage (Rainbows!/Unicorn preload_app=false)
17
+ #
18
+ # If you're using preload_app=false (the default) in your Rainbows!/Unicorn
19
+ # config file, you'll need to create the global Stats object before
20
+ # forking.
21
+ #
22
+ # require 'raindrops'
23
+ # $stats ||= Raindrops::Middleware::Stats.new
24
+ #
25
+ # In your Rack config.ru:
26
+ #
27
+ # use Raindrops::Middleware, :stats => $stats
28
+ #
29
+ # === Usage (Rainbows!/Unicorn preload_app=true)
30
+ #
31
+ # If you're using preload_app=true in your Rainbows!/Unicorn
32
+ # config file, just add the middleware to your stack:
33
+ #
34
+ # In your Rack config.ru:
35
+ #
36
+ # use Raindrops::Middleware
37
+ #
38
+ # === Linux-only extras!
39
+ #
40
+ # To get bound listener statistics under \Linux, you need to specify the
41
+ # listener names for your server. You can even include listen sockets for
42
+ # *other* servers on the same machine. This can be handy for monitoring
43
+ # your nginx proxy as well.
44
+ #
45
+ # In your Rack config.ru, just pass the :listeners argument as an array of
46
+ # strings (along with any other arguments). You can specify any
47
+ # combination of TCP or Unix domain socket names:
48
+ #
49
+ # use Raindrops::Middleware, :listeners => %w(0.0.0.0:80 /tmp/.sock)
50
+ #
51
+ # If you're running Unicorn 0.98.0 or later, you don't have to pass in
52
+ # the :listeners array, Raindrops will automatically detect the listeners
53
+ # used by Unicorn master process. This does not detect listeners in
54
+ # different processes, of course.
55
+ #
56
+ # The response body includes the following stats for each listener
57
+ # (see also Raindrops::ListenStats):
58
+ #
59
+ # * active - total number of active clients on that listener
60
+ # * queued - total number of queued (pre-accept()) clients on that listener
61
+ #
62
+ # = Demo Server
63
+ #
64
+ # There is a server running this middleware (and Watcher) at
65
+ # http://raindrops-demo.bogomips.org/_raindrops
66
+ #
67
+ # Also check out the Watcher demo at http://raindrops-demo.bogomips.org/
68
+ #
69
+ # The demo server is only limited to 30 users, so be sure not to abuse it
70
+ # by using the /tail/ endpoint too much.
71
+ #
72
+ class Raindrops::Middleware
73
+ attr_accessor :app, :stats, :path, :tcp, :unix # :nodoc:
74
+
75
+ # A Raindrops::Struct used to count the number of :calling and :writing
76
+ # clients. This struct is intended to be shared across multiple processes
77
+ # and both counters are updated atomically.
78
+ #
79
+ # This is supported on all operating systems supported by Raindrops
80
+ class Stats < Raindrops::Struct.new(:calling, :writing)
81
+ end
8
82
 
9
83
  # :stopdoc:
10
- Stats = Raindrops::Struct.new(:calling, :writing)
11
84
  PATH_INFO = "PATH_INFO"
85
+ require "raindrops/middleware/proxy"
12
86
  # :startdoc:
13
87
 
88
+ # +app+ may be any Rack application, this middleware wraps it.
89
+ # +opts+ is a hash that understands the following members:
90
+ #
91
+ # * :stats - Raindrops::Middleware::Stats struct (default: Stats.new)
92
+ # * :path - HTTP endpoint used for reading the stats (default: "/_raindrops")
93
+ # * :listeners - array of host:port or socket paths (default: from Unicorn)
14
94
  def initialize(app, opts = {})
15
- super(app, opts[:stats] || Stats.new, opts[:path] || "/_raindrops")
95
+ @app = app
96
+ @stats = opts[:stats] || Stats.new
97
+ @path = opts[:path] || "/_raindrops"
16
98
  tmp = opts[:listeners]
17
99
  if tmp.nil? && defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
18
100
  tmp = Unicorn.listener_names
19
101
  end
102
+ @tcp = @unix = nil
20
103
 
21
104
  if tmp
22
- self.tcp = tmp.grep(/\A[^:]+:\d+\z/)
23
- self.unix = tmp.grep(%r{\A/})
24
- self.tcp = nil if tcp.empty?
25
- self.unix = nil if unix.empty?
105
+ @tcp = tmp.grep(/\A.+:\d+\z/)
106
+ @unix = tmp.grep(%r{\A/})
107
+ @tcp = nil if @tcp.empty?
108
+ @unix = nil if @unix.empty?
26
109
  end
27
110
  end
28
111
 
29
112
  # standard Rack endpoint
30
- def call(env)
31
- env[PATH_INFO] == path ? stats_response : dup._call(env)
32
- end
113
+ def call(env) # :nodoc:
114
+ env[PATH_INFO] == @path and return stats_response
115
+ begin
116
+ @stats.incr_calling
33
117
 
34
- def _call(env)
35
- stats.incr_calling
36
- status, headers, self.app = app.call(env)
118
+ status, headers, body = @app.call(env)
119
+ rv = [ status, headers, Proxy.new(body, @stats) ]
37
120
 
38
- # the Rack server will start writing headers soon after this method
39
- stats.incr_writing
40
- [ status, headers, self ]
121
+ # the Rack server will start writing headers soon after this method
122
+ @stats.incr_writing
123
+ rv
41
124
  ensure
42
- stats.decr_calling
43
- end
44
-
45
- # yield to the Rack server here for writing
46
- def each(&block)
47
- app.each(&block)
48
- end
49
-
50
- # the Rack server should call this after #each (usually ensure-d)
51
- def close
52
- stats.decr_writing
53
- app.close if app.respond_to?(:close)
125
+ @stats.decr_calling
126
+ end
54
127
  end
55
128
 
56
- def stats_response
57
- body = "calling: #{stats.calling}\n" \
58
- "writing: #{stats.writing}\n"
129
+ def stats_response # :nodoc:
130
+ body = "calling: #{@stats.calling}\n" \
131
+ "writing: #{@stats.writing}\n"
59
132
 
60
- if defined?(Linux)
61
- Linux.tcp_listener_stats(tcp).each do |addr,stats|
133
+ if defined?(Raindrops::Linux)
134
+ Raindrops::Linux.tcp_listener_stats(@tcp).each do |addr,stats|
62
135
  body << "#{addr} active: #{stats.active}\n" \
63
136
  "#{addr} queued: #{stats.queued}\n"
64
- end if tcp
65
- Linux.unix_listener_stats(unix).each do |addr,stats|
137
+ end if @tcp
138
+ Raindrops::Linux.unix_listener_stats(@unix).each do |addr,stats|
66
139
  body << "#{addr} active: #{stats.active}\n" \
67
140
  "#{addr} queued: #{stats.queued}\n"
68
- end if unix
141
+ end if @unix
69
142
  end
70
143
 
71
144
  headers = {
@@ -74,6 +147,4 @@ class Middleware < ::Struct.new(:app, :stats, :path, :tcp, :unix)
74
147
  }
75
148
  [ 200, headers, [ body ] ]
76
149
  end
77
-
78
- end
79
150
  end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: binary -*-
2
+ # :stopdoc:
3
+ # This class is by Raindrops::Middleware to proxy application response
4
+ # bodies. There should be no need to use it directly.
5
+ class Raindrops::Middleware::Proxy
6
+ def initialize(body, stats)
7
+ @body, @stats = body, stats
8
+ end
9
+
10
+ # yield to the Rack server here for writing
11
+ def each
12
+ @body.each { |x| yield x }
13
+ end
14
+
15
+ # the Rack server should call this after #each (usually ensure-d)
16
+ def close
17
+ @stats.decr_writing
18
+ @body.close if @body.respond_to?(:close)
19
+ end
20
+
21
+ # Some Rack servers can optimize response processing if it responds
22
+ # to +to_path+ via the sendfile(2) system call, we proxy +to_path+
23
+ # to the underlying body if possible.
24
+ def to_path
25
+ @body.to_path
26
+ end
27
+
28
+ # Rack servers use +respond_to?+ to check for the presence of +close+
29
+ # and +to_path+ methods.
30
+ def respond_to?(m)
31
+ m = m.to_sym
32
+ :close == m || @body.respond_to?(m)
33
+ end
34
+ end
@@ -1,7 +1,22 @@
1
1
  # -*- encoding: binary -*-
2
2
 
3
+ # This is a wrapper around Raindrops objects much like the core Ruby
4
+ # \Struct can be seen as a wrapper around the core \Array class.
5
+ # It's usage is similar to the core \Struct class, except its fields
6
+ # may only be used to house unsigned long integers.
7
+ #
8
+ # class Foo < Raindrops::Struct.new(:readers, :writers)
9
+ # end
10
+ #
11
+ # foo = Foo.new 0, 0
12
+ #
13
+ # foo.incr_writers -> 1
14
+ # foo.incr_readers -> 1
15
+ #
3
16
  class Raindrops::Struct
4
17
 
18
+ # returns a new class derived from Raindrops::Struct and supporting
19
+ # the given +members+ as fields, just like \Struct.new in core Ruby.
5
20
  def self.new(*members)
6
21
  members = members.map { |x| x.to_sym }.freeze
7
22
  str = <<EOS
@@ -0,0 +1,362 @@
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}[http://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 http://example.com/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 http://example.com/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 http://example.com/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 http://example.com/queued/0.0.0.0%3A80.html
60
+ #
61
+ # === GET /tail/$LISTENER.txt?active_min=1&queued_min=1
62
+ #
63
+ # Streams chunked a response to the client.
64
+ # Interval is the preconfigured +:delay+ of the application (default 1 second)
65
+ #
66
+ # The response is plain text in the following format:
67
+ #
68
+ # ISO8601_TIMESTAMP LISTENER_NAME ACTIVE_COUNT QUEUED_COUNT LINEFEED
69
+ #
70
+ # Query parameters:
71
+ #
72
+ # - active_min - do not stream a line until this active count is reached
73
+ # - queued_min - do not stream a line until this queued count is reached
74
+ #
75
+ # == Response headers (mostly the same as Raindrops::LastDataRecv)
76
+ #
77
+ # - X-Count - number of requests received
78
+ # - X-Last-Reset - date since the last reset
79
+ #
80
+ # The following headers are only present if X-Count is greater than one.
81
+ #
82
+ # - X-Min - lowest last_data_recv time recorded (in milliseconds)
83
+ # - X-Max - highest last_data_recv time recorded (in milliseconds)
84
+ # - X-Mean - mean last_data_recv time recorded (rounded, in milliseconds)
85
+ # - X-Std-Dev - standard deviation of last_data_recv times
86
+ # - X-Outliers-Low - number of low outliers (hopefully many!)
87
+ # - X-Outliers-High - number of high outliers (hopefully zero!)
88
+ #
89
+ # = Demo Server
90
+ #
91
+ # There is a server running this app at http://raindrops-demo.bogomips.org/
92
+ # The Raindrops::Middleware demo is also accessible at
93
+ # http://raindrops-demo.bogomips.org/_raindrops
94
+ #
95
+ # The demo server is only limited to 30 users, so be sure not to abuse it
96
+ # by using the /tail/ endpoint too much.
97
+ class Raindrops::Watcher
98
+ # :stopdoc:
99
+ attr_reader :snapshot
100
+ include Rack::Utils
101
+ include Raindrops::Linux
102
+
103
+ def initialize(opts = {})
104
+ @tcp_listeners = @unix_listeners = nil
105
+ if l = opts[:listeners]
106
+ tcp, unix = [], []
107
+ Array(l).each { |addr| (addr =~ %r{\A/} ? unix : tcp) << addr }
108
+ unless tcp.empty? && unix.empty?
109
+ @tcp_listeners = tcp
110
+ @unix_listeners = unix
111
+ end
112
+ end
113
+
114
+ agg_class = opts[:agg_class] || Aggregate
115
+ start = Time.now.utc
116
+ @active = Hash.new { |h,k| h[k] = agg_class.new }
117
+ @queued = Hash.new { |h,k| h[k] = agg_class.new }
118
+ @resets = Hash.new { |h,k| h[k] = start }
119
+ @snapshot = [ start, {} ]
120
+ @delay = opts[:delay] || 1
121
+ @lock = Mutex.new
122
+ @start = Mutex.new
123
+ @cond = ConditionVariable.new
124
+ @thr = nil
125
+ end
126
+
127
+ def hostname
128
+ Socket.gethostname
129
+ end
130
+
131
+ # rack endpoint
132
+ def call(env)
133
+ @start.synchronize { @thr ||= aggregator_thread(env["rack.logger"]) }
134
+ case env["REQUEST_METHOD"]
135
+ when "HEAD", "GET"
136
+ get env
137
+ when "POST"
138
+ post env
139
+ else
140
+ Rack::Response.new(["Method Not Allowed"], 405).finish
141
+ end
142
+ end
143
+
144
+ def aggregator_thread(logger) # :nodoc:
145
+ @socket = sock = Raindrops::InetDiagSocket.new
146
+ thr = Thread.new do
147
+ begin
148
+ combined = tcp_listener_stats(@tcp_listeners, sock)
149
+ combined.merge!(unix_listener_stats(@unix_listeners))
150
+ @lock.synchronize do
151
+ combined.each do |addr,stats|
152
+ @active[addr] << stats.active
153
+ @queued[addr] << stats.queued
154
+ end
155
+ @snapshot = [ Time.now.utc, combined ]
156
+ @cond.broadcast
157
+ end
158
+ rescue => e
159
+ logger.error "#{e.class} #{e.inspect}"
160
+ end while sleep(@delay) && @socket
161
+ sock.close
162
+ end
163
+ wait_snapshot
164
+ thr
165
+ end
166
+
167
+ def active_stats(addr) # :nodoc:
168
+ @lock.synchronize do
169
+ tmp = @active[addr] or return
170
+ [ @resets[addr], tmp.dup ]
171
+ end
172
+ end
173
+
174
+ def queued_stats(addr) # :nodoc:
175
+ @lock.synchronize do
176
+ tmp = @queued[addr] or return
177
+ [ @resets[addr], tmp.dup ]
178
+ end
179
+ end
180
+
181
+ def wait_snapshot
182
+ @lock.synchronize do
183
+ @cond.wait @lock
184
+ @snapshot
185
+ end
186
+ end
187
+
188
+ def agg_to_hash(reset_at, agg)
189
+ {
190
+ "X-Count" => agg.count.to_s,
191
+ "X-Min" => agg.min.to_s,
192
+ "X-Max" => agg.max.to_s,
193
+ "X-Mean" => agg.mean.to_s,
194
+ "X-Std-Dev" => agg.std_dev.to_s,
195
+ "X-Outliers-Low" => agg.outliers_low.to_s,
196
+ "X-Outliers-High" => agg.outliers_high.to_s,
197
+ "X-Last-Reset" => reset_at.httpdate,
198
+ }
199
+ end
200
+
201
+ def histogram_txt(agg)
202
+ reset_at, agg = *agg
203
+ headers = agg_to_hash(reset_at, agg)
204
+ body = agg.to_s
205
+ headers["Content-Type"] = "text/plain"
206
+ headers["Content-Length"] = bytesize(body).to_s
207
+ [ 200, headers, [ body ] ]
208
+ end
209
+
210
+ def histogram_html(agg, addr)
211
+ reset_at, agg = *agg
212
+ headers = agg_to_hash(reset_at, agg)
213
+ body = "<html>" \
214
+ "<head><title>#{hostname} - #{escape_html addr}</title></head>" \
215
+ "<body><table>" <<
216
+ headers.map { |k,v|
217
+ "<tr><td>#{k.gsub(/^X-/, '')}</td><td>#{v}</td></tr>"
218
+ }.join << "</table><pre>#{escape_html agg}</pre>" \
219
+ "<form action='/reset/#{escape addr}' method='post'>" \
220
+ "<input type='submit' name='x' value='reset' /></form>" \
221
+ "</body>"
222
+ headers["Content-Type"] = "text/html"
223
+ headers["Content-Length"] = bytesize(body).to_s
224
+ [ 200, headers, [ body ] ]
225
+ end
226
+
227
+ def get(env)
228
+ case env["PATH_INFO"]
229
+ when "/"
230
+ index
231
+ when %r{\A/active/(.+)\.txt\z}
232
+ histogram_txt(active_stats(unescape($1)))
233
+ when %r{\A/active/(.+)\.html\z}
234
+ addr = unescape $1
235
+ histogram_html(active_stats(addr), addr)
236
+ when %r{\A/queued/(.+)\.txt\z}
237
+ histogram_txt(queued_stats(unescape($1)))
238
+ when %r{\A/queued/(.+)\.html\z}
239
+ addr = unescape $1
240
+ histogram_html(queued_stats(addr), addr)
241
+ when %r{\A/tail/(.+)\.txt\z}
242
+ tail(unescape($1), env)
243
+ else
244
+ not_found
245
+ end
246
+ rescue Errno::EDOM
247
+ raise if defined?(retried)
248
+ retried = true
249
+ wait_snapshot
250
+ retry
251
+ end
252
+
253
+ def not_found
254
+ Rack::Response.new(["Not Found"], 404).finish
255
+ end
256
+
257
+ def post(env)
258
+ case env["PATH_INFO"]
259
+ when %r{\A/reset/(.+)\z}
260
+ reset!(env, unescape($1))
261
+ else
262
+ Rack::Response.new(["Not Found"], 404).finish
263
+ end
264
+ end
265
+
266
+ def reset!(env, addr)
267
+ @lock.synchronize do
268
+ @active.include?(addr) or return not_found
269
+ @active.delete addr
270
+ @queued.delete addr
271
+ @resets[addr] = Time.now.utc
272
+ @cond.wait @lock
273
+ end
274
+ req = Rack::Request.new(env)
275
+ res = Rack::Response.new
276
+ url = req.referer || "#{req.host_with_port}/"
277
+ res.redirect(url)
278
+ res.content_type.replace "text/plain"
279
+ res.write "Redirecting to #{url}"
280
+ res.finish
281
+ end
282
+
283
+ def index
284
+ updated_at, all = snapshot
285
+ headers = {
286
+ "Content-Type" => "text/html",
287
+ "Last-Modified" => updated_at.httpdate,
288
+ }
289
+ body = "<html><head>" \
290
+ "<title>#{hostname} - all interfaces</title>" \
291
+ "</head><body><h3>Updated at #{updated_at.iso8601}</h3>" \
292
+ "<table><tr>" \
293
+ "<th>address</th><th>active</th><th>queued</th><th>reset</th>" \
294
+ "</tr>" <<
295
+ all.map do |addr,stats|
296
+ e_addr = escape addr
297
+ "<tr>" \
298
+ "<td><a href='/tail/#{e_addr}.txt'>#{escape_html addr}</a></td>" \
299
+ "<td><a href='/active/#{e_addr}.html'>#{stats.active}</a></td>" \
300
+ "<td><a href='/queued/#{e_addr}.html'>#{stats.queued}</a></td>" \
301
+ "<td><form action='/reset/#{e_addr}' method='post'>" \
302
+ "<input type='submit' name='x' value='x' /></form></td>" \
303
+ "</tr>" \
304
+ end.join << "</table></body></html>"
305
+ headers["Content-Length"] = bytesize(body).to_s
306
+ [ 200, headers, [ body ] ]
307
+ end
308
+
309
+ def tail(addr, env)
310
+ Tailer.new(self, addr, env).finish
311
+ end
312
+ # :startdoc:
313
+
314
+ # This is the response body returned for "/tail/$ADDRESS.txt". This
315
+ # must use a multi-threaded Rack server with streaming response support.
316
+ # It is an internal class and not expected to be used directly
317
+ class Tailer
318
+ def initialize(rdmon, addr, env) # :nodoc:
319
+ @rdmon = rdmon
320
+ @addr = addr
321
+ q = Rack::Utils.parse_query env["QUERY_STRING"]
322
+ @active_min = q["active_min"].to_i
323
+ @queued_min = q["queued_min"].to_i
324
+ len = Rack::Utils.bytesize(addr)
325
+ len = 35 if len > 35
326
+ @fmt = "%20s % #{len}s % 10u % 10u\n"
327
+ case env["HTTP_VERSION"]
328
+ when "HTTP/1.0", nil
329
+ @chunk = false
330
+ else
331
+ @chunk = true
332
+ end
333
+ end
334
+
335
+ def finish
336
+ headers = { "Content-Type" => "text/plain" }
337
+ headers["Transfer-Encoding"] = "chunked" if @chunk
338
+ [ 200, headers, self ]
339
+ end
340
+
341
+ # called by the Rack server
342
+ def each # :nodoc:
343
+ begin
344
+ time, all = @rdmon.wait_snapshot
345
+ stats = all[@addr] or next
346
+ stats.queued >= @queued_min or next
347
+ stats.active >= @active_min or next
348
+ body = sprintf(@fmt, time.iso8601, @addr, stats.active, stats.queued)
349
+ body = "#{body.size.to_s(16)}\r\n#{body}\r\n" if @chunk
350
+ yield body
351
+ end while true
352
+ yield "0\r\n\r\n" if @chunk
353
+ end
354
+ end
355
+
356
+ # shuts down the background thread
357
+ def shutdown
358
+ @socket = nil
359
+ @thr.join if @thr
360
+ @thr = nil
361
+ end
362
+ end