updater 0.2.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/LICENSE +24 -0
- data/README +7 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/bin/updater +20 -0
- data/lib/updater/tasks.rb +6 -0
- data/lib/updater/update.rb +257 -0
- data/lib/updater/worker.rb +61 -0
- data/lib/updater.rb +10 -0
- data/spec/lock_spec.rb +73 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/update_spec.rb +185 -0
- data/spec/worker_spec.rb +72 -0
- metadata +108 -0
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2009 John F. Miller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
This file includes code derived from the delayed_job gem which is
|
23
|
+
distributed under the same licence. delayed_job is Copyright (c)
|
24
|
+
2005 Tobias Luetke.
|
data/README
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
updater
|
2
|
+
=======
|
3
|
+
|
4
|
+
This plugin provides database driven delayed exicution of DataMapper model
|
5
|
+
classes. It can call class method on any class availible class or it can find
|
6
|
+
an instance of a class from a database and exicute any method on that instance.
|
7
|
+
Error handleing is also provided.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
VERSION_FILE = File.join(File.dirname(__FILE__), 'VERSION')
|
6
|
+
|
7
|
+
GEM_NAME = "updater"
|
8
|
+
GEM_VERSION = File.read(VERSION_FILE).strip
|
9
|
+
AUTHOR = "John F. Miller"
|
10
|
+
EMAIL = "emperor@antarestrader.com"
|
11
|
+
HOMEPAGE = "http://blog.antarestrader.com"
|
12
|
+
SUMMARY = "Plugin for the delayed calling of methods particularly DataMapper model instance and class methods."
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.name = GEM_NAME
|
16
|
+
s.version = GEM_VERSION
|
17
|
+
s.date = File.ctime(VERSION_FILE)
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.extra_rdoc_files = ["README", "LICENSE", "VERSION"]
|
21
|
+
s.summary = SUMMARY
|
22
|
+
s.description = s.summary
|
23
|
+
s.author = AUTHOR
|
24
|
+
s.email = EMAIL
|
25
|
+
s.homepage = HOMEPAGE
|
26
|
+
s.add_dependency('datamapper', '>= 0.9.11')
|
27
|
+
s.add_development_dependency('rspec', '>= 1.2.6')
|
28
|
+
s.add_development_dependency('timecop', '>= 0.2.1')
|
29
|
+
s.add_development_dependency('chronic', '>= 0.2.3')
|
30
|
+
s.require_path = 'lib'
|
31
|
+
s.bindir = 'bin'
|
32
|
+
s.files = %w(LICENSE README Rakefile VERSION) + Dir.glob("{lib,spec,bin}/**/*")
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
37
|
+
pkg.gem_spec = spec
|
38
|
+
end
|
39
|
+
|
40
|
+
Spec::Rake::SpecTask.new do |t|
|
41
|
+
t.warning = false
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "run all tests"
|
45
|
+
task :default => [:spec]
|
46
|
+
|
47
|
+
desc "Create a gemspec file"
|
48
|
+
task :gemspec do
|
49
|
+
File.open("#{GEM_NAME}.gemspec", "w") do |file|
|
50
|
+
file.puts spec.to_ruby
|
51
|
+
end
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/bin/updater
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
puts "starting update deamon..."
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'yaml'
|
7
|
+
require 'updater'
|
8
|
+
equire 'updater/worker'
|
9
|
+
|
10
|
+
|
11
|
+
dbconfig = YAML.load_file('config/database.yml')
|
12
|
+
|
13
|
+
DataMapper.setup(dbconfig['development'])
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module Updater
|
2
|
+
|
3
|
+
#the basic class that drives updater
|
4
|
+
class Update
|
5
|
+
# Contains the Error class after an error is caught in +run+. Not stored to the database.
|
6
|
+
attr_reader :error
|
7
|
+
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :target, Class
|
12
|
+
property :ident, Yaml
|
13
|
+
property :method, String
|
14
|
+
property :finder, String
|
15
|
+
property :args, Object
|
16
|
+
property :time, Integer
|
17
|
+
property :name, String
|
18
|
+
property :lock_name, String
|
19
|
+
|
20
|
+
#will be called if an error occurs
|
21
|
+
belongs_to :failure, :model=>'Update', :child_key=>[:failure_id], :nullable=>true
|
22
|
+
|
23
|
+
# Returns the Class or instance that will recieve the method call. See +Updater.at+ for
|
24
|
+
# information about how a target is derived.
|
25
|
+
def target
|
26
|
+
return @target if @ident.nil?
|
27
|
+
@target.send(@finder||:get, @ident)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Send the method with args to the target.
|
31
|
+
def run(job=nil)
|
32
|
+
t = target #do not trap errors here
|
33
|
+
final_args = job ? sub_args(job,args.dup) : args
|
34
|
+
begin
|
35
|
+
t.send(@method.to_sym,*final_args)
|
36
|
+
rescue => e
|
37
|
+
@error = e
|
38
|
+
failure.run(self) if failure
|
39
|
+
destroy unless nil == time
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
destroy unless nil == time
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def sub_args(job,a)
|
47
|
+
a.map {|e| :__job__ == e ? job : e}
|
48
|
+
end
|
49
|
+
|
50
|
+
#atempt to lock this record for the worker
|
51
|
+
def lock(worker)
|
52
|
+
return true if locked? && locked_by == worker.name
|
53
|
+
#all this to make sure the check and the lock are simultanious:
|
54
|
+
cnt = repository.update({properties[:lock_name]=>worker.name},self.class.all(:id=>self.id,:lock_name=>nil))
|
55
|
+
if 0 != cnt
|
56
|
+
@lock_name = worker.name
|
57
|
+
true
|
58
|
+
else
|
59
|
+
worker.say( "Worker #{worker.name} Failed to aquire lock on job #{id}" )
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def locked?
|
65
|
+
not @lock_name.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def locked_by
|
69
|
+
@lock_name
|
70
|
+
end
|
71
|
+
|
72
|
+
#Like run but first aquires a lock for the worker. Will return the result of run or nil
|
73
|
+
#if the record could not be locked
|
74
|
+
def run_with_lock(worker)
|
75
|
+
run if lock(worker)
|
76
|
+
end
|
77
|
+
|
78
|
+
class << self
|
79
|
+
|
80
|
+
# Request that the target be sent the method with args at the given time.
|
81
|
+
#
|
82
|
+
# == Parameters
|
83
|
+
# time <Integer | Object responding to to_i>, by default the number of seconds sence the epoch.
|
84
|
+
#What 'time' references can be set by sending the a substitute class to the time= method.
|
85
|
+
#
|
86
|
+
# target <Class | instance> . If target is a class then 'method' will be sent to that class (unless the
|
87
|
+
# finder option is used. Otherwise, the target will be assumed to be the result of
|
88
|
+
# (target.class).get(target.id). The finder method (:get by default) and the finder_args
|
89
|
+
# (target.id by default) can be set in the options. A DataMapper instance passed as the target
|
90
|
+
# will "just work." Any object can be found in this mannor is known as a 'conforming instance'.
|
91
|
+
#
|
92
|
+
# method <Symbol>. The method that will be sent to the calculated target.
|
93
|
+
#
|
94
|
+
# args <Array> a list of arguments to be sent to with the method call. Note: 'args' must be seirialiable
|
95
|
+
# with Marshal.dump. Defaults to []
|
96
|
+
#
|
97
|
+
# options <Hash> Addational options that will be used to configure the request. see Options
|
98
|
+
# section below.
|
99
|
+
#
|
100
|
+
# == Options
|
101
|
+
#
|
102
|
+
# :finder <Symbol> This method will be sent to the stored target class (either target or target.class)
|
103
|
+
# inorder to extract the instance on which to preform the request. By default :get is used. For
|
104
|
+
# example to use on an ActiveRecord class
|
105
|
+
# :finder=>:find
|
106
|
+
#
|
107
|
+
# :finder_args <Array> | <Object>. This is passed to the finder function. By default it is
|
108
|
+
# target.id. Note that by setting :finder_args you will force Updater to calculate in instance
|
109
|
+
# as the computed target even if you pass a Class as the target.
|
110
|
+
#
|
111
|
+
# :name <String> A string sent by the requesting class to identify the request. 'name' must be
|
112
|
+
# unique for a given computed target. Names cannot be used effectivally when a Class has non-
|
113
|
+
# conforming instances as there is no way predict the results of a finder call. 'name' can be used
|
114
|
+
# in conjunction with the +for+ method to manipulate requests effecting an object or class after
|
115
|
+
# they are set. See +for+ for examples
|
116
|
+
#
|
117
|
+
# :failure <Updater> an other request to be run if this request raises an error. Usually the
|
118
|
+
# failure request will be created with the +chane+ method.
|
119
|
+
#
|
120
|
+
# == Examples
|
121
|
+
#
|
122
|
+
# Updater.at(Chronic.parse('tomorrow'),Foo,:bar,[]) # will run Foo.bar() tomorrow at midnight
|
123
|
+
#
|
124
|
+
# f = Foo.create
|
125
|
+
# u = Updater.at(Chronic.parse('2 hours form now'),f,:bar,[]) # will run Foo.get(f.id).bar in 2 hours
|
126
|
+
def at(time,target,method,args=[],options={})
|
127
|
+
finder, finder_args = [:finder,:finder_args].map {|key| options.delete(key)}
|
128
|
+
hash = {:method=>method.to_s,:args=>args}
|
129
|
+
hash[:target] = target_for(target)
|
130
|
+
hash[:ident] = ident_for(target,finder,finder_args)
|
131
|
+
hash[:finder] = finder || :get
|
132
|
+
hash[:time] = time
|
133
|
+
create(hash.merge(options))
|
134
|
+
end
|
135
|
+
|
136
|
+
# like +at+ but with time as time.now. Generally this will be used to run a long running operation in
|
137
|
+
# asyncronously in a differen process. See +at+ for details
|
138
|
+
def immidiate(*args)
|
139
|
+
at(time.now,*args)
|
140
|
+
end
|
141
|
+
|
142
|
+
# like +at+ but without a time to run. This is used to create requests that run in responce to the
|
143
|
+
# failure of other requests. See +at+ for details
|
144
|
+
def chain(*args)
|
145
|
+
at(nil,*args)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Retrieves all updates for a conforming target possibly limiting the results to the named
|
149
|
+
# request.
|
150
|
+
#
|
151
|
+
# == Parameters
|
152
|
+
#
|
153
|
+
# target <Class | Object> a class or conforming object that postentially is the calculated target
|
154
|
+
# of a request.
|
155
|
+
#
|
156
|
+
# name(optional) <String> If a name is sent, the first request with fot this target with this name
|
157
|
+
# will be returned.
|
158
|
+
#
|
159
|
+
# ==Returns
|
160
|
+
#
|
161
|
+
# <Array[Updater]> unless name is given then only a single [Updater] instance.
|
162
|
+
def for(target,name=nil)
|
163
|
+
ident = ident_for(target)
|
164
|
+
target = target_for(target)
|
165
|
+
if name
|
166
|
+
first(:target=>target,:ident=>ident,:name=>name)
|
167
|
+
else
|
168
|
+
all(:target=>target,:ident=>ident)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#The time class used by Updater. See time=
|
173
|
+
def time
|
174
|
+
@@time ||= Time
|
175
|
+
end
|
176
|
+
|
177
|
+
# By default Updater will use the system time (Time class) to get the current time. The application
|
178
|
+
# that Updater was developed for used a game clock that could be paused or restarted. This method
|
179
|
+
# allows us to substitute a custom class for Time. This class must respond with in interger or Time to
|
180
|
+
# the #now method.
|
181
|
+
def time=(klass)
|
182
|
+
@@time = klass
|
183
|
+
end
|
184
|
+
|
185
|
+
#A filter for all requests that are ready to run, that is they requested to be run before or at time.now
|
186
|
+
def current
|
187
|
+
all(:time.lte=>time.now.to_i)
|
188
|
+
end
|
189
|
+
|
190
|
+
#A filter for all requests that are not yet ready to run, that is time is after time.now
|
191
|
+
def delayed
|
192
|
+
all(:time.gt=>time.now.to_i)
|
193
|
+
end
|
194
|
+
|
195
|
+
#This returns a set of update requests.
|
196
|
+
#The first parameter is the maximum number to return (get a few other workers may be in compitition)
|
197
|
+
#The second optional parameter is a list of options to be past to DataMapper.
|
198
|
+
def worker_set(limit = 5, options={})
|
199
|
+
#TODO: add priority to this.
|
200
|
+
options = {:limit=>limit, :order=>[:time.desc]}.merge(options)
|
201
|
+
current.all(options)
|
202
|
+
end
|
203
|
+
|
204
|
+
####################
|
205
|
+
# Worker Functions #
|
206
|
+
####################
|
207
|
+
|
208
|
+
#Gets a single gob form the queue, locks and runs it.
|
209
|
+
def work_off(worker)
|
210
|
+
updates = worker_set
|
211
|
+
|
212
|
+
#concept copied form delayed_job. If there are a number of
|
213
|
+
#different processes working on the queue, the niave approch
|
214
|
+
#would result in every instance trying to lock the same record.
|
215
|
+
#by shuffleing our results we greatly reduce the chances that
|
216
|
+
#multilpe workers try to lock the same process
|
217
|
+
updates = updates.to_a.sort_by{rand()}
|
218
|
+
updates.each do |u|
|
219
|
+
t = u.run_with_lock(worker)
|
220
|
+
return queue_time unless nil == t
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def queue_time
|
225
|
+
nxt = self.first(:time.not=>nil, :order=>[:time.desc])
|
226
|
+
return nil unless nxt
|
227
|
+
return 0 if nxt.time <= time.now.to_i
|
228
|
+
return nxt.time - time.now.to_i
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
# Computes the stored class an instance or class
|
236
|
+
def target_for(inst)
|
237
|
+
return inst if inst.kind_of? Class
|
238
|
+
inst.class
|
239
|
+
end
|
240
|
+
|
241
|
+
# Compute the agrument sent to the finder method
|
242
|
+
def ident_for(target,finder=nil,args=nil)
|
243
|
+
if !(target.kind_of?(Class)) || finder
|
244
|
+
args || target.id
|
245
|
+
end
|
246
|
+
#Otherwize the target is the class and ident should be nil
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
|
251
|
+
#:nodoc:
|
252
|
+
def inspect
|
253
|
+
"#<Updater id=#{id} target=#{target.inspect} time=#{time}>"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# This file based the file of the same name in the delayed_job gem by
|
2
|
+
# Tobias Luetke (Coypright (c) 2005) under the MIT License.
|
3
|
+
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
module Updater
|
7
|
+
|
8
|
+
#This class repeatedly searches the database for active jobs and runs them
|
9
|
+
class Worker
|
10
|
+
cattr_accessor :logger
|
11
|
+
attr_accessor :pid
|
12
|
+
attr_accessor :name
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
@quiet = options[:quiet]
|
16
|
+
@name = options[:name] || "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
|
17
|
+
@pid = Process.pid
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
say "*** Starting job worker #{@name}"
|
22
|
+
t = Thread.new do
|
23
|
+
loop do
|
24
|
+
delay = Update.work_off(self)
|
25
|
+
break if $exit
|
26
|
+
sleep delay
|
27
|
+
break if exit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
trap('TERM') { terminate_with t }
|
32
|
+
trap('INT') { terminate_with t }
|
33
|
+
|
34
|
+
trap('USR1') do
|
35
|
+
say "Wakeup Signal Caught"
|
36
|
+
t.run
|
37
|
+
end
|
38
|
+
|
39
|
+
sleep unless $exit
|
40
|
+
end
|
41
|
+
|
42
|
+
def say(text)
|
43
|
+
puts text unless @quiet
|
44
|
+
logger.info text if logger
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def terminate_with(t)
|
50
|
+
say "Exiting..."
|
51
|
+
$exit = true
|
52
|
+
t.run
|
53
|
+
say "Forcing Shutdown" unless status = t.join(15) #Nasty inline assignment
|
54
|
+
Update.clear_locks(self)
|
55
|
+
exit status ? 0 : 1
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/lib/updater.rb
ADDED
data/spec/lock_spec.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
describe "Update Locking:" do
|
6
|
+
|
7
|
+
class Foo
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :name, String
|
12
|
+
|
13
|
+
def bar(*args)
|
14
|
+
Foo.bar(:instance,*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
Foo.auto_migrate!
|
20
|
+
|
21
|
+
before :each do
|
22
|
+
@u = Update.immidiate(Foo,:bar,[])
|
23
|
+
@w = Worker.new(:name=>"first", :quiet=>true)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "An unlocked record should lock" do
|
27
|
+
@u.lock(@w).should be_true
|
28
|
+
@u.locked?.should be_true
|
29
|
+
@u.locked_by.should == @w.name
|
30
|
+
end
|
31
|
+
|
32
|
+
it "A locked record should NOT lock" do
|
33
|
+
@u.lock(@w).should be_true
|
34
|
+
@u.lock(Worker.new(:quiet=>true)).should be_false
|
35
|
+
end
|
36
|
+
|
37
|
+
it "A record that failed to lock should not change" do
|
38
|
+
@u.lock(@w).should be_true
|
39
|
+
@u.lock(Worker.new(:quiet=>true)).should be_false
|
40
|
+
@u.locked_by.should == @w.name
|
41
|
+
end
|
42
|
+
|
43
|
+
it "A record should report as locked if locked by the same worker twice" do
|
44
|
+
@u.lock(@w).should be_true
|
45
|
+
@u.lock(@w).should be_true
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#run_with_lock" do
|
49
|
+
|
50
|
+
it "should run an unlocked record" do
|
51
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
52
|
+
Foo.should_receive(:bar).with(:arg1,:arg2)
|
53
|
+
u.run_with_lock(@w).should be_true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should NOT run an already locked record" do
|
57
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
58
|
+
u.lock(Worker.new)
|
59
|
+
Foo.should_not_receive(:bar)
|
60
|
+
u.run_with_lock(@w).should be_nil
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should return false if the update ran but there was an error" do
|
64
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
65
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
66
|
+
u.run_with_lock(@w).should be_false
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
it "#clear_locks should lear all locks from a worker"
|
72
|
+
|
73
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
ROOT = File.join(File.dirname(__FILE__), '..')
|
4
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
|
5
|
+
|
6
|
+
require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
|
7
|
+
require "dm-core"
|
8
|
+
|
9
|
+
require 'updater'
|
10
|
+
require 'updater/worker'
|
11
|
+
|
12
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
13
|
+
DataMapper.auto_migrate!
|
14
|
+
|
15
|
+
require 'timecop'
|
16
|
+
require 'chronic'
|
17
|
+
|
data/spec/update_spec.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
describe Update do
|
6
|
+
|
7
|
+
class Foo
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :name, String
|
12
|
+
|
13
|
+
def bar(*args)
|
14
|
+
Foo.bar(:instance,*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
Foo.auto_migrate!
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
Foo.all.destroy!
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should include DataMapper::Resource" do
|
26
|
+
DataMapper::Model.descendants.to_a.should include(Update)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should have a version matching the VERSION file" do
|
30
|
+
Updater::VERSION.should == File.read(File.join(ROOT,'VERSION')).strip
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "adding an immidiate update request" do
|
34
|
+
|
35
|
+
it "with a class target" do
|
36
|
+
u = Update.immidiate(Foo,:bar,[])
|
37
|
+
u.target.should == Foo
|
38
|
+
Update.current.should include(u)
|
39
|
+
Update.delayed.should_not include(u)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "with an conforming instance target" do
|
43
|
+
f = Foo.create
|
44
|
+
u = Update.immidiate(f,:bar,[])
|
45
|
+
u.target.should == f
|
46
|
+
Update.current.should include(u)
|
47
|
+
Update.delayed.should_not include(u)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "with an custome finder" do
|
51
|
+
f = Foo.create(:name=>'baz')
|
52
|
+
u = Update.immidiate(Foo,:bar,[],:finder=>:first, :finder_args=>{:name=>'baz'})
|
53
|
+
u.target.should == f
|
54
|
+
Update.current.should include(u)
|
55
|
+
Update.delayed.should_not include(u)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "chained request" do
|
61
|
+
|
62
|
+
it "should not be in current or delayed queue" do
|
63
|
+
u = Update.chain(Foo,:bar,[:error])
|
64
|
+
u.time.should be_nil
|
65
|
+
Update.current.should_not include(u)
|
66
|
+
Update.delayed.should_not include(u)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "named request" do
|
72
|
+
|
73
|
+
it "should be found by name when target is an instance" do
|
74
|
+
f = Foo.create(:name=>'Honey')
|
75
|
+
u = Update.immidiate(f,:bar,[:named],:name=>'Now')
|
76
|
+
u.name.should ==("Now")
|
77
|
+
Update.for(f, "Now").should ==(u)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should be found by name when target is a class" do
|
81
|
+
u = Update.immidiate(Foo,:bar,[:named],:name=>'Now')
|
82
|
+
u.name.should ==("Now")
|
83
|
+
Update.for(Foo, "Now").should ==(u)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should return all updates for a given target" do
|
87
|
+
u1 = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
88
|
+
u2 = Update.immidiate(Foo,:bar,[:arg3,:arg4])
|
89
|
+
Update.for(Foo).should include(u1,u2)
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "adding an delayed update request" do
|
96
|
+
|
97
|
+
it "with a class target" do
|
98
|
+
u = Update.at(Chronic.parse('tomorrow'),Foo,:bar,[])
|
99
|
+
u.target.should == Foo
|
100
|
+
Update.current.should_not include(u)
|
101
|
+
Update.delayed.should include(u)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "with an conforming instance target" do
|
105
|
+
f = Foo.create
|
106
|
+
u = Update.at(Chronic.parse('tomorrow'),f,:bar,[])
|
107
|
+
u.target.should == f
|
108
|
+
Update.current.should_not include(u)
|
109
|
+
Update.delayed.should include(u)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "with an custome finder" do
|
113
|
+
f = Foo.create(:name=>'baz')
|
114
|
+
u = Update.at(Chronic.parse('tomorrow'),Foo,:bar,[],:finder=>:first, :finder_args=>{:name=>'baz'})
|
115
|
+
u.target.should == f
|
116
|
+
Update.current.should_not include(u)
|
117
|
+
Update.delayed.should include(u)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "running an update" do
|
123
|
+
|
124
|
+
before :each do
|
125
|
+
Update.all.destroy!
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should call the named method with a class target" do
|
129
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
130
|
+
Foo.should_receive(:bar).with(:arg1,:arg2)
|
131
|
+
u.run
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should call the named method with an conforming instance target" do
|
135
|
+
f = Foo.create
|
136
|
+
u = Update.immidiate(f,:bar,[:arg1,:arg2])
|
137
|
+
Foo.should_receive(:bar).with(:instance,:arg1,:arg2)
|
138
|
+
u.run
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should delete the record once it is run" do
|
142
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
143
|
+
Foo.should_receive(:bar).with(:arg1,:arg2)
|
144
|
+
u.run
|
145
|
+
u.should_not be_saved #NOTE: not a theological statment
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should delete the record if there is a failure" do
|
149
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
150
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
151
|
+
u.run
|
152
|
+
u.should_not be_saved #NOTE: not a theological statment
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should not delete the record if it is a chain record" do
|
156
|
+
u = Update.chain(Foo,:bar,[:arg1,:arg2])
|
157
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
158
|
+
u.run
|
159
|
+
u.should be_saved
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "Error Handeling" do
|
163
|
+
it "should return false when run" do
|
164
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
165
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
166
|
+
u.run.should be_false
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should trap errors" do
|
170
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
171
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
172
|
+
lambda {u.run}.should_not raise_error
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should run the failure task" do
|
176
|
+
err = Update.chain(Foo,:bar,[:error])
|
177
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2],:failure=>err)
|
178
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
179
|
+
Foo.should_receive(:bar).with(:error)
|
180
|
+
u.run
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
describe Worker do
|
6
|
+
|
7
|
+
it "should not print anything when quiet" do
|
8
|
+
w = Worker.new :quiet=>true
|
9
|
+
out = StringIO.new
|
10
|
+
$stdout = out
|
11
|
+
w.say "hello world"
|
12
|
+
$stdout = STDOUT
|
13
|
+
out.string.should be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have a name" do
|
17
|
+
Worker.new.name.should be_a String
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "working off jobs:" do
|
23
|
+
|
24
|
+
class Foo
|
25
|
+
include DataMapper::Resource
|
26
|
+
|
27
|
+
property :id, Serial
|
28
|
+
property :name, String
|
29
|
+
|
30
|
+
def bar(*args)
|
31
|
+
Foo.bar(:instance,*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "Update#work_off" do
|
37
|
+
|
38
|
+
before :each do
|
39
|
+
Update.all.destroy!
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should run and immidiate job"do
|
43
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
44
|
+
Foo.should_receive(:bar).with(:arg1,:arg2)
|
45
|
+
Update.work_off(Worker.new)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should aviod conflicts among mutiple workers" do
|
49
|
+
u1 = Update.immidiate(Foo,:bar,[:arg1])
|
50
|
+
u2 = Update.immidiate(Foo,:baz,[:arg2])
|
51
|
+
Foo.should_receive(:bar).with(:arg1)
|
52
|
+
Foo.should_receive(:baz).with(:arg2)
|
53
|
+
Update.work_off(Worker.new(:name=>"first", :quiet=>true))
|
54
|
+
Update.work_off(Worker.new(:name=>"second", :quiet=>true))
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return 0 if there are more jobs waiting" do
|
58
|
+
u1 = Update.immidiate(Foo,:bar,[:arg1])
|
59
|
+
u2 = Update.immidiate(Foo,:baz,[:arg2])
|
60
|
+
Update.work_off(Worker.new(:name=>"first", :quiet=>true)).should == 0
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should return the number of seconds till the next job if there are no jobs to be run"
|
64
|
+
|
65
|
+
it "should return nil if the job queue is empty" do
|
66
|
+
u1 = Update.immidiate(Foo,:bar,[:arg1])
|
67
|
+
Update.work_off(Worker.new(:name=>"first", :quiet=>true)).should be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: updater
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John F. Miller
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-09 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: datamapper
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.9.11
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.6
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: timecop
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.2.1
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: chronic
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.2.3
|
54
|
+
version:
|
55
|
+
description: Plugin for the delayed calling of methods particularly DataMapper model instance and class methods.
|
56
|
+
email: emperor@antarestrader.com
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README
|
63
|
+
- LICENSE
|
64
|
+
- VERSION
|
65
|
+
files:
|
66
|
+
- LICENSE
|
67
|
+
- README
|
68
|
+
- Rakefile
|
69
|
+
- VERSION
|
70
|
+
- lib/updater.rb
|
71
|
+
- lib/updater/update.rb
|
72
|
+
- lib/updater/tasks.rb
|
73
|
+
- lib/updater/worker.rb
|
74
|
+
- spec/worker_spec.rb
|
75
|
+
- spec/lock_spec.rb
|
76
|
+
- spec/update_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- bin/updater
|
79
|
+
has_rdoc: true
|
80
|
+
homepage: http://blog.antarestrader.com
|
81
|
+
licenses: []
|
82
|
+
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: "0"
|
93
|
+
version:
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: "0"
|
99
|
+
version:
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.3.5
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Plugin for the delayed calling of methods particularly DataMapper model instance and class methods.
|
107
|
+
test_files: []
|
108
|
+
|