josip-backgroundrb_merb 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ require "chronic"
2
+ require "lib/cron_trigger"
3
+ require "lib/invalid_dump_error"
4
+ require "lib/log_worker"
5
+ require "lib/trigger"
6
+ require "lib/master_worker"
7
+ require "lib/meta_worker"
@@ -0,0 +1,195 @@
1
+ module BackgrounDRbMerb
2
+ class CronTrigger
3
+ WDAYS = { 0 => "Sunday",1 => "Monday",2 => "Tuesday",3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday" }
4
+ LeapYearMonthDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5
+ CommonYearMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
6
+
7
+ attr_reader :sec, :min, :hour, :day, :month, :wday, :year, :cron_expr
8
+
9
+ def initialize(expr)
10
+ self.cron_expr = expr
11
+ end
12
+
13
+ def cron_expr=(expr)
14
+ @cron_expr = expr
15
+ self.sec, self.min, self.hour, self.day, self.month, self.wday, self.year = @cron_expr.split(' ')
16
+ end
17
+
18
+ def fire_after_time(p_time)
19
+ @t_sec,@t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday,@t_yday,@t_idst,@t_zone = p_time.to_a
20
+ @count = 0
21
+ loop do
22
+ @count += 1
23
+
24
+ if @year && !@year.include?(@t_year)
25
+ return nil if @t_year > @year.max
26
+ @t_year = @year.detect { |y| y > @t_year }
27
+ end
28
+
29
+ # if range of months doesn't include current month, find next month from the range
30
+ unless @month.include?(@t_month)
31
+ next_month = @month.detect { |m| m > @t_month } || @month.min
32
+ @t_day,@t_hour,@t_min,@t_sec = @day.min,@hour.min,@min.min,@sec.min
33
+ if next_month < @t_month
34
+ @t_month = next_month
35
+ @t_year += 1
36
+ retry
37
+ end
38
+ @t_month = next_month
39
+ end
40
+
41
+ if !day_restricted? && wday_restricted?
42
+ unless @wday.include?(@t_wday)
43
+ next_wday = @wday.detect { |w| w > @t_wday} || @wday.min
44
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
45
+ t_time = Chronic.parse("next #{WDAYS[next_wday]}",:now => current_time)
46
+ @t_day,@t_month,@t_year = t_time.to_a[3..5]
47
+ @t_wday = next_wday
48
+ retry
49
+ end
50
+ elsif !wday_restricted? && day_restricted?
51
+ day_range = (1.. month_days(@t_year,@t_month))
52
+ # day array, that includes days which are present in current month
53
+ day_array = @day.select { |d| day_range === d }
54
+ unless day_array.include?(@t_day)
55
+ next_day = day_array.detect { |d| d > @t_day } || day_array.min
56
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
57
+ if !next_day || next_day < @t_day
58
+ t_time = Chronic.parse("next month",:now => current_time)
59
+ @t_day = next_day.nil? ? @day.min : next_day
60
+ @t_month,@t_year = t_time.month,t_time.year
61
+ retry
62
+ end
63
+ @t_day = next_day
64
+ end
65
+ else
66
+ # if both day and wday are restricted cron should give preference to one thats closer to current time
67
+ day_range = (1 .. month_days(@t_year,@t_month))
68
+ day_array = @day.select { |d| day_range === d }
69
+ if !day_array.include?(@t_day) && !@wday.include?(@t_wday)
70
+ next_day = day_array.detect { |d| d > @t_day } || day_array.min
71
+ next_wday = @wday.detect { |w| w > @t_wday } || @wday.min
72
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
73
+
74
+ # if next_day is nil or less than @t_day it means that it should run in next month
75
+ if !next_day || next_day < @t_day
76
+ next_time_mday = Chronic.parse("next month",:now => current_time)
77
+ else
78
+ @t_day = next_day
79
+ next_time_mday = current_time
80
+ end
81
+ next_time_wday = Chronic.parse("next #{WDAYS[next_wday]}",:now => current_time)
82
+ if next_time_mday < next_time_wday
83
+ @t_day,@t_month,@t_year = next_time_mday.to_a[3..5]
84
+ else
85
+ @t_day,@t_month,@t_year = next_time_wday.to_a[3..5]
86
+ end
87
+ retry
88
+ end
89
+ end
90
+
91
+ unless @hour.include?(@t_hour)
92
+ next_hour = @hour.detect { |h| h > @t_hour } || @hour.min
93
+ @t_min,@t_sec = @min.min,@sec.min
94
+ if next_hour < @t_hour
95
+ @t_hour = next_hour
96
+ next_day = Chronic.parse("next day",:now => current_time)
97
+ @t_day,@t_month,@t_year,@t_wday = next_day.to_a[3..6]
98
+ retry
99
+ end
100
+ @t_hour = next_hour
101
+ end
102
+
103
+ unless @min.include?(@t_min)
104
+ next_min = @min.detect { |m| m > @t_min } || @min.min
105
+ @t_sec = @sec.min
106
+ if next_min < @t_min
107
+ @t_min = next_min
108
+ next_hour = Chronic.parse("next hour",:now => current_time)
109
+ @t_hour,@t_day,@t_month,@t_year,@t_wday = next_hour.to_a[2..6]
110
+ retry
111
+ end
112
+ @t_min = next_min
113
+ end
114
+
115
+ unless @sec.include?(@t_sec)
116
+ next_sec = @sec.detect { |s| s > @t_sec } || @sec.min
117
+ if next_sec < @t_sec
118
+ @t_sec = next_sec
119
+ next_min = Chronic.parse("next minute",:now => current_time)
120
+ @t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday = next_min.to_a[1..6]
121
+ retry
122
+ end
123
+ @t_sec = next_sec
124
+ end
125
+ break
126
+ end # end of loop do
127
+ current_time
128
+ end
129
+
130
+ def current_time
131
+ Time.local(@t_sec,@t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday,nil,@t_idst,@t_zone)
132
+ end
133
+
134
+ def day_restricted?
135
+ return !@day.eql?(1..31)
136
+ end
137
+
138
+ def wday_restricted?
139
+ return !@wday.eql?(0..6)
140
+ end
141
+
142
+ # TODO: mimic attr_reader to define all of these
143
+ def sec=(sec); @sec = parse_part(sec, 0 .. 59); end
144
+
145
+ def min=(min); @min = parse_part(min, 0 .. 59); end
146
+
147
+ def hour=(hour); @hour = parse_part(hour, 0 .. 23); end
148
+
149
+ def day=(day)
150
+ @day = parse_part(day, 1 .. 31)
151
+ end
152
+
153
+ def month=(month)
154
+ @month = parse_part(month, 1 .. 12)
155
+ end
156
+
157
+ def year=(year)
158
+ @year = parse_part(year)
159
+ end
160
+
161
+ def wday=(wday)
162
+ @wday = parse_part(wday, 0 .. 6)
163
+ end
164
+ private
165
+ def month_days(y, m)
166
+ if ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0)
167
+ LeapYearMonthDays[m-1]
168
+ else
169
+ CommonYearMonthDays[m-1]
170
+ end
171
+ end
172
+
173
+ # 0-5,8,10; 0-5; *; */5
174
+ def parse_part(part, range=nil)
175
+ return range if part.nil? or part == '*' or part =~ /^[*0]\/1$/
176
+
177
+ r = Array.new
178
+ part.split(',').each do |p|
179
+ if p =~ /-/ # 0-5
180
+ r << Range.new(*(p.scan(/\d+/).map { |x| x.to_i })).map { |x| x.to_i }
181
+ elsif p =~ /(\*|\d+)\/(\d+)/ && range # */5, 2/10
182
+ min = $1 == '*' ? 0 : $1.to_i
183
+ inc = $2.to_i
184
+ (min .. range.end).each_with_index do |x, i|
185
+ r << (range.begin == 1 ? x + 1 : x) if i % inc == 0
186
+ end
187
+ else
188
+ r << p.to_i
189
+ end
190
+ end
191
+ r.flatten
192
+ end
193
+ end
194
+ end
195
+
@@ -0,0 +1,4 @@
1
+ module BackgrounDRbMerb
2
+ class InvalidDumpError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,25 @@
1
+ class LogWorker < Packet::Worker
2
+ set_worker_name :log_worker
3
+ attr_accessor :log_file
4
+
5
+ def worker_init
6
+ file = Merb.root / "log" / "backgroundrb_#{CONFIG_FILE[:backgroundrb][:port]}.log"
7
+ @log_file = Merb::Logger.new(file, :debug, "", true)
8
+ end
9
+
10
+ def receive_data(p_data)
11
+ case p_data[:type]
12
+ when :request: process_request(p_data)
13
+ when :response: process_response(p_data)
14
+ end
15
+ end
16
+
17
+ def process_request(p_data)
18
+ log_data = p_data[:data]
19
+ @log_file << "[I] " + log_data
20
+ end
21
+
22
+ def process_response
23
+ puts "Not implemented and needed"
24
+ end
25
+ end
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env ruby
2
+ module BackgrounDRbMerb
3
+ # Class wraps a logger object for debugging internal errors within server
4
+ class DebugMaster
5
+ attr_accessor :log_mode,:logger,:log_flag
6
+ def initialize(log_mode,log_flag = true)
7
+ @log_mode = log_mode
8
+ @log_flag = log_flag
9
+ if @log_mode == :foreground
10
+ @logger = ::Logger.new(STDOUT)
11
+ else
12
+ @logger = ::Logger.new("#{Merb.root}/log/backgroundrb_#{CONFIG_FILE[:backgroundrb][:port]}_debug.log")
13
+ end
14
+ end
15
+
16
+ def info(data)
17
+ return unless @log_flag
18
+ @logger.info(data)
19
+ end
20
+
21
+ def debug(data)
22
+ return unless @log_flag
23
+ @logger.debug(data)
24
+ end
25
+ end
26
+
27
+ class MasterWorker
28
+ attr_accessor :debug_logger
29
+ def receive_data p_data
30
+ debug_logger.info(p_data)
31
+ @tokenizer.extract(p_data) do |b_data|
32
+ t_data = Marshal.load(b_data)
33
+ debug_logger.info(t_data)
34
+ case t_data[:type]
35
+ when :do_work: process_work(t_data)
36
+ when :get_status: process_status(t_data)
37
+ when :get_result: process_request(t_data)
38
+ when :start_worker: start_worker_request(t_data)
39
+ when :delete_worker: delete_drb_worker(t_data)
40
+ when :all_worker_status: query_all_worker_status(t_data)
41
+ when :worker_info: pass_worker_info(t_data)
42
+ when :all_worker_info: all_worker_info(t_data)
43
+ end
44
+ end
45
+ end
46
+
47
+ #
48
+ def pass_worker_info(t_data)
49
+ worker_name_key = gen_worker_key(t_data[:worker],t_data[:job_key])
50
+ worker_instance = reactor.live_workers[worker_name_key]
51
+ info_response = { :worker => t_data[:worker],:job_key => t_data[:job_key]}
52
+ worker_instance ? (info_response[:status] = :running) : (info_response[:status] = :stopped)
53
+ send_object(info_response)
54
+ end
55
+
56
+ def all_worker_info(t_data)
57
+ info_response = []
58
+ reactor.live_workers.each do |key,value|
59
+ job_key = (value.worker_key.to_s).gsub(/#{value.worker_name}_?/,"")
60
+ info_response << { :worker => value.worker_name,:job_key => job_key,:status => :running }
61
+ end
62
+ send_object(info_response)
63
+ end
64
+
65
+ def query_all_worker_status(p_data)
66
+ dumpable_status = { }
67
+ reactor.live_workers.each { |key,value| dumpable_status[key] = reactor.result_hash[key] }
68
+ send_object(dumpable_status)
69
+ end
70
+
71
+ # FIXME: although worker key is removed nonetheless from live_workers hash
72
+ # it could be a good idea to remove it here itself.
73
+ def delete_drb_worker(t_data)
74
+ worker_name = t_data[:worker]
75
+ job_key = t_data[:job_key]
76
+ worker_name_key = gen_worker_key(worker_name,job_key)
77
+ begin
78
+ # ask_worker(worker_name,:job_key => t_data[:job_key],:type => :request, :data => { :worker_method => :exit})
79
+ worker_instance = reactor.live_workers[worker_name_key]
80
+ # pgid = Process.getpgid(worker_instance.pid)
81
+ Process.kill('TERM',worker_instance.pid)
82
+ # Process.kill('-TERM',pgid)
83
+
84
+ # Process.kill('KILL',worker_instance.pid)
85
+ rescue Packet::DisconnectError => sock_error
86
+ # reactor.live_workers.delete(worker_name_key)
87
+ reactor.remove_worker(sock_error)
88
+ rescue
89
+ debug_logger.info($!.to_s)
90
+ debug_logger.info($!.backtrace.join("\n"))
91
+ end
92
+ end
93
+
94
+ def start_worker_request(p_data)
95
+ start_worker(p_data)
96
+ end
97
+
98
+ def process_work(t_data)
99
+ worker_name = t_data[:worker]
100
+ worker_name_key = gen_worker_key(worker_name,t_data[:job_key])
101
+ t_data.delete(:worker)
102
+ t_data.delete(:type)
103
+ begin
104
+ ask_worker(worker_name_key,:data => t_data, :type => :request, :result => false)
105
+ rescue Packet::DisconnectError => sock_error
106
+ reactor.live_workers.delete(worker_name_key)
107
+ rescue
108
+ debug_logger.info($!.to_s)
109
+ debug_logger.info($!.backtrace.join("\n"))
110
+ return
111
+ end
112
+
113
+ end
114
+
115
+ def process_status(t_data)
116
+ worker_name = t_data[:worker]
117
+ job_key = t_data[:job_key]
118
+ worker_name_key = gen_worker_key(worker_name,job_key)
119
+ status_data = reactor.result_hash[worker_name_key.to_sym]
120
+ send_object(status_data)
121
+ end
122
+
123
+ def process_request(t_data)
124
+ worker_name = t_data[:worker]
125
+ worker_name_key = gen_worker_key(worker_name,t_data[:job_key])
126
+ t_data.delete(:worker)
127
+ t_data.delete(:type)
128
+ begin
129
+ ask_worker(worker_name_key,:data => t_data, :type => :request,:result => true)
130
+ rescue Packet::DisconnectError => sock_error
131
+ reactor.live_workers.delete(worker_name_key)
132
+ rescue
133
+ debug_logger.info($!.to_s)
134
+ debug_logger.info($!.backtrace.join("\n"))
135
+ return
136
+ end
137
+ end
138
+
139
+ # this method can receive one shot status reports or proper results
140
+ def worker_receive p_data
141
+ send_object(p_data)
142
+ end
143
+
144
+ def unbind
145
+ debug_logger.info("Client disconected")
146
+ end
147
+ def post_init
148
+ @tokenizer = BinParser.new
149
+ end
150
+ def connection_completed; end
151
+ end
152
+
153
+ class MasterProxy
154
+ attr_accessor :config_file,:reloadable_workers,:worker_triggers,:reactor
155
+ def initialize
156
+ raise "Running old Ruby version, upgrade to Ruby >= 1.8.5" unless check_for_ruby_version
157
+ @config_file = BackgrounDRbMerb::Config.read_config("#{Merb.root}/config/backgroundrb.yml")
158
+
159
+ log_flag = CONFIG_FILE[:backgroundrb][:debug_log].nil? ? true : CONFIG_FILE[:backgroundrb][:debug_log]
160
+ debug_logger = DebugMaster.new(CONFIG_FILE[:backgroundrb][:log],log_flag)
161
+
162
+ load_merb_env
163
+
164
+ find_reloadable_worker
165
+
166
+ Packet::Reactor.run do |t_reactor|
167
+ @reactor = t_reactor
168
+ enable_memcache_result_hash(t_reactor) if CONFIG_FILE[:backgroundrb][:result_storage] && CONFIG_FILE[:backgroundrb][:result_storage][:memcache]
169
+ t_reactor.start_worker(:worker => :log_worker) if log_flag
170
+ t_reactor.start_server(CONFIG_FILE[:backgroundrb][:ip],CONFIG_FILE[:backgroundrb][:port],MasterWorker) { |conn| conn.debug_logger = debug_logger }
171
+ t_reactor.next_turn { reload_workers }
172
+ end
173
+ end
174
+
175
+ def gen_worker_key(worker_name,job_key = nil)
176
+ return worker_name if job_key.nil?
177
+ return "#{worker_name}_#{job_key}".to_sym
178
+ end
179
+
180
+
181
+ # method should find reloadable workers and load their schedule from config file
182
+ def find_reloadable_worker
183
+ t_workers = Dir["#{WORKER_ROOT}/**/*.rb"]
184
+ @reloadable_workers = t_workers.map do |x|
185
+ worker_name = File.basename(x,".rb")
186
+ require worker_name
187
+ worker_klass = Object.const_get(worker_name.camel_case)
188
+ worker_klass.reload_flag ? worker_klass : nil
189
+ end.compact
190
+ @worker_triggers = { }
191
+ @reloadable_workers.each do |t_worker|
192
+ schedule = load_reloadable_schedule(t_worker)
193
+ if schedule && !schedule.empty?
194
+ @worker_triggers[t_worker.worker_name.to_sym] = schedule
195
+ end
196
+ end
197
+ end
198
+
199
+ def load_reloadable_schedule(t_worker)
200
+ worker_method_triggers = { }
201
+ worker_schedule = CONFIG_FILE[:schedules][t_worker.worker_name.to_sym]
202
+
203
+ worker_schedule && worker_schedule.each do |key,value|
204
+ case value[:trigger_args]
205
+ when String
206
+ cron_args = value[:trigger_args] || "0 0 0 0 0"
207
+ trigger = BackgrounDRbMerb::CronTrigger.new(cron_args)
208
+ when Hash
209
+ trigger = BackgrounDRbMerb::Trigger.new(value[:trigger_args])
210
+ end
211
+ worker_method_triggers[key] = { :trigger => trigger,:data => value[:data],:runtime => trigger.fire_after_time(Time.now).to_i }
212
+ end
213
+ worker_method_triggers
214
+ end
215
+
216
+ # method will reload workers that should be loaded on each schedule
217
+ def reload_workers
218
+ return if worker_triggers.empty?
219
+ worker_triggers.each do |key,value|
220
+ value.delete_if { |key,value| value[:trigger].respond_to?(:end_time) && value[:trigger].end_time <= Time.now }
221
+ end
222
+
223
+ worker_triggers.each do |worker_name,trigger|
224
+ trigger.each do |key,value|
225
+ time_now = Time.now.to_i
226
+ if value[:runtime] < time_now
227
+ load_and_invoke(worker_name,key,value)
228
+ t_time = value[:trigger].fire_after_time(Time.now)
229
+ value[:runtime] = t_time.to_i
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ # method will load the worker and invoke worker method
236
+ def load_and_invoke(worker_name,p_method,data)
237
+ begin
238
+ require worker_name.to_s
239
+ job_key = Packet::Guid.hexdigest
240
+ @reactor.start_worker(:worker => worker_name,:job_key => job_key)
241
+ worker_name_key = gen_worker_key(worker_name,job_key)
242
+ data_request = {:data => { :worker_method => p_method,:data => data[:data]},
243
+ :type => :request, :result => false
244
+ }
245
+
246
+ exit_request = {:data => { :worker_method => :exit},
247
+ :type => :request, :result => false
248
+ }
249
+
250
+ @reactor.live_workers[worker_name_key].send_request(data_request)
251
+ @reactor.live_workers[worker_name_key].send_request(exit_request)
252
+ rescue LoadError
253
+ puts "no such worker #{worker_name}"
254
+ rescue MissingSourceFile
255
+ puts "no such worker #{worker_name}"
256
+ return
257
+ end
258
+ end
259
+
260
+ def load_merb_env
261
+ run_env = CONFIG_FILE[:backgroundrb][:environment] || 'development'
262
+ lazy_load = CONFIG_FILE[:backgroundrb][:lazy_load].nil? ? true : CONFIG_FILE[:backgroundrb][:lazy_load].nil?
263
+ require_merb_files unless lazy_load
264
+ ActiveRecord::Base.allow_concurrency = true if defined?(ActiveRecord)
265
+ end
266
+
267
+ def require_merb_files
268
+ debug_logger = DebugMaster.new(CONFIG_FILE[:backgroundrb][:log],true)
269
+
270
+ files = Dir["#{Merb.root}/app/models/**/*.rb"]
271
+ files.each { |x|
272
+ puts "***(M) #{x}"
273
+ begin
274
+ require x unless defined? x.camel_case
275
+ rescue LoadError
276
+ next
277
+ rescue MissingSourceFile
278
+ next
279
+ end
280
+ }
281
+ end
282
+
283
+ def enable_memcache_result_hash(t_reactor)
284
+ require 'memcache'
285
+ memcache_options = {
286
+ :c_threshold => 10_000,
287
+ :compression => true,
288
+ :debug => false,
289
+ :namespace => 'backgroundrb_result_hash',
290
+ :readonly => false,
291
+ :urlencode => false
292
+ }
293
+ cache = MemCache.new(memcache_options)
294
+ cache.servers = CONFIG_FILE[:backgroundrb][:result_storage][:memcache].split(',')
295
+ t_reactor.set_result_hash(cache)
296
+ end
297
+
298
+ def check_for_ruby_version; return RUBY_VERSION >= "1.8.5"; end
299
+
300
+ end # end of module BackgrounDRb
301
+ end