raindrops-maintained 0.21.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.
- checksums.yaml +7 -0
- data/.document +7 -0
- data/.gitattributes +4 -0
- data/.gitignore +16 -0
- data/.manifest +62 -0
- data/.olddoc.yml +16 -0
- data/COPYING +165 -0
- data/GIT-VERSION-FILE +1 -0
- data/GIT-VERSION-GEN +40 -0
- data/GNUmakefile +4 -0
- data/LATEST +9 -0
- data/LICENSE +16 -0
- data/NEWS +384 -0
- data/README +101 -0
- data/TODO +3 -0
- data/archive/.gitignore +3 -0
- data/archive/slrnpull.conf +4 -0
- data/examples/linux-listener-stats.rb +122 -0
- data/examples/middleware.ru +5 -0
- data/examples/watcher.ru +4 -0
- data/examples/watcher_demo.ru +13 -0
- data/examples/yahns.conf.rb +30 -0
- data/examples/zbatery.conf.rb +16 -0
- data/ext/raindrops/extconf.rb +163 -0
- data/ext/raindrops/linux_inet_diag.c +713 -0
- data/ext/raindrops/my_fileno.h +16 -0
- data/ext/raindrops/raindrops.c +487 -0
- data/ext/raindrops/raindrops_atomic.h +23 -0
- data/ext/raindrops/tcp_info.c +245 -0
- data/lib/raindrops/aggregate/last_data_recv.rb +94 -0
- data/lib/raindrops/aggregate/pmq.rb +245 -0
- data/lib/raindrops/aggregate.rb +8 -0
- data/lib/raindrops/last_data_recv.rb +102 -0
- data/lib/raindrops/linux.rb +77 -0
- data/lib/raindrops/middleware/proxy.rb +40 -0
- data/lib/raindrops/middleware.rb +153 -0
- data/lib/raindrops/struct.rb +62 -0
- data/lib/raindrops/watcher.rb +428 -0
- data/lib/raindrops.rb +72 -0
- data/pkg.mk +151 -0
- data/raindrops-maintained.gemspec +1 -0
- data/raindrops.gemspec +26 -0
- data/setup.rb +1586 -0
- data/test/ipv6_enabled.rb +9 -0
- data/test/rack_unicorn.rb +11 -0
- data/test/test_aggregate_pmq.rb +65 -0
- data/test/test_inet_diag_socket.rb +16 -0
- data/test/test_last_data_recv.rb +57 -0
- data/test/test_last_data_recv_unicorn.rb +69 -0
- data/test/test_linux.rb +281 -0
- data/test/test_linux_all_tcp_listen_stats.rb +66 -0
- data/test/test_linux_all_tcp_listen_stats_leak.rb +43 -0
- data/test/test_linux_ipv6.rb +166 -0
- data/test/test_linux_middleware.rb +64 -0
- data/test/test_linux_reuseport_tcp_listen_stats.rb +51 -0
- data/test/test_middleware.rb +128 -0
- data/test/test_middleware_unicorn.rb +37 -0
- data/test/test_middleware_unicorn_ipv6.rb +37 -0
- data/test/test_raindrops.rb +207 -0
- data/test/test_raindrops_gc.rb +38 -0
- data/test/test_struct.rb +54 -0
- data/test/test_tcp_info.rb +88 -0
- data/test/test_watcher.rb +186 -0
- 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
|