jobmanager 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -1
- data/Manifest.txt +6 -2
- data/README.txt +66 -32
- data/Rakefile +2 -2
- data/examples/jobmanager.yaml +19 -9
- data/lib/jobmanager/application.rb +32 -31
- data/lib/jobmanager/applicationconfig.rb +15 -3
- data/lib/jobmanager/applicationoptionparser.rb +19 -5
- data/lib/jobmanager/openonfirstwritefile.rb +65 -0
- data/lib/jobmanager/system.rb +15 -8
- data/lib/jobmanager/teestream.rb +21 -39
- data/lib/jobmanager/timestampedstream.rb +89 -0
- data/test/configs/all_values.yaml +7 -1
- data/test/configs/jobmanager_4_bad_job_logs_directory_1.yaml +1 -1
- data/test/configs/{jobmanager_4_bad_job_logs_directory_2.yaml → jobmanager_5_bad_job_log_file.yaml} +0 -0
- data/test/configs/jobmanager_6.yaml +9 -0
- data/test/configs/jobmanager_7_no_log_rotation.yaml +15 -0
- data/test/test_applicationlogger.rb +3 -0
- data/test/test_config.rb +33 -22
- data/test/test_jobmanager.rb +133 -62
- data/test/test_openonfirstwritefile.rb +71 -0
- data/test/test_system.rb +3 -3
- data/test/test_teestream.rb +45 -2
- metadata +11 -6
- data/examples/mysql_backup.rb +0 -18
@@ -13,13 +13,13 @@ module JobManager
|
|
13
13
|
#
|
14
14
|
class ApplicationConfig < ConfigToolkit::BaseConfig
|
15
15
|
|
16
|
-
EMAIL_CONDITIONS = [ :always, :never, :on_failure ]
|
16
|
+
EMAIL_CONDITIONS = [ :always, :never, :on_failure, :on_job_output_or_failure ]
|
17
17
|
CENTRAL_LOG_MODES = [ :syslog, :file ]
|
18
18
|
|
19
19
|
add_required_param(:command, String)
|
20
20
|
|
21
21
|
add_required_param(:job_name, String)
|
22
|
-
|
22
|
+
|
23
23
|
add_required_param(:job_logs_directory, Pathname)
|
24
24
|
|
25
25
|
add_optional_param(:email_condition, Symbol, :on_failure) do |value|
|
@@ -71,9 +71,21 @@ module JobManager
|
|
71
71
|
"jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>")
|
72
72
|
|
73
73
|
add_optional_param(:email_from_address, String)
|
74
|
-
|
74
|
+
|
75
75
|
add_optional_param(:email_to_address, String)
|
76
76
|
|
77
|
+
add_optional_param(:rotate_job_log, ConfigToolkit::Boolean, true)
|
78
|
+
|
79
|
+
add_optional_param(:job_log_prepend_date_time, ConfigToolkit::Boolean, false)
|
80
|
+
|
81
|
+
add_optional_param(:job_log_prepend_date_time_format, String, '%F_%T') do |value|
|
82
|
+
begin
|
83
|
+
DateTime.now.strftime(value)
|
84
|
+
rescue => e
|
85
|
+
raise_error("Invalid format #{e.message}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
77
89
|
def validate_all_values()
|
78
90
|
if (self.central_log_mode == :file && !self.central_log_file?)
|
79
91
|
raise_error("If central_log_mode is set to file, central_log_file must be specified.")
|
@@ -22,7 +22,7 @@ module JobManager
|
|
22
22
|
# The command line arguments.
|
23
23
|
# ====Returns:
|
24
24
|
# A hash of the command line options and arugments.
|
25
|
-
#
|
25
|
+
#
|
26
26
|
def self.parse(program_name, args)
|
27
27
|
options = {}
|
28
28
|
opts = OptionParser.new
|
@@ -89,9 +89,23 @@ module JobManager
|
|
89
89
|
"Timeout (in seconds) after which the script is killed.") do |timeout|
|
90
90
|
options["timeout"] = timeout
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
|
+
opts.on("-p", "--[no-]job_log_prepend_date_time",
|
94
|
+
"Whether to prepend the date/time to each line of the job log file.") do |value|
|
95
|
+
options["job_log_prepend_date_time"] = value
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on("-g", "--job_log_prepend_date_time_format FORMAT",
|
99
|
+
"The format of the date/time to be prepended to each line of the job log file.") do |format|
|
100
|
+
options["job_log_prepend_date_time_format"] = format
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on("-b", "--[no-]rotate_job_log",
|
104
|
+
"Whether the job log file should be rotated.") do |value|
|
105
|
+
options["rotate_job_log"] = value
|
106
|
+
end
|
107
|
+
|
93
108
|
opts.on("-e", "--email_settings_file PATH",
|
94
|
-
String,
|
95
109
|
"The configuration file for the simpleemail gem.",
|
96
110
|
"Note: If a relative path is specified, it will be considered to be relative to the",
|
97
111
|
"location of the configuration file.") do |file|
|
@@ -102,8 +116,8 @@ module JobManager
|
|
102
116
|
"The email address from which jobmanager sends results.") do |address|
|
103
117
|
options["email_from_address"] = address
|
104
118
|
end
|
105
|
-
|
106
|
-
opts.on("-
|
119
|
+
|
120
|
+
opts.on("-u", "--email_to_address ADDRESS",
|
107
121
|
"The email address to which jobmanager sends results.") do |address|
|
108
122
|
options["email_to_address"] = address
|
109
123
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module JobManager
|
2
|
+
|
3
|
+
#
|
4
|
+
# This class represents a stream that wraps a file stream.
|
5
|
+
# Its only purpose is to defer the opening of the file to
|
6
|
+
# first write. Thus, if the file is never written to, it
|
7
|
+
# is never created!
|
8
|
+
#
|
9
|
+
class OpenOnFirstWriteFile
|
10
|
+
|
11
|
+
def initialize(file_name, mode_string)
|
12
|
+
@file_name = file_name
|
13
|
+
@mode_string = mode_string
|
14
|
+
@stream = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def close()
|
18
|
+
# This method needs to be explicitly defined. If it wasn't,
|
19
|
+
# method_missing would create the file in order to close it,
|
20
|
+
# defeating the purpose of this class.
|
21
|
+
|
22
|
+
if (@stream)
|
23
|
+
@stream.close()
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def flush()
|
28
|
+
# This method needs to be explicitly defined. If it wasn't,
|
29
|
+
# method_missing would create the file in order to flush it,
|
30
|
+
# defeating the purpose of this class.
|
31
|
+
|
32
|
+
if (@stream)
|
33
|
+
@stream.flush()
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# ====Description:
|
39
|
+
# This method first opens the file (if not already opened), and then
|
40
|
+
# forwards the call to the file stream.
|
41
|
+
#
|
42
|
+
def method_missing(*args, &block)
|
43
|
+
method = args.shift
|
44
|
+
|
45
|
+
if (!@stream)
|
46
|
+
directory = File.dirname(@file_name)
|
47
|
+
begin
|
48
|
+
FileUtils.mkdir_p(directory)
|
49
|
+
rescue => e
|
50
|
+
raise "Failed to create directory #{directory}, Error (#{e.class}): #{e.message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
@stream = File.new(@file_name, @mode_string)
|
55
|
+
rescue => e
|
56
|
+
raise "Failed to open file #{@file_name}, Error (#{e.class}): #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@stream.send(method, *args)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
data/lib/jobmanager/system.rb
CHANGED
@@ -62,6 +62,7 @@ module JobManager
|
|
62
62
|
read_pipe, write_pipe = IO.pipe
|
63
63
|
original_time = Time.now()
|
64
64
|
success = false
|
65
|
+
job_log_write_success = true
|
65
66
|
timeout = optional_args[:timeout]
|
66
67
|
env_path = optional_args[:command_path] ? optional_args[:command_path] : ENV["PATH"]
|
67
68
|
|
@@ -122,7 +123,12 @@ module JobManager
|
|
122
123
|
end
|
123
124
|
|
124
125
|
if (bytes.length() > 0)
|
125
|
-
|
126
|
+
begin
|
127
|
+
log_stream << bytes
|
128
|
+
rescue => e
|
129
|
+
logger.error("Error writing to job log: #{e.message}")
|
130
|
+
job_log_write_success = false
|
131
|
+
end
|
126
132
|
end
|
127
133
|
end
|
128
134
|
|
@@ -133,20 +139,21 @@ module JobManager
|
|
133
139
|
if (Process.waitpid(child_pid, Process::WNOHANG))
|
134
140
|
|
135
141
|
# success? will return nil if the child didn't exit normally
|
136
|
-
|
142
|
+
child_success = $CHILD_STATUS.success?() ? true : false;
|
137
143
|
pid = $CHILD_STATUS.pid()
|
138
144
|
|
139
|
-
if (
|
140
|
-
logger.info("
|
145
|
+
if (child_success)
|
146
|
+
logger.info("Job exited successfully")
|
141
147
|
else
|
142
148
|
if ($CHILD_STATUS.exited?())
|
143
|
-
logger.error("Failed! - exited normally with exit code: #{$CHILD_STATUS.exitstatus()}.")
|
149
|
+
logger.error("Failed! - Job exited normally with exit code: #{$CHILD_STATUS.exitstatus()}.")
|
144
150
|
elsif ($CHILD_STATUS.signaled?())
|
145
|
-
logger.error("Failed! - exited abnormally due to signal: #{$CHILD_STATUS.termsig()}.")
|
151
|
+
logger.error("Failed! - Job exited abnormally due to signal: #{$CHILD_STATUS.termsig()}.")
|
146
152
|
else
|
147
|
-
logger.error("Failed! - exited abnormally: #{$CHILD_STATUS.inspect()}")
|
153
|
+
logger.error("Failed! - Job exited abnormally: #{$CHILD_STATUS.inspect()}")
|
148
154
|
end
|
149
155
|
end
|
156
|
+
success = child_success && job_log_write_success
|
150
157
|
|
151
158
|
break
|
152
159
|
else
|
@@ -213,7 +220,7 @@ module JobManager
|
|
213
220
|
exit(1)
|
214
221
|
end
|
215
222
|
end
|
216
|
-
|
223
|
+
|
217
224
|
|
218
225
|
#
|
219
226
|
# ====Description:
|
data/lib/jobmanager/teestream.rb
CHANGED
@@ -6,8 +6,10 @@ module JobManager
|
|
6
6
|
# This class represents a stream whose only purpose is to forward
|
7
7
|
# messages on to a list of configured streams.
|
8
8
|
#
|
9
|
-
class TeeStream
|
9
|
+
class TeeStream
|
10
10
|
|
11
|
+
attr_reader :streams
|
12
|
+
|
11
13
|
#
|
12
14
|
# ====Description:
|
13
15
|
# This method creates an TeeStream instance initialized with the
|
@@ -30,49 +32,29 @@ module JobManager
|
|
30
32
|
# The name of the stream requested.
|
31
33
|
# ====Returns:
|
32
34
|
# The associated stream if it exists, nil otherwise
|
33
|
-
def
|
35
|
+
def stream(name)
|
34
36
|
@streams[name]
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# streams. Note: This class is derived from IO, and thus only
|
41
|
-
# needs to implement the write method in order for all of the
|
42
|
-
# write-related methods (<<, print, puts, putc, etc) to work free
|
43
|
-
# of charge, as all of these methods call write.
|
44
|
-
# ====Parameters:
|
45
|
-
# [str]
|
46
|
-
# The string to be written.
|
47
|
-
# ====Returns:
|
48
|
-
# The length of the string.
|
49
|
-
#
|
50
|
-
def write(str)
|
51
|
-
@streams.each_value do |stream|
|
52
|
-
stream.write(str)
|
53
|
-
end
|
39
|
+
def method_missing(*args, &block)
|
40
|
+
is_exception_saved = false
|
41
|
+
saved_name = nil
|
54
42
|
|
55
|
-
|
43
|
+
saved_e = nil
|
44
|
+
@streams.each do |name, stream|
|
45
|
+
begin
|
46
|
+
stream.send(*args)
|
47
|
+
rescue => saved_e
|
48
|
+
is_exception_saved = true
|
49
|
+
saved_name = name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if (is_exception_saved)
|
54
|
+
@streams.delete(saved_name)
|
55
|
+
raise "Caught exception on stream #{saved_name}, Error (#{saved_e.class}): #{saved_e.message}"
|
56
|
+
end
|
56
57
|
end
|
57
58
|
|
58
|
-
#
|
59
|
-
# ====Description:
|
60
|
-
# Flush all the configured streams.
|
61
|
-
#
|
62
|
-
def flush
|
63
|
-
@streams.each_value do |stream|
|
64
|
-
stream.flush()
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
#
|
69
|
-
# ====Description:
|
70
|
-
# Close all the configured streams.
|
71
|
-
#
|
72
|
-
def close
|
73
|
-
@streams.each_value do |stream|
|
74
|
-
stream.close()
|
75
|
-
end
|
76
|
-
end
|
77
59
|
end
|
78
60
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module JobManager
|
6
|
+
|
7
|
+
#
|
8
|
+
# This class represents a stream whose only purpose is to forward
|
9
|
+
# messages on to a list of configured streams.
|
10
|
+
#
|
11
|
+
class TimestampedStream < IO
|
12
|
+
|
13
|
+
attr_reader :stream
|
14
|
+
#
|
15
|
+
# ====Description:
|
16
|
+
# This method creates an TeeStream instance initialized with the
|
17
|
+
# hash of streams passed in. When a message is written to this
|
18
|
+
# stream, it will be forwarded to all hash values passed in.
|
19
|
+
# ====Parameters:
|
20
|
+
# [streams]
|
21
|
+
# A hash table of stream name to stream.
|
22
|
+
#
|
23
|
+
def initialize(stream,
|
24
|
+
date_time_format = "%F_%T")
|
25
|
+
@stream = stream
|
26
|
+
@date_time_format = date_time_format
|
27
|
+
@is_start_of_line = true
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# ====Description:
|
32
|
+
# This method forwards the string message to all configured
|
33
|
+
# streams. Note: This class is derived from IO, and thus only
|
34
|
+
# needs to implement the write method in order for all of the
|
35
|
+
# write-related methods (<<, print, puts, putc, etc) to work free
|
36
|
+
# of charge, as all of these methods call write.
|
37
|
+
# ====Parameters:
|
38
|
+
# [str]
|
39
|
+
# The string to be written.
|
40
|
+
# ====Returns:
|
41
|
+
# The length of the string.
|
42
|
+
#
|
43
|
+
def write(str)
|
44
|
+
if (str.length() == 0)
|
45
|
+
return 0
|
46
|
+
end
|
47
|
+
|
48
|
+
str = str.dup()
|
49
|
+
now = DateTime.now().strftime(@date_time_format)
|
50
|
+
|
51
|
+
# if string ends with a new line, strip it off and save it.
|
52
|
+
if (str.sub!(/\n\z/, ""))
|
53
|
+
ends_with_new_line = true
|
54
|
+
end
|
55
|
+
|
56
|
+
# replace all of the new lines with a new line followed by the
|
57
|
+
# current date/time.
|
58
|
+
str.gsub!(/\n/, "\n#{now} ")
|
59
|
+
|
60
|
+
if (@is_start_of_line)
|
61
|
+
str = "#{now} #{str}"
|
62
|
+
@is_start_of_line = false
|
63
|
+
end
|
64
|
+
|
65
|
+
if (ends_with_new_line)
|
66
|
+
str += "\n"
|
67
|
+
@is_start_of_line = true
|
68
|
+
end
|
69
|
+
|
70
|
+
@stream.write(str)
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# ====Description:
|
75
|
+
# Flush all the configured streams.
|
76
|
+
#
|
77
|
+
def flush
|
78
|
+
@stream.flush()
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# ====Description:
|
83
|
+
# Close all the configured streams.
|
84
|
+
#
|
85
|
+
def close
|
86
|
+
@stream.close()
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -30,4 +30,10 @@ command: "echo hello"
|
|
30
30
|
|
31
31
|
email_from_address: "sender_address@gmail.com"
|
32
32
|
|
33
|
-
email_to_address: "receiver_address@gmail.com"
|
33
|
+
email_to_address: "receiver_address@gmail.com"
|
34
|
+
|
35
|
+
rotate_job_log: false
|
36
|
+
|
37
|
+
job_log_prepend_date_time: true
|
38
|
+
|
39
|
+
job_log_prepend_date_time_format: '%F'
|
data/test/configs/{jobmanager_4_bad_job_logs_directory_2.yaml → jobmanager_5_bad_job_log_file.yaml}
RENAMED
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
job_logs_directory: temp/logs
|
2
|
+
|
3
|
+
central_log_mode: file
|
4
|
+
|
5
|
+
central_log_file: temp/logs/jobmanager.log
|
6
|
+
|
7
|
+
rotate_job_log: false
|
8
|
+
|
9
|
+
email_condition: on_job_output_or_failure
|
10
|
+
|
11
|
+
email_settings_file: email_settings.rb
|
12
|
+
|
13
|
+
job_log_prepend_date_time: true
|
14
|
+
|
15
|
+
job_log_prepend_date_time_format: '%F'
|
data/test/test_config.rb
CHANGED
@@ -51,6 +51,9 @@ class ConfigTest < Test::Unit::TestCase
|
|
51
51
|
assert_equal("echo hello", config.command)
|
52
52
|
assert_equal("sender_address@gmail.com", config.email_from_address)
|
53
53
|
assert_equal("receiver_address@gmail.com", config.email_to_address)
|
54
|
+
assert_equal(false, config.rotate_job_log)
|
55
|
+
assert_equal(true, config.job_log_prepend_date_time)
|
56
|
+
assert_equal("%F", config.job_log_prepend_date_time_format)
|
54
57
|
|
55
58
|
result = 'Success'
|
56
59
|
host_name = "jackfruit"
|
@@ -77,11 +80,13 @@ class ConfigTest < Test::Unit::TestCase
|
|
77
80
|
assert_equal(nil, config.central_log_file)
|
78
81
|
assert_equal("jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>",
|
79
82
|
config.email_subject)
|
80
|
-
|
81
|
-
|
83
|
+
assert_equal(ENV['PATH'], config.command_path)
|
82
84
|
assert_equal(nil, config.timeout)
|
83
85
|
assert_equal(nil, config.email_to_address)
|
84
86
|
assert_equal(nil, config.email_from_address)
|
87
|
+
assert_equal(true, config.rotate_job_log)
|
88
|
+
assert_equal(false, config.job_log_prepend_date_time)
|
89
|
+
assert_equal("%F_%T", config.job_log_prepend_date_time_format)
|
85
90
|
end
|
86
91
|
|
87
92
|
def test_good_configs
|
@@ -101,7 +106,7 @@ class ConfigTest < Test::Unit::TestCase
|
|
101
106
|
confirm_bad_config_throws_error("bad_number_of_job_logs.yaml", msg)
|
102
107
|
|
103
108
|
msg = "error setting email_condition with value invalid: "
|
104
|
-
msg << "email_condition must be one of the following values: always, never, on_failure."
|
109
|
+
msg << "email_condition must be one of the following values: always, never, on_failure, on_job_output_or_failure."
|
105
110
|
confirm_bad_config_throws_error("bad_email_condition.yaml", msg)
|
106
111
|
|
107
112
|
msg = "JobManager::ApplicationConfig#validate_all_values error: "
|
@@ -140,25 +145,28 @@ class ConfigTest < Test::Unit::TestCase
|
|
140
145
|
|
141
146
|
# match the config specified in all_values.yaml
|
142
147
|
email_subject = "jobmanager results for <%=job_name%> on <%=host_name%> : <%=result%>"
|
143
|
-
args = [ "--job_name",
|
144
|
-
"--job_logs_directory",
|
145
|
-
"--email_condition",
|
146
|
-
"--central_log_mode",
|
147
|
-
"--central_log_file",
|
148
|
-
"--number_of_job_logs",
|
148
|
+
args = [ "--job_name", "ze_echoer",
|
149
|
+
"--job_logs_directory", "temp/logs",
|
150
|
+
"--email_condition", "always",
|
151
|
+
"--central_log_mode", "file",
|
152
|
+
"--central_log_file", "temp/logs/jobmanager.log",
|
153
|
+
"--number_of_job_logs", "4",
|
149
154
|
"--date_time_extension",
|
150
|
-
"--date_time_extension_format",
|
155
|
+
"--date_time_extension_format", "%F",
|
151
156
|
"--zip_rotated_log_file",
|
152
|
-
"--timeout",
|
153
|
-
"--email_settings_file",
|
154
|
-
"--email_from_address",
|
155
|
-
"--email_to_address",
|
156
|
-
"--command_path",
|
157
|
-
"--email_subject",
|
157
|
+
"--timeout", "1800",
|
158
|
+
"--email_settings_file", "email_settings.rb",
|
159
|
+
"--email_from_address", "sender_address@gmail.com",
|
160
|
+
"--email_to_address", "receiver_address@gmail.com",
|
161
|
+
"--command_path", "/usr/designingpatterns/bin",
|
162
|
+
"--email_subject", email_subject,
|
163
|
+
"--no-rotate_job_log",
|
164
|
+
"--job_log_prepend_date_time",
|
165
|
+
"--job_log_prepend_date_time_format", "%F",
|
158
166
|
"--debug",
|
159
167
|
"echo hello"
|
160
168
|
]
|
161
|
-
|
169
|
+
|
162
170
|
hash = JobManager::ApplicationOptionParser.parse(program_name, args)
|
163
171
|
config = JobManager::ApplicationConfig.load(ConfigToolkit::HashReader.new(hash))
|
164
172
|
confirm_all_values(config)
|
@@ -185,12 +193,15 @@ class ConfigTest < Test::Unit::TestCase
|
|
185
193
|
config_file = File.join(CONFIG_DIR, "incomplete_1.yaml")
|
186
194
|
yaml_reader = ConfigToolkit::YAMLReader.new(config_file)
|
187
195
|
|
188
|
-
args = [ "--job_name",
|
189
|
-
"--email_to_address",
|
190
|
-
"--central_log_file",
|
191
|
-
"--email_settings_file",
|
192
|
-
"--timeout",
|
196
|
+
args = [ "--job_name", "ze_echoer",
|
197
|
+
"--email_to_address", "receiver_address@gmail.com",
|
198
|
+
"--central_log_file", "temp/logs/jobmanager.log",
|
199
|
+
"--email_settings_file", "email_settings.rb",
|
200
|
+
"--timeout", "1800",
|
193
201
|
"--zip_rotated_log_file",
|
202
|
+
"--no-rotate_job_log",
|
203
|
+
"--job_log_prepend_date_time",
|
204
|
+
"--job_log_prepend_date_time_format", "%F",
|
194
205
|
"--debug",
|
195
206
|
"echo hello"
|
196
207
|
]
|