iyyov 1.0.0-java

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.
@@ -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