bookie_accounting 1.2.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -3
  3. data/README.md +4 -24
  4. data/Rakefile +9 -116
  5. data/bin/bookie-data +48 -7
  6. data/bin/bookie-send +6 -14
  7. data/bookie_accounting.gemspec +4 -3
  8. data/lib/bookie/database/group.rb +33 -0
  9. data/lib/bookie/database/job.rb +201 -0
  10. data/lib/bookie/database/job_summary.rb +268 -0
  11. data/lib/bookie/database/lock.rb +36 -0
  12. data/lib/bookie/database/system.rb +166 -0
  13. data/lib/bookie/database/system_type.rb +80 -0
  14. data/lib/bookie/database/user.rb +54 -0
  15. data/lib/bookie/database.rb +7 -805
  16. data/lib/bookie/extensions.rb +23 -44
  17. data/lib/bookie/formatter.rb +8 -4
  18. data/lib/bookie/sender.rb +12 -14
  19. data/lib/bookie/version.rb +1 -1
  20. data/snapshot/test_config.json +2 -2
  21. data/spec/config_spec.rb +2 -2
  22. data/spec/database/group_spec.rb +36 -0
  23. data/spec/database/job_spec.rb +308 -0
  24. data/spec/database/job_summary_spec.rb +302 -0
  25. data/spec/database/lock_spec.rb +41 -0
  26. data/spec/database/migration_spec.rb +44 -0
  27. data/spec/database/system_spec.rb +232 -0
  28. data/spec/database/system_type_spec.rb +68 -0
  29. data/spec/database/user_spec.rb +69 -0
  30. data/spec/formatter_spec.rb +44 -37
  31. data/spec/{comma_dump_formatter_spec.rb → formatters/comma_dump_spec.rb} +16 -30
  32. data/spec/formatters/spreadsheet_spec.rb +98 -0
  33. data/spec/{stdout_formatter_spec.rb → formatters/stdout_spec.rb} +15 -29
  34. data/spec/sender_spec.rb +92 -66
  35. data/spec/{standalone_sender_spec.rb → senders/standalone_spec.rb} +10 -9
  36. data/spec/{torque_cluster_sender_spec.rb → senders/torque_cluster_spec.rb} +9 -13
  37. data/spec/spec_helper.rb +111 -57
  38. data/todo.txt +13 -0
  39. metadata +38 -23
  40. data/rpm/activesupport.erb +0 -151
  41. data/rpm/bundle.erb +0 -71
  42. data/rpm/default.erb +0 -147
  43. data/rpm/mysql2.erb +0 -149
  44. data/rpm/pacct.erb +0 -147
  45. data/rpm/rspec-core.erb +0 -149
  46. data/rpm/sqlite3.erb +0 -147
  47. data/spec/database_spec.rb +0 -1078
  48. data/spec/spreadsheet_formatter_spec.rb +0 -114
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f140628ce83250fabffabda656f8f66a73cb89dc
4
- data.tar.gz: b0c4ed64a1adbd44986df8856eb9c01a2aa52b61
3
+ metadata.gz: 8d726372976ed3f86e19d29e2a391d233c972d5d
4
+ data.tar.gz: 99ea6555c0e94a6be3d04c5168a4a45dde020d90
5
5
  SHA512:
