nchan_tools 0.1.3 → 0.1.8

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: 7ebb8bad8e4eab2ac6e0dafd8b93941d8001324158feadbd4638701d45b2ca0e
4
- data.tar.gz: 89351ee1297b3b3bc01535e5da7a8fb2795ce16c0f3da3a7b1a5b89c2d6f4dcf
3
+ metadata.gz: 248e672dca0bd32b3f19b72ebbb6bdf9074cdb71f94ae61d26c5e06019abcba8
4
+ data.tar.gz: 0167f0442b2a325f25b64a93916563b9e53de250dff7401c7471735d7854feec
5
5
  SHA512:
6
- metadata.gz: 5cf14c247f20f07962a1087f6f9adf509576dc6e0b9d6e9285ce30401847c038d5ab072bd259271cac48212ebcdf571283a52e30ee79fabf0e3bd884e801a50c
7
- data.tar.gz: 7dc9a2ad487a014212fa27b936f5bab403f428403eb22bbede56705d17dbfc44daff7b30005b82fe6dfb2289cdb1b9a8857473290e5011584f7e565c556f4e5e
6
+ metadata.gz: 9da652c3162c4911784bf0e2b0442e63aa2a766a213b2edd3dd9ffaef2005a4f66dc7cf5ef3992ca655ebc8782d1c0468b1f14a97b268216b1f746c95b2a6cb9
7
+ data.tar.gz: 4b9b7be540e3a4584cb8eeaaffcedd673e3d657d852639f5946e31e07110b43c1c167df1d39339d87ef4bf091ae560e95a017b7385b6e032da0abc3983d69269
@@ -1,16 +1,15 @@
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'
8
- require "pry"
9
8
  require "HDRHistogram"
10
9
 
11
10
  verbose = false
12
11
  save_csv = false
13
-
12
+ csv_columns = NchanTools::Benchmark::CSV_COLUMNS_DEFAULT
14
13
  init_args = {}
15
14
 
16
15
  opt_parser=OptionParser.new do |opts|
@@ -20,6 +19,9 @@ opt_parser=OptionParser.new do |opts|
20
19
  opts.on("--csv FILENAME", "Append results to file in CSV format") do |f|
21
20
  save_csv = f
22
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
23
25
  opts.on("-t", "--time TIME", "Time to run benchmark") do |v|
24
26
  init_args[:time] = v
25
27
  end
@@ -44,225 +46,13 @@ urls += ARGV
44
46
  begin
45
47
  urls += STDIN.read_nonblock(100000).split /\s*\n+\s*/
46
48
  rescue IO::WaitReadable
49
+ rescue EOFError
47
50
  end
48
51
 
49
52
  urls.uniq!
50
53
 
