updater 0.9.2 → 0.9.3

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.2
1
+ 0.9.3
data/bin/updater CHANGED
@@ -1,18 +1,111 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- puts "starting update deamon..."
3
+ require 'optparse'
4
4
 
5
- require 'rubygems'
6
- require 'yaml'
7
- require 'updater'
8
- equire 'updater/worker'
5
+ OPERATIONS = %w{start stop noop} # restart}
6
+ WORKERS = %w{fork thread}
9
7
 
8
+ options = {}
10
9
 
11
- dbconfig = YAML.load_file('config/database.yml')
10
+ opts = OptionParser.new do |opts|
11
+ opts.banner = <<EOF
12
+ Updater: Ruby Job Processor. Copyright John F. Miller 2009-2010
12
13
 
13
- DataMapper.setup(dbconfig['development'])
14
+ Usage: updater (#{OPERATIONS.join('|')}) [options]"
15
+ Start of stop the Updater job queue processor.
16
+ EOF
17
+ opts.on("-l","--local", "Use local 'lib' directory instead of Gems.") do
18
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
19
+ end
20
+
21
+ opts.on("-c","--config-file [FILE]", "File containing configuration data.", " default: ENV['UPDATE_CONFIG'] || updater.config") do |f|
22
+ options[:config_file] = f
23
+ end
24
+
25
+ opts.on("--log-file [FILE]", "Where to send the log output.", " default: STDOUT") do |f|
26
+ options[:log_file] = f
27
+ end
28
+
29
+ opts.on("-d","--debug", "Output lots of extra data to the log file") do
30
+ options[:log_level] = "DEBUG"
31
+ end
32
+
33
+ opts.on("-w","--worker WORKERTYPE", "(#{WORKERS.join('|')}) type of worker to use", " default: fork (must be set to 'thread' for Windows)") do |w|
34
+ unless WORKERS.include? w
35
+ puts "** Invalid worker type **\n\n",opts
36
+ exit -1
37
+ end
38
+ options[:worker] = w
39
+ end
40
+
41
+ opts.on("-m","--monitor", "Start/Stop the HTTP queue monitor", " Not yet implimented") do #TODO
42
+ puts "HTTP Monitor Not Yet Implimented."
43
+ exit -1
44
+ end
45
+
46
+ opts.separator "\n -- IPC Options: These options will need to be matched by the client --\n"
47
+ opts.on('-p','--pid-file FILE', 'The name of the PID file.') do |f|
48
+ options[:pid_file] = f
49
+ end
50
+
51
+ opts.on('-s','--unix-socket FILE', 'Socket to be used for UNIX Socket IPC.') do |f|
52
+ options[:socket] = f
53
+ end
54
+
55
+ opts.on('--host [HOST]', 'For UDC and TCP the host name. (See Security section in README)', ' default: localhost') do |h|
56
+ options[:host] = 'localhost'
57
+ end
58
+
59
+ opts.on('-u','--udp PORT', 'Port to send/recieve UDP messages. (See Security section in README)') do |p|
60
+ options[:udp] = p
61
+ end
62
+
63
+ opts.on('-t','--tcp PORT', 'Port to send/recieve tcp messages. (See Security section in README)') do |p|
64
+ options[:tcp] = p
65
+ end
66
+
67
+ opts.on('-r','--remote-http PORT', 'Port to send/recieve HTTP service requests.', ' Not Yet Implimented') do |p|
68
+ options[:tcp] = p
69
+ end
70
+
71
+
72
+ opts.separator "\n -- Additional Information --\n"
73
+ opts.on_tail("-v", "--version", "Show Version information.") do
74
+ options[:version] = true
75
+ end
76
+
77
+ opts.on_tail("-h", "--help", "show this message.") do
78
+ puts opts
79
+ exit
80
+ end
81
+ end
82
+
83
+ opts.parse!
84
+
85
+ require 'updater' #must wait for -l option
86
+
87
+ if options[:version]
88
+ puts(<<EOF)
89
+ Updater: Ruby Job Processor. Version #{Updater::VERSION}
90
+ Copyright John F. Miller 2009-2010
91
+ EOF
92
+ exit
93
+ end
94
+
95
+ operation = ARGV.shift
96
+
97
+ operation = operation.downcase if operation
98
+
99
+ unless OPERATIONS.include? operation
100
+ puts "#{operation}: operation not supported" if operation
101
+ puts opts
102
+ exit
103
+ end
104
+
105
+ require 'updater/setup'
106
+
107
+ Updater::Setup.send(operation,options)
14
108
 
15
- Worker.new.start
16
109
 
17
110
 
18
111
 
@@ -76,7 +76,7 @@ module Updater
76
76
  end
77
77
 
78
78
  define_method mode do
79
- chains.all(:occasion=>mode)
79
+ chains.all(:occasion=>mode).map {|job| Updater.new(i.target).tap {|u| u.params = job.params}}
80
80
  end
81
81
  end
82
82
 
@@ -0,0 +1,276 @@
1
+ require 'mongo'
2
+ require 'active_support/inflector' #to get classes from strings
3
+ require 'active_support/core_ext/object/try'
4
+
5
+ module Updater
6
+ module ORM
7
+ class Mongo
8
+
9
+ FINDER= :get
10
+ ID=:_id
11
+
12
+ def initialize(hash = {})
13
+ @hash = {}
14
+ hash.each do |key, val|
15
+ if respond_to? "#{key}="
16
+ send("#{key}=", val)
17
+ else
18
+ @hash[key] = val
19
+ end
20
+ end
21
+ end
22
+
23
+ %w{time finder finder_args method method_args name persistant lock_name}.each do |field|
24
+ eval(<<-EOF) #,__LINE__+1,__FILE__)
25
+ def #{field};@hash['#{field}'];end
26
+ def #{field}=(value);@hash['#{field}'] = value;end
27
+ EOF
28
+ end
29
+
30
+ def _id
31
+ @hash['_id'] || @hash[:_id]
32
+ end
33
+
34
+ def _id=(val)
35
+ val = BSON::ObjectID.from_string(val.to_s) unless val.kind_of? BSON::ObjectID
36
+ @hash[:_id] = val
37
+ end
38
+
39
+ alias :id :_id
40
+ alias :'id=' :'_id='
41
+
42
+ def target
43
+ @hash['target'].try :constantize
44
+ end
45
+
46
+ def target=(value)
47
+ @hash['target'] = value.to_s
48
+ end
49
+
50
+ def save
51
+ #todo validation
52
+ [:failure,:success,:ensure].each do |mode|
53
+ next unless @hash[mode]
54
+ @hash[mode] = @hash[mode].map do |job|
55
+ if job.kind_of? Updater::Update
56
+ job.save unless job.id
57
+ job = job.id
58
+ end
59
+ job
60
+ end
61
+ end
62
+ _id = self.class.collection.save @hash
63
+ end
64
+
65
+ def destroy
66
+ self.class.collection.remove({:_id=>id})
67
+ end
68
+
69
+ def [](arg) #this allows easy mapping for time when a value coud either be U::ORM::Mongo or an ordered hash
70
+ @hash[arg]
71
+ end
72
+
73
+ def lock(worker)
74
+ raise NotImplimentedError, "Use lock_next"
75
+ end
76
+
77
+ # Non API Standard. This method returns the collection used by this instance.
78
+ # This is used to create Job Chains
79
+ def collection
80
+ self.class.instance_variable_get(:@collection)
81
+ end
82
+
83
+ #key :time, Integer, :numeric=>true
84
+ #key :target, String, :required => true
85
+ #key :finder, String
86
+ # key :finder_args, Array
87
+ #key :method, String :required => true
88
+ #key :method_args, String :required => true
89
+ #key :name
90
+ # key :persistant
91
+ #key :lock_name
92
+
93
+ %w{failure success ensure}.each do |mode|
94
+ eval(<<-EOF, binding ,__FILE__, __LINE__+1)
95
+ def #{mode}
96
+ @#{mode} ||= init_chain(:#{mode})
97
+ end
98
+
99
+ def #{mode}=(chain)
100
+ chain = [chain] unless chain.kind_of? Array
101
+ @#{mode} , @hash[:#{mode}] = build_chain_arrays(chain)
102
+ attach_intellegent_insertion(@#{mode},:#{mode},self) if @#{mode}
103
+ end
104
+ EOF
105
+ end
106
+
107
+ private
108
+ # this method is calld from he chain asignment methods eg. failure=(chain)
109
+ # chain is an array which may contain BSON::ObjectID's or Updater::Update's or both
110
+ # For BSON::ObjectID's we cannot initialize them as this could leed to infinate loops.
111
+ # (an object pool would solve this problem, but feels like overkill)
112
+ # The final result must be a @hash containing all the BSON::ObjectID' (forign keys)
113
+ # and possibly @failure containting all instanciated UpdaterUpdates read to be called
114
+ # or @failure set to nil with the chain instanciated on first use.
115
+ def build_chain_arrays(arr, build = false)
116
+ build ||= arr.any? {|j| k,_ = j; Updater::Update === k || Hash === k}
117
+ output = arr.inject({:ids=>[],:instances=>[]}) do |accl,j|
118
+ inst, id = rationalize_instance(j)
119
+ if inst.nil? && build
120
+ real_id,params = id #id could be an array
121
+ inst = Updater::Update.new(self.class.new(collection.find_one(real_id)))
122
+ inst.params = params #set params to the second element of the id array. If id is scale then set to nil.
123
+ end
124
+ accl[:ids] << id || inst #id will be nil only if inst has not ben saved.
125
+ accl[:instances] << inst if inst
126
+ accl
127
+ end
128
+ if build
129
+ return [output[:instances],output[:ids]]
130
+ end
131
+ [nil,output[:ids]]
132
+ end
133
+
134
+ # This method takes something that may be a reference to an instance(BSON::ObjectID/String),
135
+ # an instance its self (Updater::Update), or a Hash
136
+ # and returns a touple of the Updater::Update,BSON::ObjectID.
137
+ # This method will bot instanciate object from BSON::ObjectID's
138
+ # nor will it save Hashes inorder to obtain an ID (it will creat a new Updater::Update from the hash).
139
+ # Instead it will return nil in the appropriate place.
140
+ def rationalize_instance(val)
141
+ val = BSON::ObjectID.fron_string(val) if val.kind_of? String
142
+ case val #aval is the actual runable object, hval is a BSON::ObjectID that we can put into the Database
143
+ when Updater::Update
144
+ val.params ? [val,[val.id,val.params]] : [val,val.id]
145
+ when Hash
146
+ [Updater::Update.new(val),val['_id']]
147
+ when BSON::ObjectID
148
+ [nil,val]
149
+ when Array
150
+ rationalize_instance(val[0]).tap do |ret|
151
+ ret[0].params = val[1] if ret[0]
152
+ ret[1] = [ret[1],val[1]]
153
+ end
154
+ end
155
+ end
156
+
157
+ def attach_intellegent_insertion(arr,mode,parent)
158
+ arr.define_singleton_method '<<' do |val|
159
+ inst, id = rationalize_instance(val)
160
+ inst = Updater::Update.new(self.class.new(parent.collection.find_one(id))) unless inst
161
+ parent.instance_variable_get(:@hash)[mode] ||= []
162
+ parent.instance_variable_get(:@hash)[mode] << id || inst
163
+ super inst
164
+ end
165
+ arr
166
+ end
167
+
168
+ def init_chain(mode)
169
+ ret, @hash[mode] = build_chain_arrays(@hash[mode] || [],true)
170
+ attach_intellegent_insertion(ret,mode,self)
171
+ end
172
+
173
+ class << self
174
+ attr_accessor :db, :collection, :logger
175
+
176
+ # Availible options:
177
+ # * :database - *required* either an established Mongo::DB database OR the name of the database
178
+ # * :collection - which collection to store jobs in. Default: "updater"
179
+ #
180
+ # If a connection to the database must be established (ie :database is not a Mongo::DB)
181
+ # these options may be used to establish that connection.
182
+ # * :host - the host to connect to. Default: "localhost"
183
+ # * :port - the port to connect to. Default: 27017
184
+ # * :username/:password - if these are present, they will be used to authenticate against the database
185
+ def setup(options)
186
+ logger ||= options[:logger]
187
+ raise ArgumentError, "Must spesify the name of a databas when setting up Mongo driver" unless options[:database]
188
+ if options[:database].kind_of? ::Mongo::DB
189
+ @db = options[:database]
190
+ else
191
+ logger.info "Attempting to connect to mongodb at #{[options[:host] || "localhost", options[:port] || 27017].join(':')} database: \"#{options[:database]}\""
192
+ @db = ::Mongo::Connection.new(options[:host] || "localhost", options[:port] || 27017).db(options[:database].to_s)
193
+ if options[:username] && options[:password]
194
+ success = db.authenticate(options[:username] , options[:password])
195
+ raise RunTimeError, "Could not Authenticate with MongoDb \"#{options[:database]}\" Please check the username and password."
196
+ end
197
+ end
198
+ collection_name = options[:collection] || 'updater'
199
+ unless db.collection_names.include? collection_name
200
+ logger.warn "Updater MongoDB Driver is creating a new collection, \"#{collection_name}\" in \"#{options[:database]}\""
201
+ end
202
+ @collection = db.collection(collection_name)
203
+ end
204
+
205
+ def before_fork
206
+ @db.connection.close
207
+ end
208
+
209
+ def after_fork
210
+
211
+ end
212
+
213
+ def lock_next(worker)
214
+ hash = Hash.new
215
+ hash['findandmodify'] =@collection.name
216
+ hash['query'] = {:time=>{'$lte'=>tnow},:lock_name=>nil}
217
+ hash['sort'] =[[:time,'ascending']] #oldest first
218
+ hash['update'] = {'$set'=>{:lock_name=>worker.name}}
219
+ hash['new'] = true
220
+
221
+
222
+ ret = @db.command hash, :check_response=>false
223
+ return nil unless ret['ok'] == 1
224
+ return new(ret['value'])
225
+ end
226
+
227
+ def get(id)
228
+ id = BSON::ObjectID.from_string(id) if id.kind_of? String
229
+ new(@collection.find_one(id))
230
+ end
231
+
232
+ def current
233
+ raise NotImplementedError, "Mongo does not support lazy evaluation"
234
+ end
235
+
236
+ def current_load
237
+ @collection.find(:time=>{'$lte'=>tnow}, :lock_name=>nil).count
238
+ end
239
+
240
+ def delayed
241
+ @collection.find(:time=>{'$gt'=>tnow}).count
242
+ end
243
+
244
+ def future(start, finish)
245
+ @collection.find(:time=>{'$gt'=>start+tnow,'$lt'=>finish+tnow}).count
246
+ end
247
+
248
+ def queue_time
249
+ nxt = @collection.find_one({:lock_name=>nil, :time=>{'$ne'=>nil}}, :sort=>[[:time, :asc]], :fields=>[:time])
250
+ return nil unless nxt
251
+ return 0 if nxt['time'] <= tnow
252
+ return nxt['time'] - tnow
253
+ end
254
+
255
+ def create(hash)
256
+ ret = new(hash)
257
+ ret.save and ret
258
+ end
259
+
260
+ def clear_all
261
+ @collection.remove
262
+ end
263
+
264
+ def clear_locks(worker)
265
+ @collection.update({:lock_name=>worker.name},{'$unset'=>{:lock_name=>1}},:multi=>true)
266
+ end
267
+
268
+ private
269
+ def tnow
270
+ Updater::Update.time.now.to_i
271
+ end
272
+ end
273
+
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,188 @@
1
+ require 'mongo'
2
+ require 'active_support/inflector' #to get classes from strings
3
+ require 'active_support/core_ext/object/try'
4
+
5
+ module Updater
6
+ module ORM
7
+ class Mongo
8
+
9
+ FINDER= :get
10
+ ID=:_id
11
+
12
+ def initialize(hash = {})
13
+ @hash = {}
14
+ hash.each do |key, val|
15
+ if respond_to? "#{key}="
16
+ send("#{key}=", val)
17
+ else
18
+ @hash[key] = val
19
+ end
20
+ end
21
+ end
22
+
23
+ %w{time finder finder_args method method_args name persistance lock_name}.each do |field|
24
+ eval(<<-EOF) #,__LINE__+1,__FILE__)
25
+ def #{field};@hash['#{field}'];end
26
+ def #{field}=(value);@hash['#{field}'] = value;end
27
+ EOF
28
+ end
29
+
30
+ def _id
31
+ @hash['_id'] || @hash[:_id]
32
+ end
33
+
34
+ def _id=(val)
35
+ val = BSON::ObjectID.from_string(val.to_s) unless val.kind_of? BSON::ObjectID
36
+ @hash[:_id] = val
37
+ end
38
+
39
+ alias :id :_id
40
+ alias :'id=' :'_id='
41
+
42
+ def target
43
+ @hash['target'].try :constantize
44
+ end
45
+
46
+ def target=(value)
47
+ @hash['target'] = value.to_s
48
+ end
49
+
50
+ def save
51
+ #todo validation
52
+ self.class.collection.save @hash
53
+ end
54
+
55
+ def destroy
56
+ @collection.remove({:_id=>id})
57
+ end
58
+
59
+ def [](arg) #this allows easy mapping for time when a value coud either be U::ORM::Mongo or an ordered hash
60
+ @hash[arg]
61
+ end
62
+
63
+ #key :time, Integer, :numeric=>true
64
+ #key :target, String, :required => true
65
+ #key :finder, String
66
+ # key :finder_args, Array
67
+ #key :method, String :required => true
68
+ #key :method_args, String :required => true
69
+ #key :name
70
+ # key :persistant
71
+ #key :lock_name
72
+
73
+ %w{failure success ensure}.each do |mode|
74
+ eval(<<-EOF) #,__FILE__,__LINE__+1)
75
+ def #{mode}
76
+ @#{mode} ||= init_chain(:#{mode})
77
+ end
78
+ EOF
79
+ end
80
+
81
+ def init_chain(mode)
82
+ ret = [@hash[mode.to_s] || []].flatten
83
+ unless ret.empty?
84
+ ret = @collection.find(:_id=>{'$in'=>ret}).map {|i| self.class.new(i)}
85
+ end
86
+ ret.define_singleton_method '<<' do |val|
87
+ val = BSON::ObjectID.fron_string(val) if val.kind_of? String
88
+ aval,hval = case val
89
+ when self.class
90
+ [val,val.id]
91
+ when Hash
92
+ [self.class.new(val),val['_id']]
93
+ when BSON::ObjectID
94
+ [@collection.find_one(val),val]
95
+ end
96
+ @hash[mode] ||= []
97
+ @hash[mode] << hval
98
+ super aval
99
+ end
100
+ end
101
+
102
+ def lock(worker)
103
+ raise NotImplimentedError, "Use lock_next"
104
+ end
105
+
106
+ class << self
107
+ attr_accessor :db, :collection, :logger
108
+
109
+ # Availible options:
110
+ # * :database - the name of the database *required*
111
+ # * :host - the host to connect to. Default: "localhost"
112
+ # * :port - the port to connect to. Default: 27017
113
+ # * :collection - which collection to store jobs in. Default: "Updater"
114
+ # * :username/:password - if these are present, they will be used to authenticate against the database
115
+ def setup(options)
116
+ logger = options[:logger]
117
+ raise ArgumentError, "Must spesify the name of a databas when setting up Mongo driver" unless options[:database]
118
+ logger.info "Attempting to connect to mongodb at #{[options[:host] || "localhost", options[:port] || 27017}
119
+ @db = ::Mongo::Connection.new(options[:host] || "localhost", options[:port] || 27017).db(options[:database].to_s)
120
+ if options[:username] && options[:password]
121
+ success = db.authenticate(options[:username] , options[:password])
122
+ raise RunTimeError, "Could not Authenticate with MongoDb \"#{options[:database]}\" Please check the username and password."
123
+ end
124
+ collection_name = options[:collection] || 'updater'
125
+ unless db.collection_names.include collection_name
126
+ logger.warn "Updater MongoDB Driver is creating a new collection, \"#{collection_name}\" in \"#{options[:database]}\""
127
+ end
128
+ @collection = db.collection(collection_name)
129
+ end
130
+
131
+ def lock_next(worker)
132
+ hash = OrderedHash.new
133
+ hash['findandmodify'] =@collection.name
134
+ hash['query'] = {:time=>{'$lte'=>tnow},:lock_name=>nil}
135
+ hash['sort'] =[[:time,'ascending']] #oldest first
136
+ hash['update'] = {'$set'=>{:lock_name=>worker.name}}
137
+ hash['new'] = true
138
+
139
+ ret = @db.command hash
140
+ return nil unless ret['ok'] == 1
141
+ return new(ret['value'])
142
+ end
143
+
144
+ def get(id)
145
+ id = BSON::ObjectID.from_string(id) if id.kind_of? String
146
+ new(@collection.find_one(id))
147
+ end
148
+
149
+ def current
150
+ raise NotImplementedError, "Mongo does not support lazy evaluation"
151
+ end
152
+
153
+ def current_load
154
+ @collection.find(:time=>{'$lte'=>tnow}).count
155
+ end
156
+
157
+ def delayed
158
+ @collection.find(:time=>{'$gt'=>tnow}).count
159
+ end
160
+
161
+ def future(start, finish)
162
+ @collection.find(:time=>{'$gt'=>start+tnow,'$lt'=>finish+tnow}).count
163
+ end
164
+
165
+ def queue_time
166
+ nxt = @collection.find_one({:time=>{'$gt'=>3,'$lt'=>4}, :lock_name=>'foobar'}, :sort=>[[:time, :asc]], :fields=>[:time])
167
+ return nil unless nxt
168
+ return 0 if nxt['time'] <= tnow
169
+ return nxt['time'] - tnow
170
+ end
171
+
172
+ def create(hash)
173
+ new(hash).save
174
+ end
175
+
176
+ def clear_all
177
+ @collection.remove
178
+ end
179
+
180
+ private
181
+ def tnow
182
+ Updater::Update.time.now.to_i
183
+ end
184
+ end
185
+
186
+ end
187
+ end
188
+ end
@@ -106,7 +106,7 @@ module Updater
106
106
  class Base
107
107
 
108
108
  # Every ORM should set this constant to a symbol that matches the most
109
- # obvious method used to retrive a known instance. :get or:find are likely
109
+ # obvious method used to retrive a known instance. :get or :find are likely
110
110
  # candidates.
111
111
  FINDER= nil
112
112
 
data/lib/updater/setup.rb CHANGED
@@ -6,24 +6,30 @@ require 'erb'
6
6
  module Updater
7
7
  class Setup
8
8
  class << self
9
- def start
10
- new(config_file).start
9
+ def start(options={})
10
+ new(config_file(options), options).start
11
11
  end
12
12
 
13
- def stop
14
- new(config_file).stop
13
+ def stop(options={})
14
+ new(config_file(options), options).stop
15
+ end
16
+
17
+ def noop(options={})
18
+ new(config_file(options), options).noop
15
19
  end
16
20
 
17
21
  def client_setup(options = {})
18
- new(config_file, options).client_setup
22
+ new(config_file(options), options).client_setup
19
23
  end
20
24
 
21
25
  def monitor
22
26
 
23
27
  end
24
28
 
25
- def config_file
26
- if ENV['UPDATE_CONFIG'] && File.exists(ENV['UPDATE_CONFIG'])
29
+ def config_file(options = {})
30
+ if options[:config_file] && File.exists?(options[:config_file])
31
+ options[:config_file]
32
+ elsif ENV['UPDATE_CONFIG'] && File.exists(ENV['UPDATE_CONFIG'])
27
33
  ENV['UPDATE_CONFIG']
28
34
  else
29
35
  (Dir.glob('{config,.}/updater.config') + Dir.glob('.updater')).first
@@ -36,12 +42,14 @@ module Updater
36
42
  #extended used for clients who wnat to override parameters
37
43
  def initialize(file_or_hash, extended = {})
38
44
  @options = file_or_hash.kind_of?(Hash) ? file_or_hash : load_file(file_or_hash)
39
- @options.merge(extended)
45
+ @options.merge!(extended)
40
46
  @options[:pid_file] ||= File.join(ROOT,'updater.pid')
41
47
  @options[:host] ||= "localhost"
42
48
  @logger = @options[:logger] || Logger.new(@options[:log_file] || STDOUT)
43
49
  level = Logger::SEV_LABEL.index(@options[:log_level].upcase) if @options[:log_level]
44
- @logger.level = level || Logger::WARN
50
+ @logger.level = level || Logger::WARN unless @options[:logger] #only set this if we were not handed a logger
51
+ @logger.debug "Debugging output enabled"
52
+ Update.logger = @logger
45
53
  end
46
54
 
47
55
  def start
@@ -54,13 +62,26 @@ module Updater
54
62
 
55
63
  def stop
56
64
  Process.kill("TERM",File.read(@options[:pid_file]).to_i)
65
+ sleep 1.0
66
+ end
67
+
68
+ def noop
69
+ @logger.warn "NOOP: will not start service"
70
+ set_orm
71
+ init_orm
72
+ load_models
73
+ client_setup
74
+ @logger.debug @options.inspect
75
+ exit
57
76
  end
58
77
 
59
- # The client is responcible for loading classes and making connections. We will simply setup the Updater spesifics
78
+ # The client is responcible for loading classes and making connections. We will simply setup the Updater spesifics.
60
79
  def client_setup
80
+ @logger.info "Updater Client is being initialized..."
61
81
  set_orm
62
82
 
63
83
  if @options[:socket] && File.exists?(@options[:socket])
84
+ @logger.debug "Using UNIX Socket \"#{@options[:socket]}\""
64
85
  Updater::Update.socket = UNIXSocket.new(@options[:socket])
65
86
  elsif @options[:udp]
66
87
  socket = UDPSocket.new()
@@ -86,13 +107,13 @@ module Updater
86
107
  #don't setup twice. Client setup might call this as part of server setup in which case it is already done
87
108
  return false if Updater::Update.orm
88
109
  orm = @options[:orm] || "datamapper"
89
- case orm.downcase
110
+ case orm.to_s.downcase
90
111
  when "datamapper"
91
112
  require 'updater/orm/datamapper'
92
113
  Updater::Update.orm = ORM::DataMapper
93
114
  when "mongodb"
94
- require 'updater/orm/mongodb'
95
- Updater::Update.orm = ORM::MongoDB
115
+ require 'updater/orm/mongo'
116
+ Updater::Update.orm = ORM::Mongo
96
117
  when "activerecord"
97
118
  require 'updater/orm/activerecord'
98
119
  Updater::Update.orm = ORM::ActiveRecord
@@ -103,18 +124,31 @@ module Updater
103
124
  @logger.info "Data store '#{orm}' selected"
104
125
  end
105
126
 
106
- def _start
107
- #set ORM
108
- set_orm
109
- #init DataStore
127
+ def init_orm
110
128
  default_options = {:adapter=>'sqlite3', :database=>'./default.db'}
111
129
  Updater::Update.orm.setup((@options[:database] || @options[:orm_setup] || default_options).merge(:logger=>@logger))
112
- #load Models
113
-
130
+ end
131
+
132
+ def load_models
133
+ @logger.info "Loading Models..."
114
134
  models = @options[:models] || Dir.glob('./app/models/**/*.rb')
115
135
  models.each do |file|
116
- require file
136
+ @logger.debug " - loading file: #{file}"
137
+ begin
138
+ require file
139
+ rescue LoadError
140
+ require File.expand_path(File.join(Dir.pwd,file))
141
+ end
117
142
  end
143
+ end
144
+
145
+ def _start
146
+ #set ORM
147
+ set_orm
148
+ #init DataStore
149
+ init_orm
150
+ #load Models
151
+ load_models
118
152
 
119
153
  #establish Connections
120
154
  @options[:host] ||= 'localhost'
@@ -164,7 +198,7 @@ module Updater
164
198
  @config_file = File.expand_path(file.path)
165
199
  YAML.load(ERB.new(file.read).result(binding)) || {}
166
200
  ensure
167
- file.close
201
+ file.close if file.kind_of?(IO) && !file.closed?
168
202
  end
169
203
  end
170
- end
204
+ end
@@ -0,0 +1,175 @@
1
+ require 'logger'
2
+ require 'yaml'
3
+ require 'socket'
4
+ require 'erb'
5
+
6
+ module Updater
7
+ class Setup
8
+ class << self
9
+ def start
10
+ new(config_file).start
11
+ end
12
+
13
+ def stop
14
+ new(config_file).stop
15
+ end
16
+
17
+ def client_setup(options = {})
18
+ new(config_file, options).client_setup
19
+ end
20
+
21
+ def monitor
22
+
23
+ end
24
+
25
+ def config_file(options = {})
26
+ if options[:config_file] && File.exists(options[:config_file])
27
+ option[:config_file]
28
+ elsif ENV['UPDATE_CONFIG'] && File.exists(ENV['UPDATE_CONFIG'])
29
+ ENV['UPDATE_CONFIG']
30
+ else
31
+ (Dir.glob('{config,.}/updater.config') + Dir.glob('.updater')).first
32
+ end
33
+ end
34
+ end
35
+
36
+ ROOT = File.dirname(self.config_file || Dir.pwd)
37
+
38
+ #extended used for clients who wnat to override parameters
39
+ def initialize(file_or_hash, extended = {})
40
+ @options = file_or_hash.kind_of?(Hash) ? file_or_hash : load_file(file_or_hash)
41
+ @options.merge!(extended)
42
+ @options[:pid_file] ||= File.join(ROOT,'updater.pid')
43
+ @options[:host] ||= "localhost"
44
+ @logger = @options[:logger] || Logger.new(@options[:log_file] || STDOUT)
45
+ level = Logger::SEV_LABEL.index(@options[:log_level].upcase) if @options[:log_level]
46
+ @logger.level = level || Logger::WARN unless @options[:logger] #only set this if we were not handed a logger
47
+ end
48
+
49
+ def start
50
+ pid = Process.fork do
51
+ _start
52
+ end
53
+ @logger.warn "Successfully started Master Loop at pid #{pid}"
54
+ puts "Job Queue Processor Started at PID: #{pid}"
55
+ end
56
+
57
+ def stop
58
+ Process.kill("TERM",File.read(@options[:pid_file]).to_i)
59
+ end
60
+
61
+ # The client is responcible for loading classes and making connections. We will simply setup the Updater spesifics
62
+ def client_setup
63
+ @logger.info "Updater Client is being initialized..."
64
+ set_orm
65
+
66
+ if @options[:socket] && File.exists?(@options[:socket])
67
+ @logger.debug "Using UNIX Socket \"#{@options[:socket]}\""
68
+ Updater::Update.socket = UNIXSocket.new(@options[:socket])
69
+ elsif @options[:udp]
70
+ socket = UDPSocket.new()
71
+ socket.connect(@options[:host],@options[:udp])
72
+ Updater::Update.socket = socket
73
+ elsif @options[:tcp]
74
+ Updater::Update.socket = TCPSocket.new(@options[:host],@options[:tcp])
75
+ elsif @options[:remote]
76
+ raise NotImplimentedError #For future Authenticated Http Rest Server
77
+ end
78
+
79
+ #set PID
80
+ if File.exists? @options[:pid_file]
81
+ Updater::Update.pid = File.read(@options[:pid_file]).strip
82
+ end
83
+
84
+
85
+ end
86
+
87
+ private
88
+
89
+ def set_orm
90
+ #don't setup twice. Client setup might call this as part of server setup in which case it is already done
91
+ return false if Updater::Update.orm
92
+ orm = @options[:orm] || "datamapper"
93
+ @logger.info "Updater setting ORM to \"#{orm}\""
94
+ case orm.to_s.downcase
95
+ when "datamapper"
96
+ require 'updater/orm/datamapper'
97
+ Updater::Update.orm = ORM::DataMapper
98
+ when "mongodb"
99
+ require 'updater/orm/mongo'
100
+ Updater::Update.orm = ORM::Mongo
101
+ when "activerecord"
102
+ require 'updater/orm/activerecord'
103
+ Updater::Update.orm = ORM::ActiveRecord
104
+ else
105
+ require "update/orm/#{orm}"
106
+ Updater::Update.orm = Object.const_get("ORM").const_get(orm.capitalize)
107
+ end
108
+ @logger.info "Data store '#{orm}' selected"
109
+ end
110
+
111
+ def _start
112
+ #set ORM
113
+ set_orm
114
+ #init DataStore
115
+ default_options = {:adapter=>'sqlite3', :database=>'./default.db'}
116
+ Updater::Update.orm.setup((@options[:database] || @options[:orm_setup] || default_options).merge(:logger=>@logger))
117
+ #load Models
118
+
119
+ models = @options[:models] || Dir.glob('./app/models/**/*.rb')
120
+ models.each do |file|
121
+ require file
122
+ end
123
+
124
+ #establish Connections
125
+ @options[:host] ||= 'localhost'
126
+ #Unix Socket -- name at @options[:socket]
127
+ if @options[:socket]
128
+ File.unlink @options[:socket] if File.exists? @options[:socket]
129
+ @options[:sockets] ||= []
130
+ @options[:sockets] << UNIXServer.new(@options[:socket])
131
+ @logger.info "Now listening on UNIX Socket: #{@options[:socket]}"
132
+ end
133
+
134
+ #UDP potentially unsafe user monitor server for Authenticated Connections (TODO)
135
+ if @options[:udp]
136
+ @options[:sockets] ||= []
137
+ udp = UDPSocket.new
138
+ udp.bind(@options[:host],@options[:udp])
139
+ @options[:sockets] << udp
140
+ @logger.info "Now listening for UDP: #{@options[:host]}:#{@options[:udp]}"
141
+ end
142
+
143
+ #TCP Unsafe user monitor server for Authenticated Connections (TODO)
144
+ if @options[:tcp]
145
+ @options[:sockets] ||= []
146
+ @options[:sockets] << TCPServer.new(@options[:host],@options[:tcp])
147
+ @logger.info "Now listening for TCP: #{@options[:host]}:#{@options[:tcp]}"
148
+ end
149
+
150
+ #Log PID
151
+ File.open(@options[:pid_file],'w') { |f| f.write(Process.pid.to_s)}
152
+
153
+ client_setup
154
+
155
+ #start Worker
156
+ worker = @options[:worker] || 'fork' #todo make this line windows safe
157
+ require "updater/#{worker}_worker"
158
+ worker_class = Updater.const_get("#{worker.capitalize}Worker")
159
+ worker_class.logger = @logger
160
+ @logger.info "Using #{worker_class.to_s} to run jobs:"
161
+ worker_class.start(@options)
162
+ File.unlink(@options[:pid_file])
163
+ File.unlink @options[:socket] if @options[:socket] && File.exists?(@options[:socket])
164
+ end
165
+
166
+ def load_file(file)
167
+ return {} if file.nil?
168
+ file = File.open(file) if file.kind_of?(String)
169
+ @config_file = File.expand_path(file.path)
170
+ YAML.load(ERB.new(file.read).result(binding)) || {}
171
+ ensure
172
+ file.close if file.kind_of?(IO) && !file.closed?
173
+ end
174
+ end
175
+ end
@@ -7,7 +7,6 @@ module Updater
7
7
 
8
8
  #This class repeatedly searches the database for active jobs and runs them
9
9
  class ThreadWorker
10
- cattr_accessor :logger
11
10
  attr_accessor :pid
12
11
  attr_accessor :name
13
12
 
@@ -89,4 +88,4 @@ module Updater
89
88
  end
90
89
  end
91
90
 
92
- end
91
+ end
@@ -0,0 +1,92 @@
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 ThreadWorker
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 = run_job_loop
23
+
24
+ trap('TERM') { terminate_with @t }
25
+ trap('INT') { terminate_with @t }
26
+
27
+ trap('USR1') do
28
+ old_proc = trap('USR1','IGNORE')
29
+ run_loop
30
+ trap('USR1',old_proc)
31
+ end
32
+
33
+ Thread.pass
34
+
35
+ sleep unless $exit
36
+ end
37
+
38
+ def say(text)
39
+ puts text unless @quiet
40
+ logger.info text if logger
41
+ end
42
+
43
+ def stop
44
+ raise RuntimeError unless @t
45
+ terminate_with @t
46
+ end
47
+
48
+ def run_loop
49
+ if @t.alive?
50
+ @t.wakeup #calling run here is a Bad Idea
51
+ else
52
+ say " ~~ Restarting Job Loop"
53
+ @t = run_job_loop
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def run_job_loop
60
+ Thread.new do
61
+ loop do
62
+ begin
63
+ delay = Update.work_off(self)
64
+ break if $exit
65
+ if delay
66
+ sleep delay
67
+ else
68
+ sleep
69
+ end
70
+ break if $exit
71
+ rescue
72
+ say "Caught exception in Job Loop"
73
+ sleep 0.1
74
+ retry
75
+ end
76
+ end
77
+ say "Worker thread exiting!"
78
+ Update.clear_locks(self)
79
+ end
80
+ end
81
+
82
+ def terminate_with(t)
83
+ say "Exiting..."
84
+ $exit = true
85
+ t.run if t.alive?
86
+ say "Forcing Shutdown" unless status = t.join(15) #Nasty inline assignment
87
+ Update.clear_locks(self)
88
+ exit status ? 0 : 1
89
+ end
90
+ end
91
+
92
+ end
@@ -7,13 +7,14 @@ module Updater
7
7
  # Contains the Error class after an error is caught in +run+. Not stored to the database
8
8
  attr_reader :error
9
9
  attr_reader :orm
10
+ attr_accessor :params
10
11
 
11
12
  #Run the action on this traget compleating any chained actions
12
- def run(job=nil,params=nil)
13
+ def run(job=nil)
13
14
  ret = true #put return in scope
14
15
  begin
15
16
  t = target
16
- final_args = sub_args(job,params,@orm.method_args)
17
+ final_args = sub_args(job,@orm.method_args)
17
18
  t.send(@orm.method.to_sym,*final_args)
18
19
  rescue => e
19
20
  @error = e
@@ -24,7 +25,8 @@ module Updater
24
25
  run_chain :ensure
25
26
  begin
26
27
  @orm.destroy unless @orm.persistant
27
- rescue DataObjects::ConnectionError
28
+ rescue StandardError => e
29
+ raise e unless e.class.to_s =~ /Connection/
28
30
  sleep 0.1
29
31
  retry
30
32
  end
@@ -43,6 +45,7 @@ module Updater
43
45
  end
44
46
 
45
47
  def initialize(orm_inst)
48
+ raise ArgumentError if orm_inst.nil?
46
49
  @orm = orm_inst
47
50
  end
48
51
 
@@ -75,14 +78,14 @@ module Updater
75
78
 
76
79
  private
77
80
 
78
- def sub_args(job,params,a)
81
+ def sub_args(job,a)
79
82
  a.map do |e|
80
83
  begin
81
84
  case e.to_s
82
85
  when '__job__'
83
86
  job
84
87
  when '__params__'
85
- params
88
+ @params
86
89
  when '__self__'
87
90
  self
88
91
  else
@@ -102,7 +105,7 @@ module Updater
102
105
  chains = @orm.send(name)
103
106
  return unless chains
104
107
  chains.each do |job|
105
- Update.new(job.target).run(self,job.params)
108
+ job.run(self)
106
109
  end
107
110
  rescue NameError
108
111
  puts @orm.inspect
@@ -123,13 +126,21 @@ module Updater
123
126
  # This is an open IO socket that will be writen to when a job is scheduled. If it is unset
124
127
  # then @pid is signaled instead.
125
128
  attr_accessor :socket
129
+ attr_writer :logger
130
+
131
+ def logger
132
+ @logger ||= Logger.new(STDOUT)
133
+ end
126
134
 
127
135
  #Gets a single job form the queue, locks and runs it. it returns the number of second
128
136
  #Until the next job is scheduled, or 0 is there are more current jobs, or nil if there
129
137
  #are no jobs scheduled.
130
138
  def work_off(worker)
131
139
  inst = @orm.lock_next(worker)
132
- new(inst).run if inst
140
+ if inst
141
+ worker.logger.debug " running job #{inst.id}"
142
+ new(inst).run
143
+ end
133
144
  @orm.queue_time
134
145
  ensure
135
146
  clear_locks(worker)
@@ -346,7 +357,7 @@ module Updater
346
357
 
347
358
  # Given some instance return the information needed to recreate that target
348
359
  def target_for(inst)
349
- return [inst, nil, nil] if inst.kind_of? Class
360
+ return [inst, nil, nil] if (inst.kind_of?(Class) || inst.kind_of?(Module))
350
361
  [inst.class,@orm::FINDER,inst.send(orm::ID)]
351
362
  end
352
363
 
data/spec/spec_helper.rb CHANGED
@@ -3,11 +3,12 @@ require "rubygems"
3
3
  ROOT = File.join(File.dirname(__FILE__), '..')
4
4
  $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
5
5
 
6
- require "spec" # Satisfies Autotest and anyone else not using the Rake tasks
6
+ require "rspec" # Satisfies Autotest and anyone else not using the Rake tasks
7
7
  require "dm-core"
8
+ require 'dm-migrations'
8
9
 
9
10
  require 'updater'
10
- require 'updater/thread_worker'
11
+ #require 'updater/thread_worker'
11
12
  require 'updater/fork_worker'
12
13
  require 'updater/orm/datamapper'
13
14
 
@@ -0,0 +1,23 @@
1
+ require "rubygems"
2
+
3
+ ROOT = File.join(File.dirname(__FILE__), '..')
4
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')
5
+
6
+ require "rspec" # Satisfies Autotest and anyone else not using the Rake tasks
7
+ require "dm-core"
8
+ require 'dm-migrations'
9
+
10
+ require 'updater'
11
+ #require 'updater/thread_worker'
12
+ require 'updater/fork_worker'
13
+ require 'updater/orm/datamapper'
14
+
15
+ Updater::Update.orm = Updater::ORM::DataMapper
16
+
17
+ DataMapper.setup(:default, 'sqlite3::memory:')
18
+ DataMapper.auto_migrate!
19
+
20
+ require 'timecop'
21
+ require 'chronic'
22
+
23
+
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 9
8
- - 2
9
- version: 0.9.2
8
+ - 3
9
+ version: 0.9.3
10
10
  platform: ruby
11
11
  authors:
12
12
  - John F. Miller
@@ -14,13 +14,14 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-07 00:00:00 -07:00
17
+ date: 2010-08-25 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: datamapper
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
24
25
  requirements:
25
26
  - - ">="
26
27
  - !ruby/object:Gem::Version
@@ -35,6 +36,7 @@ dependencies:
35
36
  name: rspec
36
37
  prerelease: false
37
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
38
40
  requirements:
39
41
  - - "="
40
42
  - !ruby/object:Gem::Version
@@ -49,6 +51,7 @@ dependencies:
49
51
  name: timecop
50
52
  prerelease: false
51
53
  requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
52
55
  requirements:
53
56
  - - ">="
54
57
  - !ruby/object:Gem::Version
@@ -63,6 +66,7 @@ dependencies:
63
66
  name: chronic
64
67
  prerelease: false
65
68
  requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
66
70
  requirements:
67
71
  - - ">="
68
72
  - !ruby/object:Gem::Version
@@ -89,7 +93,9 @@ files:
89
93
  - Rakefile
90
94
  - VERSION
91
95
  - lib/updater.rb
96
+ - lib/updater/thread_worker.rb~
92
97
  - lib/updater/setup.rb
98
+ - lib/updater/setup.rb~
93
99
  - lib/updater/util.rb
94
100
  - lib/updater/update.rb
95
101
  - lib/updater/fork_worker.rb
@@ -97,12 +103,15 @@ files:
97
103
  - lib/updater/simulated.db
98
104
  - lib/updater/tasks.rb
99
105
  - lib/updater/thread_worker.rb
106
+ - lib/updater/orm/mongo.rb~
100
107
  - lib/updater/orm/orm.rb
101
108
  - lib/updater/orm/datamapper.rb
109
+ - lib/updater/orm/mongo.rb
102
110
  - spec/fork_worker_instance_spec.rb
103
111
  - spec/thread_worker_spec.rb
104
112
  - spec/schedule_spec.rb
105
113
  - spec/lock_spec.rb
114
+ - spec/spec_helper.rb~
106
115
  - spec/params_sub_spec.rb
107
116
  - spec/chained_spec.rb
108
117
  - spec/fooclass.rb
@@ -124,6 +133,7 @@ rdoc_options: []
124
133
  require_paths:
125
134
  - lib
126
135
  required_ruby_version: !ruby/object:Gem::Requirement
136
+ none: false
127
137
  requirements:
128
138
  - - ">="
129
139
  - !ruby/object:Gem::Version
@@ -131,6 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
131
141
  - 0
132
142
  version: "0"
133
143
  required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
134
145
  requirements:
135
146
  - - ">="
136
147
  - !ruby/object:Gem::Version
@@ -140,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
151
  requirements: []
141
152
 
142
153
  rubyforge_project:
143
- rubygems_version: 1.3.6
154
+ rubygems_version: 1.3.7
144
155
  signing_key:
145
156
  specification_version: 3
146
157
  summary: A Gem for queuing methods for later calling which is ORM Agnostic, and has advanced Error Handling