recurrent 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,3 +5,5 @@ Gemfile.lock
5
5
  .bundle
6
6
  *.gem
7
7
  pkg/*
8
+ /.idea
9
+ .zenflow-log
data/.zenflow ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ confirm_review: true
3
+ backup_remote: false
4
+ release_branch: production
5
+ project: recurrent
6
+ qa_branch: false
7
+ staging_branch: false
8
+ development_branch: master
9
+ remote: origin
10
+ confirm_staging: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ --------------------------------------------------------------------------------
2
+ ^ ADD NEW CHANGES ABOVE ^
3
+ --------------------------------------------------------------------------------
4
+
5
+ CHANGELOG
6
+ =========
7
+
data/README.markdown CHANGED
@@ -121,6 +121,15 @@ How long to wait before killing tasks that are still running.
121
121
 
122
122
  configure.wait_for_running_tasks_on_exit_for = 10.seconds
123
123
 
124
+ ###Limiting the number of concurrent tasks
125
+ To limit the number of tasks that run simultaneously:
126
+
127
+ configure.maximum_concurrent_tasks = 5
128
+
129
+ This will run up to the above number of tasks simultaneously, other tasks will wait until a slot opens up before executing. Tasks that run more frequently, e.g. once a minute, will have precedence over tasks that run once an hour or day etc.
130
+
131
+
132
+
124
133
  Submitting an Issue
125
134
  -------------------
126
135
  We use the [GitHub issue tracker](http://github.com/zencoder/recurrent/issues) to track bugs and
data/lib/recurrent.rb CHANGED
@@ -10,5 +10,6 @@ require 'recurrent/configuration'
10
10
  require 'recurrent/logger'
11
11
  require 'recurrent/scheduler'
12
12
  require 'recurrent/task'
13
+ require 'recurrent/task_collection'
13
14
  require 'recurrent/version'
14
15
  require 'recurrent/worker'
@@ -3,7 +3,7 @@ module Recurrent
3
3
 
4
4
  class << self
5
5
 
6
- attr_accessor :logging, :wait_for_running_tasks_on_exit_for
6
+ attr_accessor :logging, :wait_for_running_tasks_on_exit_for, :maximum_concurrent_tasks
7
7
 
8
8
  def self.block_accessor(*fields)
9
9
  fields.each do |field|
@@ -1,12 +1,14 @@
1
1
  module Recurrent
2
2
  class Scheduler
3
3
 
4
- attr_accessor :tasks, :logger
4
+ attr_accessor :tasks, :logger, :executing_tasks, :mutex
5
5
 
6
6
  def initialize(task_file=nil)
7
- @tasks = []
7
+ @tasks = TaskCollection.new
8
8
  identifier = "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
9
9
  @logger = Logger.new(identifier)
10
+ @mutex = Mutex.new
11
+ @executing_tasks = 0
10
12
  eval(File.read(task_file)) if task_file
11
13
  end
12
14
 
@@ -87,30 +89,16 @@ module Recurrent
87
89
 
88
90
  def every(frequency, key, options={}, &block)
89
91
  logger.info "Adding Task: #{key}"
90
- @tasks << Task.new(:name => key,
91
- :schedule => create_schedule(key, frequency, options[:start_time]),
92
- :action => block,
93
- :save => options[:save],
94
- :logger => logger)
92
+ task = Task.new(:name => key,
93
+ :schedule => create_schedule(key, frequency, options[:start_time]),
94
+ :action => block,
95
+ :save => options[:save],
96
+ :logger => logger,
97
+ :scheduler => self)
98
+ @tasks.add_or_update(task)
95
99
  logger.info "| #{key} added to Scheduler"
96
100
  end
97
101
 
98
- def next_task_time
99
- tasks.map { |task| task.next_occurrence }.sort.first
100
- end
101
-
102
- def running_tasks
103
- tasks.select do |task|
104
- task.running?
105
- end
106
- end
107
-
108
- def tasks_at_time(time)
109
- tasks.select do |task|
110
- task.next_occurrence == time
111
- end
112
- end
113
-
114
102
  def use_saved_schedule_if_rules_match(saved_schedule, new_schedule)
115
103
  if new_schedule.has_same_rules? saved_schedule
116
104
  logger.info "| Schedule matches a saved schedule, using saved schedule."
@@ -121,6 +109,18 @@ module Recurrent
121
109
  end
122
110
  end
123
111
 
112
+ def increment_executing_tasks
113
+ mutex.synchronize do
114
+ @executing_tasks += 1
115
+ end
116
+ end
117
+
118
+ def decrement_executing_tasks
119
+ mutex.synchronize do
120
+ @executing_tasks -= 1
121
+ end
122
+ end
123
+
124
124
  def self.define_frequencies(*frequencies)
125
125
  frequencies.each do |frequency|
126
126
  method_name = frequency == :day ? :daily? : :"#{frequency}ly?"
@@ -1,6 +1,8 @@
1
1
  module Recurrent
2
+ class TooManyExecutingTasks < StandardError; end
3
+
2
4
  class Task
3
- attr_accessor :action, :name, :logger, :save, :schedule, :thread
5
+ attr_accessor :action, :name, :logger, :save, :schedule, :thread, :scheduler
4
6
 
5
7
  def initialize(options={})
6
8
  @name = options[:name]
@@ -8,6 +10,7 @@ module Recurrent
8
10
  @action = options[:action]
9
11
  @save = options[:save]
10
12
  @logger = options[:logger]
13
+ @scheduler = options[:scheduler]
11
14
  Configuration.save_task_schedule.call(name, schedule) if Configuration.save_task_schedule
12
15
  end
13
16
 
@@ -16,13 +19,15 @@ module Recurrent
16
19
  @thread = Thread.new do
17
20
  Thread.current["execution_time"] = execution_time
18
21
  begin
19
- if Configuration.load_task_return_value && action.arity == 1
20
- previous_value = Configuration.load_task_return_value.call(name)
21
- return_value = action.call(previous_value)
22
+ if Configuration.maximum_concurrent_tasks.present?
23
+ limit_execution_to_max_concurrency
22
24
  else
23
- return_value = action.call
25
+ call_action
24
26
  end
25
- save_results(return_value) if save?
27
+ rescue TooManyExecutingTasks
28
+ scheduler.decrement_executing_tasks
29
+ sleep(0.1)
30
+ retry
26
31
  rescue => e
27
32
  logger.warn("#{name} - #{e.message}")
28
33
  logger.warn(e.backtrace)
@@ -30,10 +35,34 @@ module Recurrent
30
35
  end
31
36
  end
32
37
 
38
+ def limit_execution_to_max_concurrency
39
+ if (scheduler.increment_executing_tasks <= Configuration.maximum_concurrent_tasks) && task_is_next_in_line?
40
+ call_action
41
+ scheduler.decrement_executing_tasks
42
+ else
43
+ raise TooManyExecutingTasks
44
+ end
45
+ end
46
+
47
+ def task_is_next_in_line?
48
+ self == scheduler.tasks.next_for_execution_at_time(Thread.current["execution_time"])
49
+ end
50
+
51
+ def call_action
52
+ if Configuration.load_task_return_value && action.arity == 1
53
+ previous_value = Configuration.load_task_return_value.call(name)
54
+
55
+ return_value = action.call(previous_value)
56
+ else
57
+ return_value = action.call
58
+ end
59
+ save_results(return_value) if save?
60
+ end
61
+
33
62
  def handle_still_running(current_time)
34
63
  logger.info "#{name}: Execution from #{thread['execution_time'].to_s(:seconds)} still running, aborting this execution."
35
64
  if Configuration.handle_slow_task
36
- Configuration.handle_slow_task.call(name, current_time)
65
+ Configuration.handle_slow_task.call(name, current_time, thread['execution_time'])
37
66
  end
38
67
  end
39
68
 
@@ -0,0 +1,72 @@
1
+ module Recurrent
2
+ class TaskCollection
3
+
4
+ def initialize
5
+ @mutex = Mutex.new
6
+ @tasks = []
7
+ end
8
+
9
+ def add_or_update(new_task)
10
+ @mutex.synchronize do
11
+ old_task_index = @tasks.index {|task| task.name == new_task.name }
12
+ if old_task_index
13
+ @tasks[old_task_index].schedule = new_task.schedule
14
+ @tasks[old_task_index].action = new_task.action
15
+ else
16
+ @tasks << new_task
17
+ end
18
+ end
19
+ end
20
+
21
+ def next_for_execution_at_time(time)
22
+ @mutex.synchronize do
23
+ tasks_running_at_time = @tasks.select {|task| task.running? && task.thread['execution_time'] == time }
24
+ TaskCollection.sort_by_frequency(tasks_running_at_time).first
25
+ end
26
+ end
27
+
28
+ def next_execution_time
29
+ @mutex.synchronize do
30
+ @tasks.map { |task| task.next_occurrence }.sort.first
31
+ end
32
+ end
33
+
34
+ def remove(name)
35
+ @mutex.synchronize do
36
+ @tasks.reject! {|task| task.name == name }
37
+ end
38
+ end
39
+
40
+ def running
41
+ @mutex.synchronize do
42
+ @tasks.select {|task| task.running? }
43
+ end
44
+ end
45
+
46
+ def scheduled_to_execute_at(time, opts={})
47
+ @mutex.synchronize do
48
+ current_tasks = @tasks.select {|task| task.next_occurrence == time }
49
+ if opts[:sort_by_frequency]
50
+ TaskCollection.sort_by_frequency(current_tasks)
51
+ else
52
+ current_tasks
53
+ end
54
+ end
55
+ end
56
+
57
+ def self.sort_by_frequency(task_list)
58
+ task_list.sort_by do |task|
59
+ task.schedule.rrules.sort_by do |rule|
60
+ rule.frequency_in_seconds
61
+ end.first.frequency_in_seconds
62
+ end
63
+ end
64
+
65
+
66
+ def method_missing(id, *args, &block)
67
+ @mutex.synchronize do
68
+ @tasks.send(id, *args, &block)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,3 +1,3 @@
1
1
  module Recurrent
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -40,8 +40,8 @@ module Recurrent
40
40
 
41
41
  def execute
42
42
  loop do
43
- execution_time = scheduler.next_task_time
44
- tasks_to_execute = scheduler.tasks_at_time(execution_time)
43
+ execution_time = scheduler.tasks.next_execution_time
44
+ tasks_to_execute = scheduler.tasks.scheduled_to_execute_at(execution_time, :sort_by_frequency => !!Configuration.maximum_concurrent_tasks)
45
45
 
46
46
  wait_for_running_tasks && break if $exit
47
47
 
@@ -80,12 +80,12 @@ module Recurrent
80
80
  end
81
81
 
82
82
  def wait_for_running_tasks_for(seconds)
83
- while scheduler.running_tasks.any? do
83
+ while scheduler.tasks.running.any? do
84
84
  logger.info "Killing running tasks in #{seconds.inspect}."
85
85
  seconds -= 1
86
86
  sleep(1)
87
87
  if seconds == 0
88
- scheduler.running_tasks.each do |task|
88
+ scheduler.tasks.running.each do |task|
89
89
  logger.info "Killing #{task.name}."
90
90
  task.thread = nil unless task.thread.try(:kill).try(:alive?)
91
91
  end
@@ -95,7 +95,7 @@ module Recurrent
95
95
  end
96
96
 
97
97
  def wait_for_running_tasks_indefinitely
98
- if task = scheduler.running_tasks.first
98
+ if task = scheduler.tasks.running.first
99
99
  logger.info "Waiting for #{task.name} to finish."
100
100
  task.thread.try(:join)
101
101
  wait_for_running_tasks_indefinitely
@@ -195,36 +195,17 @@ module Recurrent
195
195
  describe "#next_task_time" do
196
196
  context "when there are multiple tasks" do
197
197
  it "should return the soonest time at which a task is scheduled" do
198
- task1 = stub('task1')
198
+ task1 = stub('task1', :name => :task1)
199
199
  task1.stub(:next_occurrence).and_return(10.minutes.from_now)
200
- task2 = stub('task2')
200
+ task2 = stub('task2', :name => :task2)
201
201
  task2.stub(:next_occurrence).and_return(5.minutes.from_now)
202
- task3 = stub('task3')
202
+ task3 = stub('task3', :name => :task3)
203
203
  task3.stub(:next_occurrence).and_return(15.minutes.from_now)
204
204
  schedule = Scheduler.new
205
- schedule.tasks << task1
206
- schedule.tasks << task2
207
- schedule.tasks << task3
208
- schedule.next_task_time.should == task2.next_occurrence
209
- end
210
- end
211
- end
212
-
213
- describe "#tasks_at_time" do
214
- context "when there are multiple tasks" do
215
- it "should return all the tasks whose next_occurrence is at the specified time" do
216
- in_five_minutes = 5.minutes.from_now
217
- task1 = stub('task1')
218
- task1.stub(:next_occurrence).and_return(in_five_minutes)
219
- task2 = stub('task2')
220
- task2.stub(:next_occurrence).and_return(10.minutes.from_now)
221
- task3 = stub('task3')
222
- task3.stub(:next_occurrence).and_return(in_five_minutes)
223
- schedule = Scheduler.new
224
- schedule.tasks << task1
225
- schedule.tasks << task2
226
- schedule.tasks << task3
227
- schedule.tasks_at_time(in_five_minutes).should =~ [task1, task3]
205
+ schedule.tasks.add_or_update(task1)
206
+ schedule.tasks.add_or_update(task2)
207
+ schedule.tasks.add_or_update(task3)
208
+ schedule.tasks.next_execution_time.should == task2.next_occurrence
228
209
  end
229
210
  end
230
211
  end
@@ -0,0 +1,172 @@
1
+ require 'spec_helper'
2
+
3
+ module Recurrent
4
+ describe TaskCollection do
5
+ before(:all) do
6
+ Configuration.logging = "quiet"
7
+ end
8
+
9
+ describe "#add_or_update_task" do
10
+ before(:each) do
11
+ @tasks = TaskCollection.new
12
+ end
13
+
14
+ context "when adding a new task" do
15
+ before(:each) do
16
+ @task = Task.new(:name => :new_task)
17
+ end
18
+
19
+ it "adds the task to the list of tasks" do
20
+ @tasks.size.should == 0
21
+ @tasks.add_or_update(@task)
22
+ @tasks.size.should == 1
23
+ @tasks.first.should == @task
24
+ end
25
+ end
26
+
27
+ context "when updating a task" do
28
+ before(:each) do
29
+ @original_frequency = Scheduler.new.create_schedule(:task, 5.seconds)
30
+ @original_action = proc { "I am the original task!" }
31
+ @original_task = Task.new(:name => :task,
32
+ :frequency => @original_frequency,
33
+ :action => @original_action)
34
+
35
+ @new_frequency = Scheduler.new.create_schedule(:task, 10.seconds)
36
+ @new_action = proc { "I am the new task!" }
37
+ @new_task = Task.new(:name => :task,
38
+ :frequency => @new_frequency,
39
+ :action => @new_action)
40
+ @tasks << @original_task
41
+ end
42
+
43
+ context "before updating the task" do
44
+ it "has one task" do
45
+ @tasks.size.should == 1
46
+ end
47
+
48
+ it "has the original task's action" do
49
+ @tasks.first.action.call.should == "I am the original task!"
50
+ end
51
+
52
+ it "has the original task's frequency" do
53
+ @tasks.first.schedule.should == @original_schedule
54
+ end
55
+ end
56
+
57
+ context "after updating the task" do
58
+ before(:each) do
59
+ @tasks.add_or_update(@new_task)
60
+ end
61
+
62
+ it "has one task" do
63
+ @tasks.size.should == 1
64
+ end
65
+
66
+ it "has the new task's action" do
67
+ @tasks.first.action.call.should == "I am the new task!"
68
+ end
69
+
70
+ it "has the new task's frequency" do
71
+ @tasks.first.schedule.should == @new_schedule
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "#remove_task" do
78
+ context "A TaskCollection with 3 tasks" do
79
+ before(:each) do
80
+ @tasks = TaskCollection.new
81
+ @task1 = Task.new(:name => :task1)
82
+ @task2 = Task.new(:name => :task2)
83
+ @task3 = Task.new(:name => :task3)
84
+ @tasks.add_or_update(@task1)
85
+ @tasks.add_or_update(@task2)
86
+ @tasks.add_or_update(@task3)
87
+ end
88
+
89
+ it "has 3 tasks" do
90
+ @tasks.size.should == 3
91
+ (@tasks | []).should == [@task1, @task2, @task3]
92
+ end
93
+
94
+ context "that removes a task" do
95
+ before(:each) do
96
+ @tasks.remove(:task2)
97
+ end
98
+
99
+ it "has 2 tasks" do
100
+ @tasks.size.should == 2
101
+ (@tasks | []).should == [@task1, @task3]
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "#scheduled_to_execute_at" do
108
+ context "when there are multiple tasks" do
109
+ it "should return all the tasks whose next_occurrence is at the specified time" do
110
+ task_1_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
111
+ task_1_schedule.add_recurrence_rule(IceCube::Rule.minutely(10))
112
+
113
+ task_2_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
114
+ task_2_schedule.add_recurrence_rule(IceCube::Rule.minutely(5))
115
+
116
+ task_3_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
117
+ task_3_schedule.add_recurrence_rule(IceCube::Rule.minutely(1))
118
+
119
+ current_time = Time.utc(2012, 1, 10, 14, 4)
120
+ Timecop.freeze(current_time)
121
+
122
+ task1 = Task.new(:name => 'task1',
123
+ :schedule => task_1_schedule)
124
+ task2 = Task.new(:name => 'task2',
125
+ :schedule => task_2_schedule)
126
+ task3 = Task.new(:name => 'task3',
127
+ :schedule => task_3_schedule)
128
+ tasks = TaskCollection.new
129
+ tasks.add_or_update(task1)
130
+ tasks.add_or_update(task2)
131
+ tasks.add_or_update(task3)
132
+
133
+ tasks.scheduled_to_execute_at(Time.utc(2012, 1, 10, 14, 5)).should =~ [task2, task3]
134
+ Timecop.return
135
+ end
136
+
137
+ context "when :sort_by_frequency => true is passed as an option" do
138
+ it "should return the sorted by frequency, most frequent first" do
139
+ task_1_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
140
+ task_1_schedule.add_recurrence_rule(IceCube::Rule.minutely(10))
141
+
142
+ task_2_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
143
+ task_2_schedule.add_recurrence_rule(IceCube::Rule.minutely(5))
144
+
145
+ task_3_schedule = IceCube::Schedule.new(Time.utc(2012, 1, 10))
146
+ task_3_schedule.add_recurrence_rule(IceCube::Rule.minutely(1))
147
+
148
+ current_time = Time.utc(2012, 1, 10, 14, 4)
149
+ Timecop.freeze(current_time)
150
+
151
+ task1 = Task.new(:name => 'task1',
152
+ :schedule => task_1_schedule)
153
+ task2 = Task.new(:name => 'task2',
154
+ :schedule => task_2_schedule)
155
+ task3 = Task.new(:name => 'task3',
156
+ :schedule => task_3_schedule)
157
+ tasks = TaskCollection.new
158
+ tasks.add_or_update(task1)
159
+ tasks.add_or_update(task2)
160
+ tasks.add_or_update(task3)
161
+
162
+ first_task, second_task = *tasks.scheduled_to_execute_at(Time.utc(2012, 1, 10, 14, 5), :sort_by_frequency => true)
163
+ first_task.should == task3
164
+ second_task.should == task2
165
+ Timecop.return
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
data/spec/task_spec.rb CHANGED
@@ -72,13 +72,6 @@ module Recurrent
72
72
  Configuration.load_task_return_value = nil
73
73
  end
74
74
  end
75
-
76
- context "load_task_return_value is not configured" do
77
-
78
- end
79
-
80
-
81
-
82
75
  end
83
76
 
84
77
  describe "#next_occurrence" do
@@ -129,7 +122,7 @@ module Recurrent
129
122
 
130
123
  it "logs that the task is still running and calls the method" do
131
124
  @task.logger.should_receive(:info).with("handle_still_running_test: Execution from #{@executing_task_time.to_s(:seconds)} still running, aborting this execution.")
132
- Configuration.handle_slow_task.should_receive(:call).with('handle_still_running_test', @current_time)
125
+ Configuration.handle_slow_task.should_receive(:call).with('handle_still_running_test', @current_time, @executing_task_time)
133
126
  @task.handle_still_running(@current_time)
134
127
  end
135
128
 
@@ -219,7 +212,66 @@ module Recurrent
219
212
  t.running?.should be_false
220
213
  end
221
214
  end
215
+ end
222
216
 
217
+ describe "Restricting to a maximum number of concurrent tasks" do
218
+ before(:each) do
219
+ scheduler = Scheduler.new
220
+ schedule = IceCube::Schedule.new(Time.now.utc.beginning_of_day)
221
+ schedule.add_recurrence_rule IceCube::Rule.minutely(1)
222
+ @task1 = Task.new(:name => 'task1',
223
+ :scheduler => scheduler,
224
+ :schedule => schedule.clone,
225
+ :action => lambda { sleep(1)})
226
+ @task2 = Task.new(:name => 'task2',
227
+ :scheduler => scheduler,
228
+ :schedule => schedule.clone,
229
+ :action => lambda { sleep(1) })
230
+
231
+ scheduler.tasks.add_or_update(@task1)
232
+ scheduler.tasks.add_or_update(@task2)
233
+ end
234
+
235
+ describe "when there is no concurrent task limit set" do
236
+ it "should run all tasks at the same time" do
237
+ current_time = Time.now
238
+ [@task1, @task2].each do |task|
239
+ task.execute(current_time)
240
+ end
241
+
242
+ [@task1, @task2].each {|task| task.thread.join }
243
+
244
+ finished_at = Time.now
245
+ elapsed = finished_at - current_time
246
+
247
+ elapsed.round.should == 1.seconds
248
+ end
249
+ end
250
+
251
+ describe "when there is a maximum concurrency limit set" do
252
+ before(:each) do
253
+ Configuration.maximum_concurrent_tasks = 1
254
+ end
255
+
256
+ it "should run only up to the number of tasks specified at once" do
257
+ current_time = Time.now
258
+ [@task1, @task2].each do |task|
259
+ task.execute(current_time)
260
+ end
261
+
262
+ [@task1, @task2].each {|task| task.thread.join }
263
+
264
+ finished_at = Time.now
265
+ elapsed = finished_at - current_time
266
+
267
+ elapsed.round.should == 2.seconds
268
+ end
269
+
270
+ after(:each) do
271
+ Configuration.maximum_concurrent_tasks = nil
272
+ end
273
+ end
223
274
  end
275
+
224
276
  end
225
277
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recurrent
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 3
9
9
  - 0
10
- version: 0.2.0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Adam Kittelson
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-08-13 00:00:00 Z
18
+ date: 2012-01-28 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: ice_cube
@@ -158,6 +158,8 @@ files:
158
158
  - .autotest
159
159
  - .gitignore
160
160
  - .rspec
161
+ - .zenflow
162
+ - CHANGELOG.md
161
163
  - Gemfile
162
164
  - LICENSE
163
165
  - README.markdown
@@ -170,12 +172,14 @@ files:
170
172
  - lib/recurrent/logger.rb
171
173
  - lib/recurrent/scheduler.rb
172
174
  - lib/recurrent/task.rb
175
+ - lib/recurrent/task_collection.rb
173
176
  - lib/recurrent/version.rb
174
177
  - lib/recurrent/worker.rb
175
178
  - recurrent.gemspec
176
179
  - spec/logger_spec.rb
177
180
  - spec/scheduler_spec.rb
178
181
  - spec/spec_helper.rb
182
+ - spec/task_collection_spec.rb
179
183
  - spec/task_spec.rb
180
184
  - spec/worker_spec.rb
181
185
  homepage: http://github.com/zencoder/recurrent
@@ -215,5 +219,6 @@ test_files:
215
219
  - spec/logger_spec.rb
216
220
  - spec/scheduler_spec.rb
217
221
  - spec/spec_helper.rb
222
+ - spec/task_collection_spec.rb
218
223
  - spec/task_spec.rb
219
224
  - spec/worker_spec.rb