roqua-support 0.1.26 → 0.1.27

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecfd7d74378220979e7a8839f07ea9793a37265c
4
- data.tar.gz: 8dfb4a419c0d3bc0222a2f743fcad30fc0e43141
3
+ metadata.gz: 4bda6e60469417b289cc6b362240df7943ef3a32
4
+ data.tar.gz: 8b5a13cfbeddc4eb24f7d1cca3fe5c828df019c5
5
5
  SHA512:
6
- metadata.gz: d69f343ba3c51be195a532a892d8839eea4bea7cea5ee54f23e790f6fb56c2299c3aa1f94de49a1cad22f342c0feb78d5878d3a0bffed9be143d719224b7497f
7
- data.tar.gz: 76a3f96b867e176bf948ea7bab491c1abd319b7eb02faf8dbb1f0aed73eb9c7251cef075ee5f51fca81b6383c35f1c05408aded3fda050bd339e77ddab60a668
6
+ metadata.gz: 8dbd51457aa872e2684042494741aadc952814266fde183c09a7bf48f8a6d969df495fe3f708c39aa5d6fb3d4ecbcf6ad716037177b69fe65b3da5b205ec60e2
7
+ data.tar.gz: 7f39013acd584c9faead2d9475c0f8ed8fc74b455b4eb621a0596a2fef41482f0b59b7e5fa41c7bb0b8757ed2f1c44bd24875f6bfcc0b02b9084bdca23851779
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .lock*
6
7
  InstalledFiles
7
8
  _yardoc
8
9
  coverage
data/Gemfile.lock CHANGED
@@ -12,6 +12,7 @@ PATH
12
12
  active_interaction (~> 3.0)
13
13
  activesupport (>= 3.2, < 6)
14
14
  naught (~> 1.0)
15
+ with_advisory_lock (~> 3.2)
15
16
 
16
17
  GEM
17
18
  remote: https://rubygems.org/
@@ -168,6 +169,8 @@ GEM
168
169
  tzinfo (1.2.2)
169
170
  thread_safe (~> 0.1)
170
171
  unicode-display_width (1.3.0)
172
+ with_advisory_lock (3.2.0)
173
+ activerecord (>= 3.2)
171
174
 
172
175
  PLATFORMS
173
176
  ruby
data/README.md CHANGED
@@ -110,6 +110,35 @@ Enable it in a project by calling:
110
110
  Roqua::Probes::DelayedJobProbe.enable
111
111
  ```
112
112
 
113
+ # Scheduling
114
+
115
+ For running code in a recurring fashion (for example hourly or daily) a scheduling mechanism is included in roqua-support.
116
+
117
+ To configure the jobs that need to run you need to create the file `config/schedule.rb` in the Rails project that you want
118
+ to schedule jobs for. An example configuration:
119
+
120
+ ```ruby
121
+ Roqua::Scheduling::Schedule.setup do |cron|
122
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
123
+ # Put code here that you want to run at the first minute of every hour
124
+ end
125
+
126
+ cron.add_task 'daily', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_DAY do
127
+ # Put code here that you want to run every day at 0:00
128
+ end
129
+
130
+ cron.add_task 'every_10_minutes', run_at: proc { 10.minutes.from_now } do
131
+ # Put code here that you want to approximately run every 10 minutes. Because the run_at is calculated after a job
132
+ # is finished the time between jobs will be 10 minutes plus the time the job itself takes.
133
+ end
134
+ end
135
+ ```
136
+
137
+ Normally the RoQua infrastructure should be prepared so the jobs automatically are run if the application is correctly
138
+ deployed. An AppSignal probe can be used as a fallback for job triggering. It can be enabled by placing the following
139
+ code in a Rails initializer:
140
+ `Roqua::Probes::SchedulingProbe.enable`
141
+
113
142
  ### ActiveInteraction extensions
114
143
 
115
144
  ```
data/lib/roqua-support.rb CHANGED
@@ -1,13 +1,23 @@
1
1
  require "roqua-support/version"
2
+ require "roqua-support/railtie" if defined?(Rails)
2
3
  require "roqua/support"
3
4
 
4
5
  module Roqua
5
6
  module Probes
6
7
  autoload :DelayedJobProbe, 'roqua/probes/delayed_job_probe'
8
+ autoload :SchedulingProbe, 'roqua/probes/scheduling_probe'
7
9
  end
8
10
 
9
11
  module Responders
10
12
  autoload :ApiErrorsResponder, 'roqua/responders/api_errors_responder'
