nchan_tools 0.1.4 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe6f6c40e0e15751d878e26903dd3c9812919069b8f338e0463529e836c69ae2
4
- data.tar.gz: 41b7845122c2ef1792f03f8bdb9533276c2fb5b7507d53ac670b05f3eebbf2cd
3
+ metadata.gz: ec4e4cc624f57a6d6a6684bf71db8d75b58a4e2e823e258490b9be66eb5de396
4
+ data.tar.gz: bea9e679190cff5749804e8f8e3ef14ca825edd197ef0381d4c8225f87a07ca8
5
5
  SHA512:
6
- metadata.gz: 34eb95c4a0ff0c54018ec9a1671880816582a3a7c3e3b0d1eca69c64349d78c119f5bec9daf240266ef3f1c6b4c04359bc3b7ee0cef746be1bfd881b210d7298
7
- data.tar.gz: be8316e86a2b32d411fb485d73658071e713dfad3396d75f649852d2158e7b1f85b40a0bce4e6d12ac296c000886ac5c518369b52f5131270fe14a40db66ca03
6
+ metadata.gz: 38c2d66f98c4eb30a48968197d16513ba080d68bdaf417f884fd4af72f7d63ab0788c46554d636f1a5e6c344861e7c08d885432e9e5e6d44cdb0bd1b630bc4de
7
+ data.tar.gz: 02fb75182dacf15033c6d5fe3b0191b9c62063aec06fe67942ba2b6c0559368ce86af93b0ab507f3924e524a180e6bed9b2a7dd7b77fbdeca3af702abb141807
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'securerandom'
4
- require 'nchan_tools/pubsub'
4
+ require 'nchan_tools/benchmark'
5
5
  require "optparse"
6
6
  require 'timers'
7
7
  require 'json'
@@ -9,7 +9,7 @@ require "HDRHistogram"
9
9
 
10
10
  verbose = false
11
11
  save_csv = false
12
-
12
+ csv_columns = NchanTools::Benchmark::CSV_COLUMNS_DEFAULT
13
13
  init_args = {}
14
14
 
15
15
  opt_parser=OptionParser.new do |opts|
@@ -19,6 +19,9 @@ opt_parser=OptionParser.new do |opts|
19
19
  opts.on("--csv FILENAME", "Append results to file in CSV format") do |f|
20
20
  save_csv = f
21
21
  end
22
+ opts.on("--csv-columns col1,col2,...", "csv columns list") do |f|
23
+ csv_columns = f.split(/\W+/).map(&:to_sym)
24
+ end
22
25
  opts.on("-t", "--time TIME", "Time to run benchmark") do |v|
23
26
  init_args[:time] = v
24
27
  end
@@ -43,225 +46,13 @@ urls += ARGV
43
46
  begin
44
47
  urls += STDIN.read_nonblock(100000).split /\s*\n+\s*/
45
48
  rescue IO::WaitReadable
49
+ rescue EOFError
46
50
  end
47
51
 
48
52
  urls.uniq!
49
53
 
