Class: TestRunner::ResourcePool

Inherits:
Object
  • Object
show all
Defined in:
lib/test-runner/resource_pool.rb

Defined Under Namespace

Classes: CapacitySource, HostResourceDetector

Constant Summary collapse

DEFAULT_MEMORY_RESERVE_MIB =
8192
DEFAULT_SHM_RESERVE_MIB =
8192
DEFAULT_CPU_RESERVE =
0
DEFAULT_MEMORY_OVERCOMMIT =
1.0
DEFAULT_SHM_OVERCOMMIT =
1.0
DEFAULT_CPU_OVERCOMMIT =
1.5

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(memory_mib:, shm_mib:, cpus:, memory_capacity: nil, shm_capacity: nil, cpu_capacity: nil) ⇒ ResourcePool

Returns a new instance of ResourcePool.



394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/test-runner/resource_pool.rb', line 394

def initialize(memory_mib:, shm_mib:, cpus:, memory_capacity: nil, shm_capacity: nil, cpu_capacity: nil)
  @memory_capacity = memory_capacity
  @shm_capacity = shm_capacity
  @cpu_capacity = cpu_capacity
  @memory_mib = memory_mib
  @shm_mib = shm_mib
  @cpus = cpus
  @used = TestResources.new
  @running = 0

  refresh_capacity
end

Instance Attribute Details

#cpusInteger? (readonly)

Returns:

  • (Integer, nil)


111
112
113
# File 'lib/test-runner/resource_pool.rb', line 111

def cpus
  @cpus
end

#memory_mibInteger? (readonly)

Returns:

  • (Integer, nil)


105
106
107
# File 'lib/test-runner/resource_pool.rb', line 105

def memory_mib
  @memory_mib
end

#runningInteger (readonly)

Returns:

  • (Integer)


117
118
119
# File 'lib/test-runner/resource_pool.rb', line 117

def running
  @running
end

#shm_mibInteger? (readonly)

Returns:

  • (Integer, nil)


108
109
110
# File 'lib/test-runner/resource_pool.rb', line 108

def shm_mib
  @shm_mib
end

#usedTestResources (readonly)

Returns:



114
115
116
# File 'lib/test-runner/resource_pool.rb', line 114

def used
  @used
end

Class Method Details

.capacity(value, reserve) ⇒ Object



198
199
200
201
202
# File 'lib/test-runner/resource_pool.rb', line 198

def self.capacity(value, reserve)
  return nil if value.nil?

  [value - reserve, 0].max
end

.cgroup_path_ancestors(path) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/test-runner/resource_pool.rb', line 325

def self.cgroup_path_ancestors(path)
  ret = []
  relative_path = path.sub(%r{\A/}, '')

  loop do
    ret << relative_path
    break if relative_path == ''

    relative_path = File.dirname(relative_path)
    relative_path = '' if relative_path == '.'
  end

  ret
end

.current_cgroup_path(controller) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/test-runner/resource_pool.rb', line 306

def self.current_cgroup_path(controller)
  return nil unless File.file?('/proc/self/cgroup')

  File.readlines('/proc/self/cgroup').each do |line|
    _id, controllers, path = line.strip.split(':', 3)
    next if path.nil?

    if controller.nil?
      return path if controllers == ''
    elsif controllers.split(',').include?(controller)
      return path
    end
  end

  nil
rescue Errno::ENOENT, Errno::EACCES
  nil
end

.detect_cgroup_memory_available_mibObject



244
245
246
# File 'lib/test-runner/resource_pool.rb', line 244

def self.detect_cgroup_memory_available_mib
  detect_cgroup_v2_memory_available_mib || detect_cgroup_v1_memory_available_mib
end

.detect_cgroup_memory_limit_file(path) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/test-runner/resource_pool.rb', line 340

def self.detect_cgroup_memory_limit_file(path)
  return nil unless File.file?(path)

  value = File.read(path).strip
  return nil if ['', 'max'].include?(value)

  bytes = Integer(value)
  return nil if bytes <= 0

  bytes / 1024 / 1024
rescue Errno::ENOENT, Errno::EACCES, ArgumentError
  nil
end

.detect_cgroup_memory_limit_mibObject



240
241
242
# File 'lib/test-runner/resource_pool.rb', line 240

