updater 0.3.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.9.0
@@ -35,7 +35,7 @@ module Updater
35
35
  logger.info "Max Workers set to #{@max_workers}"
36
36
  @timeout = options[:timeout] || 60
37
37
  logger.info "Timeout set to #{@timeout} sec."
38
- @current_workers = 1
38
+ @current_workers = 1 #we will actually add this worker the first time through the master loop
39
39
  @workers = {} #key is pid value is worker class
40
40
  @uptime = Time.now
41
41
  @downtime = Time.now
@@ -92,10 +92,9 @@ module Updater
92
92
  # * :timeout : how long can a worker be inactive before being killed
93
93
  # * :sockets: 0 or more IO objects that should wake up master to alert it that new data is availible
94
94
 
95
- def start(stream,options = {})
95
+ def start(options = {})
96
96
  initial_setup(options) #need this for logger
97
97
  logger.info "*** Starting Master Process***"
98
- @stream = stream
99
98
  logger.info "* Adding the first round of workers *"
100
99
  maintain_worker_count
101
100
  QUEUE_SIGS.each { |sig| trap_deferred(sig) }
@@ -121,13 +120,13 @@ module Updater
121
120
  logger.fatal "10 consecutive errors! Abandoning Master process"
122
121
  end
123
122
  stop # gracefully shutdown all workers on our way out
124
- logger.info "master process Exiting"
123
+ logger.warn "-=-=-=- master process Exiting -=-=-=-\n\n"
125
124
  end
126
125
 
127
126
  def stop(graceful = true)
128
127
  trap(:USR2,"IGNORE")
129
128
  [:INT,:TERM].each {|signal| trap(signal,"DEFAULT") }
130
- puts "Quitting. I need 30 seconds to stop my workers..."
129
+ puts "Quitting. I need 30 seconds to stop my workers..." unless @workers.empty?
131
130
  limit = Time.now + 30
132
131
  signal_each_worker(graceful ? :QUIT : :TERM)
133
132
  until @workers.empty? || Time.now > limit
@@ -140,15 +139,26 @@ module Updater
140
139
  def master_sleep
141
140
  begin
142
141
  timeout = calc_timeout
143
- logger.debug { "Sleeping for #{timeout}" } #TODO return to debug
142
+ logger.debug { "Sleeping for #{timeout}" }
144
143
  ready, _1, _2 = IO.select(@wakeup_set, nil, nil, timeout)
145
- return unless ready && ready.first #just wakeup and run maintance
146
- @signal_queue << :DATA unless ready.first == @self_pipe.first #somebody wants our attention
144
+ return unless ready && ready.first #timeout hit, just wakeup and run maintance
145
+ add_connection(ready.first) and return if ready.first.respond_to?(:accept) #open a new incomming connection
146
+ @signal_queue << :DATA unless ready.first == @self_pipe.first
147
147
  loop {ready.first.read_nonblock(16 * 1024)}
148
+ rescue EOFError #somebody closed thier connection
149
+ logger.info "closed socket connection"
150
+ @wakeup_set.delete ready.first
151
+ ready.first.close
148
152
  rescue Errno::EAGAIN, Errno::EINTR
149
153
  end
150
154
  end
151
155
 
156
+ def add_connection(server)
157
+ @wakeup_set << server.accept_nonblock
158
+ logger.info "opened socket connection: [#{@wakeup_set.last.addr.join(', ')}]"
159
+ rescue Errno::EAGAIN, Errno::EINTR
160
+ end
161
+
152
162
  def calc_timeout
153
163
  Time.now - [@uptime, @downtime].max < @timeout ? @timeout / 8 : 2*@timeout
154
164
  end
@@ -219,6 +229,7 @@ module Updater
219
229
 
220
230
  def add_worker(worker_number)
221
231
  worker = WorkerMonitor.new(worker_number,Updater::Util.tempio)
232
+ Update.orm.before_fork
222
233
  pid = Process.fork do
223
234
  fork_cleanup
224
235
  self.new(@pipe,worker).run
@@ -229,6 +240,7 @@ module Updater
229
240
 
230
241
  def fork_cleanup
231
242
  QUEUE_SIGS.each { |signal| trap(signal,"IGNORE") }