50
- class Benchan
51
- class BenchmarkError < StandardError
52
- end
53
- def initialize(urls, init_args=nil)
54
- @urls = urls
55
- @n = urls.count
56
- @initializing = 0
57
- @ready = 0
58
- @running = 0
59
- @finished = 0
60
- @subs = []
61
- @results = {}
62
- @failed = {}
63
-
64
- @init_args = init_args
65
-
66
- @hdrh_publish = nil
67
- @hdrh_receive = nil
68
-
69
- subs = []
70
- end
71
-
72
- def run
73
- puts "connecting to #{@n} Nchan server#{@n > 1 ? "s" : ""}..."
74
- @urls.each do |url|
75
- sub = Subscriber.new(url, 1, client: :websocket, timeout: 900000, extra_headers: {"Accept" => "text/x-json-hdrhistogram"})
76
- sub.on_failure do |err|
77
- unless @results[sub]
78
- unless @results[sub.url]
79
- @failed[sub] = true
80
- abort err, sub
81
- end
82
- end
83
- false
84
- end
85
- sub.on_message do |msg|
86
- msg = msg.to_s
87
- case msg
88
- when /^READY/
89
- puts " #{sub.url} ok"
90
- @ready +=1
91
- if @ready == @n
92
- control :run
93
- puts "start benchmark..."
94
- end
95
- when /^RUNNING/
96
- puts " #{sub.url} running"
97
- when /^RESULTS\n/
98
- msg = msg[8..-1]
99
- parsed = JSON.parse msg
100
- @results[sub.url] = parsed
101
- @results[sub.url]["raw"] = msg if @results[sub.url]
102
- 1+1
103
- when /^INITIALIZING/
104
- #do nothing
105
- else
106
- raise BenchmarkError, "unexpected server response: #{msg}"
107
- end
108
- end
109
- @subs << sub
110
- sub.run
111
- sub.wait :ready, 1
112
- if @failed[sub]
113
- puts " #{sub.url} failed"
114
- else
115
- puts " #{sub.url} ok"
116
- end
117
- end
118
- return if @failed.count > 0
119
- puts "initializing benchmark..."
120
- control :init
121
- self.wait
122
- puts "finished."
123
- puts ""
124
- end
125
-
126
- def wait
127
- @subs.each &:wait
128
- end
129
-
130
- def control(msg)
131
- if @init_args && (msg.to_sym ==:init || msg.to_sym ==:initialize)
132
- msg = "#{msg.to_s} #{@init_args.map{|k,v| "#{k}=#{v}"}.join(" ")}"
133
- end
134
- @subs.each { |sub| sub.client.send_data msg.to_s }
135
- end
136
-
137
- def abort(err, src_sub = nil)
138
- puts " #{err}"
139
- @subs.each do |sub|
140
- sub.terminate unless sub == src_sub
141
- end
142
- end
143
-
144
- def hdrhistogram_stats(name, histogram)
145
- fmt = <<-END.gsub(/^ {6}/, '')
146
- %s
147
- min: %.3fms
148
- avg: %.3fms
149
- 99%%ile: %.3fms
150
- max: %.3fms
151
- stddev: %.3fms
152
- samples: %d
153
- END
154
- fmt % [ name,
155
- histogram.min, histogram.mean, histogram.percentile(99.0), histogram.max, histogram.stddev, histogram.count
156
- ]
157
- end
158
-
159
- def results
160
- @channels = 0
161
- @runtime = []
162
- @subscribers = 0
163
- @message_length = []
164
- @messages_sent = 0
165
- @messages_send_confirmed = 0
166
- @messages_send_unconfirmed = 0
167
- @messages_send_failed = 0
168
- @messages_received = 0
169
- @messages_unreceived = 0
170
- @hdrh_publish = nil
171
- @hdrh_receive = nil
172
- @results.each do |url, data|
173
- @channels += data["channels"]
174
- @runtime << data["run_time_sec"]
175
- @subscribers += data["subscribers"]
176
- @message_length << data["message_length"]
177
- @messages_sent += data["messages"]["sent"]
178
- @messages_send_confirmed += data["messages"]["send_confirmed"]
179
- @messages_send_unconfirmed += data["messages"]["send_unconfirmed"]
180
- @messages_send_failed += data["messages"]["send_failed"]
181
- @messages_received += data["messages"]["received"]
182
- @messages_unreceived += data["messages"]["unreceived"]
183
-
184
- if data["message_publishing_histogram"]
185
- hdrh = HDRHistogram.unserialize(data["message_publishing_histogram"], unit: :ms, multiplier: 0.001)
186
- if @hdrh_publish
187
- @hdrh_publish.merge! hdrh
188
- else
189
- @hdrh_publish = hdrh
190
- end
191
- end
192
- if data["message_delivery_histogram"]
193
- hdrh = HDRHistogram.unserialize(data["message_delivery_histogram"], unit: :ms, multiplier: 0.001)
194
- if @hdrh_receive
195
- @hdrh_receive.merge! hdrh
196
- else
197
- @hdrh_receive = hdrh
198
- end
199
- end
200
- end
201
-
202
- @message_length = @message_length.sum.to_f / @message_length.size
203
- @runtime = @runtime.sum.to_f / @runtime.size
204
-
205
- fmt = <<-END.gsub(/^ {6}/, '')
206
- Nchan servers: %d
207
- runtime: %d
208
- channels: %d
209
- subscribers: %d
210
- subscribers per channel: %.1f
211
- messages:
212
- length: %d
213
- sent: %d
214
- send_confirmed: %d
215
- send_unconfirmed: %d
216
- send_failed: %d
217
- received: %d
218
- unreceived: %d
219
- send rate: %.3f/sec
220
- receive rate: %.3f/sec
221
- send rate per channel: %.3f/min
222
- receive rate per subscriber: %.3f/min
223
- END
224
- out = fmt % [
225
- @n, @runtime, @channels, @subscribers, @subscribers.to_f/@channels,
226
- @message_length, @messages_sent, @messages_send_confirmed, @messages_send_unconfirmed, @messages_send_failed,
227
- @messages_received, @messages_unreceived,
228
- @messages_sent.to_f/@runtime,
229
- @messages_received.to_f/@runtime,
230
- (@messages_sent.to_f* 60)/(@runtime * @channels),
231
- (@messages_received.to_f * 60)/(@runtime * @subscribers)
232
- ]
233
-
234
- out << hdrhistogram_stats("message publishing latency", @hdrh_publish) if @hdrh_publish
235
- out << hdrhistogram_stats("message delivery latency", @hdrh_receive) if @hdrh_receive
236
-
237
- puts out
238
- end
239
-
240
- def append_csv_file(file)
241
- require "csv"
242
- write_headers = File.zero?(file)
243
- headers = %i[servers runtime channels subscribers
244
- message_length messages_sent messages_send_confirmed messages_send_unconfirmed messages_send_failed
245
- messages_send_received messages_send_unreceived
246
- messages_send_rate messages_receive_rate messages_send_rate_per_channel messages_receive_rate_per_subscriber
247
- message_publishing_response_avg message_publishing_response_99percentile message_publishing_response_stddev message_publishing_response_count
248
- message_delivery_avg message_delivery_99percentile message_delivery_stddev message_delivery_count]
249
- csv = CSV.open(file, "a", {headers: headers, write_headers: write_headers})
250
- csv << [@n, @runtime, @channels, @subscribers,
251
- @message_length, @messages_sent, @messages_send_confirmed, @messages_send_unconfirmed, @messages_send_failed,
252
- @messages_received, @messages_unreceived,
253
- @messages_sent.to_f/@runtime, @messages_received.to_f/@runtime,
254
- (@messages_sent.to_f* 60)/(@runtime * @channels), (@messages_received.to_f * 60)/(@runtime * @subscribers),
255
- @hdrh_publish.mean, @hdrh_publish.percentile(99.0), @hdrh_publish.max, @hdrh_publish.stddev, @hdrh_publish.count,
256
- @hdrh_receive.mean, @hdrh_receive.percentile(99.0), @hdrh_receive.max, @hdrh_receive.stddev, @hdrh_receive.count
257
- ]
258
- csv.flush
259
- csv.close
260
- end
261
- end
262
-
263
- benchan = Benchan.new urls, init_args
54
+ benchan = NchanTools::Benchmark.new urls, init_args
264
55
  benchan.run
