maxprocs 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/README.md +103 -0
- data/lib/maxprocs/version.rb +5 -0
- data/lib/maxprocs.rb +148 -0
- data/sig/generated/maxprocs/version.rbs +5 -0
- data/sig/generated/maxprocs.rbs +56 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2d7c7f470760666f2720e464ba8ed42f8dc50af36b51c57b651cc2db22dd0b36
|
|
4
|
+
data.tar.gz: be7f37e10296aa00a6cbebdc707a3078bdb1bd65cd0826da27d91378a67463d6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 680234708f8bb39980e712d8476d1e08adfc8e6801cb49c917b42a9adda2fca00041aafafb37f91a4d479f292816faceeea9f0e433c5f76131de488830b6e7a8
|
|
7
|
+
data.tar.gz: 94ee3bd32395c3c943f6495ab4883aa54d427a512a70166010a13425a89292ce9250f5804c311d9ed4c99ff7aaa695f7814c26e759174d2fe233f335bf98d899
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# maxprocs-ruby [](https://github.com/moznion/maxprocs-ruby/actions/workflows/ci.yml)
|
|
2
|
+
|
|
3
|
+
A lightweight Ruby gem that detects CPU quota from Linux cgroups and returns the appropriate number of processors.
|
|
4
|
+
|
|
5
|
+
This is a Ruby equivalent of Go's [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs).
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Ruby's `Etc.nprocessors` does not consider cgroup CPU quota - it returns the host's CPU count. In container environments (e.g., Docker/Kubernetes), the actual available CPU count differs from the host's CPU count.
|
|
10
|
+
|
|
11
|
+
For example, this causes Puma/Sidekiq worker counts to be excessive, leading to CPU throttling.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# On a 64-core host with 2 CPU limit container
|
|
15
|
+
Etc.nprocessors # => 64 (host CPUs, not what you want)
|
|
16
|
+
Maxprocs.count # => 2 (container limit, what you want)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add this line to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'maxprocs'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
And then execute:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
$ bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install it yourself as:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
$ gem install maxprocs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Synopsis
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'maxprocs'
|
|
43
|
+
|
|
44
|
+
# Get CPU count considering cgroup quota (Integer)
|
|
45
|
+
Maxprocs.count # => 2
|
|
46
|
+
|
|
47
|
+
# Get raw quota value (Float or nil)
|
|
48
|
+
Maxprocs.quota # => 2.5 (nil if unlimited)
|
|
49
|
+
|
|
50
|
+
# Check if CPU is limited
|
|
51
|
+
Maxprocs.limited? # => true/false
|
|
52
|
+
|
|
53
|
+
# Get cgroup version
|
|
54
|
+
Maxprocs.cgroup_version # => :v1, :v2, or :none
|
|
55
|
+
|
|
56
|
+
# Clear cache (for testing)
|
|
57
|
+
Maxprocs.reset!
|
|
58
|
+
|
|
59
|
+
# Rounding options
|
|
60
|
+
Maxprocs.count(round: :floor) # => 2
|
|
61
|
+
Maxprocs.count(round: :ceil) # => 3
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
| Method | Return Type | Description |
|
|
67
|
+
|--------|-------------|-------------|
|
|
68
|
+
| `Maxprocs.count(round:)` | `Integer` | CPU count (minimum 1). `round: :floor` (default) or `:ceil` |
|
|
69
|
+
| `Maxprocs.quota` | `Float` or `nil` | Raw quota value, `nil` if unlimited |
|
|
70
|
+
| `Maxprocs.limited?` | `Boolean` | `true` if CPU quota is set |
|
|
71
|
+
| `Maxprocs.cgroup_version` | `Symbol` | `:v1`, `:v2`, or `:none` |
|
|
72
|
+
| `Maxprocs.reset!` | `void` | Clear cached values |
|
|
73
|
+
|
|
74
|
+
Use `maxprocs` when you only need cgroup-aware CPU count detection and want to minimize dependencies.
|
|
75
|
+
|
|
76
|
+
## Limitations
|
|
77
|
+
|
|
78
|
+
- On macOS/Windows, always falls back to `Etc.nprocessors`
|
|
79
|
+
- cpu.shares not supported: Only `cpu.cfs_quota_us`/`cpu.max` are read (shares are relative weights, not absolute limits)
|
|
80
|
+
- Direct cgroup only: Does not traverse parent cgroups
|
|
81
|
+
- Some platforms return -1: Some environments may return unlimited quota
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Ruby 3.2+
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bundle install
|
|
91
|
+
bundle exec rake
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/moznion/maxprocs-ruby.
|
|
97
|
+
|
|
98
|
+
## Related Projects
|
|
99
|
+
|
|
100
|
+
- [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs) - Go implementation (inspiration for this gem)
|
|
101
|
+
- [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) - Full-featured concurrency library with cgroup support
|
|
102
|
+
- [Go 1.25 GOMAXPROCS](https://github.com/golang/go/issues/73193) - Native Go runtime cgroup support
|
|
103
|
+
|
data/lib/maxprocs.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require "etc"
|
|
6
|
+
require_relative "maxprocs/version"
|
|
7
|
+
|
|
8
|
+
# Maxprocs detects CPU quota from Linux cgroups and returns the appropriate
|
|
9
|
+
# number of processors for container environments.
|
|
10
|
+
module Maxprocs
|
|
11
|
+
CGROUP_FILE_PATH = "/proc/self/cgroup"
|
|
12
|
+
CGROUP_V1_QUOTA_PATH = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
|
|
13
|
+
CGROUP_V1_PERIOD_PATH = "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
|
|
14
|
+
CGROUP_V2_CONTROLLERS_PATH = "/sys/fs/cgroup/cgroup.controllers"
|
|
15
|
+
CGROUP_V2_CPU_MAX_PATH = "/sys/fs/cgroup/cpu.max"
|
|
16
|
+
|
|
17
|
+
@mutex = Mutex.new # Thread-safe memoization using a mutex
|
|
18
|
+
@quota_cache = nil
|
|
19
|
+
@version_cache = nil
|
|
20
|
+
@initialized = false
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Returns the number of CPUs available, considering cgroup quota.
|
|
24
|
+
#
|
|
25
|
+
# @rbs round: Symbol -- :floor (default) or :ceil for rounding method
|
|
26
|
+
# @rbs return: Integer -- the number of CPUs (minimum 1)
|
|
27
|
+
def count(round: :floor)
|
|
28
|
+
q = quota
|
|
29
|
+
return Etc.nprocessors if q.nil?
|
|
30
|
+
|
|
31
|
+
result = case round
|
|
32
|
+
when :ceil
|
|
33
|
+
q.ceil
|
|
34
|
+
else
|
|
35
|
+
q.floor
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[result, 1].max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the raw CPU quota as a float.
|
|
42
|
+
#
|
|
43
|
+
# @rbs return: Float? -- the quota (e.g., 2.5 for 2.5 CPUs), or nil if unlimited
|
|
44
|
+
def quota
|
|
45
|
+
ensure_initialized
|
|
46
|
+
@quota_cache
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns whether CPU is limited by cgroup.
|
|
50
|
+
#
|
|
51
|
+
# @rbs return: bool -- true if CPU quota is set
|
|
52
|
+
def limited?
|
|
53
|
+
!quota.nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the detected cgroup version.
|
|
57
|
+
#
|
|
58
|
+
# @rbs return: Symbol -- :v1, :v2, or :none
|
|
59
|
+
def cgroup_version
|
|
60
|
+
ensure_initialized
|
|
61
|
+
@version_cache
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Clears the cached values. Useful for testing.
|
|
65
|
+
#
|
|
66
|
+
# @rbs return: void
|
|
67
|
+
def reset!
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
@quota_cache = nil
|
|
70
|
+
@version_cache = nil
|
|
71
|
+
@initialized = false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @rbs return: void
|
|
78
|
+
def ensure_initialized
|
|
79
|
+
return if @initialized
|
|
80
|
+
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
return if @initialized
|
|
83
|
+
|
|
84
|
+
@version_cache = detect_cgroup_version
|
|
85
|
+
@quota_cache = read_quota
|
|
86
|
+
@initialized = true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs return: Symbol
|
|
91
|
+
def detect_cgroup_version
|
|
92
|
+
return :none unless File.exist?(CGROUP_FILE_PATH)
|
|
93
|
+
|
|
94
|
+
# Check for cgroup v2 first
|
|
95
|
+
if File.exist?(CGROUP_V2_CONTROLLERS_PATH)
|
|
96
|
+
return :v2
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if File.exist?(CGROUP_V1_QUOTA_PATH)
|
|
100
|
+
return :v1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
:none
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @rbs return: Float?
|
|
107
|
+
def read_quota
|
|
108
|
+
case @version_cache
|
|
109
|
+
when :v2
|
|
110
|
+
read_quota_v2
|
|
111
|
+
when :v1
|
|
112
|
+
read_quota_v1
|
|
113
|
+
end
|
|
114
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EINVAL
|
|
115
|
+
# File doesn't exist, permission denied, or invalid - fallback to unlimited
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs return: Float?
|
|
120
|
+
def read_quota_v1
|
|
121
|
+
quota = File.read(CGROUP_V1_QUOTA_PATH).strip.to_i
|
|
122
|
+
return nil if quota == -1 # -1 means unlimited
|
|
123
|
+
|
|
124
|
+
period = File.read(CGROUP_V1_PERIOD_PATH).strip.to_i
|
|
125
|
+
return nil if period <= 0
|
|
126
|
+
|
|
127
|
+
quota.to_f / period
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @rbs return: Float?
|
|
131
|
+
def read_quota_v2
|
|
132
|
+
content = File.read(CGROUP_V2_CPU_MAX_PATH).strip
|
|
133
|
+
parts = content.split
|
|
134
|
+
|
|
135
|
+
max_str = parts[0]
|
|
136
|
+
return nil if max_str == "max" # "max" means unlimited
|
|
137
|
+
|
|
138
|
+
period_str = parts[1]
|
|
139
|
+
return nil if period_str.nil?
|
|
140
|
+
|
|
141
|
+
max = max_str.to_f
|
|
142
|
+
period = period_str.to_f
|
|
143
|
+
return nil if period <= 0
|
|
144
|
+
|
|
145
|
+
max / period
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Generated from lib/maxprocs.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Maxprocs detects CPU quota from Linux cgroups and returns the appropriate
|
|
4
|
+
# number of processors for container environments.
|
|
5
|
+
module Maxprocs
|
|
6
|
+
CGROUP_FILE_PATH: ::String
|
|
7
|
+
|
|
8
|
+
CGROUP_V1_QUOTA_PATH: ::String
|
|
9
|
+
|
|
10
|
+
CGROUP_V1_PERIOD_PATH: ::String
|
|
11
|
+
|
|
12
|
+
CGROUP_V2_CONTROLLERS_PATH: ::String
|
|
13
|
+
|
|
14
|
+
CGROUP_V2_CPU_MAX_PATH: ::String
|
|
15
|
+
|
|
16
|
+
# Returns the number of CPUs available, considering cgroup quota.
|
|
17
|
+
#
|
|
18
|
+
# @rbs round: Symbol -- :floor (default) or :ceil for rounding method
|
|
19
|
+
# @rbs return: Integer -- the number of CPUs (minimum 1)
|
|
20
|
+
def self.count: (?round: Symbol) -> Integer
|
|
21
|
+
|
|
22
|
+
# Returns the raw CPU quota as a float.
|
|
23
|
+
#
|
|
24
|
+
# @rbs return: Float? -- the quota (e.g., 2.5 for 2.5 CPUs), or nil if unlimited
|
|
25
|
+
def self.quota: () -> Float?
|
|
26
|
+
|
|
27
|
+
# Returns whether CPU is limited by cgroup.
|
|
28
|
+
#
|
|
29
|
+
# @rbs return: bool -- true if CPU quota is set
|
|
30
|
+
def self.limited?: () -> bool
|
|
31
|
+
|
|
32
|
+
# Returns the detected cgroup version.
|
|
33
|
+
#
|
|
34
|
+
# @rbs return: Symbol -- :v1, :v2, or :none
|
|
35
|
+
def self.cgroup_version: () -> Symbol
|
|
36
|
+
|
|
37
|
+
# Clears the cached values. Useful for testing.
|
|
38
|
+
#
|
|
39
|
+
# @rbs return: void
|
|
40
|
+
def self.reset!: () -> void
|
|
41
|
+
|
|
42
|
+
# @rbs return: void
|
|
43
|
+
private def self.ensure_initialized: () -> void
|
|
44
|
+
|
|
45
|
+
# @rbs return: Symbol
|
|
46
|
+
private def self.detect_cgroup_version: () -> Symbol
|
|
47
|
+
|
|
48
|
+
# @rbs return: Float?
|
|
49
|
+
private def self.read_quota: () -> Float?
|
|
50
|
+
|
|
51
|
+
# @rbs return: Float?
|
|
52
|
+
private def self.read_quota_v1: () -> Float?
|
|
53
|
+
|
|
54
|
+
# @rbs return: Float?
|
|
55
|
+
private def self.read_quota_v2: () -> Float?
|
|
56
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: maxprocs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- moznion
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: simplecov
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.22'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.22'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.3'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: standard
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.52'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.52'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rbs-inline
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.12'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: steep
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.10'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.10'
|
|
96
|
+
description: |
|
|
97
|
+
A lightweight Ruby gem that detects CPU quota from Linux cgroups (v1 and v2)
|
|
98
|
+
and returns the appropriate number of processors for container environments.
|
|
99
|
+
This is a Ruby equivalent of Go's uber-go/automaxprocs.
|
|
100
|
+
email:
|
|
101
|
+
- moznion@mail.moznion.net
|
|
102
|
+
executables: []
|
|
103
|
+
extensions: []
|
|
104
|
+
extra_rdoc_files: []
|
|
105
|
+
files:
|
|
106
|
+
- CHANGELOG.md
|
|
107
|
+
- README.md
|
|
108
|
+
- lib/maxprocs.rb
|
|
109
|
+
- lib/maxprocs/version.rb
|
|
110
|
+
- sig/generated/maxprocs.rbs
|
|
111
|
+
- sig/generated/maxprocs/version.rbs
|
|
112
|
+
homepage: https://github.com/moznion/maxprocs-ruby
|
|
113
|
+
licenses:
|
|
114
|
+
- MIT
|
|
115
|
+
metadata:
|
|
116
|
+
homepage_uri: https://github.com/moznion/maxprocs-ruby
|
|
117
|
+
source_code_uri: https://github.com/moznion/maxprocs-ruby
|
|
118
|
+
changelog_uri: https://github.com/moznion/maxprocs-ruby/blob/main/CHANGELOG.md
|
|
119
|
+
rubygems_mfa_required: 'true'
|
|
120
|
+
rdoc_options: []
|
|
121
|
+
require_paths:
|
|
122
|
+
- lib
|
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: 3.2.0
|
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
requirements: []
|
|
134
|
+
rubygems_version: 3.6.7
|
|
135
|
+
specification_version: 4
|
|
136
|
+
summary: Detect CPU quota from Linux cgroups for container environments
|
|
137
|
+
test_files: []
|