openwferu 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/examples/mano_tracker.rb +165 -0
  2. data/examples/scheduler_cron_usage.rb +46 -0
  3. data/examples/scheduler_usage.rb +54 -0
  4. data/lib/openwfe/contextual.rb +7 -1
  5. data/lib/openwfe/engine/engine.rb +58 -15
  6. data/lib/openwfe/expool/expressionpool.rb +116 -14
  7. data/lib/openwfe/expool/expstorage.rb +12 -12
  8. data/lib/openwfe/expool/journalexpstorage.rb +1 -1
  9. data/lib/openwfe/expool/yamlexpstorage.rb +58 -22
  10. data/lib/openwfe/expressions/environment.rb +32 -2
  11. data/lib/openwfe/expressions/expressionmap.rb +17 -0
  12. data/lib/openwfe/expressions/fe_condition.rb +122 -0
  13. data/lib/openwfe/expressions/fe_cursor.rb +14 -5
  14. data/lib/openwfe/expressions/fe_participant.rb +55 -4
  15. data/lib/openwfe/expressions/fe_raw.rb +43 -12
  16. data/lib/openwfe/expressions/fe_subprocess.rb +10 -0
  17. data/lib/openwfe/expressions/fe_time.rb +117 -22
  18. data/lib/openwfe/expressions/fe_value.rb +27 -8
  19. data/lib/openwfe/expressions/flowexpression.rb +13 -6
  20. data/lib/openwfe/expressions/raw_prog.rb +13 -11
  21. data/lib/openwfe/expressions/timeout.rb +94 -0
  22. data/lib/openwfe/flowexpressionid.rb +17 -19
  23. data/lib/openwfe/logging.rb +35 -16
  24. data/lib/openwfe/participants/atomparticipants.rb +31 -7
  25. data/lib/openwfe/participants/enoparticipant.rb +43 -3
  26. data/lib/openwfe/participants/participant.rb +21 -1
  27. data/lib/openwfe/participants/participantmap.rb +4 -2
  28. data/lib/openwfe/participants/participants.rb +12 -17
  29. data/lib/openwfe/participants/soapparticipants.rb +15 -3
  30. data/lib/openwfe/rudefinitions.rb +3 -0
  31. data/lib/openwfe/service.rb +8 -0
  32. data/lib/openwfe/storage/yamlfilestorage.rb +85 -47
  33. data/lib/openwfe/{otime.rb → util/otime.rb} +0 -0
  34. data/lib/openwfe/util/scheduler.rb +415 -231
  35. data/lib/openwfe/util/schedulers.rb +11 -3
  36. data/lib/openwfe/util/stoppable.rb +69 -0
  37. data/lib/openwfe/utils.rb +14 -25
  38. data/lib/openwfe/workitem.rb +12 -6
  39. data/lib/openwfe/worklist/storeparticipant.rb +145 -0
  40. data/test/{atomtest.rb → atom_test.rb} +0 -0
  41. data/test/{crontest.rb → cron_test.rb} +7 -6
  42. data/test/cronline_test.rb +51 -0
  43. data/test/{dollartest.rb → dollar_test.rb} +0 -0
  44. data/test/{feitest.rb → fei_test.rb} +0 -0
  45. data/test/file_persistence_test.rb +15 -9
  46. data/test/flowtestbase.rb +11 -5
  47. data/test/ft_0.rb +8 -0
  48. data/test/ft_10_loop.rb +72 -10
  49. data/test/ft_11_ppd.rb +49 -0
  50. data/test/ft_17_condition.rb +83 -0
  51. data/test/ft_18_pname.rb +59 -0
  52. data/test/hparticipant_test.rb +96 -0
  53. data/test/{misctest.rb → misc_test.rb} +1 -1
  54. data/test/rake_qtest.rb +10 -4
  55. data/test/rake_test.rb +12 -1
  56. data/test/raw_prog_test.rb +1 -1
  57. data/test/restart_cron_test.rb +78 -0
  58. data/test/restart_test.rb +79 -0
  59. data/test/scheduler_test.rb +92 -0
  60. data/test/{timetest.rb → time_test.rb} +3 -38
  61. data/test/timeout_test.rb +73 -0
  62. metadata +26 -11
  63. data/lib/openwfe/worklist/worklists.rb +0 -175
@@ -41,15 +41,27 @@
41
41
 
42
42
  require 'soap/rpc/driver'
43
43
 
44
- require 'openwfe/participants/participants'
44
+ require 'openwfe/participants/participant'
45
45
 