265
56
  benchan.results
266
- benchan.append_csv_file(save_csv) if save_csv
57
+ benchan.append_csv_file(save_csv, csv_columns) if save_csv
267
58
 
@@ -54,7 +54,7 @@ puts "Publishing to #{url}."
54
54
 
55
55
  loopmsg=("\r"*20) + "sending message #"
56
56
 
57
- pub = Publisher.new url, nostore: true, timeout: timeout, verbose: verbose, websocket: websocket
57
+ pub = NchanTools::Publisher.new url, nostore: true, timeout: timeout, verbose: verbose, websocket: websocket
58
58
  pub.accept=accept
59
59
  pub.nofail=true
60
60
  repeat=true
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "redis"
4
+ require "optparse"
5
+ require 'nchan_tools/rdsck'
6
+
7
+ $opt = {
8
+ url: "redis://127.0.0.1:6379/",
9
+ verbose: false,
10
+ command: nil
11
+ }
12
+
13
+ opt_parser=OptionParser.new do |opts|
14
+ opts.on("--url", "--url REDIS_URL (#{$opt[:url]})", "Redis server and port..") do |v|
15
+ $opt[:url]=v
16
+ end
17
+ opts.on("-q", "--quiet", "output only results without any other information") do
18
+ $opt[:quiet]=false
19
+ end
20
+ opts.on("--list-channels", "list all Nchan channels on Redis server or cluster") do |v|
21
+ $opt[:command]=:filter_channels
22
+ end
23
+ opts.on("--filter-channels-min-subscribers=[NUMBER]") do |v|
24
+ $opt[:command]=:filter_channels
25
+ $opt[:min_subscribers]=v.to_i
26
+ end
27
+ end
28
+ opt_parser.banner= <<~EOB
29
+ Debugging tools for the Redis server or cluster backing Nchan.
30
+ Usage: nchan-redis-debug [options]
31
+
32
+ WARNING: --list-channels and --filter-channels-* options for this tool
33
+ use the Redis SCAN command. This may increase CPU load on the Redis
34
+ server and may affect the latency of Nchan requests.
35
+ USE THESE OPTIONS WITH GREAT CARE
36
+
37
+ Example:
38
+ nchan-redis-debug --url redis:// --filter-channels-min-subscribers=10
39
+ EOB
40
+ opt_parser.parse!
41
+
42
+ rdsck = Rdsck.new $opt
43
+ if not rdsck.connect
44
+ STDERR.puts "failed to connect to #{$opt[:url]}"
45
+ exit 1
46
+ end
47
+
48
+ case $opt[:command]
49
+ when :filter_channels
50
+ puts "# scanning for channels #{$opt[:min_subscribers] && "with subscribers >= #{$opt[:min_subscribers]}"}"
51
+ chans = rdsck.filter_channels(min_subscribers: $opt[:min_subscribers])
52
+ puts "# found #{chans.count} channel#{chans.count != 1 && "s"}#{chans.count == 0 ? "." : ":"}"
53
+ puts chans.join("\n")
54
+ else
55
+ puts "Nothing to do"
56
+ end
@@ -33,7 +33,7 @@ opt_parser=OptionParser.new do |opts|
33
33
  opts.on("-p", "--parallel NUM (#{par})", "number of parallel clients"){|v| par = v.to_i}
34
34
  opts.on("-t", "--timeout SEC (#{opt[:timeout]})", "Long-poll timeout"){|v| opt[:timeout] = v}
35
35
  opts.on("-q", "--quit STRING (#{opt[:quit_message]})", "Quit message"){|v| opt[:quit_message] = v}
36
- opts.on("-c", "--client STRING (#{opt[:client]})", "sub client (one of #{Subscriber::Client.unique_aliases.join ', '})") do |v|
36
+ opts.on("-c", "--client STRING (#{opt[:client]})", "sub client (one of #{NchanTools::Subscriber::Client.unique_aliases.join ', '})") do |v|
37
37
  opt[:client] = v.to_sym
