georgi-nsq-ruby 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +405 -0
- data/lib/nsq.rb +13 -0
- data/lib/nsq/client_base.rb +120 -0
- data/lib/nsq/connection.rb +461 -0
- data/lib/nsq/consumer.rb +98 -0
- data/lib/nsq/discovery.rb +98 -0
- data/lib/nsq/exceptions.rb +5 -0
- data/lib/nsq/frames/error.rb +6 -0
- data/lib/nsq/frames/frame.rb +16 -0
- data/lib/nsq/frames/message.rb +33 -0
- data/lib/nsq/frames/response.rb +6 -0
- data/lib/nsq/logger.rb +38 -0
- data/lib/nsq/producer.rb +94 -0
- data/lib/version.rb +9 -0
- metadata +116 -0
data/lib/nsq/consumer.rb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require_relative 'client_base'
|
|
2
|
+
|
|
3
|
+
module Nsq
|
|
4
|
+
class Consumer < ClientBase
|
|
5
|
+
|
|
6
|
+
attr_reader :max_in_flight
|
|
7
|
+
|
|
8
|
+
def initialize(opts = {})
|
|
9
|
+
if opts[:nsqlookupd]
|
|
10
|
+
@nsqlookupds = [opts[:nsqlookupd]].flatten
|
|
11
|
+
else
|
|
12
|
+
@nsqlookupds = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@topic = opts[:topic] || raise(ArgumentError, 'topic is required')
|
|
16
|
+
@channel = opts[:channel] || raise(ArgumentError, 'channel is required')
|
|
17
|
+
@max_in_flight = opts[:max_in_flight] || 1
|
|
18
|
+
@discovery_interval = opts[:discovery_interval] || 60
|
|
19
|
+
@msg_timeout = opts[:msg_timeout]
|
|
20
|
+
@ssl_context = opts[:ssl_context]
|
|
21
|
+
@tls_options = opts[:tls_options]
|
|
22
|
+
@tls_v1 = opts[:tls_v1]
|
|
23
|
+
|
|
24
|
+
# This is where we queue up the messages we receive from each connection
|
|
25
|
+
@messages = opts[:queue] || Queue.new
|
|
26
|
+
|
|
27
|
+
# This is where we keep a record of our active nsqd connections
|
|
28
|
+
# The key is a string with the host and port of the instance (e.g.
|
|
29
|
+
# '127.0.0.1:4150') and the key is the Connection instance.
|
|
30
|
+
@connections = {}
|
|
31
|
+
|
|
32
|
+
if !@nsqlookupds.empty?
|
|
33
|
+
discover_repeatedly(
|
|
34
|
+
nsqlookupds: @nsqlookupds,
|
|
35
|
+
topic: @topic,
|
|
36
|
+
interval: @discovery_interval
|
|
37
|
+
)
|
|
38
|
+
else
|
|
39
|
+
# normally, we find nsqd instances to connect to via nsqlookupd(s)
|
|
40
|
+
# in this case let's connect to an nsqd instance directly
|
|
41
|
+
add_connection(opts[:nsqd] || '127.0.0.1:4150', max_in_flight: @max_in_flight)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# pop the next message off the queue
|
|
47
|
+
def pop
|
|
48
|
+
@messages.pop
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# By default, if the internal queue is empty, pop will block until
|
|
53
|
+
# a new message comes in.
|
|
54
|
+
#
|
|
55
|
+
# Calling this method won't block. If there are no messages, it just
|
|
56
|
+
# returns nil.
|
|
57
|
+
def pop_without_blocking
|
|
58
|
+
@messages.pop(true)
|
|
59
|
+
rescue ThreadError
|
|
60
|
+
# When the Queue is empty calling `Queue#pop(true)` will raise a ThreadError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# returns the number of messages we have locally in the queue
|
|
66
|
+
def size
|
|
67
|
+
@messages.size
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
def add_connection(nsqd, options = {})
|
|
73
|
+
super(nsqd, {
|
|
74
|
+
topic: @topic,
|
|
75
|
+
channel: @channel,
|
|
76
|
+
queue: @messages,
|
|
77
|
+
msg_timeout: @msg_timeout,
|
|
78
|
+
max_in_flight: 1
|
|
79
|
+
}.merge(options))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Be conservative, but don't set a connection's max_in_flight below 1
|
|
83
|
+
def max_in_flight_per_connection(number_of_connections = @connections.length)
|
|
84
|
+
[@max_in_flight / number_of_connections, 1].max
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def connections_changed
|
|
88
|
+
redistribute_ready
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def redistribute_ready
|
|
92
|
+
@connections.values.each do |connection|
|
|
93
|
+
connection.max_in_flight = max_in_flight_per_connection
|
|
94
|
+
connection.re_up_ready
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
require_relative 'logger'
|
|
6
|
+
|
|
7
|
+
# Connects to nsqlookup's to find the nsqd instances for a given topic
|
|
8
|
+
module Nsq
|
|
9
|
+
class Discovery
|
|
10
|
+
include Nsq::AttributeLogger
|
|
11
|
+
|
|
12
|
+
# lookupd addresses must be formatted like so: '<host>:<http-port>'
|
|
13
|
+
def initialize(lookupds)
|
|
14
|
+
@lookupds = lookupds
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns an array of nsqds instances
|
|
18
|
+
#
|
|
19
|
+
# nsqd instances returned are strings in this format: '<host>:<tcp-port>'
|
|
20
|
+
#
|
|
21
|
+
# discovery.nsqds
|
|
22
|
+
# #=> ['127.0.0.1:4150', '127.0.0.1:4152']
|
|
23
|
+
#
|
|
24
|
+
# If all nsqlookupd's are unreachable, raises Nsq::DiscoveryException
|
|
25
|
+
#
|
|
26
|
+
def nsqds
|
|
27
|
+
gather_nsqds_from_all_lookupds do |lookupd|
|
|
28
|
+
get_nsqds(lookupd)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns an array of nsqds instances that have messages for
|
|
33
|
+
# that topic.
|
|
34
|
+
#
|
|
35
|
+
# nsqd instances returned are strings in this format: '<host>:<tcp-port>'
|
|
36
|
+
#
|
|
37
|
+
# discovery.nsqds_for_topic('a-topic')
|
|
38
|
+
# #=> ['127.0.0.1:4150', '127.0.0.1:4152']
|
|
39
|
+
#
|
|
40
|
+
# If all nsqlookupd's are unreachable, raises Nsq::DiscoveryException
|
|
41
|
+
#
|
|
42
|
+
def nsqds_for_topic(topic)
|
|
43
|
+
gather_nsqds_from_all_lookupds do |lookupd|
|
|
44
|
+
get_nsqds(lookupd, topic)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def gather_nsqds_from_all_lookupds
|
|
52
|
+
nsqd_list = @lookupds.map do |lookupd|
|
|
53
|
+
yield(lookupd)
|
|
54
|
+
end.flatten
|
|
55
|
+
|
|
56
|
+
# All nsqlookupds were unreachable, raise an error!
|
|
57
|
+
if nsqd_list.length > 0 && nsqd_list.all? { |nsqd| nsqd.nil? }
|
|
58
|
+
raise DiscoveryException
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
nsqd_list.compact.uniq
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns an array of nsqd addresses
|
|
65
|
+
# If there's an error, return nil
|
|
66
|
+
def get_nsqds(lookupd, topic = nil)
|
|
67
|
+
uri_scheme = 'http://' unless lookupd.match(%r(https?://))
|
|
68
|
+
uri = URI.parse("#{uri_scheme}#{lookupd}")
|
|
69
|
+
|
|
70
|
+
uri.query = "ts=#{Time.now.to_i}"
|
|
71
|
+
if topic
|
|
72
|
+
uri.path = '/lookup'
|
|
73
|
+
uri.query += "&topic=#{URI.escape(topic)}"
|
|
74
|
+
else
|
|
75
|
+
uri.path = '/nodes'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
body = Net::HTTP.get(uri)
|
|
80
|
+
data = JSON.parse(body)
|
|
81
|
+
producers = data['producers'] || # v1.0.0-compat
|
|
82
|
+
(data['data'] && data['data']['producers'])
|
|
83
|
+
|
|
84
|
+
if producers
|
|
85
|
+
producers.map do |producer|
|
|
86
|
+
"#{producer['broadcast_address']}:#{producer['tcp_port']}"
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
rescue Exception => e
|
|
92
|
+
error "Error during discovery for #{lookupd}: #{e}"
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative '../logger'
|
|
2
|
+
|
|
3
|
+
module Nsq
|
|
4
|
+
class Frame
|
|
5
|
+
include Nsq::AttributeLogger
|
|
6
|
+
@@log_attributes = [:connection]
|
|
7
|
+
|
|
8
|
+
attr_reader :data
|
|
9
|
+
attr_reader :connection
|
|
10
|
+
|
|
11
|
+
def initialize(data, connection)
|
|
12
|
+
@data = data
|
|
13
|
+
@connection = connection
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative 'frame'
|
|
2
|
+
|
|
3
|
+
module Nsq
|
|
4
|
+
class Message < Frame
|
|
5
|
+
|
|
6
|
+
attr_reader :attempts
|
|
7
|
+
attr_reader :id
|
|
8
|
+
attr_reader :body
|
|
9
|
+
|
|
10
|
+
def initialize(data, connection)
|
|
11
|
+
super
|
|
12
|
+
@timestamp_in_nanoseconds, @attempts, @id, @body = @data.unpack('Q>s>a16a*')
|
|
13
|
+
@body.force_encoding('UTF-8')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def finish
|
|
17
|
+
connection.fin(id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def requeue(timeout = 0)
|
|
21
|
+
connection.req(id, timeout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def touch
|
|
25
|
+
connection.touch(id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def timestamp
|
|
29
|
+
Time.at(@timestamp_in_nanoseconds / 1_000_000_000.0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/nsq/logger.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
module Nsq
|
|
3
|
+
@@logger = Logger.new(nil)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def self.logger
|
|
7
|
+
@@logger
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def self.logger=(new_logger)
|
|
12
|
+
@@logger = new_logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
module AttributeLogger
|
|
17
|
+
def self.included(klass)
|
|
18
|
+
klass.send :class_variable_set, :@@log_attributes, []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
%w(fatal error warn info debug).map{|m| m.to_sym}.each do |level|
|
|
22
|
+
define_method level do |msg|
|
|
23
|
+
Nsq.logger.send(level, "#{prefix} #{msg}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def prefix
|
|
30
|
+
attrs = self.class.send(:class_variable_get, :@@log_attributes)
|
|
31
|
+
if attrs.count > 0
|
|
32
|
+
"[#{attrs.map{|a| "#{a.to_s}: #{self.send(a)}"}.join(' ')}] "
|
|
33
|
+
else
|
|
34
|
+
''
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/nsq/producer.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require_relative 'client_base'
|
|
2
|
+
|
|
3
|
+
module Nsq
|
|
4
|
+
class Producer < ClientBase
|
|
5
|
+
attr_reader :topic
|
|
6
|
+
|
|
7
|
+
def initialize(opts = {})
|
|
8
|
+
@connections = {}
|
|
9
|
+
@topic = opts[:topic]
|
|
10
|
+
@discovery_interval = opts[:discovery_interval] || 60
|
|
11
|
+
@ssl_context = opts[:ssl_context]
|
|
12
|
+
@tls_options = opts[:tls_options]
|
|
13
|
+
@tls_v1 = opts[:tls_v1]
|
|
14
|
+
|
|
15
|
+
nsqlookupds = []
|
|
16
|
+
if opts[:nsqlookupd]
|
|
17
|
+
nsqlookupds = [opts[:nsqlookupd]].flatten
|
|
18
|
+
discover_repeatedly(
|
|
19
|
+
nsqlookupds: nsqlookupds,
|
|
20
|
+
interval: @discovery_interval
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
elsif opts[:nsqd]
|
|
24
|
+
nsqds = [opts[:nsqd]].flatten
|
|
25
|
+
nsqds.each{|d| add_connection(d)}
|
|
26
|
+
|
|
27
|
+
else
|
|
28
|
+
add_connection('127.0.0.1:4150')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write(*raw_messages)
|
|
33
|
+
if !@topic
|
|
34
|
+
raise 'No topic specified. Either specify a topic when instantiating the Producer or use write_to_topic.'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
write_to_topic(@topic, *raw_messages)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Arg 'delay' in seconds
|
|
41
|
+
def deferred_write(delay, *raw_messages)
|
|
42
|
+
if !@topic
|
|
43
|
+
raise 'No topic specified. Either specify a topic when instantiating the Producer or use write_to_topic.'
|
|
44
|
+
end
|
|
45
|
+
if delay < 0.0
|
|
46
|
+
raise "Delay can't be negative, use a positive float."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
deferred_write_to_topic(@topic, delay, *raw_messages)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_to_topic(topic, *raw_messages)
|
|
53
|
+
# return error if message(s) not provided
|
|
54
|
+
raise ArgumentError, 'message not provided' if raw_messages.empty?
|
|
55
|
+
|
|
56
|
+
# stringify the messages
|
|
57
|
+
messages = raw_messages.map(&:to_s)
|
|
58
|
+
|
|
59
|
+
# get a suitable connection to write to
|
|
60
|
+
connection = connection_for_write
|
|
61
|
+
|
|
62
|
+
if messages.length > 1
|
|
63
|
+
connection.mpub(topic, messages)
|
|
64
|
+
else
|
|
65
|
+
connection.pub(topic, messages.first)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def deferred_write_to_topic(topic, delay, *raw_messages)
|
|
70
|
+
raise ArgumentError, 'message not provided' if raw_messages.empty?
|
|
71
|
+
messages = raw_messages.map(&:to_s)
|
|
72
|
+
connection = connection_for_write
|
|
73
|
+
messages.each do |msg|
|
|
74
|
+
connection.dpub(topic, (delay * 1000).to_i, msg)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
def connection_for_write
|
|
80
|
+
# Choose a random Connection that's currently connected
|
|
81
|
+
# Or, if there's nothing connected, just take any random one
|
|
82
|
+
connections_currently_connected = connections.select{|_,c| c.connected?}
|
|
83
|
+
connection = connections_currently_connected.values.sample || connections.values.sample
|
|
84
|
+
|
|
85
|
+
# Raise an exception if there's no connection available
|
|
86
|
+
unless connection
|
|
87
|
+
raise 'No connections available'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
connection
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: georgi-nsq-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wistia
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-06-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: jeweler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: nsq-cluster
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.1'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
description: ''
|
|
70
|
+
email: dev@wistia.com
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files:
|
|
74
|
+
- LICENSE.txt
|
|
75
|
+
- README.md
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE.txt
|
|
78
|
+
- README.md
|
|
79
|
+
- lib/nsq.rb
|
|
80
|
+
- lib/nsq/client_base.rb
|
|
81
|
+
- lib/nsq/connection.rb
|
|
82
|
+
- lib/nsq/consumer.rb
|
|
83
|
+
- lib/nsq/discovery.rb
|
|
84
|
+
- lib/nsq/exceptions.rb
|
|
85
|
+
- lib/nsq/frames/error.rb
|
|
86
|
+
- lib/nsq/frames/frame.rb
|
|
87
|
+
- lib/nsq/frames/message.rb
|
|
88
|
+
- lib/nsq/frames/response.rb
|
|
89
|
+
- lib/nsq/logger.rb
|
|
90
|
+
- lib/nsq/producer.rb
|
|
91
|
+
- lib/version.rb
|
|
92
|
+
homepage: http://github.com/wistia/nsq-ruby
|
|
93
|
+
licenses:
|
|
94
|
+
- MIT
|
|
95
|
+
metadata: {}
|
|
96
|
+
post_install_message:
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubyforge_project:
|
|
112
|
+
rubygems_version: 2.5.1
|
|
113
|
+
signing_key:
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Ruby client library for NSQ
|
|
116
|
+
test_files: []
|