say_when 0.4.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +13 -4
  3. data/Gemfile +1 -9
  4. data/LICENSE +21 -0
  5. data/README.md +31 -0
  6. data/Rakefile +6 -6
  7. data/lib/generators/say_when/migration/migration_generator.rb +8 -5
  8. data/lib/generators/say_when/migration/templates/migration.rb +8 -7
  9. data/lib/say_when/base_job.rb +2 -0
  10. data/lib/say_when/cron_expression.rb +129 -191
  11. data/lib/say_when/processor/active_messaging.rb +7 -8
  12. data/lib/say_when/processor/base.rb +5 -3
  13. data/lib/say_when/processor/shoryuken.rb +14 -0
  14. data/lib/say_when/processor/simple.rb +2 -0
  15. data/lib/say_when/railtie.rb +9 -0
  16. data/lib/say_when/scheduler.rb +49 -90
  17. data/lib/say_when/storage/active_record/acts.rb +16 -16
  18. data/lib/say_when/storage/active_record/job.rb +17 -24
  19. data/lib/say_when/storage/active_record/job_execution.rb +5 -8
  20. data/lib/say_when/storage/memory/base.rb +3 -1
  21. data/lib/say_when/storage/memory/job.rb +14 -42
  22. data/lib/say_when/tasks.rb +4 -4
  23. data/lib/say_when/triggers/base.rb +4 -3
  24. data/lib/say_when/triggers/cron_strategy.rb +4 -3
  25. data/lib/say_when/triggers/instance_strategy.rb +4 -3
  26. data/lib/say_when/triggers/once_strategy.rb +3 -2
  27. data/lib/say_when/version.rb +3 -1
  28. data/lib/say_when.rb +8 -7
  29. data/lib/tasks/say_when.rake +3 -1
  30. data/say_when.gemspec +26 -19
  31. data/test/active_record_helper.rb +13 -0
  32. data/{spec → test}/db/schema.rb +4 -4
  33. data/{spec/spec_helper.rb → test/minitest_helper.rb} +9 -19
  34. data/test/say_when/cron_expression_spec.rb +74 -0
  35. data/{spec/say_when/processor/active_messaging_spec.rb → test/say_when/processor/active_messaging_test.rb} +17 -13
  36. data/test/say_when/scheduler_test.rb +75 -0
  37. data/test/say_when/storage/active_record/job_test.rb +90 -0
  38. data/test/say_when/storage/memory/job_test.rb +32 -0
  39. data/test/say_when/storage/memory/trigger_test.rb +54 -0
  40. data/test/say_when/triggers/once_strategy_test.rb +23 -0
  41. data/{spec → test}/support/models.rb +5 -3
  42. metadata +97 -54
  43. data/.travis.yml +0 -4
  44. data/generators/say_when_migration/say_when_migration_generator.rb +0 -11
  45. data/generators/say_when_migration/templates/migration.rb +0 -48
  46. data/spec/active_record_spec_helper.rb +0 -9
  47. data/spec/say_when/cron_expression_spec.rb +0 -72
  48. data/spec/say_when/scheduler_spec.rb +0 -126
  49. data/spec/say_when/storage/active_record/job_spec.rb +0 -97
  50. data/spec/say_when/storage/memory/job_spec.rb +0 -45
  51. data/spec/say_when/storage/memory/trigger_spec.rb +0 -54
  52. data/spec/say_when/triggers/once_strategy_spec.rb +0 -22
  53. data/spec/spec.opts +0 -4
@@ -1,22 +1,21 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'activemessaging/message_sender'
2
4
 
3
5
  module SayWhen
4
6
  module Processor
5
-
6
7
  class ActiveMessaging < SayWhen::Processor::Base
7
-
8
+
8
9
  include ::ActiveMessaging::MessageSender
9
-
10
+
10
11
  def initialize(scheduler)
11
12
  super(scheduler)
12
13
  end
13
14
 
14
15
  # send the job to the other end, then in the a13g processor, call the execute method
15
16
  def process(job)
16
- publish(:say_when, {:job_id=>job.id}.to_yaml)
17
+ publish(:say_when, { job_id: job.id }.to_yaml )
17
18
  end
