quredis 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/Procfile +1 -0
- data/README.md +93 -0
- data/Rakefile +11 -0
- data/bin/quredis +3 -0
- data/examples/mail.rb +12 -0
- data/examples/store.rb +12 -0
- data/lib/quredis/admin.rb +80 -0
- data/lib/quredis/cli.rb +64 -0
- data/lib/quredis/public/css/bootstrap-responsive.css +815 -0
- data/lib/quredis/public/css/bootstrap-responsive.min.css +9 -0
- data/lib/quredis/public/css/bootstrap.css +4983 -0
- data/lib/quredis/public/css/bootstrap.min.css +9 -0
- data/lib/quredis/public/css/quredis.css +12 -0
- data/lib/quredis/public/img/glyphicons-halflings-white.png +0 -0
- data/lib/quredis/public/img/glyphicons-halflings.png +0 -0
- data/lib/quredis/public/js/bootstrap.js +1825 -0
- data/lib/quredis/public/js/bootstrap.min.js +6 -0
- data/lib/quredis/public/js/jquery.min.js +4 -0
- data/lib/quredis/public/js/jquery_ujs.js +377 -0
- data/lib/quredis/public/js/quredis.js +1 -0
- data/lib/quredis/version.rb +3 -0
- data/lib/quredis/views/index.erb +28 -0
- data/lib/quredis/views/layout.erb +62 -0
- data/lib/quredis/views/queue.erb +23 -0
- data/lib/quredis/web.rb +64 -0
- data/lib/quredis.rb +121 -0
- data/quredis.gemspec +23 -0
- data/spec/quredis_spec.rb +36 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: ruby -Ilib lib/quredis/web.rb
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
quredis
|
2
|
+
=======
|
3
|
+
|
4
|
+
quredis (pronounced ‘kurdis’) is a little DSL for redis safe queues using `brpoplpush`. `brpoplpush` command in Redis atomically pops a message from one list and pushes onto another list. This primitive is exactly what we need to build fail-safe queues.
|
5
|
+
|
6
|
+
A quredis queue is identified by its name. A queredis queue is employs three redis lists. The names of redis lists (queues) can be configured for these lists but also follow a naming convention described below.
|
7
|
+
|
8
|
+
|
9
|
+
Name | Default Name | Description
|
10
|
+
------------- | ------------------ | ------------
|
11
|
+
Ingress Queue | ingress:queue_name | Messages are enqueued
|
12
|
+
Transit Queue | transit:queue_name | Messages are held while they are being processed
|
13
|
+
Escape Queue | escape:queue_name | Failed messages which are not successfully processed
|
14
|
+
|
15
|
+
|
16
|
+
Producer
|
17
|
+
========
|
18
|
+
|
19
|
+
Messages are enqueued using redis `lpush` command.
|
20
|
+
|
21
|
+
redis.lpush('ingress:queue_name', message)
|
22
|
+
|
23
|
+
Consumer
|
24
|
+
========
|
25
|
+
To handle messages, include Quredis module. `quredis(name, options = {}, &block)` method sets things up. The block passed to the `quredis` method handles the message. Call to `start` method and let the messages flow.
|
26
|
+
|
27
|
+
class Shop
|
28
|
+
include Quredis
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
quredis :shop do |customer|
|
32
|
+
puts "Now serving #{customer}."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Shop.new.start
|
38
|
+
|
39
|
+
Custom queue names can be passed as options to `quredis` method:
|
40
|
+
|
41
|
+
quredis :mail,
|
42
|
+
{:ingress => :read_later, :transit => :reading_now, :escape => :trash} do |message|
|
43
|
+
puts "now reading #{message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
Configuring Redis
|
49
|
+
-----------
|
50
|
+
To configure redis connection, either provide a method called `redis`.
|
51
|
+
|
52
|
+
Configuring Logger
|
53
|
+
-----------
|
54
|
+
Provide a method called `logger` to plug a custom logger.
|
55
|
+
|
56
|
+
Under the Hood
|
57
|
+
==============
|
58
|
+
Quredis moves messages from the ingress list, `ingress:queue_name`, to a transit list, `transit:queue_name` atomically using `brpoplpush`. The operation returns the message that was moved from the ingress list to the transit list. Next, the application code processes the message and deletes the message from the transit list using red is `lrem` command.
|
59
|
+
|
60
|
+
message = redis.brpoplpush(ingress, transit, timeout)
|
61
|
+
if message
|
62
|
+
handle_message(message)
|
63
|
+
redis.lrem(transit, -1, message)
|
64
|
+
end
|
65
|
+
|
66
|
+
This code snippet above handles normal execution. To handle the the cases when application code crashes or there is a hardware failure, etc, Quredis introduces a third list, called escape list. If `handle_message(message)` fails because it throws an exception, then the message is atomically removed from `transit` list and added to the `escape` list. If the application crashes or hardware fails while handling the message, the message stays in transit list to be processed later. When the application starts again, quredis runs the recovery operation where we use `rpoplpush` to move messages from transit list the the escape list. If a message is handled normally, it’s removed from the escape queue, otherwise it stays in the escape queue.
|
67
|
+
|
68
|
+
while message = redis.rpoplpush(transit, escape)
|
69
|
+
handle_message(message)
|
70
|
+
redis.lrem(escape, -1, message)
|
71
|
+
end
|
72
|
+
|
73
|
+
Also the recovery on start automates the processing of any messages in flight during deployment, application migation, etc. shutdown and restarts.
|
74
|
+
|
75
|
+
Fault Tolerence
|
76
|
+
===============
|
77
|
+
Quredis is tolerant against following faults:
|
78
|
+
|
79
|
+
Bad Message
|
80
|
+
-----------
|
81
|
+
Queredis ensures that a bad message will not cause the system from moving forward. During normal execution, if StandardError or Timeout::Error is raised while handling a message, quredis removes the message from the transit queue and moves it to the escape queue atomically. It can be later handled using admin tool which allow you to either delete the message from escape queue or reenqueue to ingress queue.
|
82
|
+
|
83
|
+
Application Failure
|
84
|
+
-----------
|
85
|
+
Queredis ensures that a non standard error, e.g., NoMemoryError, will not cause the system from moving forward. Suppose a message is causing NoMemoryError which crashes the app and the monitoring process like monit, blue pill, god, supervisor, upstart, etc, restart the application. On restart, the recovery operation will move the message to the escape queue from the transit queue and will call `handle_message(message)`. If the app crashes again, then the message stays in the escape queue and the next application restart will not fail due to this error.
|
86
|
+
|
87
|
+
Hardware Failure
|
88
|
+
-----------
|
89
|
+
Queredis ensures that messages are moved between queues atomically which makes it fault tolrent against the application hardware failure. Quredis doesn’t protect againt any hardware problem on the redis server itself.
|
90
|
+
|
91
|
+
Quredis admin tool allows deleting and enqueuing messages from transit and escape queues.
|
92
|
+
|
93
|
+
|
data/Rakefile
ADDED
data/bin/quredis
ADDED
data/examples/mail.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'quredis')
|
2
|
+
|
3
|
+
class Mail
|
4
|
+
include Quredis
|
5
|
+
def initialize
|
6
|
+
quredis :mail, {:ingress => :read_later, :transit => :reading_now, :escape => :trash} do |message|
|
7
|
+
puts "now reading #{message}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Mail.new.start
|
data/examples/store.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
module Quredis
|
6
|
+
class Admin
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
@options = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def redis
|
13
|
+
@redis ||= Redis.new(@options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def queues(params={})
|
17
|
+
offset = params.fetch(:offset, 0).to_i
|
18
|
+
limit = params.fetch(:limit, 1000).to_i
|
19
|
+
queue_names, total = redis.multi do |multi|
|
20
|
+
multi.zrange("quredis:queues", offset, offset+limit)
|
21
|
+
multi.zcard("quredis:queues")
|
22
|
+
end
|
23
|
+
queue_keys = queue_names.map {|queue_name| "quredis:queue:#{queue_name}"}
|
24
|
+
queues = redis.mget(*queue_keys).map {|str| JSON.parse(str)}
|
25
|
+
|
26
|
+
queues.each do |queue|
|
27
|
+
ingress_count, transit_count, escape_count = redis.multi do |multi|
|
28
|
+
multi.llen(queue['ingress'])
|
29
|
+
multi.llen(queue['transit'])
|
30
|
+
multi.llen(queue['escape'])
|
31
|
+
end
|
32
|
+
queue['ingress_count'] = ingress_count
|
33
|
+
queue['transit_count'] = transit_count
|
34
|
+
queue['escape_count'] = escape_count
|
35
|
+
end
|
36
|
+
{:total => total, :offset => offset, :items => queues}
|
37
|
+
end
|
38
|
+
|
39
|
+
def queue(name, type)
|
40
|
+
str = redis.get("quredis:queue:#{name}")
|
41
|
+
queue = JSON.parse(str)
|
42
|
+
list = queue[type]
|
43
|
+
|
44
|
+
total, items = redis.multi do |multi|
|
45
|
+
multi.llen(list)
|
46
|
+
multi.lrange(list, 0, 100)
|
47
|
+
end
|
48
|
+
{:type => type, :queue => name, :list => list, :total => total, :offset => 0, :items => items}
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy_queue(name)
|
52
|
+
queue_key = "quredis:queue:#{name}"
|
53
|
+
str = redis.get(queue_key)
|
54
|
+
queue = JSON.parse(str)
|
55
|
+
redis.multi do |multi|
|
56
|
+
multi.del(queue['escape'])
|
57
|
+
multi.del(queue['transit'])
|
58
|
+
multi.del(queue['ingress'])
|
59
|
+
multi.del(queue_key)
|
60
|
+
multi.zrem("quredis:queues", name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def purge(name, type)
|
65
|
+
queue_key = "quredis:queue:#{name}"
|
66
|
+
str = redis.get(queue_key)
|
67
|
+
queue = JSON.parse(str)
|
68
|
+
redis.del(queue[type])
|
69
|
+
end
|
70
|
+
|
71
|
+
def retry(name, type)
|
72
|
+
queue_key = "quredis:queue:#{name}"
|
73
|
+
str = redis.get(queue_key)
|
74
|
+
queue = JSON.parse(str)
|
75
|
+
from = queue[type]
|
76
|
+
to = queue['ingress']
|
77
|
+
message = redis.rpoplpush(from, to) while message
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/quredis/cli.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'quredis/admin'
|
3
|
+
require 'quredis/web'
|
4
|
+
require 'quredis/version'
|
5
|
+
|
6
|
+
module Quredis
|
7
|
+
class CLI < Thor
|
8
|
+
default_task("ls")
|
9
|
+
|
10
|
+
desc "ls", "list queues"
|
11
|
+
method_option :host, :type => :string, :aliases => '-h'
|
12
|
+
method_option :port, :type => :numeric, :aliases => '-p'
|
13
|
+
def ls
|
14
|
+
table = Quredis::Admin.new(options).queues[:items].map do |q|
|
15
|
+
[q['name'],
|
16
|
+
"#{q['ingress']} (#{q['ingress_count']})",
|
17
|
+
"#{q['transit']} (#{q['transit_count']})",
|
18
|
+
"#{q['escape']} (#{q['escape_count']})"]
|
19
|
+
end
|
20
|
+
table.unshift ["Name", "Ingress", "Transit", "Escape"]
|
21
|
+
|
22
|
+
Thor::Base.shell.new.print_table(table, :indent => 2, :truncate => true)
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "destroy queue_name", "destroy the specified queue queue"
|
26
|
+
method_option :host, :type => :string, :aliases => '-h'
|
27
|
+
method_option :port, :type => :numeric, :aliases => '-p'
|
28
|
+
def destroy(queue_name)
|
29
|
+
Quredis::Admin.new(options).destroy_queue(queue_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "purge queue_name queue_type", "purges queue_name queue_type"
|
33
|
+
method_option :host, :type => :string, :aliases => '-h'
|
34
|
+
method_option :port, :type => :numeric, :aliases => '-p'
|
35
|
+
def purge(queue_name, queue_type)
|
36
|
+
Quredis::Admin.new(options).purge(queue_name, queue_type)
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "retry queue_name queue_type", "re-enqueues queue type"
|
40
|
+
method_option :host, :type => :string, :aliases => '-h'
|
41
|
+
method_option :port, :type => :numeric, :aliases => '-p'
|
42
|
+
def retry(queue_name, queue_type)
|
43
|
+
Quredis::Admin.new(options).retry(queue_name, queue_type)
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "web", "start webapp"
|
47
|
+
method_option :redis_host, :type => :string, :aliases => '-H', :desc => 'redis server host'
|
48
|
+
method_option :redis_port, :type => :numeric, :aliases => '-P', :desc => 'redis server port'
|
49
|
+
method_option :bind, :type => :string, :aliases => '-b', :desc => 'webapp IP address to bind to (default: 0.0.0.0)'
|
50
|
+
method_option :port, :type => :numeric, :aliases => '-p', :desc => 'webapp Port to listen on'
|
51
|
+
method_option :server, :type => :string, :aliases => '-s', :desc => 'specify rack server/handler (default is thin)'
|
52
|
+
def web
|
53
|
+
[:redis_host, :redis_port, :bind, :port, :server].each do |option|
|
54
|
+
Quredis::Web.set(option, options[option]) if options[option]
|
55
|
+
end
|
56
|
+
Quredis::Web.run!
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "version", "version"
|
60
|
+
def version
|
61
|
+
puts "Quredis #{Quredis::VERSION}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|