nchan_tools 0.1.4 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/exe/nchan-benchmark +8 -217
- data/exe/nchan-pub +1 -1
- data/exe/nchan-redis-debug +56 -0
- data/exe/nchan-sub +3 -3
- data/lib/nchan_tools/benchmark.rb +241 -0
- data/lib/nchan_tools/pubsub.rb +35 -22
- data/lib/nchan_tools/rdsck.rb +119 -0
- data/lib/nchan_tools/version.rb +1 -1
- data/nchan_tools.gemspec +6 -5
- metadata +32 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec4e4cc624f57a6d6a6684bf71db8d75b58a4e2e823e258490b9be66eb5de396
|
4
|
+
data.tar.gz: bea9e679190cff5749804e8f8e3ef14ca825edd197ef0381d4c8225f87a07ca8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38c2d66f98c4eb30a48968197d16513ba080d68bdaf417f884fd4af72f7d63ab0788c46554d636f1a5e6c344861e7c08d885432e9e5e6d44cdb0bd1b630bc4de
|
7
|
+
data.tar.gz: 02fb75182dacf15033c6d5fe3b0191b9c62063aec06fe67942ba2b6c0559368ce86af93b0ab507f3924e524a180e6bed9b2a7dd7b77fbdeca3af702abb141807
|
data/exe/nchan-benchmark
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'securerandom'
|
4
|
-
require 'nchan_tools/
|
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
|
-
|
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
|
|
data/exe/nchan-pub
CHANGED
@@ -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
|
data/exe/nchan-sub
CHANGED
@@ -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
|
data/lib/nchan_tools/pubsub.rb
CHANGED
@@ -3,7 +3,7 @@ require 'typhoeus'
|
|
3
3
|
require 'json'
|
4
4
|
require 'oga'
|
5
5
|
require 'yaml'
|
6
|
-
|
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
|
-
@
|
590
|
+
@subscriber.on(:ping).call ev, bundle
|
586
591
|
end
|
587
592
|
|
588
593
|
bundle.ws.on :pong do |ev|
|
589
|
-
@
|
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.
|
685
|
+
ws_client.send_ping data
|
688
686
|
end
|
689
|
-
def send_close(
|
690
|
-
ws_client.send_close
|
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
|
-
|
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
|
data/lib/nchan_tools/version.rb
CHANGED
data/nchan_tools.gemspec
CHANGED
@@ -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
|
-
|
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"
|
42
|
-
spec.add_development_dependency "rake"
|
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
|
+
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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
|
-
|
254
|
-
|
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: []
|