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,28 @@
1
+ # This generator bootstraps a Rails project for use with loops
2
+ class LoopsGenerator < Rails::Generator::Base
3
+ def manifest
4
+ record do |m|
5
+ # Generate app/loops directory and an example loop files
6
+ m.directory 'app'
7
+ m.directory 'app/loops'
8
+ m.file 'app/loops/APP_README', 'app/loops/README'
9
+ m.file 'app/loops/simple_loop.rb', 'app/loops/simple_loop.rb'
10
+ m.file 'app/loops/queue_loop.rb', 'app/loops/queue_loop.rb'
11
+
12
+ # Generate script/loops file
13
+ m.directory 'script'
14
+ m.file 'script/loops', 'script/loops', :chmod => 0755
15
+
16
+ # Generate config/loops.yml file
17
+ m.directory 'config'
18
+ m.file 'config/loops.yml', 'config/loops.yml'
19
+ end
20
+ end
21
+
22
+ protected
23
+
24
+ def banner
25
+ "Usage: #{$0} loops"
26
+ end
27
+
28
+ end
@@ -0,0 +1 @@
1
+ This directory should be used to hold all application loops.
@@ -0,0 +1,8 @@
1
+ class QueueLoop < Loops::Queue
2
+ def process_message(message)
3
+ debug "Received a message: #{message.body}"
4
+ debug "sleeping..."
5
+ sleep(0.5 + rand(10) / 10.0)
6
+ debug "done..."
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # Simple loop with its own custom run method
2
+ #
3
+ # Does nothing aside from printing loop's name, pid and current time every second
4
+ #
5
+ class SimpleLoop < Loops::Base
6
+ def run
7
+ with_period_of(1) do
8
+ debug(Time.now)
9
+ sleep(5)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # This file is a configuration file for loops rails plugin
2
+ #
3
+
4
+ # This section is used to control loops manager
5
+ global:
6
+ logger: stdout
7
+ poll_period: 5
8
+ workers_engine: fork
9
+
10
+ # Each record in this section represents one loop which could be ran using loops plugin.
11
+ # Each loop should have a file in app/loops directory with the same name as its config record.
12
+ loops:
13
+
14
+ # Simple time printing loop
15
+ simple:
16
+ type: simple
17
+ workers_number: 1
18
+ logger: default
19
+
20
+ # An example of a STOMP queue-based loop
21
+ queue:
22
+ type: queue
23
+ workers_number: 2
24
+ logger: log/loops/queue.log
25
+ # logger_rotate: daily # TODO
26
+ # logger_max_size: 10000000 # TODO
27
+
28
+ host: 127.0.0.1
29
+ port: 61613
30
+ disabled: true
31
+
32
+ # This loop has no loop class file so it should not be started
33
+ non_existent:
34
+ type: simple
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ vendored_loops_binary = Dir[File.join(File.dirname(__FILE__),
4
+ '..',
5
+ 'vendor',
6
+ '{gems,plugins}',
7
+ 'loops*',
8
+ 'bin',
9
+ 'loops')].first
10
+
11
+ # Initialize loops root variable
12
+ ENV['LOOPS_ROOT'] = File.expand_path(File.join(File.dirname(__FILE__), '..'))
13
+
14
+ if vendored_loops_binary
15
+ load File.expand_path(vendored_loops_binary)
16
+ else
17
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
18
+ require 'loops'
19
+ load Loops::BINARY
20
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'lib/loops')
@@ -0,0 +1,167 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'pathname'
4
+
5
+ module Loops
6
+ # @return [String]
7
+ # a full path to the loops "lib" directory.
8
+ LIB_ROOT = File.expand_path(File.dirname(__FILE__)) unless const_defined?('LIB_ROOT')
9
+ # @return [String]
10
+ # a full path to the loops binary file.
11
+ BINARY = File.expand_path(File.join(LIB_ROOT, '../bin/loops')) unless const_defined?('BINARY')
12
+
13
+ # Loops root directory.
14
+ #
15
+ # Usually it is initialized with framework's root dir (RAILS_ROOT or MERB_ROOT),
16
+ # but you can specify another directory using command line arguments.
17
+ #
18
+ # Loops current directory will is set to this value (chdir).
19
+ #
20
+ # @return [Pathname, nil]
21
+ # the loops root directory.
22
+ #
23
+ def self.root
24
+ @@root
25
+ end
26
+
27
+ # Set loops root directory.
28
+ #
29
+ # This is internal method used to set the loops root directory.
30
+ #
31
+ # @param [String] path
32
+ # the absolute path of the loops root directory.
33
+ # @return [Pathname]
34
+ # the loops root directory.
35
+ #
36
+ # @private
37
+ #
38
+ def self.root=(path)
39
+ @@root = Pathname.new(path).realpath
40
+ end
41
+
42
+ # Get loops config file full path.
43
+ #
44
+ # @return [Pathname]
45
+ # the loops config file path.
46
+ #
47
+ def self.config_file
48
+ @@config_file ||= root.join('config/loops.yml')
49
+ end
50
+
51
+ # Set loops config file path.
52
+ #
53
+ # This is internal method used to set the loops config file path.
54
+ #
55
+ # @param [String] path
56
+ # the absolute or relative to the loops root path of the loops
57
+ # config file.
58
+ # @return [Pathname]
59
+ # the loops config file path.
60
+ #
61
+ # @private
62
+ #
63
+ def self.config_file=(config_file)
64
+ @@config_file = root.join(config_file) if config_file
65
+ end
66
+
67
+ # Get directory containing loops classes.
68
+ #
69
+ # @return [Pathname]
70
+ # the loops directory path.
71
+ #
72
+ def self.loops_root
73
+ @@loops_root ||= root.join('app/loops')
74
+ end
75
+
76
+ # Set loops classes directory path.
77
+ #
78
+ # This is internal method used to set directory where loops classes
79
+ # will be searched.
80
+ #
81
+ # @param [String] path
82
+ # the absolute or relative to the loops classes directory.
83
+ # @return [Pathname]
84
+ # the loops classes directory path.
85
+ #
86
+ # @private
87
+ #
88
+ def self.loops_root=(loops_root)
89
+ @@loops_root = root.join(loops_root) if loops_root
90
+ end
91
+
92
+ # Get the loops monitor process pid file path.
93
+ #
94
+ # @return [Pathname]
95
+ # the loops monitor process pid file path.
96
+ #
97
+ def self.pid_file
98
+ @@pid_file ||= root.join('loops.pid')
99
+ end
100
+
101
+ # Set the loops monitor pid file path.
102
+ #
103
+ # This is internal method used to set the loops monitor pid file path.
104
+ #
105
+ # @param [String] path
106
+ # the absolute or relative to the loops monitor pid file.
107
+ # @return [Pathname]
108
+ # the loops monitor pid file path.
109
+ #
110
+ # @private
111
+ #
112
+ def self.pid_file=(pid_file)
113
+ @@pid_file = root.join(pid_file) if pid_file
114
+ end
115
+
116
+ # Get the current loops logger.
117
+ #
118
+ # There are two contexts where different devices (usually) will be
119
+ # configured for this logger instance:
120
+ #
121
+ # 1. In context of loops monitor logger device will be retrieved from
122
+ # the global section of the loops config file, or STDOUT when it
123
+ # was not configured.
124
+ # 2. In context of loop proccess logger device will be configured
125
+ # based on logger value of the particular loop section in the config
126
+ # file.
127
+ #
128
+ # @example Put all Rails logging into the loop log file (add this to the environment.rb)
129
+ # Rails.logger = Loops.logger
130
+ #
131
+ # @return [Loops::Logger]
132
+ # the current loops logger instance.
133
+ #
134
+ def self.logger
135
+ @@logger ||= ::Loops::Logger.new($stdout)
136
+ end
137
+
138
+ # Set the current loops logger.
139
+ #
140
+ def self.logger=(logger)
141
+ @@logger = logger
142
+ end
143
+
144
+ # Get the current framework's default logger.
145
+ #
146
+ # @return [Logger]
147
+ # the default logger for currently used framework.
148
+ #
149
+ def self.default_logger
150
+ @@default_logger
151
+ end
152
+
153
+ # Set the current framework's default logger.
154
+ #
155
+ # @param [Logger] logger
156
+ # the default logger for currently used framework.
157
+ # @return [Logger]
158
+ # the default logger for currently used framework.
159
+ #
160
+ # @private
161
+ #
162
+ def self.default_logger=(logger)
163
+ @@default_logger = logger
164
+ end
165
+ end
166
+
167
+ require File.join(Loops::LIB_ROOT, 'loops/autoload')
@@ -0,0 +1,20 @@
1
+ module Loops
2
+ # @private
3
+ def self.__p(*path) File.join(Loops::LIB_ROOT, 'loops', *path) end
4
+
5
+ autoload :Base, __p('base')
6
+ autoload :CLI, __p('cli')
7
+ autoload :Command, __p('command')
8
+ autoload :Commands, __p('command')
9
+ autoload :Daemonize, __p('daemonize')
10
+ autoload :Engine, __p('engine')
11
+ autoload :Errors, __p('errors')
12
+ autoload :Logger, __p('logger')
13
+ autoload :ProcessManager, __p('process_manager')
14
+ autoload :Queue, __p('queue')
15
+ autoload :Worker, __p('worker')
16
+ autoload :WorkerPool, __p('worker_pool')
17
+ autoload :Version, __p('version')
18
+
19
+ include Errors
20
+ end
@@ -0,0 +1,148 @@
1
+ # Base class for all loop processes.
2
+ #
3
+ # To create a new loop just inherit this class and override the
4
+ # {#run} method (see example below).
5
+ #
6
+ # In most cases it's a good idea to re-run your loop periodically.
7
+ # In this case your process will free all unused memory to the system,
8
+ # and all leaked resources (yes, it's real life).
9
+ #
10
+ # @example
11
+ # class MySuperLoop < Loops::Base
12
+ # def self.check_dependencies
13
+ # gem 'tinder', '=1.3.1'
14
+ # end
15
+ #
16
+ # def run
17
+ # 1000.times do
18
+ # if shutdown?
19
+ # info("Shutting down!")
20
+ # exit(0)
21
+ # end
22
+ #
23
+ # unless item = UploadItems.get_next
24
+ # sleep(config['sleep_time'])
25
+ # next
26
+ # end
27
+ #
28
+ # item.perform_upload
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ class Loops::Base
34
+ # @return [String]
35
+ # loop name.
36
+ attr_reader :name
37
+
38
+ # @return [Hash<String, Object>]
39
+ # The hash of loop options from config.
40
+ attr_reader :config
41
+
42
+ # Initializes a new instance of loop.
43
+ #
44
+ # @param [ProcessManager] pm
45
+ # the instance of process manager.
46
+ # @param [String] name
47
+ # the loop name.
48
+ # @param [Hash<String, Object>]
49
+ # the loop configuration options from the config file.
50
+ #
51
+ def initialize(pm, name, config)
52
+ @pm = pm
53
+ @name = name
54
+ @config = config
55
+ end
56
+
57
+ # Get the logger instance.
58
+ #
59
+ # @return [Logger]
60
+ # the logger instance.
61
+ #
62
+ def logger
63
+ @pm.logger
64
+ end
65
+
66
+ # Get a value indicating whether shutdown is in the progress.
67
+ #
68
+ # Check this flag periodically and if loop is in the shutdown,
69
+ # close all open handlers, update your data, and exit loop as
70
+ # soon as possible to not to loose any sensitive data.
71
+ #
72
+ # @return [Boolean]
73
+ # a value indicating whether shutdown is in the progress.
74
+ #
75
+ # @example
76
+ # if shutdown?
77
+ # info('Shutting down!')
78
+ # exit(0)
79
+ # end
80
+ #
81
+ def shutdown?
82
+ @pm.shutdown?
83
+ end
84
+
85
+ # Verifies loop dependencies.
86
+ #
87
+ # Override this method if your loop depends on any external
88
+ # libraries, resources, etc. Verify your dependencies here,
89
+ # and raise an exception in case of any trouble to disallow
90
+ # this loop to start.
91
+ #
92
+ # @example
93
+ # def self.check_dependencies
94
+ # gem 'tinder', '=1.3.1'
95
+ # end
96
+ #
97
+ def self.check_dependencies
98
+ end
99
+
100
+ # A loop entry point. Should be overridden in descendants.
101
+ #
102
+ def run
103
+ raise 'Generic loop has nothing to do'
104
+ end
105
+
106
+ # Proxy logger calls to our logger
107
+ [ :debug, :info, :warn, :error, :fatal ].each do |meth_name|
108
+ class_eval <<-EVAL, __FILE__, __LINE__
109
+ def #{meth_name}(message)
110
+ logger.#{meth_name}("loop[\#{name}/\#{Process.pid}]: \#{message}")
111
+ end
112
+ EVAL
113
+ end
114
+
115
+ def with_lock(entity_ids, loop_id, timeout, entity_name = '', &block)
116
+ entity_name = 'item' if entity_name.to_s.empty?
117
+ entity_ids = [entity_ids] unless Array === entity_ids
118
+
119
+ entity_ids.each do |entity_id|
120
+ debug("Locking #{entity_name} #{entity_id}")
121
+ lock = LoopLock.lock(:entity_id => entity_id, :loop => loop_id.to_s, :timeout => timeout)
122
+ unless lock
123
+ warn("Race condition detected for the #{entity_name}: #{entity_id}. Skipping the item.")
124
+ next
125
+ end
126
+
127
+ begin
128
+ result = if block.arity == 1
129
+ yield entity_id
130
+ else
131
+ yield
132
+ end
133
+ return result
134
+ ensure
135
+ debug("Unlocking #{entity_name} #{entity_id}")
136
+ LoopLock.unlock(:entity_id => entity_id, :loop => loop_id.to_s)
137
+ end
138
+ end
139
+ end
140
+
141
+ def with_period_of(seconds)
142
+ raise ArgumentError, "No block given!" unless block_given?
143
+ loop do
144
+ yield
145
+ sleep(seconds)
146
+ end
147
+ end
148
+ end