raindrops-maintained 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|