quredis 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+
20
+ .DS_Store
21
+
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
@@ -0,0 +1,11 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'spec'
5
+ t.pattern = "spec/*_spec.rb"
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
10
+
11
+ task :spec => :test
data/bin/quredis ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'quredis/cli'
3
+ Quredis::CLI.start
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,12 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'quredis')
2
+
3
+ class Store
4
+ include Quredis
5
+ def initialize
6
+ quredis :shop do |customer|
7
+ puts "servicing #{customer}"
8
+ end
9
+ end
10
+ end
11
+
12
+ Store.new.start
@@ -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
@@ -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