raindrops 0.1.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.
@@ -0,0 +1,192 @@
1
+ #include <ruby.h>
2
+ #include <sys/mman.h>
3
+ #include <assert.h>
4
+ #include <errno.h>
5
+ #include <stddef.h>
6
+
7
+ /*
8
+ * most modern CPUs have a cache-line size of 64 or 128.
9
+ * We choose a bigger one by default since our structure is not
10
+ * heavily used
11
+ */
12
+ #ifndef CACHE_LINE_SIZE
13
+ # define CACHE_LINE_SIZE 128
14
+ #endif
15
+
16
+ /* each raindrop is a counter */
17
+ struct raindrop {
18
+ union {
19
+ unsigned long counter;
20
+ unsigned char padding[CACHE_LINE_SIZE];
21
+ } as;
22
+ } __attribute__((packed));
23
+
24
+ /* allow mmap-ed regions can store more than one raindrop */
25
+ struct raindrops {
26
+ long size;
27
+ struct raindrop *drops;
28
+ };
29
+
30
+ /* called by GC */
31
+ static void evaporate(void *ptr)
32
+ {
33
+ struct raindrops *r = ptr;
34
+
35
+ if (r->drops) {
36
+ int rv = munmap(r->drops, sizeof(struct raindrop) * r->size);
37
+ if (rv != 0)
38
+ rb_bug("munmap failed in gc: %s", strerror(errno));
39
+ }
40
+
41
+ xfree(ptr);
42
+ }
43
+
44
+ /* automatically called at creation (before initialize) */
45
+ static VALUE alloc(VALUE klass)
46
+ {
47
+ struct raindrops *r;
48
+
49
+ return Data_Make_Struct(klass, struct raindrops, NULL, evaporate, r);
50
+ }
51
+
52
+ static struct raindrops *get(VALUE self)
53
+ {
54
+ struct raindrops *r;
55
+
56
+ Data_Get_Struct(self, struct raindrops, r);
57
+
58
+ return r;
59
+ }
60
+
61
+ /* initializes a Raindrops object to hold +size+ elements */
62
+ static VALUE init(VALUE self, VALUE size)
63
+ {
64
+ struct raindrops *r = get(self);
65
+ int tries = 1;
66
+
67
+ if (r->drops)
68
+ rb_raise(rb_eRuntimeError, "already initialized");
69
+
70
+ r->size = NUM2LONG(size);
71
+ if (r->size < 1)
72
+ rb_raise(rb_eArgError, "size must be >= 1");
73
+
74
+ retry:
75
+ r->drops = mmap(NULL, sizeof(struct raindrop) * r->size,
76
+ PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
77
+ if (r->drops == MAP_FAILED) {
78
+ if ((errno == EAGAIN || errno == ENOMEM) && tries-- > 0) {
79
+ rb_gc();
80
+ goto retry;
81
+ }
82
+ rb_sys_fail("mmap");
83
+ }
84
+
85
+ return self;
86
+ }
87
+
88
+ /* :nodoc */
89
+ static VALUE init_copy(VALUE dest, VALUE source)
90
+ {
91
+ struct raindrops *dst = get(dest);
92
+ struct raindrops *src = get(source);
93
+
94
+ init(dest, LONG2NUM(src->size));
95
+ memcpy(dst->drops, src->drops, sizeof(struct raindrop) * src->size);
96
+
97
+ return dest;
98
+ }
99
+
100
+ static unsigned long *addr_of(VALUE self, VALUE index)
101
+ {
102
+ struct raindrops *r = get(self);
103
+ unsigned long off = FIX2ULONG(index) * sizeof(struct raindrop);
104
+
105
+ if (off >= sizeof(struct raindrop) * r->size)
106
+ rb_raise(rb_eArgError, "offset overrun");
107
+
108
+ return (unsigned long *)((unsigned long)r->drops + off);
109
+ }
110
+
111
+ static unsigned long incr_decr_arg(int argc, const VALUE *argv)
112
+ {
113
+ if (argc > 2 || argc < 1)
114
+ rb_raise(rb_eArgError,
115
+ "wrong number of arguments (%d for 1+)", argc);
116
+
117
+ return argc == 2 ? NUM2ULONG(argv[1]) : 1;
118
+ }
119
+
120
+ /* increments the value referred to by the +index+ constant by 1 */
121
+ static VALUE incr(int argc, VALUE *argv, VALUE self)
122
+ {
123
+ unsigned long nr = incr_decr_arg(argc, argv);
124
+
125
+ return ULONG2NUM(__sync_add_and_fetch(addr_of(self, argv[0]), nr));
126
+ }
127
+
128
+ /* decrements the value referred to by the +index+ constant by 1 */
129
+ static VALUE decr(int argc, VALUE *argv, VALUE self)
130
+ {
131
+ unsigned long nr = incr_decr_arg(argc, argv);
132
+
133
+ return ULONG2NUM(__sync_sub_and_fetch(addr_of(self, argv[0]), nr));
134
+ }
135
+
136
+ /* converts the raindrops structure to an Array */
137
+ static VALUE to_ary(VALUE self)
138
+ {
139
+ struct raindrops *r = get(self);
140
+ VALUE rv = rb_ary_new2(r->size);
141
+ long i;
142
+ unsigned long base = (unsigned long)r->drops;
143
+
144
+ for (i = 0; i < r->size; i++) {
145
+ rb_ary_push(rv, ULONG2NUM(*((unsigned long *)base)));
146
+ base += sizeof(struct raindrop);
147
+ }
148
+
149
+ return rv;
150
+ }
151
+
152
+ static VALUE size(VALUE self)
153
+ {
154
+ return LONG2NUM(get(self)->size);
155
+ }
156
+
157
+ static VALUE aset(VALUE self, VALUE index, VALUE value)
158
+ {
159
+ unsigned long *addr = addr_of(self, index);
160
+
161
+ *addr = NUM2ULONG(value);
162
+
163
+ return value;
164
+ }
165
+
166
+ static VALUE aref(VALUE self, VALUE index)
167
+ {
168
+ return ULONG2NUM(*addr_of(self, index));
169
+ }
170
+
171
+ #ifdef __linux__
172
+ void Init_raindrops_linux_inet_diag(void);
173
+ #endif
174
+
175
+ void Init_raindrops_ext(void)
176
+ {
177
+ VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
178
+ rb_define_alloc_func(cRaindrops, alloc);
179
+
180
+ rb_define_method(cRaindrops, "initialize", init, 1);
181
+ rb_define_method(cRaindrops, "incr", incr, -1);
182
+ rb_define_method(cRaindrops, "decr", decr, -1);
183
+ rb_define_method(cRaindrops, "to_ary", to_ary, 0);
184
+ rb_define_method(cRaindrops, "[]", aref, 1);
185
+ rb_define_method(cRaindrops, "[]=", aset, 2);
186
+ rb_define_method(cRaindrops, "size", size, 0);
187
+ rb_define_method(cRaindrops, "initialize_copy", init_copy, 1);
188
+
189
+ #ifdef __linux__
190
+ Init_raindrops_linux_inet_diag();
191
+ #endif
192
+ }
data/lib/raindrops.rb ADDED
@@ -0,0 +1,35 @@
1
+ # -*- encoding: binary -*-
2
+ class Raindrops
3
+
4
+ # Raindrops is currently at version 0.1.0
5
+ VERSION = '0.1.0'
6
+
7
+ # Used to represent the number of +active+ and +queued+ sockets for
8
+ # a single listen socket across all threads and processes on a
9
+ # machine.
10
+ #
11
+ # For TCP listeners, only sockets in the TCP_ESTABLISHED state are
12
+ # accounted for. For Unix domain listeners, only CONNECTING and
13
+ # CONNECTED Unix domain sockets are accounted for.
14
+ #
15
+ # +active+ connections is the number of accept()-ed but not-yet-closed
16
+ # sockets in all threads/processes sharing the given listener.
17
+ #
18
+ # +queued+ connections is the number of un-accept()-ed sockets in the
19
+ # queue of a given listen socket.
20
+ #
21
+ # These stats are currently only available under Linux
22
+ class ListenStats < Struct.new(:active, :queued)
23
+
24
+ # the sum of +active+ and +queued+ sockets
25
+ def total
26
+ active + queued
27
+ end
28
+ end
29
+
30
+ # TODO: pure Ruby version for single processes
31
+ require 'raindrops_ext'
32
+
33
+ autoload :Struct, 'raindrops/struct'
34
+ autoload :Middleware, 'raindrops/middleware'
35
+ end
@@ -0,0 +1,55 @@
1
+ # -*- encoding: binary -*-
2
+ class Raindrops
3
+ module Linux
4
+
5
+ # The standard proc path for active UNIX domain sockets, feel free to call
6
+ # String#replace on this if your /proc is mounted in a non-standard location
7
+ # for whatever reason
8
+ PROC_NET_UNIX = "/proc/net/unix"
9
+
10
+ # Get ListenStats from an array of +paths+
11
+ #
12
+ # Socket state mapping from integer => symbol, based on socket_state
13
+ # enum from include/linux/net.h in the Linux kernel:
14
+ # typedef enum {
15
+ # SS_FREE = 0, /* not allocated */
16
+ # SS_UNCONNECTED, /* unconnected to any socket */
17
+ # SS_CONNECTING, /* in process of connecting */
18
+ # SS_CONNECTED, /* connected to socket */
19
+ # SS_DISCONNECTING /* in process of disconnecting */
20
+ # } socket_state;
21
+ # * SS_CONNECTING maps to ListenStats#active
22
+ # * SS_CONNECTED maps to ListenStats#queued
23
+ #
24
+ # This method may be significantly slower than its tcp_listener_stats
25
+ # counterpart due to the latter being able to use inet_diag via netlink.
26
+ # This parses /proc/net/unix as there is no other (known) way
27
+ # to expose Unix domain socket statistics over netlink.
28
+ def unix_listener_stats(paths)
29
+ rv = Hash.new { |h,k| h[k.freeze] = ListenStats.new(0, 0) }
30
+ paths = paths.map do |path|
31
+ path = path.dup
32
+ path.force_encoding(Encoding::BINARY) if defined?(Encoding)
33
+ rv[path]
34
+ Regexp.escape(path)
35
+ end
36
+ paths = / 00000000 \d+ (\d+)\s+\d+ (#{paths.join('|')})$/n
37
+
38
+ # no point in pread since we can't stat for size on this file
39
+ File.open(PROC_NET_UNIX, "rb") do |fp|
40
+ fp.read.scan(paths).each do |s|
41
+ path = s.last
42
+ case s.first.to_i
43
+ when 2 then rv[path].queued += 1
44
+ when 3 then rv[path].active += 1
45
+ end
46
+ end
47
+ end
48
+
49
+ rv
50
+ end
51
+
52
+ module_function :unix_listener_stats
53
+
54
+ end # Linux
55
+ end # Raindrops
@@ -0,0 +1,75 @@
1
+ # -*- encoding: binary -*-
2
+ require 'raindrops'
3
+
4
+ # Raindrops middleware should be loaded at the top of Rack
5
+ # middleware stack before other middlewares for maximum accuracy.
6
+ class Raindrops
7
+ class Middleware < ::Struct.new(:app, :stats, :path, :tcp, :unix)
8
+
9
+ # :stopdoc:
10
+ Stats = Raindrops::Struct.new(:calling, :writing)
11
+ PATH_INFO = "PATH_INFO"
12
+ # :startdoc:
13
+
14
+ def initialize(app, opts = {})
15
+ super(app, opts[:stats] || Stats.new, opts[:path] || "/_raindrops")
16
+ if tmp = opts[:listeners]
17
+ self.tcp = tmp.grep(/\A[^:]+:\d+\z/)
18
+ self.unix = tmp.grep(%r{\A/})
19
+ self.tcp = nil if tcp.empty?
20
+ self.unix = nil if unix.empty?
21
+ end
22
+ end
23
+
24
+ # standard Rack endpoing
25
+ def call(env)
26
+ env[PATH_INFO] == path ? stats_response : dup._call(env)
27
+ end
28
+
29
+ def _call(env)
30
+ stats.incr_calling
31
+ status, headers, self.app = app.call(env)
32
+
33
+ # the Rack server will start writing headers soon after this method
34
+ stats.incr_writing
35
+ [ status, headers, self ]
36
+ ensure
37
+ stats.decr_calling
38
+ end
39
+
40
+ # yield to the Rack server here for writing
41
+ def each(&block)
42
+ app.each(&block)
43
+ end
44
+
45
+ # the Rack server should call this after #each (usually ensure-d)
46
+ def close
47
+ stats.decr_writing
48
+ ensure
49
+ app.close if app.respond_to?(:close)
50
+ end
51
+
52
+ def stats_response
53
+ body = "calling: #{stats.calling}\n" \
54
+ "writing: #{stats.writing}\n"
55
+
56
+ if defined?(Linux)
57
+ Linux.tcp_listener_stats(tcp).each do |addr,stats|
58
+ body << "#{addr} active: #{stats.active}\n" \
59
+ "#{addr} queued: #{stats.queued}\n"
60
+ end if tcp
61
+ Linux.unix_listener_stats(unix).each do |addr,stats|
62
+ body << "#{addr} active: #{stats.active}\n" \
63
+ "#{addr} queued: #{stats.queued}\n"
64
+ end if unix
65
+ end
66
+
67
+ headers = {
68
+ "Content-Type" => "text/plain",
69
+ "Content-Length" => body.size.to_s,
70
+ }
71
+ [ 200, headers, [ body ] ]
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ class Raindrops::Struct
4
+
5
+ def self.new(*members)
6
+ members = members.map { |x| x.to_sym }.freeze
7
+ str = <<EOS
8
+ def initialize(*values)
9
+ (MEMBERS.size >= values.size) or raise ArgumentError, "too many arguments"
10
+ @raindrops = Raindrops.new(MEMBERS.size)
11
+ values.each_with_index { |val,i| @raindrops[i] = values[i] }
12
+ end
13
+
14
+ def initialize_copy(src)
15
+ @raindrops = src.instance_variable_get(:@raindrops).dup
16
+ end
17
+
18
+ def []=(index, value)
19
+ @raindrops[index] = value
20
+ end
21
+
22
+ def [](index)
23
+ @raindrops[index]
24
+ end
25
+
26
+ def to_hash
27
+ ary = @raindrops.to_ary
28
+ rv = {}
29
+ MEMBERS.each_with_index { |member, i| rv[member] = ary[i] }
30
+ rv
31
+ end
32
+ EOS
33
+
34
+ members.each_with_index do |member, i|
35
+ str << "def incr_#{member}; @raindrops.incr(#{i}); end; " \
36
+ "def decr_#{member}; @raindrops.decr(#{i}); end; " \
37
+ "def #{member}; @raindrops[#{i}]; end; " \
38
+ "def #{member}=(val); @raindrops[#{i}] = val; end; "
39
+ end
40
+
41
+ klass = Class.new
42
+ klass.const_set(:MEMBERS, members)
43
+ klass.class_eval(str)
44
+ klass
45
+ end
46
+
47
+ end
data/raindrops.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ ENV["VERSION"] or abort "VERSION= must be specified"
4
+ manifest = File.readlines('.manifest').map! { |x| x.chomp! }
5
+ test_files = manifest.grep(%r{\Atest/test_.*\.rb\z})
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = %q{raindrops}
9
+ s.version = ENV["VERSION"]
10
+
11
+ s.authors = ["raindrops hackers"]
12
+ s.date = Time.now.utc.strftime('%Y-%m-%d')
13
+ s.description = File.read("README").split(/\n\n/)[1]
14
+ s.email = %q{raindrops@librelist.com}
15
+ s.extensions = %w(ext/raindrops/extconf.rb)
16
+
17
+ s.extra_rdoc_files = File.readlines('.document').map! do |x|
18
+ x.chomp!
19
+ if File.directory?(x)
20
+ manifest.grep(%r{\A#{x}/})
21
+ elsif File.file?(x)
22
+ x
23
+ else
24
+ nil
25
+ end
26
+ end.flatten.compact
27
+
28
+ s.files = manifest
29
+ s.homepage = %q{http://raindrops.bogomips.org/}
30
+ s.summary = %q{real-time stats for Rack servers}
31
+ s.rdoc_options = [ "-Na", "-t", "raindrops - #{s.summary}" ]
32
+ s.require_paths = %w(lib)
33
+ s.rubyforge_project = %q{raindrops}
34
+
35
+ s.test_files = test_files
36
+
37
+ # s.licenses = %w(LGPLv3) # accessor not compatible with older RubyGems
38
+ end