def self.detect_cgroup_memory_limit_mib
  detect_cgroup_v2_memory_limit_mib || detect_cgroup_v1_memory_limit_mib
end

.detect_cgroup_memory_usage_file(path) ⇒ Object



354
355
356
357
358
359
360
361
362
363
# File 'lib/test-runner/resource_pool.rb', line 354

def self.detect_cgroup_memory_usage_file(path)
  return nil unless File.file?(path)

  bytes = Integer(File.read(path).strip)
  return nil if bytes < 0

  bytes / 1024 / 1024
rescue Errno::ENOENT, Errno::EACCES, ArgumentError
  nil
end

.detect_cgroup_v1_memory_available_mibObject



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/test-runner/resource_pool.rb', line 288

def self.detect_cgroup_v1_memory_available_mib
  path = current_cgroup_path('memory')
  return nil if path.nil?

  headrooms = cgroup_path_ancestors(path).filter_map do |ancestor|
    base = File.join('/sys/fs/cgroup/memory', ancestor)
    limit = detect_cgroup_memory_limit_file(File.join(base, 'memory.limit_in_bytes'))
    next if limit.nil?

    usage = detect_cgroup_memory_usage_file(File.join(base, 'memory.usage_in_bytes'))
    next if usage.nil?

    [limit - usage, 0].max
  end

  headrooms.min
end

.detect_cgroup_v1_memory_limit_mibObject



279
280
281
282
283
284
285
286
# File 'lib/test-runner/resource_pool.rb', line 279

def self.detect_cgroup_v1_memory_limit_mib
  path = current_cgroup_path('memory')
  return nil if path.nil?

  detect_cgroup_memory_limit_file(
    File.join('/sys/fs/cgroup/memory', path.sub(%r{\A/}, ''), 'memory.limit_in_bytes')
  )
end

.detect_cgroup_v2_memory_available_mibObject



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/test-runner/resource_pool.rb', line 261

def self.detect_cgroup_v2_memory_available_mib
  path = current_cgroup_path(nil)
  return nil if path.nil?

  headrooms = cgroup_path_ancestors(path).filter_map do |ancestor|
    base = File.join('/sys/fs/cgroup', ancestor)
    limit = detect_cgroup_memory_limit_file(File.join(base, 'memory.max'))
    next if limit.nil?

    usage = detect_cgroup_memory_usage_file(File.join(base, 'memory.current'))
    next if usage.nil?

    [limit - usage, 0].max
  end

  headrooms.min
end

.detect_cgroup_v2_memory_limit_mibObject



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/test-runner/resource_pool.rb', line 248

def self.detect_cgroup_v2_memory_limit_mib
  path = current_cgroup_path(nil)
  return nil if path.nil?

  limits = cgroup_path_ancestors(path).filter_map do |ancestor|
    detect_cgroup_memory_limit_file(
      File.join('/sys/fs/cgroup', ancestor, 'memory.max')
    )
  end

  limits.min
end

.detect_cpusObject



385
386
387
388
389
390
391
392
# File 'lib/test-runner/resource_pool.rb', line 385

def self.detect_cpus
  out, status = Open3.capture2('nproc')
  return nil unless status.success?

  Integer(out.strip)
rescue Errno::ENOENT, ArgumentError
  nil
end

.detect_df_mib(path, column) ⇒ Object



373
374
375
376
377
378
379
380
381
382
383
# File 'lib/test-runner/resource_pool.rb', line 373

def self.detect_df_mib(path, column)
  out, status = Open3.capture2('df', '-Pk', path)
  return nil unless status.success?

  line = out.lines[1]
  return nil if line.nil?

  line.split[column].to_i / 1024
rescue Errno::ENOENT
  nil
end

.detect_meminfo_mib(key) ⇒ Object



229
230
231
232
233
234
235
236
237
238
# File 'lib/test-runner/resource_pool.rb', line 229

def self.detect_meminfo_mib(key)
  return nil unless File.file?('/proc/meminfo')

  line = File.readlines('/proc/meminfo').detect { |v| v.start_with?(key) }
  return nil if line.nil?

  line.split[1].to_i / 1024
rescue Errno::ENOENT, Errno::EACCES
  nil
end

.detect_memory_available_mibObject