243
+ Update.orm.after_fork
232
244
  if @self_pipe !=nil
233
245
  @self_pipe.each {|io| io.close}
234
246
  end
@@ -327,8 +339,8 @@ module Updater
327
339
  wait_for(delay) if @continue
328
340
  rescue Exception=> e
329
341
  say "Caught exception in Job Loop"
330
- say e.message
331
- say "||=========\n|| Backtrace\n|| " + e.backtrace.join("\n|| ") + "\n||========="
342
+ say e.inspect
343
+ say "\n||=========\n|| Backtrace\n|| " + e.backtrace.join("\n|| ") + "\n||========="
332
344
  Update.clear_locks(self)
333
345
  exit; #die and be replaced by the master process
334
346
  end
@@ -392,8 +404,8 @@ module Updater
392
404
  #need to wait for another job
393
405
  t = Time.now + delay
394
406
  while Time.now < t && @continue
395
- delay = [@timeout,t-Time.now].min
396
- debug "No Jobs; #{name} sleeping for #{delay}: [#{@timeout},#{t - Time.now}].min"
407
+ delay = [@timeout/2,t-Time.now].min
408
+ debug "No Jobs; #{name} sleeping for #{delay}: [#{@timeout/2},#{t - Time.now}].min"
397
409
  wakeup,_1,_2 = select([@stream],nil,nil,delay)
398
410
  heartbeat
399
411
  if wakeup
@@ -19,13 +19,13 @@ module Updater
19
19
  storage_names[:default] = "updates"
20
20
 
21
21
  property :id, Serial
22
- property :time, Integer
23
- property :target, Class
24
- property :finder, String
25
- property :finder_args, Yaml
22
+ property :time, Integer, :index=>true
23
+ property :target, Class, :index=>:for_target
24
+ property :finder, String, :index=>:for_target
25
+ property :finder_args, Yaml, :index=>:for_target
26
26
  property :method, String
27
27
  property :method_args, Object, :lazy=>false
28
- property :name, String, :length=>255
28
+ property :name, String, :length=>255, :index=>true
29
29
  property :lock_name, String
30
30
  property :persistant, Boolean
31
31
 
@@ -112,6 +112,7 @@ module Updater
112
112
  return nxt.time - tnow
113
113
  end
114
114
 
115
+ #Returns the Locked Job or nil if no jobs were availible.
115
116
  def lock_next(worker)
116
117
  updates = worker_set
117
118
  unless updates.empty?
@@ -124,6 +125,7 @@ module Updater
124
125
  updates.each do |u|
125
126
  return u if u.lock(worker)
126
127
  end
128
+ return nil
127
129
  end
128
130
  rescue DataObjects::ConnectionError
129
131
  sleep 0.1
@@ -140,7 +142,32 @@ module Updater
140
142
  end
141
143
 
142
144
  def for(mytarget, myfinder, myfinder_args, myname=nil)
143
- #TODO
145
+ search = all(
146
+ :target=>mytarget,
147
+ :finder=>myfinder,
148
+ :finder_args=>myfinder_args,
149
+ :lock_name=>nil
150
+ )
151
+ myname ? search.all(:name=>myname ) : search
152
+ end
153
+
154
+ #For the server only, setup the connection to the database
155
+ def setup(options)
156
+ ::DataMapper.logger = options.delete(:logger)
157
+ ::DataMapper.setup(:default,options)
158
+ end
159
+
160
+ # For pooled connections it is necessary to empty the pool of the parents connections so that they
161
+ # do not comtiminate the child pool. Note that while Datamapper is thread safe, it is not safe accross a process fork.
162
+ def before_fork
163
+ return unless (defined? ::DataObjects::Pooling)
164
+ return if ::DataMapper.repository.adapter.kind_of?(::DataMapper::Adapters::Sqlite3Adapter)
165
+ ::DataMapper.logger.debug "+-+-+-+-+ Cleaning up connection pool (#{::DataObjects::Pooling.pools.length}) +-+-+-+-+"
166
+ ::DataObjects::Pooling.pools.each {|p| p.dispose}
167
+ end
168
+
169
+ def after_fork
170
+
144
171
  end
