iyyov 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,348 @@
1
+ #--
2
+ # Copyright (C) 2010 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ require 'rjack-slf4j'
18
+ require 'fileutils'
19
+
20
+ require 'iyyov/log_rotator'
21
+
22
+ module Iyyov
23
+ include RJack
24
+
25
+ # A daemon isntance to start and monitor
26
+ class Daemon
27
+
28
+ # Name of this daemon. Must be unique in combination with any
29
+ # specified instance.
30
+ #
31
+ # String (required)
32
+ attr_accessor :name
33
+
34
+ # Optional specific instance identifier, distinguishing this
35
+ # daemon from others of the same name. For example, a port number
36
+ # could be used.
37
+ #
38
+ # Proc,~to_s (default: nil)
39
+ attr_writer :instance
40
+
41
+ # Full path to executable to start.
42
+ #
43
+ # Proc,~to_s (default: compute from gem_name, init_name, and version)
44
+ attr_writer :exe_path
45
+
46
+ # Any additional args to use on start
47
+ #
48
+ # Proc,Array[~to_s] (default: [])
49
+ attr_writer :args
50
+
51
+ # Base directory under which run directories are found
52
+ #
53
+ # Proc,~to_s (default: Context.base_dir)
54
+ attr_writer :base_dir
55
+
56
+ # Directory to execute under
57
+ #
58
+ # Proc,~to_s (default: base_dir / full_name)
59
+ attr_writer :run_dir
60
+
61
+ # Whether to make run_dir, if not already present
62
+ #
63
+ # Boolean (default: Context.make_run_dir )
64
+ attr_accessor :make_run_dir
65
+
66
+ # Whether to stop this daemon when Iyyov exits
67
+ #
68
+ # Boolean (default: Context.stop_on_exit)
69
+ attr_accessor :stop_on_exit
70
+
71
+ # Duration in seconds between SIGTERM and final SIGKILL when
72
+ # stopping.
73
+ #
74
+ # Numeric (default: Context.stop_delay)
75
+ attr_accessor :stop_delay
76
+
77
+ # PID file written by the daemon process after start, containing
78
+ # the running daemon Process ID
79
+ #
80
+ # Proc,~to_s (default: run_dir, init_name + '.pid')
81
+ attr_writer :pid_file
82
+
83
+ # The gem name used, in conjunction with version for gem-based
84
+ # default exe_path
85
+ #
86
+ # Proc,~to_s (default: name)
87
+ attr_writer :gem_name
88
+
89
+ # The gem version requirements, i.e '~> 1.1.3'
90
+ #
91
+ # Proc,~to_s,Array[~to_s] (default: '>= 0')
92
+ attr_writer :version
93
+
94
+ # The init script name used for gem-based default exe_path.
95
+ #
96
+ # Proc,~to_s (default: name)
97
+ attr_writer :init_name
98
+
99
+ # Last found state of this daemon.
100
+ #
101
+ # Symbol (in STATES)
102
+ attr_reader :state
103
+
104
+ # Once do_first is called and provided a gem is found (gem version
105
+ # specified).
106
+ #
107
+ # Gem::Specification
108
+ # FIXME: May not want to expose this.
109
+ attr_reader :gem_spec
110
+
111
+ # States tracked
112
+ STATES = [ :begin, :up, :failed, :stopped ]
113
+
114
+ # Instance variables which may be set as Procs
115
+ LVARS = [ :@instance, :@exe_path, :@args, :@base_dir, :@run_dir, :@pid_file,
116
+ :@gem_name, :@version, :@init_name ]
117
+
118
+ # New daemon given specified or default global
119
+ # Iyyov.context. Yields self to block for configuration.
120
+ def initialize( context = Iyyov.context )
121
+
122
+ @context = context
123
+ @name = nil
124
+
125
+ @instance = nil
126
+ @exe_path = method :gem_exe_path
127
+ @args = []
128
+ @base_dir = method :default_base_dir
129
+ @run_dir = method :default_run_dir
130
+ @make_run_dir = @context.make_run_dir
131
+ @stop_on_exit = @context.stop_on_exit
132
+ @stop_delay = @context.stop_delay
133
+
134
+ @pid_file = method :default_pid_file
135
+ @gem_name = method :name
136
+ @version = '>= 0'
137
+ @init_name = method :name
138
+
139
+ @state = :begin
140
+ @gem_spec = nil
141
+ @rotators = {}
142
+
143
+ yield self if block_given?
144
+
145
+ raise "name not specified" unless name
146
+
147
+ @log = SLF4J[ [ SLF4J[ self.class ].name,
148
+ name, instance ].compact.join( '.' ) ]
149
+ end
150
+
151
+ # Given name + ( '-' + instance ) if provided.
152
+ def full_name
153
+ [ name, instance ].compact.join('-')
154
+ end
155
+
156
+ # Create a new LogRotator and yields it to block for
157
+ # configuration.
158
+ # The default log path is name + ".log" in run_dir
159
+ def log_rotate( &block )
160
+ lr = LogRotator.new( default_log, &block )
161
+ @rotators[ lr.log ] = lr
162
+ nil
163
+ end
164
+
165
+ # Post initialization validation, attempt immediate start if
166
+ # needed, and add appropriate tasks to scheduler.
167
+ def do_first( scheduler )
168
+ unless File.directory?( run_dir )
169
+ if make_run_dir
170
+ @log.info { "Creating run_dir [#{run_dir}]." }
171
+ FileUtils.mkdir_p( run_dir, :mode => 0755 )
172
+ else
173
+ raise( DaemonFailed, "run_dir [#{run_dir}] not found" )
174
+ end
175
+ end
176
+
177
+ res = start_check
178
+ unless res == :stop
179
+ tasks.each { |t| scheduler.add( t ) }
180
+ end
181
+ res
182
+ rescue DaemonFailed, SystemCallError => e
183
+ #FIXME: Ruby 1.4.0 throws SystemCallError when mkdir fails from
184
+ #permissions
185
+ @log.error( e.to_s )
186
+ @state = :failed
187
+ :stop
188
+ end
189
+
190
+ def tasks
191
+ t = [ Task.new( :name => full_name, :period => 5.0 ) { start_check } ]
192
+ t += @rotators.values.map do |lr|
193
+ Task.new( :name => "#{full_name}.rotate",
194
+ :mode => :async,
195
+ :period => lr.check_period ) do
196
+ lr.check_rotate( pid ) do |rlog|
197
+ @log.info { "Rotating log #{rlog}" }
198
+ end
199
+ end
200
+ end
201
+ t
202
+ end
203
+
204
+ def do_exit
205
+ stop if stop_on_exit
206
+ end
207
+
208
+ def default_base_dir
209
+ @context.base_dir
210
+ end
211
+
212
+ def default_run_dir
213
+ File.join( base_dir, full_name )
214
+ end
215
+
216
+ def default_pid_file
217
+ in_dir( init_name + '.pid' )
218
+ end
219
+
220
+ def default_log
221
+ in_dir( init_name + '.log' )
222
+ end
223
+
224
+ # Return full path to file_name within run_dir
225
+ def in_dir( file_name )
226
+ File.join( run_dir, file_name )
227
+ end
228
+
229
+ def gem_exe_path
230
+ File.join( find_gem_spec.full_gem_path, 'init', init_name )
231
+ end
232
+
233
+ def find_gem_spec
234
+ #FIXME: Use Gem.clear_paths to rescan.
235
+ @gem_spec ||= Gem.source_index.find_name( gem_name, version ).last
236
+ unless @gem_spec
237
+ raise( Gem::GemNotFoundException, "Missing gem #{gem_name} (#{version})" )
238
+ end
239
+ @gem_spec
240
+ end
241
+
242
+ def start
243
+ epath = File.expand_path( exe_path )
244
+ eargs = args.to_a.map { |a| a.to_s.strip }.compact
245
+ aversion = @gem_spec && @gem_spec.version
246
+ @log.info { ( [ "starting", aversion || epath ] + eargs ).join(' ') }
247
+
248
+ unless File.executable?( epath )
249
+ raise( DaemonFailed, "Exe path: #{epath} not found/executable." )
250
+ end
251
+
252
+ Dir.chdir( run_dir ) do
253
+ system( epath, *eargs ) or raise( DaemonFailed, "Start failed with #{$?}" )
254
+ end
255
+
256
+ @state = :up
257
+ true
258
+ rescue Gem::GemNotFoundException, DaemonFailed, Errno::ENOENT => e
259
+ @log.error( e.to_s )
260
+ @state = :failed
261
+ false
262
+ end
263
+
264
+ # Return array suitable for comparing this daemon with prior
265
+ # running instance.
266
+ def exec_key
267
+ keys = [ run_dir, exe_path ].map { |p| File.expand_path( p ) }
268
+ keys += args.to_a.map { |a| a.to_s.strip }.compact
269
+ keys.compact
270
+ end
271
+
272
+ def start_check
273
+ p = pid
274
+ if alive?( p )
275
+ @log.debug { "checked: alive pid: #{p}" }
276
+ @state = :up
277
+ else
278
+ unless start
279
+ @log.info "start failed, done trying"
280
+ :stop
281
+ end
282
+ end
283
+ end
284
+
285
+ # True if process is up
286
+ def alive?( p = pid )
287
+ ( Process.getpgid( p ) != -1 ) if p
288
+ rescue Errno::ESRCH
289
+ false
290
+ end
291
+
292
+ # Stop via SIGTERM, waiting for shutdown for up to stop_delay, then
293
+ # SIGKILL as last resort. Return true if a process was stopped.
294
+ def stop
295
+ p = pid
296
+ if p
297
+ @log.info "Sending TERM signal"
298
+ Process.kill( "TERM", p )
299
+ unless wait_pid( p )
300
+ @log.info "Sending KILL signal"
301
+ Process.kill( "KILL", p )
302
+ end
303
+ @status = :stopped
304
+ true
305
+ end
306
+ false
307
+ rescue Errno::ESRCH
308
+ # No such process: only raised by MRI ruby currently
309
+ false
310
+ rescue Errno::EPERM => e
311
+ # Not permitted: only raised by MRI ruby currently
312
+ @log.error( e )
313
+ false
314
+ end
315
+
316
+ # Wait for process to go away
317
+ def wait_pid( p = pid )
318
+ delta = 1.0 / 16
319
+ delay = 0.0
320
+ check = false
321
+ while delay < stop_delay do
322
+ break if ( check = ! alive?( p ) )
323
+ sleep delta
324
+ delay += delta
325
+ delta += ( 1.0 / 16 ) if delta < 0.50
326
+ end
327
+ check
328
+ end
329
+
330
+ # Return process ID from pid_file if exists or nil otherwise
331
+ def pid
332
+ id = IO.read( pid_file ).strip.to_i
333
+ ( id > 0 ) ? id : nil
334
+ rescue Errno::ENOENT # Pid file doesn't exist
335
+ nil
336
+ end
337
+
338
+ LVARS.each do |sym|
339
+ define_method( sym.to_s[1..-1] ) do
340
+ exp = instance_variable_get( sym )
341
+ exp.respond_to?( :call ) ? exp.call : exp
342
+ end
343
+ end
344
+
345
+ end
346
+
347
+ class DaemonFailed < StandardError; end
348
+ end
@@ -0,0 +1,19 @@
1
+ #--
2
+ # Copyright (C) 2010 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ module Iyyov
18
+ class SetupError < StandardError; end
19
+ end
@@ -0,0 +1,99 @@
1
+ #--
2
+ # Copyright (C) 2010 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ require 'logrotate'
18
+
19
+ module Iyyov
20
+ include RJack
21
+
22
+ # Support check for size and rotation of a log file.
23
+ class LogRotator
24
+
25
+ # Full path to log file to check and rotate
26
+ #
27
+ # String (required)
28
+ attr_accessor :log
29
+
30
+ # Maximum log size triggering rotation
31
+ #
32
+ # Fixnum bytes (default: 256M bytes)
33
+ attr_accessor :max_size
34
+
35
+ # Number of rotated logs in addition to active log.
36
+ #
37
+ # Fixnum (default: 3)
38
+ attr_accessor :count
39
+
40
+ # GZIP compress rotated logs?
41
+ #
42
+ # Boolean (default: true)
43
+ attr_accessor :gzip
44
+
45
+ # The signal to use post rotation (but before gzip) requesting
46
+ # that the daemon reopen its logs.
47
+ #
48
+ # String (default: "HUP")
49
+ attr_accessor :signal
50
+
51
+ # Period between subsequent checks for rotation (default: 300.0)
52
+ #
53
+ # Float seconds
54
+ attr_accessor :check_period
55
+
56
+ # Process ID to signal (if known in advance/constant, i.e. 0 for
57
+ # this process)
58
+ #
59
+ # Fixnum
60
+ attr_writer :pid
61
+
62
+ # Set max_size in megabytes
63
+ #
64
+ # mb<Fixnum>:: megabytes
65
+ def max_size_mb=( mb )
66
+ @max_size = mb * 1024 * 1024
67
+ end
68
+
69
+ def initialize( log = nil )
70
+ @log = log
71
+ max_size_mb = 256
72
+ @count = 3
73
+ @gzip = true
74
+ @signal = "HUP"
75
+ @check_period = 5 * 60.0
76
+ @pid = nil
77
+
78
+ yield self if block_given?
79
+
80
+ #FIXME: Validate log directory?
81
+ nil
82
+ end
83
+
84
+ # Check if log is over size and rotate if needed. Yield log name
85
+ # to block just before rotating
86
+ def check_rotate( pid = @pid )
87
+ if File.exist?( log ) && File.size( log ) > max_size
88
+ yield log if block_given?
89
+ opts = { :count => count, :gzip => gzip }
90
+ if signal && pid
91
+ opts[ :post_rotate ] = lambda { Process.kill( signal, pid ) }
92
+ end
93
+ LogRotate.rotate_file( log, opts )
94
+ end
95
+ nil
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,120 @@
1
+ #--
2
+ # Copyright (C) 2010 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ require 'java'
18
+
19
+ require 'thread'
20
+ require 'iyyov/shutdown_handler'
21
+
22
+ module Iyyov
23
+
24
+ # Maintains a queue of Task to be executed at fixed or periodic
25
+ # times.
26
+ class Scheduler
27
+
28
+ def initialize
29
+ # min heap
30
+ @queue = Java::java.util.PriorityQueue.new( 67, TimeComp.new )
31
+ @lock = Mutex.new
32
+ @shutdown_handler = nil
33
+ @log = SLF4J[ self.class ]
34
+ end
35
+
36
+ def add( t, now = Time.now )
37
+ @queue.add( t ) if t.schedule( now )
38
+ end
39
+
40
+ def peek
41
+ @queue.peek
42
+ end
43
+
44
+ def poll
45
+ @queue.poll
46
+ end
47
+
48
+ # Execute the specified block on exit (in a shutdown thread.) The
49
+ # Scheduler queue is drained such that the on_exit block is
50
+ # guaranteed to be the last to run.
51
+ def on_exit( &block )
52
+ off_exit
53
+ @shutdown_handler = ShutdownHandler.new do
54
+
55
+ # Need to lock out the event loop since exit handler is called
56
+ # from a different thread.
57
+ @lock.synchronize do
58
+ @queue.clear
59
+ block.call
60
+ end
61
+ end
62
+ @log.debug { "Registered exit: #{ @shutdown_handler.handler }" }
63
+ end
64
+
65
+ # Deregister any previously added on_exit block
66
+ def off_exit
67
+ if @shutdown_handler
68
+ @log.debug { "Unregistered exit: #{ @shutdown_handler.handler }" }
69
+ @shutdown_handler.unregister
70
+ @shutdown_handler = nil
71
+ end
72
+ end
73
+
74
+ # Loop forever executing tasks or waiting for the next to be
75
+ # ready. Return only when the queue is empty (which may be arranged by
76
+ # on_exit) or if a Task returns :shutdown.
77
+ def event_loop
78
+ rc = nil
79
+
80
+ # While not shutdown
81
+ while ( rc != :shutdown )
82
+ now = Time.now
83
+ delta = 0.0
84
+
85
+ @lock.synchronize do
86
+ # While we don't need to wait, and a task is available
87
+ while ( delta <= 0.0 && ( task = peek ) && rc != :shutdown )
88
+ delta = task.next_time - now
89
+
90
+ if delta <= 0.0
91
+ task = poll
92
+
93
+ rc = task.run
94
+ add( task, now ) unless ( rc == :shutdown || rc == :stop )
95
+ end
96
+ end
97
+ end #lock
98
+
99
+ break unless delta > 0.0
100
+ sleep delta
101
+ end
102
+
103
+ if rc == :shutdown
104
+ @log.debug "Begin scheduler shutdown sequence."
105
+ @queue.clear
106
+ off_exit
107
+ end
108
+ rc
109
+ end
110
+
111
+ # Implements java.util.Comparator over task.next_time values
112
+ class TimeComp
113
+ include Java::java.util.Comparator
114
+ def compare( p, n )
115
+ p.next_time <=> n.next_time
116
+ end
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,46 @@
1
+ #--
2
+ # Copyright (C) 2010 David Kellum
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ # may not use this file except in compliance with the License. You
6
+ # may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
15
+ #++
16
+
17
+ require 'java'
18
+
19
+ module Iyyov
20
+
21
+ class ShutdownHandler
22
+ Thread = Java::java.lang.Thread
23
+ Runtime = Java::java.lang.Runtime
24
+
25
+ include Java::java.lang.Runnable
26
+
27
+ attr_reader :handler
28
+
29
+ def initialize( &block )
30
+ @block = block
31
+ @handler = Thread.new( self )
32
+ Runtime::runtime.add_shutdown_hook( @handler )
33
+ end
34
+
35
+ def unregister
36
+ Runtime::runtime.remove_shutdown_hook( @handler )
37
+ @handler = nil
38
+ @block = nil
39
+ end
40
+
41
+ def run
42
+ @block.call
43
+ end
44
+
45
+ end
46
+ end