18
-
19
- end
20
-
19
+ end if defined?(::ActiveMessaging)
21
20
  end
22
- end
21
+ end
@@ -1,13 +1,15 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen
2
4
  module Processor
3
-
5
+
4
6
  class Base
5
7
  attr_accessor :scheduler
6
-
8
+
7
9
  def initialize(scheduler)
8
10
  @scheduler = scheduler
9
11
  end
10
-
12
+
11
13
  def process(job)
12
14
  raise NotImplementedError.new('You need to implement process(job)')
13
15
  end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module SayWhen
4
+ module Processor
5
+ class Shoryuken < SayWhen::Processor::Base
6
+ def initialize(scheduler)
7
+ super(scheduler)
8
+ end
9
+
10
+ def process(job)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen
2
4
  module Processor
3
5
 
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module SayWhen
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ require 'say_when/tasks'
7
+ end
8
+ end
9
+ end
@@ -1,50 +1,42 @@
1
- module SayWhen
1
+ # encoding: utf-8
2
2
 
3
+ module SayWhen
3
4
  class Scheduler
4
5
 
5
6
  DEFAULT_PROCESSOR_CLASS = SayWhen::Processor::Simple
6
7
  DEFAULT_STORAGE_STRATEGY = :memory
7
8
 
8
9
  @@scheduler = nil
9
- @@lock = nil
10
+ @@lock = Mutex.new
10
11
 
11
- attr_accessor :storage_strategy, :processor_class, :tick_length, :reset_acquired_length, :reset_next_at
12
+ attr_accessor :storage_strategy, :processor_class, :tick_length
12
13
 
13
14
  attr_accessor :running
14
15
 
15
16
  # support for a singleton scheduler, but you are not restricted to this
16
17
  class << self
17
-
18
18
  def scheduler
19
- self.lock.synchronize {
20
- if @@scheduler.nil?
21
- @@scheduler = self.new
22
- end
23
- }
19
+ return @@scheduler if @@scheduler
20
+ @@lock.synchronize { @@scheduler = self.new if @@scheduler.nil? }
24
21
  @@scheduler
25
22
  end
26
23
 
27
24
  def configure
28
- yield self.scheduler
29
- self.scheduler
30
- end
31
-
32
- def lock
33
- @@lock ||= Mutex.new
25
+ yield scheduler
26
+ scheduler
34
27
  end
35
28
 
36
29
  def schedule(job)
37
- self.scheduler.schedule(job)
30
+ scheduler.schedule(job)
38
31
  end
39
32
 
40
33
  def start
41
- self.scheduler.start
34
+ scheduler.start
42
35
  end
43
36
  end
44
37
 
45
38
  def initialize
46
- self.tick_length = 1
47
- self.reset_acquired_length = 3600
39
+ self.tick_length = [ENV['SAY_WHEN_TICK_LENGTH'].to_i, 5].max
48
40
  end
49
41
 
50
42
  def processor
@@ -63,88 +55,55 @@ module SayWhen
63
55
  trap("TERM", "EXIT")
64
56
 
65
57
  begin
58
+
66
59
  self.running = true
67
60
 
68
61
  logger.info "SayWhen::Scheduler running"
62
+ job = nil
69
63
  while running
70
- process_jobs
64
+ begin
65
+ time_now = Time.now
66
+ logger.debug "SayWhen:: Looking for job that should be ready to fire before #{time_now}"
67
+ job = job_class.acquire_next(time_now)
68
+ if job.nil?
69
+ logger.debug "SayWhen:: no jobs to acquire, sleep"
70
+ sleep(tick_length)
71
+ else
72
+ logger.debug "SayWhen:: got a job: #{job.inspect}"
73
+ # delegate processing the trigger to the processor
74
+ self.processor.process(job)
75
+ logger.debug "SayWhen:: job processed"
76
+
77
+ # this should update next fire at, and put back in list of scheduled jobs
78
+ job.fired(time_now)
79
+ logger.debug "SayWhen:: job fired complete"
80
+ end
81
+ rescue StandardError => ex
82
+ job_msg = job && "job: #{job.inspect} "
83
+ logger.error "SayWhen:: Failure: #{job_msg}exception: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
84
+ safe_release(job)
85
+ sleep(tick_length)
86
+ rescue Interrupt => ex
87
+ job_msg = job && "\n - interrupted job: #{job.inspect}\n"
88
+ logger.error "\nSayWhen:: Interrupt! #{ex.inspect}#{job_msg}"
89
+ safe_release(job)
90
+ exit
91
+ rescue Exception => ex
92
+ job_msg = job && "job: #{job.inspect} "
93
+ logger.error "SayWhen:: Exception: #{job_msg}exception: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
94
+ safe_release(job)
95
+ exit
96
+ end
71
97
  end