222
223
224
225
226
227
# File 'lib/test-runner/resource_pool.rb', line 222

def self.detect_memory_available_mib
  host_available = detect_meminfo_mib('MemAvailable:')
  cgroup_available = detect_cgroup_memory_available_mib

  [host_available, cgroup_available].compact.min
end

.detect_memory_mibObject



211
212
213
214
215
216
217
218
219
220
# File 'lib/test-runner/resource_pool.rb', line 211

def self.detect_memory_mib
  mem_total = detect_meminfo_mib('MemTotal:')
  cgroup_limit = detect_cgroup_memory_limit_mib

  if mem_total && cgroup_limit
    [mem_total, cgroup_limit].min
  else
    cgroup_limit || mem_total
  end
end

.detect_shm_available_mibObject



369
370
371
# File 'lib/test-runner/resource_pool.rb', line 369

def self.detect_shm_available_mib
  detect_df_mib('/dev/shm', 3)
end

.detect_shm_mibObject



365
366
367
# File 'lib/test-runner/resource_pool.rb', line 365

def self.detect_shm_mib
  detect_df_mib('/dev/shm', 1)
end

.detector_value(detector, *methods) ⇒ Object



204
205
206
207
208
209
# File 'lib/test-runner/resource_pool.rb', line 204

def self.detector_value(detector, *methods)
  method = methods.detect { |m| detector.respond_to?(m) }
  return nil if method.nil?

  detector.public_send(method)
end

.factor_option(*values) ⇒ Object

Raises:

  • (ArgumentError)


188
189
190
191
192
193
194
195
196
# File 'lib/test-runner/resource_pool.rb', line 188

def self.factor_option(*values)
  value = values.detect { |v| !v.nil? && v.to_s != '' }
  return nil if value.nil?

  ret = Float(value)
  raise ArgumentError, 'overcommit factors must be positive' if ret <= 0

  ret
end

.from_options(opts, env: ENV) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/test-runner/resource_pool.rb', line 119

def self.from_options(opts, env: ENV)
  memory_reserve_mib = integer_option(
    opts[:memory_reserve_mib],
    env['TEST_RUNNER_MEMORY_RESERVE_MIB'],
    DEFAULT_MEMORY_RESERVE_MIB
  )
  shm_reserve_mib = integer_option(
    opts[:shm_reserve_mib],
    env['TEST_RUNNER_SHM_RESERVE_MIB'],
    DEFAULT_SHM_RESERVE_MIB
  )
  cpu_reserve = integer_option(
    opts[:cpu_reserve],
    env['TEST_RUNNER_CPU_RESERVE'],
    DEFAULT_CPU_RESERVE
  )
  memory_overcommit = factor_option(
    opts[:memory_overcommit],
    env['TEST_RUNNER_MEMORY_OVERCOMMIT'],
    DEFAULT_MEMORY_OVERCOMMIT
  )
  shm_overcommit = factor_option(
    opts[:shm_overcommit],
    env['TEST_RUNNER_SHM_OVERCOMMIT'],
    DEFAULT_SHM_OVERCOMMIT
  )
  cpu_overcommit = factor_option(
    opts[:cpu_overcommit],
    env['TEST_RUNNER_CPU_OVERCOMMIT'],
    DEFAULT_CPU_OVERCOMMIT
  )
  detector = opts[:resource_detector] || HostResourceDetector.new

  new(
    memory_mib: nil,
    shm_mib: nil,
    cpus: nil,
    memory_capacity: CapacitySource.new(
      max_value: integer_option(opts[:max_memory_mib], env['TEST_RUNNER_MAX_MEMORY_MIB'], nil),
      reserve: memory_reserve_mib,
      detector: -> { detector_value(detector, :memory_mib) },
      available_detector: -> { detector_value(detector, :memory_available_mib) },
      overcommit: memory_overcommit,
      monotonic: true
    ),
    shm_capacity: CapacitySource.new(
      max_value: integer_option(opts[:max_shm_mib], env['TEST_RUNNER_MAX_SHM_MIB'], nil),
      reserve: shm_reserve_mib,
      detector: -> { detector_value(detector, :shm_mib) },
      available_detector: -> { detector_value(detector, :shm_available_mib) },
      overcommit: shm_overcommit,
      monotonic: true
    ),
    cpu_capacity: CapacitySource.new(
      max_value: integer_option(opts[:max_cpus], env['TEST_RUNNER_MAX_CPUS'], nil),
      reserve: cpu_reserve,
      detector: -> { detector.cpus },
      overcommit: cpu_overcommit
    )
  )