145
172
 
146
173
  private
@@ -152,7 +179,7 @@ module Updater
152
179
  options = {:lock_name=>nil,:limit=>limit, :order=>[:time.asc]}.merge(options)
153
180
  current.all(options)
154
181
  end
155
-
182
+
156
183
  def lock
157
184
 
158
185
  end
@@ -168,8 +195,8 @@ module Updater
168
195
  belongs_to :caller, :model=>Updater::ORM::DataMapper, :child_key=>[:caller_id]
169
196
  belongs_to :target, :model=>Updater::ORM::DataMapper, :child_key=>[:target_id]
170
197
 
171
- property :params, Object, :nullable=>true #:required=>false
172
- property :occasion, String, :nullable=>false #:required=>true
198
+ property :params, Object, :required=>false
199
+ property :occasion, String, :required=>true
173
200
  end
174
201
 
175
202
  end#ORM
@@ -217,6 +217,22 @@ module Updater
217
217
  NotImplementedError
218
218
  end
219
219
 
220
+ # This method is the generic way to setup the datastore. Options is a hash one of whose fields
221
+ # will be :logger, the logger instance to pass on to the ORM. The rest of the options are ORM
222
+ # spesific. The function should prepair a connection to the datastore using the given options.
223
+ # If the connection cannot be prepaired then an appropriate error should be raised.
224
+ def setup(options)
225
+ NotImplementedError
226
+ end
227
+
228
+ # This method is called by the child before a fork call. It allows the ORM to clean up any connections
229
+ # Made by the parent and establish new connections if necessary.
230
+ def before_fork
231
+
232
+ end
233
+
234
+ def after_fork
235
+
220
236
  # Optional, but strongly recomended.
221
237
  #
222
238
  # For any datastore that permits, return and Array of all delayed, chained, and current but not locked jobs that reference
data/lib/updater/setup.rb CHANGED
@@ -14,6 +14,10 @@ module Updater
14
14
  new(config_file).stop
15
15
  end
16
16
 
17
+ def client_setup(options = {})
18
+ new(config_file, options).client_setup
19
+ end
20
+
17
21
  def monitor
18
22
 
19
23
  end
@@ -27,44 +31,84 @@ module Updater
27
31
  end
28
32
  end
29
33
 
30
- ROOT = File.dirname(self.config_file)
34
+ ROOT = File.dirname(self.config_file || Dir.pwd)
31
35
 
32
- def initialize(file_or_hash)
36
+ #extended used for clients who wnat to override parameters
37
+ def initialize(file_or_hash, extended = {})
33
38
  @options = file_or_hash.kind_of?(Hash) ? file_or_hash : load_file(file_or_hash)
39
+ @options.merge(extended)
34
40
  @options[:pid_file] ||= File.join(ROOT,'updater.pid')
35
41
  @options[:host] ||= "localhost"
36
- @logger = Logger.new(@options[:log_file] || STDOUT)
42
+ @logger = @options[:logger] || Logger.new(@options[:log_file] || STDOUT)
37
43
  level = Logger::SEV_LABEL.index(@options[:log_level].upcase) if @options[:log_level]
38
44
  @logger.level = level || Logger::WARN
39
45
  end
40
46
 
41
47
  def start
42
- @logger.warn "Starting Loop"
43
48
  pid = Process.fork do
44
49
  _start
45
50
  end
46
- @logger.warn "Rake Successfully started Master Loop at pid #{pid}"
51
+ @logger.warn "Successfully started Master Loop at pid #{pid}"
52
+ puts "Job Queue Processor Started at PID: #{pid}"
47
53
  end
48
54
 
49
55
  def stop
50
56
  Process.kill("TERM",File.read(@options[:pid_file]).to_i)
51
57
  end
52
58
 