46
46
 
47
47
  module OpenWFE
48
48
 
49
49
  #
50
- # Stores the incoming workitem into an 'atom feed'
50
+ # Wrapping a simple web service call within an OpenWFEru participant.
51
51
  #
52
- class SoapParticipant < LocalParticipant
52
+ # quote_service = OpenWFE::SoapParticipant.new(
53
+ # "http://services.xmethods.net/soap", # service URI
54
+ # "urn:xmethods-delayed-quotes", # namespace
55
+ # "getQuote", # operation name
56
+ # [ "symbol" ]) # param arrays (workitem fields)
57
+ #
58
+ # engine.register_participant("quote_service", quote_service)
59
+ #
60
+ # TODO #8425 : use blocks as hooks for the mapping (pre/post)
61
+ # of workitems into webservice operation params.
62
+ #
63
+ class SoapParticipant
64
+ include LocalParticipant
53
65
 
54
66
  def initialize \
55
67
  (endpoint_url, namespace, method_name, params, param_prefix="")
@@ -87,6 +87,9 @@ module OpenWFE
87
87
  def get_engine
88
88
  return @application_context[S_ENGINE]
89
89
  end
90
+ def get_scheduler
91
+ return @application_context[S_SCHEDULER]
92
+ end
90
93
  def get_expression_map
91
94
  return @application_context[S_EXPRESSION_MAP]
92
95
  end
@@ -56,6 +56,14 @@ module OpenWFE
56
56
  @service_name = service_name
57
57
  @application_context = application_context
58
58
  end
59
+
60
+ #
61
+ # Some services (like the scheduler one for example) need to
62
+ # free some resources upon stopping. This can be achieved by
63
+ # overwriting this method.
64
+ #
65
+ def stop
66
+ end
59
67
  end
60
68
 
61
69
  class Service
@@ -40,6 +40,7 @@
40
40
  # John Mettraux at openwfe.org
41
41
  #
42
42
 
43
+ require 'find'
43
44
  require 'yaml'
44
45
  require 'fileutils'
45
46
 
@@ -59,7 +60,8 @@ module OpenWFE
59
60
 
60
61
 
61
62
  #
62
- # yaml expression storage
63
+ # Stores OpenWFEru related objects into yaml encoded files.
64
+ # This storage is meant to look and feel like a Hash.
63
65
  #
64
66
  class YamlFileStorage
65
67
  include ServiceMixin
@@ -72,67 +74,95 @@ module OpenWFE
72
74
  FileUtils.makedirs @basepath
73
75
  end
74
76
 
75
- def []= (id, object)
77
+ #
78
+ # Stores an object with its FlowExpressionId instance as its key.
79
+ #
80
+ def []= (fei, object)
76
81
 
77
- fei_path = compute_file_path(id)
82
+ fei_path = compute_file_path(fei)
78
83
  fei_parent_path = File.dirname(fei_path)
79
84
 
80
85
  FileUtils.makedirs(fei_parent_path) \
81
- if not File.exist?(fei_parent_path)
86
+ if not File.exist?(fei_parent_path)
82
87
 
83
- fd = IO.sysopen(fei_path , "w+")
84
- io = IO.open(fd , "w+")
85
-
86
- data = YAML.dump(object)
87
-
88
- io.write(data)
89
- io.close
90
- end
88
+ fd = IO.sysopen(fei_path , "w+")
89
+ io = IO.open(fd , "w+")
91
90
 
92
- #
93
- # deletes the whole storage directory... beware...
94
- #
95
- def purge
96
- FileUtils.remove_dir @basepath
97
- end
91
+ data = YAML.dump(object)
98
92
 
99
- #
100
- # check whether the key has a file in the file storage
101
- def has_key? (fei)
102
- File.exist?(compute_file_path(fei))
103
- end
93
+ io.write(data)
94
+ io.close
95
+ end
104
96
 
105
- def remove (fei, workitem)
106
-
107
- fei_path = compute_file_path(fei)
108
-
109
- if File.exist?(fei_path)
110
- File.delete(fei_path)
111
- else
112
- raise "Object not found at #{fei_path}"
113
- end
97
+ #
98
+ # Deletes the whole storage directory... beware...
99
+ #
100
+ def purge
101
+ FileUtils.remove_dir @basepath
102
+ end
103
+
104
+ #
105
+ # Checks whether there is an object (expression, workitem) stored
106
+ # for the given FlowExpressionId instance.
107
+ #
108
+ def has_key? (fei)
109
+ File.exist?(compute_file_path(fei))
110
+ end
111
+
112
+ #
113
+ # Removes the object (file) stored for the given FlowExpressionId
114
+ # instance.
115
+ #
116
+ def delete (fei)
117
+
118
+ fei_path = compute_file_path(fei)
119
+
120
+ if File.exist?(fei_path)
121
+ File.delete(fei_path)
122
+ else
123
+ raise "Object not found at #{fei_path}"
114
124
  end
