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 =
4096
DEFAULT_SHM_RESERVE_MIB =
4096
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.



302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/test-runner/resource_pool.rb', line 302

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)


77
78
79
# File 'lib/test-runner/resource_pool.rb', line 77

def cpus
  @cpus
end

#memory_mibInteger? (readonly)

Returns:

  • (Integer, nil)


71
72
73
# File 'lib/test-runner/resource_pool.rb', line 71

def memory_mib
  @memory_mib
end

#runningInteger (readonly)

Returns:

  • (Integer)


83
84
85
# File 'lib/test-runner/resource_pool.rb', line 83

def running
  @running
end

#shm_mibInteger? (readonly)

Returns:

  • (Integer, nil)


74
75
76
# File 'lib/test-runner/resource_pool.rb', line 74

def shm_mib
  @shm_mib
end

#usedTestResources (readonly)

Returns:



80
81
82
# File 'lib/test-runner/resource_pool.rb', line 80

def used
  @used
end

Class Method Details

.capacity(value, reserve) ⇒ Object



160
161
162
163
164
# File 'lib/test-runner/resource_pool.rb', line 160

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

  [value - reserve, 0].max
end

.cgroup_path_ancestors(path) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/test-runner/resource_pool.rb', line 244

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



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/test-runner/resource_pool.rb', line 225

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_limit_file(path) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/test-runner/resource_pool.rb', line 259

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



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

def self.detect_cgroup_memory_limit_mib
  detect_cgroup_v2_memory_limit_mib || detect_cgroup_v1_memory_limit_mib
end

.detect_cgroup_v1_memory_limit_mibObject



216
217
218
219
220
221
222
223
# File 'lib/test-runner/resource_pool.rb', line 216

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_limit_mibObject



203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/test-runner/resource_pool.rb', line 203

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



293
294
295
296
297
298
299
300
# File 'lib/test-runner/resource_pool.rb', line 293

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



281
282
283
284
285
286
287
288
289
290
291
# File 'lib/test-runner/resource_pool.rb', line 281

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



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

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



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

def self.detect_memory_available_mib
  detect_meminfo_mib('MemAvailable:')
end

.detect_memory_mibObject



173
174
175
176
177
178
179
180
181
182
# File 'lib/test-runner/resource_pool.rb', line 173

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



277
278
279
# File 'lib/test-runner/resource_pool.rb', line 277

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

.detect_shm_mibObject



273
274
275
# File 'lib/test-runner/resource_pool.rb', line 273

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

.detector_value(detector, *methods) ⇒ Object



166
167
168
169
170
171
# File 'lib/test-runner/resource_pool.rb', line 166

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)


150
151
152
153
154
155
156
157
158
# File 'lib/test-runner/resource_pool.rb', line 150

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



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/test-runner/resource_pool.rb', line 85

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, :memory_available_mib) },
      overcommit: memory_overcommit
    ),
    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, :shm_available_mib) },
      overcommit: shm_overcommit
    ),
    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



143
144
145
146
147
148
# File 'lib/test-runner/resource_pool.rb', line 143

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)


325
326
327
328
329
# File 'lib/test-runner/resource_pool.rb', line 325

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)



349
350
351
# File 'lib/test-runner/resource_pool.rb', line 349

def capacities
  [memory_mib, shm_mib, cpus]
end

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

Returns:

  • (Boolean)


353
354
355
# File 'lib/test-runner/resource_pool.rb', line 353

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

#format_mib(value) ⇒ Object (protected)



370
371
372
373
374
# File 'lib/test-runner/resource_pool.rb', line 370

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

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

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



357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/test-runner/resource_pool.rb', line 357

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



315
316
317
318
319
320
321
322
323
# File 'lib/test-runner/resource_pool.rb', line 315

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



336
337
338
339
# File 'lib/test-runner/resource_pool.rb', line 336

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

#reserve(resources) ⇒ Object



331
332
333
334
# File 'lib/test-runner/resource_pool.rb', line 331

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

#statusObject



341
342
343
344
345
# File 'lib/test-runner/resource_pool.rb', line 341

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