loops 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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