51
- class Benchan
52
- class BenchmarkError < StandardError
53
- end
54
- def initialize(urls, init_args=nil)
55
- @urls = urls
56
- @n = urls.count
57
- @initializing = 0
58
- @ready = 0
59
- @running = 0
60
- @finished = 0
61
- @subs = []
62
- @results = {}
63
- @failed = {}
64
-
65
- @init_args = init_args
66
-
67
- @hdrh_publish = nil
68
- @hdrh_receive = nil
69
-
70
- subs = []
71
- end
72
-
73
- def run
74
- puts "connecting to #{@n} Nchan server#{@n > 1 ? "s" : ""}..."
75
- @urls.each do |url|
76
- sub = Subscriber.new(url, 1, client: :websocket, timeout: 900000, extra_headers: {"Accept" => "text/x-json-hdrhistogram"})
77
- sub.on_failure do |err|
78
- unless @results[sub]
79
- unless @results[sub.url]
80
- @failed[sub] = true
81
- abort err, sub
82
- end
83
- end
84
- false
85
- end
86
- sub.on_message do |msg|
87
- msg = msg.to_s
88
- case msg
89
- when /^READY/
90
- puts " #{sub.url} ok"
91
- @ready +=1
92
- if @ready == @n
93
- control :run
94
- puts "start benchmark..."
95
- end
96
- when /^RUNNING/
97
- puts " #{sub.url} running"
98
- when /^RESULTS\n/
99
- msg = msg[8..-1]
100
- parsed = JSON.parse msg
101
- @results[sub.url] = parsed
102
- @results[sub.url]["raw"] = msg if @results[sub.url]
103
- 1+1
104
- when /^INITIALIZING/
105
- #do nothing
106
- else
107
- raise BenchmarkError, "unexpected server response: #{msg}"
108
- end
109
- end
110
- @subs << sub
111
- sub.run
112
- sub.wait :ready, 1
113
- if @failed[sub]
114
- puts " #{sub.url} failed"
115
- else
116
- puts " #{sub.url} ok"
117
- end
118
- end
119
- return if @failed.count > 0
120
- puts "initializing benchmark..."
121
- control :init
122
- self.wait
123
- puts "finished."
124
- puts ""
125
- end
126
-
127
- def wait
128
- @subs.each &:wait
129
- end
130
-
131
- def control(msg)
132
- if @init_args && (msg.to_sym ==:init || msg.to_sym ==:initialize)
133
- msg = "#{msg.to_s} #{@init_args.map{|k,v| "#{k}=#{v}"}.join(" ")}"
134
- end
135
- @subs.each { |sub| sub.client.send_data msg.to_s }
136
- end
137
-
138
- def abort(err, src_sub = nil)
139
- puts " #{err}"
140
- @subs.each do |sub|
141
- sub.terminate unless sub == src_sub
142
- end
143
- end
144
-
145
- def hdrhistogram_stats(name, histogram)
146
- fmt = <<-END.gsub(/^ {6}/, '')
147
- %s
148
- min: %.3fms
149
- avg: %.3fms
150
- 99%%ile: %.3fms
151
- max: %.3fms
152
- stddev: %.3fms
153
- samples: %d
154
- END
155
- fmt % [ name,
156
- histogram.min, histogram.mean, histogram.percentile(99.0), histogram.max, histogram.stddev, histogram.count
157
- ]
158
- end
159
-
160
- def results
161
- @channels = 0
162
- @runtime = []
163
- @subscribers = 0
164
- @message_length = []
165
- @messages_sent = 0
166
- @messages_send_confirmed = 0
167
- @messages_send_unconfirmed = 0
168
- @messages_send_failed = 0
169
- @messages_received = 0
170
- @messages_unreceived = 0
171
- @hdrh_publish = nil
172
- @hdrh_receive = nil
173
- @results.each do |url, data|
174
- @channels += data["channels"]
175
- @runtime << data["run_time_sec"]
176
- @subscribers += data["subscribers"]
177
- @message_length << data["message_length"]
178
- @messages_sent += data["messages"]["sent"]
179
- @messages_send_confirmed += data["messages"]["send_confirmed"]
180
- @messages_send_unconfirmed += data["messages"]["send_unconfirmed"]
181
- @messages_send_failed += data["messages"]["send_failed"]
182
- @messages_received += data["messages"]["received"]
183
- @messages_unreceived += data["messages"]["unreceived"]
184
-
185
- if data["message_publishing_histogram"]
186
- hdrh = HDRHistogram.unserialize(data["message_publishing_histogram"], unit: :ms, multiplier: 0.001)
187
- if @hdrh_publish
188
- @hdrh_publish.merge! hdrh
189
- else
190
- @hdrh_publish = hdrh
191
- end
192
- end
193
- if data["message_delivery_histogram"]
194
- hdrh = HDRHistogram.unserialize(data["message_delivery_histogram"], unit: :ms, multiplier: 0.001)
195
- if @hdrh_receive
196
- @hdrh_receive.merge! hdrh
197
- else
198
- @hdrh_receive = hdrh
199
- end
200
- end
201
- end
202
-
203
- @message_length = @message_length.sum.to_f / @message_length.size
204
- @runtime = @runtime.sum.to_f / @runtime.size
205
-
206
- fmt = <<-END.gsub(/^ {6}/, '')
207
- Nchan servers: %d
208
- runtime: %d
209
- channels: %d
210
- subscribers: %d
211
- subscribers per channel: %.1f
212
- messages:
213
- length: %d
214
- sent: %d
215
- send_confirmed: %d
216
- send_unconfirmed: %d
217
- send_failed: %d
218
- received: %d
219
- unreceived: %d
220
- send rate: %.3f/sec
221
- receive rate: %.3f/sec
222
- send rate per channel: %.3f/min
223
- receive rate per subscriber: %.3f/min
224
- END
225
- out = fmt % [
226
- @n, @runtime, @channels, @subscribers, @subscribers.to_f/@channels,
227
- @message_length, @messages_sent, @messages_send_confirmed, @messages_send_unconfirmed, @messages_send_failed,
228
- @messages_received, @messages_unreceived,
229
- @messages_sent.to_f/@runtime,
230
- @messages_received.to_f/@runtime,
231
- (@messages_sent.to_f* 60)/(@runtime * @channels),
232
- (@messages_received.to_f * 60)/(@runtime * @subscribers)
233
- ]
234
-
235
- out << hdrhistogram_stats("message publishing latency", @hdrh_publish) if @hdrh_publish
236
- out << hdrhistogram_stats("message delivery latency", @hdrh_receive) if @hdrh_receive
237
-
238
- puts out
239
- end
240
-
241
- def append_csv_file(file)
242
- require "csv"
243
- write_headers = File.zero?(file)
244
- headers = %i[servers runtime channels subscribers
245
- message_length messages_sent messages_send_confirmed messages_send_unconfirmed messages_send_failed
246
- messages_send_received messages_send_unreceived
247
- messages_send_rate messages_receive_rate messages_send_rate_per_channel messages_receive_rate_per_subscriber
248
- message_publishing_response_avg message_publishing_response_99percentile message_publishing_response_stddev message_publishing_response_count
249
- message_delivery_avg message_delivery_99percentile message_delivery_stddev message_delivery_count]
250
- csv = CSV.open(file, "a", {headers: headers, write_headers: write_headers})
251
- csv << [@n, @runtime, @channels, @subscribers,
252
- @message_length, @messages_sent, @messages_send_confirmed, @messages_send_unconfirmed, @messages_send_failed,
253
- @messages_received, @messages_unreceived,
254
- @messages_sent.to_f/@runtime, @messages_received.to_f/@runtime,
255
- (@messages_sent.to_f* 60)/(@runtime * @channels), (@messages_received.to_f * 60)/(@runtime * @subscribers),
256
- @hdrh_publish.mean, @hdrh_publish.percentile(99.0), @hdrh_publish.max, @hdrh_publish.stddev, @hdrh_publish.count,
257
- @hdrh_receive.mean, @hdrh_receive.percentile(99.0), @hdrh_receive.max, @hdrh_receive.stddev, @hdrh_receive.count
258
- ]
259
- csv.flush
260
- csv.close
261
- end
262
- end
263
-
264
- benchan = Benchan.new urls, init_args
54
+ benchan = NchanTools::Benchmark.new urls, init_args
265
55
  benchan.run
