updater 0.2.2 → 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/Rakefile +14 -0
- data/VERSION +1 -1
- data/lib/updater/fork_worker.rb +427 -0
- data/lib/updater/orm/datamapper.rb +172 -0
- data/lib/updater/{worker.rb → thread_worker.rb} +4 -8
- data/lib/updater/update.rb +170 -149
- data/lib/updater/update_dm.rb +298 -0
- data/lib/updater/util.rb +22 -0
- data/lib/updater.rb +0 -3
- data/spec/chained_spec.rb +81 -0
- data/spec/errors_spec.rb +31 -0
- data/spec/fooclass.rb +14 -0
- data/spec/fork_worker_instance_spec.rb +56 -0
- data/spec/fork_worker_spec.rb +290 -0
- data/spec/lock_spec.rb +18 -35
- data/spec/named_request_spec.rb +36 -0
- data/spec/params_sub_spec.rb +27 -0
- data/spec/schedule_spec.rb +89 -0
- data/spec/spec_helper.rb +6 -1
- data/spec/{worker_spec.rb → thread_worker_spec.rb} +11 -11
- data/spec/update_runner_spec.rb +48 -0
- data/spec/update_spec.rb +11 -173
- data/spec/util_spec.rb +11 -0
- metadata +18 -4
@@ -0,0 +1,298 @@
|
|
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, :lazy=>false
|
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
|
+
ret = create(hash.merge(options))
|
134
|
+
Process.kill('USR2',pid) if pid
|
135
|
+
ret
|
136
|
+
rescue Errno::ESRCH
|
137
|
+
@pid = nil
|
138
|
+
puts "PID invalid"
|
139
|
+
#log this as well
|
140
|
+
end
|
141
|
+
|
142
|
+
# like +at+ but with time as time.now. Generally this will be used to run a long running operation in
|
143
|
+
# asyncronously in a differen process. See +at+ for details
|
144
|
+
def immidiate(*args)
|
145
|
+
at(time.now,*args)
|
146
|
+
end
|
147
|
+
|
148
|
+
# like +at+ but without a time to run. This is used to create requests that run in responce to the
|
149
|
+
# failure of other requests. See +at+ for details
|
150
|
+
def chain(*args)
|
151
|
+
at(nil,*args)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Retrieves all updates for a conforming target possibly limiting the results to the named
|
155
|
+
# request.
|
156
|
+
#
|
157
|
+
# == Parameters
|
158
|
+
#
|
159
|
+
# target <Class | Object> a class or conforming object that postentially is the calculated target
|
160
|
+
# of a request.
|
161
|
+
#
|
162
|
+
# name(optional) <String> If a name is sent, the first request with fot this target with this name
|
163
|
+
# will be returned.
|
164
|
+
#
|
165
|
+
# ==Returns
|
166
|
+
#
|
167
|
+
# <Array[Updater]> unless name is given then only a single [Updater] instance.
|
168
|
+
def for(target,name=nil)
|
169
|
+
ident = ident_for(target)
|
170
|
+
target = target_for(target)
|
171
|
+
if name
|
172
|
+
first(:target=>target,:ident=>ident,:name=>name)
|
173
|
+
else
|
174
|
+
all(:target=>target,:ident=>ident)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#The time class used by Updater. See time=
|
179
|
+
def time
|
180
|
+
@@time ||= Time
|
181
|
+
end
|
182
|
+
|
183
|
+
# By default Updater will use the system time (Time class) to get the current time. The application
|
184
|
+
# that Updater was developed for used a game clock that could be paused or restarted. This method
|
185
|
+
# allows us to substitute a custom class for Time. This class must respond with in interger or Time to
|
186
|
+
# the #now method.
|
187
|
+
def time=(klass)
|
188
|
+
@@time = klass
|
189
|
+
end
|
190
|
+
|
191
|
+
#A filter for all requests that are ready to run, that is they requested to be run before or at time.now
|
192
|
+
def current
|
193
|
+
all(:time.lte=>time.now.to_i, :lock_name=>nil)
|
194
|
+
end
|
195
|
+
|
196
|
+
#A filter for all requests that are not yet ready to run, that is time is after time.now
|
197
|
+
def delayed
|
198
|
+
all(:time.gt=>time.now.to_i)
|
199
|
+
end
|
200
|
+
|
201
|
+
#how many jobs will happen in the next n seconds
|
202
|
+
def future(n)
|
203
|
+
ct = time.now.to_i
|
204
|
+
all(:time.gt=>ct,:time.lt=>ct+n)
|
205
|
+
end
|
206
|
+
|
207
|
+
#Sets the process id of the worker process if known. If this
|
208
|
+
#is set then an attempt will be made to signal the worker any
|
209
|
+
#time a new update is made.
|
210
|
+
#
|
211
|
+
#If pid is not set, or is set to nil then the scheduleing program
|
212
|
+
#is responcible for waking-up a potentially sleeping worker process
|
213
|
+
#in another way.
|
214
|
+
def pid=(p)
|
215
|
+
return @pid = nil unless p #tricky assignment in return
|
216
|
+
@pid = Integer("#{p}")
|
217
|
+
Process::kill 0, @pid
|
218
|
+
@pid
|
219
|
+
rescue Errno::ESRCH, ArgumentError
|
220
|
+
raise ArgumentError "PID was invalid"
|
221
|
+
end
|
222
|
+
|
223
|
+
def pid
|
224
|
+
@pid
|
225
|
+
end
|
226
|
+
|
227
|
+
####################
|
228
|
+
# Worker Functions #
|
229
|
+
####################
|
230
|
+
|
231
|
+
#This returns a set of update requests.
|
232
|
+
#The first parameter is the maximum number to return (get a few other workers may be in compitition)
|
233
|
+
#The second optional parameter is a list of options to be past to DataMapper.
|
234
|
+
def worker_set(limit = 5, options={})
|
235
|
+
#TODO: add priority to this.
|
236
|
+
options = {:lock_name=>nil,:limit=>limit, :order=>[:time.asc]}.merge(options)
|
237
|
+
current.all(options)
|
238
|
+
end
|
239
|
+
|
240
|
+
#Gets a single job form the queue, locks and runs it.
|
241
|
+
def work_off(worker)
|
242
|
+
updates = worker_set
|
243
|
+
unless updates.empty?
|
244
|
+
#concept copied form delayed_job. If there are a number of
|
245
|
+
#different processes working on the queue, the niave approch
|
246
|
+
#would result in every instance trying to lock the same record.
|
247
|
+
#by shuffleing our results we greatly reduce the chances that
|
248
|
+
#multilpe workers try to lock the same process
|
249
|
+
updates = updates.to_a.sort_by{rand()}
|
250
|
+
updates.each do |u|
|
251
|
+
t = u.run_with_lock(worker)
|
252
|
+
break unless nil == t
|
253
|
+
end
|
254
|
+
end
|
255
|
+
rescue DataObjects::ConnectionError
|
256
|
+
sleep 0.1
|
257
|
+
retry
|
258
|
+
ensure
|
259
|
+
clear_locks(worker)
|
260
|
+
return queue_time
|
261
|
+
end
|
262
|
+
|
263
|
+
def queue_time
|
264
|
+
nxt = self.first(:time.not=>nil,:lock_name=>nil, :order=>[:time.asc])
|
265
|
+
return nil unless nxt
|
266
|
+
return 0 if nxt.time <= time.now.to_i
|
267
|
+
return nxt.time - time.now.to_i
|
268
|
+
end
|
269
|
+
|
270
|
+
def clear_locks(worker)
|
271
|
+
all(:lock_name=>worker.name).update(:lock_name=>nil)
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
# Computes the stored class an instance or class
|
277
|
+
def target_for(inst)
|
278
|
+
return inst if inst.kind_of? Class
|
279
|
+
inst.class
|
280
|
+
end
|
281
|
+
|
282
|
+
# Compute the agrument sent to the finder method
|
283
|
+
def ident_for(target,finder=nil,args=nil)
|
284
|
+
if !(target.kind_of?(Class)) || finder
|
285
|
+
args || target.id
|
286
|
+
end
|
287
|
+
#Otherwize the target is the class and ident should be nil
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
#:nodoc:
|
293
|
+
def inspect
|
294
|
+
"#<Updater id=#{id} target=#{target.inspect} time=#{time}>"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
data/lib/updater/util.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module Updater
|
4
|
+
class Util
|
5
|
+
class << self
|
6
|
+
def tempio
|
7
|
+
fp = begin
|
8
|
+
File.open("#{Dir::tmpdir}/#{rand}",
|
9
|
+
File::RDWR|File::CREAT|File::EXCL, 0600)
|
10
|
+
rescue Errno::EEXIST
|
11
|
+
retry
|
12
|
+
end
|
13
|
+
File.unlink(fp.path)
|
14
|
+
fp.binmode
|
15
|
+
fp.sync = true
|
16
|
+
fp
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/updater.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
require File.join( File.dirname(__FILE__), "fooclass" )
|
6
|
+
|
7
|
+
describe "Chained Methods:" do
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
Update.clear_all
|
11
|
+
Foo.all.destroy!
|
12
|
+
@u = Update.chain(Foo,:chained,[:__job__,:__params__])
|
13
|
+
@v = Update.chain(Foo,:chained2,[:__job__,:__params__])
|
14
|
+
end
|
15
|
+
|
16
|
+
[:failure, :success, :ensure].each do |mode|
|
17
|
+
specify "adding '#{mode.to_s}' chain" do
|
18
|
+
v = Update.immidiate(Foo,:method1,[],mode=>@u)
|
19
|
+
v.orm.send(mode).should_not be_empty
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
specify "'failure' should run after an error" do
|
24
|
+
v = Update.immidiate(Foo,:method1,[],:failure=>@u)
|
25
|
+
Foo.should_receive(:method1).and_raise(RuntimeError)
|
26
|
+
Foo.should_receive(:chained).with(v,anything())
|
27
|
+
v.run
|
28
|
+
end
|
29
|
+
|
30
|
+
specify "'failure' should NOT run if their is no error" do
|
31
|
+
v = Update.immidiate(Foo,:method1,[],:failure=>@u)
|
32
|
+
Foo.should_receive(:method1).and_return(:anything)
|
33
|
+
Foo.should_not_receive(:chained)
|
34
|
+
v.run
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "'success' should NOT run after an error" do
|
38
|
+
v = Update.immidiate(Foo,:method1,[],:success=>@u)
|
39
|
+
Foo.should_receive(:method1).and_raise(RuntimeError)
|
40
|
+
Foo.should_not_receive(:chained)
|
41
|
+
v.run
|
42
|
+
end
|
43
|
+
|
44
|
+
specify "'success' should run if their is no error" do
|
45
|
+
v = Update.immidiate(Foo,:method1,[],:success=>@u)
|
46
|
+
Foo.should_receive(:method1).and_return(:anything)
|
47
|
+
Foo.should_receive(:chained).with(v,anything())
|
48
|
+
v.run
|
49
|
+
end
|
50
|
+
|
51
|
+
specify "'ensure' should run after an error" do
|
52
|
+
v = Update.immidiate(Foo,:method1,[],:ensure=>@u)
|
53
|
+
Foo.should_receive(:method1).and_raise(RuntimeError)
|
54
|
+
Foo.should_receive(:chained).with(v,anything())
|
55
|
+
v.run
|
56
|
+
end
|
57
|
+
|
58
|
+
specify "'ensure' should run if their is no error" do
|
59
|
+
v = Update.immidiate(Foo,:method1,[],:ensure=>@u)
|
60
|
+
Foo.should_receive(:method1).and_return(:anything)
|
61
|
+
Foo.should_receive(:chained).with(v,anything())
|
62
|
+
v.run
|
63
|
+
end
|
64
|
+
|
65
|
+
specify "params should be availible" do
|
66
|
+
v = Update.immidiate(Foo,:method1,[],:ensure=>{@u=>'hi', @v=>'bye'})
|
67
|
+
Foo.should_receive(:method1).and_return(:anything)
|
68
|
+
Foo.should_receive(:chained).with(anything(), 'hi')
|
69
|
+
Foo.should_receive(:chained2).with(anything(), 'bye')
|
70
|
+
v.run
|
71
|
+
end
|
72
|
+
|
73
|
+
specify "add an Array" do
|
74
|
+
v = Update.immidiate(Foo,:method1,[],:ensure=>[@u,@v])
|
75
|
+
Foo.should_receive(:method1).and_return(:anything)
|
76
|
+
Foo.should_receive(:chained).with(anything(), anything())
|
77
|
+
Foo.should_receive(:chained2).with(anything(), anything())
|
78
|
+
v.run
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
data/spec/errors_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
require File.join( File.dirname(__FILE__), "fooclass" )
|
6
|
+
|
7
|
+
describe "Job Error Handeling" do
|
8
|
+
before(:each) do
|
9
|
+
Foo.all.destroy!
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should return false when run" do
|
13
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
14
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
15
|
+
u.run.should be_false
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should trap errors" do
|
19
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
20
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
21
|
+
lambda {u.run}.should_not raise_error
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should run the failure task" do
|
25
|
+
err = Update.chain(Foo,:bar,[:error])
|
26
|
+
u = Update.immidiate(Foo,:bar,[:arg1,:arg2],:failure=>err)
|
27
|
+
Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
|
28
|
+
Foo.should_receive(:bar).with(:error)
|
29
|
+
u.run
|
30
|
+
end
|
31
|
+
end
|
data/spec/fooclass.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__), "spec_helper" )
|
2
|
+
|
3
|
+
include Updater
|
4
|
+
|
5
|
+
describe "Fork Worker Instance" do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
@worker = ForkWorker::WorkerMonitor.new(1,Updater::Util.tempio)
|
9
|
+
@w = ForkWorker.new(IO.pipe,@worker)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have a heartbeat" do
|
13
|
+
@w.instance_variable_set(:@continue, true) #otherwise heartbeat is skipped
|
14
|
+
mode = @worker.heartbeat.stat.mode
|
15
|
+
@w.heartbeat
|
16
|
+
@worker.heartbeat.stat.mode.should_not == mode
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#smoke_pipe" do
|
20
|
+
|
21
|
+
before :each do
|
22
|
+
@pipe = IO.pipe
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should remove exactly 1 char from a pipe" do
|
26
|
+
@pipe.last.write '..'
|
27
|
+
@w.smoke_pipe(@pipe.first).should be_true
|
28
|
+
@pipe.first.read_nonblock(2).should == '.'
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not raise errors or block on empty pipes" do
|
32
|
+
lambda { @w.smoke_pipe(@pipe.first) }.should_not raise_error
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#wait_for" do
|
38
|
+
|
39
|
+
it "should have specs"
|
40
|
+
|
41
|
+
describe "when there are pending jobs" do
|
42
|
+
|
43
|
+
it "should NOT wait for a signal"
|
44
|
+
|
45
|
+
it "should smoke the pipe"
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should wake as soon as a new job signal is placed on the pipe"
|
50
|
+
|
51
|
+
|
52
|
+
it "should run the heartbeat every 'timeout' seconds"
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|