simplejob 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Ken Pratt (ken@kenpratt.net)
4
+ Copyright (c) 2011 Ruboss Technology Corp (peter@ruboss.com)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ Simple Job Queue
2
+ ================
3
+
4
+ Painless job queueing, backed by AMQP.
5
+
6
+ The design goals were:
7
+
8
+ * Super simple interface
9
+ * Robust jobs that never get lost
10
+ * Jobs don't get marked complete until they are finished (they survive reboots)
11
+ * One job queue per worker pool, with jobs mapped to that queue (for easy monitoring)
12
+ * Jobs which throw exceptions are logged and removed, preventing infinite job loops
13
+
14
+ Hello World
15
+ -----------
16
+
17
+ Start your [RabbitMQ](http://www.rabbitmq.com/) server (install one if necessary).
18
+
19
+ <pre>
20
+ producer.rb:
21
+
22
+ require "simplejob"
23
+
24
+ SimpleJob.send("hello")
25
+ </pre>
26
+
27
+ <pre>
28
+ consumer.rb:
29
+
30
+ require "simplejob"
31
+
32
+ SimpleJob::Worker.start do
33
+ handle "hello" do |props|
34
+ puts "got it!"
35
+ end
36
+ end
37
+ </pre>
38
+
39
+ <pre>
40
+ $ ruby -rubygems consumer.rb
41
+ </pre>
42
+
43
+ <pre>
44
+ $ ruby -rubygems producer.rb
45
+ </pre>
46
+
47
+ More examples
48
+ -------------
49
+
50
+ See the [examples directory](http://github.com/kenpratt/simple-job-queue/tree/master/examples).
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
9
+ gem.name = "simplejob"
10
+ gem.homepage = "http://github.com/kenpratt/simplejob"
11
+ gem.license = "MIT"
12
+ gem.summary = "Painless job queueing."
13
+ gem.description = "A simple AMQP-backed job queuing system."
14
+ gem.email = "ken@kenpratt.net"
15
+ gem.authors = ["Ken Pratt"]
16
+ gem.add_dependency "activesupport", ">= 3.0.1"
17
+ gem.add_dependency "json", ">= 1.5.1"
18
+ gem.add_dependency "amqp", "0.7.1"
19
+ end
20
+ Jeweler::RubygemsDotOrgTasks.new
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ test.rcov_opts << '--exclude "gems/*"'
35
+ end
36
+
37
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,10 @@
1
+ Basic producer/consumer
2
+ =======================
3
+
4
+ Open a couple of terminal windows with consumers, and then run the producer:
5
+
6
+ ruby -rubygems consumer.rb
7
+ ruby -rubygems consumer.rb
8
+ ruby -rubygems producer.rb
9
+
10
+ You should see the consumers pulling requests of the queue and processing them. Try killing and restarting one or both of them, or even restarting RabbitMQ. No jobs should be lost.
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob::Worker.start do
6
+ handle "a_task" do |props|
7
+ puts "got x: #{props[:x]}"
8
+ sleep 1
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ 1.upto(20).each do |x|
6
+ puts "sending x: #{x}"
7
+ SimpleJob.send("a_task", :x => x)
8
+ end
@@ -0,0 +1,11 @@
1
+ Multiple jobs mapped to different consumers
2
+ ===========================================
3
+
4
+ Open a few terminal windows with various combinations of consumers, and then run the producer:
5
+
6
+ ruby -rubygems drink_consumer.rb
7
+ ruby -rubygems food_consumer.rb
8
+ ruby -rubygems food_consumer.rb
9
+ ruby -rubygems producer.rb
10
+
11
+ You should see the consumers pulling requests of the queue and processing them. Try killing and restarting them, or even restarting RabbitMQ. No jobs should be lost.
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob::Worker.start(:queue_name => "drinks") do
6
+
7
+ handle "coffee" do |props|
8
+ puts "caffeine, here i come"
9
+ sleep 1
10
+ end
11
+
12
+ handle "milkshake" do |props|
13
+ puts "a #{props[:flavor]} milkshake, excellent!"
14
+ sleep 1
15
+ end
16
+
17
+ end
@@ -0,0 +1,22 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob::Worker.start(:queue_name => "food") do
6
+
7
+ handle "pie" do |props|
8
+ puts "mmm, a #{props[:size]} #{props[:flavor]} pie, my favorite!"
9
+ sleep 3
10
+ end
11
+
12
+ handle "ice_cream" do |props|
13
+ puts "gross, #{props[:flavor]} ice cream"
14
+ sleep 1
15
+ end
16
+
17
+ handle "mousse" do |props|
18
+ puts "what's a mousse?"
19
+ sleep 0.1
20
+ end
21
+
22
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob.send("coffee")
6
+ SimpleJob.send("pie", :flavor => "apple", :size => "12in")
7
+ SimpleJob.send("ice_cream", :flavor => "vanilla")
8
+ SimpleJob.send("milkshake", :flavor => "chocolate")
9
+ SimpleJob.send("mousse")
10
+ SimpleJob.send("mousse")
11
+ SimpleJob.send("mousse")
@@ -0,0 +1,10 @@
1
+ Wildcard handlers
2
+ =================
3
+
4
+ Open the consumer and wiretap in terminal windows, and then run the producer:
5
+
6
+ ruby -rubygems consumer.rb
7
+ ruby -rubygems wiretap.rb
8
+ ruby -rubygems producer.rb
9
+
10
+ You should see the consumer pulling requests of the queue and processing them, and the wiretap printing everything out. Try killing and restarting them, or even restarting RabbitMQ. No jobs or wiretapped jobs should be lost.
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob::Worker.start(:queue_name => "foo") do
6
+
7
+ handle "foo.*" do |props|
8
+ puts "normal foo: #{props[:topic]}"
9
+ end
10
+
11
+ handle "foo.#" do |props|
12
+ puts "catchall foo: #{props[:topic]}"
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob.send("foo.bar")
6
+ SimpleJob.send("foo.baz")
7
+ SimpleJob.send("foo.bar.baz")
8
+ SimpleJob.send("foo.bar.baz.bang")
9
+ SimpleJob.send("zap")
10
+ SimpleJob.send("zap.whizz")
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
2
+
3
+ require "simplejob"
4
+
5
+ SimpleJob::Worker.start(:queue_name => "wiretap") do
6
+
7
+ handle "#" do |props|
8
+ puts "wiretapped: #{props[:topic]}"
9
+ end
10
+
11
+ end
data/lib/simplejob.rb ADDED
@@ -0,0 +1,23 @@
1
+ require "json"
2
+ require "logger"
3
+
4
+ require "simplejob/client"
5
+ require "simplejob/worker"
6
+
7
+ module SimpleJob
8
+
9
+ # Send a work request
10
+ def self.send(topic, props = {})
11
+ Client.start do
12
+ raise "Message properties should be a Hash" unless props.kind_of?(Hash)
13
+ log.info "[simplejob] New job: #{topic}, #{props.inspect}"
14
+ publish(topic, props.to_json)
15
+ stop
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ def log
22
+ Logger.new(STDOUT)
23
+ end
@@ -0,0 +1,50 @@
1
+ require "amqp"
2
+
3
+ module SimpleJob
4
+ DEFAULT_EXCHANGE_NAME = "simplejob"
5
+
6
+ class Client
7
+ def self.start(opts = {}, &proc)
8
+ instance = new
9
+ instance.start(opts, &proc)
10
+ instance
11
+ end
12
+
13
+ def start(opts, &proc)
14
+ client = self
15
+ AMQP.start(opts) do
16
+ Signal.trap("INT") { puts; stop }
17
+ Signal.trap("TERM") { puts; stop }
18
+
19
+ @channel = AMQP::Channel.new
20
+ @exchange = exchange(opts[:exchange_name] || DEFAULT_EXCHANGE_NAME)
21
+
22
+ instance_exec(opts, &proc)
23
+ end
24
+ end
25
+
26
+ def stop
27
+ AMQP.stop { EM.stop }
28
+ end
29
+
30
+ def exchange(name)
31
+ @channel.topic(name, :durable => true, :auto_delete => false)
32
+ end
33
+
34
+ def queue(name)
35
+ @channel.queue(name, :durable => true, :auto_delete => false)
36
+ end
37
+
38
+ def bind(queue, key)
39
+ queue.bind(@exchange, :key => key)
40
+ end
41
+
42
+ def publish(topic, message)
43
+ @exchange.publish(message, :routing_key => topic, :persistent => true)
44
+ end
45
+
46
+ def subscribe(queue, &proc)
47
+ queue.subscribe({ :ack => true }, &proc)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,90 @@
1
+ require "active_support/core_ext/hash"
2
+
3
+ module SimpleJob
4
+ class Worker < Client
5
+ def start(opts, &proc)
6
+ super do
7
+ # only fetch one message at a time instead of being greedy
8
+ @channel.prefetch(1)
9
+
10
+ # create the queue
11
+ @queue = queue(opts[:queue_name] || "job_queue")
12
+
13
+ # defer to instance to set up handlers
14
+ instance_exec(&proc)
15
+
16
+ # start processing jobs
17
+ start_handler_loop
18
+ end
19
+ end
20
+
21
+ def handle(key, &proc)
22
+ # bind the message to the queue
23
+ bind(@queue, key)
24
+
25
+ # register the handler
26
+ install_handler(key, proc)
27
+ end
28
+
29
+ private
30
+
31
+ def start_handler_loop
32
+ subscribe(@queue) do |headers, body|
33
+ unless AMQP.closing?
34
+ puts headers.inspect
35
+ topic = headers.properties[:routing_key]
36
+
37
+ begin
38
+ start_time = Time.now
39
+ log.info "\n#{"="*80}\n#{topic.upcase} BEGIN -- #{start_time}\n#{"="*80}"
40
+
41
+ props = JSON.parse(body).with_indifferent_access
42
+ props[:topic] = topic
43
+ log.info "[worker] Props: #{props.inspect}"
44
+
45
+ if handler = find_handler(topic)
46
+ handler.call(props)
47
+ else
48
+ log.error "[worker] no handler installed for #{topic}"
49
+ end
50
+
51
+ end_time = Time.now
52
+ elapsed = ((end_time - start_time) * 10).round / 10.0
53
+ log.info "\n#{"="*80}\n#{topic.upcase} END -- #{end_time} (elapsed: #{elapsed}s)\n#{"="*80}\n"
54
+ rescue Exception => error
55
+ log.error "[worker] handle blew up for #{topic} with body:\n#{body}\n#{error}\n#{error.backtrace.join("\n")}"
56
+ end
57
+
58
+ headers.ack
59
+ end
60
+ end
61
+ end
62
+
63
+ def install_handler(key, proc)
64
+ @handlers ||= {}
65
+ @handlers[key] = { :block => proc, :re => key_to_regex(key) }
66
+
67
+ @handler_keys ||= []
68
+ @handler_keys << key
69
+ end
70
+
71
+ def find_handler(topic)
72
+ return nil unless @handlers
73
+
74
+ # check for direct hit
75
+ if handler = @handlers[topic]
76
+ handler[:block]
77
+
78
+ # otherwise, test regexes
79
+ else
80
+ if key = @handler_keys.detect {|k| @handlers[k][:re].match(topic) }
81
+ @handlers[key][:block]
82
+ end
83
+ end
84
+ end
85
+
86
+ def key_to_regex(key)
87
+ Regexp.compile("^" + key.gsub('*', '[^\.]+').gsub('#', '.*') + "$")
88
+ end
89
+ end
90
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'simplejob'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestSimplejob < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplejob
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Ken Pratt
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-05-06 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 3
31
+ - 0
32
+ - 1
33
+ version: 3.0.1
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 1
45
+ segments:
46
+ - 1
47
+ - 5
48
+ - 1
49
+ version: 1.5.1
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: amqp
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - "="
59
+ - !ruby/object:Gem::Version
60
+ hash: 1
61
+ segments:
62
+ - 0
63
+ - 7
64
+ - 1
65
+ version: 0.7.1
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ description: A simple AMQP-backed job queuing system.
69
+ email: ken@kenpratt.net
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - LICENSE.txt
76
+ - README.md
77
+ files:
78
+ - .document
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - VERSION
83
+ - examples/basic/README.md
84
+ - examples/basic/consumer.rb
85
+ - examples/basic/producer.rb
86
+ - examples/multiple_jobs/README.md
87
+ - examples/multiple_jobs/drink_consumer.rb
88
+ - examples/multiple_jobs/food_consumer.rb
89
+ - examples/multiple_jobs/producer.rb
90
+ - examples/wildcard_handlers/README.md
91
+ - examples/wildcard_handlers/consumer.rb
92
+ - examples/wildcard_handlers/producer.rb
93
+ - examples/wildcard_handlers/wiretap.rb
94
+ - lib/simplejob.rb
95
+ - lib/simplejob/client.rb
96
+ - lib/simplejob/worker.rb
97
+ - test/helper.rb
98
+ - test/test_simplejob.rb
99
+ homepage: http://github.com/kenpratt/simplejob
100
+ licenses:
101
+ - MIT
102
+ post_install_message:
103
+ rdoc_options: []
104
+
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.1
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Painless job queueing.
132
+ test_files: []
133
+