bookie_accounting 0.0.1
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.
- data/.gitignore +19 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +15 -0
- data/bin/bookie-create-tables +52 -0
- data/bin/bookie-data +102 -0
- data/bin/bookie-send +110 -0
- data/bookie_accounting.gemspec +28 -0
- data/lib/bookie.rb +11 -0
- data/lib/bookie/config.rb +101 -0
- data/lib/bookie/database.rb +656 -0
- data/lib/bookie/formatter.rb +149 -0
- data/lib/bookie/formatters/comma_dump.rb +24 -0
- data/lib/bookie/formatters/spreadsheet.rb +45 -0
- data/lib/bookie/formatters/stdout.rb +32 -0
- data/lib/bookie/sender.rb +108 -0
- data/lib/bookie/senders/standalone.rb +37 -0
- data/lib/bookie/senders/torque_cluster.rb +166 -0
- data/lib/bookie/version.rb +4 -0
- data/snapshot/config.json +12 -0
- data/snapshot/default.json +11 -0
- data/snapshot/pacct +0 -0
- data/snapshot/pacct_large +0 -0
- data/snapshot/pacct_test_config.json +14 -0
- data/snapshot/test_config.json +13 -0
- data/snapshot/torque +3 -0
- data/snapshot/torque_invalid_lines +5 -0
- data/snapshot/torque_invalid_lines_2 +4 -0
- data/snapshot/torque_invalid_lines_3 +3 -0
- data/snapshot/torque_large +100 -0
- data/spec/comma_dump_formatter_spec.rb +56 -0
- data/spec/config_spec.rb +55 -0
- data/spec/database_spec.rb +625 -0
- data/spec/formatter_spec.rb +93 -0
- data/spec/sender_spec.rb +104 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/spreadsheet_formatter_spec.rb +112 -0
- data/spec/standalone_sender_spec.rb +40 -0
- data/spec/stdout_formatter_spec.rb +66 -0
- data/spec/torque_cluster_sender_spec.rb +111 -0
- metadata +227 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
require 'bookie/config'
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
module Bookie
|
|
6
|
+
#Contains ActiveRecord structures for the central database
|
|
7
|
+
#
|
|
8
|
+
#For a list of fields in the various models, see {Database Tables}[link:rdoc/database_rdoc.html]
|
|
9
|
+
module Database
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
#Represents a lock on a table
|
|
13
|
+
#
|
|
14
|
+
#Based on http://kseebaldt.blogspot.com/2007/11/synchronizing-using-active-record.html
|
|
15
|
+
#
|
|
16
|
+
#This should probably not be called within a transaction block; the lock might not be released
|
|
17
|
+
#until the outer transaction completes, and even if it is released before then, there might be
|
|
18
|
+
#concurrency issues.
|
|
19
|
+
class Lock < ActiveRecord::Base
|
|
20
|
+
##
|
|
21
|
+
#Acquires the lock, runs the given block, and releases the lock when finished
|
|
22
|
+
def synchronize
|
|
23
|
+
transaction do
|
|
24
|
+
#Lock this record to be inaccessible to others until this transaction is completed.
|
|
25
|
+
self.class.lock.find(id)
|
|
26
|
+
yield
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@locks = {}
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
#Returns a lock by name
|
|
34
|
+
def self.[](name)
|
|
35
|
+
@locks[name.to_sym] ||= find_by_name(name.to_s) or raise "Unable to find lock '#{name}'"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
validates_presence_of :name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
#A reported job
|
|
43
|
+
#
|
|
44
|
+
#The various filter methods can be chained to produce more complex queries.
|
|
45
|
+
#
|
|
46
|
+
#===Examples
|
|
47
|
+
# Bookie::Database::Job.by_user_name('root').by_system_name('localhost').find_each do |job|
|
|
48
|
+
# puts job.inspect
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
class Job < ActiveRecord::Base
|
|
52
|
+
belongs_to :user
|
|
53
|
+
belongs_to :system
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
#The time at which the job ended
|
|
57
|
+
def end_time
|
|
58
|
+
return start_time + wall_time
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
#Filters by user name
|
|
63
|
+
def self.by_user_name(user_name)
|
|
64
|
+
joins(:user).where('users.name = ?', user_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
#Filters by system name
|
|
69
|
+
def self.by_system_name(system_name)
|
|
70
|
+
joins(:system).where('systems.name = ?', system_name)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
#Filters by group name
|
|
75
|
+
def self.by_group_name(group_name)
|
|
76
|
+
group = Group.find_by_name(group_name)
|
|
77
|
+
return joins(:user).where('group_id = ?', group.id) if group
|
|
78
|
+
limit(0)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
#Filters by system type
|
|
83
|
+
def self.by_system_type(system_type)
|
|
84
|
+
joins(:system).where('system_type_id = ?', system_type.id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
#Filters by command name
|
|
89
|
+
def self.by_command_name(c_name)
|
|
90
|
+
where('command_name = ?', c_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
#Filters by a range of start times
|
|
95
|
+
def self.by_start_time_range(start_min, start_max)
|
|
96
|
+
where('? <= start_time AND start_time < ?', start_min, start_max)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
#Filters by a range of end times
|
|
101
|
+
def self.by_end_time_range(end_min, end_max)
|
|
102
|
+
where('? <= end_time AND end_time < ?', end_min, end_max)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
#Finds all jobs whose running intervals overlap the given time range
|
|
107
|
+
def self.by_time_range_inclusive(min_time, max_time)
|
|
108
|
+
raise ArgumentError.new('Max time must be greater than or equal to min time') if max_time < min_time
|
|
109
|
+
where('start_time < ? AND end_time > ?', max_time, min_time)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
#Produces a summary of the jobs in the given time interval
|
|
114
|
+
#
|
|
115
|
+
#Returns a hash with the following fields:
|
|
116
|
+
#- <tt>:jobs</tt>: an array of all jobs in the interval
|
|
117
|
+
#- <tt>:wall_time</tt>: the sum of all the jobs' wall times
|
|
118
|
+
#- <tt>:cpu_time</tt>: the total CPU time used
|
|
119
|
+
#- <tt>:memory_time</tt>: the sum of memory * wall_time for all jobs in the interval
|
|
120
|
+
#- <tt>:successful</tt>: the proportion of jobs that completed successfully
|
|
121
|
+
#
|
|
122
|
+
#This method should probably not be used with other queries that filter by start/end time.
|
|
123
|
+
def self.summary(min_time = nil, max_time = nil)
|
|
124
|
+
jobs = self
|
|
125
|
+
if min_time
|
|
126
|
+
raise ArgumentError.new('Max time must be specified with min time') unless max_time
|
|
127
|
+
jobs = jobs.by_time_range_inclusive(min_time, max_time)
|
|
128
|
+
end
|
|
129
|
+
jobs = jobs.where('cpu_time > 0').all_with_relations
|
|
130
|
+
wall_time = 0
|
|
131
|
+
cpu_time = 0
|
|
132
|
+
successful_jobs = 0
|
|
133
|
+
memory_time = 0
|
|
134
|
+
#To consider: job.end_time should be <= Time.now, but it might be good to check for that.
|
|
135
|
+
#Maybe in a database consistency checker tool?
|
|
136
|
+
#What if the system clock is off?
|
|
137
|
+
#Also consider a check for system start times.
|
|
138
|
+
jobs.each do |job|
|
|
139
|
+
job_start_time = job.start_time
|
|
140
|
+
job_end_time = job.end_time
|
|
141
|
+
if min_time
|
|
142
|
+
job_start_time = [job_start_time, min_time].max
|
|
143
|
+
job_end_time = [job_end_time, max_time].min
|
|
144
|
+
end
|
|
145
|
+
clipped_wall_time = job_end_time.to_i - job_start_time.to_i
|
|
146
|
+
wall_time += clipped_wall_time
|
|
147
|
+
if job.wall_time != 0
|
|
148
|
+
cpu_time += job.cpu_time * clipped_wall_time / job.wall_time
|
|
149
|
+
#To consider: what should I do about jobs that only report a max memory value?
|
|
150
|
+
memory_time += job.memory * clipped_wall_time
|
|
151
|
+
end
|
|
152
|
+
successful_jobs += 1 if job.exit_code == 0
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
:jobs => jobs,
|
|
157
|
+
#To consider: is this field even useful? It's really in job-seconds, not just seconds.
|
|
158
|
+
#What about one in just seconds (that considers gaps in activity)?
|
|
159
|
+
:wall_time => wall_time,
|
|
160
|
+
:cpu_time => cpu_time,
|
|
161
|
+
:memory_time => memory_time,
|
|
162
|
+
:successful => if jobs.length == 0 then 0.0 else Float(successful_jobs) / jobs.length end,
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
#Returns an array of all jobs, pre-loading relations to reduce the need for extra queries
|
|
168
|
+
#
|
|
169
|
+
#Relations are not cached between calls.
|
|
170
|
+
def self.all_with_relations
|
|
171
|
+
jobs = all
|
|
172
|
+
transaction do
|
|
173
|
+
users = {}
|
|
174
|
+
groups = {}
|
|
175
|
+
systems = {}
|
|
176
|
+
system_types = {}
|
|
177
|
+
jobs.each do |job|
|
|
178
|
+
system = systems[job.system_id]
|
|
179
|
+
if system
|
|
180
|
+
job.system = system
|
|
181
|
+
else
|
|
182
|
+
system = job.system
|
|
183
|
+
systems[system.id] = system
|
|
184
|
+
end
|
|
185
|
+
system_type = system_types[system.system_type_id]
|
|
186
|
+
if system_type
|
|
187
|
+
system.system_type = system_type
|
|
188
|
+
else
|
|
189
|
+
system_type = system.system_type
|
|
190
|
+
system_types[system_type.id] = system_type
|
|
191
|
+
end
|
|
192
|
+
user = users[job.user_id]
|
|
193
|
+
if user
|
|
194
|
+
job.user = user
|
|
195
|
+
else
|
|
196
|
+
user = job.user
|
|
197
|
+
users[user.id] = user
|
|
198
|
+
end
|
|
199
|
+
group = groups[user.group_id]
|
|
200
|
+
if group
|
|
201
|
+
user.group = group
|
|
202
|
+
else
|
|
203
|
+
group = user.group
|
|
204
|
+
groups[group.id] = group
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
jobs
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
before_save do
|
|
213
|
+
write_attribute(:end_time, end_time)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
before_update do
|
|
217
|
+
write_attribute(:end_time, end_time)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
validates_presence_of :user, :system, :cpu_time,
|
|
221
|
+
:start_time, :wall_time, :memory, :exit_code
|
|
222
|
+
|
|
223
|
+
validates_each :cpu_time, :wall_time, :memory do |record, attr, value|
|
|
224
|
+
record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
validates_each :start_time do |record, attr, value|
|
|
228
|
+
value = value.to_time if value.respond_to?(:to_time)
|
|
229
|
+
record.errors.add(attr, 'must be a time object') unless value.is_a?(Time)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
##
|
|
235
|
+
#A group
|
|
236
|
+
class Group < ActiveRecord::Base
|
|
237
|
+
has_many :users
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
#Finds a group by name, creating it if it doesn't exist
|
|
241
|
+
#
|
|
242
|
+
#If <tt>known_groups</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
|
|
243
|
+
#
|
|
244
|
+
#This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
|
|
245
|
+
def self.find_or_create!(name, known_groups = nil)
|
|
246
|
+
group = known_groups[name] if known_groups
|
|
247
|
+
unless group
|
|
248
|
+
Lock[:groups].synchronize do
|
|
249
|
+
group = find_by_name(name)
|
|
250
|
+
group ||= create!(:name => name)
|
|
251
|
+
end
|
|
252
|
+
known_groups[name] = group if known_groups
|
|
253
|
+
end
|
|
254
|
+
group
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
validates_presence_of :name
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#ActiveRecord structure for a user
|
|
261
|
+
class User < ActiveRecord::Base
|
|
262
|
+
belongs_to :group
|
|
263
|
+
|
|
264
|
+
##
|
|
265
|
+
#Finds a user by name and group, creating it if it doesn't exist
|
|
266
|
+
#
|
|
267
|
+
#If <tt>known_users</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
|
|
268
|
+
#
|
|
269
|
+
#This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
|
|
270
|
+
def self.find_or_create!(name, group, known_users = nil)
|
|
271
|
+
#Determine if the user/group pair must be added to/retrieved from the database.
|
|
272
|
+
user = known_users[[name, group]] if known_users
|
|
273
|
+
unless user
|
|
274
|
+
Lock[:users].synchronize do
|
|
275
|
+
#Does the user already exist?
|
|
276
|
+
user = Bookie::Database::User.find_by_name_and_group_id(name, group.id)
|
|
277
|
+
user ||= Bookie::Database::User.create!(
|
|
278
|
+
:name => name,
|
|
279
|
+
:group => group)
|
|
280
|
+
end
|
|
281
|
+
known_users[[name, group]] = user if known_users
|
|
282
|
+
end
|
|
283
|
+
user
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
validates_presence_of :group, :name
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
#A system on the network
|
|
291
|
+
class System < ActiveRecord::Base
|
|
292
|
+
##
|
|
293
|
+
#Raised when a system's specifications are different from those of the active system in the database
|
|
294
|
+
SystemConflictError = Class.new(RuntimeError)
|
|
295
|
+
|
|
296
|
+
has_many :jobs
|
|
297
|
+
belongs_to :system_type
|
|
298
|
+
|
|
299
|
+
##
|
|
300
|
+
#Finds all systems that are active (i.e. all systems with NULL values for end_time)
|
|
301
|
+
def self.active_systems
|
|
302
|
+
where('end_time IS NULL')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
##
|
|
306
|
+
#Filters by name
|
|
307
|
+
def self.by_name(name)
|
|
308
|
+
where('name = ?', name)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
##
|
|
312
|
+
#Filters by system type
|
|
313
|
+
def self.by_system_type(sys_type)
|
|
314
|
+
where('system_type_id = ?', sys_type.id)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
##
|
|
318
|
+
#Finds the active system for a given hostname
|
|
319
|
+
#
|
|
320
|
+
#<tt>values</tt> should contain a list of fields, including the name, in the format that would normally be passed to System.create!.
|
|
321
|
+
#
|
|
322
|
+
#This method also checks that this system's specifications are the same as those in the database and raises an error if they are different.
|
|
323
|
+
#
|
|
324
|
+
#This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
|
|
325
|
+
def self.find_active(values)
|
|
326
|
+
system = nil
|
|
327
|
+
name = values[:name]
|
|
328
|
+
Lock[:systems].synchronize do
|
|
329
|
+
system = active_systems.find_by_name(name)
|
|
330
|
+
if system
|
|
331
|
+
[:cores, :memory, :system_type].each do |key|
|
|
332
|
+
#To consider: this also compares the names, which is unnecessary.
|
|
333
|
+
unless system.send(key) == values[key]
|
|
334
|
+
raise SystemConflictError.new("The specifications on record for '#{name}' do not match this system's specifications.
|
|
335
|
+
Please make sure that all previous systems with this hostname have been marked as decommissioned.")
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
else
|
|
339
|
+
raise "There is no active system with hostname '#{values[:name]}' in the database."
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
system
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
##
|
|
346
|
+
#Produces a summary of all the systems for the given time interval
|
|
347
|
+
#
|
|
348
|
+
#Returns a hash with the following fields:
|
|
349
|
+
#- <tt>:avail_cpu_time</tt>: the total CPU time available for the interval
|
|
350
|
+
#- <tt>:avail_memory_time</tt>: the total amount of memory-time available (in kilobyte-seconds)
|
|
351
|
+
#- <tt>:avail_memory_avg</tt>: the average amount of memory available (in kilobytes)
|
|
352
|
+
def self.summary(min_time = nil, max_time = nil)
|
|
353
|
+
current_time = Time.now
|
|
354
|
+
#Sums that are actually returned
|
|
355
|
+
avail_cpu_time = 0
|
|
356
|
+
avail_memory_time = 0
|
|
357
|
+
#Find all the systems within the time range.
|
|
358
|
+
systems = System
|
|
359
|
+
if min_time
|
|
360
|
+
raise ArgumentError.new('Max time must be specified with min time') unless max_time
|
|
361
|
+
raise ArgumentError.new('Max time must be greater than or equal to min time') if max_time < min_time
|
|
362
|
+
#To consider: optimize as union of queries?
|
|
363
|
+
systems = systems.where(
|
|
364
|
+
'start_time < ? AND (end_time IS NULL OR end_time > ?)',
|
|
365
|
+
max_time,
|
|
366
|
+
min_time)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
systems.all.each do |system|
|
|
370
|
+
system_start_time = system.start_time
|
|
371
|
+
system_end_time = system.end_time
|
|
372
|
+
#Is there a time range constraint?
|
|
373
|
+
if min_time
|
|
374
|
+
system_start_time = [system_start_time, min_time].max
|
|
375
|
+
system_end_time = [system_end_time, max_time].min if system.end_time
|
|
376
|
+
system_end_time ||= max_time
|
|
377
|
+
else
|
|
378
|
+
system_end_time ||= current_time
|
|
379
|
+
end
|
|
380
|
+
system_wall_time = system_end_time.to_i - system_start_time.to_i
|
|
381
|
+
avail_cpu_time += system.cores * system_wall_time
|
|
382
|
+
avail_memory_time += system.memory * system_wall_time
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
wall_time_range = 0
|
|
386
|
+
if min_time
|
|
387
|
+
wall_time_range = max_time - min_time
|
|
388
|
+
else
|
|
389
|
+
first_started_system = systems.order(:start_time).first
|
|
390
|
+
if first_started_system
|
|
391
|
+
#Is there a system still active?
|
|
392
|
+
last_ended_system = systems.where('end_time IS NULL').first
|
|
393
|
+
if last_ended_system
|
|
394
|
+
wall_time_range = current_time.to_i - first_started_system.start_time.to_i
|
|
395
|
+
else
|
|
396
|
+
#No; find the system that was brought down last.
|
|
397
|
+
last_ended_system = systems.order('end_time DESC').first
|
|
398
|
+
wall_time_range = last_ended_system.end_time.to_i - first_started_system.start_time.to_i
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
{
|
|
404
|
+
:avail_cpu_time => avail_cpu_time,
|
|
405
|
+
:avail_memory_time => avail_memory_time,
|
|
406
|
+
:avail_memory_avg => if wall_time_range == 0 then 0.0 else Float(avail_memory_time) / wall_time_range end,
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
##
|
|
411
|
+
#Decommissions the given system, setting its end time to <tt>end_time</tt>
|
|
412
|
+
#
|
|
413
|
+
#This should be called any time a system is brought down or its specifications are changed.
|
|
414
|
+
def decommission(end_time)
|
|
415
|
+
self.end_time = end_time
|
|
416
|
+
self.save!
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
validates_presence_of :name, :cores, :memory, :system_type, :start_time
|
|
420
|
+
|
|
421
|
+
validates_each :cores, :memory do |record, attr, value|
|
|
422
|
+
record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
validates_each :start_time do |record, attr, value|
|
|
426
|
+
value = value.to_time if value.respond_to?(:to_time)
|
|
427
|
+
record.errors.add(attr, 'must be a time object') unless value.is_a?(Time)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
validates_each :end_time do |record, attr, value|
|
|
431
|
+
record.errors.add(attr, 'must be at or after start time') if value && value < record.start_time
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
##
|
|
436
|
+
#A hash mapping memory stat type names to their database codes
|
|
437
|
+
#
|
|
438
|
+
#- <tt>:unknown => 0</tt>
|
|
439
|
+
#- <tt>:avg => 1</tt>
|
|
440
|
+
#- <tt>:max => 2</tt>
|
|
441
|
+
#
|
|
442
|
+
MEMORY_STAT_TYPE = {:unknown => 0, :avg => 1, :max => 2}
|
|
443
|
+
|
|
444
|
+
##
|
|
445
|
+
#The inverse of MEMORY_STAT_TYPE
|
|
446
|
+
MEMORY_STAT_TYPE_INVERSE = MEMORY_STAT_TYPE.invert
|
|
447
|
+
|
|
448
|
+
#A system type
|
|
449
|
+
class SystemType < ActiveRecord::Base
|
|
450
|
+
has_many :systems
|
|
451
|
+
|
|
452
|
+
validates_presence_of :name, :memory_stat_type
|
|
453
|
+
|
|
454
|
+
##
|
|
455
|
+
#Finds a system type by name and memory stat type, creating it if it doesn't exist
|
|
456
|
+
#
|
|
457
|
+
#It is an error to attempt to create two types with the same name but different memory stat types.
|
|
458
|
+
#
|
|
459
|
+
#This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
|
|
460
|
+
def self.find_or_create!(name, memory_stat_type)
|
|
461
|
+
sys_type = nil
|
|
462
|
+
Lock[:system_types].synchronize do
|
|
463
|
+
sys_type = SystemType.find_by_name(name)
|
|
464
|
+
if sys_type
|
|
465
|
+
unless sys_type.memory_stat_type == memory_stat_type
|
|
466
|
+
type_code = MEMORY_STAT_TYPE[memory_stat_type]
|
|
467
|
+
if type_code == nil
|
|
468
|
+
raise "Unrecognized memory stat type '#{memory_stat_type}'"
|
|
469
|
+
else
|
|
470
|
+
raise "The recorded memory stat type for system type '#{name}' does not match the required type of #{type_code}"
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
else
|
|
474
|
+
sys_type = create!(
|
|
475
|
+
:name => name,
|
|
476
|
+
:memory_stat_type => memory_stat_type
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
sys_type
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
##
|
|
484
|
+
#Returns the memory stat type as a symbol
|
|
485
|
+
#
|
|
486
|
+
#See Bookie::Database::MEMORY_STAT_TYPE for possible values.
|
|
487
|
+
#
|
|
488
|
+
#Based on http://www.kensodev.com/2012/05/08/the-simplest-enum-you-will-ever-find-for-your-activerecord-models/
|
|
489
|
+
def memory_stat_type
|
|
490
|
+
type_code = read_attribute(:memory_stat_type)
|
|
491
|
+
raise 'Memory stat type must not be nil' if type_code == nil
|
|
492
|
+
type = MEMORY_STAT_TYPE_INVERSE[type_code]
|
|
493
|
+
raise "Unrecognized memory stat type code #{type_code}" unless type
|
|
494
|
+
type
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
##
|
|
498
|
+
#Sets the memory stat type
|
|
499
|
+
#
|
|
500
|
+
#<tt>type</tt> should be a symbol.
|
|
501
|
+
def memory_stat_type=(type)
|
|
502
|
+
raise 'Memory stat type must not be nil' if type == nil
|
|
503
|
+
type_code = MEMORY_STAT_TYPE[type]
|
|
504
|
+
raise "Unrecognized memory stat type '#{type}'" unless type_code
|
|
505
|
+
write_attribute(:memory_stat_type, type_code)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
##
|
|
510
|
+
#Database migrations
|
|
511
|
+
module Migration
|
|
512
|
+
class CreateUsers < ActiveRecord::Migration
|
|
513
|
+
def up
|
|
514
|
+
create_table :users do |t|
|
|
515
|
+
t.string :name, :null => false
|
|
516
|
+
t.references :group, :null => false
|
|
517
|
+
end
|
|
518
|
+
change_table :users do |t|
|
|
519
|
+
t.index [:name, :group_id], :unique => true
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def down
|
|
524
|
+
drop_table :users
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
class CreateGroups < ActiveRecord::Migration
|
|
529
|
+
def up
|
|
530
|
+
create_table :groups do |t|
|
|
531
|
+
t.string :name, :null => false
|
|
532
|
+
end
|
|
533
|
+
change_table :groups do |t|
|
|
534
|
+
t.index :name, :unique => true
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def down
|
|
539
|
+
drop_table :groups
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
class CreateSystems < ActiveRecord::Migration
|
|
544
|
+
def up
|
|
545
|
+
create_table :systems do |t|
|
|
546
|
+
t.string :name, :null => false
|
|
547
|
+
t.references :system_type, :null => false
|
|
548
|
+
t.datetime :start_time, :null => false
|
|
549
|
+
t.datetime :end_time
|
|
550
|
+
t.integer :cores, :null => false
|
|
551
|
+
#To consider: replace with a float? (more compact)
|
|
552
|
+
t.integer :memory, :null => false, :limit => 8
|
|
553
|
+
end
|
|
554
|
+
change_table :systems do |t|
|
|
555
|
+
t.index [:name, :end_time], :unique => true
|
|
556
|
+
t.index :start_time
|
|
557
|
+
t.index :end_time
|
|
558
|
+
t.index :system_type_id
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def down
|
|
563
|
+
drop_table :systems
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
class CreateSystemTypes < ActiveRecord::Migration
|
|
568
|
+
def up
|
|
569
|
+
create_table :system_types do |t|
|
|
570
|
+
t.string :name, :null => false
|
|
571
|
+
t.integer :memory_stat_type, :limit => 1, :null => false
|
|
572
|
+
end
|
|
573
|
+
change_table :system_types do |t|
|
|
574
|
+
t.index :name, :unique => true
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def down
|
|
579
|
+
drop_table :system_types
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
class CreateJobs < ActiveRecord::Migration
|
|
584
|
+
def up
|
|
585
|
+
create_table :jobs do |t|
|
|
586
|
+
t.references :user, :null => false
|
|
587
|
+
t.references :system, :null => false
|
|
588
|
+
t.string :command_name, :limit => 24
|
|
589
|
+
t.datetime :start_time, :null => false
|
|
590
|
+
t.datetime :end_time, :null => false
|
|
591
|
+
t.integer :wall_time, :null => false
|
|
592
|
+
t.integer :cpu_time, :null => false
|
|
593
|
+
t.integer :memory, :null => false
|
|
594
|
+
t.integer :exit_code, :null => false
|
|
595
|
+
end
|
|
596
|
+
change_table :jobs do |t|
|
|
597
|
+
t.index :user_id
|
|
598
|
+
t.index :system_id
|
|
599
|
+
t.index :command_name
|
|
600
|
+
t.index :start_time
|
|
601
|
+
t.index :end_time
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def down
|
|
606
|
+
drop_table :jobs
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
class CreateLocks < ActiveRecord::Migration
|
|
611
|
+
def up
|
|
612
|
+
create_table :locks do |t|
|
|
613
|
+
t.string :name
|
|
614
|
+
end
|
|
615
|
+
change_table :locks do |t|
|
|
616
|
+
t.index :name, :unique => true
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
['users', 'groups', 'systems', 'system_types'].each do |name|
|
|
620
|
+
Lock.create!(:name => name)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def down
|
|
625
|
+
drop_table :locks
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
class << self;
|
|
630
|
+
##
|
|
631
|
+
#Brings up all migrations
|
|
632
|
+
def up
|
|
633
|
+
CreateUsers.new.up
|
|
634
|
+
CreateGroups.new.up
|
|
635
|
+
CreateSystems.new.up
|
|
636
|
+
CreateSystemTypes.new.up
|
|
637
|
+
CreateJobs.new.up
|
|
638
|
+
CreateLocks.new.up
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
##
|
|
642
|
+
#Brings down all migrations
|
|
643
|
+
#
|
|
644
|
+
#Warning: this will destroy all data!
|
|
645
|
+
def down
|
|
646
|
+
CreateUsers.new.down
|
|
647
|
+
CreateGroups.new.down
|
|
648
|
+
CreateSystems.new.down
|
|
649
|
+
CreateSystemTypes.new.down
|
|
650
|
+
CreateJobs.new.down
|
|
651
|
+
CreateLocks.new.down
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|