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 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
- ```