updater 0.2.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/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
+