125
+ end
126
+
127
+ #
128
+ # Actually loads and returns the object for the given
129
+ # FlowExpressionId instance.
130
+ #
131
+ def [] (fei)
132
+
133
+ fei_path = compute_file_path(fei)
115
134
 
116
- def [] (fei)
117
- fei_path = compute_file_path(fei)
118
-
119
- return nil if not File.exist?(fei_path)
120
-
121
- data = IO.read(compute_file_path(fei))
135
+ return nil if not File.exist?(fei_path)
136
+
137
+ load_object(fei_path)
138
+ end
139
+
140
+ #
141
+ # Returns the count of objects currently stored in this instance.
142
+ #
143
+ def length
144
+ return count_objects(0, @basepath)
145
+ end
146
+
147
+ alias :size :length
148
+
149
+ protected
150
+
151
+ def load_object (path)
152
+
153
+ data = IO.read(path)
122
154
  object = YAML.load(data)
123
155
 
124
- object.application_context = @application_context
156
+ object.application_context = @application_context \
157
+ if object.respond_to? :application_context=
125
158
 
126
159
  return object
127
160
  end
128
161
 
129
- def length
130
- return count_expression(0, @basepath)
131
- end
132
-
133
- protected
134
-
135
- def count_expression (count, item)
162
+ def count_objects (count, item)
163
+
164
+ # TODO #8346 : use "find" to do that job
165
+ # measure perf before and after change !
136
166
 
137
167
  return count + 1 if OpenWFE::ends_with(item, ".yaml")
138
168
 
@@ -142,13 +172,21 @@ module OpenWFE
142
172
  d.each do |i|
143
173
  next if i == "." or i == ".."
144
174
  i = item + "/" + i
145
- count = count_expression(count, i)
175
+ count = count_objects(count, i)
146
176
  end
147
177
  d.close()
148
178
  end
149
179
 
150
180
  return count
151
181
  end
182
+
183
+ def each_object_path (&block)
184
+ return unless block
185
+ Find.find(@basepath) do |path|
186
+ block.call path \
187
+ unless File.stat(path).directory?
188
+ end
189
+ end
152
190
 
153
191
  def compute_file_path (fei)
154
192
  raise "this should be implemented in a subclass"
File without changes
@@ -41,14 +41,54 @@
41
41
 
42
42
  require 'monitor'
43
43
 
44
- require 'openwfe/otime'
45
- require 'openwfe/utils'
44
+ #require 'openwfe/utils'
45
+ require 'openwfe/util/otime'
46
+ require 'openwfe/util/stoppable'
46
47
 
47
48
 
48
49
  module OpenWFE
49
50
 
51
+ #
52
+ # The Scheduler is used by OpenWFEru for registering 'at' and 'cron' jobs.
53
+ # 'at' jobs to execute once at a given point in time. 'cron' jobs
54
+ # execute a specified intervals.
55
+ # The two main methods are thus schedule_at() and schedule().
56
+ #
57
+ # schedule_at() and schedule() await either a Schedulable instance and
58
+ # params (usually an array or nil), either a block, which is more in the
59
+ # Ruby way.
60
+ #
61
+ # Two examples :
62
+ #
63
+ # scheduler.schedule_in("3d") do
64
+ # regenerate_monthly_report()
65
+ # end
66
+ # #
67
+ # # will call the regenerate_monthly_report method
68
+ # # in 3 days from now
69
+ #
70
+ # and
71
+ #
72
+ # class Regenerator < Schedulable
73
+ # def trigger (frequency)
74
+ # self.send(frequency)
75
+ # end
76
+ # def monthly
77
+ # # ...
78
+ # end
79
+ # def yearly
80
+ # # ...
81
+ # end
82
+ # end
83
+ #
84
+ # regenerator = Regenerator.new
85
+ #
86
+ # scheduler.schedule_in("4d", r, :monthly)
87
+ # #
88
+ # # will regenerate the monthly report in four days
89
+ #
50
90
  class Scheduler
51
- include MonitorMixin
91
+ include MonitorMixin, Stoppable
52
92
 
53
93
  attr_accessor \
54
94
  :precision
@@ -69,92 +109,46 @@ module OpenWFE
69
109
  @last_cron_minute = -1
70
110
  end
71
111
 
72
- def stop
73
- @scheduler_thread.stop \
74
- if @scheduler_thread and not @scheduler_thread.stop?
75
- end
76
-
112
+ #
113
+ # Starts this scheduler (or restart it if it was previously stopped)
114
+ #
77
115
  def start
78
- if @scheduler_thread
79
- @scheduler_thread.wakeup
80
- return
81
- end
116
+
117
+ #if @scheduler_thread
118
+ # @scheduler_thread.wakeup
119
+ # return
120
+ #end
82
121
 
83
122
  @scheduler_thread = Thread.new do
84
123
  while true
124
+ break if self.is_stopped?
125
+ #print "."
126
+ #$stdout.flush
85
127
  step
86
128
  sleep(@precision)
87
129
  end
88
130
  end
89
- end
90
-
91
- def step
92
- synchronize do
93
- now = Time.new
94
- minute = now.to_i / 60
95
-
96
- #puts "step() minute is #{minute}"
97
- #puts "step() last_cron_minute is #{@last_cron_minute}"
98
-
99
- #
100
- # cron entries
101
-
102
- begin
103
- if minute > @last_cron_minute
104
- @last_cron_minute = minute
105
- @cron_entries.each do |cron_id, cron_entry|
106
- #puts "step() cron_id : #{cron_id}"
107
- cron_entry.trigger \
108
- if cron_entry.matches? now
109
- end
110
- end
111
- rescue Exception => e
112
- #puts \
113
- # "step() caught exception\n" +
114
- # OpenWFE::exception_to_s(e)
115
- end
116
-
117
- #
118
- # pending jobs
119
-
120
- now = now.to_f
121
- #
122
- # that's what at jobs do understand
123
-
124
- while true
125
-
126
- #puts "step() job.count is #{@pending_jobs.length}"
127
-
128
- break if @pending_jobs.length < 1
129
-
130
- job = @pending_jobs[0]
131
-
132
- #puts "step() job.at is #{job.at}"
133
- #puts "step() now is #{now}"
134
-
135
- break if job.at > now
136
-
137
- #if job.at <= now
138
- #
139
- # obviously
140
131
 
141
- job.trigger()
142
- @pending_jobs.delete_at(0)
143
- end
144
- end
132
+ do_restart
145
133
  end
146
134
 
147
135
  #
148
- # joins on the scheduler thread
136
+ # The scheduler is stoppable via stop() or do_stop()
137
+ #
138
+ alias :stop :do_stop
139
+
140
+ #
141
+ # Joins on the scheduler thread
149
142
  #
150
143
  def join
151
144
  @scheduler_thread.join
152
145
  end
153
146
 
154
147
  #
155
- # schedules a job by specifying at which time it should trigger
148
+ # Schedules a job by specifying at which time it should trigger.
149
+ # Returns the a job_id that can be used to unschedule the job.
156
150
  #
157
- def schedule_at (at, schedulable, params)
151
+ def schedule_at (at, schedulable=nil, params=nil, &block)
158
152
  synchronize do
159
153
 
160
154
  #puts "0 at is '#{at.to_s}' (#{at.class})"
@@ -168,9 +162,10 @@ module OpenWFE
168
162
  at = at.to_f \
169
163
  if at.kind_of? Time
170
164
 
171
- #puts "1 at is '#{at.to_s}' (#{at.class})"
165
+ #puts "1 at is '#{at.to_s}' (#{at.class})"}"
172
166
 
173
- job = JobEntry.new(at, schedulable, params)
167
+ b = to_block(schedulable, params, &block)
168
+ job = AtEntry.new(at, &b)
174
169
 
175
170
  if at < (Time.new.to_f + @precision)
176
171
  job.trigger()
@@ -193,14 +188,14 @@ module OpenWFE
193
188
  end
194
189
 
195
190
  return push(job)
196
-
197
191
  end
198
192
  end
199
193
 
200
194
  #
201
- # schedules a job by stating in how much time it should trigger
195
+ # Schedules a job by stating in how much time it should trigger.
196
+ # Returns the a job_id that can be used to unschedule the job.
202
197
  #
203
- def schedule_in (duration, schedulable, params)
198
+ def schedule_in (duration, schedulable=nil, params=nil, &block)
204
199
 
205
200
  if duration.kind_of?(String)
206
201
  duration = OpenWFE::parse_time_string(duration)
@@ -208,44 +203,124 @@ module OpenWFE
208
203
  duration = Float(duration.to_s)
209
204
  end
210
205
 
211
- return schedule_at(Time.new.to_f + duration, schedulable, params)
206
+ return schedule_at(
207
+ Time.new.to_f + duration, schedulable, params, &block)
212
208
  end
213
209
 
214
210
  #
215
- # unschedules 'at' or 'cron' job
211
+ # Unschedules an 'at' or a 'cron' job identified by the id
212
+ # it was given at schedule time.
216
213
  #
217
- def unschedule (entry_id)
214
+ def unschedule (job_id)
218
215
  synchronize do
219
216
 
220
217
  for i in 0...@pending_jobs.length
221
- if @pending_jobs[i].eid == entry_id
218
+ if @pending_jobs[i].eid == job_id
222
219
  @pending_jobs.delete_at(i)
223
220
  return true
224
221
  end
225
222
  end
226
223
 
227
- if @cron_entries.has_key?(entry_id)
228
- @cron_entries.delete(entry_id)
224
+ return true if unschedule_cron_job(job_id)
225
+
226
+ return false
227
+ end
228
+ end
229
+
230
+ #
231
+ # Unschedules a cron job
232
+ #
233
+ def unschedule_cron_job (job_id)
234
+ synchronize do
235
+ if @cron_entries.has_key?(job_id)
236
+ @cron_entries.delete(job_id)
229
237
  return true
230
238
  end
231
-
232
239
  return false
233
240
  end
234
241
  end
235
242
 
236
243
  #
237
- # schedules a cron job
244
+ # Schedules a cron job, the 'cron_line' is a string
245
+ # following the Unix cron standard (see "man 5 crontab" in your command
246
+ # line).
247
+ #
248
+ # For example :
249
+ #
250
+ # scheduler.schedule("5 0 * * *", nil, s, p)
251
+ # # will trigger the schedulable s with params p every day
252
+ # # five minutes after midnight
253
+ #
254
+ # scheduler.schedule("15 14 1 * *", nil, s, p)
255
+ # # will trigger s at 14:15 on the first of every month
256
+ #
257
+ # scheduler.schedule("0 22 * * 1-5") do
258
+ # puts "it's break time..."
259
+ # end
260
+ # # outputs a message every weekday at 10pm
238
261
  #
239
- def schedule (cron_line, schedulable, params)
262
+ # Returns the job id attributed to this 'cron job', this id can
263
+ # be used to unschedule the job.
264
+ #
265
+ def schedule \
266
+ (cron_line, cron_id=nil, schedulable=nil, params=nil, &block)
267
+
240
268
  synchronize do
241
- entry = CronEntry.new(cron_line, schedulable, params)
269
+
270
+ #
271
+ # is a job with the same id already scheduled ?
272
+
273
+ if cron_id
274
+ if unschedule(cron_id)
275
+ ldebug do
276
+ "schedule() unscheduled previous job "+
277
+ "under same name '#{cron_id}'"
278
+ end
279
+ end
280
+ end
281
+
282
+ #
283
+ # schedule
284
+
285
+ b = to_block(schedulable, params, &block)
286
+ entry = CronEntry.new(cron_id, cron_line, &b)
242
287
  @cron_entries[entry.eid] = entry
288
+
243
289
  return entry.eid
244
290
  end
245
291
  end
246
292
 
293
+ #
294
+ # Returns the job corresponding to job_id, an instance of AtEntry
295
+ # or CronEntry will be returned.
296
+ #
297
+ def get_job (job_id)
298
+
299
+ entry = @cron_entries[job_id]
300
+ return c if c
301
+
302
+ @pending_jobs.each do |entry|
303
+ return entry if entry.eid == job_id
304
+ end
305
+
306
+ return nil
307
+ end
308
+
247
309
  protected
248
310
 
311
+ def to_block (schedulable, params, &block)
312
+ if schedulable
313
+ lambda do
314
+ schedulable.trigger(params)
315
+ end
316
+ else
317
+ block
318
+ end
319
+ end
320
+
321
+ #
322
+ # Pushes an 'at' job into the pending job list
323
+ #
249
324
  def push (job, index=-1)
250
325
 
251
326
  if index == -1
@@ -264,6 +339,70 @@ module OpenWFE
264
339
 
265
340
  return job.eid
266
341
  end
342
+
343
+ #
344
+ # This is the method called each time the scheduler wakes up
345
+ # (by default 4 times per second). It's meant to quickly
346
+ # determine if there are jobs to trigger else to get back to sleep.
347
+ # 'cron' jobs get executed if necessary then 'at' jobs.
348
+ #
349
+ def step
350
+ synchronize do
351
+ now = Time.new
352
+ minute = now.min
353
+
354
+ #
355
+ # cron entries
356
+
357
+ begin
358
+ if now.sec == 0 and minute > @last_cron_minute
359
+ #
360
+ # only consider cron entries at the second 0 of a
361
+ # minute
362
+
363
+ @last_cron_minute = minute
364
+
365
+ @cron_entries.each do |cron_id, cron_entry|
366
+ #puts "step() cron_id : #{cron_id}"
367
+ cron_entry.trigger \
368
+ if cron_entry.matches? now
369
+ end
370
+ end
371
+ rescue Exception => e
372
+ #puts \
373
+ # "step() caught exception\n" +
374
+ # OpenWFE::exception_to_s(e)
375
+ end
376
+
377
+ #
378
+ # pending jobs
379
+
380
+ now = now.to_f
381
+ #
382
+ # that's what at jobs do understand
383
+
384
+ while true
385
+
386
+ #puts "step() job.count is #{@pending_jobs.length}"
387
+
388
+ break if @pending_jobs.length < 1
389
+
390
+ job = @pending_jobs[0]
391
+
392
+ #puts "step() job.at is #{job.at}"
393
+ #puts "step() now is #{now}"
394
+
395
+ break if job.at > now
396
+
397
+ #if job.at <= now
398
+ #
399
+ # obviously
400
+
401
+ job.trigger()
402
+ @pending_jobs.delete_at(0)
403
+ end
404
+ end
405
+ end
267
406
  end
268
407
 
269
408
  #
@@ -275,213 +414,258 @@ module OpenWFE
275
414
  def trigger (params)
276
415
  raise "trigger() implementation is missing"
277
416
  end
417
+
418
+ def reschedule (scheduler)
419
+ raise "reschedule() implentation is missing"
420
+ end
278
421
  end
279
422
 
280
- #
281
- # a 'cron line' is a line in the sense of a crontab (man 5 cron) file
282
- #
283
- class CronLine
423
+ protected
284
424
 
285
- def initialize (line)
425
+ JOB_ID_LOCK = Monitor.new
286
426
 
287
- super()
427
+ class Entry
428
+
429
+ @@last_given_id = 0
430
+ #
431
+ # as a scheduler is fully transient, no need to
432
+ # have persistent ids, a simple counter is sufficient
288
433
 
289
- items = line.split
434
+ attr_accessor \
435
+ :eid, :block
290
436
 
291
- if items.length != 5
292
- raise \
293
- "cron '#{line}' string should hold 5 items, " +
294
- "not #{items.length}" \
437
+ def initialize (entry_id=nil, &block)
438
+ @block = block
439
+ if entry_id
440
+ @eid = entry_id
441
+ else
442
+ JOB_ID_LOCK.synchronize do
443
+ @eid = @@last_given_id
444
+ @@last_given_id = @eid + 1
445
+ end
446
+ end
295
447
  end
296
448
 
297
- @minutes = parse_item(items[0], 0, 59)
298
- @hours = parse_item(items[1], 0, 24)
299
- @days = parse_item(items[2], 1, 31)
300
- @months = parse_item(items[3], 1, 12)
301
- @weekdays = parse_item(items[4], 1, 7)
302
-
303
- adjust_arrays()
449
+ #def trigger
450
+ # @block.call @eid
451
+ #end
304
452
  end
305
453
 
306
- def matches? (time)
454
+ class AtEntry < Entry
307
455
 
308
- if time.kind_of?(Float) or time.kind_of?(Integer)
309
- time = Time.at(time)
310
- end
456
+ attr_accessor \
457
+ :at
311
458
 
312
- return false if no_match?(time.min, @minutes)
313
- return false if no_match?(time.hour, @hours)
314
- return false if no_match?(time.day, @days)
315
- return false if no_match?(time.month, @months)
316
- return false if no_match?(time.wday, @weekdays)
459
+ def initialize (at, &block)
460
+ super(&block)
461
+ @at = at
462
+ end
317
463
 
