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.
@@ -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
@@ -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
-