backgroundrb-rails3 1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/.autotest +17 -0
  2. data/ChangeLog +50 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE +4 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README +22 -0
  7. data/Rakefile +128 -0
  8. data/TODO.org +5 -0
  9. data/app/controller/backgroundrb_status_controller.rb +6 -0
  10. data/backgroundrb-rails3.gemspec +219 -0
  11. data/config/backgroundrb.yml +11 -0
  12. data/doc/Rakefile +5 -0
  13. data/doc/config.yaml +2 -0
  14. data/doc/content/advanced/advanced.txt +76 -0
  15. data/doc/content/advanced/advanced.yaml +4 -0
  16. data/doc/content/bugs/bugs.txt +20 -0
  17. data/doc/content/bugs/bugs.yaml +5 -0
  18. data/doc/content/community/community.txt +36 -0
  19. data/doc/content/community/community.yaml +5 -0
  20. data/doc/content/content.txt +168 -0
  21. data/doc/content/content.yaml +5 -0
  22. data/doc/content/faq/faq.txt +41 -0
  23. data/doc/content/faq/faq.yaml +5 -0
  24. data/doc/content/rails/rails.txt +182 -0
  25. data/doc/content/rails/rails.yaml +5 -0
  26. data/doc/content/scheduling/scheduling.txt +166 -0
  27. data/doc/content/scheduling/scheduling.yaml +5 -0
  28. data/doc/content/workers/workers.txt +178 -0
  29. data/doc/content/workers/workers.yaml +5 -0
  30. data/doc/layouts/default/default.erb +56 -0
  31. data/doc/layouts/default/default.yaml +4 -0
  32. data/doc/lib/default.rb +7 -0
  33. data/doc/output/Assets/BG-Ad-Top.png +0 -0
  34. data/doc/output/Assets/BG-Body.png +0 -0
  35. data/doc/output/Assets/BG-Feed.png +0 -0
  36. data/doc/output/Assets/BG-Menu-Hover.png +0 -0
  37. data/doc/output/Assets/BG-Menu.png +0 -0
  38. data/doc/output/Assets/BG-Sidebar-Bottom.png +0 -0
  39. data/doc/output/Assets/Button-Feed.png +0 -0
  40. data/doc/output/images/bg-ad-top.png +0 -0
  41. data/doc/output/images/bg-body.png +0 -0
  42. data/doc/output/images/bg-feed.gif +0 -0
  43. data/doc/output/images/bg-footer.jpg +0 -0
  44. data/doc/output/images/bg-header.jpg +0 -0
  45. data/doc/output/images/bg-menu-hover.png +0 -0
  46. data/doc/output/images/bg-menu.png +0 -0
  47. data/doc/output/images/bg-sidebar-bottom.gif +0 -0
  48. data/doc/output/images/button-feed.png +0 -0
  49. data/doc/output/images/icon-comment.png +0 -0
  50. data/doc/output/images/more_icon.gif +0 -0
  51. data/doc/output/style.css +299 -0
  52. data/doc/page_defaults.yaml +13 -0
  53. data/doc/tasks/default.rake +3 -0
  54. data/doc/templates/default/default.txt +1 -0
  55. data/doc/templates/default/default.yaml +4 -0
  56. data/examples/backgroundrb.yml +25 -0
  57. data/examples/foo_controller.rb +48 -0
  58. data/examples/god_worker.rb +7 -0
  59. data/examples/worker_tests/god_worker_test.rb +8 -0
  60. data/examples/workers/error_worker.rb +17 -0
  61. data/examples/workers/foo_worker.rb +38 -0
  62. data/examples/workers/god_worker.rb +7 -0
  63. data/examples/workers/model_worker.rb +13 -0
  64. data/examples/workers/renewal_worker.rb +11 -0
  65. data/examples/workers/rss_worker.rb +26 -0
  66. data/examples/workers/server_worker.rb +31 -0
  67. data/examples/workers/world_worker.rb +12 -0
  68. data/examples/workers/xmpp_worker.rb +7 -0
  69. data/init.rb +7 -0
  70. data/install.rb +1 -0
  71. data/know_issues.org +5 -0
  72. data/lib/backgroundrb.rb +1 -0
  73. data/lib/backgroundrb/bdrb_client_helper.rb +8 -0
  74. data/lib/backgroundrb/bdrb_cluster_connection.rb +156 -0
  75. data/lib/backgroundrb/bdrb_config.rb +43 -0
  76. data/lib/backgroundrb/bdrb_conn_error.rb +29 -0
  77. data/lib/backgroundrb/bdrb_connection.rb +179 -0
  78. data/lib/backgroundrb/bdrb_job_queue.rb +79 -0
  79. data/lib/backgroundrb/bdrb_result.rb +19 -0
  80. data/lib/backgroundrb/bdrb_start_stop.rb +146 -0
  81. data/lib/backgroundrb/rails_worker_proxy.rb +181 -0
  82. data/lib/backgroundrb/railtie.rb +48 -0
  83. data/lib/generators/backgroundrb/bdrb_migration/USAGE +12 -0
  84. data/lib/generators/backgroundrb/bdrb_migration/bdrb_migration_generator.rb +15 -0
  85. data/lib/generators/backgroundrb/bdrb_migration/templates/migration.rb +27 -0
  86. data/lib/generators/backgroundrb/worker/USAGE +16 -0
  87. data/lib/generators/backgroundrb/worker/templates/unit_test.rb +12 -0
  88. data/lib/generators/backgroundrb/worker/templates/worker.rb +7 -0
  89. data/lib/generators/backgroundrb/worker/worker_generator.rb +14 -0
  90. data/lib/tasks/backgroundrb_tasks.rake +103 -0
  91. data/release_notes.org +48 -0
  92. data/release_points.org +46 -0
  93. data/script/backgroundrb +52 -0
  94. data/script/bdrb_test_helper.rb +99 -0
  95. data/script/load_worker_env.rb +31 -0
  96. data/script/monitrc +25 -0
  97. data/server/backgroundrb_server.rb +12 -0
  98. data/server/lib/bdrb_result_storage.rb +62 -0
  99. data/server/lib/bdrb_server_helper.rb +24 -0
  100. data/server/lib/bdrb_thread_pool.rb +127 -0
  101. data/server/lib/cron_trigger.rb +197 -0
  102. data/server/lib/invalid_dump_error.rb +4 -0
  103. data/server/lib/log_worker.rb +25 -0
  104. data/server/lib/master_proxy.rb +140 -0
  105. data/server/lib/master_worker.rb +187 -0
  106. data/server/lib/meta_worker.rb +432 -0
  107. data/server/lib/trigger.rb +34 -0
  108. data/test/bdrb_client_test_helper.rb +5 -0
  109. data/test/bdrb_test_helper.rb +35 -0
  110. data/test/client/backgroundrb.yml +17 -0
  111. data/test/client/test_bdrb_client_helper.rb +13 -0
  112. data/test/client/test_bdrb_cluster_connection.rb +162 -0
  113. data/test/client/test_bdrb_config.rb +20 -0
  114. data/test/client/test_bdrb_connection.rb +29 -0
  115. data/test/client/test_bdrb_job_queue.rb +63 -0
  116. data/test/client/test_worker_proxy.rb +130 -0
  117. data/test/server/test_cron_trigger.rb +281 -0
  118. data/test/server/test_master_proxy.rb +54 -0
  119. data/test/server/test_master_worker.rb +157 -0
  120. data/test/server/test_meta_worker.rb +281 -0
  121. data/test/server/test_result_storage.rb +14 -0
  122. data/test/socket_mocker.rb +34 -0
  123. data/test/workers/bar_worker.rb +10 -0
  124. data/test/workers/foo_worker.rb +10 -0
  125. data/uninstall.rb +1 -0
  126. metadata +345 -0
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ RAILS_HOME = File.expand_path(File.join(File.dirname(__FILE__),".."))
4
+
5
+ require "rubygems"
6
+ require "active_support"
7
+ require "active_record"
8
+
9
+ require "yaml"
10
+ require "erb"
11
+ require "logger"
12
+ require "optparse"
13
+
14
+ require RAILS_HOME + "/config/boot"
15
+ require "backgroundrb"
16
+
17
+ BDRB_HOME = ::BackgrounDRb::BACKGROUNDRB_ROOT
18
+
19
+ ["server","server/lib","lib","lib/backgroundrb"].each { |x| $LOAD_PATH.unshift(BDRB_HOME + "/#{x}")}
20
+
21
+ $LOAD_PATH.unshift(File.join(RAILS_HOME,"lib","workers"))
22
+
23
+ require "bdrb_config"
24
+
25
+ BDRB_CONFIG = BackgrounDRb::Config.read_config("#{RAILS_HOME}/config/backgroundrb.yml")
26
+
27
+ if !(::Packet::WorkerRunner::WORKER_OPTIONS[:worker_env] == false)
28
+ require RAILS_HOME + "/config/environment"
29
+ end
30
+ require "backgroundrb_server"
31
+
data/script/monitrc ADDED
@@ -0,0 +1,25 @@
1
+ set daemon 60
2
+ set mailserver localhost
3
+ set mail-format {
4
+ from: sample@foobar.com
5
+ subject: Alert from Backgroundrb
6
+ }
7
+ set alert hemant@gmail.com
8
+
9
+ set httpd port 3000 and
10
+ use address localhost # only accept connection from localhost
11
+ allow localhost # allow localhost to connect to the server and
12
+
13
+ check process backgroundrb
14
+ with pidfile /home/hemant/rails_sandbox/tmp/pids/backgroundrb_11008.pid
15
+ start program = "/usr/bin/ruby /home/hemant/rails_sandbox/script/backgroundrb start"
16
+ stop program = "/usr/bin/ruby /home/hemant/rails_sandbox/script/backgroundrb stop"
17
+ if totalmem > 50.0 MB for 5 cycles then restart
18
+ if cpu usage > 95% for 3 cycles then restart
19
+
20
+ if failed port 11008 type tcp
21
+ with timeout 30 seconds
22
+ for 5 cycles
23
+ then restart
24
+ group backgroundrb
25
+
@@ -0,0 +1,12 @@
1
+ require "chronic"
2
+ require "lib/bdrb_result_storage"
3
+ require "lib/bdrb_thread_pool"
4
+ require "lib/bdrb_server_helper"
5
+ require "lib/master_worker"
6
+ require "lib/master_proxy"
7
+ require "lib/cron_trigger"
8
+ require "lib/invalid_dump_error"
9
+ require "lib/log_worker"
10
+ require "lib/trigger"
11
+ require "lib/meta_worker"
12
+
@@ -0,0 +1,62 @@
1
+ module BackgrounDRb
2
+ class ResultStorage
3
+ attr_accessor :cache,:worker_name,:worker_key,:storage_type
4
+ def initialize(worker_name,worker_key,storage_type = nil)
5
+ @worker_name = worker_name
6
+ @worker_key = worker_key
7
+ @mutex = Mutex.new
8
+ @storage_type = storage_type
9
+ @cache = (@storage_type == 'memcache') ? memcache_instance : {}
10
+ end
11
+
12
+ # Initialize Memcache for result or object caching
13
+ def memcache_instance
14
+ require 'memcache'
15
+ memcache_options = {
16
+ :c_threshold => 10_000,
17
+ :compression => true,
18
+ :debug => false,
19
+ :namespace => 'backgroundrb_result_hash',
20
+ :readonly => false,
21
+ :urlencode => false
22
+ }
23
+ t_cache = MemCache.new(memcache_options)
24
+ t_cache.servers = BDRB_CONFIG[:memcache].split(',')
25
+ t_cache
26
+ end
27
+
28
+ # generate key based on worker_name and worker_key
29
+ # for local cache, there is no need of unique key
30
+ def gen_key key
31
+ if storage_type == 'memcache'
32
+ key = [worker_name,worker_key,key].compact.join('_')
33
+ key
34
+ else
35
+ key
36
+ end
37
+ end
38
+
39
+ # fetch object from cache
40
+ def [] key
41
+ @mutex.synchronize { @cache[gen_key(key)] }
42
+ end
43
+
44
+ def []= key,value
45
+ @mutex.synchronize { @cache[gen_key(key)] = value }
46
+ end
47
+
48
+ def delete key
49
+ @mutex.synchronize { @cache.delete(gen_key(key)) }
50
+ end
51
+
52
+ def shift key
53
+ val = nil
54
+ @mutex.synchronize do
55
+ val = @cache[key]
56
+ @cache.delete(key)
57
+ end
58
+ return val
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,24 @@
1
+ module BackgrounDRb
2
+ module BdrbServerHelper
3
+ # Load data using Marshal.load, if load fails because of undefined constant
4
+ # try to load the constant. FIXME: regexp needs to handle all the cases.
5
+ def load_data data
6
+ begin
7
+ return Marshal.load(data)
8
+ rescue
9
+ error_msg = $!.message
10
+ if error_msg =~ /^undefined\ .+\ ([A-Z][^:]+)/
11
+ file_name = $1.underscore
12
+ begin
13
+ require file_name
14
+ return Marshal.load(data)
15
+ rescue
16
+ return nil
17
+ end
18
+ else
19
+ return nil
20
+ end
21
+ end # end of load_data method
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,127 @@
1
+ module BackgrounDRb
2
+
3
+ class InterruptedException < RuntimeError ; end
4
+
5
+ class WorkData
6
+ attr_accessor :args,:block,:job_method,:persistent_job_id,:job_key
7
+ def initialize(args,job_key,job_method,persistent_job_id)
8
+ @args = args
9
+ @job_key = job_key
10
+ @job_method = job_method
11
+ @persistent_job_id = persistent_job_id
12
+ end
13
+ end
14
+
15
+ class ThreadPool
16
+ attr_accessor :size,:threads,:work_queue,:logger
17
+ attr_accessor :result_queue,:master
18
+
19
+ def initialize(master,size,logger)
20
+ @master = master
21
+ @logger = logger
22
+ @size = size
23
+ @threads = []
24
+ @work_queue = []
25
+ @mutex = Monitor.new
26
+ @cv = @mutex.new_cond
27
+ @size.times { add_thread }
28
+ end
29
+
30
+ # can be used to make a call in threaded manner
31
+ # passed block runs in a thread from thread pool
32
+ # for example in a worker method you can do:
33
+ # def user_tags url
34
+ # thread_pool.defer(:fetch_url,url)
35
+ # end
36
+ # def fetch_url(url)
37
+ # begin
38
+ # data = Net::HTTP.get(url,'/')
39
+ # File.open("#{RAILS_ROOT}/log/pages.txt","w") do |fl|
40
+ # fl.puts(data)
41
+ # end
42
+ # rescue
43
+ # logger.info "Error downloading page"
44
+ # end
45
+ # end
46
+ # you can invoke above method from rails as:
47
+ # MiddleMan.worker(:rss_worker).async_user_tags(:arg => "en.wikipedia.org")
48
+ # assuming method is defined in rss_worker
49
+
50
+ def defer(method_name,args = nil)
51
+ @mutex.synchronize do
52
+ job_key = Thread.current[:job_key]
53
+ persistent_job_id = Thread.current[:persistent_job_id]
54
+ @cv.wait_while { @work_queue.size >= size }
55
+ @work_queue.push(WorkData.new(args,job_key,method_name,persistent_job_id))
56
+ @cv.broadcast
57
+ end
58
+ end
59
+
60
+ # Start worker threads
61
+ def add_thread
62
+ @threads << Thread.new do
63
+ Thread.current[:job_key] = nil
64
+ Thread.current[:persistent_job_id] = nil
65
+ while true
66
+ begin
67
+ task = nil
68
+ @mutex.synchronize do
69
+ @cv.wait_while { @work_queue.size == 0 }
70
+ task = @work_queue.pop
71
+ @cv.broadcast
72
+ end
73
+ if task
74
+ Thread.current[:job_key] = task.job_key
75
+ Thread.current[:persistent_job_id] = task.persistent_job_id
76
+ block_result = run_task(task)
77
+ end
78
+ rescue BackgrounDRb::InterruptedException
79
+ STDERR.puts("BackgrounDRb thread interrupted: #{Thread.current.inspect}")
80
+ STDERR.flush
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # run tasks popped out of queue
87
+ def run_task task
88
+ block_arity = master.method(task.job_method).arity
89
+ begin
90
+ check_db_connection
91
+ t_data = task.args
92
+ result = nil
93
+ if block_arity != 0
94
+ result = master.send(task.job_method,task.args)
95
+ else
96
+ result = master.send(task.job_method)
97
+ end
98
+ return result
99
+ rescue BackgrounDRb::InterruptedException => e
100
+ # Don't log, just re-raise
101
+ raise e
102
+ rescue Object => bdrb_error
103
+ log_exception(bdrb_error)
104
+ return nil
105
+ end
106
+ end
107
+
108
+ def log_exception exception_object
109
+ STDERR.puts exception_object.to_s
110
+ STDERR.puts exception_object.backtrace.join("\n")
111
+ STDERR.flush
112
+ end
113
+
114
+
115
+ # Periodic check for lost database connections and closed connections
116
+ def check_db_connection
117
+ begin
118
+ ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
119
+ rescue Object => bdrb_error
120
+ log_exception(bdrb_error)
121
+ end
122
+ end
123
+
124
+
125
+ end #end of class ThreadPool
126
+ end # end of module BackgrounDRb
127
+
@@ -0,0 +1,197 @@
1
+ module BackgrounDRb
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
+ # initialize the Cron Trigger
10
+ def initialize(expr)
11
+ self.cron_expr = expr
12
+ end
13
+
14
+ # create the cron expression and populate instance variables.
15
+ def cron_expr=(expr)
16
+ @cron_expr = expr
17
+ self.sec, self.min, self.hour, self.day, self.month, self.wday, self.year = @cron_expr.split(' ')
18
+ end
19
+
20
+ def fire_after_time(p_time)
21
+ @t_sec,@t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday,@t_yday,@t_idst,@t_zone = p_time.to_a
22
+ @count = 0
23
+ loop do
24
+ @count += 1
25
+
26
+ if @year && !@year.include?(@t_year)
27
+ return nil if @t_year > @year.max
28
+ @t_year = @year.detect { |y| y > @t_year }
29
+ end
30
+
31
+ # if range of months doesn't include current month, find next month from the range
32
+ unless @month.include?(@t_month)
33
+ next_month = @month.detect { |m| m > @t_month } || @month.min
34
+ @t_day,@t_hour,@t_min,@t_sec = @day.min,@hour.min,@min.min,@sec.min
35
+ if next_month < @t_month
36
+ @t_month = next_month
37
+ @t_year += 1
38
+ retry
39
+ end
40
+ @t_month = next_month
41
+ end
42
+
43
+ if !day_restricted? && wday_restricted?
44
+ unless @wday.include?(@t_wday)
45
+ next_wday = @wday.detect { |w| w > @t_wday} || @wday.min
46
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
47
+ t_time = Chronic.parse("next #{WDAYS[next_wday]}",:now => current_time)
48
+ @t_day,@t_month,@t_year = t_time.to_a[3..5]
49
+ @t_wday = next_wday
50
+ retry
51
+ end
52
+ elsif !wday_restricted? && day_restricted?
53
+ day_range = (1.. month_days(@t_year,@t_month))
54
+ # day array, that includes days which are present in current month
55
+ day_array = @day.select { |d| day_range === d }
56
+ unless day_array.include?(@t_day)
57
+ next_day = day_array.detect { |d| d > @t_day } || day_array.min
58
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
59
+ if !next_day || next_day < @t_day
60
+ t_time = Chronic.parse("next month",:now => current_time)
61
+ @t_day = next_day.nil? ? @day.min : next_day
62
+ @t_month,@t_year = t_time.month,t_time.year
63
+ retry
64
+ end
65
+ @t_day = next_day
66
+ end
67
+ else
68
+ # if both day and wday are restricted cron should give preference to one thats closer to current time
69
+ day_range = (1 .. month_days(@t_year,@t_month))
70
+ day_array = @day.select { |d| day_range === d }
71
+ if !day_array.include?(@t_day) && !@wday.include?(@t_wday)
72
+ next_day = day_array.detect { |d| d > @t_day } || day_array.min
73
+ next_wday = @wday.detect { |w| w > @t_wday } || @wday.min
74
+ @t_hour,@t_min,@t_sec = @hour.min,@min.min,@sec.min
75
+
76
+ # if next_day is nil or less than @t_day it means that it should run in next month
77
+ if !next_day || next_day < @t_day
78
+ next_time_mday = Chronic.parse("next month",:now => current_time)
79
+ else
80
+ @t_day = next_day
81
+ next_time_mday = current_time
82
+ end
83
+ next_time_wday = Chronic.parse("next #{WDAYS[next_wday]}",:now => current_time)
84
+ if next_time_mday < next_time_wday
85
+ @t_day,@t_month,@t_year = next_time_mday.to_a[3..5]
86
+ else
87
+ @t_day,@t_month,@t_year = next_time_wday.to_a[3..5]
88
+ end
89
+ retry
90
+ end
91
+ end
92
+
93
+ unless @hour.include?(@t_hour)
94
+ next_hour = @hour.detect { |h| h > @t_hour } || @hour.min
95
+ @t_min,@t_sec = @min.min,@sec.min
96
+ if next_hour < @t_hour
97
+ @t_hour = next_hour
98
+ next_day = Chronic.parse("next day",:now => current_time)
99
+ @t_day,@t_month,@t_year,@t_wday = next_day.to_a[3..6]
100
+ retry
101
+ end
102
+ @t_hour = next_hour
103
+ end
104
+
105
+ unless @min.include?(@t_min)
106
+ next_min = @min.detect { |m| m > @t_min } || @min.min
107
+ @t_sec = @sec.min
108
+ if next_min < @t_min
109
+ @t_min = next_min
110
+ next_hour = Chronic.parse("next hour",:now => current_time)
111
+ @t_hour,@t_day,@t_month,@t_year,@t_wday = next_hour.to_a[2..6]
112
+ retry
113
+ end
114
+ @t_min = next_min
115
+ end
116
+
117
+ unless @sec.include?(@t_sec)
118
+ next_sec = @sec.detect { |s| s > @t_sec } || @sec.min
119
+ if next_sec < @t_sec
120
+ @t_sec = next_sec
121
+ next_min = Chronic.parse("next minute",:now => current_time)
122
+ @t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday = next_min.to_a[1..6]
123
+ retry
124
+ end
125
+ @t_sec = next_sec
126
+ end
127
+ break
128
+ end # end of loop do
129
+ current_time
130
+ end
131
+
132
+ def current_time
133
+ Time.local(@t_sec,@t_min,@t_hour,@t_day,@t_month,@t_year,@t_wday,nil,@t_idst,@t_zone)
134
+ end
135
+
136
+ def day_restricted?
137
+ return !@day.eql?(1..31)
138
+ end
139
+
140
+ def wday_restricted?
141
+ return !@wday.eql?(0..6)
142
+ end
143
+
144
+ # TODO: mimic attr_reader to define all of these
145
+ def sec=(sec); @sec = parse_part(sec, 0 .. 59); end
146
+
147
+ def min=(min); @min = parse_part(min, 0 .. 59); end
148
+
149
+ def hour=(hour); @hour = parse_part(hour, 0 .. 23); end
150
+
151
+ def day=(day)
152
+ @day = parse_part(day, 1 .. 31)
153
+ end
154
+
155
+ def month=(month)
156
+ @month = parse_part(month, 1 .. 12)
157
+ end
158
+
159
+ def year=(year)
160
+ @year = parse_part(year)
161
+ end
162
+
163
+ def wday=(wday)
164
+ @wday = parse_part(wday, 0 .. 6)
165
+ end
166
+ private
167
+ def month_days(y, m)
168
+ if ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0)
169
+ LeapYearMonthDays[m-1]
170
+ else
171
+ CommonYearMonthDays[m-1]
172
+ end
173
+ end
174
+
175
+ # 0-5,8,10; 0-5; *; */5
176
+ def parse_part(part, range=nil)
177
+ return range if part.nil? or part == '*' or part =~ /^[*0]\/1$/
178
+
179
+ r = Array.new
180
+ part.split(',').each do |p|
181
+ if p =~ /-/ # 0-5
182
+ r << Range.new(*(p.scan(/\d+/).map { |x| x.to_i })).map { |x| x.to_i }
183
+ elsif p =~ /(\*|\d+)\/(\d+)/ && range # */5, 2/10
184
+ min = $1 == '*' ? 0 : $1.to_i
185
+ inc = $2.to_i
186
+ (min .. range.end).each_with_index do |x, i|
187
+ r << (range.begin == 1 ? x + 1 : x) if i % inc == 0
188
+ end
189
+ else
190
+ r << p.to_i
191
+ end
192
+ end
193
+ r.flatten
194
+ end
195
+ end
196
+ end
197
+