38
38
  end
39
39
  opts.on("--content-type", "show received content-type"){|v| print_content_type = true}
@@ -68,7 +68,7 @@ if origin
68
68
  opt[:extra_headers]['Origin'] = origin
69
69
  end
70
70
 
71
- sub = Subscriber.new url, par, opt
71
+ sub = NchanTools::Subscriber.new url, par, opt
72
72
 
73
73
 
74
74
  NOMSGF="\r"*30 + "Received message %i, len:%i"
@@ -111,7 +111,7 @@ sub.on_message do |msg|
111
111
  end
112
112
 
113
113
  sub.on_failure do |err_msg|
114
- if Subscriber::IntervalPollClient === sub.client
114
+ if NchanTools::Subscriber::IntervalPollClient === sub.client
115
115
  unless err_msg.match(/\(code 304\)/)
116
116
  false
117
117
  end
@@ -0,0 +1,241 @@
1
+ require 'nchan_tools/pubsub'
2
+ require 'securerandom'
3
+ require 'timers'
4
+ require 'json'
5
+
6
+ module NchanTools
7
+ class Benchmark
8
+ CSV_COLUMNS_ALL=%i[servers runtime channels channels_K channels_M subscribers message_length messages_sent messages_send_confirmed messages_send_unconfirmed messages_send_failed messages_received messages_unreceived messages_send_rate messages_receive_rate messages_send_rate_per_channel messages_receive_rate_per_subscriber message_publishing_avg message_publishing_99th message_publishing_max message_publishing_stddev message_publishing_count message_delivery_avg message_delivery_99th message_delivery_max message_delivery_stddev message_delivery_count]
9
+ CSV_COLUMNS_DEFAULT=%i[servers runtime channels subscribers message_length messages_sent messages_send_confirmed messages_send_unconfirmed messages_send_failed messages_received messages_unreceived messages_send_rate messages_receive_rate messages_send_rate_per_channel messages_receive_rate_per_subscriber message_publishing_avg message_publishing_99th message_publishing_max message_publishing_stddev message_publishing_count message_delivery_avg message_delivery_99th message_delivery_max message_delivery_stddev message_delivery_count]
10
+ class BenchmarkError < StandardError
11
+ end
12
+ def initialize(urls, init_args=nil)
13
+ @urls = urls
14
+ @n = urls.count
15
+ @initializing = 0
16
+ @ready = 0
17
+ @running = 0
18
+ @finished = 0
19
+ @subs = []
20
+ @results = {}
21
+ @failed = {}
22
+
23
+ @init_args = init_args
24
+ @histograms = {}
25
+
26
+ subs = []
27
+ end
28
+
29
+ def run
30
+ puts "connecting to #{@n} Nchan server#{@n > 1 ? "s" : ""}..."
31
+ @urls.each do |url|
32
+ sub = NchanTools::Subscriber.new(url, 1, client: :websocket, timeout: 900000, extra_headers: {"Accept" => "text/x-json-hdrhistogram"})
33
+ sub.on_failure do |err|
34
+ unless @results[sub]
35
+ unless @results[sub.url]
36
+ @failed[sub] = true
37
+ abort err, sub
38
+ end
39
+ end
40
+ false
41
+ end
42
+ sub.on_message do |msg|
43
+ msg = msg.to_s
44
+ case msg
45
+ when /^READY/
46
+ puts " #{sub.url} ok"
47
+ @ready +=1
48
+ if @ready == @n
49
+ puts "start benchmark..."
50
+ control :run
51
+ end
52
+ when /^RUNNING/
53
+ puts " #{sub.url} running"
54
+ when /^RESULTS\n/
55
+ msg = msg[8..-1]
56
+ parsed = JSON.parse msg
57
+
58
+ #backwards-compatible histogram fields
59
+ parsed["histograms"]||={}
60
+ if parsed[:message_publishing_histogram] then
61
+ parsed[:histograms]["message publishing"]=parsed[:message_publishing_histogram]
62
+ end
63
+ if parsed[:message_delivery_histogram] then
64
+ parsed[:histograms]["message delivery"]=parsed[:message_delivery_histogram]
65
+ end
66
+
67
+ @results[sub.url] = parsed
68
+ @results[sub.url]["raw"] = msg if @results[sub.url]
69
+ sub.client.send_close
70
+ when /^INITIALIZING/
71
+ #do nothing
72
+ else
73
+ raise BenchmarkError, "unexpected server response: #{msg}"
74
+ end
75
+ end
76
+ @subs << sub
77
+ sub.run
78
+ sub.wait :ready, 1
79
+ if @failed[sub]
80
+ puts " #{sub.url} failed"
81
+ else
82
+ puts " #{sub.url} ok"
83
+ end
84
+ end
85
+ return if @failed.count > 0
86
+ puts "initializing benchmark..."
87
+ control :init
88
+ self.wait
89
+ puts "finished."
90
+ puts ""
91
+ end
92
+
93
+ def wait
94
+ @subs.each &:wait
95
+ end
96
+
97
+ def control(msg)
98
+ if @init_args && (msg.to_sym ==:init || msg.to_sym ==:initialize)
99
+ msg = "#{msg.to_s} #{@init_args.map{|k,v| "#{k}=#{v}"}.join(" ")}"
100
+ end
101
+ @subs.each { |sub| sub.client.send_data msg.to_s }
102
+ end
103
+
104
+ def abort(err, src_sub = nil)
105
+ puts " #{err}"
106
+ @subs.each do |sub|
107
+ sub.terminate unless sub == src_sub
108
+ end
109
+ end
110
+
111
+ def hdrhistogram_stats(name, histogram)
112
+ fmt = <<-END.gsub(/^ {6}/, '')
113
+ %s
114
+ min: %.3fms
115
+ avg: %.3fms
116
+ 99%%ile: %.3fms
117
+ max: %.3fms
118
+ stddev: %.3fms
119
+ samples: %d
120
+ END
121
+ fmt % [ name,
122
+ histogram.min, histogram.mean, histogram.percentile(99.0), histogram.max, histogram.stddev, histogram.count
123
+ ]
124
+ end
125
+
126
+ def results
127
+ @channels = 0
128
+ @runtime = []
129
+ @subscribers = 0
130
+ @message_length = []
131
+ @messages_sent = 0
132
+ @messages_send_confirmed = 0
133
+ @messages_send_unconfirmed = 0
134
+ @messages_send_failed = 0
135
+ @messages_received = 0
136
+ @messages_unreceived = 0
137
+ @histograms = {}
138
+ @results.each do |url, data|
139
+ @channels += data["channels"]
140
+ @runtime << data["run_time_sec"]
141
+ @subscribers += data["subscribers"]
142
+ @message_length << data["message_length"]
143
+ @messages_sent += data["messages"]["sent"]
144
+ @messages_send_confirmed += data["messages"]["send_confirmed"]
145
+ @messages_send_unconfirmed += data["messages"]["send_unconfirmed"]
146
+ @messages_send_failed += data["messages"]["send_failed"]
147
+ @messages_received += data["messages"]["received"]
148
+ @messages_unreceived += data["messages"]["unreceived"]
149
+ if data["histograms"]
150
+ data["histograms"].each do |name, str|
151
+ name = name.to_sym
152
+ hdrh = HDRHistogram.unserialize(str, unit: :ms, multiplier: 0.001)
153
+ if @histograms[name]
154
+ @histograms[name].merge! hdrh
155
+ else
156
+ @histograms[name] = hdrh
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ @message_length = @message_length.inject(0, :+).to_f / @message_length.size
163
+ @runtime = @runtime.inject(0, :+).to_f / @runtime.size
164
+
165
+ fmt = <<-END.gsub(/^ {6}/, '')
166
+ Nchan servers: %d
167
+ runtime: %d
168
+ channels: %d
169
+ subscribers: %d
170
+ subscribers per channel: %.1f
171
+ messages:
172
+ length: %d
173
+ sent: %d
174
+ send_confirmed: %d
175
+ send_unconfirmed: %d
176
+ send_failed: %d
177
+ received: %d
178
+ unreceived: %d
179
+ send rate: %.3f/sec
180
+ receive rate: %.3f/sec
181
+ send rate per channel: %.3f/min
182
+ receive rate per subscriber: %.3f/min
183
+ END
184
+ out = fmt % [
185
+ @n, @runtime, @channels, @subscribers, @subscribers.to_f/@channels,
186
+ @message_length, @messages_sent, @messages_send_confirmed, @messages_send_unconfirmed, @messages_send_failed,
187
+ @messages_received, @messages_unreceived,
188
+ @messages_sent.to_f/@runtime,
189
+ @messages_received.to_f/@runtime,
190
+ (@messages_sent.to_f* 60)/(@runtime * @channels),
191
+ (@messages_received.to_f * 60)/(@runtime * @subscribers)
192
+ ]
193
+ @histograms.each do |name, histogram|
194
+ out << hdrhistogram_stats("#{name} latency:", histogram)
195
+ end
196
+
197
+ puts out
198
+ end
199
+
200
+ def append_csv_file(file, columns=Benchmark::CSV_COLUMNS_DEFAULT)
201
+ require "csv"
202
+ write_headers = File.zero?(file) || !File.exists?(file)
203
+ headers = columns
204
+ vals = {
205
+ servers: @n,
206
+ runtime: @runtime,
207
+ channels: @channels,
208
+ channels_K: @channels/1000.0,
209
+ channels_M: @channels/1000000.0,
210
+ subscribers: @subscribers * @channels,
211
+ message_length: @message_length,
212
+ messages_sent: @messages_sent,
213
+ messages_send_confirmed: @messages_send_confirmed,
214
+ messages_send_unconfirmed: @messages_send_unconfirmed,
215
+ messages_send_failed: @messages_send_failed,
216
+ messages_received: @messages_received,
217
+ messages_unreceived: @messages_unreceived,
218
+ messages_send_rate: @messages_sent.to_f/@runtime,
219
+ messages_receive_rate: @messages_received.to_f/@runtime,
220
+ messages_send_rate_per_channel: (@messages_sent.to_f* 60)/(@runtime * @channels),
221
+ messages_receive_rate_per_subscriber: (@messages_received.to_f * 60)/(@runtime * @subscribers * @channels)
222
+ }
223
+ @histograms.each do |name, histogram|
224
+ vals["#{name}_avg".to_sym]=histogram.mean
225
+ vals["#{name}_95th".to_sym]=histogram.percentile(95.0)
226
+ vals["#{name}_99th".to_sym]=histogram.percentile(99.0)
227
+ vals["#{name}_max".to_sym]=histogram.max
228
+ vals["#{name}_stddev".to_sym]=histogram.stddev
229
+ vals["#{name}_count".to_sym]=histogram.count
230
+ end
231
+
232
+ row = []
233
+ headers.each { |header| row << (vals[header.to_sym] || "-")}
234
+
235
+ csv = CSV.open(file, "a", {headers: headers, write_headers: write_headers})
236
+ csv << row
237
+ csv.flush
238
+ csv.close
239
+ end
240
+ end
241
+ end
@@ -3,7 +3,7 @@ require 'typhoeus'
3
3
  require 'json'
