batch-kit 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +165 -0
  4. data/lib/batch-kit.rb +9 -0
  5. data/lib/batch-kit/arguments.rb +57 -0
  6. data/lib/batch-kit/config.rb +517 -0
  7. data/lib/batch-kit/configurable.rb +68 -0
  8. data/lib/batch-kit/core_ext/enumerable.rb +97 -0
  9. data/lib/batch-kit/core_ext/file.rb +69 -0
  10. data/lib/batch-kit/core_ext/file_utils.rb +103 -0
  11. data/lib/batch-kit/core_ext/hash.rb +17 -0
  12. data/lib/batch-kit/core_ext/numeric.rb +17 -0
  13. data/lib/batch-kit/core_ext/string.rb +88 -0
  14. data/lib/batch-kit/database.rb +133 -0
  15. data/lib/batch-kit/database/java_util_log_handler.rb +65 -0
  16. data/lib/batch-kit/database/log4r_outputter.rb +57 -0
  17. data/lib/batch-kit/database/models.rb +548 -0
  18. data/lib/batch-kit/database/schema.rb +229 -0
  19. data/lib/batch-kit/encryption.rb +7 -0
  20. data/lib/batch-kit/encryption/java_encryption.rb +178 -0
  21. data/lib/batch-kit/encryption/ruby_encryption.rb +175 -0
  22. data/lib/batch-kit/events.rb +157 -0
  23. data/lib/batch-kit/framework/acts_as_job.rb +197 -0
  24. data/lib/batch-kit/framework/acts_as_sequence.rb +123 -0
  25. data/lib/batch-kit/framework/definable.rb +169 -0
  26. data/lib/batch-kit/framework/job.rb +121 -0
  27. data/lib/batch-kit/framework/job_definition.rb +105 -0
  28. data/lib/batch-kit/framework/job_run.rb +145 -0
  29. data/lib/batch-kit/framework/runnable.rb +235 -0
  30. data/lib/batch-kit/framework/sequence.rb +87 -0
  31. data/lib/batch-kit/framework/sequence_definition.rb +38 -0
  32. data/lib/batch-kit/framework/sequence_run.rb +48 -0
  33. data/lib/batch-kit/framework/task_definition.rb +89 -0
  34. data/lib/batch-kit/framework/task_run.rb +53 -0
  35. data/lib/batch-kit/helpers/date_time.rb +54 -0
  36. data/lib/batch-kit/helpers/email.rb +198 -0
  37. data/lib/batch-kit/helpers/html.rb +175 -0
  38. data/lib/batch-kit/helpers/process.rb +101 -0
  39. data/lib/batch-kit/helpers/zip.rb +30 -0
  40. data/lib/batch-kit/job.rb +11 -0
  41. data/lib/batch-kit/lockable.rb +138 -0
  42. data/lib/batch-kit/loggable.rb +78 -0
  43. data/lib/batch-kit/logging.rb +169 -0
  44. data/lib/batch-kit/logging/java_util_logger.rb +87 -0
  45. data/lib/batch-kit/logging/log4r_logger.rb +71 -0
  46. data/lib/batch-kit/logging/null_logger.rb +35 -0
  47. data/lib/batch-kit/logging/stdout_logger.rb +96 -0
  48. data/lib/batch-kit/resources.rb +191 -0
  49. data/lib/batch-kit/sequence.rb +7 -0
  50. metadata +122 -0
