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
data/lib/raindrops.rb CHANGED
@@ -1,9 +1,20 @@
1
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@librelist.com
2
16
  class Raindrops
3
17
 
4
- # Raindrops is currently at version 0.4.1
5
- VERSION = '0.4.1'
6
-
7
18
  # Used to represent the number of +active+ and +queued+ sockets for
8
19
  # a single listen socket across all threads and processes on a
9
20
  # machine.
@@ -18,7 +29,7 @@ class Raindrops
18
29
  # +queued+ connections is the number of un-accept()-ed sockets in the
19
30
  # queue of a given listen socket.
20
31
  #
21
- # These stats are currently only available under Linux
32
+ # These stats are currently only available under \Linux
22
33
  class ListenStats < Struct.new(:active, :queued)
23
34
 
24
35
  # the sum of +active+ and +queued+ sockets
@@ -27,9 +38,11 @@ class Raindrops
27
38
  end
28
39
  end
29
40
 
30
- # TODO: pure Ruby version for single processes
31
- require 'raindrops_ext'
32
-
41
+ autoload :Linux, 'raindrops/linux'
33
42
  autoload :Struct, 'raindrops/struct'
34
43
  autoload :Middleware, 'raindrops/middleware'
44
+ autoload :Aggregate, 'raindrops/aggregate'
45
+ autoload :LastDataRecv, 'raindrops/last_data_recv'
46
+ autoload :Watcher, 'raindrops/watcher'
35
47
  end