4
4
  require 'oga'
5
5
  require 'yaml'
6
- require 'pry'
6
+
7
7
  require 'celluloid/current'
8
8
  require 'date'
9
9
  Typhoeus::Config.memoize = false
@@ -46,15 +46,12 @@ module URI
46
46
  u
47
47
  end
48
48
  end
49
-
50
- $seq = 0
49
+ module NchanTools
51
50
  class Message
52
51
  attr_accessor :content_type, :message, :times_seen, :etag, :last_modified, :eventsource_event
53
52
  def initialize(msg, last_modified=nil, etag=nil)
54
53
  @times_seen=1
55
54
  @message, @last_modified, @etag = msg, last_modified, etag
56
- $seq+=1
57
- @seq = $seq
58
55
  @idhist = []
59
56
  end
60
57
  def serverside_id
@@ -490,6 +487,14 @@ class Subscriber
490
487
  @ws.binary data
491
488
  end
492
489
 
490
+ def send_ping(msg=nil)
491
+ @ws.ping(msg)
492
+ end
493
+
494
+ def send_close(reason=nil, code=1000)
495
+ @ws.close(reason, code)
496
+ end
497
+
493
498
  def write(data)
494
499
  @sock.write data
495
500
  end
@@ -582,11 +587,11 @@ class Subscriber
582
587
  end
