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,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