53
- def client
59
+ # The client is responcible for loading classes and making connections. We will simply setup the Updater spesifics
60
+ def client_setup
61
+ set_orm
62
+
63
+ if @options[:socket] && File.exists?(@options[:socket])
64
+ Updater::Update.socket = UNIXSocket.new(@options[:socket])
65
+ elsif @options[:udp]
66
+ socket = UDPSocket.new()
67
+ socket.connect(@options[:host],@options[:udp])
68
+ Updater::Update.socket = socket
69
+ elsif @options[:tcp]
70
+ Updater::Update.socket = TCPSocket.new(@options[:host],@options[:tcp])
71
+ elsif @options[:remote]
72
+ raise NotImplimentedError #For future Authenticated Http Rest Server
73
+ end
74
+
75
+ #set PID
76
+ if File.exists? @options[:pid_file]
77
+ Updater::Update.pid = File.read(@options[:pid_file]).strip
78
+ end
79
+
54
80
 
55
81
  end
56
82
 
57
83
  private
58
84
 
85
+ def set_orm
86
+ #don't setup twice. Client setup might call this as part of server setup in which case it is already done
87
+ return false if Updater::Update.orm
88
+ orm = @options[:orm] || "datamapper"
89
+ case orm.downcase
90
+ when "datamapper"
91
+ require 'updater/orm/datamapper'
92
+ Updater::Update.orm = ORM::DataMapper
93
+ when "mongodb"
94
+ require 'updater/orm/mongodb'
95
+ Updater::Update.orm = ORM::MongoDB
96
+ when "activerecord"
97
+ require 'updater/orm/activerecord'
98
+ Updater::Update.orm = ORM::ActiveRecord
99
+ else
100
+ require "update/orm/#{orm}"
101
+ Updater::Update.orm = Object.const_get("ORM").const_get(orm.capitalize)
102
+ end
103
+ @logger.info "Data store '#{orm}' selected"
104
+ end
105
+
59
106
  def _start
60
107
  #set ORM
61
- require 'updater/orm/datamapper'
62
- Updater::Update.orm = ORM::DataMapper
63
-
108
+ set_orm
64
109
  #init DataStore
65
- DataMapper.logger = @logger
66
- DataMapper.setup(:default, :adapter=>'sqlite3', :database=>'./simulated.db')
67
-
110
+ default_options = {:adapter=>'sqlite3', :database=>'./default.db'}
111
+ Updater::Update.orm.setup((@options[:database] || @options[:orm_setup] || default_options).merge(:logger=>@logger))
68
112
  #load Models
69
113
 
70
114
  models = @options[:models] || Dir.glob('./app/models/**/*.rb')
@@ -73,11 +117,13 @@ module Updater
73
117
  end
74
118
 
75
119
  #establish Connections
120
+ @options[:host] ||= 'localhost'
76
121
  #Unix Socket -- name at @options[:socket]
77
122
  if @options[:socket]
78
123
  File.unlink @options[:socket] if File.exists? @options[:socket]
79
124
  @options[:sockets] ||= []
80
125
  @options[:sockets] << UNIXServer.new(@options[:socket])
126
+ @logger.info "Now listening on UNIX Socket: #{@options[:socket]}"
81
127
  end
82
128
 
83
129
  #UDP potentially unsafe user monitor server for Authenticated Connections (TODO)
@@ -86,28 +132,39 @@ module Updater
86
132
  udp = UDPSocket.new
87
133
  udp.bind(@options[:host],@options[:udp])
88
134
  @options[:sockets] << udp
135
+ @logger.info "Now listening for UDP: #{@options[:host]}:#{@options[:udp]}"
89
136
  end
90
137
 
91
138
  #TCP Unsafe user monitor server for Authenticated Connections (TODO)
92
139
  if @options[:tcp]
93
140
  @options[:sockets] ||= []
94
141
  @options[:sockets] << TCPServer.new(@options[:host],@options[:tcp])
142
+ @logger.info "Now listening for TCP: #{@options[:host]}:#{@options[:tcp]}"
95
143
  end
96
144
 
97
145
  #Log PID
98
146
  File.open(@options[:pid_file],'w') { |f| f.write(Process.pid.to_s)}
147
+
148
+ client_setup
149
+
99
150
  #start Worker
100
- require 'updater/fork_worker'
101
- worker_class = ForkWorker
151
+ worker = @options[:worker] || 'fork' #todo make this line windows safe
152
+ require "updater/#{worker}_worker"
153
+ worker_class = Updater.const_get("#{worker.capitalize}Worker")
102
154
  worker_class.logger = @logger
