precompiled-raindrops 0.18.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.document +7 -0
  3. data/.gitattributes +4 -0
  4. data/.github/workflows/cibuildgem.yaml +90 -0
  5. data/.gitignore +16 -0
  6. data/.olddoc.yml +16 -0
  7. data/COPYING +165 -0
  8. data/GIT-VERSION-GEN +40 -0
  9. data/GNUmakefile +4 -0
  10. data/Gemfile +1 -0
  11. data/LICENSE +16 -0
  12. data/README +111 -0
  13. data/Rakefile +0 -0
  14. data/TODO +3 -0
  15. data/archive/.gitignore +3 -0
  16. data/archive/slrnpull.conf +4 -0
  17. data/examples/linux-listener-stats.rb +123 -0
  18. data/examples/middleware.ru +6 -0
  19. data/examples/watcher.ru +5 -0
  20. data/examples/watcher_demo.ru +14 -0
  21. data/examples/yahns.conf.rb +31 -0
  22. data/examples/zbatery.conf.rb +17 -0
  23. data/ext/raindrops/extconf.rb +164 -0
  24. data/ext/raindrops/khashl.h +444 -0
  25. data/ext/raindrops/linux_inet_diag.c +719 -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 +95 -0
  31. data/lib/raindrops/aggregate/pmq.rb +246 -0
  32. data/lib/raindrops/aggregate.rb +9 -0
  33. data/lib/raindrops/last_data_recv.rb +103 -0
  34. data/lib/raindrops/linux.rb +78 -0
  35. data/lib/raindrops/middleware/proxy.rb +41 -0
  36. data/lib/raindrops/middleware.rb +154 -0
  37. data/lib/raindrops/struct.rb +63 -0
  38. data/lib/raindrops/watcher.rb +429 -0
  39. data/lib/raindrops.rb +79 -0
  40. data/pkg.mk +151 -0
  41. data/raindrops.gemspec +26 -0
  42. data/setup.rb +1587 -0
  43. data/test/ipv6_enabled.rb +10 -0
  44. data/test/rack_unicorn.rb +12 -0
  45. data/test/test_aggregate_pmq.rb +66 -0
  46. data/test/test_inet_diag_socket.rb +17 -0
  47. data/test/test_last_data_recv.rb +58 -0
  48. data/test/test_last_data_recv_unicorn.rb +70 -0
  49. data/test/test_linux.rb +282 -0
  50. data/test/test_linux_all_tcp_listen_stats.rb +67 -0
  51. data/test/test_linux_all_tcp_listen_stats_leak.rb +44 -0
  52. data/test/test_linux_ipv6.rb +167 -0
  53. data/test/test_linux_middleware.rb +65 -0
  54. data/test/test_linux_reuseport_tcp_listen_stats.rb +52 -0
  55. data/test/test_middleware.rb +129 -0
  56. data/test/test_middleware_unicorn.rb +38 -0
  57. data/test/test_middleware_unicorn_ipv6.rb +38 -0
  58. data/test/test_raindrops.rb +208 -0
  59. data/test/test_raindrops_gc.rb +39 -0
  60. data/test/test_struct.rb +55 -0
  61. data/test/test_tcp_info.rb +89 -0
  62. data/test/test_watcher.rb +187 -0
  63. metadata +194 -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,95 @@
