remq 0.0.1a

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ .config
4
+ Gemfile
5
+ Gemfile.lock
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "vendor/remq"]
2
+ path = vendor/remq
3
+ url = git@github.com:kainosnoema/remq.git
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Evan Owen
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,49 @@
1
+ # Remq-rb
2
+
3
+ A Ruby client library for [Remq](https://github.com/kainosnoema/remq), a [Redis](http://redis.io)-based protocol for building fast, persistent pub/sub message queues.
4
+
5
+ NOTE: In early-stage development, API not locked.
6
+
7
+ ## Usage
8
+
9
+ **Producer:**
10
+
11
+ ``` rb
12
+ require 'remq'
13
+
14
+ remq = Remq.new
15
+
16
+ message = { event: 'signup', account_id: 694 }
17
+ id = remq.publish('events.accounts', message)
18
+ ```
19
+
20
+ **Pub/sub consumer (messages lost during failure):**
21
+
22
+ ``` rb
23
+ require 'remq'
24
+
25
+ remq = Remq.new
26
+
27
+ # TODO: not implemented yet
28
+ ```
29
+
30
+ **Polling consumer with cursor (resumes post-failure):**
31
+
32
+ ``` rb
33
+ require 'remq'
34
+
35
+ remq = Remq.new
36
+
37
+ # TODO: not implemented yet
38
+ ```
39
+
40
+ **Purging old messages:**
41
+
42
+ ``` rb
43
+
44
+ require 'remq'
45
+
46
+ remq = Remq.new
47
+
48
+ # TODO: not implemented yet
49
+ ```
data/lib/remq.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'redis'
2
+ require 'remq/script'
3
+ require 'remq/multi_json_coder'
4
+
5
+ class Remq
6
+
7
+ attr :redis, :namespace, :scripts, :coder
8
+
9
+ def initialize(options = {})
10
+ @redis = options[:redis] || Redis.new(options)
11
+ @namespace = 'remq'
12
+ @scripts = {}
13
+ end
14
+
15
+ def publish(channel, message)
16
+ channel = channel.join('.') if channel.is_a?(Array)
17
+ msg, utc_seconds = coder.encode(message), Time.now.to_i
18
+ id = eval_script(:publish, namespace, channel, msg, utc_seconds)
19
+ id.nil? ? nil : id.to_s
20
+ end
21
+
22
+ def consume(channel, options = {})
23
+ cursor, limit = options[:cursor] || 0, options[:limit] || 1000
24
+ msgs_ids = eval_script(:consume, namespace, channel, cursor, limit)
25
+ msgs = {}
26
+ msgs_ids.each_index { |i|
27
+ if i % 2 == 0
28
+ msgs[msgs_ids[i+1]] = coder.decode(msgs_ids[i])
29
+ end
30
+ }
31
+ msgs
32
+ end
33
+
34
+ def coder
35
+ @coder ||= MultiJsonCoder.new
36
+ end
37
+
38
+ protected
39
+
40
+ def eval_script(name, *args)
41
+ script(name).eval(@redis, *args)
42
+ end
43
+
44
+ def script(name)
45
+ @scripts[name.to_sym] ||= Script.new(File.read(script_path(name)))
46
+ end
47
+
48
+ def script_path(name)
49
+ File.expand_path("../vendor/remq/scripts/#{name}.lua", File.dirname(__FILE__))
50
+ end
51
+
52
+ end
data/lib/remq/coder.rb ADDED
@@ -0,0 +1,30 @@
1
+
2
+ # borrowed from Resque::Coder. Thanks @defunkt!
3
+
4
+ class Remq
5
+ class EncodeException < StandardError; end
6
+ class DecodeException < StandardError; end
7
+
8
+ class Coder
9
+ # Given a Ruby object, returns a string suitable for storage in a
10
+ # queue.
11
+ def encode(object)
12
+ raise EncodeException
13
+ end
14
+
15
+ # alias for encode
16
+ def dump(object)
17
+ encode(object)
18
+ end
19
+
20
+ # Given a string, returns a Ruby object.
21
+ def decode(object)
22
+ raise DecodeException
23
+ end
24
+
25
+ # alias for decode
26
+ def load(object)
27
+ decode(object)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ require 'multi_json'
2
+ require 'remq/coder'
3
+
4
+ # borrowed from Resque::MultiJsonCoder. Thanks @defunkt!
5
+
6
+ if MultiJson.respond_to?(:adapter)
7
+ raise "Please install the yajl-ruby or json gem" if MultiJson.adapter.to_s == 'MultiJson::Adapters::OkJson'
8
+ elsif MultiJson.respond_to?(:engine)
9
+ raise "Please install the yajl-ruby or json gem" if MultiJson.engine.to_s == 'MultiJson::Engines::OkJson'
10
+ end
11
+
12
+ class Remq
13
+ class MultiJsonCoder < Coder
14
+ class EncodeException < StandardError; end
15
+ class DecodeException < StandardError; end
16
+
17
+ def encode(object)
18
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
19
+ MultiJson.dump object
20
+ else
21
+ MultiJson.encode object
22
+ end
23
+ end
24
+
25
+ def decode(object)
26
+ return unless object
27
+
28
+ begin
29
+ if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
30
+ MultiJson.load object
31
+ else
32
+ MultiJson.decode object
33
+ end
34
+ rescue ::MultiJson::DecodeError => e
35
+ raise DecodeException, e.message, e.backtrace
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ require 'digest/sha1'
2
+
3
+ class Remq
4
+ class Script
5
+
6
+ attr :source, :sha
7
+
8
+ def initialize(source)
9
+ @source = source
10
+ end
11
+
12
+ def eval(redis, *args)
13
+ redis.evalsha(sha, [], [*args])
14
+ rescue => e
15
+ if e.message =~ /NOSCRIPT/
16
+ redis.eval(source, [], [*args])
17
+ else
18
+ raise
19
+ end
20
+ end
21
+
22
+ def sha
23
+ @sha ||= Digest::SHA1.hexdigest source
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ class Remq
2
+ VERSION = "0.0.1a"
3
+ end
data/remq.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+
3
+ require 'remq/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "remq"
7
+ s.version = Remq::VERSION
8
+ s.date = Time.now.strftime('%Y-%m-%d')
9
+ s.summary = "A Remq client library for Ruby."
10
+ s.homepage = "http://github.com/kainosnoema/remq"
11
+ s.email = "kainosnoema@gmail.com"
12
+ s.authors = [ "Evan Owen" ]
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.files += Dir.glob('vendor/**/*')
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+
18
+ s.add_dependency "redis", "~> 3.0.1"
19
+ s.add_dependency "multi_json", "~> 1.0"
20
+
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "rspec", "~> 2.6"
23
+
24
+ s.description = <<description
25
+ Remq is a Redis-based protocol for building fast, persistent
26
+ pub/sub message queues.
27
+
28
+ The Remq protocol is defined by a collection of Lua scripts
29
+ (located at https://github.com/kainosnoema/remq) which effectively
30
+ turn Redis into a capable message queue broker for fast inter-service
31
+ communication. The Remq Ruby client library is built on top of these
32
+ scripts, making it easy to build fast, persisted pub/sub message queues.
33
+ description
34
+ end
data/spec/remq_spec.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'remq'
2
+
3
+ describe Remq do
4
+
5
+ before :all do
6
+ subject { Remq.new(db: 4).tap { |r| r.redis.flushdb } }
7
+ end
8
+
9
+ after :each do
10
+ subject.redis.flushdb
11
+ end
12
+
13
+ context "class" do
14
+ describe ".new" do
15
+ it "creates a Redis client when options hash missing" do
16
+ Remq.new.redis.should be_a(Redis)
17
+ end
18
+
19
+ it "takes a Redis client as an option" do
20
+ Remq.new(redis: (redis = Redis.new)).redis.should eql redis
21
+ end
22
+ end
23
+ end
24
+
25
+ describe ".publish" do
26
+ it "publishes a message to the given channel and returns an id" do
27
+ id = subject.publish('events.things', { test: 'one' })
28
+ id.should be_a String
29
+ end
30
+ end
31
+
32
+ describe ".consume" do
33
+ it "consumes messages published to the given channel" do
34
+ subject.publish('events.things', { 'test' => 'one' })
35
+ subject.publish('events.things', { 'test' => 'two' })
36
+ subject.publish('events.things', { 'test' => 'three' })
37
+
38
+ msgs = subject.consume('events.things')
39
+ msgs.should have(3).items
40
+ msgs[msgs.keys[0]].should eql({ 'test' => 'one' })
41
+ msgs[msgs.keys[2]].should eql({ 'test' => 'three' })
42
+ end
43
+
44
+ it "limits the messages returned to value given in the :limit option" do
45
+ subject.publish('events.things', { 'test' => 'one' })
46
+ subject.publish('events.things', { 'test' => 'two' })
47
+ subject.publish('events.things', { 'test' => 'three' })
48
+
49
+ msgs = subject.consume('events.things', limit: 2)
50
+ msgs.should have(2).items
51
+ msgs[msgs.keys[0]].should eql({ 'test' => 'one' })
52
+ msgs[msgs.keys[1]].should eql({ 'test' => 'two' })
53
+ msgs[msgs.keys[2]].should be_nil
54
+ end
55
+
56
+ it "returns messages published since the id given in the :cursor option" do
57
+ cursor = subject.publish('events.things', { 'test' => 'one' })
58
+ subject.publish('events.things', { 'test' => 'two' })
59
+ subject.publish('events.things', { 'test' => 'three' })
60
+
61
+ msgs = subject.consume('events.things', cursor: cursor)
62
+ msgs.should have(2).items
63
+ msgs[msgs.keys[0]].should eql({ 'test' => 'two' })
64
+ msgs[msgs.keys[1]].should eql({ 'test' => 'three' })
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ # Remq
2
+
3
+ Remq (pronounced 'rem-que') is two things: (1) A [Redis](http://redis.io)-based protocol defined by a collection of Lua scripts (this project) which effectively turn Redis into a capable message queue broker for fast inter-service communication. (2) Multiple client libraries using these scripts for building fast, persisted pub/sub message queues.
4
+
5
+ - Producers publish any string to a message channel and receive a unique message-id
6
+ - Consumers subscribe to message channels via polling with a cursor (allowing resume), or via Redis pub/sub
7
+ - Consumers can subscribe to multible queues at once using Redis key globbing (ie. `'events.*'`)
8
+ - Able to sustain ~15k messages/sec on loopback interface (1 producer -> 1 consumer)
9
+ - Consistent performance if Redis has enough memory (tested up to ~15m messages, 3GB in memory)
10
+ - Purge channels of old messages periodically to maintain performance
11
+
12
+ NOTE: In early-stage development, API not locked.
13
+
14
+ ## Client Libraries
15
+
16
+ - Node.js: [remq-node](https://github.com/kainosnoema/remq-node) (`npm install remq`)
17
+ - Ruby: [remq-rb](https://github.com/kainosnoema/remq-rb) (`gem install remq`)
18
+
19
+ ## Usage
20
+
21
+ This project includes just the core Lua scripts that define the Remq protocol. To use Remq to build a message queue, install Redis along with one or more of the client libraries listed above.
22
+
23
+ Raw Redis syntax:
24
+
25
+ **Producer:**
26
+ ``` sh
27
+ redis> EVAL <publish.lua> 0 namespace channel message utcseconds
28
+ # returns a unique message id
29
+ ```
30
+
31
+ **Consumer:**
32
+ ``` sh
33
+ redis> EVAL <consume.lua> 0 namespace channel cursor limit
34
+ # returns each message followed by its id, just like ZRANGEBYSCORE
35
+ ```
36
+
37
+ **Purge:**
38
+ ``` sh
39
+ redis> EVAL <purge.lua> 0 namespace channel <BEFORE id (or) KEEP count>
40
+ # returns the count of messages purged
41
+ ```
@@ -0,0 +1,45 @@
1
+ local namespace, channel, cursor, limit = ARGV[1], ARGV[2], ARGV[3], ARGV[4]
2
+
3
+ limit = math.min(limit, 3999) -- 3999 is the limit of unpack()
4
+
5
+ local channel_key = namespace .. ':channel:' .. channel
6
+
7
+ -- for results from multiple channels, we'll merge them into a single set
8
+ local union_key
9
+ if string.find(channel_key, '*') then
10
+ -- if the pattern matches multiple keys, we have to merge the keys
11
+ -- we could use zunionstore here, but it wouldn't be optimal for very large sets
12
+ local matched_keys = redis.call('keys', channel_key)
13
+ if #matched_keys > 1 then
14
+ union_key = channel_key .. '@' .. redis.call('get', namespace .. ':id')
15
+ for i,key in ipairs(matched_keys) do
16
+ local msgs_ids = redis.call('zrangebyscore', key, '(' .. cursor, '+inf', 'WITHSCORES', 'LIMIT', 0, limit)
17
+ if #msgs_ids > 0 then
18
+ -- `zadd` takes scores first, so we have to reverse
19
+ local len, reversed = #msgs_ids, {}
20
+ for i = len, 1, -1 do reversed[len - i + 1] = msgs_ids[i] end
21
+ redis.call('zadd', union_key, unpack(reversed))
22
+ end
23
+ end
24
+ channel_key = union_key
25
+ else
26
+ channel_key = matched_keys[1]
27
+ end
28
+ end
29
+
30
+ -- as long as we have a channel key, get the messages and add wrap with them with ids
31
+ local msgs = {}
32
+ if channel_key ~= nil then
33
+ msgs = redis.call('zrangebyscore', channel_key, '(' .. cursor, '+inf', 'WITHSCORES', 'LIMIT', 0, limit)
34
+ -- zset decimal precision isn't great enough to retain utc seconds, so we have to round
35
+ for i,key in ipairs(msgs) do
36
+ if i % 2 == 0 then msgs[i] = string.format("%.10f", msgs[i]) end
37
+ end
38
+ end
39
+
40
+ -- if we've merged multiple channels, remove the union key
41
+ if union_key ~= nil then
42
+ redis.call('del', union_key)
43
+ end
44
+
45
+ return msgs
@@ -0,0 +1,12 @@
1
+ local namespace, channel, msg, utc_sec = ARGV[1], ARGV[2], ARGV[3], ARGV[4]
2
+ local channel_key = namespace .. ':channel:' .. channel
3
+
4
+ -- ids are an incrementing integer followed by UTC time as a decimal value
5
+ local id = redis.call('incr', namespace .. ':id') .. '.' .. (utc_sec or 0)
6
+
7
+ redis.call('zadd', channel_key, id, msg)
8
+
9
+ redis.call('publish', channel_key, msg)
10
+ redis.call('publish', namespace .. ':stats:' .. channel, id)
11
+
12
+ return id
@@ -0,0 +1,18 @@
1
+ local namespace, channel, cmd, value = ARGV[1], ARGV[2], ARGV[3], ARGV[4]
2
+ local channel_key = namespace .. ':channel:' .. channel
3
+
4
+ local matched_keys = { channel_key }
5
+ if string.find(channel_key, '*') then
6
+ matched_keys = redis.call('keys', channel_key)
7
+ end
8
+
9
+ local purged = 0
10
+ for i,key in ipairs(matched_keys) do
11
+ if cmd == 'BEFORE' then
12
+ purged = purged + redis.call('zremrangebyscore', key, '-inf', '(' .. value)
13
+ elseif cmd == 'KEEP' then
14
+ purged = purged + redis.call('zremrangebyrank', key, 0, 0 - (value - 1))
15
+ end
16
+ end
17
+
18
+ return purged
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1a
5
+ prerelease: 5
6
+ platform: ruby
7
+ authors:
8
+ - Evan Owen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70122011105460 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70122011105460
25
+ - !ruby/object:Gem::Dependency
26
+ name: multi_json
27
+ requirement: &70122011104960 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70122011104960
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70122011104580 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70122011104580
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &70122011103980 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70122011103980
58
+ description: ! " Remq is a Redis-based protocol for building fast, persistent\n
59
+ \ pub/sub message queues.\n\n The Remq protocol is defined by a collection
60
+ of Lua scripts\n (located at https://github.com/kainosnoema/remq) which effectively\n
61
+ \ turn Redis into a capable message queue broker for fast inter-service\n communication.
62
+ The Remq Ruby client library is built on top of these\n scripts, making it easy
63
+ to build fast, persisted pub/sub message queues.\n"
64
+ email: kainosnoema@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .gitmodules
71
+ - LICENSE
72
+ - Readme.md
73
+ - lib/remq.rb
74
+ - lib/remq/coder.rb
75
+ - lib/remq/multi_json_coder.rb
76
+ - lib/remq/script.rb
77
+ - lib/remq/version.rb
78
+ - remq.gemspec
79
+ - spec/remq_spec.rb
80
+ - vendor/remq/Readme.md
81
+ - vendor/remq/scripts/consume.lua
82
+ - vendor/remq/scripts/publish.lua
83
+ - vendor/remq/scripts/purge.lua
84
+ homepage: http://github.com/kainosnoema/remq
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>'
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.1
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.11
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: A Remq client library for Ruby.
108
+ test_files:
109
+ - spec/remq_spec.rb
110
+ has_rdoc: