updater 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
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