loops 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +8 -0
  2. data/LICENSE +21 -0
  3. data/README.rdoc +238 -0
  4. data/Rakefile +48 -0
  5. data/VERSION.yml +5 -0
  6. data/bin/loops +16 -0
  7. data/bin/loops-memory-stats +259 -0
  8. data/generators/loops/loops_generator.rb +28 -0
  9. data/generators/loops/templates/app/loops/APP_README +1 -0
  10. data/generators/loops/templates/app/loops/queue_loop.rb +8 -0
  11. data/generators/loops/templates/app/loops/simple_loop.rb +12 -0
  12. data/generators/loops/templates/config/loops.yml +34 -0
  13. data/generators/loops/templates/script/loops +20 -0
  14. data/init.rb +1 -0
  15. data/lib/loops.rb +167 -0
  16. data/lib/loops/autoload.rb +20 -0
  17. data/lib/loops/base.rb +148 -0
  18. data/lib/loops/cli.rb +35 -0
  19. data/lib/loops/cli/commands.rb +124 -0
  20. data/lib/loops/cli/options.rb +273 -0
  21. data/lib/loops/command.rb +36 -0
  22. data/lib/loops/commands/debug_command.rb +8 -0
  23. data/lib/loops/commands/list_command.rb +11 -0
  24. data/lib/loops/commands/start_command.rb +24 -0
  25. data/lib/loops/commands/stats_command.rb +5 -0
  26. data/lib/loops/commands/stop_command.rb +18 -0
  27. data/lib/loops/daemonize.rb +68 -0
  28. data/lib/loops/engine.rb +207 -0
  29. data/lib/loops/errors.rb +6 -0
  30. data/lib/loops/logger.rb +212 -0
  31. data/lib/loops/process_manager.rb +114 -0
  32. data/lib/loops/queue.rb +78 -0
  33. data/lib/loops/version.rb +31 -0
  34. data/lib/loops/worker.rb +101 -0
  35. data/lib/loops/worker_pool.rb +55 -0
  36. data/loops.gemspec +98 -0
  37. data/spec/loop_lock_spec.rb +61 -0
  38. data/spec/loops/base_spec.rb +92 -0
  39. data/spec/loops/cli_spec.rb +156 -0
  40. data/spec/loops_spec.rb +20 -0
  41. data/spec/rails/another_loop.rb +4 -0
  42. data/spec/rails/app/loops/complex_loop.rb +12 -0
  43. data/spec/rails/app/loops/simple_loop.rb +6 -0
  44. data/spec/rails/config.yml +6 -0
  45. data/spec/rails/config/boot.rb +1 -0
  46. data/spec/rails/config/environment.rb +5 -0
  47. data/spec/rails/config/loops.yml +13 -0
  48. data/spec/spec_helper.rb +110 -0
  49. 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
@@ -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
@@ -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
@@ -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
+