266
56
  benchan.results
267
- benchan.append_csv_file(save_csv) if save_csv
57
+ benchan.append_csv_file(save_csv, csv_columns) if save_csv
268
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.3"
2
+ VERSION = "0.1.8"
3
3
  end
@@ -34,9 +34,9 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency 'websocket-extensions'
35
35
  spec.add_dependency "permessage_deflate"
36
36
  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
37
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.2')
38
+ spec.add_dependency 'http-2'
39
+ end
40
40
  spec.add_development_dependency "pry"
41
41
  spec.add_development_dependency "bundler", "~> 1.16"
42
42
  spec.add_development_dependency "rake", "~> 10.0"
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.3
4
+ version: 0.1.8
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
@@ -212,6 +212,7 @@ email:
212
212
  executables:
213
213
  - nchan-benchmark
214
214
  - nchan-pub
215
+ - nchan-redis-debug
215
216
  - nchan-sub
216
217
  extensions: []
217
218
  extra_rdoc_files: []
@@ -226,16 +227,19 @@ files:
226
227
  - bin/setup
227
228
  - exe/nchan-benchmark
228
229
  - exe/nchan-pub
230
+ - exe/nchan-redis-debug
229
231
  - exe/nchan-sub
230
232
  - lib/nchan_tools.rb
233
+ - lib/nchan_tools/benchmark.rb
231
234
  - lib/nchan_tools/pubsub.rb
235
+ - lib/nchan_tools/rdsck.rb
232
236
  - lib/nchan_tools/version.rb
233
237
  - nchan_tools.gemspec
234
238
  homepage: https://nchan.io
235
239
  licenses:
236
240
  - WTFPL
237
241
  metadata: {}
238
- post_install_message:
242
+ post_install_message:
239
243
  rdoc_options: []
240
244
  require_paths:
241
245
  - lib
@@ -250,9 +254,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
254
  - !ruby/object:Gem::Version
251
255
  version: '0'
252
256
  requirements: []
253
- rubyforge_project:
254
- rubygems_version: 2.7.7
255
- signing_key:
257
+ rubygems_version: 3.1.4
258
+ signing_key:
256
259
  specification_version: 4
257
260
  summary: Development and testing utilities for Nchan
258
261
  test_files: []