6
- metadata.gz: ca2c83c5150357c4cef54d129670c9ef93f530c4ec4846f333de7ea79b19a9cf88a377a69e1895459a40bffa1f254a26bab55a2684cb69e13301df3ee710ab88
7
- data.tar.gz: 1ebafa8e77bf140b8942289b53bab7d9a6345250f2e2888c8ecceb417181f7eee7f6975d09bd5ce7744af940320f27ec8c3c39f55d826521a17377e484b5964c
6
+ metadata.gz: e123b58f99c5529014a135948ccfbbcde6c2090a2436d5b9076b49bcb839fce55d1f37636713301c7eb783b3aeffbc7444d5f4b424a757ab92c3dd30464c13b5
7
+ data.tar.gz: 3fad9d7b47452444935ea70b821229c066789a04f8b929d2cf33cae6e10b8573c7ff87ebe2d581e33f5bd7082b6b4efabc5faefd85825d353feb72c17eb10308
data/Gemfile CHANGED
@@ -3,8 +3,9 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'bundler', :group => :development
6
- gem 'mocha', :group => :development
7
- gem 'rspec', :group => :development
8
- gem 'simplecov', :group => :development
6
+ gem 'byebug', :group => :test
7
+ gem 'mocha', :group => :test
8
+ gem 'rspec', :group => :test
9
+ gem 'simplecov', :group => :test
9
10
  gem 'sqlite3', :group => :development
10
11
 
data/README.md CHANGED
@@ -1,29 +1,9 @@
1
1
  # Bookie
2
-
3
- TODO: Write a gem description
2
+ A simple system to consolidate and analyze process accounting records
4
3
 
5
4
  ## Installation
5
+ See the [Installation](https://github.com/blm768/bookie/wiki/Installation) page on the wiki.
6
6
 
7
- Add this line to your application's Gemfile:
8
-
9
- gem 'bookie'
10
-
11
- And then execute:
12
-
13
- $ bundle
14
-
15
- Or install it yourself as:
16
-
17
- $ gem install bookie
18
-
19
- ## Usage
20
-
21
- TODO: Write usage instructions here
22
-
23
- ## Contributing
7
+ ##Usage
8
+ The wiki contains a [command reference](https://github.com/blm768/bookie/wiki/Command-line-utilities
24
9
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Added some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
data/Rakefile CHANGED
@@ -3,130 +3,23 @@ require 'bundler'
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
 
6
- require 'erb'
6
+ require 'find'
7
7
 
8
8
  task :default => :spec
9
9
 
10
10
  desc "Run specs"
11
11
  RSpec::Core::RakeTask.new(:spec) do |task|
12
- task.rspec_opts =%w{--color --format progress}
13
- task.pattern = 'spec/*_spec.rb'
14
- end
15
-
16
- task :rdoc do
17
- system("rdoc rdoc lib")
18
- end
19
-
20
- #Returns whether the given file is older than its dependencies (or doesn't even exist)
21
- def older(file, *dep_files)
22
- dep_files.each do |dep_file|
23
- return true if !File.exists?(file) || File.mtime(file) < File.mtime(dep_file)
24
- end
25
- false
26
- end
27
-
28
- desc "Build RPM and dependencies (designed for use on CentOS)"
29
- task :rpm_deps do
30
- gem_filenames = {
31
- "pacct" => "pacct-0.8.4-universal-linux.gem"
32
- }
33
-
34
- lockfile = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
35
-
36
- home_dir = ENV['HOME']
37
- spec_dir = File.join(home_dir, 'rpmbuild/SPECS')
38
- src_dir = File.join(home_dir, 'rpmbuild/SOURCES')
39
- rpm_dir = File.join(home_dir, 'rpmbuild/RPMS')
40
-
41
- FileUtils.mkdir_p(spec_dir)
42
- FileUtils.mkdir_p(src_dir)
43
-
44
- lockfile.specs.each do |spec|
45
- puts "#{spec.name} (#{spec.version})"
46
-
47
- spec_filename = File.join(spec_dir, "rubygem-#{spec.name}.spec")
48
-
49
- gem_filename = gem_filenames[spec.name] || "#{spec.name}-#{spec.version}.gem"
50
-
51
- system("gem fetch #{spec.name} -v #{spec.version}") unless File.exists?(gem_filename)
52
-
53
- template = "rpm/#{spec.name}.erb"
54
- template = "rpm/default.erb" unless File.exists?(template)
55
-
56
- system("gem2rpm #{gem_filename} -t #{template} > #{spec_filename}") if older(spec_filename, gem_filename, template)
57
-
58
- src = File.join(src_dir, gem_filename)
59
- FileUtils.cp(gem_filename, src_dir) if older(src, gem_filename)
60
-
61
- rpm_glob = File.join(rpm_dir, "*/rubygem-#{spec.name}-#{spec.version}-*.rpm")
62
- rpm_glob_results = Dir.glob(rpm_glob)
63
- if rpm_glob_results.length > 1
64
- puts "Multiple RPMs exist for #{spec.name} (#{spec.version}):"
65
- rpm_glob_results.each{ |r| puts " #{r}" }
66
- puts "Unable to resolve dependency due to ambiguous target"
67
- exit 1
68
- end
69
-
70
- if rpm_glob_results.length == 0 || older(rpm_glob_results[0], spec_filename)
71
- msg = `rpmbuild -ba #{spec_filename}`
72
- unless $?.success?
73
- puts msg
74
- exit 1
75
- end
12
+ task.rspec_opts =%w{--color --order rand --format progress}
13
+ paths = []
14
+ Find.find('spec') do |path|
15
+ if File.directory?(path)
16
+ paths << "#{path}/*_spec.rb"
76
17
  end
77
18
  end
19
+ task.pattern = paths.join(" ")
78
20
  end
79
21
 
80
- desc "Build RPM with all dependencies bundled"
81
- task "rpm_bundle" do
82
- gem_filenames = {
83
- "pacct" => "pacct-0.8.4-universal-linux.gem"
84
- }
85
-
86
- lockfile = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
87
-
88
- home_dir = ENV['HOME']
89
- spec_dir = File.join(home_dir, 'rpmbuild/SPECS')
90
- src_dir = File.join(home_dir, 'rpmbuild/SOURCES')
91
- rpm_dir = File.join(home_dir, 'rpmbuild/RPMS')
92
-
93
- FileUtils.mkdir_p(spec_dir)
94
- FileUtils.mkdir_p(src_dir)
95
-
96
- spec_filename = File.join(spec_dir, "bookie_accounting.spec")
97
-
98
- template = ERB.new(File.read("rpm/bundle.erb"))
99
-
100
- specs = lockfile.specs
101
-
102
- #To do: ensure that the rubygems version matches the local version?
103
- bookie_spec = Gem::Specification.load("bookie_accounting.gemspec")
104
-
105
- sources = []
106
-
107
- pwd = Dir.pwd
108
- Dir.chdir(src_dir)
109
- specs.each do |spec|
110
- puts "#{spec.name} (#{spec.version})"
111
-
112
- #bookie_spec = spec if spec.name == "bookie_accounting"
113
-
114
- gem_filename = gem_filenames[spec.name] || "#{spec.name}-#{spec.version}.gem"
115
-
116
- system("gem fetch #{spec.name} -v #{spec.version}") unless File.exists?(gem_filename)
117
-
118
- sources << gem_filename
119
- end
120
- Dir.chdir(pwd)
121
-
122
- File.open(spec_filename, "w") do |f|
123
- f.write(template.result(binding))
124
- end
125
-
126
- msg = `rpmbuild -ba #{spec_filename}`
127
- unless $?.success?
128
- puts msg
129
- exit 1
130
- end
22
+ task :rdoc do
23
+ system("rdoc rdoc lib")
131
24
  end
132
25
 
data/bin/bookie-data CHANGED
@@ -11,8 +11,12 @@ jobs = Bookie::Database::Job
11
11
  summaries = Bookie::Database::JobSummary
12
12
  systems = Bookie::Database::System
13
13
 
14
+ DEFAULT_DETAILS_PER_PAGE = 20
15
+
14
16
  config_filename = ENV['BOOKIE_CONFIG'] || '/etc/bookie/config.json'
15
17
  include_details = false
18
+ details_per_page = nil
19
+ details_page = 1
16
20
 
17
21
  output_type = :stdout
18
22
  filename = nil
@@ -21,11 +25,11 @@ time_range = nil
21
25
 
22
26
  #Process arguments
23
27
 
24
- #The first run only gets the configuration filename
28
+ #The first run only gets the configuration filename.
25
29
  ARGV.each_with_index do |value, i|
26
30
  if value == '-c' || value == '--config'
27
31
  v = ARGV[i + 1]
28
- #If the argument is missing, ignore it; OptionParser will catch it later.
32
+ #If the argument is missing or invalid, ignore the problem; OptionParser will catch it later.
29
33
  config_filename = v if v
30
34
  end
31
35
  end
@@ -43,6 +47,25 @@ opts = OptionParser.new do |opts|
43
47
  opts.on('-d', '--details', "include full details") do
44
48
  include_details = true
45
49
  end
50
+
51
+ opts.on('-p', '--page PAGE', Integer, "show only the given page of details") do |page_num|
52
+ if page_num < 1
53
+ STDERR.puts "invalid page number: #{page_num}"
54
+ exit 1
55
+ end
56
+ include_details = true
57
+ details_per_page ||= DEFAULT_DETAILS_PER_PAGE
58
+ details_page = page_num
59
+ end
60
+
61
+ opts.on('-l', '--limit COUNT', Integer, "limit the number of jobs per page") do |count|
62
+ if count < 1
63
+ STDERR.PUTS "invalid page length: #{count}"
64
+ exit 1
65
+ end
66
+ include_details = true
67
+ details_per_page = count
68
+ end
46
69
 
47
70
  opts.on('-u', '--user NAME', "filter by username") do |name|
48
71
  jobs = jobs.by_user_name(name)
@@ -68,7 +91,7 @@ opts = OptionParser.new do |opts|
68
91
  opts.on('-t', '--type TYPE', "filter by system type") do |type|
69
92
  t = Bookie::Database::SystemType.find_by_name(type)
70
93
  unless t
71
- STDERR.puts "Unknown system type '#{type}'"
94
+ STDERR.puts "unknown system type '#{type}'"
72
95
  exit 1
73
96
  end
74
97
  jobs = jobs.by_system_type(t)
@@ -89,11 +112,12 @@ opts = OptionParser.new do |opts|
89
112
  when /\.csv$/
90
113
  output_type = :comma_dump
91
114
  else
92
- $stderr.puts "Unrecognized output file extension"
115
+ STDERR.puts "unrecognized output file extension"
93
116
  exit 1
94
117
  end
95
118
  end
96
119
  end
120
+
97
121
  begin
98
122
  opts.parse!(ARGV)
99
123
  rescue OptionParser::ParseError => e
@@ -104,7 +128,24 @@ end
104
128
 
105
129
  formatter = Bookie::Formatter.new(output_type, filename)
106
130
 
107
- jobs_summary, systems_summary = formatter.print_summary(jobs, summaries, systems, time_range)
108
- jobs = jobs.by_time_range_inclusive(time_range) if time_range
109
- formatter.print_jobs(jobs.all) if include_details
131
+ formatter.print_summary(jobs, summaries, systems, time_range)
132
+
133
+ if include_details
134
+ #TODO: include separator or blank line.
135
+
136
+ jobs = jobs.by_time_range(time_range) if time_range
137
+ jobs = jobs.order(:start_time)
138
+
139
+ if details_per_page
140
+ page_start = details_per_page * (details_page - 1)
141
+ if jobs.count <= page_start
142
+ puts "No jobs on page #{details_page}"
143
+ else
144
+ jobs = jobs.offset(page_start).limit(details_per_page)
145
+ end
146
+ end
147
+ formatter.print_jobs(jobs.all_with_associations)
148
+ end
149
+
110
150
  formatter.flush
151
+
data/bin/bookie-send CHANGED
@@ -2,17 +2,6 @@
2
2
 
3
3
  require 'optparse'
4
4
 
5
- #For development:
6
- #$LOAD_PATH << 'lib'
7
-
8
- #To consider: restore for production?
9
- =begin
10
- unless Process.uid == 0
11
- $stderr.puts "This command must be run as root."
12
- exit 1
13
- end
14
- =end
15
-
16
5
  require 'bookie/sender'
17
6
 
18
7
  config_file = ENV['BOOKIE_CONFIG'] || '/etc/bookie/config.json'
@@ -60,7 +49,10 @@ config = Bookie::Config.new(config_file)
60
49
  config.connect
61
50
 
62
51
  filename = ARGV[0]
63
- fail("No operation specified") unless filename || will_create || will_decommission
52
+ unless filename || will_create || will_decommission
53
+ STDERR.puts "No operation specified"
54
+ exit 1
55
+ end
64
56
 
65
57
  if filename
66
58
  sender = Bookie::Sender.new(config)
@@ -75,7 +67,7 @@ if will_decommission
75
67
  system_hostname ||= config.hostname
76
68
  system_end_time ||= Time.now
77
69
  Bookie::Database::Lock[:systems].synchronize do
78
- system = Bookie::Database::System.active_systems.find_by_name(system_hostname)
70
+ system = Bookie::Database::System.active_systems.where(:name => system_hostname).first
79
71
  if system
80
72
  puts "Note: make sure that all of this system's jobs have been recorded in the database before decommissioning it."
81
73
  STDOUT.write "Decommission this system? "
@@ -102,7 +94,7 @@ end
102
94
  if will_create
103
95
  system_start_time ||= Time.now
104
96
  Bookie::Database::Lock[:systems].synchronize do
105
- system = Bookie::Database::System.active_systems.find_by_name(config.hostname)
97
+ system = Bookie::Database::System.active_systems.where(:name => config.hostname).first
106
98
  if system
107
99
  stderr.puts "An active system is already in the database with hostname '#{config.hostname}'."
108
100
  exit 1
@@ -5,8 +5,8 @@ Gem::Specification.new do |gem|
5
5
  gem.authors = ["Ben Merritt"]
6
6
  gem.email = ["blm768@gmail.com"]
7
7
  gem.license = "MIT"
8
- gem.description = %q{A simple system to record and query process accounting records}
9
- gem.summary = %q{A simple system to record and query process accounting records}
8
+ gem.description = %q{A simple system to consolidate and analyze process accounting records}
9
+ gem.summary = %q{A simple system to consolidate and analyze process accounting records}
10
10
  gem.homepage = "https://github.com/blm768/bookie/"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
@@ -20,11 +20,12 @@ Gem::Specification.new do |gem|
20
20
  gem.add_dependency('json')
21
21
  #We need this because Bundler has no concept of optional dependencies
22
22
  #and complains about using non-dependency gems.
23
- #To do: figure out how to remove (file issue?)
23
+ #TODO: figure out how to remove
24
24
  gem.add_dependency('mysql2')
25
25
  gem.add_dependency('pacct')
26
26
  #Introduces the old ActiveRecord mass assignment security methods
27
27
  #(until I update the database code for the new methods)
28
+ #TODO: get rid of this.
28
29
  gem.add_dependency('protected_attributes')
29
30
  gem.add_dependency('spreadsheet')
30
31
  end
@@ -0,0 +1,33 @@
1
+ require 'active_record'
2
+
3
+ require 'bookie/database/lock.rb'
4
+
5
+ module Bookie
6
+ module Database
7
+ ##
8
+ #A group of users
9
+ class Group < ActiveRecord::Base
10
+ has_many :users
11
+
12
+ ##
13
+ #Finds a group by name, creating it if it doesn't exist
14
+ #
15
+ #If <tt>known_groups</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
16
+ #
17
+ #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
18
+ def self.find_or_create!(name, known_groups = nil)
19
+ group = known_groups[name] if known_groups
20
+ unless group
21
+ Lock[:groups].synchronize do
22
+ group = find_by_name(name)
23
+ group ||= create!(:name => name)
24
+ end
25
+ known_groups[name] = group if known_groups
26
+ end
27
+ group
28
+ end
29
+
30
+ validates_presence_of :name
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,201 @@
1
+ require 'active_record'
2
+
3
+ require 'bookie/database/lock.rb'
4
+ require 'bookie/database/user.rb'
5
+ require 'bookie/database/system.rb'
6
+
7
+ module Bookie
8
+ module Database
9
+ ##
10
+ #A reported job
11
+ #
12
+ #The various filter methods can be chained to produce more complex queries.
13
+ #
14
+ #===Examples
15
+ # Bookie::Database::Job.by_user_name('root').by_system_name('localhost').find_each do |job|
16
+ # puts job.inspect
17
+ # end
18
+ #
19
+ class Job < ActiveRecord::Base
20
+ belongs_to :user
21
+ belongs_to :system
22
+ has_one :group, :through => :user
23
+ has_one :system_type, :through => :system
24
+
25
+ ##
26
+ #The time at which the job ended
27
+ def end_time
28
+ return start_time + wall_time
29
+ end
30
+
31
+ def end_time=(time)
32
+ self.wall_time = (time - start_time)
33
+ end
34
+
35
+ #To consider: disable #end_time= ?
36
+
37
+ def self.by_user(user)
38
+ where('jobs.user_id = ?', user.id)
39
+ end
40
+
41
+ ##
42
+ #Filters by user name
43
+ def self.by_user_name(user_name)
44
+ joins(:user).where('users.name = ?', user_name)
45
+ end
46
+
47
+ def self.by_system(system)
48
+ where('jobs.system_id = ?', system.id)
49
+ end
50
+
51
+ ##
52
+ #Filters by system name
53
+ def self.by_system_name(system_name)
54
+ joins(:system).where('systems.name = ?', system_name)
55
+ end
56
+
57
+ ##
58
+ #Filters by group name
59
+ def self.by_group_name(group_name)
60
+ group = Group.find_by_name(group_name)
61
+ return joins(:user).where('users.group_id = ?', group.id) if group
62
+ self.none
63
+ end
64
+
65
+ ##
66
+ #Filters by system type
67
+ def self.by_system_type(system_type)
68
+ joins(:system).where('systems.system_type_id = ?', system_type.id)
69
+ end
70
+
71
+ ##
72
+ #Filters by command name
73
+ def self.by_command_name(c_name)
74
+ where('jobs.command_name = ?', c_name)
75
+ end
76
+
77
+ ##
78
+ #Finds all jobs that were running at some point in a given time range
79
+ def self.by_time_range(time_range)
80
+ if time_range.empty?
81
+ self.none
82
+ else
83
+ time_range = time_range.exclusive
84
+ where('jobs.end_time > ? AND jobs.start_time < ?', time_range.begin, time_range.end)
85
+ end
86
+ end
87
+
88
+ ##
89
+ #Similar to #by_time_range, but only includes jobs that are completely contained within the
90
+ #time range
91
+ def self.within_time_range(time_range)
92
+ if time_range.empty?
93
+ self.none
94
+ else
95
+ time_range = time_range.exclusive
96
+ #The second "<=" operator _is_ intentional.
97
+ #If the job's end_time is one second past the last value in the range, it
98
+ #is still considered to be contained within time_range because it did not
99
+ #run outside time_range; it only _stopped_ outside it.
100
+ where('? <= jobs.start_time AND jobs.end_time <= ?', time_range.begin, time_range.end)
101
+ end
102
+ end
103
+
104
+ ##
105
+ #Finds all jobs that overlap the edges of the given time range
106
+ def self.overlapping_edges(time_range)
107
+ if time_range.empty?
108
+ self.none
109
+ else
110
+ time_range = time_range.exclusive
111
+ query_str = ['begin', 'end'].map{ |edge| "(jobs.start_time < :#{edge} AND jobs.end_time > :#{edge})" }.join(" OR ")
112
+ where(query_str, {:begin => time_range.begin, :end => time_range.end})
113
+ end
114
+ end
115
+
116
+ ##
117
+ #Produces a summary of the jobs in the given time interval
118
+ #
119
+ #Returns a hash with the following fields:
120
+ #- <tt>:num_jobs</tt>: the number of jobs in the interval
121
+ #- <tt>:successful</tt>: the number of jobs that have completed successfully
122
+ #- <tt>:cpu_time</tt>: the total CPU time used
123
+ #- <tt>:memory_time</tt>: the sum of memory * wall_time for all jobs in the interval
124
+ #
125
+ #This method should probably not be chained with other queries that filter by start/end time.
126
+ #It also doesn't work with the limit() method.
127
+ #
128
+ #The time_range parameter is always treated as if time_range.exclude_end? is true.
129
+ def self.summary(time_range = nil)
130
+ jobs = self
131
+
132
+ num_jobs = 0
133
+ successful = 0
134
+ cpu_time = 0.0
135
+ memory_time = 0
136
+
137
+ if time_range
138
+ unless time_range.empty?
139
+ time_range = time_range.exclusive
140
+
141
+ #Any jobs that are completely within the time range can
142
+ #be summarized as-is.
143
+ jobs_within = jobs.within_time_range(time_range)
144
+ #TODO: optimize into one query?
145
+ num_jobs += jobs_within.count
146
+ successful += jobs_within.where(:exit_code => 0).count
147
+ cpu_time += jobs_within.sum(:cpu_time)
148
+ memory_time += jobs_within.sum('jobs.memory * jobs.wall_time')
149
+
150
+ #Any jobs that overlap an edge of the time range
151
+ #must be clipped.
152
+ jobs_overlapped = jobs.overlapping_edges(time_range)
153
+ jobs_overlapped.find_each do |job|
154
+ start_time = [job.start_time, time_range.begin].max
155
+ end_time = [job.end_time, time_range.end].min
156
+ clipped_wall_time = end_time.to_i - start_time.to_i
157
+ if job.wall_time != 0
158
+ cpu_time += Float(job.cpu_time * clipped_wall_time) / job.wall_time
159
+ memory_time += job.memory * clipped_wall_time
160
+ end
161
+ num_jobs += 1
162
+ successful += 1 if job.exit_code == 0
163
+ end
164
+ end
165
+ else
166
+ #There's no time_range constraint; just summarize everything.
167
+ num_jobs = jobs.count
168
+ successful = jobs.where(:exit_code => 0).count
169
+ cpu_time = jobs.sum(:cpu_time)
170
+ memory_time = jobs.sum('jobs.memory * jobs.wall_time')
171
+ end
172
+
173
+ return {
174
+ :num_jobs => num_jobs,
175
+ :successful => successful,
176
+ :cpu_time => cpu_time.round,
177
+ :memory_time => memory_time,
178
+ }
179
+ end
180
+
181
+ before_save do
182
+ write_attribute(:end_time, end_time)
183
+ end
184
+
185
+ before_update do
186
+ write_attribute(:end_time, end_time)
187
+ end
188
+
189
+ validates_presence_of :user, :system, :cpu_time,
190
+ :start_time, :wall_time, :memory, :exit_code
191
+
192
+ validates_each :command_name do |record, attr, value|
193
+ record.errors.add(attr, 'must not be nil') if value == nil
194
+ end
195
+
196
+ validates_each :cpu_time, :wall_time, :memory do |record, attr, value|
197
+ record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
198
+ end
199
+ end
200
+ end
201
+ end