batch-kit 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+