loops 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/LICENSE +21 -0
- data/README.rdoc +238 -0
- data/Rakefile +48 -0
- data/VERSION.yml +5 -0
- data/bin/loops +16 -0
- data/bin/loops-memory-stats +259 -0
- data/generators/loops/loops_generator.rb +28 -0
- data/generators/loops/templates/app/loops/APP_README +1 -0
- data/generators/loops/templates/app/loops/queue_loop.rb +8 -0
- data/generators/loops/templates/app/loops/simple_loop.rb +12 -0
- data/generators/loops/templates/config/loops.yml +34 -0
- data/generators/loops/templates/script/loops +20 -0
- data/init.rb +1 -0
- data/lib/loops.rb +167 -0
- data/lib/loops/autoload.rb +20 -0
- data/lib/loops/base.rb +148 -0
- data/lib/loops/cli.rb +35 -0
- data/lib/loops/cli/commands.rb +124 -0
- data/lib/loops/cli/options.rb +273 -0
- data/lib/loops/command.rb +36 -0
- data/lib/loops/commands/debug_command.rb +8 -0
- data/lib/loops/commands/list_command.rb +11 -0
- data/lib/loops/commands/start_command.rb +24 -0
- data/lib/loops/commands/stats_command.rb +5 -0
- data/lib/loops/commands/stop_command.rb +18 -0
- data/lib/loops/daemonize.rb +68 -0
- data/lib/loops/engine.rb +207 -0
- data/lib/loops/errors.rb +6 -0
- data/lib/loops/logger.rb +212 -0
- data/lib/loops/process_manager.rb +114 -0
- data/lib/loops/queue.rb +78 -0
- data/lib/loops/version.rb +31 -0
- data/lib/loops/worker.rb +101 -0
- data/lib/loops/worker_pool.rb +55 -0
- data/loops.gemspec +98 -0
- data/spec/loop_lock_spec.rb +61 -0
- data/spec/loops/base_spec.rb +92 -0
- data/spec/loops/cli_spec.rb +156 -0
- data/spec/loops_spec.rb +20 -0
- data/spec/rails/another_loop.rb +4 -0
- data/spec/rails/app/loops/complex_loop.rb +12 -0
- data/spec/rails/app/loops/simple_loop.rb +6 -0
- data/spec/rails/config.yml +6 -0
- data/spec/rails/config/boot.rb +1 -0
- data/spec/rails/config/environment.rb +5 -0
- data/spec/rails/config/loops.yml +13 -0
- data/spec/spec_helper.rb +110 -0
- metadata +121 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
module Loops
|
2
|
+
class ProcessManager
|
3
|
+
attr_reader :logger
|
4
|
+
|
5
|
+
def initialize(config, logger)
|
6
|
+
@config = {
|
7
|
+
'poll_period' => 1,
|
8
|
+
'workers_engine' => 'fork'
|
9
|
+
}.merge(config)
|
10
|
+
|
11
|
+
@logger = logger
|
12
|
+
@worker_pools = {}
|
13
|
+
@shutdown = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_workers(name, number, &blk)
|
17
|
+
raise ArgumentError, "Need a worker block!" unless block_given?
|
18
|
+
|
19
|
+
logger.debug("Creating a workers pool of #{number} workers for #{name} loop...")
|
20
|
+
@worker_pools[name] = Loops::WorkerPool.new(name, self, @config['workers_engine'], &blk)
|
21
|
+
@worker_pools[name].start_workers(number)
|
22
|
+
end
|
23
|
+
|
24
|
+
def monitor_workers
|
25
|
+
setup_signals
|
26
|
+
|
27
|
+
logger.info('Starting workers monitoring code...')
|
28
|
+
loop do
|
29
|
+
logger.debug("Checking workers' health...")
|
30
|
+
@worker_pools.each do |name, pool|
|
31
|
+
break if shutdown?
|
32
|
+
pool.check_workers
|
33
|
+
end
|
34
|
+
|
35
|
+
break if shutdown?
|
36
|
+
logger.debug("Sleeping for #{@config['poll_period']} seconds...")
|
37
|
+
sleep(@config['poll_period'])
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
unless wait_for_workers(10)
|
41
|
+
logger.info("Some workers are still alive after 10 seconds of waiting. Killing them...")
|
42
|
+
stop_workers(true)
|
43
|
+
wait_for_workers(5)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_signals
|
48
|
+
# Zombie reapers
|
49
|
+
trap('CHLD') {}
|
50
|
+
trap('EXIT') {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait_for_workers(seconds)
|
54
|
+
seconds.times do
|
55
|
+
logger.info("Shutting down... waiting for workers to die (we have #{seconds} seconds)...")
|
56
|
+
running_total = 0
|
57
|
+
|
58
|
+
@worker_pools.each do |name, pool|
|
59
|
+
running_total += pool.wait_workers
|
60
|
+
end
|
61
|
+
|
62
|
+
if running_total.zero?
|
63
|
+
logger.info("All workers are dead. Exiting...")
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
|
67
|
+
logger.info("#{running_total} workers are still running! Sleeping for a second...")
|
68
|
+
sleep(1)
|
69
|
+
end
|
70
|
+
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop_workers(force = false)
|
75
|
+
# Return if already shuting down (and not forced to stop)
|
76
|
+
return if shutdown? && !force
|
77
|
+
|
78
|
+
# Set shutdown flag
|
79
|
+
logger.info("Stopping workers#{force ? ' (forced)' : ''}...")
|
80
|
+
start_shutdown!
|
81
|
+
|
82
|
+
# Termination loop
|
83
|
+
@worker_pools.each do |name, pool|
|
84
|
+
pool.stop_workers(force)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def stop_workers!
|
89
|
+
# return if already shutting down
|
90
|
+
return if shutdown?
|
91
|
+
|
92
|
+
# Set shutdown flag
|
93
|
+
start_shutdown!
|
94
|
+
|
95
|
+
# Ask gently to stop
|
96
|
+
stop_workers(false)
|
97
|
+
|
98
|
+
# Give it a second
|
99
|
+
sleep(1)
|
100
|
+
|
101
|
+
# Forcefully stop the workers
|
102
|
+
stop_workers(true)
|
103
|
+
end
|
104
|
+
|
105
|
+
def shutdown?
|
106
|
+
@shutdown
|
107
|
+
end
|
108
|
+
|
109
|
+
def start_shutdown!
|
110
|
+
logger.info("Starting shutdown (shutdown flag set)...")
|
111
|
+
@shutdown = true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/loops/queue.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
begin
|
2
|
+
require 'stomp'
|
3
|
+
rescue LoadError
|
4
|
+
puts "Can't load stomp gem - all queue loops will be disabled!"
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'timeout'
|
8
|
+
|
9
|
+
module Loops
|
10
|
+
class Queue < Base
|
11
|
+
def self.check_dependencies
|
12
|
+
raise "No stomp gem installed!" unless defined?(Stomp::Client)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
create_client
|
17
|
+
|
18
|
+
config['queue_name'] ||= "/queue/loops/#{name}"
|
19
|
+
config['prefetch_size'] ||= 1
|
20
|
+
debug "Subscribing for the queue #{config['queue_name']}..."
|
21
|
+
|
22
|
+
headers = { :ack => :client }
|
23
|
+
headers["activemq.prefetchSize"] = config['prefetch_size'] if config['prefetch_size']
|
24
|
+
|
25
|
+
@total_served = 0
|
26
|
+
@client.subscribe(config['queue_name'], headers) do |msg|
|
27
|
+
begin
|
28
|
+
if config['action_timeout']
|
29
|
+
timeout(config['action_timeout']) { process_message(msg) }
|
30
|
+
else
|
31
|
+
process_message(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
@client.acknowledge(msg)
|
35
|
+
@total_served += 1
|
36
|
+
if config['max_requests'] && @total_served >= config['max_requests'].to_i
|
37
|
+
disconnect_client_and_exit
|
38
|
+
end
|
39
|
+
rescue Exception => e
|
40
|
+
error "Exception from process message! We won't be ACKing the message."
|
41
|
+
error "Details: #{e} at #{e.backtrace.first}"
|
42
|
+
disconnect_client_and_exit
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@client.join
|
47
|
+
rescue Exception => e
|
48
|
+
error "Closing queue connection because of exception: #{e} at #{e.backtrace.first}"
|
49
|
+
disconnect_client_and_exit
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_message(msg)
|
53
|
+
raise "This method process_message(msg) should be overriden in the loop class!"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def create_client
|
59
|
+
config['port'] ||= config['port'].to_i == 0 ? 61613 : config['port'].to_i
|
60
|
+
config['host'] ||= 'localhost'
|
61
|
+
|
62
|
+
@client = Stomp::Client.open(config['user'], config['password'], config['host'], config['port'], true)
|
63
|
+
setup_signals
|
64
|
+
end
|
65
|
+
|
66
|
+
def disconnect_client_and_exit
|
67
|
+
debug "Unsubscribing..."
|
68
|
+
@client.unsubscribe(name) rescue nil
|
69
|
+
@client.close() rescue nil
|
70
|
+
exit(0)
|
71
|
+
end
|
72
|
+
|
73
|
+
def setup_signals
|
74
|
+
Signal.trap('INT') { disconnect_client_and_exit }
|
75
|
+
Signal.trap('TERM') { disconnect_client_and_exit }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Contains information about currently used Loops version.
|
2
|
+
#
|
3
|
+
# @example
|
4
|
+
# puts "Loops #{Loops::Version}"
|
5
|
+
#
|
6
|
+
class Loops::Version
|
7
|
+
# @return [Hash<Symbol, Integer>]
|
8
|
+
# a +Hash+ containing major, minor, and patch version parts.
|
9
|
+
CURRENT = YAML.load_file(File.join(Loops::LIB_ROOT, '../VERSION.yml'))
|
10
|
+
|
11
|
+
# @return [Integer]
|
12
|
+
# a major part of the Loops version.
|
13
|
+
MAJOR = CURRENT[:major]
|
14
|
+
# @return [Integer]
|
15
|
+
# a minor part of the Loops version.
|
16
|
+
MINOR = CURRENT[:minor]
|
17
|
+
# @return [Integer]
|
18
|
+
# a patch part of the Loops version.
|
19
|
+
PATCH = CURRENT[:patch]
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
# a string representation of the Loops version.
|
23
|
+
STRING = "%d.%d.%d" % [MAJOR, MINOR, PATCH]
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
# a string representation of the Loops version.
|
27
|
+
#
|
28
|
+
def self.to_s
|
29
|
+
STRING
|
30
|
+
end
|
31
|
+
end
|
data/lib/loops/worker.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module Loops
|
2
|
+
class Worker
|
3
|
+
attr_reader :name
|
4
|
+
attr_reader :pid
|
5
|
+
|
6
|
+
def initialize(name, pm, engine, &blk)
|
7
|
+
raise ArgumentError, "Need a worker block!" unless block_given?
|
8
|
+
|
9
|
+
@name = name
|
10
|
+
@pm = pm
|
11
|
+
@engine = engine
|
12
|
+
@worker_block = blk
|
13
|
+
end
|
14
|
+
|
15
|
+
def logger
|
16
|
+
@pm.logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def shutdown?
|
20
|
+
@pm.shutdown?
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
return if shutdown?
|
25
|
+
if @engine == 'fork'
|
26
|
+
# Enable COW-friendly garbage collector in Ruby Enterprise Edition
|
27
|
+
# See http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow for more details
|
28
|
+
if GC.respond_to?(:copy_on_write_friendly=)
|
29
|
+
GC.copy_on_write_friendly = true
|
30
|
+
end
|
31
|
+
|
32
|
+
@pid = Kernel.fork do
|
33
|
+
@pid = Process.pid
|
34
|
+
normal_exit = false
|
35
|
+
begin
|
36
|
+
$0 = "loop worker: #{@name}\0"
|
37
|
+
@worker_block.call
|
38
|
+
normal_exit = true
|
39
|
+
exit(0)
|
40
|
+
rescue Exception => e
|
41
|
+
message = SystemExit === e ? "exit(#{e.status})" : e.to_s
|
42
|
+
if SystemExit === e and e.success?
|
43
|
+
if normal_exit
|
44
|
+
logger.info("Worker finished: normal return")
|
45
|
+
else
|
46
|
+
logger.info("Worker exited: #{message} at #{e.backtrace.first}")
|
47
|
+
end
|
48
|
+
else
|
49
|
+
logger.fatal("Worker exited with error: #{message}\n #{e.backtrace.join("\n ")}")
|
50
|
+
end
|
51
|
+
logger.fatal("Terminating #{@name} worker: #{@pid}")
|
52
|
+
raise # so that the error gets written to stderr
|
53
|
+
end
|
54
|
+
end
|
55
|
+
elsif @engine == 'thread'
|
56
|
+
@thread = Thread.start do
|
57
|
+
@worker_block.call
|
58
|
+
end
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
61
|
+
end
|
62
|
+
rescue Exception => e
|
63
|
+
logger.error("Exception from worker: #{e} at #{e.backtrace.first}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def running?(verbose = false)
|
67
|
+
if @engine == 'fork'
|
68
|
+
return false unless @pid
|
69
|
+
begin
|
70
|
+
Process.waitpid(@pid, Process::WNOHANG)
|
71
|
+
res = Process.kill(0, @pid)
|
72
|
+
logger.debug("KILL(#{@pid}) = #{res}") if verbose
|
73
|
+
return true
|
74
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM => e
|
75
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}") if verbose
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
elsif @engine == 'thread'
|
79
|
+
@thread && @thread.alive?
|
80
|
+
else
|
81
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop(force = false)
|
86
|
+
if @engine == 'fork'
|
87
|
+
begin
|
88
|
+
sig = force ? 'SIGKILL' : 'SIGTERM'
|
89
|
+
logger.debug("Sending #{sig} to ##{@pid}")
|
90
|
+
Process.kill(sig, @pid)
|
91
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM=> e
|
92
|
+
logger.error("Exception from kill: #{e} at #{e.backtrace.first}")
|
93
|
+
end
|
94
|
+
elsif @engine == 'thread'
|
95
|
+
force && !defined?(::JRuby) ? @thread.kill! : @thread.kill
|
96
|
+
else
|
97
|
+
raise ArgumentError, "Invalid engine name: #{@engine}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Loops
|
2
|
+
class WorkerPool
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, pm, engine, &blk)
|
6
|
+
@name = name
|
7
|
+
@pm = pm
|
8
|
+
@worker_block = blk
|
9
|
+
@engine = engine
|
10
|
+
@workers = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def logger
|
14
|
+
@pm.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def shutdown?
|
18
|
+
@pm.shutdown?
|
19
|
+
end
|
20
|
+
|
21
|
+
def start_workers(number)
|
22
|
+
logger.info("Creating #{number} workers for #{name} loop...")
|
23
|
+
number.times do
|
24
|
+
@workers << Worker.new(name, @pm, @engine, &@worker_block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def check_workers
|
29
|
+
logger.debug("Checking loop #{name} workers...")
|
30
|
+
@workers.each do |worker|
|
31
|
+
next if worker.running? || worker.shutdown?
|
32
|
+
logger.info("Worker #{worker.name} is not running. Restart!")
|
33
|
+
worker.run
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait_workers
|
38
|
+
running = 0
|
39
|
+
@workers.each do |worker|
|
40
|
+
next unless worker.running?
|
41
|
+
running += 1
|
42
|
+
logger.info("Worker #{name} is still running (#{worker.pid})")
|
43
|
+
end
|
44
|
+
return running
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop_workers(force)
|
48
|
+
logger.info("Stopping loop #{name} workers...")
|
49
|
+
@workers.each do |worker|
|
50
|
+
next unless worker.running?
|
51
|
+
worker.stop(force)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/loops.gemspec
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{loops}
|
8
|
+
s.version = "2.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Alexey Kovyrin", "Dmytro Shteflyuk"]
|
12
|
+
s.date = %q{2010-03-19}
|
13
|
+
s.description = %q{Loops is a small and lightweight framework for Ruby on Rails, Merb and other ruby frameworks 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).}
|
14
|
+
s.email = %q{alexey@kovyrin.net}
|
15
|
+
s.executables = ["loops", "loops-memory-stats"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE",
|
18
|
+
"README.rdoc"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION.yml",
|
26
|
+
"bin/loops",
|
27
|
+
"bin/loops-memory-stats",
|
28
|
+
"generators/loops/loops_generator.rb",
|
29
|
+
"generators/loops/templates/app/loops/APP_README",
|
30
|
+
"generators/loops/templates/app/loops/queue_loop.rb",
|
31
|
+
"generators/loops/templates/app/loops/simple_loop.rb",
|
32
|
+
"generators/loops/templates/config/loops.yml",
|
33
|
+
"generators/loops/templates/script/loops",
|
34
|
+
"init.rb",
|
35
|
+
"lib/loops.rb",
|
36
|
+
"lib/loops/autoload.rb",
|
37
|
+
"lib/loops/base.rb",
|
38
|
+
"lib/loops/cli.rb",
|
39
|
+
"lib/loops/cli/commands.rb",
|
40
|
+
"lib/loops/cli/options.rb",
|
41
|
+
"lib/loops/command.rb",
|
42
|
+
"lib/loops/commands/debug_command.rb",
|
43
|
+
"lib/loops/commands/list_command.rb",
|
44
|
+
"lib/loops/commands/start_command.rb",
|
45
|
+
"lib/loops/commands/stats_command.rb",
|
46
|
+
"lib/loops/commands/stop_command.rb",
|
47
|
+
"lib/loops/daemonize.rb",
|
48
|
+
"lib/loops/engine.rb",
|
49
|
+
"lib/loops/errors.rb",
|
50
|
+
"lib/loops/logger.rb",
|
51
|
+
"lib/loops/process_manager.rb",
|
52
|
+
"lib/loops/queue.rb",
|
53
|
+
"lib/loops/version.rb",
|
54
|
+
"lib/loops/worker.rb",
|
55
|
+
"lib/loops/worker_pool.rb",
|
56
|
+
"loops.gemspec",
|
57
|
+
"spec/loop_lock_spec.rb",
|
58
|
+
"spec/loops/base_spec.rb",
|
59
|
+
"spec/loops/cli_spec.rb",
|
60
|
+
"spec/loops_spec.rb",
|
61
|
+
"spec/rails/another_loop.rb",
|
62
|
+
"spec/rails/app/loops/complex_loop.rb",
|
63
|
+
"spec/rails/app/loops/simple_loop.rb",
|
64
|
+
"spec/rails/config.yml",
|
65
|
+
"spec/rails/config/boot.rb",
|
66
|
+
"spec/rails/config/environment.rb",
|
67
|
+
"spec/rails/config/loops.yml",
|
68
|
+
"spec/spec_helper.rb"
|
69
|
+
]
|
70
|
+
s.homepage = %q{http://github.com/kovyrin/loops}
|
71
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
72
|
+
s.require_paths = ["lib"]
|
73
|
+
s.rubygems_version = %q{1.3.6}
|
74
|
+
s.summary = %q{Simple background loops framework for ruby}
|
75
|
+
s.test_files = [
|
76
|
+
"spec/loop_lock_spec.rb",
|
77
|
+
"spec/loops/base_spec.rb",
|
78
|
+
"spec/loops/cli_spec.rb",
|
79
|
+
"spec/loops_spec.rb",
|
80
|
+
"spec/rails/another_loop.rb",
|
81
|
+
"spec/rails/app/loops/complex_loop.rb",
|
82
|
+
"spec/rails/app/loops/simple_loop.rb",
|
83
|
+
"spec/rails/config/boot.rb",
|
84
|
+
"spec/rails/config/environment.rb",
|
85
|
+
"spec/spec_helper.rb"
|
86
|
+
]
|
87
|
+
|
88
|
+
if s.respond_to? :specification_version then
|
89
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
90
|
+
s.specification_version = 3
|
91
|
+
|
92
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
93
|
+
else
|
94
|
+
end
|
95
|
+
else
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|