1
+ # -*- encoding: binary -*-
2
+ # frozen_string_literal: false
3
+ require "socket"
4
+ #
5
+ #
6
+ # This module is used to extend TCPServer and Kgio::TCPServer objects
7
+ # and aggregate +last_data_recv+ times for all accepted clients. It
8
+ # is designed to be used with Raindrops::LastDataRecv Rack application
9
+ # but can be easily changed to work with other stats collection devices.
10
+ #
11
+ # Methods wrapped include:
12
+ # - TCPServer#accept
13
+ # - TCPServer#accept_nonblock
14
+ # - Socket#accept
15
+ # - Socket#accept_nonblock
16
+ # - Kgio::TCPServer#kgio_accept
17
+ # - Kgio::TCPServer#kgio_tryaccept
18
+ module Raindrops::Aggregate::LastDataRecv
19
+ # The integer value of +last_data_recv+ is sent to this object.
20
+ # This is usually a duck type compatible with the \Aggregate class,
21
+ # but can be *anything* that accepts the *<<* method.
22
+ attr_accessor :raindrops_aggregate
23
+
24
+ @@default_aggregate = nil
25
+
26
+ # By default, this is a Raindrops::Aggregate::PMQ object
27
+ # It may be anything that responds to *<<*
28
+ def self.default_aggregate
29
+ @@default_aggregate ||= Raindrops::Aggregate::PMQ.new
30
+ end
31
+
32
+ # Assign any object that responds to *<<*
33
+ def self.default_aggregate=(agg)
34
+ @@default_aggregate = agg
35
+ end
36
+
37
+ # automatically extends any TCPServer objects used by Unicorn
38
+ def self.cornify!
39
+ Unicorn::HttpServer::LISTENERS.each do |s|
40
+ if TCPServer === s || (s.instance_of?(Socket) && s.local_address.ip?)
41
+ s.extend(self)
42
+ end
43
+ end
44
+ end
45
+
46
+ # each extended object needs to have TCP_DEFER_ACCEPT enabled
47
+ # for accuracy.
48
+ def self.extended(obj)
49
+ obj.raindrops_aggregate = default_aggregate
50
+ # obj.setsockopt Socket::SOL_TCP, tcp_defer_accept = 9, seconds = 60
51
+ obj.setsockopt Socket::SOL_TCP, 9, 60
52
+ end
53
+
54
+ # :stopdoc:
55
+
56
+ def kgio_tryaccept(*args)
57
+ count! super
58
+ end
59
+
60
+ def kgio_accept(*args)
61
+ count! super
62
+ end
63
+
64
+ def accept
65
+ count! super
66
+ end
67
+
68
+ def accept_nonblock(exception: true)
69
+ count! super(exception: exception)
70
+ end
71
+
72
+ # :startdoc:
73
+
74
+ # The +last_data_recv+ member of Raindrops::TCP_Info can be used to
75
+ # infer the time a client spent in the listen queue before it was
76
+ # accepted.
77
+ #
78
+ # We require TCP_DEFER_ACCEPT on the listen socket for
79
+ # +last_data_recv+ to be accurate
80
+ def count!(ret)
81
+ case ret
82
+ when :wait_readable
83
+ when Array # Socket#accept_nonblock
84
+ io = ret[0]
85
+ else # TCPSocket#accept_nonblock
86
+ io = ret
87
+ end
88
+ if io
89
+ x = Raindrops::TCP_Info.new(io)
90
+ @raindrops_aggregate << x.last_data_recv
91
+ end
92
+ ret
93
+ end
94
+ end
95
+
@@ -0,0 +1,246 @@
1
+ # -*- encoding: binary -*-
2
+ # frozen_string_literal: false
3
+ require "tempfile"
4
+ require "aggregate"
5
+ require "posix_mq"
6
+ require "fcntl"
7
+ require "thread"
8
+ require "stringio"
9
+
10
+ # \Aggregate + POSIX message queues support for Ruby 1.9+ and \Linux
11
+ #
12
+ # This class is duck-type compatible with \Aggregate and allows us to
13
+ # aggregate and share statistics from multiple processes/threads aided
14
+ # POSIX message queues. This is designed to be used with the
15
+ # Raindrops::LastDataRecv Rack application, but can be used independently
16
+ # on compatible Runtimes.
17
+ #
18
+ # Unlike the core of raindrops, this is only supported on Ruby 1.9+ and
19
+ # Linux 2.6+. Using this class requires the following additional RubyGems
20
+ # or libraries:
21
+ #
22
+ # * aggregate (tested with 0.2.2)
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".freeze).freeze
43
+ WRLOCK = [ Fcntl::F_WRLCK ].pack("s @256".freeze).freeze
44
+ UNLOCK = [ Fcntl::F_UNLCK ].pack("s @256".freeze).freeze
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
+ @wr.sync = true
88
+ @cached_aggregate = @aggregate
89
+ flush_master
90
+ @mq_send = if opts[:lossy]
91
+ @nr_dropped = 0
92
+ mq.nonblock = true
93
+ mq.method :trysend
94
+ else
95
+ mq.method :send
96
+ end
97
+ end
98
+
99
+ # adds a sample to the underlying \Aggregate object
100
+ def << val
101
+ if q = @worker_queue
102
+ q << val
103
+ if q.size >= @worker_interval
104
+ mq_send(q) or @nr_dropped += 1
105
+ q.clear
106
+ end
107
+ else
108
+ mq_send(val) or @nr_dropped += 1
109
+ end
110
+ end
111
+
112
+ def mq_send(val) # :nodoc:
113
+ @cached_aggregate = nil
114
+ @mq_send.call Marshal.dump(val)
115
+ end
116
+
117
+ #
118
+ # Starts running a master loop, usually in a dedicated thread or process:
119
+ #
120
+ # Thread.new { agg.master_loop }
121
+ #
122
+ # Any worker can call +agg.stop_master_loop+ to stop the master loop
123
+ # (possibly causing the thread or process to exit)
124
+ def master_loop
125
+ buf = ""
126
+ a = @aggregate
127
+ nr = 0
128
+ mq = POSIX_MQ.new @mq_name, :r # this one is always blocking
129
+ begin
130
+ if (nr -= 1) < 0
131
+ nr = @master_interval
132
+ flush_master
133
+ end
134
+ mq.shift(buf)
135
+ data = begin
136
+ Marshal.load(buf) or return
137
+ rescue ArgumentError, TypeError
138
+ next
139
+ end
140
+ Array === data ? data.each { |x| a << x } : a << data
141
+ rescue Errno::EINTR
142
+ rescue => e
143
+ warn "Unhandled exception in #{__FILE__}:#{__LINE__}: #{e}"
144
+ break
145
+ end while true
146
+ ensure
147
+ flush_master
148
+ end
149
+
150
+ # Loads the last shared \Aggregate from the master thread/process
151
+ def aggregate
152
+ @cached_aggregate ||= begin
153
+ flush
154
+ Marshal.load(synchronize(@rd, RDLOCK) do |rd|
155
+ dst = StringIO.new
156
+ dst.binmode
157
+ IO.copy_stream(rd, dst, rd.size, 0)
158
+ dst.string
159
+ end)
160
+ end
161
+ end
162
+
163
+ # Flushes the currently aggregate statistics to a temporary file.
164
+ # There is no need to call this explicitly as +:worker_interval+ defines
165
+ # how frequently your data will be flushed for workers to read.
166
+ def flush_master
167
+ dump = Marshal.dump @aggregate
168
+ synchronize(@wr, WRLOCK) do |wr|
169
+ wr.truncate 0
170
+ wr.rewind
171
+ wr.write(dump)
172
+ end
173
+ end
174
+
175
+ # stops the currently running master loop, may be called from any
176
+ # worker thread or process
177
+ def stop_master_loop
178
+ sleep 0.1 until mq_send(false)
179
+ rescue Errno::EINTR
180
+ retry
181
+ end
182
+
183
+ def lock! io, type # :nodoc:
184
+ io.fcntl Fcntl::F_SETLKW, type
185
+ rescue Errno::EINTR
186
+ retry
187
+ end
188
+
189
+ # we use both a mutex for thread-safety and fcntl lock for process-safety
190
+ def synchronize io, type # :nodoc:
191
+ @mutex.synchronize do
192
+ begin
193
+ type = type.dup
194
+ lock! io, type
195
+ yield io
196
+ ensure
197
+ lock! io, type.replace(UNLOCK)
198
+ type.clear
199
+ end
200
+ end
201
+ end
202
+
203
+ # flushes the local queue of the worker process, sending all pending
204
+ # data to the master. There is no need to call this explicitly as
205
+ # +:worker_interval+ defines how frequently your queue will be flushed
206
+ def flush
207
+ if q = @local_queue && ! q.empty?
208
+ mq_send q
209
+ q.clear
210
+ end
211
+ nil
212
+ end
213
+
214
+ # proxy for \Aggregate#count
215
+ def count; aggregate.count; end
216
+
217
+ # proxy for \Aggregate#max
218
+ def max; aggregate.max; end
219
+
220
+ # proxy for \Aggregate#min
221
+ def min; aggregate.min; end
222
+
223
+ # proxy for \Aggregate#sum
224
+ def sum; aggregate.sum; end
225
+
226
+ # proxy for \Aggregate#mean
227
+ def mean; aggregate.mean; end
228
+
229
+ # proxy for \Aggregate#std_dev
230
+ def std_dev; aggregate.std_dev; end
231
+
232
+ # proxy for \Aggregate#outliers_low
233
+ def outliers_low; aggregate.outliers_low; end
234
+
235
+ # proxy for \Aggregate#outliers_high
236
+ def outliers_high; aggregate.outliers_high; end
237
+
238
+ # proxy for \Aggregate#to_s
239
+ def to_s(*args); aggregate.to_s(*args); end
240
+
241
+ # proxy for \Aggregate#each
242
+ def each; aggregate.each { |*args| yield(*args) }; end
243
+
244
+ # proxy for \Aggregate#each_nonzero
245
+ def each_nonzero; aggregate.each_nonzero { |*args| yield(*args) }; end
246
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding: binary -*-
2
+ # frozen_string_literal: false
3
+ #
4
+ # raindrops may use the {aggregate}[https://github.com/josephruscio/aggregate]
5
+ # RubyGem to aggregate statistics from TCP_Info lookups.
6
+ module Raindrops::Aggregate
7
+ autoload :PMQ, "raindrops/aggregate/pmq"
8
+ autoload :LastDataRecv, "raindrops/aggregate/last_data_recv"
9
+ end
@@ -0,0 +1,103 @@
1
+ # -*- encoding: binary -*-
2
+ # frozen_string_literal: false
3
+ require "raindrops"
4
+
5
+ # This is highly experimental!
6
+ #
7
+ # A self-contained Rack application for aggregating in the
8
+ # +tcpi_last_data_recv+ field in +struct+ +tcp_info+ defined in
9
+ # +/usr/include/linux/tcp.h+. This is only useful for \Linux 2.6 and later.
10
+ # This primarily supports Unicorn and derived servers, but may also be
11
+ # used with any Ruby web server using the core TCPServer class in Ruby.
12
+ #
13
+ # Hitting the Rack endpoint configured for this application will return
14
+ # a an ASCII histogram response body with the following headers:
15
+ #
16
+ # - X-Count - number of requests received
17
+ #
18
+ # The following headers are only present if X-Count is greater than one.
19
+ #
20
+ # - X-Min - lowest last_data_recv time recorded (in milliseconds)
21
+ # - X-Max - highest last_data_recv time recorded (in milliseconds)
22
+ # - X-Mean - mean last_data_recv time recorded (rounded, in milliseconds)
23
+ # - X-Std-Dev - standard deviation of last_data_recv times
24
+ # - X-Outliers-Low - number of low outliers (hopefully many!)
25
+ # - X-Outliers-High - number of high outliers (hopefully zero!)
26
+ #
27
+ # == To use with Unicorn and derived servers (preload_app=false):
28
+ #
29
+ # Put the following in our Unicorn config file (not config.ru):
30
+ #
31
+ # require "raindrops/last_data_recv"
32
+ #
33
+ # Then follow the instructions below for config.ru:
34
+ #
35
+ # == To use with any Rack server using TCPServer
36
+ #
37
+ # Setup a route for Raindrops::LastDataRecv in your Rackup config file
38
+ # (typically config.ru):
39
+ #
40
+ # require "raindrops"
41
+ # map "/raindrops/last_data_recv" do
42
+ # run Raindrops::LastDataRecv.new
43
+ # end
44
+ # map "/" do
45
+ # use SomeMiddleware
46
+ # use MoreMiddleware
47
+ # # ...
48
+ # run YourAppHere.new
49
+ # end
50
+ #
51
+ # == To use with any other Ruby web server that uses TCPServer
52
+ #
53
+ # Put the following in any piece of Ruby code loaded after the server has
54
+ # bound its TCP listeners:
55
+ #
56
+ # ObjectSpace.each_object(TCPServer) do |s|
57
+ # s.extend Raindrops::Aggregate::LastDataRecv
58
+ # end
59
+ #
60
+ # Thread.new do
61
+ # Raindrops::Aggregate::LastDataRecv.default_aggregate.master_loop
62
+ # end
63
+ #
64
+ # Then follow the above instructions for config.ru
65
+ #
66
+ class Raindrops::LastDataRecv
67
+ # :stopdoc:
68
+
69
+ # trigger autoloads
70
+ if defined?(Unicorn)
71
+ agg = Raindrops::Aggregate::LastDataRecv.default_aggregate
72
+ AGGREGATE_THREAD = Thread.new { agg.master_loop }
73
+ end
74
+ # :startdoc
75
+
76
+ def initialize(opts = {})
77
+ if defined?(Unicorn::HttpServer::LISTENERS)
78
+ Raindrops::Aggregate::LastDataRecv.cornify!
79
+ end
80
+ @aggregate =
81
+ opts[:aggregate] || Raindrops::Aggregate::LastDataRecv.default_aggregate
82
+ end
83
+
84
+ def call(_)
85
+ a = @aggregate
86
+ count = a.count
87
+ headers = {
88
+ "Content-Type" => "text/plain",
89
+ "X-Count" => count.to_s,
90
+ }
91
+ if count > 1
92
+ headers["X-Min"] = a.min.to_s
93
+ headers["X-Max"] = a.max.to_s
94
+ headers["X-Mean"] = a.mean.round.to_s
95
+ headers["X-Std-Dev"] = a.std_dev.round.to_s
96
+ headers["X-Outliers-Low"] = a.outliers_low.to_s
97
+ headers["X-Outliers-High"] = a.outliers_high.to_s
98
+ end
99
+ body = a.to_s
100
+ headers["Content-Length"] = body.size.to_s
101
+ [ 200, headers, [ body ] ]
102
+ end
103
+ end