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 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 old html and new ajax functions, for example ext manage the layout of page, grids, tree and errors, but form are in html code."
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'
@@ -71,7 +71,7 @@ module Lipsiadmin
71
71
  options[:query] = false
72
72
  end
73
73
 
74
- # Reforma DataIndex
74
+ # Reformat DataIndex
75
75
  if options[:dataIndex].is_a?(Array)
76
76
  options[:dataIndex] = options[:dataIndex].collect do |f|
77
77
  f.is_a?(Symbol) ? "#{@model.table_name}.#{f}" : f
@@ -205,12 +205,9 @@ module Lipsiadmin
205
205
 
206
206
  def find_credentials(creds)
207
207
  case creds
208
- when File:
209
- YAML.load_file(creds.path)
210
- when String:
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