updater 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|