loops 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/LICENSE +21 -0
- data/README.rdoc +238 -0
- data/Rakefile +48 -0
- data/VERSION.yml +5 -0
- data/bin/loops +16 -0
- data/bin/loops-memory-stats +259 -0
- data/generators/loops/loops_generator.rb +28 -0
- data/generators/loops/templates/app/loops/APP_README +1 -0
- data/generators/loops/templates/app/loops/queue_loop.rb +8 -0
- data/generators/loops/templates/app/loops/simple_loop.rb +12 -0
- data/generators/loops/templates/config/loops.yml +34 -0
- data/generators/loops/templates/script/loops +20 -0
- data/init.rb +1 -0
- data/lib/loops.rb +167 -0
- data/lib/loops/autoload.rb +20 -0
- data/lib/loops/base.rb +148 -0
- data/lib/loops/cli.rb +35 -0
- data/lib/loops/cli/commands.rb +124 -0
- data/lib/loops/cli/options.rb +273 -0
- data/lib/loops/command.rb +36 -0
- data/lib/loops/commands/debug_command.rb +8 -0
- data/lib/loops/commands/list_command.rb +11 -0
- data/lib/loops/commands/start_command.rb +24 -0
- data/lib/loops/commands/stats_command.rb +5 -0
- data/lib/loops/commands/stop_command.rb +18 -0
- data/lib/loops/daemonize.rb +68 -0
- data/lib/loops/engine.rb +207 -0
- data/lib/loops/errors.rb +6 -0
- data/lib/loops/logger.rb +212 -0
- data/lib/loops/process_manager.rb +114 -0
- data/lib/loops/queue.rb +78 -0
- data/lib/loops/version.rb +31 -0
- data/lib/loops/worker.rb +101 -0
- data/lib/loops/worker_pool.rb +55 -0
- data/loops.gemspec +98 -0
- data/spec/loop_lock_spec.rb +61 -0
- data/spec/loops/base_spec.rb +92 -0
- data/spec/loops/cli_spec.rb +156 -0
- data/spec/loops_spec.rb +20 -0
- data/spec/rails/another_loop.rb +4 -0
- data/spec/rails/app/loops/complex_loop.rb +12 -0
- data/spec/rails/app/loops/simple_loop.rb +6 -0
- data/spec/rails/config.yml +6 -0
- data/spec/rails/config/boot.rb +1 -0
- data/spec/rails/config/environment.rb +5 -0
- data/spec/rails/config/loops.yml +13 -0
- data/spec/spec_helper.rb +110 -0
- metadata +121 -0
data/lib/loops/cli.rb
ADDED
@@ -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
|