583
588
 
584
589
  bundle.ws.on :ping do |ev|
585
- @on_ping.call if @on_ping
590
+ @subscriber.on(:ping).call ev, bundle
586
591
  end
587
592
 
588
593
  bundle.ws.on :pong do |ev|
589
- @on_pong.call if @on_pong
594
+ @subscriber.on(:pong).call ev, bundle
590
595
  end
591
596
 
592
597
  bundle.ws.on :error do |ev|
@@ -648,13 +653,6 @@ class Subscriber
648
653
  end
649
654
  end
650
655
 
651
- def on_ping
652
- @on_ping = Proc.new if block_given?
653
- end
654
- def on_pong
655
- @on_pong = Proc.new if block_given?
656
- end
657
-
658
656
  def listen(bundle)
659
657
  while @ws[bundle]
660
658
  begin
@@ -684,10 +682,10 @@ class Subscriber
684
682
  private :ws_client
685
683
 
686
684
  def send_ping(data=nil)
687
- ws_client.ping data
685
+ ws_client.send_ping data
688
686
  end
689
- def send_close(code=1000, reason=nil)
690
- ws_client.send_close code, reason
687
+ def send_close(reason=nil, code=1000)
688
+ ws_client.send_close reason, code
691
689
  end
692
690
  def send_data(data)
693
691
  ws_client.send_data data
@@ -697,17 +695,18 @@ class Subscriber
697
695
  end
698
696
 
699
697
  def close(bundle)
700
- if bundle
698
+ if bundle then
701
699
  @ws.delete bundle
702
700
  bundle.sock.close unless bundle.sock.closed?
703
701
  end
704
702
  @connected -= 1
705
- if @connected <= 0
706
- binding.pry unless @ws.count == 0
703
+ if @connected <= 0 then
704
+ until @ws.count == 0 do
705
+ sleep 0.1
706
+ end
707
707
  @cooked.signal true
708
708
  end
709
709
  end
710
-
711
710
  end
712
711
 
713
712
  class LongPollClient < Client
@@ -1521,6 +1520,8 @@ class Subscriber
1521
1520
 
1522
1521
  attr_accessor :url, :client, :messages, :max_round_trips, :quit_message, :errors, :concurrency, :waiting, :finished, :client_class, :log
1523
1522
  def initialize(url, concurrency=1, opt={})
1523
+ @empty_block = Proc.new {}
1524
+ @on={}
1524
1525
  @care_about_message_ids=opt[:use_message_id].nil? ? true : opt[:use_message_id]
