nsq-ruby-maglev- 1.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 +311 -0
- data/lib/nsq.rb +13 -0
- data/lib/nsq/client_base.rb +117 -0
- data/lib/nsq/connection.rb +402 -0
- data/lib/nsq/consumer.rb +84 -0
- data/lib/nsq/discovery.rb +96 -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 +34 -0
- data/lib/nsq/frames/response.rb +6 -0
- data/lib/nsq/logger.rb +38 -0
- data/lib/nsq/producer.rb +63 -0
- data/lib/version.rb +9 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 404388ace1303e56c07f6b18284a81abe2cd03e8
|
4
|
+
data.tar.gz: 77946db3d3f9d10a2d515ff75ac3ca48888fbc55
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 106aa12520e1d473c57ff760eff08267c5bd1294a0586dc06ee607d513722a0e9651435eea6a2ef4403d72f70bfd50e412909e4bfc93740e58e756d89c490132
|
7
|
+
data.tar.gz: 24d46489acd7a824001d0a6720e0b9bedc1b259e27a974fbe7e5c8b33b83259fc3555aea1ae1749785e31ac6def0d7eb543b3a9cd8e018055c73cbc9d816a98e
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Wistia, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
# nsq-ruby
|
2
|
+
|
3
|
+
nsq-ruby is a simple NSQ client library written in Ruby.
|
4
|
+
|
5
|
+
- The code is straightforward.
|
6
|
+
- It has no dependencies.
|
7
|
+
- It's well tested.
|
8
|
+
- It's being used in production and has processed billions of messages.
|
9
|
+
|
10
|
+
|
11
|
+
## Quick start
|
12
|
+
|
13
|
+
### Publish messages
|
14
|
+
|
15
|
+
```Ruby
|
16
|
+
require 'nsq'
|
17
|
+
producer = Nsq::Producer.new(
|
18
|
+
nsqd: '127.0.0.1:4150',
|
19
|
+
topic: 'some-topic'
|
20
|
+
)
|
21
|
+
|
22
|
+
# Write a message to NSQ
|
23
|
+
producer.write('some-message')
|
24
|
+
|
25
|
+
# Write a bunch of messages to NSQ (uses mpub)
|
26
|
+
producer.write('one', 'two', 'three', 'four', 'five')
|
27
|
+
|
28
|
+
# Close the connection
|
29
|
+
producer.terminate
|
30
|
+
```
|
31
|
+
|
32
|
+
### Consume messages
|
33
|
+
|
34
|
+
```Ruby
|
35
|
+
require 'nsq'
|
36
|
+
consumer = Nsq::Consumer.new(
|
37
|
+
nsqlookupd: '127.0.0.1:4161',
|
38
|
+
topic: 'some-topic',
|
39
|
+
channel: 'some-channel'
|
40
|
+
)
|
41
|
+
|
42
|
+
# Pop a message off the queue
|
43
|
+
msg = consumer.pop
|
44
|
+
puts msg.body
|
45
|
+
msg.finish
|
46
|
+
|
47
|
+
# Close the connections
|
48
|
+
consumer.terminate
|
49
|
+
```
|
50
|
+
|
51
|
+
|
52
|
+
## Producer
|
53
|
+
|
54
|
+
### Initialization
|
55
|
+
|
56
|
+
The Nsq::Producer constructor takes the following options:
|
57
|
+
|
58
|
+
| Option | Description | Default |
|
59
|
+
|---------------|----------------------------------------|--------------------|
|
60
|
+
| `topic` | Topic to which to publish messages | |
|
61
|
+
| `nsqd` | Host and port of the nsqd instance | '127.0.0.1:4150' |
|
62
|
+
| `nsqlookupd` | Use lookupd to automatically discover nsqds | |
|
63
|
+
|
64
|
+
For example, if you'd like to publish messages to a single nsqd.
|
65
|
+
|
66
|
+
```Ruby
|
67
|
+
producer = Nsq::Producer.new(
|
68
|
+
nsqd: '6.7.8.9:4150',
|
69
|
+
topic: 'topic-of-great-esteem'
|
70
|
+
)
|
71
|
+
```
|
72
|
+
|
73
|
+
Alternatively, you can use nsqlookupd to find all nsqd nodes in the cluster.
|
74
|
+
When you instantiate Nsq::Producer in this way, it will automatically maintain
|
75
|
+
connections to all nsqd instances. When you publish a message, it will be sent
|
76
|
+
to a random nsqd instance.
|
77
|
+
|
78
|
+
```Ruby
|
79
|
+
producer = Nsq::Producer.new(
|
80
|
+
nsqlookupd: ['1.2.3.4:4161', '6.7.8.9:4161'],
|
81
|
+
topic: 'topic-of-great-esteem'
|
82
|
+
)
|
83
|
+
```
|
84
|
+
|
85
|
+
|
86
|
+
### `#write`
|
87
|
+
|
88
|
+
Publishes one or more message to nsqd. If you give it a single argument, it will
|
89
|
+
send it to nsqd via `PUB`. If you give it multiple arguments, it will send all
|
90
|
+
those messages to nsqd via `MPUB`. It will automatically call `to_s` on any
|
91
|
+
arguments you give it.
|
92
|
+
|
93
|
+
```Ruby
|
94
|
+
# Send a single message via PUB
|
95
|
+
producer.write(123)
|
96
|
+
|
97
|
+
# Send three messages via MPUB
|
98
|
+
producer.write(456, 'another-message', { key: 'value' }.to_json)
|
99
|
+
```
|
100
|
+
|
101
|
+
If it's connection to nsqd fails, it will automatically try to reconnect with
|
102
|
+
exponential backoff. Any messages that were sent to `#write` will be queued
|
103
|
+
and transmitted after reconnecting.
|
104
|
+
|
105
|
+
**Note** we don't wait for nsqd to acknowledge our writes. As a result, if the
|
106
|
+
connection to nsqd fails, you can lose messages. This is acceptable for our use
|
107
|
+
cases, mostly because we are sending messages to a local nsqd instance and
|
108
|
+
failure is very rare.
|
109
|
+
|
110
|
+
### `#connected?`
|
111
|
+
|
112
|
+
Returns true if it's currently connected to nsqd and false if not.
|
113
|
+
|
114
|
+
### `#terminate`
|
115
|
+
|
116
|
+
Closes the connection to nsqd and stops it from trying to automatically
|
117
|
+
reconnect.
|
118
|
+
|
119
|
+
This is automatically called `at_exit`, but it's good practice to close your
|
120
|
+
producers when you're done with them.
|
121
|
+
|
122
|
+
|
123
|
+
## Consumer
|
124
|
+
|
125
|
+
### Initialization
|
126
|
+
|
127
|
+
| Option | Description | Default |
|
128
|
+
|----------------------|-----------------------------------------------|--------------------|
|
129
|
+
| `topic` | Topic to consume messages from | |
|
130
|
+
| `channel` | Channel name for this consumer | |
|
131
|
+
| `nsqlookupd` | Use lookupd to automatically discover nsqds | |
|
132
|
+
| `nsqd` | Connect directly to a single nsqd instance | '127.0.0.1:4150' |
|
133
|
+
| `max_in_flight` | Max number of messages for this consumer to have in flight at a time | 1 |
|
134
|
+
| `discovery_interval` | Seconds between queue discovery via nsqlookupd | 60.0 |
|
135
|
+
| `msg_timeout` | Milliseconds before nsqd will timeout a message | 60000 |
|
136
|
+
|
137
|
+
|
138
|
+
For example:
|
139
|
+
|
140
|
+
```Ruby
|
141
|
+
consumer = Nsq::Consumer.new(
|
142
|
+
topic: 'the-topic',
|
143
|
+
channel: 'my-channel',
|
144
|
+
nsqlookupd: ['127.0.0.1:4161', '4.5.6.7:4161'],
|
145
|
+
max_in_flight: 100,
|
146
|
+
discovery_interval: 30,
|
147
|
+
msq_timeout: 120_000
|
148
|
+
)
|
149
|
+
```
|
150
|
+
|
151
|
+
Notes:
|
152
|
+
|
153
|
+
- `nsqlookupd` can be a string or array of strings for each nsqlookupd service
|
154
|
+
you'd like to use. The format is `"<host>:<http-port>"`. If you specify
|
155
|
+
`nsqlookupd`, it ignores the `nsqd` option.
|
156
|
+
- `max_in_flight` is for the total max in flight across all the connections,
|
157
|
+
but to make the implementation of `nsq-ruby` as simple as possible, the minimum
|
158
|
+
`max_in_flight` _per_ connection is 1. So if you set `max_in_flight` to 1 and
|
159
|
+
are connected to 3 nsqds, you may have up to 3 messages in flight at a time.
|
160
|
+
|
161
|
+
|
162
|
+
### `#pop`
|
163
|
+
|
164
|
+
`nsq-ruby` works by maintaining a local queue of in flight messages from NSQ.
|
165
|
+
To get at these messages, just call pop.
|
166
|
+
|
167
|
+
```Ruby
|
168
|
+
message = consumer.pop
|
169
|
+
```
|
170
|
+
|
171
|
+
If there are messages on the queue, `pop` will return one immediately. If there
|
172
|
+
are no messages on the queue, `pop` will block execution until one arrives.
|
173
|
+
|
174
|
+
|
175
|
+
### `#size`
|
176
|
+
|
177
|
+
`size` returns the size of the local message queue.
|
178
|
+
|
179
|
+
|
180
|
+
### `#terminate`
|
181
|
+
|
182
|
+
Gracefully closes all connections and stops the consumer. You should call this
|
183
|
+
when you're finished with a consumer object.
|
184
|
+
|
185
|
+
|
186
|
+
## Message
|
187
|
+
|
188
|
+
The `Message` object is what you get when you call `pop` on a consumer.
|
189
|
+
Once you have a message, you'll likely want to get its contents using the `#body`
|
190
|
+
method, and then call `#finish` once you're done with it.
|
191
|
+
|
192
|
+
### `body`
|
193
|
+
|
194
|
+
Returns the body of the message as a UTF-8 encoded string.
|
195
|
+
|
196
|
+
### `attempts`
|
197
|
+
|
198
|
+
Returns the number of times this message was attempted to be processed. For
|
199
|
+
most messages this should be 1 (since it will be your first attempt processing
|
200
|
+
them). If it's more than 1, that means that you requeued the message or it
|
201
|
+
timed out in flight.
|
202
|
+
|
203
|
+
### `timestamp`
|
204
|
+
|
205
|
+
Returns the time this message was originally sent to NSQ as a `Time` object.
|
206
|
+
|
207
|
+
### `#finish`
|
208
|
+
|
209
|
+
Notify NSQ that you've completed processing of this message.
|
210
|
+
|
211
|
+
### `#touch`
|
212
|
+
|
213
|
+
Tells NSQ to reset the message timeout for this message so you have more time
|
214
|
+
to process it.
|
215
|
+
|
216
|
+
### `#requeue(timeout = 0)`
|
217
|
+
|
218
|
+
Tells NSQ to requeue this message. Called with no arguments, this will requeue
|
219
|
+
the message and it will be available to be received immediately.
|
220
|
+
|
221
|
+
Optionally you can pass a number of milliseconds as an argument. This tells
|
222
|
+
NSQ to delay its requeueing by that number of milliseconds.
|
223
|
+
|
224
|
+
|
225
|
+
## Logging
|
226
|
+
|
227
|
+
By default, `nsq-ruby` doesn't log anything. To enable logging, use
|
228
|
+
`Nsq.logger=` and point it at a Ruby Logger instance. Like this:
|
229
|
+
|
230
|
+
```Ruby
|
231
|
+
Nsq.logger = Logger.new(STDOUT)
|
232
|
+
```
|
233
|
+
|
234
|
+
|
235
|
+
## Requirements
|
236
|
+
|
237
|
+
NSQ v0.2.29 or later for IDENTIFY metadata specification (0.2.28) and per-
|
238
|
+
connection timeout support (0.2.29).
|
239
|
+
|
240
|
+
|
241
|
+
### Supports
|
242
|
+
|
243
|
+
- Discovery via nsqlookupd
|
244
|
+
- Automatic reconnection to nsqd
|
245
|
+
- Producing to all nsqd instances automatically via nsqlookupd
|
246
|
+
|
247
|
+
|
248
|
+
### Does not support
|
249
|
+
|
250
|
+
- TLS
|
251
|
+
- Compression
|
252
|
+
- Backoff
|
253
|
+
- Authentication
|
254
|
+
|
255
|
+
If you need more advanced features, like these, you should check out
|
256
|
+
[Krakow](https://github.com/chrisroberts/krakow), a more fully featured NSQ
|
257
|
+
client for Ruby.
|
258
|
+
|
259
|
+
|
260
|
+
## Testing
|
261
|
+
|
262
|
+
Run the tests like this:
|
263
|
+
|
264
|
+
```
|
265
|
+
rake spec
|
266
|
+
```
|
267
|
+
|
268
|
+
Want a deluge of logging while running the specs to help determine what is
|
269
|
+
going on?
|
270
|
+
|
271
|
+
```
|
272
|
+
VERBOSE=true rake spec
|
273
|
+
```
|
274
|
+
|
275
|
+
|
276
|
+
## Is this production ready?
|
277
|
+
|
278
|
+
Yes! It's used in several critical parts of our infrastructure at
|
279
|
+
[Wistia](http://wistia.com) and currently produces and consumes tens of
|
280
|
+
millions of messages a day.
|
281
|
+
|
282
|
+
|
283
|
+
## Authors
|
284
|
+
|
285
|
+
- Robby Grossman (@freerobby)
|
286
|
+
- Brendan Schwartz (@bschwartz)
|
287
|
+
- Marshall Moutenot (@mmoutenot)
|
288
|
+
|
289
|
+
|
290
|
+
## MIT License
|
291
|
+
|
292
|
+
Copyright (C) 2014 Wistia, Inc.
|
293
|
+
|
294
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
295
|
+
this software and associated documentation files (the "Software"), to deal in
|
296
|
+
the Software without restriction, including without limitation the rights to
|
297
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
298
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
299
|
+
so, subject to the following conditions:
|
300
|
+
|
301
|
+
The above copyright notice and this permission notice shall be included in all
|
302
|
+
copies or substantial portions of the Software.
|
303
|
+
|
304
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
305
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
306
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
307
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
308
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
309
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
310
|
+
SOFTWARE.
|
311
|
+
|
data/lib/nsq.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'version'
|
2
|
+
|
3
|
+
require_relative 'nsq/logger'
|
4
|
+
|
5
|
+
require_relative 'nsq/exceptions'
|
6
|
+
|
7
|
+
require_relative 'nsq/frames/frame'
|
8
|
+
require_relative 'nsq/frames/error'
|
9
|
+
require_relative 'nsq/frames/response'
|
10
|
+
require_relative 'nsq/frames/message'
|
11
|
+
|
12
|
+
require_relative 'nsq/consumer'
|
13
|
+
require_relative 'nsq/producer'
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require_relative 'discovery'
|
2
|
+
require_relative 'connection'
|
3
|
+
require_relative 'logger'
|
4
|
+
|
5
|
+
module Nsq
|
6
|
+
class ClientBase
|
7
|
+
include Nsq::AttributeLogger
|
8
|
+
@@log_attributes = [:topic]
|
9
|
+
|
10
|
+
attr_reader :topic
|
11
|
+
attr_reader :connections
|
12
|
+
|
13
|
+
def connected?
|
14
|
+
@connections.values.any?(&:connected?)
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def terminate
|
19
|
+
@discovery_thread.kill if @discovery_thread
|
20
|
+
drop_all_connections
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# discovers nsqds from an nsqlookupd repeatedly
|
27
|
+
#
|
28
|
+
# opts:
|
29
|
+
# nsqlookups: ['127.0.0.1:4161'],
|
30
|
+
# topic: 'topic-to-find-nsqds-for',
|
31
|
+
# interval: 60
|
32
|
+
#
|
33
|
+
def discover_repeatedly(opts = {})
|
34
|
+
@discovery_thread = Thread.new do
|
35
|
+
|
36
|
+
@discovery = Discovery.new(opts[:nsqlookupds])
|
37
|
+
|
38
|
+
loop do
|
39
|
+
begin
|
40
|
+
nsqds = nsqds_from_lookupd(opts[:topic])
|
41
|
+
drop_and_add_connections(nsqds)
|
42
|
+
rescue DiscoveryException
|
43
|
+
# We can't connect to any nsqlookupds. That's okay, we'll just
|
44
|
+
# leave our current nsqd connections alone and try again later.
|
45
|
+
warn 'Could not connect to any nsqlookupd instances in discovery loop'
|
46
|
+
end
|
47
|
+
sleep opts[:interval]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
@discovery_thread.abort_on_exception = true
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def nsqds_from_lookupd(topic = nil)
|
57
|
+
if topic
|
58
|
+
@discovery.nsqds_for_topic(topic)
|
59
|
+
else
|
60
|
+
@discovery.nsqds
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def drop_and_add_connections(nsqds)
|
66
|
+
# drop nsqd connections that are no longer in lookupd
|
67
|
+
missing_nsqds = @connections.keys - nsqds
|
68
|
+
missing_nsqds.each do |nsqd|
|
69
|
+
drop_connection(nsqd)
|
70
|
+
end
|
71
|
+
|
72
|
+
# add new ones
|
73
|
+
new_nsqds = nsqds - @connections.keys
|
74
|
+
new_nsqds.each do |nsqd|
|
75
|
+
begin
|
76
|
+
add_connection(nsqd)
|
77
|
+
rescue Exception => ex
|
78
|
+
error "Failed to connect to nsqd @ #{nsqd}: #{ex}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# balance RDY state amongst the connections
|
83
|
+
connections_changed
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def add_connection(nsqd, options = {})
|
88
|
+
info "+ Adding connection #{nsqd}"
|
89
|
+
host, port = nsqd.split(':')
|
90
|
+
connection = Connection.new({
|
91
|
+
host: host,
|
92
|
+
port: port
|
93
|
+
}.merge(options))
|
94
|
+
@connections[nsqd] = connection
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def drop_connection(nsqd)
|
99
|
+
info "- Dropping connection #{nsqd}"
|
100
|
+
connection = @connections.delete(nsqd)
|
101
|
+
connection.close if connection
|
102
|
+
connections_changed
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def drop_all_connections
|
107
|
+
@connections.keys.each do |nsqd|
|
108
|
+
drop_connection(nsqd)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# optional subclass hook
|
114
|
+
def connections_changed
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,402 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'socket'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
require_relative 'frames/error'
|
6
|
+
require_relative 'frames/message'
|
7
|
+
require_relative 'frames/response'
|
8
|
+
require_relative 'logger'
|
9
|
+
|
10
|
+
module Nsq
|
11
|
+
class Connection
|
12
|
+
include Nsq::AttributeLogger
|
13
|
+
@@log_attributes = [:host, :port]
|
14
|
+
|
15
|
+
attr_reader :host
|
16
|
+
attr_reader :port
|
17
|
+
attr_accessor :max_in_flight
|
18
|
+
attr_reader :presumed_in_flight
|
19
|
+
|
20
|
+
USER_AGENT = "nsq-ruby/#{Nsq::Version::STRING}"
|
21
|
+
RESPONSE_HEARTBEAT = '_heartbeat_'
|
22
|
+
RESPONSE_OK = 'OK'
|
23
|
+
|
24
|
+
|
25
|
+
def initialize(opts = {})
|
26
|
+
@host = opts[:host] || (raise ArgumentError, 'host is required')
|
27
|
+
@port = opts[:port] || (raise ArgumentError, 'host is required')
|
28
|
+
@queue = opts[:queue]
|
29
|
+
@topic = opts[:topic]
|
30
|
+
@channel = opts[:channel]
|
31
|
+
@msg_timeout = opts[:msg_timeout] || 60_000 # 60s
|
32
|
+
@max_in_flight = opts[:max_in_flight] || 1
|
33
|
+
|
34
|
+
if @msg_timeout < 1000
|
35
|
+
raise ArgumentError, 'msg_timeout cannot be less than 1000. it\'s in milliseconds.'
|
36
|
+
end
|
37
|
+
|
38
|
+
# for outgoing communication
|
39
|
+
@write_queue = Queue.new
|
40
|
+
|
41
|
+
# For indicating that the connection has died.
|
42
|
+
# We use a Queue so we don't have to poll. Used to communicate across
|
43
|
+
# threads (from write_loop and read_loop to connect_and_monitor).
|
44
|
+
@death_queue = Queue.new
|
45
|
+
|
46
|
+
@connected = false
|
47
|
+
@presumed_in_flight = 0
|
48
|
+
|
49
|
+
open_connection
|
50
|
+
start_monitoring_connection
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def connected?
|
55
|
+
@connected
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# close the connection and don't try to re-open it
|
60
|
+
def close
|
61
|
+
stop_monitoring_connection
|
62
|
+
close_connection
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def sub(topic, channel)
|
67
|
+
write "SUB #{topic} #{channel}\n"
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def rdy(count)
|
72
|
+
write "RDY #{count}\n"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def fin(message_id)
|
77
|
+
write "FIN #{message_id}\n"
|
78
|
+
decrement_in_flight
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def req(message_id, timeout)
|
83
|
+
write "REQ #{message_id} #{timeout}\n"
|
84
|
+
decrement_in_flight
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def touch(message_id)
|
89
|
+
write "TOUCH #{message_id}\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def pub(topic, message)
|
94
|
+
write ["PUB #{topic}\n", message.bytesize, message].pack('a*Na*')
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def mpub(topic, messages)
|
99
|
+
body = messages.map do |message|
|
100
|
+
[message.bytesize, message].pack('Na*')
|
101
|
+
end.join
|
102
|
+
|
103
|
+
write ["MPUB #{topic}\n", body.bytesize, messages.size, body].pack('a*NNa*')
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
# Tell the server we are ready for more messages!
|
108
|
+
def re_up_ready
|
109
|
+
rdy(@max_in_flight)
|
110
|
+
# assume these messages are coming our way. yes, this might not be the
|
111
|
+
# case, but it's much easier to manage our RDY state with the server if
|
112
|
+
# we treat things this way.
|
113
|
+
@presumed_in_flight = @max_in_flight
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def cls
|
120
|
+
write "CLS\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
def nop
|
125
|
+
write "NOP\n"
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def write(raw)
|
130
|
+
@write_queue.push(raw)
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def write_to_socket(raw)
|
135
|
+
debug ">>> #{raw.inspect}"
|
136
|
+
@socket.write(raw)
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def identify
|
141
|
+
hostname = Socket.gethostname
|
142
|
+
metadata = {
|
143
|
+
client_id: Socket.gethostbyname(hostname).flatten.compact.first,
|
144
|
+
hostname: hostname,
|
145
|
+
feature_negotiation: true,
|
146
|
+
heartbeat_interval: 30_000, # 30 seconds
|
147
|
+
output_buffer: 16_000, # 16kb
|
148
|
+
output_buffer_timeout: 250, # 250ms
|
149
|
+
tls_v1: false,
|
150
|
+
snappy: false,
|
151
|
+
deflate: false,
|
152
|
+
sample_rate: 0, # disable sampling
|
153
|
+
user_agent: USER_AGENT,
|
154
|
+
msg_timeout: @msg_timeout
|
155
|
+
}.to_json
|
156
|
+
write_to_socket ["IDENTIFY\n", metadata.length, metadata].pack('a*Na*')
|
157
|
+
|
158
|
+
# Now wait for the response!
|
159
|
+
frame = receive_frame
|
160
|
+
server = JSON.parse(frame.data)
|
161
|
+
|
162
|
+
if @max_in_flight > server['max_rdy_count']
|
163
|
+
raise "max_in_flight is set to #{@max_in_flight}, server only supports #{server['max_rdy_count']}"
|
164
|
+
end
|
165
|
+
|
166
|
+
@server_version = server['version']
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
def handle_response(frame)
|
171
|
+
if frame.data == RESPONSE_HEARTBEAT
|
172
|
+
debug 'Received heartbeat'
|
173
|
+
nop
|
174
|
+
elsif frame.data == RESPONSE_OK
|
175
|
+
debug 'Received OK'
|
176
|
+
else
|
177
|
+
die "Received response we don't know how to handle: #{frame.data}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
def receive_frame
|
183
|
+
if buffer = @socket.read(8)
|
184
|
+
size, type = buffer.unpack('NN')
|
185
|
+
size -= 4 # we want the size of the data part and type already took up 4 bytes
|
186
|
+
data = @socket.read(size)
|
187
|
+
frame_class = frame_class_for_type(type)
|
188
|
+
return frame_class.new(data, self)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
FRAME_CLASSES = [Response, Error, Message]
|
194
|
+
def frame_class_for_type(type)
|
195
|
+
raise "Bad frame type specified: #{type}" if type > FRAME_CLASSES.length - 1
|
196
|
+
[Response, Error, Message][type]
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
def decrement_in_flight
|
201
|
+
@presumed_in_flight -= 1
|
202
|
+
|
203
|
+
if server_needs_rdy_re_ups?
|
204
|
+
# now that we're less than @max_in_flight we might need to re-up our RDY state
|
205
|
+
threshold = (@max_in_flight * 0.2).ceil
|
206
|
+
re_up_ready if @presumed_in_flight <= threshold
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
def start_read_loop
|
212
|
+
@read_loop_thread ||= Thread.new{read_loop}
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
def stop_read_loop
|
217
|
+
@read_loop_thread.kill if @read_loop_thread
|
218
|
+
@read_loop_thread = nil
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
def read_loop
|
223
|
+
loop do
|
224
|
+
frame = receive_frame
|
225
|
+
if frame.is_a?(Response)
|
226
|
+
handle_response(frame)
|
227
|
+
elsif frame.is_a?(Error)
|
228
|
+
error "Error received: #{frame.data}"
|
229
|
+
elsif frame.is_a?(Message)
|
230
|
+
debug "<<< #{frame.body}"
|
231
|
+
@queue.push(frame) if @queue
|
232
|
+
else
|
233
|
+
raise 'No data from socket'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
rescue Exception => ex
|
237
|
+
die(ex)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
def start_write_loop
|
242
|
+
@write_loop_thread ||= Thread.new{write_loop}
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
def stop_write_loop
|
247
|
+
@stop_write_loop = true
|
248
|
+
@write_loop_thread.join(1) if @write_loop_thread
|
249
|
+
@write_loop_thread = nil
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
def write_loop
|
254
|
+
@stop_write_loop = false
|
255
|
+
data = nil
|
256
|
+
loop do
|
257
|
+
data = @write_queue.pop
|
258
|
+
write_to_socket(data)
|
259
|
+
break if @stop_write_loop && @write_queue.size == 0
|
260
|
+
end
|
261
|
+
rescue Exception => ex
|
262
|
+
# requeue PUB and MPUB commands
|
263
|
+
if data =~ /^M?PUB/
|
264
|
+
debug "Requeueing to write_queue: #{data.inspect}"
|
265
|
+
@write_queue.push(data)
|
266
|
+
end
|
267
|
+
die(ex)
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
# Waits for death of connection
|
272
|
+
def start_monitoring_connection
|
273
|
+
@connection_monitor_thread ||= Thread.new{monitor_connection}
|
274
|
+
@connection_monitor_thread.abort_on_exception = true
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
def stop_monitoring_connection
|
279
|
+
@connection_monitor_thread.kill if @connection_monitor_thread
|
280
|
+
@connection_monitor = nil
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
def monitor_connection
|
285
|
+
loop do
|
286
|
+
# wait for death, hopefully it never comes
|
287
|
+
cause_of_death = @death_queue.pop
|
288
|
+
warn "Died from: #{cause_of_death}"
|
289
|
+
|
290
|
+
debug 'Reconnecting...'
|
291
|
+
reconnect
|
292
|
+
debug 'Reconnected!'
|
293
|
+
|
294
|
+
# clear all death messages, since we're now reconnected.
|
295
|
+
# we don't want to complete this loop and immediately reconnect again.
|
296
|
+
@death_queue.clear
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
# close the connection if it's not already closed and try to reconnect
|
302
|
+
# over and over until we succeed!
|
303
|
+
def reconnect
|
304
|
+
close_connection
|
305
|
+
with_retries do
|
306
|
+
open_connection
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
def open_connection
|
312
|
+
@socket = TCPSocket.new(@host, @port)
|
313
|
+
# write the version and IDENTIFY directly to the socket to make sure
|
314
|
+
# it gets to nsqd ahead of anything in the `@write_queue`
|
315
|
+
write_to_socket ' V2'
|
316
|
+
identify
|
317
|
+
|
318
|
+
start_read_loop
|
319
|
+
start_write_loop
|
320
|
+
@connected = true
|
321
|
+
|
322
|
+
# we need to re-subscribe if there's a topic specified
|
323
|
+
if @topic
|
324
|
+
debug "Subscribing to #{@topic}"
|
325
|
+
sub(@topic, @channel)
|
326
|
+
re_up_ready
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
|
331
|
+
# closes the connection and stops listening for messages
|
332
|
+
def close_connection
|
333
|
+
cls if connected?
|
334
|
+
stop_read_loop
|
335
|
+
stop_write_loop
|
336
|
+
@socket = nil
|
337
|
+
@connected = false
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
# this is called when there's a connection error in the read or write loop
|
342
|
+
# it triggers `connect_and_monitor` to try to reconnect
|
343
|
+
def die(reason)
|
344
|
+
@connected = false
|
345
|
+
@death_queue.push(reason)
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
# Retry the supplied block with exponential backoff.
|
350
|
+
#
|
351
|
+
# Borrowed liberally from:
|
352
|
+
# https://github.com/ooyala/retries/blob/master/lib/retries.rb
|
353
|
+
def with_retries(&block)
|
354
|
+
base_sleep_seconds = 0.5
|
355
|
+
max_sleep_seconds = 300 # 5 minutes
|
356
|
+
|
357
|
+
# Let's do this thing
|
358
|
+
attempts = 0
|
359
|
+
start_time = Time.now
|
360
|
+
|
361
|
+
begin
|
362
|
+
attempts += 1
|
363
|
+
return block.call(attempts)
|
364
|
+
|
365
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
|
366
|
+
Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ETIMEDOUT, Timeout::Error => ex
|
367
|
+
|
368
|
+
raise ex if attempts >= 100
|
369
|
+
|
370
|
+
# The sleep time is an exponentially-increasing function of base_sleep_seconds.
|
371
|
+
# But, it never exceeds max_sleep_seconds.
|
372
|
+
sleep_seconds = [base_sleep_seconds * (2 ** (attempts - 1)), max_sleep_seconds].min
|
373
|
+
# Randomize to a random value in the range sleep_seconds/2 .. sleep_seconds
|
374
|
+
sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
|
375
|
+
# But never sleep less than base_sleep_seconds
|
376
|
+
sleep_seconds = [base_sleep_seconds, sleep_seconds].max
|
377
|
+
|
378
|
+
warn "Failed to connect: #{ex}. Retrying in #{sleep_seconds.round(1)} seconds."
|
379
|
+
|
380
|
+
snooze(sleep_seconds)
|
381
|
+
|
382
|
+
retry
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
# Se we can stub for testing and reconnect in a tight loop
|
388
|
+
def snooze(t)
|
389
|
+
sleep(t)
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
def server_needs_rdy_re_ups?
|
394
|
+
# versions less than 0.3.0 need RDY re-ups
|
395
|
+
# see: https://github.com/bitly/nsq/blob/master/ChangeLog.md#030---2014-11-18
|
396
|
+
major, minor, patch = @server_version.split('.').map(&:to_i)
|
397
|
+
major == 0 && minor <= 2
|
398
|
+
end
|
399
|
+
|
400
|
+
|
401
|
+
end
|
402
|
+
end
|
data/lib/nsq/consumer.rb
ADDED
@@ -0,0 +1,84 @@
|
|
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
|
+
|
21
|
+
# This is where we queue up the messages we receive from each connection
|
22
|
+
@messages = opts[:queue] || Queue.new
|
23
|
+
|
24
|
+
# This is where we keep a record of our active nsqd connections
|
25
|
+
# The key is a string with the host and port of the instance (e.g.
|
26
|
+
# '127.0.0.1:4150') and the key is the Connection instance.
|
27
|
+
@connections = {}
|
28
|
+
|
29
|
+
if !@nsqlookupds.empty?
|
30
|
+
discover_repeatedly(
|
31
|
+
nsqlookupds: @nsqlookupds,
|
32
|
+
topic: @topic,
|
33
|
+
interval: @discovery_interval
|
34
|
+
)
|
35
|
+
else
|
36
|
+
# normally, we find nsqd instances to connect to via nsqlookupd(s)
|
37
|
+
# in this case let's connect to an nsqd instance directly
|
38
|
+
add_connection(opts[:nsqd] || '127.0.0.1:4150', max_in_flight: @max_in_flight)
|
39
|
+
end
|
40
|
+
|
41
|
+
at_exit{terminate}
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# pop the next message off the queue
|
46
|
+
def pop
|
47
|
+
@messages.pop
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# returns the number of messages we have locally in the queue
|
52
|
+
def size
|
53
|
+
@messages.size
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
private
|
58
|
+
def add_connection(nsqd, options = {})
|
59
|
+
super(nsqd, {
|
60
|
+
topic: @topic,
|
61
|
+
channel: @channel,
|
62
|
+
queue: @messages,
|
63
|
+
msg_timeout: @msg_timeout,
|
64
|
+
max_in_flight: 1
|
65
|
+
}.merge(options))
|
66
|
+
end
|
67
|
+
|
68
|
+
# Be conservative, but don't set a connection's max_in_flight below 1
|
69
|
+
def max_in_flight_per_connection(number_of_connections = @connections.length)
|
70
|
+
[@max_in_flight / number_of_connections, 1].max
|
71
|
+
end
|
72
|
+
|
73
|
+
def connections_changed
|
74
|
+
redistribute_ready
|
75
|
+
end
|
76
|
+
|
77
|
+
def redistribute_ready
|
78
|
+
@connections.values.each do |connection|
|
79
|
+
connection.max_in_flight = max_in_flight_per_connection
|
80
|
+
connection.re_up_ready
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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=#{topic}"
|
74
|
+
else
|
75
|
+
uri.path = '/nodes'
|
76
|
+
end
|
77
|
+
|
78
|
+
begin
|
79
|
+
body = Net::HTTP.get(uri)
|
80
|
+
data = JSON.parse(body)
|
81
|
+
|
82
|
+
if data['data'] && data['data']['producers']
|
83
|
+
data['data']['producers'].map do |producer|
|
84
|
+
"#{producer['broadcast_address']}:#{producer['tcp_port']}"
|
85
|
+
end
|
86
|
+
else
|
87
|
+
[]
|
88
|
+
end
|
89
|
+
rescue Exception => e
|
90
|
+
error "Error during discovery for #{lookupd}: #{e}"
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
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,34 @@
|
|
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
|
+
ts_1, ts_2, @attempts, @id, @body = @data.unpack('NNs>a16a*')
|
13
|
+
@timestamp_in_nanoseconds = (ts_1 * (2**32)) + ts_2
|
14
|
+
@body.force_encoding('UTF-8')
|
15
|
+
end
|
16
|
+
|
17
|
+
def finish
|
18
|
+
connection.fin(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def requeue(timeout = 0)
|
22
|
+
connection.req(id, timeout)
|
23
|
+
end
|
24
|
+
|
25
|
+
def touch
|
26
|
+
connection.touch(id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def timestamp
|
30
|
+
Time.at(@timestamp_in_nanoseconds / 1_000_000_000.0)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
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,63 @@
|
|
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] || raise(ArgumentError, 'topic is required')
|
10
|
+
@discovery_interval = opts[:discovery_interval] || 60
|
11
|
+
|
12
|
+
nsqlookupds = []
|
13
|
+
if opts[:nsqlookupd]
|
14
|
+
nsqlookupds = [opts[:nsqlookupd]].flatten
|
15
|
+
discover_repeatedly(
|
16
|
+
nsqlookupds: nsqlookupds,
|
17
|
+
interval: @discovery_interval
|
18
|
+
)
|
19
|
+
|
20
|
+
elsif opts[:nsqd]
|
21
|
+
nsqds = [opts[:nsqd]].flatten
|
22
|
+
nsqds.each{|d| add_connection(d)}
|
23
|
+
|
24
|
+
else
|
25
|
+
add_connection('127.0.0.1:4150')
|
26
|
+
end
|
27
|
+
|
28
|
+
at_exit{terminate}
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def write(*raw_messages)
|
33
|
+
# stringify the messages
|
34
|
+
messages = raw_messages.map(&:to_s)
|
35
|
+
|
36
|
+
# get a suitable connection to write to
|
37
|
+
connection = connection_for_write
|
38
|
+
|
39
|
+
if messages.length > 1
|
40
|
+
connection.mpub(@topic, messages)
|
41
|
+
else
|
42
|
+
connection.pub(@topic, messages.first)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
def connection_for_write
|
49
|
+
# Choose a random Connection that's currently connected
|
50
|
+
# Or, if there's nothing connected, just take any random one
|
51
|
+
connections_currently_connected = connections.select{|_,c| c.connected?}
|
52
|
+
connection = connections_currently_connected.values.sample || connections.values.sample
|
53
|
+
|
54
|
+
# Raise an exception if there's no connection available
|
55
|
+
unless connection
|
56
|
+
raise 'No connections available'
|
57
|
+
end
|
58
|
+
|
59
|
+
connection
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nsq-ruby-maglev-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wistia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-14 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: '1.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.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/johnnyt/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.2.2
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Ruby client library for NSQ
|
116
|
+
test_files: []
|