updater 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- 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)
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 = job ? sub_args(job,args.dup) : 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
- failure.run(self) if failure
39
- destroy unless nil == time
40
- return false
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
- destroy unless nil == time
43
- true
25
+ ret
44
26
  end
45
27
 
46
- def sub_args(job,a)
47
- a.map {|e| :__job__ == e ? job : e}
28
+ def method_missing(method, *args)
29
+ @orm.send(method,*args)
48
30
  end
49
31
 
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
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
- def locked?
65
- not @lock_name.nil?
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 locked_by
69
- @lock_name
53
+ def persistant?
54
+ @orm.persistant
70
55
  end
71
56
 
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)
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). 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'.
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. Defaults to []
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 if this request raises an error. Usually the
118
- # failure request will be created with the +chane+ method.
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(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('USR1',pid) if pid
135
- ret
136
- rescue Errno::ESRCH
137
- @pid = nil
138
- puts "PID invalid"
139
- #log this as well
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
- 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
235
+ #TODO
176
236
  end
177
237
 
178
- #The time class used by Updater. See time=
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
- all(:time.lte=>time.now.to_i)
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 filter for all requests that are not yet ready to run, that is time is after time.now
262
+ #A count of how many jobs are scheduled but not yet run
197
263
  def delayed
198
- all(:time.gt=>time.now.to_i)
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
- # Computes the stored class an instance or class
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