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
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