48
+ require 'raindrops_ext'
@@ -0,0 +1,8 @@
1
+ # -*- encoding: binary -*-
2
+ #
3
+ # raindrops may use the {aggregate}[http://github.com/josephruscio/aggregate]
4
+ # RubyGem to aggregate statistics from TCP_Info lookups.
5
+ module Raindrops::Aggregate
6
+ autoload :PMQ, "raindrops/aggregate/pmq"
7
+ autoload :LastDataRecv, "raindrops/aggregate/last_data_recv"
8
+ end
@@ -0,0 +1,86 @@
1
+ # -*- encoding: binary -*-
2
+ require "socket"
3
+ #
4
+ #
5
+ # This module is used to extend TCPServer and Kgio::TCPServer objects
6
+ # and aggregate +last_data_recv+ times for all accepted clients. It
7
+ # is designed to be used with Raindrops::LastDataRecv Rack application
8
+ # but can be easily changed to work with other stats collection devices.
9
+ #
10
+ # Methods wrapped include:
11
+ # - TCPServer#accept
12
+ # - TCPServer#accept_nonblock
13
+ # - Kgio::TCPServer#kgio_accept
14
+ # - Kgio::TCPServer#kgio_tryaccept
15
+ module Raindrops::Aggregate::LastDataRecv
16
+ # :stopdoc:
17
+ TCP_Info = Raindrops::TCP_Info
18
+ # :startdoc:
19
+
20
+ # The integer value of +last_data_recv+ is sent to this object.
21
+ # This is usually a duck type compatible with the \Aggregate class,
22
+ # but can be *anything* that accepts the *<<* method.
23
+ attr_accessor :raindrops_aggregate
24
+
25
+ @@default_aggregate = nil
26
+
27
+ # By default, this is a Raindrops::Aggregate::PMQ object
28
+ # It may be anything that responds to *<<*
29
+ def self.default_aggregate
30
+ @@default_aggregate ||= Raindrops::Aggregate::PMQ.new
31
+ end
32
+
33
+ # Assign any object that responds to *<<*
34
+ def self.default_aggregate=(agg)
35
+ @@default_aggregate = agg
36
+ end
37
+
38
+ # automatically extends any TCPServer objects used by Unicorn
39
+ def self.cornify!
40
+ Unicorn::HttpServer::LISTENERS.each do |sock|
41
+ sock.extend(self) if TCPServer === sock
42
+ end
43
+ end
44
+
45
+ # each extended object needs to have TCP_DEFER_ACCEPT enabled
46
+ # for accuracy.
47
+ def self.extended(obj)
48
+ obj.raindrops_aggregate = default_aggregate
49
+ obj.setsockopt Socket::SOL_TCP, tcp_defer_accept = 9, seconds = 60
50
+ end
51
+
52
+ # :stopdoc:
53
+
54
+ def kgio_tryaccept(*args)
55
+ count! super
56
+ end
57
+
58
+ def kgio_accept(*args)
59
+ count! super
60
+ end
61
+
62
+ def accept
63
+ count! super
64
+ end
65
+
66
+ def accept_nonblock
67
+ count! super
68
+ end
69
+
70
+ # :startdoc:
71
+
72
+ # The +last_data_recv+ member of Raindrops::TCP_Info can be used to
73
+ # infer the time a client spent in the listen queue before it was
74
+ # accepted.
75
+ #
76
+ # We require TCP_DEFER_ACCEPT on the listen socket for
77
+ # +last_data_recv+ to be accurate
78
+ def count!(io)
79
+ if io
80
+ x = TCP_Info.new(io)
81
+ @raindrops_aggregate << x.last_data_recv
82
+ end
83
+ io
84
+ end
85
+ end
86
+
@@ -0,0 +1,239 @@
1
+ # -*- encoding: binary -*-
2
+ require "tempfile"
3
+ require "aggregate"
4
+ require "posix_mq"
5
+ require "fcntl"
6
+ require "io/extra"
7
+ require "thread"
8
+
9
+ # \Aggregate + POSIX message queues support for Ruby 1.9 and \Linux
10
+ #
11
+ # This class is duck-type compatible with \Aggregate and allows us to
12
+ # aggregate and share statistics from multiple processes/threads aided
13
+ # POSIX message queues. This is designed to be used with the
14
+ # Raindrops::LastDataRecv Rack application, but can be used independently
15
+ # on compatible Runtimes.
16
+ #
17
+ # Unlike the core of raindrops, this is only supported on Ruby 1.9 and
18
+ # Linux 2.6. Using this class requires the following additional RubyGems
19
+ # or libraries:
20
+ #
21
+ # * aggregate (tested with 0.2.2)
22
+ # * io-extra (tested with 1.2.3)
23
+ # * posix_mq (tested with 1.0.0)
24
+ #
25
+ # == Design
26
+ #
27
+ # There is one master thread which aggregates statistics. Individual
28
+ # worker processes or threads will write to a shared POSIX message
29
+ # queue (default: "/raindrops") that the master reads from. At a
30
+ # predefined interval, the master thread will write out to a shared,
31
+ # anonymous temporary file that workers may read from
32
+ #
33
+ # Setting +:worker_interval+ and +:master_interval+ to +1+ will result
34
+ # in perfect accuracy but at the cost of a high synchronization
35
+ # overhead. Larger intervals mean less frequent messaging for higher
36
+ # performance but lower accuracy.
37
+ class Raindrops::Aggregate::PMQ
38
+
39
+ # :stopdoc:
40
+ # These constants are for Linux. This is designed for aggregating
41
+ # TCP_INFO.
42
+ RDLOCK = [ Fcntl::F_RDLCK ].pack("s @256")
43
+ WRLOCK = [ Fcntl::F_WRLCK ].pack("s @256")
44
+ UNLOCK = [ Fcntl::F_UNLCK ].pack("s @256")
45
+ # :startdoc:
46
+
47
+ # returns the number of dropped messages sent to a POSIX message
48
+ # queue if non-blocking operation was desired with :lossy
49
+ attr_reader :nr_dropped
50
+
51
+ #
52
+ # Creates a new Raindrops::Aggregate::PMQ object
53
+ #
54
+ # Raindrops::Aggregate::PMQ.new(options = {}) -> aggregate
55
+ #
56
+ # +options+ is a hash that accepts the following keys:
57
+ #
58
+ # * :queue - name of the POSIX message queue (default: "/raindrops")
59
+ # * :worker_interval - interval to send to the master (default: 10)
60
+ # * :master_interval - interval to for the master to write out (default: 5)
61
+ # * :lossy - workers drop packets if master cannot keep up (default: false)
62
+ # * :aggregate - \Aggregate object (default: \Aggregate.new)
63
+ # * :mq_umask - umask for creatingthe POSIX message queue (default: 0666)
64
+ #
65
+ def initialize(params = {})
66
+ opts = {
67
+ :queue => ENV["RAINDROPS_MQUEUE"] || "/raindrops",
68
+ :worker_interval => 10,
69
+ :master_interval => 5,
70
+ :lossy => false,
71
+ :mq_attr => nil,
72
+ :mq_umask => 0666,
73
+ :aggregate => Aggregate.new,
74
+ }.merge! params
75
+ @master_interval = opts[:master_interval]
76
+ @worker_interval = opts[:worker_interval]
77
+ @aggregate = opts[:aggregate]
78
+ @worker_queue = @worker_interval ? [] : nil
79
+ @mutex = Mutex.new
80
+
81
+ @mq_name = opts[:queue]
82
+ mq = POSIX_MQ.new @mq_name, :w, opts[:mq_umask], opts[:mq_attr]
83
+ Tempfile.open("raindrops_pmq") do |t|
84
+ @wr = File.open(t.path, "wb")
85
+ @rd = File.open(t.path, "rb")
86
+ end
87
+ @cached_aggregate = @aggregate
88
+ flush_master
89
+ @mq_send = if opts[:lossy]
90
+ @nr_dropped = 0
91
+ mq.nonblock = true
92
+ mq.method :trysend
93
+ else
94
+ mq.method :send
95
+ end
96
+ end
97
+
98
+ # adds a sample to the underlying \Aggregate object
99
+ def << val
100
+ if q = @worker_queue
101
+ q << val
102
+ if q.size >= @worker_interval
103
+ mq_send(q) or @nr_dropped += 1
104
+ q.clear
105
+ end
106
+ else
107
+ mq_send(val) or @nr_dropped += 1
108
+ end
109
+ end
110
+
111
+ def mq_send(val) # :nodoc:
112
+ @cached_aggregate = nil
113
+ @mq_send.call Marshal.dump(val)
114
+ end
115
+
116
+ #
117
+ # Starts running a master loop, usually in a dedicated thread or process:
118
+ #
119
+ # Thread.new { agg.master_loop }
120
+ #
121
+ # Any worker can call +agg.stop_master_loop+ to stop the master loop
122
+ # (possibly causing the thread or process to exit)
123
+ def master_loop
124
+ buf = ""
125
+ a = @aggregate
126
+ nr = 0
127
+ mq = POSIX_MQ.new @mq_name, :r # this one is always blocking
128
+ begin
129
+ if (nr -= 1) < 0
130
+ nr = @master_interval
131
+ flush_master
132
+ end
133
+ mq.shift(buf)
134
+ data = begin
135
+ Marshal.load(buf) or return
136
+ rescue ArgumentError, TypeError
137
+ next
138
+ end
139
+ Array === data ? data.each { |x| a << x } : a << data
140
+ rescue Errno::EINTR
141
+ rescue => e
142
+ warn "Unhandled exception in #{__FILE__}:#{__LINE__}: #{e}"
143
+ break
144
+ end while true
145
+ ensure
146
+ flush_master
147
+ end
148
+
149
+ # Loads the last shared \Aggregate from the master thread/process
150
+ def aggregate
151
+ @cached_aggregate ||= begin
152
+ flush
153
+ Marshal.load(synchronize(@rd, RDLOCK) do |rd|
154
+ IO.pread rd.fileno, rd.stat.size, 0
155
+ end)
156
+ end
157
+ end
158
+
159
+ # Flushes the currently aggregate statistics to a temporary file.
160
+ # There is no need to call this explicitly as +:worker_interval+ defines
161
+ # how frequently your data will be flushed for workers to read.
162
+ def flush_master
163
+ dump = Marshal.dump @aggregate
164
+ synchronize(@wr, WRLOCK) do |wr|
165
+ wr.truncate 0
166
+ IO.pwrite wr.fileno, dump, 0
167
+ end
168
+ end
169
+
170
+ # stops the currently running master loop, may be called from any
171
+ # worker thread or process
172
+ def stop_master_loop
173
+ sleep 0.1 until mq_send(false)
174
+ rescue Errno::EINTR
175
+ retry
176
+ end
177
+
178
+ def lock! io, type # :nodoc:
179
+ io.fcntl Fcntl::F_SETLKW, type
180
+ rescue Errno::EINTR
181
+ retry
182
+ end
183
+
184
+ # we use both a mutex for thread-safety and fcntl lock for process-safety
185
+ def synchronize io, type # :nodoc:
186
+ @mutex.synchronize do
187
+ begin
188
+ lock! io, type
189
+ yield io
190
+ ensure
191
+ lock! io, UNLOCK
192
+ end
193
+ end
194
+ end
195
+
196
+ # flushes the local queue of the worker process, sending all pending
197
+ # data to the master. There is no need to call this explicitly as
198
+ # +:worker_interval+ defines how frequently your queue will be flushed
199
+ def flush
200
+ if q = @local_queue && ! q.empty?
201
+ mq_send q
202
+ q.clear
203
+ end
204
+ nil
205
+ end
206
+
207
+ # proxy for \Aggregate#count
208
+ def count; aggregate.count; end
209
+
210
+ # proxy for \Aggregate#max
211
+ def max; aggregate.max; end
212
+
213
+ # proxy for \Aggregate#min
214
+ def min; aggregate.min; end
215
+
216
+ # proxy for \Aggregate#sum
217
+ def sum; aggregate.sum; end
218
+
219
+ # proxy for \Aggregate#mean
220
+ def mean; aggregate.mean; end
221
+
222
+ # proxy for \Aggregate#std_dev
223
+ def std_dev; aggregate.std_dev; end
224
+
225
+ # proxy for \Aggregate#outliers_low
226
+ def outliers_low; aggregate.outliers_low; end
227
+
228
+ # proxy for \Aggregate#outliers_high
229
+ def outliers_high; aggregate.outliers_high; end
230
+
231
+ # proxy for \Aggregate#to_s
232
+ def to_s(*args); aggregate.to_s *args; end
233
+
234
+ # proxy for \Aggregate#each
235
+ def each; aggregate.each { |*args| yield *args }; end
236
+
237
+ # proxy for \Aggregate#each_nonzero
238
+ def each_nonzero; aggregate.each_nonzero { |*args| yield *args }; end
239
+ end
@@ -0,0 +1,100 @@
1
+ # -*- encoding: binary -*-
2
+ require "raindrops"
3
+
4
+ # This is highly experimental!
5
+ #
6
+ # A self-contained Rack application for aggregating in the
7
+ # +tcpi_last_data_recv+ field in +struct tcp_info+ if
8
+ # /usr/include/linux/tcp.h. This is only useful for \Linux 2.6 and later.
9
+ # This primarily supports Unicorn and derived servers, but may also be
10
+ # used with any Ruby web server using the core TCPServer class in Ruby.
11
+ #
12
+ # Hitting the Rack endpoint configured for this application will return
13
+ # a an ASCII histogram response body with the following headers:
14
+ #
15
+ # - X-Count - number of requests received
16
+ #
17
+ # The following headers are only present if X-Count is greater than one.
18
+ #
19
+ # - X-Min - lowest last_data_recv time recorded (in milliseconds)
20
+ # - X-Max - highest last_data_recv time recorded (in milliseconds)
21
+ # - X-Mean - mean last_data_recv time recorded (rounded, in milliseconds)
22
+ # - X-Std-Dev - standard deviation of last_data_recv times
23
+ # - X-Outliers-Low - number of low outliers (hopefully many!)
24
+ # - X-Outliers-High - number of high outliers (hopefully zero!)
25
+ #
26
+ # == To use with Unicorn and derived servers (preload_app=false):
27
+ #
28
+ # Put the following in our Unicorn config file (not config.ru):
29
+ #
30
+ # require "raindrops/last_data_recv"
31
+ #
32
+ # Then follow the instructions below for config.ru:
33
+ #
34
+ # == To use with any Rack server using TCPServer
35
+ #
36
+ # Setup a route for Raindrops::LastDataRecv in your Rackup config file
37
+ # (typically config.ru):
38
+ #
39
+ # require "raindrops"
40
+ # map "/raindrops/last_data_recv" do
41
+ # run Raindrops::LastDataRecv.new
42
+ # end
43
+ # map "/" do
44
+ # use SomeMiddleware
45
+ # use MoreMiddleware
46
+ # # ...
47
+ # run YourAppHere.new
48
+ # end
49
+ #
50
+ # == To use with any other Ruby web server that uses TCPServer
51
+ #
52
+ # Put the following in any piece of Ruby code loaded after the server has
53
+ # bound its TCP listeners:
54
+ #
55
+ # ObjectSpace.each_object(TCPServer) do |s|
56
+ # s.extend Raindrops::Aggregate::LastDataRecv
57
+ # end
58
+ #
59
+ # Thread.new do
60
+ # Raindrops::Aggregate::LastDataRecv.default_aggregate.master_loop
61
+ # end
62
+ #
63
+ # Then follow the above instructions for config.ru
64
+ #
65
+ class Raindrops::LastDataRecv
66
+ # :stopdoc:
67
+
68
+ # trigger autoloads
69
+ if defined?(Unicorn)
70
+ agg = Raindrops::Aggregate::LastDataRecv.default_aggregate
71
+ AGGREGATE_THREAD = Thread.new { agg.master_loop }
72
+ end
73
+ # :startdoc
74
+
75
+ def initialize(opts = {})
76
+ Raindrops::Aggregate::LastDataRecv.cornify! if defined?(Unicorn)
77
+ @aggregate =
78
+ opts[:aggregate] || Raindrops::Aggregate::LastDataRecv.default_aggregate
79
+ end
80
+
81
+ def call(_)
82
+ a = @aggregate
83
+ count = a.count
84
+ headers = {
85
+ "Content-Type" => "text/plain",
86
+ "X-Count" => count.to_s,
87
+ }
88
+ if count > 1
89
+ headers["X-Min"] = a.min.to_s
90
+ headers["X-Max"] = a.max.to_s
91
+ headers["X-Mean"] = a.mean.round.to_s
92
+ headers["X-Std-Dev"] = a.std_dev.round.to_s
93
+ headers["X-Outliers-Low"] = a.outliers_low.to_s
94
+ headers["X-Outliers-High"] = a.outliers_high.to_s
95
+ end
96
+ body = a.to_s
97
+ headers["Content-Length"] = body.size.to_s
98
+ [ 200, headers, [ body ] ]
99
+ end
100
+ end