72
- rescue Exception => ex
73
- logger.error "SayWhen::Scheduler stopping, error: #{ex.class.name}: #{ex.message}"
74
- exit
75
98
  end
76
99
 
77
100
  logger.info "SayWhen::Scheduler stopped"
78
101
  end
79
102
 
80
- def process_waiting_jobs(max_jobs=1000)
81
- jobs_processed = 0
82
- while(jobs_processed < max_jobs)
83
- if job = process_jobs
84
- jobs_processed += 1
85
- else
86
- break
87
- end
88
- end
89
- return jobs_processed
90
- end
91
-
92
- def process_jobs
93
- job = nil
94
- time_now = Time.now
95
- self.reset_next_at ||= Time.now
96
-
97
- if reset_acquired_length > 0 && reset_next_at <= time_now
98
- self.reset_next_at = time_now + reset_acquired_length
99
- logger.debug "SayWhen:: reset acquired at #{time_now}, try again at #{reset_next_at}"
100
- job_class.reset_acquired(reset_acquired_length)
101
- end
102
-
103
- begin
104
- logger.debug "SayWhen:: Looking for job that should be ready to fire before #{time_now}"
105
- job = job_class.acquire_next(time_now)
106
- rescue StandardError => ex
107
- job_error("Failure to acquire job", job, ex)
108
- job = nil
109
- end
110
-
111
- if job.nil?
112
- logger.debug "SayWhen:: no jobs to acquire, sleep"
113
- sleep(tick_length)
114
- return job
115
- end
116
-
117
- begin
118
- logger.debug "SayWhen:: got a job: #{job.inspect}"
119
- # delegate processing the trigger to the processor
120
- self.processor.process(job)
121
- logger.debug "SayWhen:: job processed"
122
-
123
- # if successful, update next fire at, put back to waiting / ended
124
- job.fired(time_now)
125
- logger.debug "SayWhen:: job fired complete"
126
- rescue StandardError => ex
127
- job_error("Failure to process", job, ex)
128
- end
129
-
130
- return job
131
-
132
- rescue StandardError => ex
133
- job_error("Error!", job, ex)
134
- sleep(tick_length)
135
- return job
136
- rescue Interrupt => ex
137
- job_error("Interrupt!", job, ex)
138
- raise ex
139
- rescue Exception => ex
140
- job_error("Exception!", job, ex)
141
- raise ex
142
- end
143
-
144
- def job_error(msg, job, ex)
145
- job_msg = job && " job:'#{job.inspect}'"
146
- logger.error "SayWhen::Scheduler #{msg}#{job_msg}: #{ex.message}\n\t#{ex.backtrace.join("\t\n")}"
103
+ def safe_release(job)
147
104
  job.release if job
105
+ rescue
106
+ logger "Failed to release job: #{job.inspect}" rescue nil
148
107
  end
149
108
 
150
109
  def stop
@@ -1,57 +1,59 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen #:nodoc:
2
4
  module Storage #:nodoc:
3
5
  module ActiveRecord #:nodoc:
4
6
  module Acts #:nodoc:
5
7
 
6
- def self.included(base) # :nodoc:
7
- base.extend ClassMethods
8
- end
8
+ extend ActiveSupport::Concern
9
9
 
10
10
  module ClassMethods
11
11
  def acts_as_scheduled