155
+ @logger.info "Using #{worker_class.to_s} to run jobs:"
103
156
  worker_class.start(@options)
157
+ File.unlink(@options[:pid_file])
158
+ File.unlink @options[:socket] if @options[:socket] && File.exists?(@options[:socket])
104
159
  end
105
160
 
106
161
  def load_file(file)
107
162
  return {} if file.nil?
108
163
  file = File.open(file) if file.kind_of?(String)
109
164
  @config_file = File.expand_path(file.path)
110
- YAML.load(ERB.new(File.read(file)).result(binding)) || {}
165
+ YAML.load(ERB.new(file.read).result(binding)) || {}
166
+ ensure
167
+ file.close
111
168
  end
112
169
  end
113
170
  end
@@ -22,7 +22,12 @@ module Updater
22
22
  ensure
23
23
  run_chain :success if ret
24
24
  run_chain :ensure
25
- @orm.destroy unless @orm.persistant
25
+ begin
26
+ @orm.destroy unless @orm.persistant
27
+ rescue DataObjects::ConnectionError
28
+ sleep 0.1
29
+ retry
30
+ end
26
31
  end
27
32
  ret
28
33
  end
@@ -33,7 +38,7 @@ module Updater
33
38
 
34
39
  def target
35
40
  target = @orm.finder.nil? ? @orm.target : @orm.target.send(@orm.finder,@orm.finder_args)
36
- raise TargetMissingError, "Class:'#{@orm.target}' Finder:'#{@orm.finder}', Args:'#{@orm.finder_args.inspect}'" unless target
41
+ raise TargetMissingError, "Target missing --Class:'#{@orm.target}' Finder:'#{@orm.finder}', Args:'#{@orm.finder_args.inspect}'" unless target
37
42
  target
38
43
  end
39
44
 
@@ -49,17 +54,23 @@ module Updater
49
54
  @orm.name
50
55
  end
51
56
 
52
- #This is the appropriate valut ot use for a chanable field value
57
+ #This is the appropriate value to use for a chanable field value
53
58
  def id
54
59
  @orm.id
55
60
  end
56
61
 
62
+ def ==(other)
63
+ id = other.id
64
+ end
65
+
57
66
  def persistant?
58
67
  @orm.persistant
59
68
  end
60
69
 
61
70
  def inspect
62
71
  "#<Updater::Update target=#{target.inspect} time=#{orm.time}>"
72
+ rescue TargetMissingError
73
+ "#<Updater::Update target=<missing> time=#{orm.time}>"
63
74
  end
64
75
 
65
76
  private
@@ -93,6 +104,9 @@ module Updater
93
104
  chains.each do |job|
94
105
  Update.new(job.target).run(self,job.params)
95
106
  end
107
+ rescue NameError
108
+ puts @orm.inspect
109
+ raise
96
110
  end
97
111
 
98
112
  class << self
@@ -100,6 +114,16 @@ module Updater
100
114
  #This attribute must be set to some ORM that will persist the data
101
115
  attr_accessor :orm
102
116
 
117
+ #remove once Bug is discovered
118
+ def orm=(input)
119
+ raise ArgumentError, "Must set ORM to and appropriate class" unless input.kind_of? Class
120
+ @orm = input
121
+ end
122
+
123
+ # This is an open IO socket that will be writen to when a job is scheduled. If it is unset
124
+ # then @pid is signaled instead.
125
+ attr_accessor :socket
126
+
103
127
  #Gets a single job form the queue, locks and runs it. it returns the number of second
104
128
  #Until the next job is scheduled, or 0 is there are more current jobs, or nil if there
105
129
  #are no jobs scheduled.
@@ -198,8 +222,12 @@ module Updater
198
222
  # Advanced: This method allows values to be passed directly to the ORM layer's create method.
199
223
  # use +at+ and friends for everyday use cases.
200
224
  def schedule(hash)
201
- new(@orm.create(hash))
225
+ r = new(@orm.create(hash))
202
226
  signal_worker
227
+ r
228
+ rescue NoMethodError
229
+ raise ArgumentError, "ORM not initialized!" if @orm.nil?
230
+ raise
203
231
  end
