josip-backgroundrb_merb 1.0.3

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