backgroundrb-rails3 1.1

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