11
13
  autoload :ActiveInteractionAwareResponder, 'roqua/responders/active_interaction_aware_responder'
12
14
  end
15
+
16
+ module Scheduling
17
+ autoload :CronJob, 'roqua/scheduling/cron_job'
18
+ autoload :CronJobTable, 'roqua/scheduling/cron_job_table'
19
+ autoload :Scheduler, 'roqua/scheduling/scheduler'
20
+ autoload :Schedule, 'roqua/scheduling/schedule'
21
+ autoload :Task, 'roqua/scheduling/task'
22
+ end
13
23
  end
@@ -0,0 +1,16 @@
1
+ module Roqua
2
+ # Include global rake tasks
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ namespace :roqua do
6
+ desc 'Check if cron needs to run on this instance of the application'
7
+ task :cron_ping do
8
+ ActiveRecord::Base.establish_connection
9
+ require_relative Rails.root.join('config', 'schedule.rb')
10
+ Roqua::Scheduling::Scheduler.new.ping
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -1,5 +1,5 @@
1
1
  module Roqua
2
2
  module Support
3
- VERSION = '0.1.26'.freeze
3
+ VERSION = '0.1.27'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,15 @@
1
+ require_relative 'base_probe'
2
+
3
+ module Roqua
4
+ module Probes
5
+ class SchedulingProbe
6
+ extend BaseProbe
7
+
8
+ def call
9
+ ActiveRecord::Base.establish_connection
10
+ require_relative Rails.root.join('config', 'schedule.rb')
11
+ Roqua::Scheduling::Scheduler.new.ping
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ class Roqua::Scheduling::CronJob < ActiveRecord::Base
2
+ self.table_name = 'roqua_scheduling_cron_jobs'
3
+ end
@@ -0,0 +1,12 @@
1
+ module Roqua::Scheduling::CronJobTable
2
+ def self.create_cron_jobs_table(schema)
3
+ schema.create_table :roqua_scheduling_cron_jobs, force: true do |t|
4
+ t.string :name, null: false
5
+ t.datetime :next_run_at, null: false
6
+ t.datetime :completed_at
7
+ t.timestamps null: false
8
+ end
9
+
10
+ schema.add_index :roqua_scheduling_cron_jobs, :name, unique: true
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ class Roqua::Scheduling::Schedule
2
+ attr_reader :tasks
3
+
4
+ BEGINNING_OF_EVERY_DAY = proc { Time.now.beginning_of_day + 1.day }
5
+ BEGINNING_OF_EVERY_HOUR = proc { Time.now.beginning_of_hour + 1.hour }
6
+
7
+ def initialize
8
+ @tasks = {}
9
+ end
10
+
11
+ def add_task(name, options, &block)
12
+ @tasks[name] = Roqua::Scheduling::Task.new(name, options, block)
13
+ end
14
+
15
+ def self.setup
16
+ @schedule = Roqua::Scheduling::Schedule.new.tap do |new_schedule|
17
+ yield(new_schedule)
18
+ new_schedule.initialize_cronjob_table
19
+ end
20
+ end
21
+
22
+ def self.current_schedule
23
+ @schedule ||= Roqua::Scheduling::Schedule.new
24
+ end
25
+
26
+ def initialize_cronjob_table
27
+ Roqua::Scheduling::CronJob.where('name NOT IN (?)', tasks.values.map(&:name)).delete_all
28
+
29
+ tasks.each_value do |task|
30
+ cron_job = Roqua::Scheduling::CronJob.find_or_initialize_by(name: task.name)
31
+ cron_job.update next_run_at: task.next_run_at unless cron_job.persisted?
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ require 'with_advisory_lock'
2
+
3
+ class Roqua::Scheduling::Scheduler
4
+ def ping
5
+ with_advisory_lock do
6
+ jobs_to_run.each do |cron_job|
7
+ begin
8
+ run_task cron_job
9
+ rescue Exception => ex
10
+ Roqua::Support::Errors.report(ex)
11
+ raise ex if Rails.env.test?
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def jobs_to_run
18
+ Roqua::Scheduling::CronJob.where('next_run_at <= ?', Time.now)
19
+ end
20
+
21
+ def tasks
22
+ schedule.tasks
23
+ end
24
+
25
+ def schedule
26
+ Roqua::Scheduling::Schedule.current_schedule
27
+ end
28
+
29
+ def advisory_lock_name
30
+ "#{ActiveRecord::Base.connection_config[:database]}_cron_lock"
31
+ end
32
+
33
+ private
34
+
35
+ def with_advisory_lock
36
+ ActiveRecord::Base.with_advisory_lock(advisory_lock_name, timeout_seconds: 0) do
37
+ yield
38
+ end
39
+ end
40
+
41
+ def run_task(cron_job)
42
+ task = schedule.tasks[cron_job.name]
43
+ task.run
44
+
45
+ cron_job.update completed_at: Time.now, next_run_at: task.next_run_at
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module Roqua::Scheduling
2
+ class Task
3
+ attr_reader :name, :options
4
+
5
+ def initialize(name, options, callback)
6
+ @name = name
7
+ @options = options
8
+ @callback = callback
9
+ end
10
+
11
+ def run_at
12
+ options[:run_at]
13
+ end
14
+
15
+ def next_run_at
16
+ run_at.call
17
+ end
18
+
19
+ def run
20
+ @callback.call
21
+ end
22
+ end
23
+ end
@@ -18,15 +18,16 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
+ gem.add_dependency 'active_interaction', '~> 3.0'
21
22
  gem.add_dependency 'activesupport', '>= 3.2', '< 6'
