updater 0.9.4 → 0.10.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 +8 -9
- data/VERSION +1 -1
- data/lib/updater/fork_worker.rb +1 -1
- data/lib/updater/orm/activerocord.rb +23 -0
- data/lib/updater/orm/datamapper.rb +28 -8
- data/lib/updater/orm/mock.rb +155 -0
- data/lib/updater/orm/mongo.rb +50 -15
- data/lib/updater/orm/orm.rb +3 -1
- data/lib/updater/setup.rb +24 -7
- data/lib/updater/thread_worker.rb +5 -0
- data/lib/updater/update.rb +8 -5
- data/spec/chained_spec.rb +3 -2
- data/spec/datamapper_orm_spec.rb +7 -0
- data/spec/errors_spec.rb +1 -1
- data/spec/fooclass.rb +42 -11
- data/spec/fooclass_spec.rb +38 -0
- data/spec/fork_worker_instance_spec.rb +0 -19
- data/spec/fork_worker_spec.rb +1 -0
- data/spec/mock_orm_spec.rb +7 -0
- data/spec/mongo_orm_spec.rb +7 -0
- data/spec/named_request_spec.rb +1 -4
- data/spec/orm_lint.rb +292 -0
- data/spec/params_sub_spec.rb +2 -1
- data/spec/results.html +1567 -0
- data/spec/schedule_spec.rb +18 -9
- data/spec/spec_helper.rb +7 -8
- data/spec/thread_worker_spec.rb +0 -37
- data/spec/update_runner_spec.rb +4 -6
- data/spec/update_spec.rb +1 -1
- data/spec/util_spec.rb +0 -2
- metadata +16 -15
- data/lib/updater/orm/mongo.rb~ +0 -188
- data/lib/updater/setup.rb~ +0 -175
- data/lib/updater/simulated.db +0 -0
- data/lib/updater/thread_worker.rb~ +0 -92
- data/lib/updater/update_dm.rb +0 -298
- data/spec/lock_spec.rb +0 -64
- data/spec/spec_helper.rb~ +0 -23
@@ -1,92 +0,0 @@
|
|
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 ThreadWorker
|
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 = run_job_loop
|
23
|
-
|
24
|
-
trap('TERM') { terminate_with @t }
|
25
|
-
trap('INT') { terminate_with @t }
|
26
|
-
|
27
|
-
trap('USR1') do
|
28
|
-
old_proc = trap('USR1','IGNORE')
|
29
|
-
run_loop
|
30
|
-
trap('USR1',old_proc)
|
31
|
-
end
|
32
|
-
|
33
|
-
Thread.pass
|
34
|
-
|
35
|
-
sleep unless $exit
|
36
|
-
end
|
37
|
-
|
38
|
-
def say(text)
|
39
|
-
puts text unless @quiet
|
40
|
-
logger.info text if logger
|
41
|
-
end
|
42
|
-
|
43
|
-
def stop
|
44
|
-
raise RuntimeError unless @t
|
45
|
-
terminate_with @t
|
46
|
-
end
|
47
|
-
|
48
|
-
def run_loop
|
49
|
-
if @t.alive?
|
50
|
-
@t.wakeup #calling run here is a Bad Idea
|
51
|
-
else
|
52
|
-
say " ~~ Restarting Job Loop"
|
53
|
-
@t = run_job_loop
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def run_job_loop
|
60
|
-
Thread.new do
|
61
|
-
loop do
|
62
|
-
begin
|
63
|
-
delay = Update.work_off(self)
|
64
|
-
break if $exit
|
65
|
-
if delay
|
66
|
-
sleep delay
|
67
|
-
else
|
68
|
-
sleep
|
69
|
-
end
|
70
|
-
break if $exit
|
71
|
-
rescue
|
72
|
-
say "Caught exception in Job Loop"
|
73
|
-
sleep 0.1
|
74
|
-
retry
|
75
|
-
end
|
76
|
-
end
|
77
|
-
say "Worker thread exiting!"
|
78
|
-
Update.clear_locks(self)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def terminate_with(t)
|
83
|
-
say "Exiting..."
|
84
|
-
$exit = true
|
85
|
-
t.run if t.alive?
|
86
|
-
say "Forcing Shutdown" unless status = t.join(15) #Nasty inline assignment
|
87
|
-
Update.clear_locks(self)
|
88
|
-
exit status ? 0 : 1
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
end
|
data/lib/updater/update_dm.rb
DELETED
@@ -1,298 +0,0 @@
|
|
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/spec/lock_spec.rb
DELETED
@@ -1,64 +0,0 @@
|
|
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 "Update Locking:" do
|
8
|
-
|
9
|
-
|
10
|
-
class Worker
|
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 say(text)
|
21
|
-
puts text
|
22
|
-
nil
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
before :each do
|
27
|
-
Foo.all.destroy!
|
28
|
-
@u = Update.immidiate(Foo,:bar,[])
|
29
|
-
@w = Worker.new(:name=>"first", :quiet=>true)
|
30
|
-
end
|
31
|
-
|
32
|
-
it "An unlocked record should lock" do
|
33
|
-
@u.lock(@w).should be_true
|
34
|
-
@u.locked?.should be_true
|
35
|
-
@u.locked_by.should == @w.name
|
36
|
-
end
|
37
|
-
|
38
|
-
it "A locked record should NOT lock" do
|
39
|
-
@u.lock(@w).should be_true
|
40
|
-
@u.lock(Worker.new(:quiet=>true)).should be_false
|
41
|
-
end
|
42
|
-
|
43
|
-
it "A record that failed to lock should not change" do
|
44
|
-
@u.lock(@w).should be_true
|
45
|
-
@u.lock(Worker.new(:quiet=>true)).should be_false
|
46
|
-
@u.locked_by.should == @w.name
|
47
|
-
end
|
48
|
-
|
49
|
-
it "A record should report as locked if locked by the same worker twice" do
|
50
|
-
@u.lock(@w).should be_true
|
51
|
-
@u.lock(@w).should be_true
|
52
|
-
end
|
53
|
-
|
54
|
-
it "#clear_locks should clear all locks from a worker" do
|
55
|
-
@v = Update.immidiate(Foo,:bar,[:arg1,:arg2])
|
56
|
-
@u.lock(@w)
|
57
|
-
@v.lock(@w)
|
58
|
-
@u.locked?.should be_true
|
59
|
-
Update.clear_locks(@w)
|
60
|
-
@u.reload.locked?.should be_false
|
61
|
-
@v.reload.locked?.should be_false
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
data/spec/spec_helper.rb~
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
|
3
|
-
ROOT = File.join(File.dirname(__FILE__), '..')
|
4
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
|
5
|
-
|
6
|
-
require "rspec" # Satisfies Autotest and anyone else not using the Rake tasks
|
7
|
-
require "dm-core"
|
8
|
-
require 'dm-migrations'
|
9
|
-
|
10
|
-
require 'updater'
|
11
|
-
#require 'updater/thread_worker'
|
12
|
-
require 'updater/fork_worker'
|
13
|
-
require 'updater/orm/datamapper'
|
14
|
-
|
15
|
-
Updater::Update.orm = Updater::ORM::DataMapper
|
16
|
-
|
17
|
-
DataMapper.setup(:default, 'sqlite3::memory:')
|
18
|
-
DataMapper.auto_migrate!
|
19
|
-
|
20
|
-
require 'timecop'
|
21
|
-
require 'chronic'
|
22
|
-
|
23
|
-
|