Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit b0a29cfefbb70bf45363505afd72700e2b22e10f @sporkmonger committed May 27, 2009
Showing with 822 additions and 0 deletions.
  1. +7 −0 .gitignore
  2. +3 −0 CHANGELOG
  3. +20 −0 LICENSE
  4. +26 −0 README
  5. +49 −0 Rakefile
  6. +114 −0 lib/chaingang.rb
  7. +108 −0 lib/chaingang/daemon.rb
  8. +42 −0 lib/chaingang/rake/tasks.rb
  9. +35 −0 lib/chaingang/version.rb
  10. +2 −0 spec/spec.opts
  11. +7 −0 spec/spec_helper.rb
  12. +2 −0 tasks/clobber.rake
  13. +68 −0 tasks/gem.rake
  14. +40 −0 tasks/git.rake
  15. +22 −0 tasks/metrics.rake
  16. +29 −0 tasks/rdoc.rake
  17. +89 −0 tasks/rubyforge.rake
  18. +64 −0 tasks/spec.rake
  19. +95 −0 website/index.html
@@ -0,0 +1,7 @@
+.DS_Store
+.yardoc
+coverage
+doc
+heckling
+pkg
+specdoc
@@ -0,0 +1,3 @@
+== 0.1.0
+
+* initial release
20 LICENSE
@@ -0,0 +1,20 @@
+ChainGang, Copyright (c) 2009 Bob Aman
+
+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.
26 README
@@ -0,0 +1,26 @@
+== ChainGang
+
+Homepage:: chaingang.rubyforge.org[http://chaingang.rubyforge.org/]
+Author:: Bob Aman (mailto:bob@sporkmonger.com)
+Copyright:: Copyright © 2009 Bob Aman
+License:: MIT
+
+== Description
+
+ChainGang is a drop-dead simple worker process framework.
+
+== Features
+
+* A feature list goes here.
+
+== Example Usage
+
+# Some code goes here.
+
+== Requirements
+
+* ChainGang has no dependencies.
+
+== Install
+
+* sudo gem install chaingang
@@ -0,0 +1,49 @@
+lib_dir = File.expand_path(File.join(File.dirname(__FILE__), "lib"))
+$:.unshift(lib_dir)
+$:.uniq!
+
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+require 'spec/rake/spectask'
+
+require File.join(File.dirname(__FILE__), 'lib/chaingang', 'version')
+
+PKG_DISPLAY_NAME = 'ChainGang'
+PKG_NAME = PKG_DISPLAY_NAME.downcase
+PKG_VERSION = ChainGang::VERSION::STRING
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+RELEASE_NAME = "REL #{PKG_VERSION}"
+
+RUBY_FORGE_PROJECT = PKG_NAME
+RUBY_FORGE_USER = "sporkmonger"
+RUBY_FORGE_PATH = "/var/www/gforge-projects/#{RUBY_FORGE_PROJECT}"
+RUBY_FORGE_URL = "http://#{RUBY_FORGE_PROJECT}.rubyforge.org/"
+
+PKG_SUMMARY = "Package Summary"
+PKG_DESCRIPTION = <<-TEXT
+ChainGang is a drop-dead simple worker process framework intended for use with a message queue.
+TEXT
+
+PKG_FILES = FileList[
+ "lib/**/*", "spec/**/*", "vendor/**/*",
+ "tasks/**/*", "website/**/*",
+ "[A-Z]*", "Rakefile"
+].exclude(/database\.yml/).exclude(/[_\.]git$/)
+
+RCOV_ENABLED = (RUBY_PLATFORM != "java" && RUBY_VERSION =~ /^1\.8/)
+if RCOV_ENABLED
+ task :default => "spec:verify"
+else
+ task :default => "spec"
+end
+
+WINDOWS = (RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/) rescue false
+SUDO = WINDOWS ? '' : ('sudo' unless ENV['SUDOLESS'])
+
+Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,114 @@
+# ++
+# ChainGang, Copyright (c) 2009 Bob Aman
+#
+# 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.
+# --
+
+require "chaingang/version"
+require "chaingang/daemon"
+
+module ChainGang
+ def self.prepared_daemon
+ return @prepared_daemon
+ end
+
+ def self.prepare(worker_or_daemon=nil, &block)
+ if worker_or_daemon != nil && block != nil
+ raise ArgumentError,
+ "Either supply a worker or a block, but not both."
+ elsif block != nil
+ worker_or_daemon = block
+ end
+ daemon = (worker_or_daemon.kind_of?(ChainGang::Daemon) ?
+ worker_or_daemon : ChainGang::Daemon.new(:worker => worker_or_daemon))
+ return (@prepared_daemon = daemon)
+ end
+
+ def self.work(worker_or_daemon=nil, &block)
+ if !worker_or_daemon && !block && self.prepared_daemon
+ daemon = self.prepared_daemon
+ else
+ daemon = self.prepare(worker_or_daemon, &block)
+ end
+ pid = self.fork_daemon(daemon)
+ self.write_pidfile(pid, daemon)
+ return pid
+ end
+
+ def self.write_pidfile(pid, daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ File.open(daemon.pidfile, "a") { |file| file.write(pid.to_s + "\n") }
+ end
+
+ def self.read_pidfile(daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ if File.exist?(daemon.pidfile)
+ return File.open(daemon.pidfile, "r") do |file|
+ file.read.split("\n").map { |pid| pid.to_i }
+ end
+ else
+ return []
+ end
+ end
+
+ def self.clear_pidfile(daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ if File.exist?(daemon.pidfile)
+ return File.delete(daemon.pidfile)
+ end
+ end
+
+ def self.fork_daemon(daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ return fork { daemon.run }
+ end
+
+ def self.stop_daemons(daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ pids = self.read_pidfile(daemon)
+ pids.each { |pid| Process.kill("TERM", pid) }
+ self.clear_pidfile(daemon)
+ return pids
+ end
+
+ def self.restart_daemons(daemon=nil)
+ daemon = self.prepared_daemon if daemon == nil
+ if !daemon.kind_of?(ChainGang::Daemon)
+ raise TypeError, "Expected ChainGang::Daemon, got #{daemon.class}."
+ end
+ pids = self.stop_daemons(daemon)
+ return pids.inject([]) { |accu, _| accu << self.fork_daemon(daemon) }
+ end
+end
@@ -0,0 +1,108 @@
+module ChainGang
+ # This class logically represents the running daemon process after fork.
+ # It needs to be given a reference to a worker object that responds to the
+ # :call message.
+ class Daemon
+ def initialize(options={})
+ @alive = true
+ @options = {:threads => 1, :signals => {}}.merge(options)
+ @worker = @options[:worker]
+ if @worker == nil
+ raise ArgumentError,
+ "Expected :worker option to be specified."
+ elsif !@worker.respond_to?(:call)
+ raise TypeError,
+ "Expected #{@worker.class} to respond to :call message."
+ end
+ if !@options[:signals].respond_to?(:to_hash)
+ raise TypeError,
+ "Cannot convert #{options[:signals].class} to Hash."
+ end
+ if !@options[:threads].kind_of?(Integer) || @options[:threads] <= 0
+ raise ArgumentError,
+ "Expected :threads option to be an Integer greater than 0."
+ end
+ @threads = []
+ @signals = @options[:signals].to_hash.merge({
+ "TERM" => lambda do
+ @alive = false
+ end,
+ "HUP" => "IGNORE"
+ })
+ end
+
+ def worker
+ return @worker
+ end
+
+ def alive?
+ return @alive
+ end
+
+ def config
+ @config ||= {
+ :pidfile => (
+ @worker.kind_of?(Proc) ?
+ "chaingang.pid" :
+ @worker.class.name.downcase.gsub(/^.*::/, "") + ".pid"
+ )
+ }.merge(@worker.respond_to?(:config) ? @worker.config : {})
+ end
+
+ def pidfile
+ if !config[:pidfile].respond_to?(:to_str)
+ raise TypeError,
+ "Could not convert #{config[:pidfile].class} to String."
+ end
+ pidfile = config[:pidfile].to_str
+ if File.exists?("tmp/pids")
+ pidfile = File.join("tmp/pids", pidfile)
+ elsif File.exists?("tmp")
+ pidfile = File.join("tmp", pidfile)
+ elsif File.exists?("log")
+ pidfile = File.join("log", pidfile)
+ end
+ return pidfile
+ end
+
+ def setup
+ @worker.setup if @worker.respond_to?(:setup)
+ end
+
+ def teardown
+ @worker.teardown if @worker.respond_to?(:teardown)
+ end
+
+ def options
+ return @options
+ end
+
+ def threads
+ return @threads
+ end
+
+ def run
+ @signals.each do |(signal, action)|
+ if action.kind_of?(Proc)
+ Signal.trap(signal, &action)
+ elsif action.kind_of?(String)
+ Signal.trap(signal, action)
+ else
+ raise TypeError, "Expected #{action.class} to be String or Proc."
+ end
+ end
+ setup
+ options[:threads].times do
+ threads << Thread.new do
+ Thread.pass
+ while(alive?)
+ worker.call
+ Thread.pass
+ end
+ end
+ end
+ threads.each { |thread| thread.join }
+ teardown
+ end
+ end
+end
@@ -0,0 +1,42 @@
+require "rake"
+require "rake/tasklib"
+
+module ChainGang
+ module Rake
+ class DaemonTask < ::Rake::TaskLib
+ # The name of the daemon being created
+ attr_accessor :name
+
+ # The path to the daemon's worker
+ attr_accessor :worker
+
+ # Defines a new task, using the name +name+.
+ def initialize(name=:chaingang)
+ @name = name
+ @worker = nil
+ yield self if block_given?
+ build_tasks
+ end
+
+ private
+ def build_tasks # :nodoc:
+ if worker == nil
+ raise ArgumentError, "Worker must be set."
+ end
+ desc "Start the #{name} daemon"
+ task :start do
+ exec("ruby -e \"require '#{worker}'; ChainGang.work\"")
+ end
+ desc "Stop the #{name} daemon"
+ task :stop do
+ exec("ruby -e \"require '#{worker}'; ChainGang.stop_daemons\"")
+ end
+ desc "Restart the #{name} daemon"
+ task :restart do
+ exec("ruby -e \"require '#{worker}'; ChainGang.restart_daemons\"")
+ end
+ self
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit b0a29cf

Please sign in to comment.