require 'yaml'
module Lipsiadmin
# ==Simple background loops framework for rails
#
# Loops is a small and lightweight framework for Ruby on Rails created to support simple
# background loops in your application which are usually used to do some background data processing
# on your servers (queue workers, batch tasks processors, etc).
#
# Authors:: Alexey Kovyrin and Dmytro Shteflyuk
# Comments:: This plugin has been created in Scribd.com for internal use.
#
# == What tasks could you use it for?
#
# Originally loops plugin was created to make our own loops code more organized. We used to have tens
# of different modules with methods that were called with script/runner and then used with nohup and
# other not so convenient backgrounding techniques. When you have such a number of loops/workers to
# run in background it becomes a nightmare to manage them on a regular basis (restarts, code upgrades,
# status/health checking, etc).
#
# After a short time of writing our loops in more organized ways we were able to generalize most of the
# loops code so now our loops look like a classes with a single mandatory public method called *run*.
# Everything else (spawning many workers, managing them, logging, backgrounding, pid-files management,
# etc) is handled by the plugin it
#
#
# == But there are dozens of libraries like this! Why do we need one more?
#
# The major idea behind this small project was to create a deadly simple and yet robust framework to
# be able to run some tasks in background and do not think about spawning many workers, restarting
# them when they die, etc. So, if you need to be able to run either one or many copies of your worker or
# you do not want to think about re-spawning dead workers and do not want to spend megabytes of RAM on
# separate copies of Ruby interpreter (when you run each copy of your loop as a separate process
# controlled by monit/god/etc), then I'd recommend you to try this framework -- you'd like it.
#
#
# == How to use?
#
# Generate binary and configuration files by running
#
# script/generate loops
#
# This will create the following list of files:
#
# script/loops # binary file that will be used to manage your loops
# config/loops.yml # example configuration file
# app/loops/simple.rb # REALLY simple loop example
#
# Here is a simple loop scaffold for you to start from (put this file to app/loops/hello_world_loop.rb):
#
# class HelloWorldLoop < Lipsiadmin::Loops::Base
# def run
# debug("Hello, debug log!")
# sleep(config['sleep_period']) # Do something "useful" and make it configurable
# debug("Hello, debug log (yes, once again)!")
# end
# end
#
# When you have your loop ready to use, add the following lines to your (maybe empty yet) config/loops.yml
# file:
#
# hello_world:
# sleep_period: 10
#
# This is it! To start your loop, just run one of the following commands:
#
# # Generates: list all configured loops:
# $ script/loops -L
#
# # Generates: run all enabled (actually non-disabled) loops in foreground:
# $ script/loops -a
#
# # Generates: run all enabled loops in background:
# $ script/loops -d -a
#
# # Generates: run specific loop in background:
# $ ./script/loops -d -l hello_world
#
# # Generates: all possible options:
# $ ./script/loops -h
#
#
# == How to run more than one worker?
#
# If you want to have more than one copy of your worker running, that is as simple as adding one
# option to your loop configuration:
#
# hello_world:
# sleep_period: 10
# workers_number: 1
#
# This _workers_number_ option would say loops manager to spawn more than one copy of your loop
# and run them in parallel. The only thing you'd need to do is to think about concurrent work of
# your loops. For example, if you have some kind of database table with elements you need to
# process, you can create a simple database-based locks system or use any memcache-based locks.
#
#
# == There is this workers_engine option in config file. What it could be used for?
#
# There are two so called "workers engines" in this plugin: fork and thread. They're used
# to control the way process manager would spawn new loops workers: with fork engine we'll
# load all loops classes and then fork ruby interpreter as many times as many workers we need.
# With thread engine we'd do Thread.new instead of forks. Thread engine could be useful if you
# are sure your loop won't lock ruby interpreter (it does not do native calls, etc) or if you
# use some interpreter that does not support forks (like jruby).
#
# Default engine is fork.
#
#
# == What Ruby implementations does it work for?
#
# We've tested and used the plugin on MRI 1.8.6 and on JRuby 1.1.5. At this point we do not support
# demonization in JRuby and never tested the code on Ruby 1.9. Obviously because of JVM limitations
# you won't be able to use +fork+ workers engine in JRuby, but threaded workers do pretty well.
#
module Loops
class << self
# Set/Return the main config
def config
@@config
end
# Set/Return the loops config
def loops_config
@@loops_config
end
# Set/Return the global config
def global_config
@@global_config
end
# Load the yml config file, default config/loops.yml
def load_config(file)
@@config = YAML.load_file(file)
@@global_config = @@config['global']
@@loops_config = @@config['loops']
@@logger = create_logger('global', global_config)
end
# Start loops, default :all
def start_loops!(loops_to_start = :all)
@@running_loops = []
@@pm = Loops::ProcessManager.new(global_config, @@logger)
# Start all loops
loops_config.each do |name, config|
next if config['disabled']
next unless loops_to_start == :all || loops_to_start.member?(name)
klass = load_loop_class(name)
next unless klass
start_loop(name, klass, config)
@@running_loops << name
end
# Do not continue if there is nothing to run
if @@running_loops.empty?
puts "WARNING: No loops to run! Exiting..."
return
end
# Start monitoring loop
setup_signals
@@pm.monitor_workers
info "Loops are stopped now!"
end
private
# Proxy logger calls to the default loops logger
[ :debug, :error, :fatal, :info, :warn ].each do |meth_name|
class_eval <<-EVAL
def #{meth_name}(message)
LOOPS_DEFAULT_LOGGER.#{meth_name} "\#{Time.now}: loops[RUNNER/\#{Process.pid}]: \#{message}"
end
EVAL
end
def load_loop_class(name)
begin
klass_file = LOOPS_ROOT + "/app/loops/#{name}.rb"
debug "Loading class file: #{klass_file}"
require(klass_file)
rescue Exception
error "Can't load the class file: #{klass_file}. Worker #{name} won't be started!"
return false
end
klass_name = "#{name}".classify
klass = klass_name.constantize rescue nil
unless klass
error "Can't find class: #{klass_name}. Worker #{name} won't be started!"
return false
end
begin
klass.check_dependencies
rescue Exception => e
error "Loop #{name} dependencies check failed: #{e} at #{e.backtrace.first}"
return false
end
return klass
end
def start_loop(name, klass, config)
puts "Starting loop: #{name}"
info "Starting loop: #{name}"
info " - config: #{config.inspect}"
@@pm.start_workers(name, config['workers_number'] || 1) do
debug "Instantiating class: #{klass}"
looop = klass.new(create_logger(name, config))
looop.name = name
looop.config = config
debug "Starting the loop #{name}!"
fix_ar_after_fork
looop.run
end
end
def create_logger(loop_name, config)
config['logger'] ||= (global_config['logger'] || 'default')
return LOOPS_DEFAULT_LOGGER if config['logger'] == 'default'
return Logger.new(STDOUT) if config['logger'] == 'stdout'
return Logger.new(STDERR) if config['logger'] == 'stderr'
config['logger'] = File.join(LOOPS_ROOT, config['logger']) unless config['logger'] =~ /^\//
Logger.new(config['logger'])
rescue Exception => e
message = "Can't create a logger for the #{loop_name} loop! Will log to the default logger!"
puts "ERROR: #{message}"
message << "\nException: #{e} at #{e.backtrace.first}"
error(message)
return LOOPS_DEFAULT_LOGGER
end
def setup_signals
trap('TERM') {
warn "Received a TERM signal... stopping..."
@@pm.stop_workers!
}
trap('INT') {
warn "Received an INT signal... stopping..."
@@pm.stop_workers!
}
trap('EXIT') {
warn "Received a EXIT 'signal'... stopping..."
@@pm.stop_workers!
}
end
def fix_ar_after_fork
ActiveRecord::Base.clear_active_connections!
ActiveRecord::Base.verify_active_connections!
end
end
end
end
require 'loops/process_manager'
require 'loops/base'