updater 0.2.2 → 0.3.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.
@@ -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