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