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,35 @@
1
+ %w(commands options).each { |p| require File.join(Loops::LIB_ROOT, 'loops/cli', p) }
2
+
3
+ module Loops
4
+ # Command line interface for the Loops system.
5
+ #
6
+ # Used to parse command line options, initialize engine, and
7
+ # execute command requested.
8
+ #
9
+ # @example
10
+ # Loops::CLI.execute
11
+ #
12
+ class CLI
13
+ include Commands, Options
14
+
15
+ # Register all available commands.
16
+ register_command :list
17
+ register_command :debug
18
+ register_command :start
19
+ register_command :stop
20
+ register_command :stats
21
+
22
+ # @return [Array<String>]
23
+ # The +Array+ of (unparsed) command-line options.
24
+ attr_reader :args
25
+
26
+ # Initializes a new instance of the {CLI} class.
27
+ #
28
+ # @param [Array<String>] args
29
+ # an +Array+ of command line arguments.
30
+ #
31
+ def initialize(args)
32
+ @args = args.dup
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,124 @@
1
+ module Loops
2
+ class CLI
3
+ # Contains methods related to Loops commands: retrieving, instantiating,
4
+ # executing.
5
+ #
6
+ # @example
7
+ # Loops::CLI.execute(ARGV)
8
+ #
9
+ module Commands
10
+ # @private
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ # Parse arguments, find and execute command requested.
17
+ #
18
+ def execute
19
+ parse(ARGV).run!
20
+ end
21
+
22
+ # Register a Loops command.
23
+ #
24
+ # @param [Symbol, String] command_name
25
+ # a command name to register.
26
+ #
27
+ def register_command(command_name)
28
+ @@commands ||= {}
29
+ @@commands[command_name.to_sym] = nil
30
+ end
31
+
32
+ # Get a list of command names.
33
+ #
34
+ # @return [Array<String>]
35
+ # an +Array+ of command names.
36
+ #
37
+ def command_names
38
+ @@commands.keys.map { |c| c.to_s }
39
+ end
40
+
41
+ # Return the registered command from the command name.
42
+ #
43
+ # @param [Symbol, String] command_name
44
+ # a command name to register.
45
+ # @return [Command, nil]
46
+ # an instance of requested command.
47
+ #
48
+ def [](command_name)
49
+ command_name = command_name.to_sym
50
+ @@commands[command_name] ||= load_and_instantiate(command_name)
51
+ end
52
+
53
+ # Load and instantiate a given command.
54
+ #
55
+ # @param [Symbol, String] command_name
56
+ # a command name to register.
57
+ # @return [Command, nil]
58
+ # an instantiated command or +nil+, when command is not found.
59
+ #
60
+ def load_and_instantiate(command_name)
61
+ command_name = command_name.to_s
62
+ retried = false
63
+
64
+ begin
65
+ const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase }
66
+ Loops::Commands.const_get("#{const_name}Command").new
67
+ rescue NameError
68
+ if retried then
69
+ nil
70
+ else
71
+ retried = true
72
+ require File.join(Loops::LIB_ROOT, 'loops/commands', "#{command_name}_command")
73
+ retry
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Run command requested.
80
+ #
81
+ # Finds, instantiates and invokes a command.
82
+ #
83
+ def run!
84
+ if cmd = find_command(options[:command])
85
+ cmd.invoke(engine, options)
86
+ else
87
+ STDERR << option_parser
88
+ exit
89
+ end
90
+ end
91
+
92
+ # Find and return an instance of {Command} by command name.
93
+ #
94
+ # @param [Symbol, String] command_name
95
+ # a command name to register.
96
+ # @return [Command, nil]
97
+ # an instantiated command or +nil+, when command is not found.
98
+ #
99
+ def find_command(command_name)
100
+ possibilities = find_command_possibilities(command_name)
101
+ if possibilities.size > 1 then
102
+ raise Loops::InvalidCommandError, "Ambiguous command #{command_name} matches [#{possibilities.join(', ')}]"
103
+ elsif possibilities.size < 1 then
104
+ raise Loops::InvalidCommandError, "Unknown command #{command_name}"
105
+ end
106
+
107
+ self.class[possibilities.first]
108
+ end
109
+
110
+ # Find command possibilities (used to find command by a short name).
111
+ #
112
+ # @param [Symbol, String] command_name
113
+ # a command name to register.
114
+ # @return [Array<String>]
115
+ # a list of possible commands matched to the specified short or
116
+ # full name.
117
+ #
118
+ def find_command_possibilities(command_name)
119
+ len = command_name.length
120
+ self.class.command_names.select { |c| command_name == c[0, len] }
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,273 @@
1
+ require 'pathname'
2
+ require 'optparse'
3
+
4
+ module Loops
5
+ class CLI
6
+ # Contains methods to parse startup options, bootstrap application,
7
+ # and prepare #{CLI} class to run.
8
+ #
9
+ # @example
10
+ # Loops::CLI.parse(ARGV)
11
+ #
12
+ module Options
13
+ # @private
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ module ClassMethods
19
+ # Return a new {CLI} instance with the given arguments pre-parsed and
20
+ # ready for execution.
21
+ #
22
+ # @param [Array<String>] args
23
+ # an +Array+ of options.
24
+ # @return [CLI]
25
+ # an instance of {CLI} with the given arguments pre-parsed.
26
+ #
27
+ def parse(args)
28
+ cli = new(args)
29
+ cli.parse_options!
30
+ cli
31
+ end
32
+ end
33
+
34
+ # @return [Hash<Symbol, Object>]
35
+ # The hash of (parsed) command-line options.
36
+ attr_reader :options
37
+
38
+ # @return [Engine]
39
+ # The loops engine instance.
40
+ attr_reader :engine
41
+
42
+ # Returns an option parser configured with all options
43
+ # available.
44
+ #
45
+ # @return [OptionParser]
46
+ # an option parser instance.
47
+ #
48
+ def option_parser
49
+ @option_parser ||= OptionParser.new do |opt|
50
+ opt.banner = "Usage: #{File.basename($0)} command [arg1 [arg2]] [options]"
51
+ opt.separator ''
52
+ opt.separator COMMANDS_HELP
53
+ opt.separator ''
54
+ opt.separator 'Specific options:'
55
+
56
+ opt.on('-c', '--config=file', 'Configuration file') do |config_file|
57
+ options[:config_file] = config_file
58
+ end
59
+
60
+ opt.on('-d', '--daemonize', 'Daemonize when all loops started') do |value|
61
+ options[:daemonize] = true
62
+ end
63
+
64
+ opt.on('-e', '--environment=env', 'Set RAILS_ENV (MERB_ENV) value') do |env|
65
+ options[:environment] = env
66
+ end
67
+
68
+ opt.on('-f', '--framework=name', "Bootstraps Rails (rails - default value) or Merb (merb) before#{SPLIT_HELP_LINE}starting loops. Use \"none\" for plain ruby loops.") do |framework|
69
+ options[:framework] = framework
70
+ end
71
+
72
+ opt.on('-l', '--loops=dir', 'Root directory with loops classes') do |loops_root|
73
+ options[:loops_root] = loops_root
74
+ end
75
+
76
+ opt.on('-p', '--pid=file', 'Override loops.yml pid_file option') do |pid_file|
77
+ options[:pid_file] = pid_file
78
+ end
79
+
80
+ opt.on('-r', '--root=dir', 'Root directory which will be used as a loops home dir (chdir)') do |root|
81
+ options[:root] = root
82
+ end
83
+
84
+ opt.on('-Rlibrary', '--require=library', 'require the library before executing the script') do |library|
85
+ require library
86
+ end
87
+
88
+ opt.on_tail("-h", '--help', 'Show this message') do
89
+ puts(opt)
90
+ exit(0)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Parses startup options, bootstraps application, starts loops engine.
96
+ #
97
+ # Method exits process when unknown option passed or
98
+ # invalid value specified.
99
+ #
100
+ # @return [Hash]
101
+ # a hash of parsed options.
102
+ #
103
+ def parse_options!
104
+ @options = {
105
+ :daemonize => false,
106
+ :config_file => 'config/loops.yml',
107
+ :environment => nil,
108
+ :framework => 'rails',
109
+ :loops_root => 'app/loops',
110
+ :pid_file => nil,
111
+ :root => nil
112
+ }
113
+
114
+ begin
115
+ option_parser.parse!(args)
116
+ rescue OptionParser::ParseError => e
117
+ STDERR.puts e.message
118
+ STDERR << "\n" << option_parser
119
+ exit
120
+ end
121
+
122
+ # Root directory
123
+ guess_root_dir
124
+ Loops.root = options.delete(:root)
125
+ Dir.chdir(Loops.root)
126
+
127
+ # Config file
128
+ Loops.config_file = options.delete(:config_file)
129
+ # Loops root
130
+ Loops.loops_root = options.delete(:loops_root)
131
+
132
+ extract_command!
133
+ if options[:command].nil? || options[:command] == 'help'
134
+ puts option_parser
135
+ exit
136
+ end
137
+
138
+ bootstrap!
139
+ start_engine!
140
+
141
+ # Pid file
142
+ Loops.pid_file = options.delete(:pid_file)
143
+
144
+ @options
145
+ end
146
+
147
+ # Extracts command name from arguments.
148
+ #
149
+ # Other parameters are stored in the <tt>:args</tt> option
150
+ # of the {#options} hash.
151
+ #
152
+ # @return [String]
153
+ # a command name passed.
154
+ #
155
+ def extract_command!
156
+ options[:command], *options[:args] = args
157
+ options[:command]
158
+ end
159
+
160
+ # Detect the application root directory (contatining "app"
161
+ # subfolder).
162
+ #
163
+ # @return [String]
164
+ # absolute path of the application root directory.
165
+ #
166
+ def guess_root_dir
167
+ # Check for environment variable LOOP_ROOT containing
168
+ # the application root folder
169
+ return options[:root] = ENV['LOOPS_ROOT'] if ENV['LOOPS_ROOT']
170
+ # Check root parameter
171
+ return options[:root] if options[:root]
172
+
173
+ # Try to detect root dir (should contain app subfolder)
174
+ current_dir = Dir.pwd
175
+ loop do
176
+ if File.directory?(File.join(current_dir, 'app'))
177
+ # Found it!
178
+ return options[:root] = current_dir
179
+ end
180
+
181
+ # Move up the FS hierarhy
182
+ pwd = File.expand_path(File.join(current_dir, '..'))
183
+ break if pwd == current_dir # if changing the directory made no difference, then we're at the top
184
+ current_dir = pwd
185
+ end
186
+
187
+ # Oops, not app folder found. Use the current dir as the root
188
+ current_dir = Dir.pwd
189
+ options[:root] = current_dir
190
+ end
191
+
192
+ # Application bootstrap.
193
+ #
194
+ # Checks framework option passed and load application
195
+ # stratup files conrresponding to its value. Also intitalizes
196
+ # the {Loops.default_logger} variable with the framework's
197
+ # default logger value.
198
+ #
199
+ # @return [String]
200
+ # the used framework name (rails, merb, or none).
201
+ # @raise [InvalidFrameworkError]
202
+ # occurred when unknown framework option value passed.
203
+ #
204
+ def bootstrap!
205
+ case options[:framework]
206
+ when 'rails'
207
+ ENV['RAILS_ENV'] = options[:environment] if options[:environment]
208
+
209
+ # Bootstrap Rails
210
+ require Loops.root + 'config/boot'
211
+ require Loops.root + 'config/environment'
212
+
213
+ # Loops default logger
214
+ Loops.default_logger = Rails.logger
215
+ when 'merb'
216
+ require 'merb-core'
217
+
218
+ ENV['MERB_ENV'] = options[:environment] if options[:environment]
219
+
220
+ # Bootstrap Merb
221
+ Merb.start_environment(:adapter => 'runner', :environment => ENV['MERB_ENV'] || 'development')
222
+
223
+ # Loops default logger
224
+ Loops.default_logger = Merb.logger
225
+ when 'none' then
226
+ # Plain ruby loops
227
+ Loops.default_logger = Loops::Logger.new($stdout)
228
+ else
229
+ raise InvalidFrameworkError, "Invalid framework name: #{options[:framework]}. Valid values are: none, rails, merb."
230
+ end
231
+ options.delete(:environment)
232
+ options.delete(:framework)
233
+ end
234
+
235
+ # Initializes a loops engine instance.
236
+ #
237
+ # Method loads and parses loops config file, and then
238
+ # initializes pid file path.
239
+ #
240
+ # @return [Engine]
241
+ # a loops engine instance.
242
+ #
243
+ def start_engine!
244
+ # Start loops engine
245
+ @engine = Loops::Engine.new
246
+ # If pid file option is not passed, get if from loops config ...
247
+ unless options[:pid_file] ||= @engine.global_config['pid_file']
248
+ # ... or try Rails' tmp/pids folder ...
249
+ options[:pid_file] = if Loops.root.join('tmp/pids').directory?
250
+ 'tmp/pids/loops.pid'
251
+ else
252
+ # ... or use global system pids folder
253
+ '/var/run/loops.pid'
254
+ end
255
+ end
256
+ @engine
257
+ end
258
+
259
+ COMMANDS_HELP = <<-HELP
260
+ Available commands:
261
+ list List available loops (based on config file)
262
+ start Start all loops except ones marked with disabled:true in config
263
+ start loop1 [loop2] Start only loops specified
264
+ stop Stop daemonized loops monitor
265
+ stats Print loops memory statistics
266
+ debug loop Debug specified loop
267
+ help Show this message
268
+ HELP
269
+
270
+ SPLIT_HELP_LINE = "\n#{' ' * 37}"
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,36 @@
1
+ # Represents a Loops command.
2
+ class Loops::Command
3
+ # @return [Engine]
4
+ # The instance of {Engine} to execute command in.
5
+ attr_reader :engine
6
+
7
+ # @return [Hash<String, Object>]
8
+ # The hash of (parsed) command-line options.
9
+ attr_reader :options
10
+
11
+ # Initializes a new {Command} instance.
12
+ def initialize
13
+ end
14
+
15
+ # Invoke a command.
16
+ #
17
+ # Initiaizes {#engine} and {#options} variables and
18
+ # executes a command.
19
+ #
20
+ def invoke(engine, options)
21
+ @engine = engine
22
+ @options = options
23
+
24
+ execute
25
+ end
26
+
27
+ # A command entry point. Should be overridden in descendants.
28
+ #
29
+ def execute
30
+ raise 'Generic command has no actions'
31
+ end
32
+ end
33
+
34
+ # All Loops command registered.
35
+ module Loops::Commands
36
+ end