@@ -0,0 +1,101 @@
1
+ require 'shellwords'
2
+ require 'open3'
3
+
4
+ require_relative '../logging'
5
+
6
+
7
+ class BatchKit
8
+
9
+ module Helpers
10
+
11
+ # Provides support for running an external process.
12
+ # This support consists of support for:
13
+ # - launching the process as a child
14
+ # - capturing the output of the process and logging it
15
+ # - handling the return code of the process, and raising an exception for
16
+ # failures.
17
+ module Process
18
+
19
+ # Provides a means for executing a command-line.
20
+ #
21
+ # @param cmd_line [String] The command-line that is to be launched.
22
+ # @param options [Hash] An options hash.
23
+ # @option options [Proc] :callback If specified, the supplied Proc will
24
+ # be invoked for each line of output produced by the process.
25
+ # @option options [Proc] :input If specified, the supplied Proc will
26
+ # be invoked for each line of output produced by the process. It will
27
+ # be passed the pipe on which input for the process can be written,
28
+ # plus the last line of output produced. This is useful in cases where
29
+ # it is necessary to communicate with the child process via its STDIN.
30
+ # @return [Fixnum] The exit status code from the external process.
31
+ def popen(cmd_line, options = {}, &block)
32
+ callback = options[:callback]
33
+ input = options[:input]
34
+ IO.popen(cmd_line, input ? 'r+' : 'r') do |pipe|
35
+ while !pipe.eof?
36
+ line = pipe.gets.chomp
37
+ input.call(pipe, line) if input
38
+ callback.call(line) if callback
39
+ block.call(line) if block_given?
40
+ end
41
+ end
42
+ $?.exitstatus
43
+ end
44
+ module_function :popen
45
+
46
+
47
+ # Launch an external process with logging etc. By default, an exception
48
+ # will be raised if the process returns a non-zero exit code.
49
+ #
50
+ # @param cmd_line [String, Array<String>] The command-line to be run,
51
+ # in the form of either a single String, or an Array of Strings.
52
+ # @param options [Hash] An options hash.
53
+ # @option options [Boolean] :raise_on_error If true (default), an
54
+ # exception is raised if the return code is not a success code.
55
+ # @option options [Fixnum, Array<Fixnum>] The return code(s) that the
56
+ # process can return if successful (default 0).
57
+ # @option options [Boolean] :show_duration If true (default), logs the
58
+ # duration taken by the process.
59
+ # @option options [Logger] :logger The logger to use; defaults to using
60
+ # a logger named after the process being executed.
61
+ def launch(cmd_line, options = {}, &block)
62
+ exe = cmd_line.is_a?(String) ?
63
+ File.basename(Shellwords.shellwords(cmd_line.gsub(/\\/, '/')).first) :
64
+ File.basename(cmd_line.first)
65
+
66
+ raise_on_error = options.fetch(:raise_on_error, true)
67
+ show_duration = options.fetch(:show_duration, true)
68
+ success_code = options.fetch(:success_code, 0)
69
+ log = options.fetch(:logger, BatchKit::LogManager.logger(exe))
70
+ log_level = options.fetch(:log_level, :detail)
71
+ unless block_given? || options[:callback]
72
+ options = options.dup
73
+ options[:callback] = lambda{ |line| log.send(log_level, line) }
74
+ end
75
+
76
+ log.trace("Executing command line: #{cmd_line}") if log
77
+ begin
78
+ start = Time.now
79
+ rc = popen(cmd_line, options, &block)
80
+ ensure
81
+ if log && show_duration
82
+ log.detail "#{exe} completed in #{Time.now - start} seconds with exit code #{rc}"
83
+ end
84
+ end
85
+
86
+ if raise_on_error
87
+ ok = case success_code
88
+ when Fixnum then success_code == rc
89
+ when Array then success_code.include?(rc)
90
+ end
91
+ raise "#{exe} returned failure exit code #{rc}" unless ok
92
+ end
93
+ rc
94
+ end
95
+ module_function :launch
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,30 @@
1
+ require 'fileutils'
2
+ require 'zip/zip'
3
+
4
+
5
+ class BatchKit
6
+
7
+ module Helpers
8
+
9
+ module Zip
10
+
11
+ # Creates a new +zip_file+, adding +files+ to it.
12
+ #
13
+ # @param zip_file [String] A path to the zip file to be created
14
+ # @param files [String] One or more paths to files to be added to
15
+ # the zip.
16
+ def create_zip(zip_file, *files)
17
+ FileUtils.rm_f(zip_file)
18
+ Zip::ZipFile.open(zip_file, Zip::ZipFile::CREATE) do |zip|
19
+ files.each do |file|
20
+ zip.add(File.basename(file), file)
21
+ end
22
+ yield zip if block_given?
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'events'
2
+ require_relative 'lockable'
3
+ require_relative 'framework/definable'
4
+ require_relative 'framework/job_definition'
5
+ require_relative 'framework/task_definition'
6
+ require_relative 'framework/runnable'
7
+ require_relative 'framework/job_run'
8
+ require_relative 'framework/task_run'
9
+ require_relative 'framework/acts_as_job'
10
+ require_relative 'framework/job'
11
+
@@ -0,0 +1,138 @@
1
+ require 'timeout'
2
+ require_relative 'events'
3
+
4
+
5
+ class BatchKit
6
+
7
+ # Defines lockable behaviour, which can be added to any batch process.
8
+ # This behavior allows a process to define a named lock that it needs
9
+ # exclusively during execution.
10
+ # When the process is about to be executed, it will first attempt to obtain
11
+ # the named lock. If it is successful, execution will proceed as normal, and
12
+ # on completion of processing (whether succesful or otherwise), the lock
13
+ # will be released.
14
+ # If the lock is already held by another process, the requesting process
15
+ # will block and wait for the lock to become available. The process will
16
+ # only wait as long as lock_wait_timeout; if the lock has not become
17
+ # availabe in that time period, a LockTimeout exception will be thrown,
18
+ # and processing will not take place.
19
+ module Lockable
20
+
21
+ # Attempts to obtain the named lock +lock_name+. If the lock is already
22
+ # held by another process, this method blocks until one of the following
23
+ # occurs:
24
+ # - the lock is released by the process that currently holds it
25
+ # - the lock expires, by reaching it's timeout period
26
+ # - the +lock_wait_timeout+ period is reached.
27
+ #
28
+ # Lock management is managed via the event publishing system; subscribers
29
+ # to the 'lock?' event indicate whether a lock is available by their
30
+ # response to the event. A value of false indicates the lock is
31
+ # currently held; a response of true indicates the lock has been granted.
32
+ #
33
+ # @param lock_name [String] The name of the lock that is needed.
34
+ # @param lock_timeout [Fixnum] The maximum number of seconds that this
35
+ # process can hold the requested lock before it times out (allowing
36
+ # any other processes waiting on the lock to proceed). This value
37
+ # should be set high enough that the lock does not timeout while
38
+ # processing that relies on the lock is not still running.
39
+ # @param lock_wait_timeout [Fixnum] The maximum time this process is
40
+ # prepared to wait for the lock to become available. If not specified,
41
+ # the wait will timeout after the same amount of time as +lock_timeout+.
42
+ # @raise Timeout::Error If the lock is not obtained within
43
+ # +lock_wait_timeout+ seconds.
44
+ def lock(lock_name, lock_timeout, lock_wait_timeout = nil)
45
+ unless lock_timeout && lock_timeout.is_a?(Fixnum) && lock_timeout > 0
46
+ raise ArgumentError, "Invalid lock_timeout; must be > 0"
47
+ end
48
+ unless lock_wait_timeout.nil? || (lock_wait_timeout.is_a?(Fixnum) && lock_wait_timeout >= 0)
49
+ raise ArgumentError, "Invalid lock_wait_timeout; must be nil or >= 0"
50
+ end
51
+ unless Events.has_subscribers?(self, 'lock?')
52
+ if self.respond_to?(:log)
53
+ log.warn "No lock manager available; proceeding without locking"
54
+ end
55
+ return
56
+ end
57
+ lock_wait_timeout ||= lock_timeout
58
+ lock_expire_time = nil
59
+ wait_expire_time = Time.now + lock_wait_timeout
60
+ if lock_wait_timeout > 0
61
+ # Loop waiting for lock if not available
62
+ begin
63
+ Timeout.timeout(lock_wait_timeout) do
64
+ i = 0
65
+ loop do
66
+ lock_holder = {}
67
+ lock_expire_time = Events.publish(self, 'lock?', lock_name,
68
+ lock_timeout, lock_holder)
69
+ break if lock_expire_time
70
+ if i == 0
71
+ Events.publish(self, 'lock_held', lock_name,
72
+ lock_holder[:lock_holder],
73
+ lock_holder[:lock_expires_at])
74
+ Events.publish(self, 'lock_wait', lock_name, wait_expire_time)
75
+ end
76
+ sleep 1
77
+ i += 1
78
+ end
79
+ Events.publish(self, 'locked', lock_name, lock_expire_time)
80
+ end
81
+ rescue Timeout::Error
82
+ Events.publish(self, 'lock_wait_timeout', lock_name, wait_expire_time)
83
+ raise Timeout::Error, "Timed out waiting for lock '#{lock_name}' to become available"
84
+ end
85
+ else
86
+ # No waiting for lock to become free
87
+ lock_holder = {}
88
+ if lock_expire_time = Events.publish(self, 'lock?', lock_name, lock_timeout, lock_holder)
89
+ Events.publish(self, 'locked', lock_name, lock_expire_time)
90
+ else
91
+ Events.publish(self, 'lock_held', lock_name,
92
+ lock_holder[:lock_holder], lock_holder[:lock_expires_at])
93
+ Events.publish(self, 'lock_wait_timeout', lock_name, wait_expire_time)
94
+ raise Timeout::Error, "Lock '#{lock_name}' is already in use"
95
+ end
96
+ end
97
+ end
98
+
99
+
100
+ # Release a lock held by this object.
101
+ #
102
+ # @param lock_name [String] The name of the lock to be released.
103
+ def unlock(lock_name)
104
+ unless Events.has_subscribers?(self, 'unlock?')
105
+ return
106
+ end
107
+ if Events.publish(self, 'unlock?', lock_name)
108
+ Events.publish(self, 'unlocked', lock_name)
109
+ end
110
+ end
111
+
112
+
113
+ # Obtains the requested +lock_name+, then yields to the supplied block.
114
+ # Ensures the lock is released when the block ends or raises an error.
115
+ #
116
+ # @param lock_name [String] The name of the lock to obtain.
117
+ # @param lock_timeout [Fixnum] The maximum number of seconds that this
118
+ # process can hold the requested lock before it times out (allowing
119
+ # any other processes waiting on the lock to proceed). This value
120
+ # should be set high enough that the lock does not timeout while
121
+ # processing that relies on the lock is not still running.
122
+ # @param lock_wait_timeout [Fixnum] The maximum time this process is
123
+ # prepared to wait for the lock to become available. If not specified,
124
+ # the wait will timeout after the same amount of time as +lock_timeout+.
125
+ # @raise Timeout::Error If the lock is not obtained within
126
+ # +lock_wait_timeout+ seconds.
127
+ def with_lock(lock_name, lock_timeout, lock_wait_timeout = nil)
128
+ self.lock(lock_name, lock_timeout, lock_wait_timeout)
129
+ begin
130
+ yield
131
+ ensure
132
+ self.unlock(lock_name)
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'logging'
2
+
3
+
4
+ class BatchKit
5
+
6
+ # Adds logging behaviour to a batch-kit process, causing its lifecycle to be
7
+ # logged.
8
+ module Loggable
9
+
10
+ # Returns a logger instance named after the class
11
+ def log
12
+ @log ||= LogManager.logger(self.class.name)
13
+ end
14
+
15
+
16
+ if defined?(BatchKit::Events)
17
+
18
+ # Subscribe to batch-kit lifecycle events that should be logged
19
+ Events.subscribe(Configurable, 'config.post-load') do |job_cls, cfg|
20
+ if cfg.has_key?(:log_level) || cfg.has_key?(:log_file)
21
+ log = LogManager.logger(job_cls.name)
22
+ if cfg[:log_level]
23
+ log.level = cfg[:log_level]
24
+ log.config "Log level set to #{cfg[:log_level].upcase}"
25
+ end
26
+ if cfg.has_key?(:log_file)
27
+ log.config "Logging output to: #{cfg[:log_file]}" if cfg[:log_file]
28
+ FileUtils.mkdir_p(File.dirname(cfg[:log_file]))
29
+ log.log_file = cfg[:log_file]
30
+ end
31
+ end
32
+ end
33
+ Events.subscribe(Loggable, 'sequence_run.execute') do |job_obj, run, *args|
34
+ job_obj.log.info "Sequence '#{run.label}' started"
35
+ end
36
+ Events.subscribe(Loggable, 'job_run.execute') do |job_obj, run, *args|
37
+ id = run.job_run_id ? " as job run #{run.job_run_id}" : ''
38
+ job_obj.log.info "Job '#{run.label}' started on #{run.computer} by #{run.run_by}#{id}"
39
+ end
40
+ Events.subscribe(Loggable, 'task_run.execute') do |job_obj, run, *args|
41
+ id = run.task_run_id ? " as task run #{run.task_run_id}" : ''
42
+ job_obj.log.info "Task '#{run.label}' started#{id}"
43
+ end
44
+ %w{sequence_run job_run task_run}.each do |runnable|
45
+ Events.subscribe(Loggable, "#{runnable}.post-execute") do |job_obj, run, ok|
46
+ job_obj.log.info "#{run.class.name.split('::')[-2]} '#{run.label}' completed #{
47
+ ok ? 'successfully' : 'with errors'} in #{'%.3f' % run.elapsed} seconds"
48
+ end
49
+ end
50
+
51
+ Events.subscribe(Lockable, 'lock_wait') do |job_run, lock_name|
52
+ if (job_obj = job_run.object).is_a?(Loggable)
53
+ job_obj.log.detail "Waiting for lock '#{lock_name}' to become avaialable"
54
+ end
55
+ end
56
+ Events.subscribe(Lockable, 'lock_held') do |job_run, lock_name, lock_holder, lock_expire_time|
57
+ if (job_obj = job_run.object).is_a?(Loggable)
58
+ job_obj.log.warn "Lock '#{lock_name}' is currently held by #{lock_holder}; expires at #{
59
+ lock_expire_time.strftime('%H:%M:%S')}"
60
+ end
61
+ end
62
+ Events.subscribe(Lockable, 'locked') do |job_run, lock_name, lock_expire_time|
63
+ if (job_obj = job_run.object).is_a?(Loggable)
64
+ job_obj.log.detail "Obtained lock '#{lock_name}'; expires at #{
65
+ lock_expire_time.strftime('%H:%M:%S')}"
66
+ end
67
+ end
68
+ Events.subscribe(Lockable, 'unlocked') do |job_run, lock_name|
69
+ if (job_obj = job_run.object).is_a?(Loggable)
70
+ job_obj.log.detail "Released lock '#{lock_name}'"
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,169 @@
1
+ require 'fileutils'
2
+
3
+
4
+ class BatchKit
5
+
6
+ module Logging
7
+
8
+ # Log levels available
9
+ LEVELS = [:error, :warning, :info, :config, :detail, :trace, :debug]
10
+
11
+ # Supported logging frameworks
12
+ FRAMEWORKS = [
13
+ :null,
14
+ :stdout,
15
+ :log4r,
16
+ :java_util_logging
17
+ ]
18
+
19
+ # Method aliasing needed to provide log methods corresponding to levels
20
+ FRAMEWORK_INIT = {
21
+ null: lambda{
22
+ require_relative 'logging/null_logger'
23
+ },
24
+ stdout: lambda{
25
+ require_relative 'logging/stdout_logger'
26
+ },
27
+ java_util_logging: lambda{
28
+ require_relative 'logging/java_util_logger'
29
+ },
30
+ log4r: lambda{
31
+ require_relative 'logging/log4r_logger'
32
+ }
33
+ }
34
+
35
+ end
36
+
37
+
38
+ # Used for setting the log framework to use, and retrieving a logger
39
+ # from the current framework.
40
+ class LogManager
41
+
42
+ class << self
43
+
44
+ def configure(options = {})
45
+ self.log_framework = options[:log_framework] if options[:log_framework]
46
+ if options.fetch(:log_color, true)
47
+ case self.log_framework
48
+ when :log4r
49
+ require 'color_console/log4r_logger'
50
+ Console.replace_console_logger(logger: 'batch')
51
+ when :java_util_logging
52
+ require 'color_console/java_util_logger'
53
+ Console.replace_console_logger(
54
+ level: Java::JavaUtilLogging::Level::FINE,
55
+ level_labels: {
56
+ Java::JavaUtilLogging::Level::FINE => 'DETAIL',
57
+ Java::JavaUtilLogging::Level::FINER => 'TRACE'
58
+ })
59
+ else
60
+ require 'color_console'
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ # Returns a symbol identifying which logging framework is being used.
67
+ def log_framework
68
+ unless @log_framework
69
+ if RUBY_PLATFORM == 'java'
70
+ LogManager.log_framework = :java_util_logging
71
+ else
72
+ begin
73
+ require 'log4r'
74
+ LogManager.log_framework = :log4r
75
+ rescue LoadError
76
+ LogManager.log_framework = :stdout
77
+ end
78
+ end
79
+ end
80
+ @log_framework
81
+ end
82
+
83
+
84
+ # Sets the logging framework
85
+ def log_framework=(framework)
86
+ unless Logging::FRAMEWORKS.include?(framework)
87
+ raise ArgumentError, "Unknown logging framework #{framework.inspect}"
88
+ end
89
+ if @log_framework
90
+ lvl = self.level
91
+ end
92
+ @log_framework = framework
93
+ if init_proc = Logging::FRAMEWORK_INIT[@log_framework]
94
+ init_proc.call
95
+ end
96
+ self.level = lvl if lvl
97
+ logger.trace "Log framework is #{@log_framework}"
98
+ end
99
+
100
+
101
+ # Returns the current root log level
102
+ def level
103
+ logger.level
104
+ end
105
+
106
+
107
+ # Sets the log level
108
+ def level=(level)
109
+ case log_framework
110
+ when :log4r
111
+ lvl = Log4r::LNAMES.index(level.to_s.upcase)
112
+ Log4r::Logger.each_logger{ |l| l.level = lvl }
113
+ else
114
+ logger.level = level
115
+ end
116
+ end
117
+
118
+
119
+ # Returns a logger with a given name, which must be under the 'batch'
120
+ # namespace. If name is omitted, the logger is named 'batch'. If a
121
+ # name is specified that is not under 'batch', then it is prepended
122
+ # with 'batch'.
123
+ #
124
+ # @return [Logger] a logger object that can be used for generating
125
+ # log messages. The type of logger returned will depend on the
126
+ # log framework being used, but the logger is guaranteed to
127
+ # implement the following log methods:
128
+ # - error
129
+ # - warning
130
+ # - info
131
+ # - config
132
+ # - detail
133
+ # - trace
134
+ # - debug
135
+ def logger(name = nil)
136
+ case name
137
+ when NilClass, ''
138
+ name = 'batch'
139
+ when /^batch/
140
+ when /\./
141
+ when String
142
+ name = "batch.#{name}"
143
+ end
144
+ case log_framework
145
+ when :stdout
146
+ BatchKit::Logging::StdOutLogger.logger(name)
147
+ when :java_util_logging
148
+ BatchKit::Logging::JavaLogFacade.new(Java::JavaUtilLogging::Logger.getLogger(name))
149
+ when :log4r
150
+ log4r_name = name.gsub('.', '::')
151
+ BatchKit::Logging::Log4rFacade.new(Log4r::Logger[log4r_name] ||
152
+ Log4r::Logger.new(log4r_name))
153
+ else BatchKit::Logging::NullLogger.instance
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+
160
+ if defined?(Events) && defined?(Configurable)
161
+ Events.subscribe(Configurable, 'post-configure') do |src, cfg|
162
+ LogManager.configure(cfg)
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ end
169
+