12
12
  include SayWhen::Storage::ActiveRecord::Acts::InstanceMethods
13
-
14
- has_many :scheduled_jobs, :as=>:scheduled, :class_name=>'SayWhen::Storage::ActiveRecord::Job', :dependent => :destroy
13
+
14
+ has_many :scheduled_jobs, as: :scheduled, class_name: 'SayWhen::Storage::ActiveRecord::Job', dependent: :destroy
15
15
  end
16
16
  end
17
-
17
+
18
18
  module InstanceMethods
19
19
 
20
20
  def schedule_instance(next_at_method, job={})
21
21
  options = job_options(job)
22
22
  options[:trigger_strategy] = 'instance'
23
- options[:trigger_options] = {:next_at_method => next_at_method}
23
+ options[:trigger_options] = { next_at_method: next_at_method }
24
24
  Scheduler.schedule(options)
25
25
  end
26
26
 
27
27
  def schedule_cron(expression, time_zone, job={})
28
28
  options = job_options(job)
29
29
  options[:trigger_strategy] = 'cron'
30
- options[:trigger_options] = {:expression => expression, :time_zone => time_zone}
30
+ options[:trigger_options] = { expression: expression, time_zone: time_zone }
31
31
  Scheduler.schedule(options)
32
32
  end
33
33
 
34
34
  def schedule_once(time, job={})
35
35
  options = job_options(job)
36
36
  options[:trigger_strategy] = 'once'
37
- options[:trigger_options] = {:at => time}
37
+ options[:trigger_options] = { at: time}
38
38
  Scheduler.schedule(options)
39
39
  end
40
40
 
41
41
  def schedule_in(after, job={})
42
42
  options = job_options(job)
43
43
  options[:trigger_strategy] = 'once'
44
- options[:trigger_options] = {:at => (Time.now + after)}
44
+ options[:trigger_options] = { at: (Time.now + after)}
45
45
  Scheduler.schedule(options)
46
46
  end
47
47
 
48
48
  # helpers
49
49
 
50
50
  def job_options(job)
51
- { :scheduled => self,
52
- :job_class => extract_job_class(job),
53
- :job_method => extract_job_method(job),
54
- :data => extract_data(job) }
51
+ {
52
+ scheduled: self,
53
+ job_class: extract_job_class(job),
54
+ job_method: extract_job_method(job),
55
+ data: extract_data(job)
56
+ }
55
57
  end
56
58
 
57
59
  def extract_job_class(job)
@@ -81,9 +83,7 @@ module SayWhen #:nodoc:
81
83
  nil
82
84
  end
83
85
  end
84
-
85
86
  end # InstanceMethods
86
-
87
87
  end
88
88
  end
89
89
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'active_record'
2
4
  require 'say_when/base_job'
3
5
  require 'say_when/storage/active_record/job_execution'
@@ -8,41 +10,34 @@ module SayWhen
8
10
  module ActiveRecord
9
11
 
10
12
  class Job < ::ActiveRecord::Base
13
+
11
14
  include SayWhen::BaseJob
12
- self.table_name = "say_when_jobs"
15
+
16
+ self.table_name = 'say_when_jobs'
17
+
13
18
  serialize :trigger_options
14
19
  serialize :data
15
- belongs_to :scheduled, :polymorphic => true
16
- has_many :job_executions, :class_name=>'SayWhen::Storage::ActiveRecord::JobExecution'
17
- before_create :set_defaults
18
20
 
19
- def self.reset_acquired(older_than_seconds)
20
- return unless older_than_seconds.to_i > 0
21
- older_than = (Time.now - older_than_seconds.to_i)
22
- update_all(
23
- "status = '#{STATE_WAITING}'",
24
- ["status = ? and updated_at < ?", STATE_ACQUIRED, older_than]
25
- )
26
- end
21
+ belongs_to :scheduled, polymorphic: true
22
+ has_many :job_executions, class_name: 'SayWhen::Storage::ActiveRecord::JobExecution'
23
+
24
+ before_create :set_defaults
27
25
 
28
26
  def self.acquire_next(no_later_than)
