updater 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2009 John F. Miller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ This file includes code derived from the delayed_job gem which is
23
+ distributed under the same licence. delayed_job is Copyright (c)
24
+ 2005 Tobias Luetke.
data/README ADDED
@@ -0,0 +1,7 @@
1
+ updater
2
+ =======
3
+
4
+ This plugin provides database driven delayed exicution of DataMapper model
5
+ classes. It can call class method on any class availible class or it can find
6
+ an instance of a class from a database and exicute any method on that instance.
7
+ Error handleing is also provided.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'spec/rake/spectask'
4
+
5
+ VERSION_FILE = File.join(File.dirname(__FILE__), 'VERSION')
6
+
7
+ GEM_NAME = "updater"
8
+ GEM_VERSION = File.read(VERSION_FILE).strip
9
+ AUTHOR = "John F. Miller"
10
+ EMAIL = "emperor@antarestrader.com"
11
+ HOMEPAGE = "http://blog.antarestrader.com"
12
+ SUMMARY = "Plugin for the delayed calling of methods particularly DataMapper model instance and class methods."
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = GEM_NAME
16
+ s.version = GEM_VERSION
17
+ s.date = File.ctime(VERSION_FILE)
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["README", "LICENSE", "VERSION"]
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.author = AUTHOR
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.add_dependency('datamapper', '>= 0.9.11')
27
+ s.add_development_dependency('rspec', '>= 1.2.6')
28
+ s.add_development_dependency('timecop', '>= 0.2.1')
29
+ s.add_development_dependency('chronic', '>= 0.2.3')
30
+ s.require_path = 'lib'
31
+ s.bindir = 'bin'
32
+ s.files = %w(LICENSE README Rakefile VERSION) + Dir.glob("{lib,spec,bin}/**/*")
33
+
34
+ end
35
+
36
+ Rake::GemPackageTask.new(spec) do |pkg|
37
+ pkg.gem_spec = spec
38
+ end
39
+
40
+ Spec::Rake::SpecTask.new do |t|
41
+ t.warning = false
42
+ end
43
+
44
+ desc "run all tests"
45
+ task :default => [:spec]
46
+
47
+ desc "Create a gemspec file"
48
+ task :gemspec do
49
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
50
+ file.puts spec.to_ruby
51
+ end
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/updater ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ puts "starting update deamon..."
4
+
5
+ require 'rubygems'
6
+ require 'yaml'
7
+ require 'updater'
8
+ equire 'updater/worker'
9
+
10
+
11
+ dbconfig = YAML.load_file('config/database.yml')
12
+
13
+ DataMapper.setup(dbconfig['development'])
14
+
15
+
16
+
17
+
18
+
19
+
20
+
@@ -0,0 +1,6 @@
1
+ namespace :updater do
2
+ desc "Do something for updater"
3
+ task :default do
4
+ puts "updater doesn't do anything"
5
+ end
6
+ end
@@ -0,0 +1,257 @@
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
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
+ create(hash.merge(options))
134
+ end
135
+
136
+ # like +at+ but with time as time.now. Generally this will be used to run a long running operation in
137
+ # asyncronously in a differen process. See +at+ for details
138
+ def immidiate(*args)
139
+ at(time.now,*args)
140
+ end
141
+
142
+ # like +at+ but without a time to run. This is used to create requests that run in responce to the
143
+ # failure of other requests. See +at+ for details
144
+ def chain(*args)
145
+ at(nil,*args)
146
+ end
147
+
148
+ # Retrieves all updates for a conforming target possibly limiting the results to the named
149
+ # request.
150
+ #
151
+ # == Parameters
152
+ #
153
+ # target <Class | Object> a class or conforming object that postentially is the calculated target
154
+ # of a request.
155
+ #
156
+ # name(optional) <String> If a name is sent, the first request with fot this target with this name
157
+ # will be returned.
158
+ #
159
+ # ==Returns
160
+ #
161
+ # <Array[Updater]> unless name is given then only a single [Updater] instance.
162
+ def for(target,name=nil)
163
+ ident = ident_for(target)
164
+ target = target_for(target)
165
+ if name
166
+ first(:target=>target,:ident=>ident,:name=>name)
167
+ else
168
+ all(:target=>target,:ident=>ident)
169
+ end
170
+ end
171
+
172
+ #The time class used by Updater. See time=
173
+ def time
174
+ @@time ||= Time
175
+ end
176
+
177
+ # By default Updater will use the system time (Time class) to get the current time. The application
178
+ # that Updater was developed for used a game clock that could be paused or restarted. This method
179
+ # allows us to substitute a custom class for Time. This class must respond with in interger or Time to
180
+ # the #now method.
181
+ def time=(klass)
182
+ @@time = klass
183
+ end
184
+
185
+ #A filter for all requests that are ready to run, that is they requested to be run before or at time.now
186
+ def current
187
+ all(:time.lte=>time.now.to_i)
188
+ end
189
+
190
+ #A filter for all requests that are not yet ready to run, that is time is after time.now
191
+ def delayed
192
+ all(:time.gt=>time.now.to_i)
193
+ end
194
+
195
+ #This returns a set of update requests.
196
+ #The first parameter is the maximum number to return (get a few other workers may be in compitition)
197
+ #The second optional parameter is a list of options to be past to DataMapper.
198
+ def worker_set(limit = 5, options={})
199
+ #TODO: add priority to this.
200
+ options = {:limit=>limit, :order=>[:time.desc]}.merge(options)
201
+ current.all(options)
202
+ end
203
+
204
+ ####################
205
+ # Worker Functions #
206
+ ####################
207
+
208
+ #Gets a single gob form the queue, locks and runs it.
209
+ def work_off(worker)
210
+ updates = worker_set
211
+
212
+ #concept copied form delayed_job. If there are a number of
213
+ #different processes working on the queue, the niave approch
214
+ #would result in every instance trying to lock the same record.
215
+ #by shuffleing our results we greatly reduce the chances that
216
+ #multilpe workers try to lock the same process
217
+ updates = updates.to_a.sort_by{rand()}
218
+ updates.each do |u|
219
+ t = u.run_with_lock(worker)
220
+ return queue_time unless nil == t
221
+ end
222
+ end
223
+
224
+ def queue_time
225
+ nxt = self.first(:time.not=>nil, :order=>[:time.desc])
226
+ return nil unless nxt
227
+ return 0 if nxt.time <= time.now.to_i
228
+ return nxt.time - time.now.to_i
229
+ end
230
+
231
+
232
+
233
+ private
234
+
235
+ # Computes the stored class an instance or class
236
+ def target_for(inst)
237
+ return inst if inst.kind_of? Class
238
+ inst.class
239
+ end
240
+
241
+ # Compute the agrument sent to the finder method
242
+ def ident_for(target,finder=nil,args=nil)
243
+ if !(target.kind_of?(Class)) || finder
244
+ args || target.id
245
+ end
246
+ #Otherwize the target is the class and ident should be nil
247
+ end
248
+
249
+ end
250
+
251
+ #:nodoc:
252
+ def inspect
253
+ "#<Updater id=#{id} target=#{target.inspect} time=#{time}>"
254
+ end
255
+ end
256
+
257
+ end
@@ -0,0 +1,61 @@
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 Worker
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 = Thread.new do
23
+ loop do
24
+ delay = Update.work_off(self)
25
+ break if $exit
26
+ sleep delay
27
+ break if exit
28
+ end
29
+ end
30
+
31
+ trap('TERM') { terminate_with t }
32
+ trap('INT') { terminate_with t }
33
+
34
+ trap('USR1') do
35
+ say "Wakeup Signal Caught"
36
+ t.run
37
+ end
38
+
39
+ sleep unless $exit
40
+ end
41
+
42
+ def say(text)
43
+ puts text unless @quiet
44
+ logger.info text if logger
45
+ end
46
+
47
+ private
48
+
49
+ def terminate_with(t)
50
+ say "Exiting..."
51
+ $exit = true
52
+ t.run
53
+ say "Forcing Shutdown" unless status = t.join(15) #Nasty inline assignment
54
+ Update.clear_locks(self)
55
+ exit status ? 0 : 1
56
+ end
57
+
58
+
59
+ end
60
+
61
+ end
data/lib/updater.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "rubygems"
2
+
3
+ require 'dm-core'
4
+ require 'dm-types'
5
+
6
+ module Updater
7
+ VERSION = File.read(File.join(File.dirname(__FILE__),'..','VERSION')).strip
8
+ end
9
+
10
+ require 'updater/update.rb'
data/spec/lock_spec.rb ADDED
@@ -0,0 +1,73 @@
1
+ require File.join( File.dirname(__FILE__), "spec_helper" )
2
+
3
+ include Updater
4
+
5
+ describe "Update Locking:" do
6
+
7
+ class Foo
8
+ include DataMapper::Resource
9
+
10
+ property :id, Serial
11
+ property :name, String
12
+
13
+ def bar(*args)
14
+ Foo.bar(:instance,*args)
15
+ end
16
+
17
+ end
18
+
19
+ Foo.auto_migrate!
20
+
21
+ before :each do
22
+ @u = Update.immidiate(Foo,:bar,[])
23
+ @w = Worker.new(:name=>"first", :quiet=>true)
24
+ end
25
+
26
+ it "An unlocked record should lock" do
27
+ @u.lock(@w).should be_true
28
+ @u.locked?.should be_true
29
+ @u.locked_by.should == @w.name
30
+ end
31
+
32
+ it "A locked record should NOT lock" do
33
+ @u.lock(@w).should be_true
34
+ @u.lock(Worker.new(:quiet=>true)).should be_false
35
+ end
36
+
37
+ it "A record that failed to lock should not change" do
38
+ @u.lock(@w).should be_true
39
+ @u.lock(Worker.new(:quiet=>true)).should be_false
40
+ @u.locked_by.should == @w.name
41
+ end
42
+
43
+ it "A record should report as locked if locked by the same worker twice" do
44
+ @u.lock(@w).should be_true
45
+ @u.lock(@w).should be_true
46
+ end
47
+
48
+ describe "#run_with_lock" do
49
+
50
+ it "should run an unlocked record" do
51
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
52
+ Foo.should_receive(:bar).with(:arg1,:arg2)
53
+ u.run_with_lock(@w).should be_true
54
+ end
55
+
56
+ it "should NOT run an already locked record" do
57
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
58
+ u.lock(Worker.new)
59
+ Foo.should_not_receive(:bar)
60
+ u.run_with_lock(@w).should be_nil
61
+ end
62
+
63
+ it "should return false if the update ran but there was an error" do
64
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
65
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
66
+ u.run_with_lock(@w).should be_false
67
+ end
68
+
69
+ end
70
+
71
+ it "#clear_locks should lear all locks from a worker"
72
+
73
+ end
@@ -0,0 +1,17 @@
1
+ require "rubygems"
2
+
3
+ ROOT = File.join(File.dirname(__FILE__), '..')
4
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
5
+
6
+ require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
7
+ require "dm-core"
8
+
9
+ require 'updater'
10
+ require 'updater/worker'
11
+
12
+ DataMapper.setup(:default, 'sqlite3::memory:')
13
+ DataMapper.auto_migrate!
14
+
15
+ require 'timecop'
16
+ require 'chronic'
17
+
@@ -0,0 +1,185 @@
1
+ require File.join( File.dirname(__FILE__), "spec_helper" )
2
+
3
+ include Updater
4
+
5
+ describe Update do
6
+
7
+ class Foo
8
+ include DataMapper::Resource
9
+
10
+ property :id, Serial
11
+ property :name, String
12
+
13
+ def bar(*args)
14
+ Foo.bar(:instance,*args)
15
+ end
16
+
17
+ end
18
+
19
+ Foo.auto_migrate!
20
+
21
+ before(:each) do
22
+ Foo.all.destroy!
23
+ end
24
+
25
+ it "should include DataMapper::Resource" do
26
+ DataMapper::Model.descendants.to_a.should include(Update)
27
+ end
28
+
29
+ it "should have a version matching the VERSION file" do
30
+ Updater::VERSION.should == File.read(File.join(ROOT,'VERSION')).strip
31
+ end
32
+
33
+ describe "adding an immidiate update request" do
34
+
35
+ it "with a class target" do
36
+ u = Update.immidiate(Foo,:bar,[])
37
+ u.target.should == Foo
38
+ Update.current.should include(u)
39
+ Update.delayed.should_not include(u)
40
+ end
41
+
42
+ it "with an conforming instance target" do
43
+ f = Foo.create
44
+ u = Update.immidiate(f,:bar,[])
45
+ u.target.should == f
46
+ Update.current.should include(u)
47
+ Update.delayed.should_not include(u)
48
+ end
49
+
50
+ it "with an custome finder" do
51
+ f = Foo.create(:name=>'baz')
52
+ u = Update.immidiate(Foo,:bar,[],:finder=>:first, :finder_args=>{:name=>'baz'})
53
+ u.target.should == f
54
+ Update.current.should include(u)
55
+ Update.delayed.should_not include(u)
56
+ end
57
+
58
+ end
59
+
60
+ describe "chained request" do
61
+
62
+ it "should not be in current or delayed queue" do
63
+ u = Update.chain(Foo,:bar,[:error])
64
+ u.time.should be_nil
65
+ Update.current.should_not include(u)
66
+ Update.delayed.should_not include(u)
67
+ end
68
+
69
+ end
70
+
71
+ describe "named request" do
72
+
73
+ it "should be found by name when target is an instance" do
74
+ f = Foo.create(:name=>'Honey')
75
+ u = Update.immidiate(f,:bar,[:named],:name=>'Now')
76
+ u.name.should ==("Now")
77
+ Update.for(f, "Now").should ==(u)
78
+ end
79
+
80
+ it "should be found by name when target is a class" do
81
+ u = Update.immidiate(Foo,:bar,[:named],:name=>'Now')
82
+ u.name.should ==("Now")
83
+ Update.for(Foo, "Now").should ==(u)
84
+ end
85
+
86
+ it "should return all updates for a given target" do
87
+ u1 = Update.immidiate(Foo,:bar,[:arg1,:arg2])
88
+ u2 = Update.immidiate(Foo,:bar,[:arg3,:arg4])
89
+ Update.for(Foo).should include(u1,u2)
90
+ end
91
+
92
+
93
+ end
94
+
95
+ describe "adding an delayed update request" do
96
+
97
+ it "with a class target" do
98
+ u = Update.at(Chronic.parse('tomorrow'),Foo,:bar,[])
99
+ u.target.should == Foo
100
+ Update.current.should_not include(u)
101
+ Update.delayed.should include(u)
102
+ end
103
+
104
+ it "with an conforming instance target" do
105
+ f = Foo.create
106
+ u = Update.at(Chronic.parse('tomorrow'),f,:bar,[])
107
+ u.target.should == f
108
+ Update.current.should_not include(u)
109
+ Update.delayed.should include(u)
110
+ end
111
+
112
+ it "with an custome finder" do
113
+ f = Foo.create(:name=>'baz')
114
+ u = Update.at(Chronic.parse('tomorrow'),Foo,:bar,[],:finder=>:first, :finder_args=>{:name=>'baz'})
115
+ u.target.should == f
116
+ Update.current.should_not include(u)
117
+ Update.delayed.should include(u)
118
+ end
119
+
120
+ end
121
+
122
+ describe "running an update" do
123
+
124
+ before :each do
125
+ Update.all.destroy!
126
+ end
127
+
128
+ it "should call the named method with a class target" do
129
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
130
+ Foo.should_receive(:bar).with(:arg1,:arg2)
131
+ u.run
132
+ end
133
+
134
+ it "should call the named method with an conforming instance target" do
135
+ f = Foo.create
136
+ u = Update.immidiate(f,:bar,[:arg1,:arg2])
137
+ Foo.should_receive(:bar).with(:instance,:arg1,:arg2)
138
+ u.run
139
+ end
140
+
141
+ it "should delete the record once it is run" do
142
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
143
+ Foo.should_receive(:bar).with(:arg1,:arg2)
144
+ u.run
145
+ u.should_not be_saved #NOTE: not a theological statment
146
+ end
147
+
148
+ it "should delete the record if there is a failure" do
149
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
150
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
151
+ u.run
152
+ u.should_not be_saved #NOTE: not a theological statment
153
+ end
154
+
155
+ it "should not delete the record if it is a chain record" do
156
+ u = Update.chain(Foo,:bar,[:arg1,:arg2])
157
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
158
+ u.run
159
+ u.should be_saved
160
+ end
161
+
162
+ describe "Error Handeling" do
163
+ it "should return false when run" do
164
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
165
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
166
+ u.run.should be_false
167
+ end
168
+
169
+ it "should trap errors" do
170
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
171
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
172
+ lambda {u.run}.should_not raise_error
173
+ end
174
+
175
+ it "should run the failure task" do
176
+ err = Update.chain(Foo,:bar,[:error])
177
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2],:failure=>err)
178
+ Foo.should_receive(:bar).with(:arg1,:arg2).and_raise(RuntimeError)
179
+ Foo.should_receive(:bar).with(:error)
180
+ u.run
181
+ end
182
+ end
183
+ end
184
+
185
+ end
@@ -0,0 +1,72 @@
1
+ require File.join( File.dirname(__FILE__), "spec_helper" )
2
+
3
+ include Updater
4
+
5
+ describe Worker do
6
+
7
+ it "should not print anything when quiet" do
8
+ w = Worker.new :quiet=>true
9
+ out = StringIO.new
10
+ $stdout = out
11
+ w.say "hello world"
12
+ $stdout = STDOUT
13
+ out.string.should be_empty
14
+ end
15
+
16
+ it "should have a name" do
17
+ Worker.new.name.should be_a String
18
+ end
19
+
20
+ end
21
+
22
+ describe "working off jobs:" do
23
+
24
+ class Foo
25
+ include DataMapper::Resource
26
+
27
+ property :id, Serial
28
+ property :name, String
29
+
30
+ def bar(*args)
31
+ Foo.bar(:instance,*args)
32
+ end
33
+
34
+ end
35
+
36
+ describe "Update#work_off" do
37
+
38
+ before :each do
39
+ Update.all.destroy!
40
+ end
41
+
42
+ it "should run and immidiate job"do
43
+ u = Update.immidiate(Foo,:bar,[:arg1,:arg2])
44
+ Foo.should_receive(:bar).with(:arg1,:arg2)
45
+ Update.work_off(Worker.new)
46
+ end
47
+
48
+ it "should aviod conflicts among mutiple workers" do
49
+ u1 = Update.immidiate(Foo,:bar,[:arg1])
50
+ u2 = Update.immidiate(Foo,:baz,[:arg2])
51
+ Foo.should_receive(:bar).with(:arg1)
52
+ Foo.should_receive(:baz).with(:arg2)
53
+ Update.work_off(Worker.new(:name=>"first", :quiet=>true))
54
+ Update.work_off(Worker.new(:name=>"second", :quiet=>true))
55
+ end
56
+
57
+ it "should return 0 if there are more jobs waiting" do
58
+ u1 = Update.immidiate(Foo,:bar,[:arg1])
59
+ u2 = Update.immidiate(Foo,:baz,[:arg2])
60
+ Update.work_off(Worker.new(:name=>"first", :quiet=>true)).should == 0
61
+ end
62
+
63
+ it "should return the number of seconds till the next job if there are no jobs to be run"
64
+
65
+ it "should return nil if the job queue is empty" do
66
+ u1 = Update.immidiate(Foo,:bar,[:arg1])
67
+ Update.work_off(Worker.new(:name=>"first", :quiet=>true)).should be_nil
68
+ end
69
+
70
+ end
71
+
72
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: updater
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - John F. Miller
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-09 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: datamapper
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.11
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.6
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: timecop
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.2.1
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: chronic
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.2.3
54
+ version:
55
+ description: Plugin for the delayed calling of methods particularly DataMapper model instance and class methods.
56
+ email: emperor@antarestrader.com
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - README
63
+ - LICENSE
64
+ - VERSION
65
+ files:
66
+ - LICENSE
67
+ - README
68
+ - Rakefile
69
+ - VERSION
70
+ - lib/updater.rb
71
+ - lib/updater/update.rb
72
+ - lib/updater/tasks.rb
73
+ - lib/updater/worker.rb
74
+ - spec/worker_spec.rb
75
+ - spec/lock_spec.rb
76
+ - spec/update_spec.rb
77
+ - spec/spec_helper.rb
78
+ - bin/updater
79
+ has_rdoc: true
80
+ homepage: http://blog.antarestrader.com
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: "0"
99
+ version:
100
+ requirements: []
101
+
102
+ rubyforge_project:
103
+ rubygems_version: 1.3.5
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Plugin for the delayed calling of methods particularly DataMapper model instance and class methods.
107
+ test_files: []
108
+