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
@@ -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,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')
|
data/lib/loops.rb
ADDED
@@ -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
|
data/lib/loops/base.rb
ADDED
@@ -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
|