Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import.

  • Loading branch information...
commit d71040e75d19c6fa25d72e6492768a410e91aac3 0 parents
@soveran authored
1  .gitignore
@@ -0,0 +1 @@
+/pkg
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Michel Martens
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
107 README.markdown
@@ -0,0 +1,107 @@
+Ost
+===
+
+Manage your Redis based queues and workers.
+
+Description
+-----------
+
+**Ost** makes it easy to enqueue object ids and process them with workers.
+
+Say you want to process video uploads. In your application you will have something like this:
+
+ Ost[:videos_to_process].push(@video.id)
+
+Then, you will have a worker that will look like this:
+
+ require "ost"
+
+ Ost[:videos_to_process].each do |id|
+ # Do something with it!
+ end
+
+Usage
+-----
+
+**Ost** connects to Redis automatically with the default options (localhost:6379, database 0).
+
+You can customize the connection by calling `connect`:
+
+ Ost.connect port: 6380, db: 2
+
+Then you only need to refer to a queue for it to pop into existence:
+
+ Ost[:rss_feeds] << @feed.id
+
+A worker is a Ruby file with this basic code:
+
+ require "ost"
+
+ Ost[:rss_feeds].each do |id|
+ ...
+ end
+
+Available methods for a queue are `push` (aliased to `<<`) and `pop` (aliased to `each`).
+
+Note that in these examples we are pushing numbers to the queue. As we have unlimited queues, each queue should be specialized and the workers must be smart enough to know what to do with the numbers they pop.
+
+Failures
+========
+
+If the block raises an error, it is captured by **Ost** and the exception is logged in Redis.
+
+Consider this example:
+
+ require "ost"
+
+ Ost[:rss_feeds].each do |id|
+ ...
+ raise "Invalid format"
+ end
+
+Then, in the console you can do:
+
+ >> Ost[:rss_feeds].push 1
+ => 1
+
+ >> Ost[:rss_feeds].errors
+ => ["2010-04-12 21:57:23 -0300 ost:rss_feeds:1 => #<RuntimeError: Invalid format>"]
+
+Differences with Delayed::Job and Resque
+--------------------------------------
+
+Both [Delayed::Job](http://github.com/tobi/delayed_job) and [Resque](http://github.com/defunkt/resque)
+provide queues and workers (the later using Redis). They provide dumb workers that process jobs, which are specialized for each task. The specialization takes place in the application side, and the job is serialized and pushed into a queue.
+
+**Ost**, by contrast, just pushes numbers into specialized queues, and uses workers that are subscribed to specific queues and know what to do with the items they get. The total sum of logic is almost the same.
+
+Installation
+------------
+
+ $ sudo gem install ost
+
+License
+-------
+
+Copyright (c) 2010 Michel Martens
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
5 Rakefile
@@ -0,0 +1,5 @@
+task :test do
+ system "cd test && ruby ost_test.rb"
+end
+
+task :default => :test
68 lib/ost.rb
@@ -0,0 +1,68 @@
+require "redis"
+require "nest"
+
+module Ost
+ VERSION = "0.0.1"
+ TIMEOUT = ENV["TIMEOUT"] || 2
+
+ class Queue
+ attr :ns
+
+ def initialize(name)
+ @ns = Nest.new(:ost)[name]
+ end
+
+ def push(value)
+ redis.lpush(ns, value)
+ end
+
+ def each(&block)
+ loop do
+ _, item = redis.brpop(ns, TIMEOUT)
+ next if item.nil? or item.empty?
+
+ begin
+ block.call(item)
+ rescue Exception => e
+ error = "#{Time.now} #{ns[item]} => #{e.inspect}"
+
+ redis.rpush ns[:errors], error
+ redis.publish ns[:errors], error
+ end
+ end
+ end
+
+ def errors
+ redis.lrange ns[:errors], 0, -1
+ end
+
+ alias << push
+ alias pop each
+
+ private
+
+ def redis
+ Ost.redis
+ end
+ end
+
+ @queues = Hash.new do |hash, key|
+ hash[key] = Queue.new(key)
+ end
+
+ def self.[](queue)
+ @queues[queue]
+ end
+
+ def self.connect(options = {})
+ @redis = Redis.new(options)
+ end
+
+ def self.redis
+ @redis ||= Redis.new
+ end
+
+ def self.redis=(redis)
+ @redis = redis
+ end
+end
10 ost.gemspec
@@ -0,0 +1,10 @@
+Gem::Specification.new do |s|
+ s.name = "ost"
+ s.version = "0.0.1"
+ s.summary = "Redis based queues and workers."
+ s.description = "Ost lets you manage queues and workers with Redis."
+ s.authors = ["Michel Martens"]
+ s.email = ["michel@soveran.com"]
+ s.homepage = "http://github.com/soveran/ost"
+ s.files = ["LICENSE", "README.markdown", "Rakefile", "lib/ost.rb", "ost.gemspec", "test/ost_test.rb", "test/test_helper.rb"]
+end
18 ost.gemspec.erb
@@ -0,0 +1,18 @@
+<% require "lib/ost" -%>
+Gem::Specification.new do |s|
+ s.name = "ost"
+ s.version = "<%= Ost::VERSION %>"
+ s.summary = "Redis based queues and workers."
+ s.description = "Ost lets you manage queues and workers with Redis."
+ s.authors = ["Michel Martens"]
+ s.email = ["michel@soveran.com"]
+ s.homepage = "http://github.com/soveran/ost"
+ s.files = <%= Dir[
+ "LICENSE",
+ "README.markdown",
+ "Rakefile",
+ "lib/**/*.rb",
+ "*.gemspec",
+ "test/*.*"
+ ].inspect %>
+end
77 test/ost_test.rb
@@ -0,0 +1,77 @@
+require File.join(File.dirname(__FILE__), "test_helper")
+
+class TestOst < Test::Unit::TestCase
+ def ost(&job)
+ thread = Thread.new do
+ Ost[:events].each(&job)
+ end
+
+ sleep 0.2
+
+ thread.kill
+ end
+
+ setup do
+ @redis = Redis.new
+ @redis.flushdb
+
+ Ost.connect
+ Ost[:events].push(1)
+ end
+
+ teardown do
+ Ost.redis.flushdb
+ end
+
+ should "insert items in the queue" do
+ assert_equal ["1"], @redis.lrange("ost:events", 0, -1)
+ end
+
+ should "process items from the queue" do
+ @results = []
+
+ ost do |item|
+ @results << item
+ end
+
+ assert_equal [], @redis.lrange("ost:events", 0, -1)
+ assert_equal ["1"], @results
+ end
+
+ should "add failures to a special list" do
+ ost do |item|
+ raise "Wrong answer"
+ end
+
+ assert_equal 0, @redis.llen("ost:events")
+ assert_equal 1, @redis.llen("ost:events:errors")
+
+ assert_match /ost:events:1 => #<RuntimeError: Wrong answer/, @redis.rpop("ost:events:errors")
+ end
+
+ should "publish the error to a specific channel" do
+ @results = []
+
+ t1 = Thread.new do
+ @redis.subscribe("ost:events:errors") do |on|
+ on.message do |channel, message|
+ if message[/ost:events:1 => #<RuntimeError: Wrong answer/]
+ @results << message
+ @redis.unsubscribe
+ end
+ end
+ end
+ end
+
+ ost do |item|
+ raise "Wrong answer"
+ end
+
+ t1.kill
+
+ assert_equal 0, @redis.llen("ost:events")
+ assert_equal 1, @redis.llen("ost:events:errors")
+
+ assert_match /ost:events:1 => #<RuntimeError: Wrong answer/, @redis.rpop("ost:events:errors")
+ end
+end
3  test/test_helper.rb
@@ -0,0 +1,3 @@
+require "rubygems"
+require "contest"
+require File.join(File.dirname(__FILE__), "..", "lib", "ost")
Please sign in to comment.
Something went wrong with that request. Please try again.