22
23
  gem.add_dependency 'naught', '~> 1.0'
23
- gem.add_dependency 'active_interaction', '~> 3.0'
24
+ gem.add_dependency 'with_advisory_lock', '~> 3.2'
24
25
 
26
+ gem.add_development_dependency 'appsignal', '>= 2.3.1'
25
27
  gem.add_development_dependency 'bundler', '~> 1.0'
28
+ gem.add_development_dependency 'delayed_job_active_record'
26
29
  gem.add_development_dependency 'rake'
27
30
  gem.add_development_dependency 'rspec', '>= 2.12.0', '< 4.0'
28
- gem.add_development_dependency 'appsignal', '>= 2.3.1'
29
- gem.add_development_dependency 'delayed_job_active_record'
30
31
  gem.add_development_dependency 'sqlite3'
31
32
  gem.add_development_dependency 'timecop'
32
33
  end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+
4
+ describe Roqua::Scheduling::Scheduler do
5
+ before do
6
+ Timecop.freeze
7
+ Roqua::Scheduling::CronJob.delete_all
8
+ end
9
+
10
+ after { Timecop.return }
11
+
12
+ subject { described_class.new }
13
+
14
+ def setup_daily_and_hourly_schedule
15
+ Roqua::Scheduling::Schedule.setup do |cron|
16
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
17
+ end
18
+
19
+ cron.add_task 'daily', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_DAY do
20
+ end
21
+ end
22
+ end
23
+
24
+ def setup_hourly_schedule
25
+ Roqua::Scheduling::Schedule.setup do |cron|
26
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
27
+ end
28
+ end
29
+ end
30
+
31
+ it 'creates the correct table rows' do
32
+ expect { setup_daily_and_hourly_schedule }
33
+ .to change { Roqua::Scheduling::CronJob.all.pluck(:name, :next_run_at, :completed_at) }
34
+ .from(
35
+ []
36
+ )
37
+ .to(
38
+ [
39
+ ['hourly', Time.now.beginning_of_hour + 1.hour, nil],
40
+ ['daily', Time.now.beginning_of_day + 1.day, nil]
41
+ ]
42
+ )
43
+ end
44
+
45
+ it 'removes unused table rows' do
46
+ setup_daily_and_hourly_schedule
47
+
48
+ expect { setup_hourly_schedule }
49
+ .to change { Roqua::Scheduling::CronJob.all.pluck(:name).sort }
50
+ .from(['daily', 'hourly']).to(['hourly'])
51
+ end
52
+
53
+ it 'generates a database specific advisory lock name' do
54
+ expect(ActiveRecord::Base.connection_config[:database]).to eql(':memory:')
55
+ expect(subject.advisory_lock_name).to eql ':memory:_cron_lock'
56
+ end
57
+
58
+ describe 'with a run_at in the future' do
59
+ before do
60
+ Roqua::Scheduling::Schedule.setup do |cron|
61
+ cron.add_task 'hourly', run_at: proc { 1.minute.from_now } do
62
+ end
63
+ end
64
+ end
65
+
66
+ it 'the task will not be run' do
67
+ expect(subject.schedule.tasks['hourly']).to_not receive(:run)
68
+ subject.ping
69
+ end
70
+ end
71
+
72
+ describe 'callbacks' do
73
+ let(:callback_spy) { spy(:callback) }
74
+
75
+ it 'get executed' do
76
+ Roqua::Scheduling::Schedule.setup do |cron|
77
+ cron.add_task 'hourly', run_at: proc { 1.minute.ago } do
78
+ callback_spy.execute
79
+ end
80
+ end
81
+
82
+ expect(callback_spy).to receive(:execute)
83
+ subject.ping
84
+ end
85
+ end
86
+
87
+ describe 'when running multiple times' do
88
+ before do
89
+ Timecop.freeze(60.seconds.ago)
90
+
91
+ Roqua::Scheduling::Schedule.setup do |cron|
92
+ cron.add_task 'task', run_at: proc { 30.seconds.from_now } do
93
+ end
94
+ end
95
+ end
96
+
97
+ let(:schedule) { Roqua::Scheduling::Schedule.current_schedule }
98
+
99
+ it 'the task is only called once' do
100
+ expect(schedule.tasks['task']).to receive(:run).once
101
+
102
+ Timecop.return
103
+ 3.times { Roqua::Scheduling::Scheduler.new.ping }
104
+ end
105
+ end
106
+ end
data/spec/spec_helper.rb CHANGED
@@ -19,9 +19,9 @@ ActiveRecord::Base.logger = Delayed::Worker.logger
19
19
  ActiveRecord::Migration.verbose = false