end

.integer_option(*values) ⇒ Object



181
182
183
184
185
186
# File 'lib/test-runner/resource_pool.rb', line 181

def self.integer_option(*values)
  value = values.detect { |v| !v.nil? && v.to_s != '' }
  return nil if value.nil?

  Integer(value)
end

Instance Method Details

#can_reserve?(resources) ⇒ Boolean

Returns:

  • (Boolean)


417
418
419
420
421
# File 'lib/test-runner/resource_pool.rb', line 417

def can_reserve?(resources)
  fits?(memory_mib, used.memory_mib + resources.memory_mib) &&
    fits?(shm_mib, used.shm_mib + resources.shm_mib) &&
    fits?(cpus, used.cpus + resources.cpus)
end

#capacitiesObject (protected)



448
449
450
# File 'lib/test-runner/resource_pool.rb', line 448

def capacities
  [memory_mib, shm_mib, cpus]
end

#capacity_statusObject



439
440
441
442
443
444
# File 'lib/test-runner/resource_pool.rb', line 439

def capacity_status
  [
    format_capacity_source('memory', @memory_capacity),
    format_capacity_source('shm', @shm_capacity)
  ].join('; ')
end

#fits?(capacity, value) ⇒ Boolean (protected)

Returns:

  • (Boolean)


452
453
454
# File 'lib/test-runner/resource_pool.rb', line 452

def fits?(capacity, value)
  capacity.nil? || value <= capacity
end

#format_capacity_source(name, source) ⇒ Object (protected)



475
476
477
478
479
480
# File 'lib/test-runner/resource_pool.rb', line 475

def format_capacity_source(name, source)
  "#{name} assigned=#{format_optional_mib(source.detected)}, " \
    "available-at-start=#{format_optional_mib(source.initial_available)}, " \
    "reserve=#{format_mib(source.reserve)}, " \
    "limit=#{format_optional_mib(source.current_value)}"
end

#format_mib(value) ⇒ Object (protected)



469
470
471
472
473
# File 'lib/test-runner/resource_pool.rb', line 469

def format_mib(value)
  return "#{value} MiB" if value < 1024

  format('%.1f GiB', value / 1024.0)
end

#format_optional_mib(value) ⇒ Object (protected)



482
483
484
# File 'lib/test-runner/resource_pool.rb', line 482

def format_optional_mib(value)
  value.nil? ? 'unknown' : format_mib(value)
end

#format_used(capacity, value, unit: true) ⇒ Object (protected)



456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/test-runner/resource_pool.rb', line 456

def format_used(capacity, value, unit: true)
  if capacity.nil?
    formatted = unit ? format_mib(value) : value
    return "#{formatted}/unlimited"
  end

  if unit
    "#{format_mib(value)}/#{format_mib(capacity)}"
  else
    "#{value}/#{capacity}"
  end
end

#refresh_capacityObject



407
408
409
410
411
412
413
414
415
# File 'lib/test-runner/resource_pool.rb', line 407

def refresh_capacity
  previous = capacities

  @memory_mib = @memory_capacity.current if @memory_capacity
  @shm_mib = @shm_capacity.current if @shm_capacity
  @cpus = @cpu_capacity.current if @cpu_capacity

  previous != capacities
end

#release(resources) ⇒ Object



428
429
430
431
# File 'lib/test-runner/resource_pool.rb', line 428

def release(resources)
  @used -= resources
  @running -= 1
end

#reserve(resources) ⇒ Object



423
424
425
426
# File 'lib/test-runner/resource_pool.rb', line 423

def reserve(resources)
  @used += resources
  @running += 1
end

#statusObject



433
434
435
436
437
# File 'lib/test-runner/resource_pool.rb', line 433

def status
  "memory=#{format_used(memory_mib, used.memory_mib)}, " \
    "shm=#{format_used(shm_mib, used.shm_mib)}, " \
    "cpus=#{format_used(cpus, used.cpus, unit: false)}, running=#{running}"
end