bookie_accounting 1.2.3 → 2.0.0

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