20
20
 
21
21
  ActiveRecord::Schema.define do
22
- create_table :delayed_jobs, :force => true do |t|
23
- t.integer :priority, :default => 0
24
- t.integer :attempts, :default => 0
22
+ create_table :delayed_jobs, force: true do |t|
23
+ t.integer :priority, default: 0
24
+ t.integer :attempts, default: 0
25
25
  t.text :handler
26
26
  t.text :last_error
27
27
  t.datetime :run_at
@@ -33,5 +33,7 @@ ActiveRecord::Schema.define do
33
33
  t.timestamps null: false
34
34
  end
35
35
 
36
- add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
36
+ add_index :delayed_jobs, [:priority, :run_at], name: 'delayed_jobs_priority'
37
+
38
+ Roqua::Scheduling::CronJobTable.create_cron_jobs_table(self)
37
39
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roqua-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.26
4
+ version: 0.1.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marten Veldthuis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2018-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_interaction
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: activesupport
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -45,95 +59,95 @@ dependencies:
45
59
  - !ruby/object:Gem::Version
46
60
  version: '1.0'
47
61
  - !ruby/object:Gem::Dependency
48
- name: active_interaction
62
+ name: with_advisory_lock
49
63
  requirement: !ruby/object:Gem::Requirement
50
64
  requirements:
51
65
  - - "~>"
52
66
  - !ruby/object:Gem::Version
53
- version: '3.0'
67
+ version: '3.2'
54
68
  type: :runtime
55
69
  prerelease: false
56
70
  version_requirements: !ruby/object:Gem::Requirement
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '3.0'
74
+ version: '3.2'
61
75
  - !ruby/object:Gem::Dependency
62
- name: bundler
76
+ name: appsignal
63
77
  requirement: !ruby/object:Gem::Requirement
64
78
  requirements:
65
- - - "~>"
79
+ - - ">="
66
80
  - !ruby/object:Gem::Version
67
- version: '1.0'
81
+ version: 2.3.1
68
82
  type: :development
69
83
  prerelease: false
70
84
  version_requirements: !ruby/object:Gem::Requirement
71
85
  requirements:
72
- - - "~>"
86
+ - - ">="
73
87
  - !ruby/object:Gem::Version
74
- version: '1.0'
88
+ version: 2.3.1
75
89
  - !ruby/object:Gem::Dependency
76
- name: rake
90
+ name: bundler
77
91
  requirement: !ruby/object:Gem::Requirement
78
92
  requirements:
79
- - - ">="
93
+ - - "~>"
80
94
  - !ruby/object:Gem::Version
81
- version: '0'
95
+ version: '1.0'
82
96
  type: :development
83
97
  prerelease: false
84
98
  version_requirements: !ruby/object:Gem::Requirement
85
99
  requirements:
86
- - - ">="
100
+ - - "~>"
87
101
  - !ruby/object:Gem::Version
88
- version: '0'
102
+ version: '1.0'
89
103
  - !ruby/object:Gem::Dependency
90
- name: rspec
104
+ name: delayed_job_active_record
91
105
  requirement: !ruby/object:Gem::Requirement
92
106
  requirements:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
