lipsiadmin 3.3.4 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +14 -0
- data/README +1 -1
- data/Rakefile +1 -1
- data/lib/controller/ext.rb +1 -1
- data/lib/data_base/attachment/storage.rb +3 -6
- data/lib/loops.rb +277 -0
- data/lib/loops/base.rb +52 -0
- data/lib/loops/daemonize.rb +72 -0
- data/lib/loops/process_manager.rb +101 -0
- data/lib/loops/worker.rb +81 -0
- data/lib/loops/worker_pool.rb +53 -0
- data/lib/utils/html_entities.rb +1 -0
- data/lib/version.rb +2 -2
- data/lib/view/helpers/backend_helper.rb +6 -4
- data/lib/view/helpers/ext/component.rb +1 -0
- data/lib/view/helpers/ext/grid.rb +6 -8
- data/lipsiadmin_generators/backend/REMEMBER +1 -1
- data/lipsiadmin_generators/backend/backend_generator.rb +1 -0
- data/lipsiadmin_generators/backend/templates/models/account.rb +1 -1
- data/lipsiadmin_generators/backend/templates/test/fixtures/accounts.yml +8 -0
- data/lipsiadmin_generators/backend/templates/test/unit/account_test.rb +10 -0
- data/lipsiadmin_generators/loops/loops_generator.rb +27 -0
- data/lipsiadmin_generators/loops/templates/app/loops/APP_README +1 -0
- data/lipsiadmin_generators/loops/templates/app/loops/simple_loop.rb +9 -0
- data/lipsiadmin_generators/loops/templates/config/loops.yml +17 -0
- data/lipsiadmin_generators/loops/templates/script/loops +125 -0
- data/tasks/lipsiadmin_tasks.rake +16 -0
- metadata +20 -106
data/CHANGELOG
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
2009-04-23
|
2
|
+
* Fix rdoc for grid view
|
3
|
+
* Fix a bug override default ext component methods
|
4
|
+
|
5
|
+
2009-04-20
|
6
|
+
* Fix compatibility issues with ruby 1.9
|
7
|
+
* Added a new module for background workers
|
8
|
+
* Little formatting to tab helper for accept custom options
|
9
|
+
* Fix rdoc for open_window helper
|
10
|
+
|
11
|
+
2009-04-10
|
12
|
+
* Added startup test for account
|
13
|
+
* Added raketasks for init test and autotest
|
14
|
+
|
1
15
|
2009-03-24
|
2
16
|
* Fix landescape mode in pdf builder
|
3
17
|
|
data/README
CHANGED
@@ -45,7 +45,7 @@ now run script/generate and you can see some like:
|
|
45
45
|
|
46
46
|
Installed Generators
|
47
47
|
Builtin: controller, integration_test, mailer, migration, model, etc..
|
48
|
-
Lipsiadmin: attachment, backend, backend_page, pdf
|
48
|
+
Lipsiadmin: attachment, backend, backend_page, pdf, loops
|
49
49
|
|
50
50
|
So you can do:
|
51
51
|
|
data/Rakefile
CHANGED
@@ -42,7 +42,7 @@ spec = Gem::Specification.new do |s|
|
|
42
42
|
s.homepage = "http://groups.google.com/group/lipsiadmin"
|
43
43
|
s.rubyforge_project = "lipsiadmin"
|
44
44
|
s.platform = Gem::Platform::RUBY
|
45
|
-
s.summary = "Lipsiadmin is a new revolutionary admin for your projects.Lipsiadmin is based on Ext Js 2.0. framework (with prototype adapter) and is ready for Rails 2.0. This admin is for newbie developper but also for experts, is not entirely written in javascript because the aim of developper wose build in a agile way web/site apps so we use extjs in a new intelligent way a mixin of
|
45
|
+
s.summary = "Lipsiadmin is a new revolutionary admin for your projects.Lipsiadmin is based on Ext Js 2.0. framework (with prototype adapter) and is ready for Rails 2.0. This admin is for newbie developper but also for experts, is not entirely written in javascript because the aim of developper wose build in a agile way web/site apps so we use extjs in a new intelligent way a mixin of 'old' html and new ajax functions, for example ext manage the layout of page, grids, tree and errors, but form are in html code."
|
46
46
|
s.files = FileList["CHANGELOG", "README", "MIT-LICENSE", "Rakefile", "init.rb", "{lipsiadmin_generators,lib,resources,tasks}/**/*"].to_a
|
47
47
|
s.has_rdoc = true
|
48
48
|
s.requirements << 'Haml'
|
data/lib/controller/ext.rb
CHANGED
@@ -205,12 +205,9 @@ module Lipsiadmin
|
|
205
205
|
|
206
206
|
def find_credentials(creds)
|
207
207
|
case creds
|
208
|
-
when File
|
209
|
-
|
210
|
-
when
|
211
|
-
YAML.load_file(creds)
|
212
|
-
when Hash:
|
213
|
-
creds
|
208
|
+
when File then YAML.load_file(creds.path)
|
209
|
+
when String then YAML.load_file(creds)
|
210
|
+
when Hash then creds
|
214
211
|
else
|
215
212
|
raise ArgumentError, "Credentials are not a path, file, or hash."
|
216
213
|
end
|
data/lib/loops.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Lipsiadmin
|
4
|
+
|
5
|
+
# ==Simple background loops framework for rails
|
6
|
+
#
|
7
|
+
# Loops is a small and lightweight framework for Ruby on Rails created to support simple
|
8
|
+
# background loops in your application which are usually used to do some background data processing
|
9
|
+
# on your servers (queue workers, batch tasks processors, etc).
|
10
|
+
#
|
11
|
+
# Authors:: Alexey Kovyrin and Dmytro Shteflyuk
|
12
|
+
# Comments:: This plugin has been created in Scribd.com for internal use.
|
13
|
+
#
|
14
|
+
# == What tasks could you use it for?
|
15
|
+
#
|
16
|
+
# Originally loops plugin was created to make our own loops code more organized. We used to have tens
|
17
|
+
# of different modules with methods that were called with script/runner and then used with nohup and
|
18
|
+
# other not so convenient backgrounding techniques. When you have such a number of loops/workers to
|
19
|
+
# run in background it becomes a nightmare to manage them on a regular basis (restarts, code upgrades,
|
20
|
+
# status/health checking, etc).
|
21
|
+
#
|
22
|
+
# After a short time of writing our loops in more organized ways we were able to generalize most of the
|
23
|
+
# loops code so now our loops look like a classes with a single mandatory public method called *run*.
|
24
|
+
# Everything else (spawning many workers, managing them, logging, backgrounding, pid-files management,
|
25
|
+
# etc) is handled by the plugin it
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# == But there are dozens of libraries like this! Why do we need one more?
|
29
|
+
#
|
30
|
+
# The major idea behind this small project was to create a deadly simple and yet robust framework to
|
31
|
+
# be able to run some tasks in background and do not think about spawning many workers, restarting
|
32
|
+
# them when they die, etc. So, if you need to be able to run either one or many copies of your worker or
|
33
|
+
# you do not want to think about re-spawning dead workers and do not want to spend megabytes of RAM on
|
34
|
+
# separate copies of Ruby interpreter (when you run each copy of your loop as a separate process
|
35
|
+
# controlled by monit/god/etc), then I'd recommend you to try this framework -- you'd like it.
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# == How to use?
|
39
|
+
#
|
40
|
+
# Generate binary and configuration files by running
|
41
|
+
#
|
42
|
+
# script/generate loops
|
43
|
+
#
|
44
|
+
# This will create the following list of files:
|
45
|
+
#
|
46
|
+
# script/loops # binary file that will be used to manage your loops
|
47
|
+
# config/loops.yml # example configuration file
|
48
|
+
# app/loops/simple.rb # REALLY simple loop example
|
49
|
+
#
|
50
|
+
# Here is a simple loop scaffold for you to start from (put this file to app/loops/hello_world_loop.rb):
|
51
|
+
#
|
52
|
+
# class HelloWorldLoop < Lipsiadmin::Loops::Base
|
53
|
+
# def run
|
54
|
+
# debug("Hello, debug log!")
|
55
|
+
# sleep(config['sleep_period']) # Do something "useful" and make it configurable
|
56
|
+
# debug("Hello, debug log (yes, once again)!")
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# When you have your loop ready to use, add the following lines to your (maybe empty yet) config/loops.yml
|
61
|
+
# file:
|
62
|
+
#
|
63
|
+
# hello_world:
|
64
|
+
# sleep_period: 10
|
65
|
+
#
|
66
|
+
# This is it! To start your loop, just run one of the following commands:
|
67
|
+
#
|
68
|
+
# # Generates: list all configured loops:
|
69
|
+
# $ script/loops -L
|
70
|
+
#
|
71
|
+
# # Generates: run all enabled (actually non-disabled) loops in foreground:
|
72
|
+
# $ script/loops -a
|
73
|
+
#
|
74
|
+
# # Generates: run all enabled loops in background:
|
75
|
+
# $ script/loops -d -a
|
76
|
+
#
|
77
|
+
# # Generates: run specific loop in background:
|
78
|
+
# $ ./script/loops -d -l hello_world
|
79
|
+
#
|
80
|
+
# # Generates: all possible options:
|
81
|
+
# $ ./script/loops -h
|
82
|
+
#
|
83
|
+
#
|
84
|
+
# == How to run more than one worker?
|
85
|
+
#
|
86
|
+
# If you want to have more than one copy of your worker running, that is as simple as adding one
|
87
|
+
# option to your loop configuration:
|
88
|
+
#
|
89
|
+
# hello_world:
|
90
|
+
# sleep_period: 10
|
91
|
+
# workers_number: 1
|
92
|
+
#
|
93
|
+
# This _workers_number_ option would say loops manager to spawn more than one copy of your loop
|
94
|
+
# and run them in parallel. The only thing you'd need to do is to think about concurrent work of
|
95
|
+
# your loops. For example, if you have some kind of database table with elements you need to
|
96
|
+
# process, you can create a simple database-based locks system or use any memcache-based locks.
|
97
|
+
#
|
98
|
+
#
|
99
|
+
# == There is this <tt>workers_engine</tt> option in config file. What it could be used for?
|
100
|
+
#
|
101
|
+
# There are two so called "workers engines" in this plugin: <tt>fork</tt> and <tt>thread</tt>. They're used
|
102
|
+
# to control the way process manager would spawn new loops workers: with <tt>fork</tt> engine we'll
|
103
|
+
# load all loops classes and then fork ruby interpreter as many times as many workers we need.
|
104
|
+
# With <tt>thread</tt> engine we'd do Thread.new instead of forks. Thread engine could be useful if you
|
105
|
+
# are sure your loop won't lock ruby interpreter (it does not do native calls, etc) or if you
|
106
|
+
# use some interpreter that does not support forks (like jruby).
|
107
|
+
#
|
108
|
+
# Default engine is <tt>fork</tt>.
|
109
|
+
#
|
110
|
+
#
|
111
|
+
# == What Ruby implementations does it work for?
|
112
|
+
#
|
113
|
+
# We've tested and used the plugin on MRI 1.8.6 and on JRuby 1.1.5. At this point we do not support
|
114
|
+
# demonization in JRuby and never tested the code on Ruby 1.9. Obviously because of JVM limitations
|
115
|
+
# you won't be able to use +fork+ workers engine in JRuby, but threaded workers do pretty well.
|
116
|
+
#
|
117
|
+
module Loops
|
118
|
+
|
119
|
+
class << self
|
120
|
+
|
121
|
+
# Set/Return the main config
|
122
|
+
def config
|
123
|
+
@@config
|
124
|
+
end
|
125
|
+
|
126
|
+
# Set/Return the loops config
|
127
|
+
def loops_config
|
128
|
+
@@loops_config
|
129
|
+
end
|
130
|
+
|
131
|
+
# Set/Return the global config
|
132
|
+
def global_config
|
133
|
+
@@global_config
|
134
|
+
end
|
135
|
+
|
136
|
+
# Load the yml config file, default config/loops.yml
|
137
|
+
def load_config(file)
|
138
|
+
@@config = YAML.load_file(file)
|
139
|
+
@@global_config = @@config['global']
|
140
|
+
@@loops_config = @@config['loops']
|
141
|
+
|
142
|
+
@@logger = create_logger('global', global_config)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Start loops, default :all
|
146
|
+
def start_loops!(loops_to_start = :all)
|
147
|
+
@@running_loops = []
|
148
|
+
@@pm = Loops::ProcessManager.new(global_config, @@logger)
|
149
|
+
|
150
|
+
# Start all loops
|
151
|
+
loops_config.each do |name, config|
|
152
|
+
next if config['disabled']
|
153
|
+
next unless loops_to_start == :all || loops_to_start.member?(name)
|
154
|
+
klass = load_loop_class(name)
|
155
|
+
next unless klass
|
156
|
+
|
157
|
+
start_loop(name, klass, config)
|
158
|
+
@@running_loops << name
|
159
|
+
end
|
160
|
+
|
161
|
+
# Do not continue if there is nothing to run
|
162
|
+
if @@running_loops.empty?
|
163
|
+
puts "WARNING: No loops to run! Exiting..."
|
164
|
+
return
|
165
|
+
end
|
166
|
+
|
167
|
+
# Start monitoring loop
|
168
|
+
setup_signals
|
169
|
+
@@pm.monitor_workers
|
170
|
+
|
171
|
+
info "Loops are stopped now!"
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
# Proxy logger calls to the default loops logger
|
177
|
+
[ :debug, :error, :fatal, :info, :warn ].each do |meth_name|
|
178
|
+
class_eval <<-EVAL
|
179
|
+
def #{meth_name}(message)
|
180
|
+
LOOPS_DEFAULT_LOGGER.#{meth_name} "\#{Time.now}: loops[RUNNER/\#{Process.pid}]: \#{message}"
|
181
|
+
end
|
182
|
+
EVAL
|
183
|
+
end
|
184
|
+
|
185
|
+
def load_loop_class(name)
|
186
|
+
begin
|
187
|
+
klass_file = LOOPS_ROOT + "/app/loops/#{name}.rb"
|
188
|
+
debug "Loading class file: #{klass_file}"
|
189
|
+
require(klass_file)
|
190
|
+
rescue Exception
|
191
|
+
error "Can't load the class file: #{klass_file}. Worker #{name} won't be started!"
|
192
|
+
return false
|
193
|
+
end
|
194
|
+
|
195
|
+
klass_name = "#{name}".classify
|
196
|
+
klass = klass_name.constantize rescue nil
|
197
|
+
|
198
|
+
unless klass
|
199
|
+
error "Can't find class: #{klass_name}. Worker #{name} won't be started!"
|
200
|
+
return false
|
201
|
+
end
|
202
|
+
|
203
|
+
begin
|
204
|
+
klass.check_dependencies
|
205
|
+
rescue Exception => e
|
206
|
+
error "Loop #{name} dependencies check failed: #{e} at #{e.backtrace.first}"
|
207
|
+
return false
|
208
|
+
end
|
209
|
+
|
210
|
+
return klass
|
211
|
+
end
|
212
|
+
|
213
|
+
def start_loop(name, klass, config)
|
214
|
+
puts "Starting loop: #{name}"
|
215
|
+
info "Starting loop: #{name}"
|
216
|
+
info " - config: #{config.inspect}"
|
217
|
+
|
218
|
+
@@pm.start_workers(name, config['workers_number'] || 1) do
|
219
|
+
debug "Instantiating class: #{klass}"
|
220
|
+
looop = klass.new(create_logger(name, config))
|
221
|
+
looop.name = name
|
222
|
+
looop.config = config
|
223
|
+
|
224
|
+
debug "Starting the loop #{name}!"
|
225
|
+
fix_ar_after_fork
|
226
|
+
looop.run
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def create_logger(loop_name, config)
|
231
|
+
config['logger'] ||= (global_config['logger'] || 'default')
|
232
|
+
|
233
|
+
return LOOPS_DEFAULT_LOGGER if config['logger'] == 'default'
|
234
|
+
return Logger.new(STDOUT) if config['logger'] == 'stdout'
|
235
|
+
return Logger.new(STDERR) if config['logger'] == 'stderr'
|
236
|
+
|
237
|
+
config['logger'] = File.join(LOOPS_ROOT, config['logger']) unless config['logger'] =~ /^\//
|
238
|
+
Logger.new(config['logger'])
|
239
|
+
|
240
|
+
rescue Exception => e
|
241
|
+
message = "Can't create a logger for the #{loop_name} loop! Will log to the default logger!"
|
242
|
+
puts "ERROR: #{message}"
|
243
|
+
|
244
|
+
message << "\nException: #{e} at #{e.backtrace.first}"
|
245
|
+
error(message)
|
246
|
+
|
247
|
+
return LOOPS_DEFAULT_LOGGER
|
248
|
+
end
|
249
|
+
|
250
|
+
def setup_signals
|
251
|
+
trap('TERM') {
|
252
|
+
warn "Received a TERM signal... stopping..."
|
253
|
+
@@pm.stop_workers!
|
254
|
+
}
|
255
|
+
|
256
|
+
trap('INT') {
|
257
|
+
warn "Received an INT signal... stopping..."
|
258
|
+
@@pm.stop_workers!
|
259
|
+
}
|
260
|
+
|
261
|
+
trap('EXIT') {
|
262
|
+
warn "Received a EXIT 'signal'... stopping..."
|
263
|
+
@@pm.stop_workers!
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
def fix_ar_after_fork
|
268
|
+
ActiveRecord::Base.clear_active_connections!
|
269
|
+
ActiveRecord::Base.verify_active_connections!
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
require 'loops/process_manager'
|
277
|
+
require 'loops/base'
|
data/lib/loops/base.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Lipsiadmin
|
2
|
+
module Loops
|
3
|
+
# This is the base class for make a simple loop with its own custom run method
|
4
|
+
#
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# class SimpleLoop < Lipsiadmin::Loops::Base
|
8
|
+
# def run
|
9
|
+
# debug(Time.now)
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Base#:nodoc:
|
14
|
+
attr_accessor :name, :config, :logger
|
15
|
+
|
16
|
+
# The initialize method, default we pass the logger
|
17
|
+
def initialize(logger)
|
18
|
+
self.logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ovveride this method for check dependencies
|
22
|
+
def self.check_dependencies; end
|
23
|
+
|
24
|
+
# Proxy logger calls to our logger
|
25
|
+
[ :debug, :error, :fatal, :info, :warn ].each do |meth_name|
|
26
|
+
class_eval <<-EVAL
|
27
|
+
def #{meth_name}(message)
|
28
|
+
logger.#{meth_name}("\#{Time.now}: loop[\#{name}/\#{Process.pid}]: \#{message}")
|
29
|
+
end
|
30
|
+
EVAL
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_lock(entity_id, loop_id, timeout, entity_name = '')#:nodoc:
|
34
|
+
entity_name = 'item' if entity_name.to_s.empty?
|
35
|
+
|
36
|
+
debug("Locking #{entity_name} #{entity_id}")
|
37
|
+
lock = LoopLock.lock(:entity_id => entity_id, :loop => loop_id.to_s, :timeout => timeout)
|
38
|
+
unless lock
|
39
|
+
warn("Race condition detected for the #{entity_name}: #{entity_id}. Skipping the item.")
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
yield
|
45
|
+
ensure
|
46
|
+
debug("Unlocking #{entity_name} #{entity_id}")
|
47
|
+
LoopLock.unlock(:entity_id => entity_id, :loop => loop_id.to_s)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Lipsiadmin
|
2
|
+
module Loops
|
3
|
+
module Daemonize#:nodoc:
|
4
|
+
def self.read_pid(pid_file)
|
5
|
+
File.open(pid_file) do |f|
|
6
|
+
f.gets.to_i
|
7
|
+
end
|
8
|
+
rescue Errno::ENOENT
|
9
|
+
0
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.check_pid(pid_file)
|
13
|
+
pid = read_pid(pid_file)
|
14
|
+
return false if pid.zero?
|
15
|
+
if defined?(::JRuby)
|
16
|
+
system "kill -0 #{pid} &> /dev/null"
|
17
|
+
return $? == 0
|
18
|
+
else
|
19
|
+
Process.kill(0, pid)
|
20
|
+
end
|
21
|
+
true
|
22
|
+
rescue Errno::ESRCH, Errno::ECHILD, Errno::EPERM
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create_pid(pid_file)
|
27
|
+
if File.exist?(pid_file)
|
28
|
+
puts "Pid file #{pid_file} exists! Checking the process..."
|
29
|
+
if check_pid(pid_file)
|
30
|
+
puts "Can't create new pid file because another process is runnig!"
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
puts "Stale pid file! Removing..."
|
34
|
+
File.delete(pid_file)
|
35
|
+
end
|
36
|
+
|
37
|
+
puts "Creating pid file..."
|
38
|
+
File.open(pid_file, 'w') do |f|
|
39
|
+
f.puts(Process.pid)
|
40
|
+
end
|
41
|
+
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.daemonize(app_name)
|
46
|
+
if defined?(::JRuby)
|
47
|
+
puts "WARNING: daemonize method is not implemented for JRuby (yet), please consider using nohup."
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
srand # Split rand streams between spawning and daemonized process
|
52
|
+
fork && exit # Fork and exit from the parent
|
53
|
+
|
54
|
+
# Detach from the controlling terminal
|
55
|
+
unless sess_id = Process.setsid
|
56
|
+
raise Daemons.RuntimeException.new('cannot detach from controlling terminal')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Prevent the possibility of acquiring a controlling terminal
|
60
|
+
trap 'SIGHUP', 'IGNORE'
|
61
|
+
exit if pid = fork
|
62
|
+
|
63
|
+
$0 = app_name if app_name
|
64
|
+
|
65
|
+
Dir.chdir(LOOPS_ROOT) # Make sure we're in the working directory
|
66
|
+
File.umask(0000) # Insure sensible umask
|
67
|
+
|
68
|
+
return sess_id
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|