remq 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,5 +1,4 @@
1
1
  *.gem
2
2
  .bundle
3
3
  .config
4
- Gemfile
5
4
  Gemfile.lock
@@ -1,3 +1,3 @@
1
1
  [submodule "vendor/remq"]
2
2
  path = vendor/remq
3
- url = git@github.com:kainosnoema/remq.git
3
+ url = git://github.com/kainosnoema/remq.git
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - 1.8.7
6
+ - jruby-19mode
7
+ - jruby-18mode
8
+ services:
9
+ - redis-server
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -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
@@ -1,8 +1,7 @@
1
- require 'rubygems'
2
1
  require 'bundler'
3
2
  Bundler.setup :default, :test, :development
4
3
 
5
4
  require 'rspec/core/rake_task'
6
5
  RSpec::Core::RakeTask.new(:spec)
7
6
 
8
- task default: :spec
7
+ task :default => :spec
@@ -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
@@ -15,8 +15,12 @@ class Remq
15
15
 
16
16
  include MonitorMixin
17
17
 
18
- attr :redis, :predis
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
- def on(event, proc=nil, &block)
66
- listener = proc || block
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: 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
- subscription = nil
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 { subscription = Thread.current }
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 !subscription
212
+ Thread.pass while !subscribed_thread
144
213
 
145
- subscription
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
 
@@ -1,3 +1,3 @@
1
1
  class Remq
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -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: 4) }
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: 4) }
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(1)
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 'catches up to pub/sub from cursor using consume' do
52
+ it 'replays any missed messages' do
53
53
  publish 3, producer
54
54
 
55
55
  msgs = []
56
- thread = subject.subscribe(channel, from_id: 0) do |channel, msg|
56
+ thread = subject.subscribe(channel, :from_id => 0) do |channel, msg|
57
57
  msgs << msg
58
- subject.unsubscribe if msgs.length == 6
58
+ subject.unsubscribe if msgs.length == 3
59
59
  end
60
60
 
61
- publish 3, producer
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(6).items
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: 2)
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: 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.3
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
- - Readme.md
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
- ```