204
232
 
205
233
  # Create a new job having the same charistics as the old, except that 'hash' will override the original.
@@ -237,7 +265,9 @@ module Updater
237
265
  #
238
266
  # <Array[Updater]> unless name is given then only a single [Updater] instance.
239
267
  def for(target,name=nil)
240
- #TODO
268
+ target,finder,args = target_for(target)
269
+ ret = @orm.for(target,finder,args,name).map {|i| new(i)}
270
+ name ? ret.first : ret
241
271
  end
242
272
 
243
273
  #The time class used by Updater. See time=
@@ -286,6 +316,8 @@ module Updater
286
316
  #is set then an attempt will be made to signal the worker any
287
317
  #time a new update is made.
288
318
  #
319
+ #The PID will not be signaled if @socket is availible, but should be set as a back-up
320
+ #
289
321
  #If pid is not set, or is set to nil then the scheduleing program
290
322
  #is responcible for waking-up a potentially sleeping worker process
291
323
  #in another way.
@@ -295,6 +327,7 @@ module Updater
295
327
  Process::kill 0, @pid
296
328
  @pid
297
329
  rescue Errno::ESRCH, ArgumentError
330
+ @pid = nil
298
331
  raise ArgumentError, "PID was invalid"
299
332
  end
300
333
 
@@ -304,7 +337,9 @@ module Updater
304
337
 
305
338
  private
306
339
  def signal_worker
307
- if @pid
340
+ if @socket
341
+ @socket.write '.'
342
+ elsif @pid
308
343
  Process::kill "USR2", @pid
309
344
  end
310
345
  end
@@ -8,29 +8,49 @@ describe "named request" do
8
8
 
9
9
  before(:each) do
10
10
  Foo.all.destroy!
11
+ Update.clear_all
11
12
  end
12
13
 
13
14
  it "should be found by name when target is an instance" do
14
15
  f = Foo.create(:name=>'Honey')
15
16
  u = Update.immidiate(f,:bar,[:named],:name=>'Now')
16
17
  u.name.should ==("Now")
17
- pending "'for' not implemented"
18
18
  Update.for(f, "Now").should ==(u)
19
19
  end
20
20
 
21
21
  it "should be found by name when target is a class" do
22
22
  u = Update.immidiate(Foo,:bar,[:named],:name=>'Now')
23
23
  u.name.should ==("Now")
24
- pending "'for' not implemented"
25
24
  Update.for(Foo, "Now").should ==(u)
26
25
  end
27
26
 
28
27
  it "should return all updates for a given target" do
29
- u1 = Update.immidiate(Foo,:bar,[:arg1,:arg2])
28
+ u1 = Update.immidiate(Foo,:bar,[:arg1,:arg2], :name=>'First')
30
29
  u2 = Update.immidiate(Foo,:bar,[:arg3,:arg4])
31
- pending "'for' not implemented"
32
30
  Update.for(Foo).should include(u1,u2)
33
31
  end
34
-
32
+
33
+ #locked updates are already running and can therefore not be modified
34
+ it "should not include locked updates" do
35
+ u = Update.immidiate(Foo,:bar,[:named],:name=>'Now')
36
+ u.orm.lock(Struct.new(:name).new('test_worker'))
37
+ u.orm.should be_locked
38
+ Update.for(Foo).should_not include(u)
39
+ Update.for(Foo).should be_empty
40
+ end
41
+
42
+ it "should not return rusults with the wrong name" do
43
+ u = Update.immidiate(Foo,:bar,[:named],:name=>'Now')
44
+ u.name.should ==("Now")
45
+ Update.for(Foo, "Then").should be_nil
46
+ end
47
+
48
+ it "should not return results for the wring target" do
49
+ f = Foo.create(:name=>'Honey')
50
+ g = Foo.create(:name=>'Sweetie Pie')
51
+ u = Update.immidiate(f,:bar,[:named],:name=>'Now')
52
+ Update.for(f).should include(u)
53
+ Update.for(g).should be_empty
54
+ end
35
55
 
36
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: updater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John F. Miller
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-18 00:00:00 -08:00
12
+ date: 2010-03-26 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency