openwferu 0.9.2 → 0.9.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.
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