raindrops 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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