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
data/lib/updater/update.rb
CHANGED
@@ -2,81 +2,113 @@ module Updater
|
|
2
2
|
|
3
3
|
#the basic class that drives updater
|
4
4
|
class Update
|
5
|
-
# Contains the Error class after an error is caught in +run+. Not stored to the database
|
5
|
+
# Contains the Error class after an error is caught in +run+. Not stored to the database
|
6
6
|
attr_reader :error
|
7
|
+
attr_reader :orm
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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)
|
9
|
+
#Run the action on this traget compleating any chained actions
|
10
|
+
def run(job=nil,params=nil)
|
11
|
+
ret = true #put return in scope
|
32
12
|
t = target #do not trap errors here
|
33
|
-
final_args =
|
13
|
+
final_args = sub_args(job,params,@orm.method_args)
|
34
14
|
begin
|
35
|
-
t.send(@method.to_sym,*final_args)
|
15
|
+
t.send(@orm.method.to_sym,*final_args)
|
36
16
|
rescue => e
|
37
17
|
@error = e
|
38
|
-
|
39
|
-
|
40
|
-
|
18
|
+
run_chain :failure
|
19
|
+
ret = false
|
20
|
+
ensure
|
21
|
+
run_chain :success if ret
|
22
|
+
run_chain :ensure
|
23
|
+
@orm.destroy! unless @orm.persistant
|
41
24
|
end
|
42
|
-
|
43
|
-
true
|
25
|
+
ret
|
44
26
|
end
|
45
27
|
|
46
|
-
def
|
47
|
-
|
28
|
+
def method_missing(method, *args)
|
29
|
+
@orm.send(method,*args)
|
48
30
|
end
|
49
31
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
32
|
+
def target
|
33
|
+
@orm.finder.nil? ? @orm.target : @orm.target.send(@orm.finder,@orm.finder_args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(orm_inst)
|
37
|
+
@orm = orm_inst
|
38
|
+
end
|
39
|
+
|
40
|
+
def name=(n)
|
41
|
+
@orm.name=n
|
42
|
+
end
|
43
|
+
|
44
|
+
def name
|
45
|
+
@orm.name
|
62
46
|
end
|
63
47
|
|
64
|
-
|
65
|
-
|
48
|
+
#This is the appropriate valut ot use for a chanable field value
|
49
|
+
def id
|
50
|
+
@orm.id
|
66
51
|
end
|
67
52
|
|
68
|
-
def
|
69
|
-
@
|
53
|
+
def persistant?
|
54
|
+
@orm.persistant
|
70
55
|
end
|
71
56
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
57
|
+
def inspect
|
58
|
+
"#<Updater::Update target=#{target.inspect} time=#{orm.time}>"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def sub_args(job,params,a)
|
64
|
+
a.map do |e|
|
65
|
+
begin
|
66
|
+
case e.to_s
|
67
|
+
when '__job__'
|
68
|
+
job
|
69
|
+
when '__params__'
|
70
|
+
params
|
71
|
+
when '__self__'
|
72
|
+
self
|
73
|
+
else
|
74
|
+
e
|
75
|
+
end
|
76
|
+
# For the unfortunate case where e doesn't handle to_s nicely.
|
77
|
+
# On the other hand I dare someone to find something that can be marshaled,
|
78
|
+
# but doesn't do #to_s.
|
79
|
+
rescue NoMethodError=>err
|
80
|
+
raise err unless err.message =~ /\`to_s\'/
|
81
|
+
e
|
82
|
+
end #begin
|
83
|
+
end# map
|
84
|
+
end #def
|
85
|
+
|
86
|
+
def run_chain(name)
|
87
|
+
chains = @orm.send(name)
|
88
|
+
return unless chains
|
89
|
+
chains.each do |job|
|
90
|
+
Update.new(job.target).run(self,job.params)
|
91
|
+
end
|
76
92
|
end
|
77
93
|
|
78
94
|
class << self
|
79
95
|
|
96
|
+
#This attribute must be set to some ORM that will persist the data
|
97
|
+
attr_accessor :orm
|
98
|
+
|
99
|
+
#Gets a single job form the queue, locks and runs it. it returns the number of second
|
100
|
+
#Until the next job is scheduled, or 0 is there are more current jobs, or nil if there
|
101
|
+
#are no jobs scheduled.
|
102
|
+
def work_off(worker)
|
103
|
+
inst = @orm.lock_next(worker)
|
104
|
+
new(inst).run if inst
|
105
|
+
@orm.queue_time
|
106
|
+
ensure
|
107
|
+
clear_locks(worker)
|
108
|
+
end
|
109
|
+
|
110
|
+
def clear_locks(worker); @orm.clear_locks(worker); end
|
111
|
+
|
80
112
|
# Request that the target be sent the method with args at the given time.
|
81
113
|
#
|
82
114
|
# == Parameters
|
@@ -85,14 +117,21 @@ module Updater
|
|
85
117
|
#
|
86
118
|
# target <Class | instance> . If target is a class then 'method' will be sent to that class (unless the
|
87
119
|
# finder option is used. Otherwise, the target will be assumed to be the result of
|
88
|
-
# (target.class).get(target.id).
|
89
|
-
#
|
90
|
-
#
|
120
|
+
# (target.class).get(target.id). (note: The ORM can/should override #get and #id with the proper
|
121
|
+
# methods for it's storage model.) The finder method (:get by default) and the finder_args
|
122
|
+
# (target.id by default) can be set in the options. A ORM (eg DataMapper) instance passed as the target
|
123
|
+
# will "just work." Any object can be found in this mannor is known as a 'conforming instance'. TODO:
|
124
|
+
# make ORM finder and id constants overridable for times when one ORM is used for Updater and another
|
125
|
+
# is used by the model classes.
|
91
126
|
#
|
92
127
|
# method <Symbol>. The method that will be sent to the calculated target.
|
93
128
|
#
|
94
129
|
# args <Array> a list of arguments to be sent to with the method call. Note: 'args' must be seirialiable
|
95
|
-
# with Marshal.dump.
|
130
|
+
# with Marshal.dump. The special values '__job__', '__params__', and '__self__' are replaced they are found
|
131
|
+
# in this list. Defaults to []. (note: the #to_s method will be called on all args before variable substitution
|
132
|
+
# any arg that responds with one of the special values will be replaced as noted above. E.g :__job__ . If
|
133
|
+
# something is silly enough to respond to to_s with a non-pure method you *will* have problems.
|
134
|
+
# NoMethodError is caught and handled gracefully)
|
96
135
|
#
|
97
136
|
# options <Hash> Addational options that will be used to configure the request. see Options
|
98
137
|
# section below.
|
@@ -114,8 +153,12 @@ module Updater
|
|
114
153
|
# in conjunction with the +for+ method to manipulate requests effecting an object or class after
|
115
154
|
# they are set. See +for+ for examples
|
116
155
|
#
|
117
|
-
# :failure <Updater> an other request to be run
|
118
|
-
#
|
156
|
+
# :failure, :success,:ensure <Updater::Update instance> an other request to be run when the request compleste. Usually these
|
157
|
+
# valuses will be created with the +chained+ method. As an alternative a hash with keys of Updater::Update instances and
|
158
|
+
# values of Hash may be used. The hash will be substituted for the '__param__' argument if/when the chained method is called.
|
159
|
+
#
|
160
|
+
# :persistant <true|false> if true the object will not be destroyed after the completion of its run. By default
|
161
|
+
# this is false except when time is nil.
|
119
162
|
#
|
120
163
|
# == Examples
|
121
164
|
#
|
@@ -123,22 +166,45 @@ module Updater
|
|
123
166
|
#
|
124
167
|
# f = Foo.create
|
125
168
|
# u = Updater.at(Chronic.parse('2 hours form now'),f,:bar,[]) # will run Foo.get(f.id).bar in 2 hours
|
126
|
-
def at(
|
127
|
-
|
128
|
-
hash =
|
129
|
-
|
130
|
-
hash[:
|
131
|
-
hash[:finder] = finder || :
|
132
|
-
hash[:
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
169
|
+
def at(t,target,method = nil,args=[],options={})
|
170
|
+
hash = Hash.new
|
171
|
+
hash[:time] = t.to_i unless t.nil?
|
172
|
+
|
173
|
+
hash[:target],hash[:finder],hash[:finder_args] = target_for(target)
|
174
|
+
hash[:finder] = options[:finder] || hash[:finder]
|
175
|
+
hash[:finder_args] = options[:finder_args] || hash[:finder_args]
|
176
|
+
|
177
|
+
hash[:method] = method || :process
|
178
|
+
hash[:method_args] = args
|
179
|
+
|
180
|
+
[:name,:failure,:success,:ensure].each do |opt|
|
181
|
+
hash[opt] = options[opt] if options[opt]
|
182
|
+
end
|
183
|
+
|
184
|
+
hash[:persistant] = options[:persistant] || t.nil? ? true : false
|
185
|
+
|
186
|
+
schedule(hash)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Run this job in 'time' seconds from now. See +at+ for details on expected args.
|
190
|
+
def in(t,*args)
|
191
|
+
at(time.now+t,*args)
|
140
192
|
end
|
141
193
|
|
194
|
+
# Advanced: This method allows values to be passed directly to the ORM layer's create method.
|
195
|
+
# use +at+ and friends for everyday use cases.
|
196
|
+
def schedule(hash)
|
197
|
+
new(@orm.create(hash))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Create a new job having the same charistics as the old, except that 'hash' will override the original.
|
201
|
+
def reschedule(update, hash={})
|
202
|
+
new_job = update.orm.dup
|
203
|
+
new_job.update_attributes(hash)
|
204
|
+
new_job.save
|
205
|
+
new(new_job)
|
206
|
+
end
|
207
|
+
|
142
208
|
# like +at+ but with time as time.now. Generally this will be used to run a long running operation in
|
143
209
|
# asyncronously in a differen process. See +at+ for details
|
144
210
|
def immidiate(*args)
|
@@ -166,16 +232,10 @@ module Updater
|
|
166
232
|
#
|
167
233
|
# <Array[Updater]> unless name is given then only a single [Updater] instance.
|
168
234
|
def for(target,name=nil)
|
169
|
-
|
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
|
235
|
+
#TODO
|
176
236
|
end
|
177
237
|
|
178
|
-
|
238
|
+
#The time class used by Updater. See time=
|
179
239
|
def time
|
180
240
|
@@time ||= Time
|
181
241
|
end
|
@@ -188,14 +248,33 @@ module Updater
|
|
188
248
|
@@time = klass
|
189
249
|
end
|
190
250
|
|
191
|
-
#A filter for all requests that are ready to run, that is they requested to be run before or at time.now
|
251
|
+
# A filter for all requests that are ready to run, that is they requested to be run before or at time.now
|
252
|
+
# and ar not being processed by another worker
|
192
253
|
def current
|
193
|
-
|
254
|
+
@orm.current
|
255
|
+
end
|
256
|
+
|
257
|
+
#The number of jobs currently backloged in the system
|
258
|
+
def load
|
259
|
+
@orm.current_load
|
194
260
|
end
|
195
261
|
|
196
|
-
#A
|
262
|
+
#A count of how many jobs are scheduled but not yet run
|
197
263
|
def delayed
|
198
|
-
|
264
|
+
@orm.delayed
|
265
|
+
end
|
266
|
+
|
267
|
+
#How many jobs will happen at least 'start' seconds from now, but not more then finish seconds from now.
|
268
|
+
#If the second parameter is nil then it is the number of jobbs between now and the first parameter.
|
269
|
+
def future(start,finish = nil)
|
270
|
+
start, finish = [0, start] unless finish
|
271
|
+
@orm.future(start,finish)
|
272
|
+
end
|
273
|
+
|
274
|
+
#Remove all scheduled jobs. Mostly intended for testing, but may also be useful in cases of crashes
|
275
|
+
#or system corruption
|
276
|
+
def clear_all
|
277
|
+
@orm.clear_all
|
199
278
|
end
|
200
279
|
|
201
280
|
#Sets the process id of the worker process if known. If this
|
@@ -218,73 +297,15 @@ module Updater
|
|
218
297
|
@pid
|
219
298
|
end
|
220
299
|
|
221
|
-
#This returns a set of update requests.
|
222
|
-
#The first parameter is the maximum number to return (get a few other workers may be in compitition)
|
223
|
-
#The second optional parameter is a list of options to be past to DataMapper.
|
224
|
-
def worker_set(limit = 5, options={})
|
225
|
-
#TODO: add priority to this.
|
226
|
-
options = {:lock_name=>nil,:limit=>limit, :order=>[:time.asc]}.merge(options)
|
227
|
-
current.all(options)
|
228
|
-
end
|
229
|
-
|
230
|
-
####################
|
231
|
-
# Worker Functions #
|
232
|
-
####################
|
233
|
-
|
234
|
-
#Gets a single gob form the queue, locks and runs it.
|
235
|
-
def work_off(worker)
|
236
|
-
updates = worker_set
|
237
|
-
unless updates.empty?
|
238
|
-
#concept copied form delayed_job. If there are a number of
|
239
|
-
#different processes working on the queue, the niave approch
|
240
|
-
#would result in every instance trying to lock the same record.
|
241
|
-
#by shuffleing our results we greatly reduce the chances that
|
242
|
-
#multilpe workers try to lock the same process
|
243
|
-
updates = updates.to_a.sort_by{rand()}
|
244
|
-
updates.each do |u|
|
245
|
-
t = u.run_with_lock(worker)
|
246
|
-
break unless nil == t
|
247
|
-
end
|
248
|
-
end
|
249
|
-
rescue DataObjects::ConnectionError
|
250
|
-
sleep 0.1
|
251
|
-
retry
|
252
|
-
ensure
|
253
|
-
worker.clear_locks
|
254
|
-
return queue_time
|
255
|
-
end
|
256
|
-
|
257
|
-
def queue_time
|
258
|
-
nxt = self.first(:time.not=>nil,:lock_name=>nil, :order=>[:time.asc])
|
259
|
-
return nil unless nxt
|
260
|
-
return 0 if nxt.time <= time.now.to_i
|
261
|
-
return nxt.time - time.now.to_i
|
262
|
-
end
|
263
|
-
|
264
|
-
|
265
|
-
|
266
300
|
private
|
267
301
|
|
268
|
-
#
|
302
|
+
# Given some instance return the information needed to recreate that target
|
269
303
|
def target_for(inst)
|
270
|
-
return inst if inst.kind_of? Class
|
271
|
-
inst.class
|
304
|
+
return [inst, nil, nil] if inst.kind_of? Class
|
305
|
+
[inst.class,@orm::FINDER,inst.send(orm::ID)]
|
272
306
|
end
|
273
307
|
|
274
|
-
# Compute the agrument sent to the finder method
|
275
|
-
def ident_for(target,finder=nil,args=nil)
|
276
|
-
if !(target.kind_of?(Class)) || finder
|
277
|
-
args || target.id
|
278
|
-
end
|
279
|
-
#Otherwize the target is the class and ident should be nil
|
280
|
-
end
|
281
|
-
|
282
|
-
end
|
283
|
-
|
284
|
-
#:nodoc:
|
285
|
-
def inspect
|
286
|
-
"#<Updater id=#{id} target=#{target.inspect} time=#{time}>"
|
287
308
|
end
|
288
|
-
end
|
309
|
+
end #class Update
|
289
310
|
|
290
|
-
end
|
311
|
+
end #Module Updater
|