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,53 @@
1
+ class BatchKit
2
+
3
+ module Task
4
+
5
+ # Captures details of an execution of a task.
6
+ class Run < Runnable
7
+
8
+ # @return [Job::Run] The job run that this task is running under.
9
+ attr_reader :job_run
10
+ # @return [Fixnum] An integer identifier that uniquely identifies
11
+ # this task run.
12
+ attr_accessor :task_run_id
13
+
14
+ # Make Task::Defintion properties accessible off this Task::Run.
15
+ add_delegated_properties(*Task::Definition.properties)
16
+
17
+
18
+ # Create a new task run.
19
+ #
20
+ # @param task_def [Task::Definition] The Task::Definition to which this
21
+ # run relates.
22
+ # @param job_object [Object] The job object instance from which the
23
+ # task is being executed.
24
+ # @param job_run [Job::Run] The job run to which this task run belongs.
25
+ # @param run_args [Array<Object>] An array of the argument values
26
+ # passed to the task method.
27
+ def initialize(task_def, job_object, job_run, *run_args)
28
+ raise ArgumentError, "task_def not a Task::Definition" unless task_def.is_a?(Task::Definition)
29
+ raise ArgumentError, "job_run not a Job::Run" unless job_run.is_a?(Job::Run)
30
+ @job_run = job_run
31
+ @job_run << self
32
+ super(task_def, job_object, run_args)
33
+ end
34
+
35
+
36
+ # @return [Boolean] True if this task run should be persisted in any
37
+ # persistence layer.
38
+ def persist?
39
+ !definition.job.do_not_track
40
+ end
41
+
42
+
43
+ # @return [String] A short representation of this Task::Run.
44
+ def to_s
45
+ "<BatchKit::Task::Run label='#{label}'>"
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+
@@ -0,0 +1,54 @@
1
+
2
+ class BatchKit
3
+
4
+ module Helpers
5
+
6
+ # Methods for displaying times/durations
7
+ module DateTime
8
+
9
+ # Converts the elapsed time in seconds to a string showing days, hours,
10
+ # minutes and seconds.
11
+ def display_duration(elapsed)
12
+ return nil unless elapsed
13
+ elapsed = elapsed.round
14
+ display = ''
15
+ [['days', 86400], ['h', 3600], ['m', 60], ['s', 1]].each do |int, seg|
16
+ if elapsed >= seg
17
+ count, elapsed = elapsed.divmod(seg)
18
+ display << "#{count}#{int.length > 1 && count == 1 ? int[0..-2] : int} "
19
+ elsif display.length > 0
20
+ display << "0#{int}"
21
+ end
22
+ end
23
+ display = "0s" if display == ''
24
+ display.strip
25
+ end
26
+ module_function :display_duration
27
+
28
+
29
+ # Displays a date/time in abbreviated format, suppressing elements of
30
+ # the format string for more recent dates/times.
31
+ #
32
+ # @param ts [Time, Date, DateTime] The date/time object to be displayed
33
+ # @return [String] A formatted representation of the date/time.
34
+ def display_timestamp(ts)
35
+ return unless ts
36
+ ts_date = ts.to_date
37
+ today = Date.today
38
+ if today - ts_date < 7
39
+ # Date is within the last week
40
+ ts.strftime('%a %H:%M:%S')
41
+ elsif today.year != ts.year
42
+ # Data is from a different year
43
+ ts.strftime('%a %b %d %Y %H:%M:%S')
44
+ else
45
+ ts.strftime('%a %b %d %H:%M:%S')
46
+ end
47
+ end
48
+ module_function :display_timestamp
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,198 @@
1
+ require 'mail'
2
+ require_relative '../core_ext/file_utils'
3
+ require_relative 'html'
4
+ require_relative 'date_time'
5
+
6
+
7
+ class BatchKit
8
+
9
+ module Helpers
10
+
11
+ # Defines a number of methods to help with generating email messages in
12
+ # both plain-text and HTML formats.
13
+ module Email
14
+
15
+ include Html
16
+
17
+ # Creates a new Mail message object that can be used to create and
18
+ # send an email.
19
+ #
20
+ # @param cfg [BatchKit::Config] A config object containing details of
21
+ # an SMTP gateway for delivering the message. Defaults to the
22
+ # config object defined by including BatchKit#Configurable.
23
+ def create_email(cfg = nil)
24
+ cfg = config if cfg.nil? && self.respond_to?(:config)
25
+ Mail.defaults do
26
+ delivery_method :smtp, cfg.smtp
27
+ end
28
+
29
+ Mail.new(to: mail_list(cfg[:to]),
30
+ cc: mail_list(cfg[:cc]),
31
+ from: cfg[:email_from] || "#{self.job.job_class.name}@#{self.job.computer}",
32
+ reply_to: mail_list(cfg[:reply_to]))
33
+ end
34
+
35
+
36
+ # Mail likes its recipient lists as a comma-separated list in a
37
+ # String. To make this easier to use, this helper method converts
38
+ # an array of values into sucn a string.
39
+ def mail_list(recips)
40
+ recips.is_a?(Array) ? recips.join(', ') : recips
41
+ end
42
+
43
+
44
+ # Creates an HTML formatted email, with a default set of styles.
45
+ #
46
+ # @param cfg [BatchKit::Config] A config object containing details of
47
+ # an SMTP gateway for delivering the message. Defaults to the
48
+ # config object defined by including BatchKit#Configurable.
49
+ # @param body_text [String] An optional string containing text to
50
+ # add to the email body.
51
+ # @yield [Array<String>] an Array of strings to which body content
52
+ # can be added.
53
+ def create_html_email(cfg = config, body_text = nil, &blk)
54
+ if cfg.is_a?(String) || cfg.is_a?(Array)
55
+ body_text = cfg
56
+ cfg = nil
57
+ end
58
+ msg = create_email(cfg)
59
+ body = create_html_document(body_text, &blk)
60
+ msg.html_part = Mail::Part.new do |part|
61
+ part.content_type('text/html; charset=UTF-8')
62
+ part.body(body.join("\n"))
63
+ end
64
+ msg
65
+ end
66
+
67
+
68
+ # Adds details of tasks run and their duration to an email.
69
+ #
70
+ # @param body [Array<String>] An array containing the lines of the
71
+ # message body. Job execution details will be added as an HTML
72
+ # table to this.
73
+ def add_job_details_to_email(body)
74
+ has_instances = self.job_run.task_runs.find{ |tr| tr.instance }
75
+ body << "<br>"
76
+ body << "<div class='separator'></div>"
77
+ body << "<p>"
78
+ body << "<p>Job execution details:</p>"
79
+ create_html_table(body, self.job_run.task_runs,
80
+ {name: :name, label: 'Task'},
81
+ {name: :instance, show: has_instances},
82
+ {name: :start_time, label: 'Start Time'},
83
+ {name: :end_time, label: 'End Time'},
84
+ {label: 'Duration', class: 'right',
85
+ value: lambda{ |tr| DateTime.display_duration(tr.elapsed) }})
86
+ body.slice!(-2..-1)
87
+ body << "<tr><th>#{self.job.name}</th>"
88
+ body << "<th>#{self.job_run.instance}</th>" if has_instances
89
+ body << "<th class='right'>#{self.job_run.start_time.strftime("%H:%M:%S")}</th>"
90
+ body << "<th class='right'></th>"
91
+ body << "<th class='right'>#{DateTime.display_duration(self.job_run.elapsed)}</th></tr>"
92
+ body << "</tbody>"
93
+ body << "</table>"
94
+ body << "<br>"
95
+ end
96
+
97
+
98
+ # Creates an email message containing details of the exception that
99
+ # caused this job to fail.
100
+ def create_failure_email(cfg = config)
101
+ msg = create_email(cfg)
102
+ to = cfg[:failure_email_to]
103
+ to = to.join(', ') if to.is_a?(Array)
104
+ cc = cfg[:failure_email_cc]
105
+ cc = cc.join(', ') if cc.is_a?(Array)
106
+ msg.to = to
107
+ msg.cc = cc
108
+ msg.subject = "#{self.job.name} job on #{self.job.computer} Failed!"
109
+
110
+ # Add details of the failed job and task runs
111
+ body = []
112
+ self.job.runs.each do |jr|
113
+ ex = nil
114
+ jr.task_runs.select{ |tr| tr.exception != nil }.each do |tr|
115
+ ex = tr.exception
116
+ body << "Job '#{jr.label}' has failed in task '#{tr.label}'."
117
+ body << "\n#{ex.class.name}: #{ex.message}"
118
+ body << "\nBacktrace:"
119
+ body += ex.backtrace
120
+ body << "\n"
121
+ end
122
+ if ex != jr.exception
123
+ body << "Job '#{jr.label}' has failed."
124
+ body << "\n#{jr.exception.class.name}: #{jr.exception.message}"
125
+ body << "\nBacktrace:"
126
+ body += jr.exception.backtrace
127
+ body << "\n"
128
+ end
129
+ end
130
+
131
+ # Add job log file as attachment (if it exists)
132
+ if self.respond_to?(:log) && self.log.log_file
133
+ body << "See the attached log for details.\n"
134
+ msg.add_file(self.log.log_file)
135
+ end
136
+ msg.body = body.join("\n")
137
+ msg
138
+ end
139
+
140
+
141
+ # Sends a failure email message in response to a job failure.
142
+ #
143
+ # @param recipients [String|Array] The recipient(s) to receive the
144
+ # email. If no recipients are specified, the con
145
+ def send_failure_email(cfg = config, recipients = nil)
146
+ unless cfg.is_a?(Hash)
147
+ recipients = cfg
148
+ cfg = config
149
+ end
150
+ msg = create_failure_email(cfg)
151
+ if recipients
152
+ # Override default recipients
153
+ msg.to = recipients
154
+ msg.cc = nil
155
+ end
156
+ msg.deliver!
157
+ log.detail "Failure email sent to #{recipient_count(msg)} recipients"
158
+ end
159
+
160
+
161
+ # Given a message, returns the number of recipients currently set.
162
+ #
163
+ # @param msg [Mail] A Mail message object.
164
+ def recipient_count(msg)
165
+ count = 0
166
+ count += msg.to.size if msg.to
167
+ count += msg.cc.size if msg.cc
168
+ count
169
+ end
170
+
171
+
172
+ # Saves the content of a message to a file.
173
+ #
174
+ # @param msg [Mail] The message whose content is to be saved.
175
+ # @param path [String] The path to the file to be created.
176
+ # @param options [Hash] An options hash; see FileUtils.archive for
177
+ # details of supported option settings.
178
+ def save_msg_to_file(msg, path, options = {})
179
+ FileUtils.archive(path, options)
180
+ file = File.open(path, "w")
181
+ in_html = false
182
+ msg.html_part.to_s.each_line do |line|
183
+ line.chomp!
184
+ in_html ||= (line =~ /^<html/i)
185
+ if in_html
186
+ file.puts line
187
+ file.puts "<title>#{msg.subject}</title>" if line =~ /<head>/
188
+ end
189
+ end
190
+ file.close
191
+ log.detail "Saved email body to #{path}"
192
+ end
193
+
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -0,0 +1,175 @@
1
+ require_relative '../core_ext/numeric'
2
+ require_relative '../core_ext/string'
3
+
4
+
5
+ class BatchKit
6
+
7
+ module Helpers
8
+
9
+ # Defines a number of methods to help with generating simple HTML documents
10
+ module Html
11
+
12
+ # Creates a new HTML document with a pre-defined set of styles
13
+ def create_html_document(body_text = nil, opts = {})
14
+ if body_text.is_a?(Hash)
15
+ opts = body_text
16
+ body_text = nil
17
+ end
18
+
19
+ hdr = <<-EOS.gsub(/\s{20}/, '')
20
+ <html>
21
+ #{create_head_tag(opts)}
22
+ <body>
23
+ EOS
24
+ body = [hdr]
25
+ body << body_text if body_text
26
+ yield body if block_given?
27
+ body << <<-EOS.gsub(/\s{20}/, '')
28
+ </body>
29
+ </html>
30
+ EOS
31
+ end
32
+
33
+
34
+ def create_head_tag(opts = {})
35
+ head_tag = <<-EOS.gsub(/^\w+\n$/, '')
36
+ <head>
37
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=us-ascii">
38
+ #{opts[:title] ? "<title>#{opts[:title]}</title>" : ''}
39
+ #{create_style_tag(opts)}
40
+ </head>
41
+ EOS
42
+ end
43
+
44
+
45
+ def create_style_tag(opts = {})
46
+ font = opts.fetch(:font, 'Calibri')
47
+ style_tag = <<-EOS
48
+ <style>
49
+ @font-face {font-family: #{font};}
50
+
51
+ h1 {font-family: #{font}; font-size: 16pt;}
52
+ h2 {font-family: #{font}; font-size: 14pt; margin: 1em 0em .2em;}
53
+ h3 {font-family: #{font}; font-size: 12pt; margin: 1em 0em .2em;}
54
+ body {font-family: #{font}; font-size: 11pt;}
55
+ p {margin: .2em 0em;}
56
+ table {font-family: #{font}; font-size: 10pt;
57
+ line-height: 12pt; border-collapse: collapse;}
58
+ th {background-color: #00205B; color: white;
59
+ font-size: 11pt; font-weight: bold; text-align: left;
60
+ border: 1px solid #DDDDFF; padding: 1px 5px;}
61
+ td {border: 1px solid #DDDDFF; padding: 1px 5px;}
62
+
63
+ .summary {font-size: 13pt;}
64
+ .red {background-color: white; color: #FF0000;}
65
+ .amber {background-color: white; color: #FFA500;}
66
+ .green {background-color: white; color: #33A000;}
67
+ .blue {background-color: white; color: #0000A0;}
68
+ .bold {font-weight: bold;}
69
+ .center {text-align: center;}
70
+ .right {text-align: right;}
71
+ .separator {width: 200px; border-bottom: 1px gray solid;}
72
+ </style>
73
+ </head>
74
+ <body>
75
+ EOS
76
+ end
77
+
78
+
79
+ # Creates an HTML table from +data+.
80
+ #
81
+ # @param body [Array] The HTML body to which this table will be
82
+ # appended.
83
+ # @param data [Array|Hash] The data to be added to the table.
84
+ # @param cols [Array<Symbol|String|Hash>] An array of symbols,
85
+ # strings, or Hashes. String and symbols output as-is, while the
86
+ # hash can contain various options that control the display of
87
+ # the column and the content below it, as follows:
88
+ # - :name is the name of the property. It will be used to access
89
+ # data values if +data+ is a Hash, and will be used as the
90
+ # column header unless a :label property is passed.
91
+ # - :label is the label to use as the column header.
92
+ # - :class specifies a CSS class name to assign to each cell in
93
+ # the column.
94
+ # - :show is a boolean that determines whether the column is
95
+ # output or suppressed.
96
+ # - :prefix is any text that should appear before the content.
97
+ # - :suffix is any text that should appear after the content.
98
+ def create_html_table(body, data, *cols)
99
+ cols.map!{ |col| col.is_a?(Symbol) || col.is_a?(String) ? {name: col.intern} : col }
100
+ body << "<table>"
101
+ body << "<thead><tr>"
102
+ add_table_cells(body,
103
+ cols.map{ |col| (col[:label] || col[:name]).to_s.titleize },
104
+ cols.map{ |col| {show: col.fetch(:show, true)} },
105
+ :th)
106
+ body << "</tr></thead>"
107
+ body << "<tbody>"
108
+ data.each do |row|
109
+ body << "<tr>"
110
+ add_table_cells(body, row, cols)
111
+ body << "</tr>"
112
+ end
113
+ body << "</tbody>"
114
+ body << "</table>"
115
+ end
116
+
117
+
118
+ # Adds a row of cells to a table.
119
+ #
120
+ # @param body [Array<String>] An array of lines containing the body
121
+ # of the HTML message to which this table row should be added.
122
+ # @param row [Array, Hash, Object] An Array, Hash, or Object from
123
+ # which a row of data shall be retrieved to populate the table.
124
+ # @param cols [Array<Hash>] An Array of Hashes, each containing
125
+ # details for a single column. Each Hash can contain the following
126
+ # options:
127
+ # - :class: The CSS class with which to style the column cells.
128
+ # - :show: A boolean value indicating whether the column should
129
+ # be displayed or skipped.
130
+ # - :prefix: Text to appear before the content of the cell.
131
+ # - :suffix: Text to appear after the content of the cell.
132
+ # - :value: A setting controlling how values are retrieved from
133
+ # +row+. By default, this is by index, but this setting can
134
+ # override that, and either supply a name or method to call
135
+ # on +row+, or a Proc object to invoke on row.
136
+ # @param cell_type [Symbol] Either :td (the default) or :th.
137
+ def add_table_cells(body, row, cols, cell_type = :td)
138
+ cols.each_with_index do |col, i|
139
+ cls = col[:class]
140
+ show = col.fetch(:show, true)
141
+ prefix = col.fetch(:prefix, '')
142
+ suffix = col.fetch(:suffix, '')
143
+ next if !show
144
+ val = case
145
+ when col[:value]
146
+ col[:value].call(row)
147
+ when row.is_a?(Array)
148
+ row[i]
149
+ when row.is_a?(Hash)
150
+ row[col[:name]]
151
+ when row.respond_to?(col[:name])
152
+ row.send(col[:name])
153
+ when row.respond_to?(:[])
154
+ row[col[:name]]
155
+ else
156
+ row
157
+ end
158
+ case val
159
+ when Numeric
160
+ val = val.with_commas
161
+ cls = 'right' unless cls
162
+ when Date, Time, DateTime
163
+ val = val.strftime('%H:%M:%S')
164
+ cls = 'right' unless cls
165
+ end
166
+ td = %Q{<#{cell_type}#{cls ? " class='#{cls}'" : ''}>#{prefix}#{val}#{suffix}</#{cell_type}>}
167
+ body << td
168
+ end
169
+ end
170
+
171
+ end
172
+
173
+ end
174
+
175
+ end