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,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