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 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
@@ -0,0 +1,6 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-19
4
+
5
+ - Initial release
6
+
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # maxprocs-ruby [![CI](https://github.com/moznion/maxprocs-ruby/actions/workflows/ci.yml/badge.svg)](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
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maxprocs
4
+ VERSION = "0.1.0" #: String
5
+ end
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,5 @@
1
+ # Generated from lib/maxprocs/version.rb with RBS::Inline
2
+
3
+ module Maxprocs
4
+ VERSION: String
5
+ 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: []