29
- @next_job = nil
27
+ next_job = nil
28
+
30
29
  hide_logging do
31
30
  SayWhen::Storage::ActiveRecord::Job.transaction do
32
31
  # select and lock the next job that needs executin' (status waiting, and after no_later_than)
33
- @next_job = find(:first,
34
- :lock => true,
35
- :order => 'next_fire_at ASC',
36
- :conditions => ['status = ? and ? >= next_fire_at',
37
- STATE_WAITING,
38
- no_later_than])
32
+ next_job = where('status = ? and next_fire_at < ?', STATE_WAITING, no_later_than.in_time_zone('UTC')).
33
+ order('next_fire_at ASC').
34
+ lock(true).first
39
35
 
40
36
  # set status to acquired to take it out of rotation
41
- @next_job.update_attribute(:status, STATE_ACQUIRED) unless @next_job.nil?
42
-
37
+ next_job.update_attribute(:status, STATE_ACQUIRED) unless next_job.nil?
43
38
  end
44
39
  end
45
- @next_job
40
+ next_job
46
41
  end
47
42
 
48
43
  def set_defaults
@@ -99,9 +94,7 @@ module SayWhen
99
94
  ::ActiveRecord::Base.logger = old_logger
100
95
  end
101
96
  end
102
-
103
97
  end
104
-
105
98
  end
106
99
  end
107
100
  end
@@ -1,17 +1,14 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'active_record'
2
4
 
3
5
  module SayWhen
4
6
  module Storage
5
7
  module ActiveRecord
6
-
7
- class JobExecution < ::ActiveRecord::Base
8
-
8
+ class JobExecution < ::ActiveRecord::Base
9
9
  self.table_name = "say_when_job_executions"
10
-
11
- belongs_to :job, :class_name=>'SayWhen::Storage::ActiveRecord::Job'
12
-
10
+ belongs_to :job, class_name: 'SayWhen::Storage::ActiveRecord::Job'
13
11
  end
14
-
15
12
  end
16
13
  end
17
- end
14
+ end
@@ -1,9 +1,11 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen
2
4
  module Storage
3
5
  module Memory
4
6
 
5
7
  module Base
6
-
8
+
7
9
  attr_accessor :props
8
10
 
9
11
  def has_properties(*args)
@@ -1,81 +1,53 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'say_when/storage/memory/base'
2
4
 
3
5
  module SayWhen
4
- module Store
6
+ module Storage
5
7
  module Memory
6
8
 
7
9
  # define a trigger class
8
10
  class Job
9
11
 
10
- cattr_accessor :jobs
11
- @@jobs = SortedSet.new
12
-
13
12
  include SayWhen::Storage::Memory::Base
14
- include SayWhen::BaseJob
13
+ include SayWhen::BaseJob
15
14
 
16
15
  has_properties :group, :name, :status, :start_at, :end_at
17
16
  has_properties :trigger_strategy, :trigger_options, :last_fire_at, :next_fire_at
18
17
  has_properties :job_class, :job_method, :data
19
18
  has_properties :scheduled
20
- has_properties :updated_at
21
-
22
- def self.class_lock
23
- @@_lock ||= Mutex.new
24
- end
25
19
 
26
- def self._reset
27
- @@jobs = SortedSet.new
28
- end
29
-
30
- def self.reset_acquired(older_than_seconds)
31
- return unless older_than_seconds.to_i > 0
32
- older_than = (Time.now - older_than_seconds.to_i)
33
- self.class_lock.synchronize {
34
- jobs.select do |j|
35
- j.status == STATE_ACQUIRED && j.updated_at < older_than
36
- end.each{ |j| j.status = STATE_WAITING }
37
- }
38
- end
20
+ cattr_accessor :jobs
21
+ @@jobs = SortedSet.new
22
+ @@acquire_lock = Mutex.new
39
23
 
40
24
  def self.acquire_next(no_later_than)
