raindrops-maintained 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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,245 @@
1
+ #include <ruby.h>
2
+ #include <sys/socket.h>
3
+ #include <netinet/in.h>
4
+ #if defined(HAVE_LINUX_TCP_H)
5
+ # include <linux/tcp.h>
6
+ #else
7
+ # if defined(HAVE_NETINET_TCP_H)
8
+ # include <netinet/tcp.h>
9
+ # endif
10
+ # if defined(HAVE_NETINET_TCP_FSM_H)
11
+ # include <netinet/tcp_fsm.h>
12
+ # endif
13
+ #endif
14
+
15
+ #ifdef HAVE_TYPE_STRUCT_TCP_INFO
16
+ #include "my_fileno.h"
17
+
18
+ CFUNC_tcp_info_tcpi_state
19
+ CFUNC_tcp_info_tcpi_ca_state
20
+ CFUNC_tcp_info_tcpi_retransmits
21
+ CFUNC_tcp_info_tcpi_probes
22
+ CFUNC_tcp_info_tcpi_backoff
23
+ CFUNC_tcp_info_tcpi_options
24
+ CFUNC_tcp_info_tcpi_snd_wscale
25
+ CFUNC_tcp_info_tcpi_rcv_wscale
26
+ CFUNC_tcp_info_tcpi_rto
27
+ CFUNC_tcp_info_tcpi_ato
28
+ CFUNC_tcp_info_tcpi_snd_mss
29
+ CFUNC_tcp_info_tcpi_rcv_mss
30
+ CFUNC_tcp_info_tcpi_unacked
31
+ CFUNC_tcp_info_tcpi_sacked
32
+ CFUNC_tcp_info_tcpi_lost
33
+ CFUNC_tcp_info_tcpi_retrans
34
+ CFUNC_tcp_info_tcpi_fackets
35
+ CFUNC_tcp_info_tcpi_last_data_sent
36
+ CFUNC_tcp_info_tcpi_last_ack_sent
37
+ CFUNC_tcp_info_tcpi_last_data_recv
38
+ CFUNC_tcp_info_tcpi_last_ack_recv
39
+ CFUNC_tcp_info_tcpi_pmtu
40
+ CFUNC_tcp_info_tcpi_rcv_ssthresh
41
+ CFUNC_tcp_info_tcpi_rtt
42
+ CFUNC_tcp_info_tcpi_rttvar
43
+ CFUNC_tcp_info_tcpi_snd_ssthresh
44
+ CFUNC_tcp_info_tcpi_snd_cwnd
45
+ CFUNC_tcp_info_tcpi_advmss
46
+ CFUNC_tcp_info_tcpi_reordering
47
+ CFUNC_tcp_info_tcpi_rcv_rtt
48
+ CFUNC_tcp_info_tcpi_rcv_space
49
+ CFUNC_tcp_info_tcpi_total_retrans
50
+
51
+ static size_t tcpi_memsize(const void *ptr)
52
+ {
53
+ return sizeof(struct tcp_info);
54
+ }
55
+
56
+ static const rb_data_type_t tcpi_type = {
57
+ "tcp_info",
58
+ { NULL, RUBY_TYPED_DEFAULT_FREE, tcpi_memsize, /* reserved */ },
59
+ /* parent, data, [ flags ] */
60
+ };
61
+
62
+ static VALUE alloc(VALUE klass)
63
+ {
64
+ struct tcp_info *info;
65
+
66
+ return TypedData_Make_Struct(klass, struct tcp_info, &tcpi_type, info);
67
+ }
68
+
69
+ /*
70
+ * call-seq:
71
+ *
72
+ * Raindrops::TCP_Info.new(tcp_socket) -> TCP_Info object
73
+ *
74
+ * Reads a TCP_Info object from any given +tcp_socket+. See the tcp(7)
75
+ * manpage and /usr/include/linux/tcp.h for more details.
76
+ */
77
+ static VALUE init(VALUE self, VALUE io)
78
+ {
79
+ int fd = my_fileno(rb_io_get_io(io));
80
+ struct tcp_info *info = DATA_PTR(self);
81
+ socklen_t len = (socklen_t)sizeof(struct tcp_info);
82
+ int rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, info, &len);
83
+
84
+ if (rc != 0)
85
+ rb_sys_fail("getsockopt");
86
+
87
+ return self;
88
+ }
89
+
90
+ void Init_raindrops_tcp_info(void)
91
+ {
92
+ VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
93
+ VALUE cTCP_Info;
94
+
95
+ /*
96
+ * Document-class: Raindrops::TCP_Info
97
+ *
98
+ * This is used to wrap "struct tcp_info" as described in tcp(7)
99
+ * and /usr/include/linux/tcp.h. The following readers methods
100
+ * are defined corresponding to the "tcpi_" fields in the
101
+ * tcp_info struct.
102
+ *
103
+ * As of raindrops 0.18.0+, this is supported on FreeBSD and OpenBSD
104
+ * systems as well as Linux, although not all fields exist or
105
+ * match the documentation, below.
106
+ *
107
+ * In particular, the +last_data_recv+ field is useful for measuring
108
+ * the amount of time a client spent in the listen queue before
109
+ * +accept()+, but only if +TCP_DEFER_ACCEPT+ is used with the
110
+ * listen socket (it is on by default in Unicorn).
111
+ *
112
+ * - state
113
+ * - ca_state
114
+ * - retransmits
115
+ * - probes
116
+ * - backoff
117
+ * - options
118
+ * - snd_wscale
119
+ * - rcv_wscale
120
+ * - rto
121
+ * - ato
122
+ * - snd_mss
123
+ * - rcv_mss
124
+ * - unacked
125
+ * - sacked
126
+ * - lost
127
+ * - retrans
128
+ * - fackets
129
+ * - last_data_sent
130
+ * - last_ack_sent
131
+ * - last_data_recv
132
+ * - last_ack_recv
133
+ * - pmtu
134
+ * - rcv_ssthresh
135
+ * - rtt
136
+ * - rttvar
137
+ * - snd_ssthresh
138
+ * - snd_cwnd
139
+ * - advmss
140
+ * - reordering
141
+ * - rcv_rtt
142
+ * - rcv_space
143
+ * - total_retrans
144
+ *
145
+ * https://kernel.org/doc/man-pages/online/pages/man7/tcp.7.html
146
+ */
147
+ cTCP_Info = rb_define_class_under(cRaindrops, "TCP_Info", rb_cObject);
148
+ rb_define_alloc_func(cTCP_Info, alloc);
149
+ rb_define_private_method(cTCP_Info, "initialize", init, 1);
150
+
151
+ /*
152
+ * Document-method: Raindrops::TCP_Info#get!
153
+ *
154
+ * call-seq:
155
+ *
156
+ * info = Raindrops::TCP_Info.new(tcp_socket)
157
+ * info.get!(tcp_socket)
158
+ *
159
+ * Update an existing TCP_Info objects with the latest stats
160
+ * from the given socket. This even allows sharing TCP_Info
161
+ * objects between different sockets to avoid garbage.
162
+ */
163
+ rb_define_method(cTCP_Info, "get!", init, 1);
164
+
165
+ DEFINE_METHOD_tcp_info_tcpi_state;
166
+ DEFINE_METHOD_tcp_info_tcpi_ca_state;
167
+ DEFINE_METHOD_tcp_info_tcpi_retransmits;
168
+ DEFINE_METHOD_tcp_info_tcpi_probes;
169
+ DEFINE_METHOD_tcp_info_tcpi_backoff;
170
+ DEFINE_METHOD_tcp_info_tcpi_options;
171
+ DEFINE_METHOD_tcp_info_tcpi_snd_wscale;
172
+ DEFINE_METHOD_tcp_info_tcpi_rcv_wscale;
173
+ DEFINE_METHOD_tcp_info_tcpi_rto;
174
+ DEFINE_METHOD_tcp_info_tcpi_ato;
175
+ DEFINE_METHOD_tcp_info_tcpi_snd_mss;
176
+ DEFINE_METHOD_tcp_info_tcpi_rcv_mss;
177
+ DEFINE_METHOD_tcp_info_tcpi_unacked;
178
+ DEFINE_METHOD_tcp_info_tcpi_sacked;
179
+ DEFINE_METHOD_tcp_info_tcpi_lost;
180
+ DEFINE_METHOD_tcp_info_tcpi_retrans;
181
+ DEFINE_METHOD_tcp_info_tcpi_fackets;
182
+ DEFINE_METHOD_tcp_info_tcpi_last_data_sent;
183
+ DEFINE_METHOD_tcp_info_tcpi_last_ack_sent;
184
+ DEFINE_METHOD_tcp_info_tcpi_last_data_recv;
185
+ DEFINE_METHOD_tcp_info_tcpi_last_ack_recv;
186
+ DEFINE_METHOD_tcp_info_tcpi_pmtu;
187
+ DEFINE_METHOD_tcp_info_tcpi_rcv_ssthresh;
188
+ DEFINE_METHOD_tcp_info_tcpi_rtt;
189
+ DEFINE_METHOD_tcp_info_tcpi_rttvar;
190
+ DEFINE_METHOD_tcp_info_tcpi_snd_ssthresh;
191
+ DEFINE_METHOD_tcp_info_tcpi_snd_cwnd;
192
+ DEFINE_METHOD_tcp_info_tcpi_advmss;
193
+ DEFINE_METHOD_tcp_info_tcpi_reordering;
194
+ DEFINE_METHOD_tcp_info_tcpi_rcv_rtt;
195
+ DEFINE_METHOD_tcp_info_tcpi_rcv_space;
196
+ DEFINE_METHOD_tcp_info_tcpi_total_retrans;
197
+
198
+ #ifdef RAINDROPS_TCP_STATES_ALL_KNOWN
199
+
200
+ /*
201
+ * Document-const: Raindrops::TCP
202
+ *
203
+ * This is a frozen hash storing the numeric values
204
+ * maps platform-independent symbol keys to
205
+ * platform-dependent numeric values. These states
206
+ * are all valid values for the Raindrops::TCP_Info#state field.
207
+ *
208
+ * The platform-independent names of the keys in this hash are:
209
+ *
210
+ * - :ESTABLISHED
211
+ * - :SYN_SENT
212
+ * - :SYN_RECV
213
+ * - :FIN_WAIT1
214
+ * - :FIN_WAIT2
215
+ * - :TIME_WAIT
216
+ * - :CLOSE
217
+ * - :CLOSE_WAIT
218
+ * - :LAST_ACK
219
+ * - :LISTEN
220
+ * - :CLOSING
221
+ *
222
+ * This is only supported on platforms where TCP_Info is supported,
223
+ * currently FreeBSD, OpenBSD, and Linux-based systems.
224
+ */
225
+ {
226
+ #define TCPSET(n,v) rb_hash_aset(tcp, ID2SYM(rb_intern(#n)), INT2NUM(v))
227
+ VALUE tcp = rb_hash_new();
228
+ TCPSET(ESTABLISHED, RAINDROPS_TCP_ESTABLISHED);
229
+ TCPSET(SYN_SENT, RAINDROPS_TCP_SYN_SENT);
230
+ TCPSET(SYN_RECV, RAINDROPS_TCP_SYN_RECV);
231
+ TCPSET(FIN_WAIT1, RAINDROPS_TCP_FIN_WAIT1);
232
+ TCPSET(FIN_WAIT2, RAINDROPS_TCP_FIN_WAIT2);
233
+ TCPSET(TIME_WAIT, RAINDROPS_TCP_TIME_WAIT);
234
+ TCPSET(CLOSE, RAINDROPS_TCP_CLOSE);
235
+ TCPSET(CLOSE_WAIT, RAINDROPS_TCP_CLOSE_WAIT);
236
+ TCPSET(LAST_ACK, RAINDROPS_TCP_LAST_ACK);
237
+ TCPSET(LISTEN, RAINDROPS_TCP_LISTEN);
238
+ TCPSET(CLOSING, RAINDROPS_TCP_CLOSING);
239
+ #undef TCPSET
240
+ OBJ_FREEZE(tcp);
241
+ rb_define_const(cRaindrops, "TCP", tcp);
242
+ }
243
+ #endif
244
+ }
245
+ #endif /* HAVE_STRUCT_TCP_INFO */
@@ -0,0 +1,94 @@
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
+ # - Socket#accept
14
+ # - Socket#accept_nonblock
15
+ # - Kgio::TCPServer#kgio_accept
16
+ # - Kgio::TCPServer#kgio_tryaccept
17
+ module Raindrops::Aggregate::LastDataRecv
18
+ # The integer value of +last_data_recv+ is sent to this object.
19
+ # This is usually a duck type compatible with the \Aggregate class,
20
+ # but can be *anything* that accepts the *<<* method.
21
+ attr_accessor :raindrops_aggregate
22
+
23
+ @@default_aggregate = nil
24
+
25
+ # By default, this is a Raindrops::Aggregate::PMQ object
26
+ # It may be anything that responds to *<<*
27
+ def self.default_aggregate
28
+ @@default_aggregate ||= Raindrops::Aggregate::PMQ.new
29
+ end
30
+
31
+ # Assign any object that responds to *<<*
32
+ def self.default_aggregate=(agg)
33
+ @@default_aggregate = agg
34
+ end
35
+
36
+ # automatically extends any TCPServer objects used by Unicorn
37
+ def self.cornify!
38
+ Unicorn::HttpServer::LISTENERS.each do |s|
39
+ if TCPServer === s || (s.instance_of?(Socket) && s.local_address.ip?)
40
+ s.extend(self)
41
+ end
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
+ obj.setsockopt Socket::SOL_TCP, 9, 60
51
+ end
52
+
53
+ # :stopdoc:
54
+
55
+ def kgio_tryaccept(*args)
56
+ count! super
57
+ end
58
+
59
+ def kgio_accept(*args)
60
+ count! super
61
+ end
62
+
63
+ def accept
64
+ count! super
65
+ end
66
+
67
+ def accept_nonblock(exception: true)
68
+ count! super(exception: exception)
69
+ end
70
+
71
+ # :startdoc:
72
+
73
+ # The +last_data_recv+ member of Raindrops::TCP_Info can be used to
74
+ # infer the time a client spent in the listen queue before it was
75
+ # accepted.
76
+ #
77
+ # We require TCP_DEFER_ACCEPT on the listen socket for
78
+ # +last_data_recv+ to be accurate
79
+ def count!(ret)
80
+ case ret
81
+ when :wait_readable
82
+ when Array # Socket#accept_nonblock
83
+ io = ret[0]
84
+ else # TCPSocket#accept_nonblock
85
+ io = ret
86
+ end
87
+ if io
88
+ x = Raindrops::TCP_Info.new(io)
89
+ @raindrops_aggregate << x.last_data_recv
90
+ end
91
+ ret
92
+ end
93
+ end
94
+
@@ -0,0 +1,245 @@
1
+ # -*- encoding: binary -*-
2
+ require "tempfile"
3
+ require "aggregate"
4
+ require "posix_mq"
5
+ require "fcntl"
6
+ require "thread"
7
+ require "stringio"
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
+ # * posix_mq (tested with 1.0.0)
23
+ #
24
+ # == Design
25
+ #
26
+ # There is one master thread which aggregates statistics. Individual
27
+ # worker processes or threads will write to a shared POSIX message
28
+ # queue (default: "/raindrops") that the master reads from. At a
29
+ # predefined interval, the master thread will write out to a shared,
30
+ # anonymous temporary file that workers may read from
31
+ #
32
+ # Setting +:worker_interval+ and +:master_interval+ to +1+ will result
33
+ # in perfect accuracy but at the cost of a high synchronization
34
+ # overhead. Larger intervals mean less frequent messaging for higher
35
+ # performance but lower accuracy.
36
+ class Raindrops::Aggregate::PMQ
37
+
38
+ # :stopdoc:
39
+ # These constants are for Linux. This is designed for aggregating
40
+ # TCP_INFO.
41
+ RDLOCK = [ Fcntl::F_RDLCK ].pack("s @256".freeze).freeze
42
+ WRLOCK = [ Fcntl::F_WRLCK ].pack("s @256".freeze).freeze
43
+ UNLOCK = [ Fcntl::F_UNLCK ].pack("s @256".freeze).freeze
44
+ # :startdoc:
45
+
46
+ # returns the number of dropped messages sent to a POSIX message
47
+ # queue if non-blocking operation was desired with :lossy
48
+ attr_reader :nr_dropped
49
+
50
+ #
51
+ # Creates a new Raindrops::Aggregate::PMQ object
52
+ #
53
+ # Raindrops::Aggregate::PMQ.new(options = {}) -> aggregate
54
+ #
55
+ # +options+ is a hash that accepts the following keys:
56
+ #
57
+ # * :queue - name of the POSIX message queue (default: "/raindrops")
58
+ # * :worker_interval - interval to send to the master (default: 10)
59
+ # * :master_interval - interval to for the master to write out (default: 5)
60
+ # * :lossy - workers drop packets if master cannot keep up (default: false)
61
+ # * :aggregate - \Aggregate object (default: \Aggregate.new)
62
+ # * :mq_umask - umask for creatingthe POSIX message queue (default: 0666)
63
+ #
64
+ def initialize(params = {})
65
+ opts = {
66
+ :queue => ENV["RAINDROPS_MQUEUE"] || "/raindrops",
67
+ :worker_interval => 10,
68
+ :master_interval => 5,
69
+ :lossy => false,
70
+ :mq_attr => nil,
71
+ :mq_umask => 0666,
72
+ :aggregate => Aggregate.new,
73
+ }.merge! params
74
+ @master_interval = opts[:master_interval]
75
+ @worker_interval = opts[:worker_interval]
76
+ @aggregate = opts[:aggregate]
77
+ @worker_queue = @worker_interval ? [] : nil
78
+ @mutex = Mutex.new
79
+
80
+ @mq_name = opts[:queue]
81
+ mq = POSIX_MQ.new @mq_name, :w, opts[:mq_umask], opts[:mq_attr]
82
+ Tempfile.open("raindrops_pmq") do |t|
83
+ @wr = File.open(t.path, "wb")
84
+ @rd = File.open(t.path, "rb")
85
+ end
86
+ @wr.sync = true
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
+ dst = StringIO.new
155
+ dst.binmode
156
+ IO.copy_stream(rd, dst, rd.size, 0)
157
+ dst.string
158
+ end)
159
+ end
160
+ end
161
+
162
+ # Flushes the currently aggregate statistics to a temporary file.
163
+ # There is no need to call this explicitly as +:worker_interval+ defines
164
+ # how frequently your data will be flushed for workers to read.
165
+ def flush_master
166
+ dump = Marshal.dump @aggregate
167
+ synchronize(@wr, WRLOCK) do |wr|
168
+ wr.truncate 0
169
+ wr.rewind
170
+ wr.write(dump)
171
+ end
172
+ end
173
+
174
+ # stops the currently running master loop, may be called from any
175
+ # worker thread or process
176
+ def stop_master_loop
177
+ sleep 0.1 until mq_send(false)
178
+ rescue Errno::EINTR
179
+ retry
180
+ end
181
+
182
+ def lock! io, type # :nodoc:
183
+ io.fcntl Fcntl::F_SETLKW, type
184
+ rescue Errno::EINTR
185
+ retry
186
+ end
187
+
188
+ # we use both a mutex for thread-safety and fcntl lock for process-safety
189
+ def synchronize io, type # :nodoc:
190
+ @mutex.synchronize do
191
+ begin
192
+ type = type.dup
193
+ lock! io, type
194
+ yield io
195
+ ensure
196
+ lock! io, type.replace(UNLOCK)
197
+ type.clear
198
+ end
199
+ end
200
+ end
201
+
202
+ # flushes the local queue of the worker process, sending all pending
203
+ # data to the master. There is no need to call this explicitly as
204
+ # +:worker_interval+ defines how frequently your queue will be flushed
205
+ def flush
206
+ if q = @local_queue && ! q.empty?
207
+ mq_send q
208
+ q.clear
209
+ end
210
+ nil
211
+ end
212
+
213
+ # proxy for \Aggregate#count
214
+ def count; aggregate.count; end
215
+
216
+ # proxy for \Aggregate#max
217
+ def max; aggregate.max; end
218
+
219
+ # proxy for \Aggregate#min
220
+ def min; aggregate.min; end
221
+
222
+ # proxy for \Aggregate#sum
223
+ def sum; aggregate.sum; end
224
+
225
+ # proxy for \Aggregate#mean
226
+ def mean; aggregate.mean; end
227
+
228
+ # proxy for \Aggregate#std_dev
229
+ def std_dev; aggregate.std_dev; end
230
+
231
+ # proxy for \Aggregate#outliers_low
232
+ def outliers_low; aggregate.outliers_low; end
233
+
234
+ # proxy for \Aggregate#outliers_high
235
+ def outliers_high; aggregate.outliers_high; end
236
+
237
+ # proxy for \Aggregate#to_s
238
+ def to_s(*args); aggregate.to_s(*args); end
239
+
240
+ # proxy for \Aggregate#each
241
+ def each; aggregate.each { |*args| yield(*args) }; end
242
+
243
+ # proxy for \Aggregate#each_nonzero
244
+ def each_nonzero; aggregate.each_nonzero { |*args| yield(*args) }; end
245
+ end
@@ -0,0 +1,8 @@
1
+ # -*- encoding: binary -*-
2
+ #
3
+ # raindrops may use the {aggregate}[https://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,102 @@
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+ defined in
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
+ if defined?(Unicorn::HttpServer::LISTENERS)
77
+ Raindrops::Aggregate::LastDataRecv.cornify!
78
+ end
79
+ @aggregate =
80
+ opts[:aggregate] || Raindrops::Aggregate::LastDataRecv.default_aggregate
81
+ end
82
+
83
+ def call(_)
84
+ a = @aggregate
85
+ count = a.count
86
+ headers = {
87
+ "Content-Type" => "text/plain",
88
+ "X-Count" => count.to_s,
89
+ }
90
+ if count > 1
91
+ headers["X-Min"] = a.min.to_s
92
+ headers["X-Max"] = a.max.to_s
93
+ headers["X-Mean"] = a.mean.round.to_s
94
+ headers["X-Std-Dev"] = a.std_dev.round.to_s
95
+ headers["X-Outliers-Low"] = a.outliers_low.to_s
96
+ headers["X-Outliers-High"] = a.outliers_high.to_s
97
+ end
98
+ body = a.to_s
99
+ headers["Content-Length"] = body.size.to_s
100
+ [ 200, headers, [ body ] ]
101
+ end
102
+ end