recurrent 0.2.0 → 0.3.0

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