gaspar 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +91 -0
- data/Rakefile +3 -0
- data/gaspar.gemspec +26 -0
- data/lib/gaspar/version.rb +3 -0
- data/lib/gaspar.rb +200 -0
- data/spec/gaspar/gaspar_spec.rb +190 -0
- data/spec/spec_helper.rb +7 -0
- metadata +155 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Mashable, Inc
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Gaspar
|
2
|
+
<img src="http://cdn.wikimg.net/strategywiki/images/0/04/Chrono_Trigger_Sprites_Gaspar.png" align="right" style="margin: 0 0 20px 20px" />
|
3
|
+
|
4
|
+
Gaspar is a recurring job ("cron") manager for Ruby daemons. It's primarily intended to be used with Rails + Sidekiq/Resque and friends.
|
5
|
+
|
6
|
+
Gaspar runs in-process, meaning there is no additional daemon to configure or maintain. Just define your jobs and they'll get fired by *something*,
|
7
|
+
whether that's your Rails processes, Sidekiq workers, or whatnot. Of course, you can always run it in a separate daemon if you wanted, too.
|
8
|
+
|
9
|
+
By default, Gaspar does not run if you are running under Rails and `Rails.env.test?`. Pass `:permit_test_mode => true` to `Gaspar.schedule` if you want Gaspar to run in test mode.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'gaspar'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install gaspar
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Usage is straightforward. Jobs are defined in a DSL. At a minimum, you need to pass a Redis instance to Gaspar, which is used for synchronizing job locks. The redis instance should be threadsafe; if you haven't turned it off explicitly, thread safety should already be present.
|
28
|
+
|
29
|
+
Gaspar.configure(:logger => Rails.logger) do
|
30
|
+
every "5s", "Ping" do
|
31
|
+
Rails.logger.debug "ping"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Job definitions take one of several formats:
|
36
|
+
|
37
|
+
every [time], [job name] do
|
38
|
+
# Code to run
|
39
|
+
end
|
40
|
+
|
41
|
+
If you have Resque or Sidekiq installed, you can pass a worker class name as string or symbol (plus any args), and it'll be automatically enqueued when the job fires:
|
42
|
+
|
43
|
+
every [time], :DoStuffWorker, 1, 2, 3
|
44
|
+
# If you are using Resque, this will fire `Resque.enqueue(DoStuffWorker, 1, 2, 3)`
|
45
|
+
# If you are using Sidekiq, this will fire `DoStuffWorker.perform_async(1, 2, 3)`
|
46
|
+
|
47
|
+
Jobs also accept cron formats:
|
48
|
+
|
49
|
+
cron "15,30 * * * *" do
|
50
|
+
# Run stuff at 15 and 30 past the hour
|
51
|
+
end
|
52
|
+
|
53
|
+
Jobs are each run in their own individual thread, but you should keep your jobs as lightweight as possible, so best practices will generally mean firing off background workers. Jobs should never exceed 15 seconds runtime, as on process exit, Gaspar will delay for up to 15 seconds to allow currently-running jobs to terminate before they are abandoned. Additionally, jobs should be threadsafe.
|
54
|
+
|
55
|
+
Gaspar.configure(:logger => Rails.logger) do
|
56
|
+
every "10m", :UpdateStuffWorker
|
57
|
+
cron "0 * * * * ", :DailyUpdateWorker, "with", :some_options => true
|
58
|
+
every("1h") { HourlyUpdateWorker.perform_async }
|
59
|
+
end
|
60
|
+
|
61
|
+
Once you have Gaspar configured, you'll need to choose when to start it, and you'll pass a Redis connection for Gaspar to use. This is done separately from the configuration with the expectation that you won't want to run Gaspar for everything that boots your app, and you'll need to take care to close Gaspar (using `Gaspar#retire`) pre-forking, and to start it post-forking (using `Gaspar#start!`)
|
62
|
+
|
63
|
+
Gaspar.start!(Redis.new)
|
64
|
+
|
65
|
+
Since Gaspar uses a Redis connection, you should initialize it after your daemon forks. For example, to use it with Unicorn:
|
66
|
+
|
67
|
+
before_fork do |server, worker|
|
68
|
+
Gaspar.retire
|
69
|
+
end
|
70
|
+
|
71
|
+
after_fork do |server, worker|
|
72
|
+
Gaspar.start! Redis.new
|
73
|
+
end
|
74
|
+
|
75
|
+
Or with Passenger:
|
76
|
+
|
77
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
78
|
+
if forked
|
79
|
+
Gaspar.start! Redis.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Finally, Gaspar will refuse to initialize if the process has a controlling TTY. This prevents it from running for, say, rake tasks and the like.
|
84
|
+
|
85
|
+
## Contributing
|
86
|
+
|
87
|
+
1. Fork it
|
88
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
89
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
90
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
91
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/gaspar.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gaspar/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "gaspar"
|
8
|
+
gem.version = Gaspar::VERSION
|
9
|
+
gem.authors = ["Chris Heald"]
|
10
|
+
gem.email = ["cheald@mashable.com"]
|
11
|
+
gem.description = %q{Gaspar is an in-process recurring job manager. It is intended to be used in place of cron when you don't want a separate daemon.}
|
12
|
+
gem.summary = %q{Gaspar is an in-process recurring job manager.}
|
13
|
+
gem.homepage = "http://github.com/mashable/gaspar"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency('rufus-scheduler')
|
21
|
+
gem.add_dependency('redis', '>= 2.2.0')
|
22
|
+
gem.add_dependency('colorize')
|
23
|
+
gem.add_dependency('activesupport')
|
24
|
+
gem.add_development_dependency('rspec')
|
25
|
+
gem.add_development_dependency('timecop')
|
26
|
+
end
|
data/lib/gaspar.rb
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require "gaspar/version"
|
3
|
+
require "rufus/scheduler"
|
4
|
+
require 'colorize'
|
5
|
+
require 'active_support/core_ext/array/extract_options'
|
6
|
+
require 'active_support/inflector'
|
7
|
+
require 'active_support/concern'
|
8
|
+
require 'active_support/callbacks'
|
9
|
+
|
10
|
+
class Gaspar
|
11
|
+
attr_reader :drift, :scheduler
|
12
|
+
include ActiveSupport::Callbacks
|
13
|
+
define_callbacks :run
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_reader :singleton
|
17
|
+
|
18
|
+
# Public: Configure Gaspar
|
19
|
+
#
|
20
|
+
# options - an options hash
|
21
|
+
def configure(options = {}, &block)
|
22
|
+
@singleton ||= new(options, &block)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Get whether Gaspar has been configured yet or not
|
27
|
+
#
|
28
|
+
# Returns: [Boolean]
|
29
|
+
def configured?
|
30
|
+
!@singleton.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Stop processing jobs and destroy the singleton. Returns Gaspar to an unconfigured state.
|
34
|
+
def destruct!
|
35
|
+
retire
|
36
|
+
@singleton = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Execute the configuration and start processing jobs.
|
40
|
+
#
|
41
|
+
# redis - The redis instance to use for synchronization
|
42
|
+
def start!(redis)
|
43
|
+
raise "Gaspar#configure has not been called, or did not succeed" if @singleton.nil?
|
44
|
+
@singleton.send(:start!, redis) if @singleton
|
45
|
+
end
|
46
|
+
|
47
|
+
def retire
|
48
|
+
return unless @singleton
|
49
|
+
@singleton.send(:shutdown!)
|
50
|
+
end
|
51
|
+
|
52
|
+
def log(logger, message)
|
53
|
+
logger.debug "[%s] %s" % ["Gaspar".yellow, message] if logger
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def before_each(&block)
|
58
|
+
self.class.set_callback :run, :before, &block
|
59
|
+
end
|
60
|
+
|
61
|
+
def after_each(&block)
|
62
|
+
self.class.set_callback :run, :after, &block
|
63
|
+
end
|
64
|
+
|
65
|
+
def around_each(&block)
|
66
|
+
self.class.set_callback :run, :around, &block
|
67
|
+
end
|
68
|
+
|
69
|
+
def every(timing, *args, &block)
|
70
|
+
options = args.extract_options!
|
71
|
+
|
72
|
+
# In order to make sure that jobs are executed at the same time regardless of who runs them
|
73
|
+
# we quantitize the start time to the next-nearest time slice. This more closely emulates
|
74
|
+
# cron-style behavior.
|
75
|
+
if timing.is_a? String
|
76
|
+
seconds = Rufus.parse_duration_string(timing)
|
77
|
+
else
|
78
|
+
seconds = timing.to_i
|
79
|
+
end
|
80
|
+
now = Time.now.to_i - drift
|
81
|
+
start_at = Time.at( now + (seconds - (now % seconds)) )
|
82
|
+
|
83
|
+
options = options.merge(:first_at => start_at)
|
84
|
+
options[:period] = seconds
|
85
|
+
|
86
|
+
schedule :every, timing, args, options, &block
|
87
|
+
end
|
88
|
+
|
89
|
+
def cron(timing, *args, &block)
|
90
|
+
options = args.extract_options!
|
91
|
+
next_fire = Rufus::CronLine.new(timing).next_time
|
92
|
+
|
93
|
+
options[:period] = next_fire.to_i - Time.now.to_i
|
94
|
+
schedule :cron, timing, args, options, &block
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def lock
|
100
|
+
@lock.synchronize { yield }
|
101
|
+
end
|
102
|
+
|
103
|
+
def schedule(method, timing, args = [], options = {}, &block)
|
104
|
+
if block_given?
|
105
|
+
options[:name] ||= args.first
|
106
|
+
else
|
107
|
+
klass, worker_args = *args
|
108
|
+
options[:name] ||= "%s(%s)" % [klass, args.join(", ")]
|
109
|
+
klass = klass.to_s
|
110
|
+
case @options[:worker]
|
111
|
+
when :resque
|
112
|
+
block = Proc.new { Resque.enqueue klass.constantize, *worker_args }
|
113
|
+
when :sidekiq
|
114
|
+
block = Proc.new { klass.constantize.perform_async *worker_args }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
name = options.delete :name
|
119
|
+
if name.nil?
|
120
|
+
if Object.const_defined? :Sourcify
|
121
|
+
name = Digest::SHA1.hexdigest(block.to_source)
|
122
|
+
else
|
123
|
+
raise "No :name specified and sourcify is not available. Specify a name, or add sourcify to your bundle."
|
124
|
+
end
|
125
|
+
end
|
126
|
+
key = "#{@options[:namespace]}:%s-%s" % [timing, name]
|
127
|
+
period = options.delete :period
|
128
|
+
expiry = period - 5
|
129
|
+
expiry = 1 if expiry < 1
|
130
|
+
|
131
|
+
scheduler.send method, timing, options do
|
132
|
+
# If we can acquire a lock...
|
133
|
+
if @redis.setnx key, Process.pid
|
134
|
+
log "#{Process.pid} running #{name}"
|
135
|
+
# ...set the lock to expire, which makes sure that staggered workers with out-of-sync clocks don't
|
136
|
+
lock { @running_jobs += 1 }
|
137
|
+
@redis.expire key, expiry.to_i
|
138
|
+
# ...and then run the job
|
139
|
+
run_callbacks(:run) { block.call }
|
140
|
+
lock { @running_jobs -= 1 }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def log(message)
|
146
|
+
self.class.log @logger, message
|
147
|
+
end
|
148
|
+
|
149
|
+
def initialize(options = {}, &block)
|
150
|
+
@logger = options[:logger]
|
151
|
+
@options = options
|
152
|
+
@block = block
|
153
|
+
@lock = Mutex.new
|
154
|
+
lock { @running_jobs = 0 }
|
155
|
+
|
156
|
+
@options[:namespace] ||= "gaspar"
|
157
|
+
@options[:worker] ||= :sidekiq if Object.const_defined? :Sidekiq
|
158
|
+
@options[:worker] ||= :resque if Object.const_defined? :Resque
|
159
|
+
end
|
160
|
+
|
161
|
+
def start!(redis)
|
162
|
+
return log "Running under a controlling TTY. Refusing to start. Try starting from a daemonized process." if STDOUT.tty? or STDERR.tty?
|
163
|
+
if Object.const_defined? :Rails and Rails.env.test?
|
164
|
+
return unless @options[:permit_test_mode]
|
165
|
+
end
|
166
|
+
|
167
|
+
@redis = redis
|
168
|
+
|
169
|
+
return if @started
|
170
|
+
|
171
|
+
@started = true
|
172
|
+
sync_watches
|
173
|
+
@scheduler = Rufus::Scheduler.start_new
|
174
|
+
@scheduler.every("1h") { sync_watches }
|
175
|
+
instance_eval &@block
|
176
|
+
|
177
|
+
at_exit do
|
178
|
+
@scheduler.stop if @scheduler
|
179
|
+
force_shutdown_at = Time.now.to_i + 15
|
180
|
+
sleep(0.1) while lock { @running_jobs } > 0 and Time.now.to_i < force_shutdown_at
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def shutdown!
|
185
|
+
@scheduler.stop if @scheduler
|
186
|
+
@started = false
|
187
|
+
end
|
188
|
+
|
189
|
+
# Abuse Redis key TTLs to synchronize our watches
|
190
|
+
def sync_watches
|
191
|
+
if @redis.setnx "#{@options[:namespace]}:timesync", Time.now.to_i
|
192
|
+
@redis.expire "#{@options[:namespace]}:timesync", 3.2e8.to_i # Set to expire in ~100 years
|
193
|
+
end
|
194
|
+
epoch = @redis.get("#{@options[:namespace]}:timesync").to_i
|
195
|
+
ttl = @redis.ttl "#{@options[:namespace]}:timesync"
|
196
|
+
offset = (3.2e8 - ttl)
|
197
|
+
@drift = Time.now.to_i - (epoch + offset)
|
198
|
+
log "Resynced - Drift is #{@drift}"
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gaspar do
|
4
|
+
let(:redis) { Redis.new }
|
5
|
+
after(:each) { Gaspar.destruct! }
|
6
|
+
context "when running in a non-daemon" do
|
7
|
+
it "should refuse to start if under a controlling TTY" do
|
8
|
+
STDOUT.stub(:tty?).and_return(true)
|
9
|
+
Gaspar.should_receive(:log).with(nil, "Running under a controlling TTY. Refusing to start. Try starting from a daemonized process.")
|
10
|
+
Gaspar.configure do
|
11
|
+
every "5m", :Foo
|
12
|
+
end.start!(redis)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when running in a daemon" do
|
17
|
+
before(:each) {
|
18
|
+
STDOUT.stub(:tty?).and_return(false)
|
19
|
+
STDERR.stub(:tty?).and_return(false)
|
20
|
+
}
|
21
|
+
after(:each) { Gaspar.reset_callbacks(:run) }
|
22
|
+
|
23
|
+
context "configuration" do
|
24
|
+
it "should accept #every during configuration" do
|
25
|
+
Gaspar.any_instance.should_receive(:schedule).with(:every, "5m", [], instance_of(Hash))
|
26
|
+
Gaspar.configure do
|
27
|
+
every("5m") { puts "Doing stuff" }
|
28
|
+
end.start!(redis)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should require a name if a block is passed to a job" do
|
32
|
+
expect {
|
33
|
+
Gaspar.configure do
|
34
|
+
every("5m") { puts "Doing stuff" }
|
35
|
+
end.start!(redis)
|
36
|
+
}.to raise_error("No :name specified and sourcify is not available. Specify a name, or add sourcify to your bundle.")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should run callbacks" do
|
40
|
+
callbacks = []
|
41
|
+
Gaspar.configure do
|
42
|
+
before_each { callbacks.push "before" }
|
43
|
+
after_each { callbacks.push "after" }
|
44
|
+
around_each {|&block| callbacks.push "around"; block.call }
|
45
|
+
|
46
|
+
every("0.35s", "run callbacks") { callbacks.push "inside" }
|
47
|
+
end.start!(redis)
|
48
|
+
sleep(0.4)
|
49
|
+
|
50
|
+
callbacks.should == %w(before around inside after)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should not require a name if a symbol is passed to a job" do
|
54
|
+
expect {
|
55
|
+
Gaspar.configure do
|
56
|
+
every "5m", :Foobar
|
57
|
+
end.start!(redis)
|
58
|
+
}.to_not raise_error
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should enqueue a job" do
|
62
|
+
Time.stub!(:now).and_return Time.at(1351061802)
|
63
|
+
Gaspar.any_instance.stub(:drift).and_return(0)
|
64
|
+
Gaspar.configure do
|
65
|
+
every "5m", :Foobar
|
66
|
+
end
|
67
|
+
scheduler = double(:scheduler)
|
68
|
+
Gaspar.singleton.stub(:scheduler).and_return scheduler
|
69
|
+
scheduler.should_receive(:every).with("5m", :first_at => Time.at(1351062000))
|
70
|
+
Gaspar.start!(redis)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should accept #cron during configuration" do
|
74
|
+
Gaspar.any_instance.should_receive(:schedule).with(:cron, "* * * * *", [], instance_of(Hash))
|
75
|
+
Gaspar.configure do
|
76
|
+
cron("* * * * *") { puts "Doing stuff" }
|
77
|
+
end.start!(redis)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should quantitize #every to the next timeslice" do
|
81
|
+
Time.stub!(:now).and_return Time.at(1351061802)
|
82
|
+
Gaspar.any_instance.stub(:drift).and_return(0)
|
83
|
+
Gaspar.any_instance.should_receive(:schedule).with(:every, "5m", [], {:first_at => Time.at(1351062000), :period => 300.0})
|
84
|
+
Gaspar.configure do
|
85
|
+
every("5m") { puts "Doing stuff" }
|
86
|
+
end.start!(redis)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should detect Resque" do
|
90
|
+
Resque = 1
|
91
|
+
Gaspar.configure do
|
92
|
+
every "5m", :Foo
|
93
|
+
end.start!(redis)
|
94
|
+
Gaspar.singleton.instance_variable_get("@options")[:worker].should == :resque
|
95
|
+
Object.send :remove_const, :Resque
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should detect Sidekiq" do
|
99
|
+
Sidekiq = 1
|
100
|
+
Gaspar.configure do
|
101
|
+
every "5m", :Foo
|
102
|
+
end.start!(redis)
|
103
|
+
Gaspar.singleton.instance_variable_get("@options")[:worker].should == :sidekiq
|
104
|
+
Object.send :remove_const, :Sidekiq
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should process jobs" do
|
108
|
+
value = 0
|
109
|
+
sleep 0.4
|
110
|
+
Gaspar.configure do
|
111
|
+
every "0.35s", "update variable" do
|
112
|
+
value += 1
|
113
|
+
end
|
114
|
+
end.start!(redis)
|
115
|
+
value.should == 0
|
116
|
+
sleep(0.4)
|
117
|
+
value.should > 0
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should prevent jobs from running multiple times for the same time period" do
|
121
|
+
value = 0
|
122
|
+
sleep 0.4
|
123
|
+
Gaspar.configure do
|
124
|
+
every("0.35s", "update variable with lock") { value += 1 }
|
125
|
+
every("0.35s", "update variable with lock") { value += 1 }
|
126
|
+
every("0.35s", "update variable with lock") { value += 1 }
|
127
|
+
end.start!(redis)
|
128
|
+
value.should == 0
|
129
|
+
sleep(0.4)
|
130
|
+
value.should == 1
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
context "class methods" do
|
136
|
+
describe "#log" do
|
137
|
+
it "should silently eat logging messages when no logger is specified" do
|
138
|
+
expect { Gaspar.log(nil, "foobar") }.to_not raise_error
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#log" do
|
143
|
+
it "should log when a logger is passed" do
|
144
|
+
logger = double(:logger, :debug => true)
|
145
|
+
expect { Gaspar.log(logger, "foobar") }.to_not raise_error
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#destruct!" do
|
150
|
+
it "should not fail when the singleton has not been initialized" do
|
151
|
+
expect { Gaspar.destruct! }.to_not raise_error
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should kill the singleton if it has been previous initialized" do
|
155
|
+
Gaspar.configure do
|
156
|
+
every("5m") { puts "Doing stuff" }
|
157
|
+
end
|
158
|
+
Gaspar.singleton.should_receive(:shutdown!)
|
159
|
+
Gaspar.destruct!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "#start!" do
|
164
|
+
it "should fail if the singleton has not been initialized" do
|
165
|
+
expect { Gaspar.start!(redis) }.to raise_error "Gaspar#configure has not been called, or did not succeed"
|
166
|
+
end
|
167
|
+
|
168
|
+
it "succeeds if the singleton has been initialized" do
|
169
|
+
Gaspar.configure do
|
170
|
+
every("5m", :name => "do stuff") { puts "Doing stuff" }
|
171
|
+
end.start!(redis)
|
172
|
+
Gaspar.singleton.instance_variable_get("@started").should == true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "instance methods" do
|
178
|
+
describe "#sync_watches" do
|
179
|
+
it "should compute drift" do
|
180
|
+
Time.stub!(:now).and_return(150)
|
181
|
+
redis = double :redis, :setnx => false, :get => "100", :ttl => (3.2e8.to_i - 25)
|
182
|
+
instance = Gaspar.send(:new)
|
183
|
+
instance.instance_variable_set(:@redis, redis)
|
184
|
+
instance.send :sync_watches
|
185
|
+
instance.drift.should == 25
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gaspar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Heald
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rufus-scheduler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: redis
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.2.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.2.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: colorize
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activesupport
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: timecop
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: Gaspar is an in-process recurring job manager. It is intended to be used
|
111
|
+
in place of cron when you don't want a separate daemon.
|
112
|
+
email:
|
113
|
+
- cheald@mashable.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- gaspar.gemspec
|
124
|
+
- lib/gaspar.rb
|
125
|
+
- lib/gaspar/version.rb
|
126
|
+
- spec/gaspar/gaspar_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
homepage: http://github.com/mashable/gaspar
|
129
|
+
licenses: []
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 1.8.24
|
149
|
+
signing_key:
|
150
|
+
specification_version: 3
|
151
|
+
summary: Gaspar is an in-process recurring job manager.
|
152
|
+
test_files:
|
153
|
+
- spec/gaspar/gaspar_spec.rb
|
154
|
+
- spec/spec_helper.rb
|
155
|
+
has_rdoc:
|