- version: 2.12.0
96
- - - "<"
97
- - !ruby/object:Gem::Version
98
- version: '4.0'
109
+ version: '0'
99
110
  type: :development
100
111
  prerelease: false
101
112
  version_requirements: !ruby/object:Gem::Requirement
102
113
  requirements:
103
114
  - - ">="
104
115
  - !ruby/object:Gem::Version
105
- version: 2.12.0
106
- - - "<"
107
- - !ruby/object:Gem::Version
108
- version: '4.0'
116
+ version: '0'
109
117
  - !ruby/object:Gem::Dependency
110
- name: appsignal
118
+ name: rake
111
119
  requirement: !ruby/object:Gem::Requirement
112
120
  requirements:
113
121
  - - ">="
114
122
  - !ruby/object:Gem::Version
115
- version: 2.3.1
123
+ version: '0'
116
124
  type: :development
117
125
  prerelease: false
118
126
  version_requirements: !ruby/object:Gem::Requirement
119
127
  requirements:
120
128
  - - ">="
121
129
  - !ruby/object:Gem::Version
122
- version: 2.3.1
130
+ version: '0'
123
131
  - !ruby/object:Gem::Dependency
124
- name: delayed_job_active_record
132
+ name: rspec
125
133
  requirement: !ruby/object:Gem::Requirement
126
134
  requirements:
127
135
  - - ">="
128
136
  - !ruby/object:Gem::Version
129
- version: '0'
137
+ version: 2.12.0
138
+ - - "<"
139
+ - !ruby/object:Gem::Version
140
+ version: '4.0'
130
141
  type: :development
131
142
  prerelease: false
132
143
  version_requirements: !ruby/object:Gem::Requirement
133
144
  requirements:
134
145
  - - ">="
135
146
  - !ruby/object:Gem::Version
136
- version: '0'
147
+ version: 2.12.0
148
+ - - "<"
149
+ - !ruby/object:Gem::Version
150
+ version: '4.0'
137
151
  - !ruby/object:Gem::Dependency
138
152
  name: sqlite3
139
153
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +200,7 @@ files:
186
200
  - gemfiles/rails42.gemfile
187
201
  - gemfiles/rails50.gemfile
188
202
  - lib/roqua-support.rb
203
+ - lib/roqua-support/railtie.rb
189
204
  - lib/roqua-support/version.rb
190
205
  - lib/roqua/core_ext/active_interaction/filters/date_time_as_unix_extension.rb
191
206
  - lib/roqua/core_ext/active_interaction/filters/duration_filter.rb
@@ -197,8 +212,14 @@ files:
197
212
  - lib/roqua/core_ext/fixnum/clamp.rb
198
213
  - lib/roqua/probes/base_probe.rb
199
214
  - lib/roqua/probes/delayed_job_probe.rb
215
+ - lib/roqua/probes/scheduling_probe.rb
200
216
  - lib/roqua/responders/active_interaction_aware_responder.rb
201
217
  - lib/roqua/responders/api_errors_responder.rb
218
+ - lib/roqua/scheduling/cron_job.rb
219
+ - lib/roqua/scheduling/cron_job_table.rb
220
+ - lib/roqua/scheduling/schedule.rb
221
+ - lib/roqua/scheduling/scheduler.rb
222
+ - lib/roqua/scheduling/task.rb
202
223
  - lib/roqua/support.rb
203
224
  - lib/roqua/support/command_runner.rb
204
225
  - lib/roqua/support/errors.rb
@@ -220,6 +241,7 @@ files:
220
241
  - spec/roqua/probes/delayed_job_probe_spec.rb
221
242
  - spec/roqua/responders/active_interaction_aware_responder_spec.rb
222
243
  - spec/roqua/responders/api_errors_responder_spec.rb
244
+ - spec/roqua/scheduling/scheduler_spec.rb
223
245
  - spec/roqua/support/errors_spec.rb
224
246
  - spec/roqua/support/helpers_spec.rb
225
247
  - spec/roqua/support/logwrapper_spec.rb
@@ -265,6 +287,7 @@ test_files:
265
287
  - spec/roqua/probes/delayed_job_probe_spec.rb
266
288
  - spec/roqua/responders/active_interaction_aware_responder_spec.rb
267
289
  - spec/roqua/responders/api_errors_responder_spec.rb
290
+ - spec/roqua/scheduling/scheduler_spec.rb
268
291
  - spec/roqua/support/errors_spec.rb
269
292
  - spec/roqua/support/helpers_spec.rb
270
293
  - spec/roqua/support/logwrapper_spec.rb