remq 0.0.3 → 0.0.4
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.
- data/.gitignore +0 -1
- data/.gitmodules +1 -1
- data/.travis.yml +9 -0
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/README.md +66 -0
- data/Rakefile +1 -2
- data/examples/consumer.rb +26 -0
- data/examples/producer.rb +19 -0
- data/examples/shared/message.rb +22 -0
- data/lib/remq.rb +79 -10
- data/lib/remq/version.rb +1 -1
- data/spec/remq_spec.rb +26 -11
- data/vendor/bundle/gems/multi_json-1.6.1/Gemfile +31 -0
- metadata +8 -2
- data/Readme.md +0 -63
data/.gitignore
CHANGED
data/.gitmodules
CHANGED
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# remq-rb [![Build Status][travis-image]][travis-link]
|
2
|
+
|
3
|
+
[travis-image]: https://secure.travis-ci.org/kainosnoema/remq-rb.png?branch=master
|
4
|
+
[travis-link]: http://travis-ci.org/kainosnoema/remq-rb
|
5
|
+
|
6
|
+
A Ruby client for [Remq](https://github.com/kainosnoema/remq), a
|
7
|
+
[Redis](http://redis.io)-based protocol for building fast, durable message
|
8
|
+
queues.
|
9
|
+
|
10
|
+
**WARNING**: In early-stage development, API not stable. If you've used a
|
11
|
+
previous version, you'll most likely have to clear all previously published
|
12
|
+
messages in order to upgrade to the latest version.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
``` sh
|
17
|
+
gem install remq
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
**Producer:**
|
23
|
+
|
24
|
+
``` rb
|
25
|
+
require 'json'
|
26
|
+
require 'remq'
|
27
|
+
|
28
|
+
$remq = Remq.new
|
29
|
+
|
30
|
+
message = { event: 'signup', account_id: 694 }
|
31
|
+
|
32
|
+
id = $remq.publish('events.accounts', JSON.dump(message))
|
33
|
+
```
|
34
|
+
|
35
|
+
**Consumer:**
|
36
|
+
|
37
|
+
``` rb
|
38
|
+
require 'json'
|
39
|
+
require 'remq'
|
40
|
+
|
41
|
+
$remq = Remq.new
|
42
|
+
|
43
|
+
last_id_key = $remq.key('cursor', 'consumer-1')
|
44
|
+
last_id = $remq.redis.get(last_id_key) || 0
|
45
|
+
|
46
|
+
$remq.subscribe('events.*', from_id: last_id) do |channel, message|
|
47
|
+
last_id = message.id
|
48
|
+
|
49
|
+
# by persisting the last_id every 10 messages, a maximum of
|
50
|
+
# 10 messages will be replayed in the case of consumer failure
|
51
|
+
if last_id % 10 == 0
|
52
|
+
$remq.redis.set(last_id_key, last_id)
|
53
|
+
end
|
54
|
+
|
55
|
+
message.body = JSON.parse(message.body)
|
56
|
+
|
57
|
+
puts "Received message on '#{channel}' with id: #{message.id}"
|
58
|
+
puts "Account signed up with id: #{message.body['account_id']}"
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
**Flush:**
|
63
|
+
|
64
|
+
``` rb
|
65
|
+
# TODO: not implemented yet
|
66
|
+
```
|
data/Rakefile
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'remq'
|
5
|
+
|
6
|
+
$remq = Remq.new
|
7
|
+
|
8
|
+
last_id_key = $remq.key('cursor', 'consumer-1')
|
9
|
+
last_id = $remq.redis.get(last_id_key) || 0
|
10
|
+
|
11
|
+
$remq.on(:message) do |channel, message|
|
12
|
+
last_id = message.id
|
13
|
+
|
14
|
+
# by persisting the last_id every 10 messages, a maximum of
|
15
|
+
# 10 messages will be replayed in the case of consumer failure
|
16
|
+
if last_id % 10 == 0
|
17
|
+
$remq.redis.set last_id_key, last_id
|
18
|
+
end
|
19
|
+
|
20
|
+
message.body = JSON.parse(message.body)
|
21
|
+
puts message.inspect
|
22
|
+
end
|
23
|
+
|
24
|
+
thread = $remq.subscribe('events.*', from_id: last_id)
|
25
|
+
|
26
|
+
thread.join
|
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
$LOAD_PATH.unshift 'examples/shared'
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'remq'
|
6
|
+
require 'message'
|
7
|
+
|
8
|
+
$remq = Remq.new
|
9
|
+
|
10
|
+
def publish message
|
11
|
+
channel = "events.#{message.type.downcase}.#{message.event}"
|
12
|
+
id = $remq.publish(channel, JSON.dump(message.attributes))
|
13
|
+
puts "Published ##{id} to channel '#{channel}'"
|
14
|
+
end
|
15
|
+
|
16
|
+
loop do
|
17
|
+
publish Message.new
|
18
|
+
sleep 0.15
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Message
|
2
|
+
EVENTS = %w(create update delete)
|
3
|
+
TYPES = %w(Account Subscription Transaction)
|
4
|
+
|
5
|
+
def self.id
|
6
|
+
@id ||= 0
|
7
|
+
@id += 1
|
8
|
+
end
|
9
|
+
|
10
|
+
attr :event, :type, :attributes
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@event = EVENTS.sample
|
14
|
+
@type = TYPES.sample
|
15
|
+
@attributes = {
|
16
|
+
account_id: self.class.id,
|
17
|
+
first_name: 'Evan',
|
18
|
+
last_name: 'Owen',
|
19
|
+
state: 'active'
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
data/lib/remq.rb
CHANGED
@@ -15,8 +15,12 @@ class Remq
|
|
15
15
|
|
16
16
|
include MonitorMixin
|
17
17
|
|
18
|
-
attr :redis
|
18
|
+
attr :redis
|
19
|
+
attr :predis
|
19
20
|
|
21
|
+
# Create a `Remq` client with the given `options`, which are passed to redis.
|
22
|
+
#
|
23
|
+
# @param [Hash] options
|
20
24
|
def initialize(options = {})
|
21
25
|
@redis = Redis.new(options)
|
22
26
|
@predis = Redis.new(options) # seperate connection for pub/sub
|
@@ -25,6 +29,14 @@ class Remq
|
|
25
29
|
super() # Monitor#initialize
|
26
30
|
end
|
27
31
|
|
32
|
+
# Publish a `message` to the given `channel`. The `message` must be a string,
|
33
|
+
# but objects can easily be serialized using JSON, etc. The id of the
|
34
|
+
# published message will be returned as an integer.
|
35
|
+
#
|
36
|
+
# @param [String] channel
|
37
|
+
# @param [String] message
|
38
|
+
#
|
39
|
+
# @return [Integer] id
|
28
40
|
def publish(channel, message)
|
29
41
|
synchronize do
|
30
42
|
id = call(:publish, channel, message)
|
@@ -32,12 +44,30 @@ class Remq
|
|
32
44
|
end
|
33
45
|
end
|
34
46
|
|
47
|
+
# Subscribe to the channels matching given `pattern`. If no initial `from_id`
|
48
|
+
# is provided, Remq subscribes using vanilla Redis pub/sub. Any Redis pub/sub
|
49
|
+
# pattern will work. If `from_id` is provided, Remq replays messages after the
|
50
|
+
# given id until its caught up and able to switch to pub/sub.
|
51
|
+
#
|
52
|
+
# Remq-rb subscribes to pub/sub on another thread, which is returned so you
|
53
|
+
# can handle it and call `Thread#join` when ready to block.
|
54
|
+
#
|
55
|
+
# @param [String] pattern
|
56
|
+
# @param [Hash] options
|
57
|
+
# - `:from_id => Integer`: The message id to replay from (usually the last)
|
58
|
+
#
|
59
|
+
# @yield a block to add as a listener to the `message` event
|
60
|
+
# @yieldparam [Remq::Message] received message
|
61
|
+
#
|
62
|
+
# @return [Thread] thread where messages will be received
|
35
63
|
def subscribe(pattern, options = {}, &block)
|
36
64
|
synchronize do
|
37
65
|
return if @subscription
|
38
66
|
|
39
67
|
on(:message, &block) if block
|
40
68
|
|
69
|
+
@subscription = true
|
70
|
+
|
41
71
|
if cursor = options[:from_id]
|
42
72
|
@subscription = _subscribe_from_cursor(pattern, cursor)
|
43
73
|
else
|
@@ -46,14 +76,25 @@ class Remq
|
|
46
76
|
end
|
47
77
|
end
|
48
78
|
|
79
|
+
# Unsubscribe. No more `message` events will be emitted after this is called.
|
49
80
|
def unsubscribe
|
50
81
|
synchronize do
|
51
82
|
return unless @subscription
|
52
|
-
@subscription.exit
|
83
|
+
@subscription.exit if @subscription.is_a?(Thread)
|
53
84
|
@subscription = nil
|
54
85
|
end
|
55
86
|
end
|
56
87
|
|
88
|
+
# Consume persisted messages from channels matching the given `pattern`,
|
89
|
+
# starting with the `cursor` if provided, or the first message. `limit`
|
90
|
+
# determines how many messages will be return each time `consume` is called.
|
91
|
+
#
|
92
|
+
# @param [String] pattern
|
93
|
+
# @param [Hash] options
|
94
|
+
# - `:cursor => Integer`: id of the first message to return
|
95
|
+
# - `:limit => Integer`: maximum number of messages to return
|
96
|
+
#
|
97
|
+
# @return [Array<Remq::Message>] array of parsed messages
|
57
98
|
def consume(pattern, options = {})
|
58
99
|
synchronize do
|
59
100
|
cursor, limit = options.fetch(:cursor, 0), options.fetch(:limit, LIMIT)
|
@@ -62,8 +103,24 @@ class Remq
|
|
62
103
|
end
|
63
104
|
end
|
64
105
|
|
65
|
-
|
66
|
-
|
106
|
+
# Forcibly close the connections to the Redis server.
|
107
|
+
def quit
|
108
|
+
synchronize do
|
109
|
+
redis.quit
|
110
|
+
predis.quit
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add a listener to the given event.
|
115
|
+
#
|
116
|
+
# @param [String|Symbol] event
|
117
|
+
# @param [Proc] listener
|
118
|
+
#
|
119
|
+
# @yield a block to be called when the event is emitted
|
120
|
+
#
|
121
|
+
# @return [Remq] self
|
122
|
+
def on(event, listener=nil, &block)
|
123
|
+
listener ||= block
|
67
124
|
unless listener.respond_to?(:call)
|
68
125
|
raise ArgumentError.new('Listener must respond to #call')
|
69
126
|
end
|
@@ -75,6 +132,12 @@ class Remq
|
|
75
132
|
self
|
76
133
|
end
|
77
134
|
|
135
|
+
# Remove a listener from the given event.
|
136
|
+
#
|
137
|
+
# @param [String|Symbol] event
|
138
|
+
# @param [Proc] listener
|
139
|
+
#
|
140
|
+
# @return [Remq] self
|
78
141
|
def off(event, listener)
|
79
142
|
synchronize do
|
80
143
|
@listeners[event.to_sym].delete(listener)
|
@@ -83,6 +146,12 @@ class Remq
|
|
83
146
|
self
|
84
147
|
end
|
85
148
|
|
149
|
+
# Build a key from the given `name` and `channel`.
|
150
|
+
#
|
151
|
+
# @param [String] name
|
152
|
+
# @param [String] channel
|
153
|
+
#
|
154
|
+
# @return [String] key
|
86
155
|
def key(*args)
|
87
156
|
(['remq'] + args).join(':')
|
88
157
|
end
|
@@ -118,7 +187,7 @@ class Remq
|
|
118
187
|
|
119
188
|
def _subscribe_from_cursor(pattern, cursor)
|
120
189
|
begin
|
121
|
-
msgs = consume(pattern, { cursor
|
190
|
+
msgs = consume(pattern, { :cursor => cursor })
|
122
191
|
cursor = msgs.last.id unless msgs.empty?
|
123
192
|
end while msgs.length == LIMIT
|
124
193
|
|
@@ -126,13 +195,13 @@ class Remq
|
|
126
195
|
end
|
127
196
|
|
128
197
|
def _subscribe_to_pubsub(pattern)
|
129
|
-
|
198
|
+
subscribed_thread = nil
|
130
199
|
|
131
200
|
Thread.new do
|
132
201
|
begin
|
133
202
|
predis.client.connect
|
134
203
|
predis.psubscribe(key('channel', pattern)) do |on|
|
135
|
-
on.psubscribe {
|
204
|
+
on.psubscribe { subscribed_thread = Thread.current }
|
136
205
|
on.pmessage { |_, _, msg| _handle_raw_message(msg) }
|
137
206
|
end
|
138
207
|
rescue => e
|
@@ -140,14 +209,14 @@ class Remq
|
|
140
209
|
end
|
141
210
|
end
|
142
211
|
|
143
|
-
Thread.pass while !
|
212
|
+
Thread.pass while !subscribed_thread
|
144
213
|
|
145
|
-
|
214
|
+
subscribed_thread
|
146
215
|
end
|
147
216
|
|
148
217
|
def _handle_raw_message(raw)
|
149
218
|
msg = Message.parse raw
|
150
|
-
emit(:message, msg.channel, msg)
|
219
|
+
emit(:message, msg.channel, msg) if @subscription
|
151
220
|
msg
|
152
221
|
end
|
153
222
|
|
data/lib/remq/version.rb
CHANGED
data/spec/remq_spec.rb
CHANGED
@@ -8,7 +8,7 @@ RSpec::Matchers.define :be_ordered_from do |cursor|
|
|
8
8
|
end
|
9
9
|
|
10
10
|
describe Remq do
|
11
|
-
subject { Remq.new(db
|
11
|
+
subject { Remq.new(:db => 4) }
|
12
12
|
|
13
13
|
let(:channel) { 'events.things' }
|
14
14
|
|
@@ -31,7 +31,7 @@ describe Remq do
|
|
31
31
|
end
|
32
32
|
|
33
33
|
describe '#subscribe' do
|
34
|
-
let(:producer) { Remq.new(db
|
34
|
+
let(:producer) { Remq.new(:db => 4) }
|
35
35
|
|
36
36
|
it 'receives messages on a separate thread' do
|
37
37
|
msgs = []
|
@@ -42,27 +42,42 @@ describe Remq do
|
|
42
42
|
|
43
43
|
publish 3, producer
|
44
44
|
|
45
|
-
thread.join(
|
45
|
+
thread.join(0.01)
|
46
46
|
|
47
47
|
msgs.should have(3).items
|
48
48
|
msgs.should be_ordered_from 0
|
49
49
|
end
|
50
50
|
|
51
51
|
context 'with :from_id option' do
|
52
|
-
it '
|
52
|
+
it 'replays any missed messages' do
|
53
53
|
publish 3, producer
|
54
54
|
|
55
55
|
msgs = []
|
56
|
-
thread = subject.subscribe(channel, from_id
|
56
|
+
thread = subject.subscribe(channel, :from_id => 0) do |channel, msg|
|
57
57
|
msgs << msg
|
58
|
-
subject.unsubscribe if msgs.length ==
|
58
|
+
subject.unsubscribe if msgs.length == 3
|
59
59
|
end
|
60
60
|
|
61
|
-
|
61
|
+
thread.join(0.01)
|
62
|
+
|
63
|
+
msgs.should have(3).items
|
64
|
+
msgs.should be_ordered_from 0
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'doesn\'t miss or repeat when switching to pub/sub' do
|
68
|
+
publish 100, producer
|
69
|
+
|
70
|
+
msgs = []
|
71
|
+
thread = subject.subscribe(channel, :from_id => 0) do |channel, msg|
|
72
|
+
msgs << msg
|
73
|
+
subject.unsubscribe if msgs.length == 200
|
74
|
+
end
|
75
|
+
|
76
|
+
publish 100, producer
|
62
77
|
|
63
|
-
thread.join(1)
|
78
|
+
thread.join(0.1)
|
64
79
|
|
65
|
-
msgs.should have(
|
80
|
+
msgs.should have(200).items
|
66
81
|
msgs.should be_ordered_from 0
|
67
82
|
end
|
68
83
|
end
|
@@ -78,14 +93,14 @@ describe Remq do
|
|
78
93
|
|
79
94
|
it 'limits the messages returned to value given in the :limit option' do
|
80
95
|
publish 3
|
81
|
-
msgs = subject.consume(channel, limit
|
96
|
+
msgs = subject.consume(channel, :limit => 2)
|
82
97
|
msgs.should have(2).items
|
83
98
|
msgs.should be_ordered_from 0
|
84
99
|
end
|
85
100
|
|
86
101
|
it 'returns messages published since the id given in the :cursor option' do
|
87
102
|
cursor = publish(3).first
|
88
|
-
msgs = subject.consume(channel, cursor
|
103
|
+
msgs = subject.consume(channel, :cursor => cursor)
|
89
104
|
msgs.should have(2).items
|
90
105
|
msgs.should be_ordered_from cursor
|
91
106
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'rake', '>= 0.9'
|
4
|
+
gem 'yard', '>= 0.8'
|
5
|
+
|
6
|
+
platforms :ruby_18 do
|
7
|
+
gem 'json', '~> 1.4', :require => nil
|
8
|
+
end
|
9
|
+
|
10
|
+
gem 'json_pure', '~> 1.4', :require => nil
|
11
|
+
|
12
|
+
platforms :ruby, :mswin, :mingw do
|
13
|
+
gem 'oj', '~> 2.0', :require => nil
|
14
|
+
gem 'yajl-ruby', '~> 1.0', :require => nil
|
15
|
+
end
|
16
|
+
platforms :jruby do
|
17
|
+
gem 'gson', '>= 0.6', :require => nil
|
18
|
+
end
|
19
|
+
|
20
|
+
group :development do
|
21
|
+
gem 'kramdown', '>= 0.14'
|
22
|
+
gem 'pry'
|
23
|
+
gem 'pry-debugger', :platforms => :mri_19
|
24
|
+
end
|
25
|
+
|
26
|
+
group :test do
|
27
|
+
gem 'rspec', '>= 2.11'
|
28
|
+
gem 'simplecov', :require => false
|
29
|
+
end
|
30
|
+
|
31
|
+
gemspec
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remq
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -72,9 +72,15 @@ extra_rdoc_files: []
|
|
72
72
|
files:
|
73
73
|
- .gitignore
|
74
74
|
- .gitmodules
|
75
|
+
- .travis.yml
|
76
|
+
- .yardopts
|
77
|
+
- Gemfile
|
75
78
|
- LICENSE
|
79
|
+
- README.md
|
76
80
|
- Rakefile
|
77
|
-
-
|
81
|
+
- examples/consumer.rb
|
82
|
+
- examples/producer.rb
|
83
|
+
- examples/shared/message.rb
|
78
84
|
- lib/remq.rb
|
79
85
|
- lib/remq/script.rb
|
80
86
|
- lib/remq/version.rb
|
data/Readme.md
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# Remq-rb
|
2
|
-
|
3
|
-
A Ruby client library for [Remq](https://github.com/kainosnoema/remq),
|
4
|
-
a [Redis](http://redis.io)-based protocol for building fast, durable
|
5
|
-
message queues.
|
6
|
-
|
7
|
-
NOTE: In early-stage development, API not stable. If you've used a previous
|
8
|
-
version, you'll most likely have to clear all previously published messages
|
9
|
-
in order to upgrade to the latest version.
|
10
|
-
|
11
|
-
## Installation
|
12
|
-
|
13
|
-
``` sh
|
14
|
-
gem install remq --pre
|
15
|
-
```
|
16
|
-
|
17
|
-
## Usage
|
18
|
-
|
19
|
-
**Producer:**
|
20
|
-
|
21
|
-
``` rb
|
22
|
-
require 'json'
|
23
|
-
require 'remq'
|
24
|
-
|
25
|
-
remq = Remq.new
|
26
|
-
|
27
|
-
message = { event: 'signup', account_id: 694 }
|
28
|
-
|
29
|
-
id = remq.publish('events.accounts', JSON.dump(message))
|
30
|
-
```
|
31
|
-
|
32
|
-
**Consumer:**
|
33
|
-
|
34
|
-
``` rb
|
35
|
-
require 'json'
|
36
|
-
require 'remq'
|
37
|
-
|
38
|
-
remq = Remq.new
|
39
|
-
|
40
|
-
last_id_key = remq.key('cursor', 'consumer-1')
|
41
|
-
last_id = remq.redis.get(last_id_key) || 0
|
42
|
-
|
43
|
-
remq.subscribe('events.*', from_id: last_id) do |channel, message|
|
44
|
-
last_id = message.id
|
45
|
-
|
46
|
-
# by persisting the last_id every 10 messages, a maximum of
|
47
|
-
# 10 messages will be replayed in the case of consumer failure
|
48
|
-
if last_id % 10 == 0
|
49
|
-
remq.redis.set(last_id_key, last_id)
|
50
|
-
end
|
51
|
-
|
52
|
-
message.body = JSON.parse(message.body)
|
53
|
-
|
54
|
-
puts "Received message on '#{channel}' with id: #{message.id}"
|
55
|
-
puts "Account signed up with id: #{message.body['account_id']}"
|
56
|
-
end
|
57
|
-
```
|
58
|
-
|
59
|
-
**Flush:**
|
60
|
-
|
61
|
-
``` rb
|
62
|
-
# TODO: not implemented yet
|
63
|
-
```
|