41
- self.class_lock.synchronize {
25
+ @@acquire_lock.synchronize {
42
26
 
43
27
  next_job = jobs.detect(nil) do |j|
44
28
  (j.status == STATE_WAITING) && (j.next_fire_at.to_i <= no_later_than.to_i)
45
29
  end
46
30
 
47
- if next_job
48
- next_job.status = STATE_ACQUIRED
49
- next_job.updated_at = Time.now
50
- end
51
-
31
+ next_job.status = STATE_ACQUIRED if next_job
52
32
  next_job
53
33
  }
54
34
  end
55
35
 
36
+ def self.create(job)
37
+ job = new(job) if job.is_a?(Hash)
38
+ self.jobs << job
39
+ end
40
+
56
41
  def initialize(options={})
57
42
  super
58
- self.updated_at = Time.now
59
43
  self.status = STATE_WAITING unless self.status
60
44
  self.next_fire_at = trigger.next_fire_at
61
- self.class.jobs << self
62
45
  end
63
46
 
64
47
  def <=>(job)
65
48
  self.next_fire_at.to_i <=> job.next_fire_at.to_i
66
49
  end
67
-
68
- def fired(fired_at=Time.now)
69
- super
70
- self.updated_at = Time.now
71
- end
72
-
73
- def release
74
- super
75
- self.updated_at = Time.now
76
- end
77
50
  end
78
-
79
51
  end
80
52
  end
81
53
  end
@@ -1,9 +1,9 @@
1
- namespace :say_when do
1
+ # encoding: utf-8
2
2
 
3
- desc "Start the SayWhen Scheduler"
4
- task :start => :environment do
3
+ namespace :say_when do
4
+ desc 'Start the SayWhen Scheduler'
5
+ task start: :environment do
5
6
  require 'say_when'
6
7
  SayWhen::Scheduler.start
7
8
  end
8
-
9
9
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen
2
4
  module Triggers
3
5
  module Base
@@ -5,13 +7,12 @@ module SayWhen
5
7
  attr_accessor :job
6
8
 
7
9
  def initialize(options={})
8
- @job = options.delete(:job)
10
+ self.job = options.delete(:job)
9
11
  end
10
12
 
11
13
  def next_fire_at(time=nil)
12
14
  raise NotImplementedError.new('You need to implement next_fire_at in your strategy')
13
15
  end
14
-
15
16
  end
16
17
  end
17
- end
18
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'say_when/triggers/base'
2
4
  require 'say_when/cron_expression'
3
5
 
@@ -11,13 +13,12 @@ module SayWhen
11
13
 
12
14
  def initialize(options={})
13
15
  super
14
- @cron_expression = SayWhen::CronExpression.new(options)
16
+ self.cron_expression = SayWhen::CronExpression.new(options)
15
17
  end
16
18
 
17
19
  def next_fire_at(time=nil)
18
20
  cron_expression.next_fire_at(time || Time.now)
19
21
  end
20
-
21
22
  end
22
23
  end
23
- end
24
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'say_when/triggers/base'
2
4
 
3
5
  module SayWhen
@@ -10,14 +12,13 @@ module SayWhen
10
12
 
11
13
  def initialize(options={})
12
14
  super
13
- @instance = @job.scheduled
14
- @next_at_method = options[:next_at_method] || 'next_fire_at'
15
+ self.instance = job.scheduled
16
+ self.next_at_method = options[:next_at_method] || 'next_fire_at'
15
17
  end
16
18
 
17
19
  def next_fire_at(time=Time.now)
18
20
  instance.send(next_at_method, time)
19
21
  end
20
-
21
22
  end
22
23
  end
23
24
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'say_when/triggers/base'
2
4
 
3
5
  module SayWhen
@@ -10,14 +12,13 @@ module SayWhen
10
12
 
11
13
  def initialize(options=nil)
12
14
  super
13
- @once_at = options[:at] || Time.now
15
+ self.once_at = options[:at] || Time.now
14
16
  end
15
17
 
16
18
  def next_fire_at(time=nil)
17
19
  nfa = once_at if (!time || (time <= once_at))
18
20
  return nfa
19
21
  end
20
-
21
22
  end
22
23
  end
23
24
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module SayWhen
2
- VERSION = "0.4.1"
4
+ VERSION = '1.0.0'
3
5
  end