318
- return true
464
+ def trigger
465
+ @block.call @eid, @at
466
+ end
319
467
  end
320
468
 
321
- private
469
+ class CronEntry < Entry
322
470
 
323
- #
324
- # adjust values to Ruby
325
- #
326
- def adjust_arrays()
327
- if @hours
328
- @hours.each do |h|
329
- h = 0 if h == 23
330
- end
331
- end
332
- if @weekdays
333
- @weekdays.each do |wd|
334
- wd = wd - 1
335
- end
471
+ attr_accessor \
472
+ :cron_line
473
+
474
+ def initialize (cron_id, line, &block)
475
+
476
+ super(cron_id, &block)
477
+
478
+ if line.kind_of? String
479
+ @cron_line = CronLine.new(line)
480
+ elsif line.kind_of? CronLine
481
+ @cron_line = line
482
+ else
483
+ raise \
484
+ "Cannot initialize a CronEntry " +
485
+ "with a param of class #{line.class}"
336
486
  end
337
487
  end
338
488
 
339
- def parse_item (item, min, max)
489
+ def matches? (time)
490
+ @cron_line.matches? time
491
+ end
492
+
493
+ def trigger
494
+ @block.call @eid, @cron_line
495
+ end
496
+ end
497
+
498
+ #
499
+ # A 'cron line' is a line in the sense of a crontab
500
+ # (man 5 crontab) file line.
501
+ #
502
+ class CronLine
340
503
 
341
- return nil \
342
- if item == "*"
343
- return parse_list(item, min, max) \
344
- if item.index(",") > -1
345
- return parse_range(item, min, max) \
346
- if item.index("*") > -1 or item.index("-") > -1
504
+ attr_reader \
505
+ :minutes,
506
+ :hours,
507
+ :days,
508
+ :months,
509
+ :weekdays
347
510
 
348
- i = Integer(item)
511
+ def initialize (line)
349
512
 
350
- i = min if i < min
351
- i = max if i > max
513
+ super()
352
514
 
353
- return [ i ]
354
- end
515
+ items = line.split
355
516
 
356
- def parse_list (item, min, max)
357
- items = item.split(",")
358
- result = []
359
- items.each do |i|
360
- i = Integer(i)
361
- i = min if i < min
362
- i = max if i > max
363
- result << i
517
+ if items.length != 5
518
+ raise \
519
+ "cron '#{line}' string should hold 5 items, " +
520
+ "not #{items.length}" \
364
521
  end
365
- return result
522
+
523
+ @minutes = parse_item(items[0], 0, 59)
524
+ @hours = parse_item(items[1], 0, 24)
525
+ @days = parse_item(items[2], 1, 31)
526
+ @months = parse_item(items[3], 1, 12)
527
+ @weekdays = parse_weekdays(items[4])
528
+
529
+ adjust_arrays()
366
530
  end
367
531
 
368
- def parse_range (item, min, max)
369
- i = item.index("-")
370
- j = item.index("/")
532
+ def matches? (time)
371
533
 
372
- inc = 1
534
+ if time.kind_of?(Float) or time.kind_of?(Integer)
535
+ time = Time.at(time)
536
+ end
373
537
 
374
- inc = Integer(item[j+1..-1]) if j > -1
538
+ return false if no_match?(time.min, @minutes)
539
+ return false if no_match?(time.hour, @hours)
540
+ return false if no_match?(time.day, @days)
541
+ return false if no_match?(time.month, @months)
542
+ return false if no_match?(time.wday, @weekdays)
375
543
 
376
- istart = -1
377
- iend = -1
544
+ return true
545
+ end
378
546
 
379
- if i > -1
547
+ #
548
+ # Returns an array of 5 arrays (minutes, hours, days, months,
549
+ # weekdays).
550
+ # This method is used by the cronline unit tests.
551
+ #
552
+ def to_array
553
+ [ @minutes, @hours, @days, @months, @weekdays ]
554
+ end
380
555
 
381
- istart = Integer(item[0..i])
556
+ private
382
557
 
383
- if j > -1
384
- iend = Integer(item[i+1..j])
385
- else
386
- iend = Integer(i+1..-1)
558
+ #
559
+ # adjust values to Ruby
560
+ #
561
+ def adjust_arrays()
562
+ if @hours
563
+ @hours.each do |h|
564
+ h = 0 if h == 23
565
+ end
566
+ end
567
+ if @weekdays
568
+ @weekdays.each do |wd|
569
+ wd = wd - 1
570
+ end
387
571
  end