1525
1526
  @url=url
1526
1527
  @quit_message = opt[:quit_message]
@@ -1615,6 +1616,14 @@ class Subscriber
1615
1616
  @client.poke until_what, timeout
1616
1617
  end
1617
1618
 
1619
+ def on(evt_name = nil, &block)
1620
+ if block_given?
1621
+ @on[evt_name.to_sym] = block
1622
+ else
1623
+ @on[evt_name.to_sym] or @empty_block
1624
+ end
1625
+ end
1626
+
1618
1627
  def on_message(msg=nil, bundle=nil, &block)
1619
1628
  #puts "received message #{msg && msg.to_s[0..15]}"
1620
1629
  if block_given?
@@ -1660,6 +1669,7 @@ class Publisher
1660
1669
  @accept = opt[:accept]
1661
1670
  @verbose = opt[:verbose]
1662
1671
  @on_response = opt[:on_response]
1672
+ @http2 = opt[:http2]
1663
1673
 
1664
1674
  @ws_wait_until_response = true
1665
1675
 
@@ -1800,6 +1810,7 @@ class Publisher
1800
1810
  headers = {:'Content-Type' => content_type, :'Accept' => accept}
1801
1811
  headers[:'X-Eventsource-Event'] = eventsource_event if eventsource_event
1802
1812
  headers.merge! @extra_headers if @extra_headers
1813
+
1803
1814
  post = Typhoeus::Request.new(
1804
1815
  @url,
1805
1816
  headers: headers,
@@ -1807,7 +1818,8 @@ class Publisher
1807
1818
  body: body,
1808
1819
  timeout: @timeout || PUBLISH_TIMEOUT,
1809
1820
  connecttimeout: @timeout || PUBLISH_TIMEOUT,
1810
- verbose: @verbose
1821
+ verbose: @verbose,
1822
+ http_version: @http2 ? :httpv2_0 : :none
1811
1823
  )
1812
1824
  if body && @messages
1813
1825
  msg=Message.new body
@@ -1895,3 +1907,4 @@ class Publisher
1895
1907
 
1896
1908
 
1897
1909
  end
1910
+ end
@@ -0,0 +1,119 @@
1
+ class Rdsck
2
+ attr_accessor :url, :verbose, :namespace
3
+ attr_accessor :redis, :masters
4
+
5
+ def dbg(*args)
6
+ if $opt[:verbose]
7
+ print("# ")
8
+ puts(*args)
9
+ end
10
+ end
11
+
12
+ def initialize(opt)
13
+ @url=opt[:url]
14
+ @verbose=opt[:verbose]
15
+ @namespace=opt[:namespace]
16
+ end
17
+
18
+ def cluster?
19
+ @cluster_mode
20
+ end
21
+
22
+ def connect
23
+ begin
24
+ @redis=Redis.new url: $opt[:url]
25
+ mode = redis.info["redis_mode"]
26
+ rescue StandardError => e
27
+ STDERR.puts e.message
28
+ return false
29
+ end
30
+
31
+ if mode == "cluster"
32
+ @redis.close
33
+ begin
34
+ @redis=Redis.new cluster: [$opt[:url]]
35
+ @redis.ping
36
+ rescue StandardError => e
37
+ STDERR.puts e.message
38
+ return false
39
+ end
40
+
41
+ @cluster_mode = true
42
+ @masters = []
43
+
44
+ redis.connection.each do |c|
45
+ node = Redis.new url: c[:id]
46
+ @masters << node
47
+ end
48
+ else
49
+ @masters = [@redis]
50
+ end
51
+
52
+ dbg "Connected to Redis #{mode == "cluster" ? "cluster" : "server"}"
53
+ (Array === @redis.connection ? @redis.connection : [@redis.connection]) .each do |v|
54
+ dbg " #{v[:id]}"
55
+ end
56
+ self
57
+ end
58
+
59
+ def key(subkey=nil)
60
+ k = "{channel:#{$opt[:namespace]}/#{$opt[:channel_id]}}"
61
+ return subkey ? "#{k}:#{subkey}" : k
62
+ end
63
+
64
+ def info
65
+ channel_hash=@redis.hgetall key
66
+ hash_ttl=@redis.ttl key
67
+ channel_subs=@redis.hgetall key("subscribers")
68
+ #...
69
+ end
70
+
71
+ def filter_channels(filters={})
72
+ script = <<~EOF
73
+ local prev_cursor = ARGV[1]
74
+ local pattern = ARGV[2]
75
+ local scan_batch_size = ARGV[3]
76
+
77
+ local min_subscribers = ARGV[4] and #ARGV[4] > 0 and tonumber(ARGV[4])
78
+
79
+ local cursor, iteration
80
+ if pattern and #pattern > 0 then
81
+ cursor, iteration = unpack(redis.call("SCAN", prev_cursor, "MATCH", pattern, "COUNT", scan_batch_size))
82
+ else
83
+ cursor, iteration = unpack(redis.call("SCAN", prev_cursor, "COUNT", scan_batch_size))
84
+ end
85
+
86
+ local matched = {}
87
+ for _, chankey in pairs(iteration) do
88
+ local match = true
89
+ if min_subscribers then
90
+ match = match and (tonumber(redis.call('HGET', chankey, 'fake_subscribers') or 0) >= min_subscribers)
91
+ end
92
+ if match then
93
+ table.insert(matched, chankey)
94
+ end
95
+ end
96
+
97
+ return {cursor, matched}
98
+ EOF
99
+
100
+ results = []
101
+ batch_size=500
102
+ masters.each do |m|
103
+ hash = m.script "load", script
104
+ cursor, pattern = "0", "{channel:*}"
105
+ loop do
106
+ cursor, batch_results = m.evalsha hash, keys: [], argv: [cursor, pattern, batch_size, filters[:min_subscribers]]
107
+ results += batch_results
108
+ pattern = ""
109
+ break if cursor.to_i == 0
110
+ end
111
+ end
112
+ results
113
+
114
+ results.map! do |key|
115
+ m = key.match(/^\{channel\:(.*)\}$/)
116
+ m[1] || key
117
+ end
118
+ end
119
+ end
@@ -1,3 +1,3 @@
1
1
  module NchanTools
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.9"
3
3
  end
