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.
@@ -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,5 @@
1
+ module Nsq
2
+ # Raised when nsqlookupd discovery fails
3
+ class DiscoveryException < Exception; end
4
+ end
5
+
@@ -0,0 +1,6 @@
1
+ require_relative 'frame'
2
+
3
+ module Nsq
4
+ class Error < Frame
5
+ end
6
+ 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
@@ -0,0 +1,6 @@
1
+ require_relative 'frame'
2
+
3
+ module Nsq
4
+ class Response < Frame
5
+ end
6
+ end
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ module Nsq
2
+ module Version
3
+ MAJOR = 2
4
+ MINOR = 1
5
+ PATCH = 0
6
+ BUILD = nil
7
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
8
+ end
9
+ end
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: []