388
-
389
- else # case */x
390
- istart = min
391
- iend = max
392
572
  end
393
573
 
394
- istart = min if istart < min
395
- iend = max if iend > max
574
+ WDS = [ "mon", "tue", "wed", "thu", "fri", "sat", "sun" ]
575
+ #
576
+ # used by parse_weekday()
577
+
578
+ def parse_weekdays (item)
396
579
 
397
- result = []
580
+ item = item.downcase
398
581
 
399
- value = istart
400
- while true
401
- result << value
402
- value = value + inc
403
- break if value > iend
582
+ WDS.each_with_index do |day, index|
583
+ item = item.gsub(day, "#{index+1}")
584
+ end
585
+
586
+ return parse_item(item, 1, 7)
404
587
  end
405
588
 
406
- return result
407
- end
589
+ def parse_item (item, min, max)
590
+
591
+ return nil \
592
+ if item == "*"
593
+ return parse_list(item, min, max) \
594
+ if item.index(",")
595
+ return parse_range(item, min, max) \
596
+ if item.index("*") or item.index("-")
408
597
 
409
- def no_match? (value, cron_values)
598
+ i = Integer(item)
410
599
 
411
- return false if not cron_values
600
+ i = min if i < min
601
+ i = max if i > max
412
602
 
413
- cron_values.each do |v|
414
- return false if value == v
603
+ return [ i ]
415
604
  end
416
605
 
417
- return true
418
- end
419
- end
606
+ def parse_list (item, min, max)
607
+ items = item.split(",")
608
+ result = []
609
+ items.each do |i|
610
+ i = Integer(i)
611
+ i = min if i < min
612
+ i = max if i > max
613
+ result << i
614
+ end
615
+ return result
616
+ end
420
617
 
421
- protected
618
+ def parse_range (item, min, max)
619
+ i = item.index("-")
620
+ j = item.index("/")
422
621
 
423
- JOB_ID_LOCK = Monitor.new
622
+ inc = 1
424
623
 
425
- class Entry
624
+ inc = Integer(item[j+1..-1]) if j
426
625
 
427
- @@last_given_id = 0
428
- #
429
- # as a scheduler is fully transient, no need to
430
- # have persistent ids, a simple counter is sufficient
626
+ istart = -1
627
+ iend = -1
431
628
 
432
- attr_accessor \
433
- :eid, :schedulable, :params
434
-
435
- def initialize (schedulable, params)
436
- @schedulable = schedulable
437
- @params = params
438
- JOB_ID_LOCK.synchronize do
439
- @eid = @@last_given_id + 1
440
- @@last_given_id = @eid
441
- end
442
- end
629
+ if i
443
630
 
444
- def trigger
445
- @schedulable.trigger(params)
446
- end
447
- end
631
+ istart = Integer(item[0..i-1])
448
632
 
449
- class JobEntry < Entry
633
+ if j
634
+ iend = Integer(item[i+1..j])
635
+ else
636
+ iend = Integer(item[i+1..-1])
637
+ end
450
638
 
451
- attr_accessor \
452
- :at
639
+ else # case */x
640
+ istart = min
641
+ iend = max
642
+ end
453
643
 
454
- def initialize (at, schedulable, params)
455
- super(schedulable, params)
456
- @at = at
457
- end
458
- end
644
+ istart = min if istart < min
645
+ iend = max if iend > max
459
646
 
460
- class CronEntry < Entry
647
+ result = []
461
648
 
462
- attr_accessor \
463
- :cron_line
649
+ value = istart
650
+ while true
651
+ result << value
652
+ value = value + inc
653
+ break if value > iend
654
+ end
464
655
 
465
- def initialize (line, schedulable, params)
656
+ return result
657
+ end
466
658
 
467
- super(schedulable, params)
659
+ def no_match? (value, cron_values)
468
660
 
469
- if line.kind_of?(String)
470
- @cronline = CronLine.new(line)
471
- elsif line.kind_of?(CronLine)
472
- @cronline = line
473
- else
474
- raise \
475
- "Cannot initialize a CronEntry " +
476
- "with a param of class #{line.class}"
477
- end
661
+ return false if not cron_values
478
662
 
479
- @cron_line = CronLine.new(line)
480
- end
663
+ cron_values.each do |v|
664
+ return false if value == v
665
+ end
481
666
 
482
- def matches? (time)
483
- @cron_line.matches?(time)
484
- end
667
+ return true
668
+ end
485
669
  end
486
670
 
487
671
  end