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 +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: []
|