@@ -29,15 +29,16 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency "celluloid"
30
30
  spec.add_dependency "celluloid-io"
31
31
  spec.add_dependency "HDRHistogram"
32
+ spec.add_dependency "redis"
32
33
 
33
34
  spec.add_dependency "websocket-driver"
34
35
  spec.add_dependency 'websocket-extensions'
35
36
  spec.add_dependency "permessage_deflate"
36
37
  spec.add_dependency 'http_parser.rb'
37
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.2')
38
- spec.add_dependency 'http-2'
39
- end
38
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.2')
39
+ spec.add_dependency 'http-2'
40
+ end
40
41
  spec.add_development_dependency "pry"
41
- spec.add_development_dependency "bundler", "~> 1.16"
42
- spec.add_development_dependency "rake", "~> 10.0"
42
+ spec.add_development_dependency "bundler"
43
+ spec.add_development_dependency "rake"
43
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nchan_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leo Ponomarev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-06 00:00:00.000000000 Z
11
+ date: 2021-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: redis
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: websocket-driver
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -182,36 +196,37 @@ dependencies:
182
196
  name: bundler
183
197
  requirement: !ruby/object:Gem::Requirement
184
198
  requirements:
185
- - - "~>"
199
+ - - ">="
186
200
  - !ruby/object:Gem::Version
187
- version: '1.16'
201
+ version: '0'
188
202
  type: :development
189
203
  prerelease: false
190
204
  version_requirements: !ruby/object:Gem::Requirement
191
205
  requirements:
192
- - - "~>"
206
+ - - ">="
193
207
  - !ruby/object:Gem::Version
194
- version: '1.16'
208
+ version: '0'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: rake
197
211
  requirement: !ruby/object:Gem::Requirement
198
212
  requirements:
199
- - - "~>"
213
+ - - ">="
200
214
  - !ruby/object:Gem::Version
201
- version: '10.0'
215
+ version: '0'
202
216
  type: :development
203
217
  prerelease: false
204
218
  version_requirements: !ruby/object:Gem::Requirement
205
219
  requirements:
206
- - - "~>"
220
+ - - ">="
207
221
  - !ruby/object:Gem::Version
208
- version: '10.0'
222
+ version: '0'
209
223
  description: publishing, subscribing, testing, and benchmarking utilities for Nchan.
210
224
  email:
211
225
  - leo@nchan.io
212
226
  executables:
213
227
  - nchan-benchmark
214
228
  - nchan-pub
229
+ - nchan-redis-debug
215
230
  - nchan-sub
216
231
  extensions: []
217
232
  extra_rdoc_files: []
@@ -226,16 +241,19 @@ files:
226
241
  - bin/setup
227
242
  - exe/nchan-benchmark
228
243
  - exe/nchan-pub
244
+ - exe/nchan-redis-debug
229
245
  - exe/nchan-sub
230
246
  - lib/nchan_tools.rb
247
+ - lib/nchan_tools/benchmark.rb
231
248
  - lib/nchan_tools/pubsub.rb
249
+ - lib/nchan_tools/rdsck.rb
232
250
  - lib/nchan_tools/version.rb
233
251
  - nchan_tools.gemspec
234
252
  homepage: https://nchan.io
235
253
  licenses:
236
254
  - WTFPL
237
255
  metadata: {}
238
- post_install_message:
256
+ post_install_message:
239
257
  rdoc_options: []
240
258
  require_paths:
241
259
  - lib
@@ -250,9 +268,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
268
  - !ruby/object:Gem::Version
251
269
  version: '0'
252
270
  requirements: []
253
- rubyforge_project:
254
- rubygems_version: 2.7.7
255
- signing_key:
271
+ rubygems_version: 3.1.4
272
+ signing_key:
256
273
  specification_version: 4
257
274
  summary: Development and testing utilities for Nchan
258
275
  test_files: []