├── .build
└── check_license.sh
├── .codecov.yml
├── .github
└── workflows
│ ├── fossa.yaml
│ └── go.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── automaxprocs.go
├── example_test.go
├── go.mod
├── go.sum
├── internal
├── cgroups
│ ├── cgroup.go
│ ├── cgroup_test.go
│ ├── cgroups.go
│ ├── cgroups2.go
│ ├── cgroups2_test.go
│ ├── cgroups_test.go
│ ├── doc.go
│ ├── errors.go
│ ├── mountpoint.go
│ ├── mountpoint_test.go
│ ├── subsys.go
│ ├── subsys_test.go
│ ├── testdata
│ │ ├── cgroups
│ │ │ ├── cpu
│ │ │ │ ├── cpu.cfs_period_us
│ │ │ │ └── cpu.cfs_quota_us
│ │ │ ├── empty
│ │ │ │ └── cpu.cfs_quota_us
│ │ │ ├── invalid
│ │ │ │ └── cpu.cfs_quota_us
│ │ │ ├── undefined-period
│ │ │ │ └── cpu.cfs_quota_us
│ │ │ ├── undefined
│ │ │ │ ├── cpu.cfs_period_us
│ │ │ │ └── cpu.cfs_quota_us
│ │ │ ├── v2
│ │ │ │ ├── empty
│ │ │ │ ├── invalid-max
│ │ │ │ ├── invalid-period
│ │ │ │ ├── only-max
│ │ │ │ ├── set
│ │ │ │ ├── too-few-fields
│ │ │ │ ├── too-many-fields
│ │ │ │ ├── unset
│ │ │ │ └── zero-period
│ │ │ └── zero-period
│ │ │ │ ├── cpu.cfs_period_us
│ │ │ │ └── cpu.cfs_quota_us
│ │ └── proc
│ │ │ ├── cgroups
│ │ │ ├── cgroup
│ │ │ └── mountinfo
│ │ │ ├── invalid-cgroup
│ │ │ └── cgroup
│ │ │ ├── invalid-mountinfo
│ │ │ └── mountinfo
│ │ │ ├── untranslatable
│ │ │ ├── cgroup
│ │ │ └── mountinfo
│ │ │ └── v2
│ │ │ ├── cgroup-invalid
│ │ │ ├── cgroup-no-match
│ │ │ ├── cgroup-root
│ │ │ ├── cgroup-subdir
│ │ │ ├── mountinfo
│ │ │ ├── mountinfo-v1-v2
│ │ │ └── mountinfo-v2
│ └── util_test.go
└── runtime
│ ├── cpu_quota_linux.go
│ ├── cpu_quota_linux_test.go
│ ├── cpu_quota_unsupported.go
│ └── runtime.go
├── maxprocs
├── example_test.go
├── maxprocs.go
├── maxprocs_test.go
└── version.go
└── tools
├── go.mod
├── go.sum
└── tools.go
/.build/check_license.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | ERROR_COUNT=0
4 | while read -r file
5 | do
6 | case "$(head -1 "${file}")" in
7 | *"Copyright (c) "*" Uber Technologies, Inc.")
8 | # everything's cool
9 | ;;
10 | *)
11 | echo "$file is missing license header."
12 | (( ERROR_COUNT++ ))
13 | ;;
14 | esac
15 | done < <(git ls-files "*\.go")
16 |
17 | exit $ERROR_COUNT
18 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: 80..100
3 | round: down
4 | precision: 2
5 |
6 | status:
7 | project: # measuring the overall project coverage
8 | default: # context, you can create multiple ones with custom titles
9 | enabled: yes # must be yes|true to enable this status
10 | target: 90% # specify the target coverage for each commit status
11 | # option: "auto" (must increase from parent commit or pull request base)
12 | # option: "X%" a static target percentage to hit
13 | if_not_found: success # if parent is not found report status as success, error, or failure
14 | if_ci_failed: error # if ci fails report status as success, error, or failure
15 |
--------------------------------------------------------------------------------
/.github/workflows/fossa.yaml:
--------------------------------------------------------------------------------
1 | name: FOSSA Analysis
2 | on: push
3 |
4 | jobs:
5 |
6 | build:
7 | runs-on: ubuntu-latest
8 | if: github.repository_owner == 'uber-go'
9 | steps:
10 | - name: Checkout code
11 | uses: actions/checkout@v2
12 |
13 | - name: FOSSA analysis
14 | uses: fossas/fossa-action@v1
15 | with:
16 | api-key: ${{ secrets.FOSSA_API_KEY }}
17 |
18 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: ['*']
6 | tags: ['v*']
7 | pull_request:
8 | branches: ['*']
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 |
15 | build:
16 | runs-on: ubuntu-latest
17 | strategy:
18 | matrix:
19 | go: ["1.20.x", "1.21.x"]
20 | include:
21 | - go: 1.21.x
22 | latest: true
23 |
24 | steps:
25 | - name: Setup Go
26 | uses: actions/setup-go@v4
27 | with:
28 | go-version: ${{ matrix.go }}
29 |
30 | - name: Checkout code
31 | uses: actions/checkout@v3
32 |
33 | - name: Load cached dependencies
34 | uses: actions/cache@v1
35 | with:
36 | path: ~/go/pkg/mod
37 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
38 | restore-keys: |
39 | ${{ runner.os }}-go-
40 | - name: Lint
41 | if: matrix.latest
42 | run: make lint
43 |
44 | - name: Test
45 | run: make cover
46 |
47 | - name: Upload coverage to codecov.io
48 | uses: codecov/codecov-action@v3
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 | vendor
10 |
11 | # Architecture specific extensions/prefixes
12 | *.[568vq]
13 | [568vq].out
14 |
15 | *.cgo1.go
16 | *.cgo2.c
17 | _cgo_defun.c
18 | _cgo_gotypes.go
19 | _cgo_export.*
20 |
21 | _testmain.go
22 |
23 | *.exe
24 | *.test
25 | *.prof
26 | *.pprof
27 | *.out
28 | *.log
29 | coverage.txt
30 |
31 | /bin
32 | cover.out
33 | cover.html
34 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v1.6.0 (2024-07-24)
4 |
5 | - Add RoundQuotaFunc option that allows configuration of rounding
6 | behavior for floating point CPU quota.
7 |
8 | ## v1.5.3 (2023-07-19)
9 |
10 | - Fix mountinfo parsing when super options have fields with spaces.
11 | - Fix division by zero while parsing cgroups.
12 |
13 | ## v1.5.2 (2023-03-16)
14 |
15 | - Support child control cgroups
16 | - Fix file descriptor leak
17 | - Update dependencies
18 |
19 | ## v1.5.1 (2022-04-06)
20 |
21 | - Fix cgroups v2 mountpoint detection.
22 |
23 | ## v1.5.0 (2022-04-05)
24 |
25 | - Add support for cgroups v2.
26 |
27 | Thanks to @emadolsky for their contribution to this release.
28 |
29 | ## v1.4.0 (2021-02-01)
30 |
31 | - Support colons in cgroup names.
32 | - Remove linters from runtime dependencies.
33 |
34 | ## v1.3.0 (2020-01-23)
35 |
36 | - Migrate to Go modules.
37 |
38 | ## v1.2.0 (2018-02-22)
39 |
40 | - Fixed quota clamping to always round down rather than up; Rather than
41 | guaranteeing constant throttling at saturation, instead assume that the
42 | fractional CPU was added as a hedge for factors outside of Go's scheduler.
43 |
44 | ## v1.1.0 (2017-11-10)
45 |
46 | - Log the new value of `GOMAXPROCS` rather than the current value.
47 | - Make logs more explicit about whether `GOMAXPROCS` was modified or not.
48 | - Allow customization of the minimum `GOMAXPROCS`, and modify default from 2 to 1.
49 |
50 | ## v1.0.0 (2017-08-09)
51 |
52 | - Initial release.
53 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age,
8 | body size, disability, ethnicity, gender identity and expression, level of
9 | experience, nationality, personal appearance, race, religion, or sexual
10 | identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an
52 | appointed representative at an online or offline event. Representation of a
53 | project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at oss-conduct@uber.com. The project
59 | team will review and investigate all complaints, and will respond in a way
60 | that it deems appropriate to the circumstances. The project team is obligated
61 | to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
71 | version 1.4, available at
72 | [http://contributor-covenant.org/version/1/4][version].
73 |
74 | [homepage]: http://contributor-covenant.org
75 | [version]: http://contributor-covenant.org/version/1/4/
76 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We'd love your help improving this package!
4 |
5 | If you'd like to add new exported APIs, please [open an issue][open-issue]
6 | describing your proposal — discussing API changes ahead of time makes
7 | pull request review much smoother. In your issue, pull request, and any other
8 | communications, please remember to treat your fellow contributors with
9 | respect! We take our [code of conduct](CODE_OF_CONDUCT.md) seriously.
10 |
11 | Note that you'll need to sign [Uber's Contributor License Agreement][cla]
12 | before we can accept any of your contributions. If necessary, a bot will remind
13 | you to accept the CLA when you open your pull request.
14 |
15 | ## Setup
16 |
17 | [Fork][fork], then clone the repository:
18 |
19 | ```
20 | mkdir -p $GOPATH/src/go.uber.org
21 | cd $GOPATH/src/go.uber.org
22 | git clone git@github.com:your_github_username/automaxprocs.git
23 | cd automaxprocs
24 | git remote add upstream https://github.com/uber-go/automaxprocs.git
25 | git fetch upstream
26 | ```
27 |
28 | Install the test dependencies:
29 |
30 | ```
31 | make dependencies
32 | ```
33 |
34 | Make sure that the tests and the linters pass:
35 |
36 | ```
37 | make test
38 | make lint
39 | ```
40 |
41 | If you're not using the minor version of Go specified in the Makefile's
42 | `LINTABLE_MINOR_VERSIONS` variable, `make lint` doesn't do anything. This is
43 | fine, but it means that you'll only discover lint failures after you open your
44 | pull request.
45 |
46 | ## Making Changes
47 |
48 | Start by creating a new branch for your changes:
49 |
50 | ```
51 | cd $GOPATH/src/go.uber.org/automaxprocs
52 | git checkout master
53 | git fetch upstream
54 | git rebase upstream/master
55 | git checkout -b cool_new_feature
56 | ```
57 |
58 | Make your changes, then ensure that `make lint` and `make test` still pass. If
59 | you're satisfied with your changes, push them to your fork.
60 |
61 | ```
62 | git push origin cool_new_feature
63 | ```
64 |
65 | Then use the GitHub UI to open a pull request.
66 |
67 | At this point, you're waiting on us to review your changes. We *try* to respond
68 | to issues and pull requests within a few business days, and we may suggest some
69 | improvements or alternatives. Once your changes are approved, one of the
70 | project maintainers will merge them.
71 |
72 | We're much more likely to approve your changes if you:
73 |
74 | * Add tests for new functionality.
75 | * Write a [good commit message][commit-message].
76 | * Maintain backward compatibility.
77 |
78 | [fork]: https://github.com/uber-go/automaxprocs/fork
79 | [open-issue]: https://github.com/uber-go/automaxprocs/issues/new
80 | [cla]: https://cla-assistant.io/uber-go/automaxprocs
81 | [commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
82 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 Uber Technologies, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | export GOBIN ?= $(shell pwd)/bin
2 |
3 | GO_FILES := $(shell \
4 | find . '(' -path '*/.*' -o -path './vendor' ')' -prune \
5 | -o -name '*.go' -print | cut -b3-)
6 |
7 | GOLINT = $(GOBIN)/golint
8 | STATICCHECK = $(GOBIN)/staticcheck
9 |
10 | .PHONY: build
11 | build:
12 | go build ./...
13 |
14 | .PHONY: install
15 | install:
16 | go mod download
17 |
18 | .PHONY: test
19 | test:
20 | go test -race ./...
21 |
22 | .PHONY: cover
23 | cover:
24 | go test -coverprofile=cover.out -covermode=atomic -coverpkg=./... ./...
25 | go tool cover -html=cover.out -o cover.html
26 |
27 | $(GOLINT): tools/go.mod
28 | cd tools && go install golang.org/x/lint/golint
29 |
30 | $(STATICCHECK): tools/go.mod
31 | cd tools && go install honnef.co/go/tools/cmd/staticcheck@2023.1.2
32 |
33 | .PHONY: lint
34 | lint: $(GOLINT) $(STATICCHECK)
35 | @rm -rf lint.log
36 | @echo "Checking gofmt"
37 | @gofmt -d -s $(GO_FILES) 2>&1 | tee lint.log
38 | @echo "Checking go vet"
39 | @go vet ./... 2>&1 | tee -a lint.log
40 | @echo "Checking golint"
41 | @$(GOLINT) ./... | tee -a lint.log
42 | @echo "Checking staticcheck"
43 | @$(STATICCHECK) ./... 2>&1 | tee -a lint.log
44 | @echo "Checking for license headers..."
45 | @./.build/check_license.sh | tee -a lint.log
46 | @[ ! -s lint.log ]
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # automaxprocs [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]
2 |
3 | Automatically set `GOMAXPROCS` to match Linux container CPU quota.
4 |
5 | ## Installation
6 |
7 | `go get -u go.uber.org/automaxprocs`
8 |
9 | ## Quick Start
10 |
11 | ```go
12 | import _ "go.uber.org/automaxprocs"
13 |
14 | func main() {
15 | // Your application logic here.
16 | }
17 | ```
18 |
19 | # Performance
20 | Data measured from Uber's internal load balancer. We ran the load balancer with 200% CPU quota (i.e., 2 cores):
21 |
22 | | GOMAXPROCS | RPS | P50 (ms) | P99.9 (ms) |
23 | | ------------------ | --------- | -------- | ---------- |
24 | | 1 | 28,893.18 | 1.46 | 19.70 |
25 | | 2 (equal to quota) | 44,715.07 | 0.84 | 26.38 |
26 | | 3 | 44,212.93 | 0.66 | 30.07 |
27 | | 4 | 41,071.15 | 0.57 | 42.94 |
28 | | 8 | 33,111.69 | 0.43 | 64.32 |
29 | | Default (24) | 22,191.40 | 0.45 | 76.19 |
30 |
31 | When `GOMAXPROCS` is increased above the CPU quota, we see P50 decrease slightly, but see significant increases to P99. We also see that the total RPS handled also decreases.
32 |
33 | When `GOMAXPROCS` is higher than the CPU quota allocated, we also saw significant throttling:
34 |
35 | ```
36 | $ cat /sys/fs/cgroup/cpu,cpuacct/system.slice/[...]/cpu.stat
37 | nr_periods 42227334
38 | nr_throttled 131923
39 | throttled_time 88613212216618
40 | ```
41 |
42 | Once `GOMAXPROCS` was reduced to match the CPU quota, we saw no CPU throttling.
43 |
44 | ## Development Status: Stable
45 |
46 | All APIs are finalized, and no breaking changes will be made in the 1.x series
47 | of releases. Users of semver-aware dependency management systems should pin
48 | automaxprocs to `^1`.
49 |
50 | ## Contributing
51 |
52 | We encourage and support an active, healthy community of contributors —
53 | including you! Details are in the [contribution guide](CONTRIBUTING.md) and
54 | the [code of conduct](CODE_OF_CONDUCT.md). The automaxprocs maintainers keep
55 | an eye on issues and pull requests, but you can also report any negative
56 | conduct to oss-conduct@uber.com. That email list is a private, safe space;
57 | even the automaxprocs maintainers don't have access, so don't hesitate to hold
58 | us to a high standard.
59 |
60 |
61 |
62 | Released under the [MIT License](LICENSE).
63 |
64 | [doc-img]: https://godoc.org/go.uber.org/automaxprocs?status.svg
65 | [doc]: https://godoc.org/go.uber.org/automaxprocs
66 | [ci-img]: https://github.com/uber-go/automaxprocs/actions/workflows/go.yml/badge.svg
67 | [ci]: https://github.com/uber-go/automaxprocs/actions/workflows/go.yml
68 | [cov-img]: https://codecov.io/gh/uber-go/automaxprocs/branch/master/graph/badge.svg
69 | [cov]: https://codecov.io/gh/uber-go/automaxprocs
70 |
71 |
72 |
--------------------------------------------------------------------------------
/automaxprocs.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | // Package automaxprocs automatically sets GOMAXPROCS to match the Linux
22 | // container CPU quota, if any.
23 | package automaxprocs // import "go.uber.org/automaxprocs"
24 |
25 | import (
26 | "log"
27 |
28 | "go.uber.org/automaxprocs/maxprocs"
29 | )
30 |
31 | func init() {
32 | maxprocs.Set(maxprocs.Logger(log.Printf))
33 | }
34 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package automaxprocs_test
22 |
23 | // Importing automaxprocs automatically adjusts GOMAXPROCS.
24 | import _ "go.uber.org/automaxprocs"
25 |
26 | // To render a whole-file example, we need a package-level declaration.
27 | var _ = ""
28 |
29 | func Example() {}
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module go.uber.org/automaxprocs
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/prashantv/gostub v1.1.0
7 | github.com/stretchr/testify v1.7.1
8 | )
9 |
10 | require (
11 | github.com/davecgh/go-spew v1.1.1 // indirect
12 | github.com/kr/pretty v0.1.0 // indirect
13 | github.com/pmezard/go-difflib v1.0.0 // indirect
14 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
15 | gopkg.in/yaml.v3 v3.0.1 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
12 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
18 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
22 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroup.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "bufio"
28 | "io"
29 | "os"
30 | "path/filepath"
31 | "strconv"
32 | )
33 |
34 | // CGroup represents the data structure for a Linux control group.
35 | type CGroup struct {
36 | path string
37 | }
38 |
39 | // NewCGroup returns a new *CGroup from a given path.
40 | func NewCGroup(path string) *CGroup {
41 | return &CGroup{path: path}
42 | }
43 |
44 | // Path returns the path of the CGroup*.
45 | func (cg *CGroup) Path() string {
46 | return cg.path
47 | }
48 |
49 | // ParamPath returns the path of the given cgroup param under itself.
50 | func (cg *CGroup) ParamPath(param string) string {
51 | return filepath.Join(cg.path, param)
52 | }
53 |
54 | // readFirstLine reads the first line from a cgroup param file.
55 | func (cg *CGroup) readFirstLine(param string) (string, error) {
56 | paramFile, err := os.Open(cg.ParamPath(param))
57 | if err != nil {
58 | return "", err
59 | }
60 | defer paramFile.Close()
61 |
62 | scanner := bufio.NewScanner(paramFile)
63 | if scanner.Scan() {
64 | return scanner.Text(), nil
65 | }
66 | if err := scanner.Err(); err != nil {
67 | return "", err
68 | }
69 | return "", io.ErrUnexpectedEOF
70 | }
71 |
72 | // readInt parses the first line from a cgroup param file as int.
73 | func (cg *CGroup) readInt(param string) (int, error) {
74 | text, err := cg.readFirstLine(param)
75 | if err != nil {
76 | return 0, err
77 | }
78 | return strconv.Atoi(text)
79 | }
80 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroup_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "path/filepath"
28 | "testing"
29 |
30 | "github.com/stretchr/testify/assert"
31 | )
32 |
33 | func TestCGroupParamPath(t *testing.T) {
34 | cgroup := NewCGroup("/sys/fs/cgroup/cpu")
35 | assert.Equal(t, "/sys/fs/cgroup/cpu", cgroup.Path())
36 | assert.Equal(t, "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", cgroup.ParamPath("cpu.cfs_quota_us"))
37 | }
38 |
39 | func TestCGroupReadFirstLine(t *testing.T) {
40 | testTable := []struct {
41 | name string
42 | paramName string
43 | expectedContent string
44 | shouldHaveError bool
45 | }{
46 | {
47 | name: "cpu",
48 | paramName: "cpu.cfs_period_us",
49 | expectedContent: "100000",
50 | shouldHaveError: false,
51 | },
52 | {
53 | name: "absent",
54 | paramName: "cpu.stat",
55 | expectedContent: "",
56 | shouldHaveError: true,
57 | },
58 | {
59 | name: "empty",
60 | paramName: "cpu.cfs_quota_us",
61 | expectedContent: "",
62 | shouldHaveError: true,
63 | },
64 | }
65 |
66 | for _, tt := range testTable {
67 | cgroupPath := filepath.Join(testDataCGroupsPath, tt.name)
68 | cgroup := NewCGroup(cgroupPath)
69 |
70 | content, err := cgroup.readFirstLine(tt.paramName)
71 | assert.Equal(t, tt.expectedContent, content, tt.name)
72 |
73 | if tt.shouldHaveError {
74 | assert.Error(t, err, tt.name)
75 | } else {
76 | assert.NoError(t, err, tt.name)
77 | }
78 | }
79 | }
80 |
81 | func TestCGroupReadInt(t *testing.T) {
82 | testTable := []struct {
83 | name string
84 | paramName string
85 | expectedValue int
86 | shouldHaveError bool
87 | }{
88 | {
89 | name: "cpu",
90 | paramName: "cpu.cfs_period_us",
91 | expectedValue: 100000,
92 | shouldHaveError: false,
93 | },
94 | {
95 | name: "empty",
96 | paramName: "cpu.cfs_quota_us",
97 | expectedValue: 0,
98 | shouldHaveError: true,
99 | },
100 | {
101 | name: "invalid",
102 | paramName: "cpu.cfs_quota_us",
103 | expectedValue: 0,
104 | shouldHaveError: true,
105 | },
106 | {
107 | name: "absent",
108 | paramName: "cpu.cfs_quota_us",
109 | expectedValue: 0,
110 | shouldHaveError: true,
111 | },
112 | }
113 |
114 | for _, tt := range testTable {
115 | cgroupPath := filepath.Join(testDataCGroupsPath, tt.name)
116 | cgroup := NewCGroup(cgroupPath)
117 |
118 | value, err := cgroup.readInt(tt.paramName)
119 | assert.Equal(t, tt.expectedValue, value, "%s/%s", tt.name, tt.paramName)
120 |
121 | if tt.shouldHaveError {
122 | assert.Error(t, err, tt.name)
123 | } else {
124 | assert.NoError(t, err, tt.name)
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroups.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | const (
27 | // _cgroupFSType is the Linux CGroup file system type used in
28 | // `/proc/$PID/mountinfo`.
29 | _cgroupFSType = "cgroup"
30 | // _cgroupSubsysCPU is the CPU CGroup subsystem.
31 | _cgroupSubsysCPU = "cpu"
32 | // _cgroupSubsysCPUAcct is the CPU accounting CGroup subsystem.
33 | _cgroupSubsysCPUAcct = "cpuacct"
34 | // _cgroupSubsysCPUSet is the CPUSet CGroup subsystem.
35 | _cgroupSubsysCPUSet = "cpuset"
36 | // _cgroupSubsysMemory is the Memory CGroup subsystem.
37 | _cgroupSubsysMemory = "memory"
38 |
39 | // _cgroupCPUCFSQuotaUsParam is the file name for the CGroup CFS quota
40 | // parameter.
41 | _cgroupCPUCFSQuotaUsParam = "cpu.cfs_quota_us"
42 | // _cgroupCPUCFSPeriodUsParam is the file name for the CGroup CFS period
43 | // parameter.
44 | _cgroupCPUCFSPeriodUsParam = "cpu.cfs_period_us"
45 | )
46 |
47 | const (
48 | _procPathCGroup = "/proc/self/cgroup"
49 | _procPathMountInfo = "/proc/self/mountinfo"
50 | )
51 |
52 | // CGroups is a map that associates each CGroup with its subsystem name.
53 | type CGroups map[string]*CGroup
54 |
55 | // NewCGroups returns a new *CGroups from given `mountinfo` and `cgroup` files
56 | // under for some process under `/proc` file system (see also proc(5) for more
57 | // information).
58 | func NewCGroups(procPathMountInfo, procPathCGroup string) (CGroups, error) {
59 | cgroupSubsystems, err := parseCGroupSubsystems(procPathCGroup)
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | cgroups := make(CGroups)
65 | newMountPoint := func(mp *MountPoint) error {
66 | if mp.FSType != _cgroupFSType {
67 | return nil
68 | }
69 |
70 | for _, opt := range mp.SuperOptions {
71 | subsys, exists := cgroupSubsystems[opt]
72 | if !exists {
73 | continue
74 | }
75 |
76 | cgroupPath, err := mp.Translate(subsys.Name)
77 | if err != nil {
78 | return err
79 | }
80 | cgroups[opt] = NewCGroup(cgroupPath)
81 | }
82 |
83 | return nil
84 | }
85 |
86 | if err := parseMountInfo(procPathMountInfo, newMountPoint); err != nil {
87 | return nil, err
88 | }
89 | return cgroups, nil
90 | }
91 |
92 | // NewCGroupsForCurrentProcess returns a new *CGroups instance for the current
93 | // process.
94 | func NewCGroupsForCurrentProcess() (CGroups, error) {
95 | return NewCGroups(_procPathMountInfo, _procPathCGroup)
96 | }
97 |
98 | // CPUQuota returns the CPU quota applied with the CPU cgroup controller.
99 | // It is a result of `cpu.cfs_quota_us / cpu.cfs_period_us`. If the value of
100 | // `cpu.cfs_quota_us` was not set (-1), the method returns `(-1, nil)`.
101 | func (cg CGroups) CPUQuota() (float64, bool, error) {
102 | cpuCGroup, exists := cg[_cgroupSubsysCPU]
103 | if !exists {
104 | return -1, false, nil
105 | }
106 |
107 | cfsQuotaUs, err := cpuCGroup.readInt(_cgroupCPUCFSQuotaUsParam)
108 | if defined := cfsQuotaUs > 0; err != nil || !defined {
109 | return -1, defined, err
110 | }
111 |
112 | cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam)
113 | if defined := cfsPeriodUs > 0; err != nil || !defined {
114 | return -1, defined, err
115 | }
116 |
117 | return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil
118 | }
119 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroups2.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "bufio"
28 | "errors"
29 | "fmt"
30 | "io"
31 | "os"
32 | "path"
33 | "strconv"
34 | "strings"
35 | )
36 |
37 | const (
38 | // _cgroupv2CPUMax is the file name for the CGroup-V2 CPU max and period
39 | // parameter.
40 | _cgroupv2CPUMax = "cpu.max"
41 | // _cgroupFSType is the Linux CGroup-V2 file system type used in
42 | // `/proc/$PID/mountinfo`.
43 | _cgroupv2FSType = "cgroup2"
44 |
45 | _cgroupv2MountPoint = "/sys/fs/cgroup"
46 |
47 | _cgroupV2CPUMaxDefaultPeriod = 100000
48 | _cgroupV2CPUMaxQuotaMax = "max"
49 | )
50 |
51 | const (
52 | _cgroupv2CPUMaxQuotaIndex = iota
53 | _cgroupv2CPUMaxPeriodIndex
54 | )
55 |
56 | // ErrNotV2 indicates that the system is not using cgroups2.
57 | var ErrNotV2 = errors.New("not using cgroups2")
58 |
59 | // CGroups2 provides access to cgroups data for systems using cgroups2.
60 | type CGroups2 struct {
61 | mountPoint string
62 | groupPath string
63 | cpuMaxFile string
64 | }
65 |
66 | // NewCGroups2ForCurrentProcess builds a CGroups2 for the current process.
67 | //
68 | // This returns ErrNotV2 if the system is not using cgroups2.
69 | func NewCGroups2ForCurrentProcess() (*CGroups2, error) {
70 | return newCGroups2From(_procPathMountInfo, _procPathCGroup)
71 | }
72 |
73 | func newCGroups2From(mountInfoPath, procPathCGroup string) (*CGroups2, error) {
74 | isV2, err := isCGroupV2(mountInfoPath)
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | if !isV2 {
80 | return nil, ErrNotV2
81 | }
82 |
83 | subsystems, err := parseCGroupSubsystems(procPathCGroup)
84 | if err != nil {
85 | return nil, err
86 | }
87 |
88 | // Find v2 subsystem by looking for the `0` id
89 | var v2subsys *CGroupSubsys
90 | for _, subsys := range subsystems {
91 | if subsys.ID == 0 {
92 | v2subsys = subsys
93 | break
94 | }
95 | }
96 |
97 | if v2subsys == nil {
98 | return nil, ErrNotV2
99 | }
100 |
101 | return &CGroups2{
102 | mountPoint: _cgroupv2MountPoint,
103 | groupPath: v2subsys.Name,
104 | cpuMaxFile: _cgroupv2CPUMax,
105 | }, nil
106 | }
107 |
108 | func isCGroupV2(procPathMountInfo string) (bool, error) {
109 | var (
110 | isV2 bool
111 | newMountPoint = func(mp *MountPoint) error {
112 | isV2 = isV2 || (mp.FSType == _cgroupv2FSType && mp.MountPoint == _cgroupv2MountPoint)
113 | return nil
114 | }
115 | )
116 |
117 | if err := parseMountInfo(procPathMountInfo, newMountPoint); err != nil {
118 | return false, err
119 | }
120 |
121 | return isV2, nil
122 | }
123 |
124 | // CPUQuota returns the CPU quota applied with the CPU cgroup2 controller.
125 | // It is a result of reading cpu quota and period from cpu.max file.
126 | // It will return `cpu.max / cpu.period`. If cpu.max is set to max, it returns
127 | // (-1, false, nil)
128 | func (cg *CGroups2) CPUQuota() (float64, bool, error) {
129 | cpuMaxParams, err := os.Open(path.Join(cg.mountPoint, cg.groupPath, cg.cpuMaxFile))
130 | if err != nil {
131 | if os.IsNotExist(err) {
132 | return -1, false, nil
133 | }
134 | return -1, false, err
135 | }
136 | defer cpuMaxParams.Close()
137 |
138 | scanner := bufio.NewScanner(cpuMaxParams)
139 | if scanner.Scan() {
140 | fields := strings.Fields(scanner.Text())
141 | if len(fields) == 0 || len(fields) > 2 {
142 | return -1, false, fmt.Errorf("invalid format")
143 | }
144 |
145 | if fields[_cgroupv2CPUMaxQuotaIndex] == _cgroupV2CPUMaxQuotaMax {
146 | return -1, false, nil
147 | }
148 |
149 | max, err := strconv.Atoi(fields[_cgroupv2CPUMaxQuotaIndex])
150 | if err != nil {
151 | return -1, false, err
152 | }
153 |
154 | var period int
155 | if len(fields) == 1 {
156 | period = _cgroupV2CPUMaxDefaultPeriod
157 | } else {
158 | period, err = strconv.Atoi(fields[_cgroupv2CPUMaxPeriodIndex])
159 | if err != nil {
160 | return -1, false, err
161 | }
162 |
163 | if period == 0 {
164 | return -1, false, errors.New("zero value for period is not allowed")
165 | }
166 | }
167 |
168 | return float64(max) / float64(period), true, nil
169 | }
170 |
171 | if err := scanner.Err(); err != nil {
172 | return -1, false, err
173 | }
174 |
175 | return 0, false, io.ErrUnexpectedEOF
176 | }
177 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroups2_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "os"
28 | "os/user"
29 | "path/filepath"
30 | "testing"
31 |
32 | "github.com/stretchr/testify/assert"
33 | "github.com/stretchr/testify/require"
34 | )
35 |
36 | func TestCGroupsIsCGroupV2(t *testing.T) {
37 | tests := []struct {
38 | name string
39 | isV2 bool
40 | wantErr bool // should be false if isV2 is true
41 | }{
42 | {
43 | name: "mountinfo",
44 | isV2: false,
45 | wantErr: false,
46 | },
47 | {
48 | name: "mountinfo-v1-v2",
49 | isV2: false,
50 | wantErr: false,
51 | },
52 | {
53 | name: "mountinfo-v2",
54 | isV2: true,
55 | wantErr: false,
56 | },
57 | {
58 | name: "mountinfo-nonexistent",
59 | isV2: false,
60 | wantErr: true,
61 | },
62 | }
63 |
64 | for _, tt := range tests {
65 | t.Run(tt.name, func(t *testing.T) {
66 | mountInfoPath := filepath.Join(testDataProcPath, "v2", tt.name)
67 | procCgroupPath := filepath.Join(testDataProcPath, "v2", "cgroup-root")
68 | _, err := newCGroups2From(mountInfoPath, procCgroupPath)
69 | switch {
70 | case tt.wantErr:
71 | assert.Error(t, err)
72 | case !tt.isV2:
73 | assert.ErrorIs(t, err, ErrNotV2)
74 | default:
75 | assert.NoError(t, err)
76 | }
77 | })
78 | }
79 | }
80 |
81 | func TestCGroupsCPUQuotaV2(t *testing.T) {
82 | tests := []struct {
83 | name string
84 | want float64
85 | wantOK bool
86 | wantErr string
87 | }{
88 | {
89 | name: "set",
90 | want: 2.5,
91 | wantOK: true,
92 | },
93 | {
94 | name: "unset",
95 | want: -1.0,
96 | wantOK: false,
97 | },
98 | {
99 | name: "only-max",
100 | want: 5.0,
101 | wantOK: true,
102 | },
103 | {
104 | name: "invalid-max",
105 | wantErr: `parsing "asdf": invalid syntax`,
106 | },
107 | {
108 | name: "invalid-period",
109 | wantErr: `parsing "njn": invalid syntax`,
110 | },
111 | {
112 | name: "nonexistent",
113 | want: -1.0,
114 | wantOK: false,
115 | },
116 | {
117 | name: "empty",
118 | wantErr: "unexpected EOF",
119 | },
120 | {
121 | name: "too-few-fields",
122 | wantErr: "invalid format",
123 | },
124 | {
125 | name: "too-many-fields",
126 | wantErr: "invalid format",
127 | },
128 | {
129 | name: "zero-period",
130 | wantErr: "zero value for period is not allowed",
131 | },
132 | }
133 |
134 | mountPoint := filepath.Join(testDataCGroupsPath, "v2")
135 | for _, tt := range tests {
136 | t.Run(tt.name, func(t *testing.T) {
137 | quota, defined, err := (&CGroups2{
138 | mountPoint: mountPoint,
139 | groupPath: "/",
140 | cpuMaxFile: tt.name,
141 | }).CPUQuota()
142 |
143 | if len(tt.wantErr) > 0 {
144 | require.Error(t, err, tt.name)
145 | assert.Contains(t, err.Error(), tt.wantErr)
146 | } else {
147 | require.NoError(t, err, tt.name)
148 | assert.Equal(t, tt.want, quota, tt.name)
149 | assert.Equal(t, tt.wantOK, defined, tt.name)
150 | }
151 | })
152 | }
153 | }
154 |
155 | func TestCGroup2GroupPathDiscovery(t *testing.T) {
156 | tests := []struct {
157 | procCgroup string
158 | wantPath string
159 | }{
160 | {
161 | procCgroup: "cgroup-root",
162 | wantPath: "/",
163 | },
164 | {
165 | procCgroup: "cgroup-subdir",
166 | wantPath: "/Example",
167 | },
168 | }
169 |
170 | for _, tt := range tests {
171 | t.Run(tt.procCgroup, func(t *testing.T) {
172 | mountInfoPath := filepath.Join(testDataProcPath, "v2", "mountinfo-v2")
173 | procCgroupPath := filepath.Join(testDataProcPath, "v2", tt.procCgroup)
174 | cgroups, err := newCGroups2From(mountInfoPath, procCgroupPath)
175 | require.NoError(t, err)
176 | assert.Equal(t, tt.wantPath, cgroups.groupPath)
177 | })
178 | }
179 | }
180 |
181 | func TestCGroup2GroupPathDiscovery_Errors(t *testing.T) {
182 | t.Run("no matching subsystem", func(t *testing.T) {
183 | mountInfoPath := filepath.Join(testDataProcPath, "v2", "mountinfo-v2")
184 | procCgroupPath := filepath.Join(testDataProcPath, "v2", "cgroup-no-match")
185 | _, err := newCGroups2From(mountInfoPath, procCgroupPath)
186 | assert.ErrorIs(t, err, ErrNotV2)
187 | })
188 |
189 | t.Run("invalid subsystems", func(t *testing.T) {
190 | mountInfoPath := filepath.Join(testDataProcPath, "v2", "mountinfo-v2")
191 | procCgroupPath := filepath.Join(testDataProcPath, "v2", "cgroup-invalid")
192 | _, err := newCGroups2From(mountInfoPath, procCgroupPath)
193 | assert.Contains(t, err.Error(), "invalid format for CGroupSubsys")
194 | })
195 | }
196 |
197 | func TestCGroupsCPUQuotaV2_OtherErrors(t *testing.T) {
198 | t.Run("no permissions to open", func(t *testing.T) {
199 | if u, err := user.Current(); err == nil && u.Uid == "0" {
200 | t.Skip("running as root, test skipped")
201 | }
202 |
203 | t.Parallel()
204 |
205 | const name = "foo"
206 |
207 | mountPoint := t.TempDir()
208 | require.NoError(t, os.WriteFile(filepath.Join(mountPoint, name), nil /* write only*/, 0222))
209 |
210 | _, _, err := (&CGroups2{mountPoint: mountPoint, groupPath: "/", cpuMaxFile: name}).CPUQuota()
211 | require.Error(t, err)
212 | assert.Contains(t, err.Error(), "permission denied")
213 | })
214 | }
215 |
--------------------------------------------------------------------------------
/internal/cgroups/cgroups_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "path/filepath"
28 | "testing"
29 |
30 | "github.com/stretchr/testify/assert"
31 | )
32 |
33 | func TestNewCGroups(t *testing.T) {
34 | cgroupsProcCGroupPath := filepath.Join(testDataProcPath, "cgroups", "cgroup")
35 | cgroupsProcMountInfoPath := filepath.Join(testDataProcPath, "cgroups", "mountinfo")
36 |
37 | testTable := []struct {
38 | subsys string
39 | path string
40 | }{
41 | {_cgroupSubsysCPU, "/sys/fs/cgroup/cpu,cpuacct"},
42 | {_cgroupSubsysCPUAcct, "/sys/fs/cgroup/cpu,cpuacct"},
43 | {_cgroupSubsysCPUSet, "/sys/fs/cgroup/cpuset"},
44 | {_cgroupSubsysMemory, "/sys/fs/cgroup/memory/large"},
45 | }
46 |
47 | cgroups, err := NewCGroups(cgroupsProcMountInfoPath, cgroupsProcCGroupPath)
48 | assert.Equal(t, len(testTable), len(cgroups))
49 | assert.NoError(t, err)
50 |
51 | for _, tt := range testTable {
52 | cgroup, exists := cgroups[tt.subsys]
53 | assert.Equal(t, true, exists, "%q expected to present in `cgroups`", tt.subsys)
54 | assert.Equal(t, tt.path, cgroup.path, "%q expected for `cgroups[%q].path`, got %q", tt.path, tt.subsys, cgroup.path)
55 | }
56 | }
57 |
58 | func TestNewCGroupsWithErrors(t *testing.T) {
59 | testTable := []struct {
60 | mountInfoPath string
61 | cgroupPath string
62 | }{
63 | {"non-existing-file", "/dev/null"},
64 | {"/dev/null", "non-existing-file"},
65 | {
66 | "/dev/null",
67 | filepath.Join(testDataProcPath, "invalid-cgroup", "cgroup"),
68 | },
69 | {
70 | filepath.Join(testDataProcPath, "invalid-mountinfo", "mountinfo"),
71 | "/dev/null",
72 | },
73 | {
74 | filepath.Join(testDataProcPath, "untranslatable", "mountinfo"),
75 | filepath.Join(testDataProcPath, "untranslatable", "cgroup"),
76 | },
77 | }
78 |
79 | for _, tt := range testTable {
80 | cgroups, err := NewCGroups(tt.mountInfoPath, tt.cgroupPath)
81 | assert.Nil(t, cgroups)
82 | assert.Error(t, err)
83 | }
84 | }
85 |
86 | func TestCGroupsCPUQuota(t *testing.T) {
87 | testTable := []struct {
88 | name string
89 | expectedQuota float64
90 | expectedDefined bool
91 | shouldHaveError bool
92 | }{
93 | {
94 | name: "cpu",
95 | expectedQuota: 6.0,
96 | expectedDefined: true,
97 | shouldHaveError: false,
98 | },
99 | {
100 | name: "undefined",
101 | expectedQuota: -1.0,
102 | expectedDefined: false,
103 | shouldHaveError: false,
104 | },
105 | {
106 | name: "zero-period",
107 | expectedQuota: -1.0,
108 | expectedDefined: false,
109 | shouldHaveError: false,
110 | },
111 | {
112 | name: "undefined-period",
113 | expectedQuota: -1.0,
114 | expectedDefined: false,
115 | shouldHaveError: true,
116 | },
117 | }
118 |
119 | cgroups := make(CGroups)
120 |
121 | quota, defined, err := cgroups.CPUQuota()
122 | assert.Equal(t, -1.0, quota, "nonexistent")
123 | assert.Equal(t, false, defined, "nonexistent")
124 | assert.NoError(t, err, "nonexistent")
125 |
126 | for _, tt := range testTable {
127 | cgroupPath := filepath.Join(testDataCGroupsPath, tt.name)
128 | cgroups[_cgroupSubsysCPU] = NewCGroup(cgroupPath)
129 |
130 | quota, defined, err := cgroups.CPUQuota()
131 | assert.Equal(t, tt.expectedQuota, quota, tt.name)
132 | assert.Equal(t, tt.expectedDefined, defined, tt.name)
133 |
134 | if tt.shouldHaveError {
135 | assert.Error(t, err, tt.name)
136 | } else {
137 | assert.NoError(t, err, tt.name)
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/internal/cgroups/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | // Package cgroups provides utilities to access Linux control group (CGroups)
22 | // parameters (CPU quota, for example) for a given process.
23 | package cgroups
24 |
--------------------------------------------------------------------------------
/internal/cgroups/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import "fmt"
27 |
28 | type cgroupSubsysFormatInvalidError struct {
29 | line string
30 | }
31 |
32 | type mountPointFormatInvalidError struct {
33 | line string
34 | }
35 |
36 | type pathNotExposedFromMountPointError struct {
37 | mountPoint string
38 | root string
39 | path string
40 | }
41 |
42 | func (err cgroupSubsysFormatInvalidError) Error() string {
43 | return fmt.Sprintf("invalid format for CGroupSubsys: %q", err.line)
44 | }
45 |
46 | func (err mountPointFormatInvalidError) Error() string {
47 | return fmt.Sprintf("invalid format for MountPoint: %q", err.line)
48 | }
49 |
50 | func (err pathNotExposedFromMountPointError) Error() string {
51 | return fmt.Sprintf("path %q is not a descendant of mount point root %q and cannot be exposed from %q", err.path, err.root, err.mountPoint)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/cgroups/mountpoint.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "bufio"
28 | "os"
29 | "path/filepath"
30 | "strconv"
31 | "strings"
32 | )
33 |
34 | const (
35 | _mountInfoSep = " "
36 | _mountInfoOptsSep = ","
37 | _mountInfoOptionalFieldsSep = "-"
38 | )
39 |
40 | const (
41 | _miFieldIDMountID = iota
42 | _miFieldIDParentID
43 | _miFieldIDDeviceID
44 | _miFieldIDRoot
45 | _miFieldIDMountPoint
46 | _miFieldIDOptions
47 | _miFieldIDOptionalFields
48 |
49 | _miFieldCountFirstHalf
50 | )
51 |
52 | const (
53 | _miFieldOffsetFSType = iota
54 | _miFieldOffsetMountSource
55 | _miFieldOffsetSuperOptions
56 |
57 | _miFieldCountSecondHalf
58 | )
59 |
60 | const _miFieldCountMin = _miFieldCountFirstHalf + _miFieldCountSecondHalf
61 |
62 | // MountPoint is the data structure for the mount points in
63 | // `/proc/$PID/mountinfo`. See also proc(5) for more information.
64 | type MountPoint struct {
65 | MountID int
66 | ParentID int
67 | DeviceID string
68 | Root string
69 | MountPoint string
70 | Options []string
71 | OptionalFields []string
72 | FSType string
73 | MountSource string
74 | SuperOptions []string
75 | }
76 |
77 | // NewMountPointFromLine parses a line read from `/proc/$PID/mountinfo` and
78 | // returns a new *MountPoint.
79 | func NewMountPointFromLine(line string) (*MountPoint, error) {
80 | fields := strings.Split(line, _mountInfoSep)
81 |
82 | if len(fields) < _miFieldCountMin {
83 | return nil, mountPointFormatInvalidError{line}
84 | }
85 |
86 | mountID, err := strconv.Atoi(fields[_miFieldIDMountID])
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | parentID, err := strconv.Atoi(fields[_miFieldIDParentID])
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | for i, field := range fields[_miFieldIDOptionalFields:] {
97 | if field == _mountInfoOptionalFieldsSep {
98 | // End of optional fields.
99 | fsTypeStart := _miFieldIDOptionalFields + i + 1
100 |
101 | // Now we know where the optional fields end, split the line again with a
102 | // limit to avoid issues with spaces in super options as present on WSL.
103 | fields = strings.SplitN(line, _mountInfoSep, fsTypeStart+_miFieldCountSecondHalf)
104 | if len(fields) != fsTypeStart+_miFieldCountSecondHalf {
105 | return nil, mountPointFormatInvalidError{line}
106 | }
107 |
108 | miFieldIDFSType := _miFieldOffsetFSType + fsTypeStart
109 | miFieldIDMountSource := _miFieldOffsetMountSource + fsTypeStart
110 | miFieldIDSuperOptions := _miFieldOffsetSuperOptions + fsTypeStart
111 |
112 | return &MountPoint{
113 | MountID: mountID,
114 | ParentID: parentID,
115 | DeviceID: fields[_miFieldIDDeviceID],
116 | Root: fields[_miFieldIDRoot],
117 | MountPoint: fields[_miFieldIDMountPoint],
118 | Options: strings.Split(fields[_miFieldIDOptions], _mountInfoOptsSep),
119 | OptionalFields: fields[_miFieldIDOptionalFields:(fsTypeStart - 1)],
120 | FSType: fields[miFieldIDFSType],
121 | MountSource: fields[miFieldIDMountSource],
122 | SuperOptions: strings.Split(fields[miFieldIDSuperOptions], _mountInfoOptsSep),
123 | }, nil
124 | }
125 | }
126 |
127 | return nil, mountPointFormatInvalidError{line}
128 | }
129 |
130 | // Translate converts an absolute path inside the *MountPoint's file system to
131 | // the host file system path in the mount namespace the *MountPoint belongs to.
132 | func (mp *MountPoint) Translate(absPath string) (string, error) {
133 | relPath, err := filepath.Rel(mp.Root, absPath)
134 |
135 | if err != nil {
136 | return "", err
137 | }
138 | if relPath == ".." || strings.HasPrefix(relPath, "../") {
139 | return "", pathNotExposedFromMountPointError{
140 | mountPoint: mp.MountPoint,
141 | root: mp.Root,
142 | path: absPath,
143 | }
144 | }
145 |
146 | return filepath.Join(mp.MountPoint, relPath), nil
147 | }
148 |
149 | // parseMountInfo parses procPathMountInfo (usually at `/proc/$PID/mountinfo`)
150 | // and yields parsed *MountPoint into newMountPoint.
151 | func parseMountInfo(procPathMountInfo string, newMountPoint func(*MountPoint) error) error {
152 | mountInfoFile, err := os.Open(procPathMountInfo)
153 | if err != nil {
154 | return err
155 | }
156 | defer mountInfoFile.Close()
157 |
158 | scanner := bufio.NewScanner(mountInfoFile)
159 |
160 | for scanner.Scan() {
161 | mountPoint, err := NewMountPointFromLine(scanner.Text())
162 | if err != nil {
163 | return err
164 | }
165 | if err := newMountPoint(mountPoint); err != nil {
166 | return err
167 | }
168 | }
169 |
170 | return scanner.Err()
171 | }
172 |
--------------------------------------------------------------------------------
/internal/cgroups/mountpoint_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "testing"
28 |
29 | "github.com/stretchr/testify/assert"
30 | )
31 |
32 | func TestNewMountPointFromLine(t *testing.T) {
33 | testTable := []struct {
34 | name string
35 | line string
36 | expected *MountPoint
37 | }{
38 | {
39 | name: "root",
40 | line: "1 0 252:0 / / rw,noatime - ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
41 | expected: &MountPoint{
42 | MountID: 1,
43 | ParentID: 0,
44 | DeviceID: "252:0",
45 | Root: "/",
46 | MountPoint: "/",
47 | Options: []string{"rw", "noatime"},
48 | OptionalFields: []string{},
49 | FSType: "ext4",
50 | MountSource: "/dev/dm-0",
51 | SuperOptions: []string{"rw", "errors=remount-ro", "data=ordered"},
52 | },
53 | },
54 | {
55 | name: "cgroup",
56 | line: "31 23 0:24 /docker /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime shared:1 - cgroup cgroup rw,cpu",
57 | expected: &MountPoint{
58 | MountID: 31,
59 | ParentID: 23,
60 | DeviceID: "0:24",
61 | Root: "/docker",
62 | MountPoint: "/sys/fs/cgroup/cpu",
63 | Options: []string{"rw", "nosuid", "nodev", "noexec", "relatime"},
64 | OptionalFields: []string{"shared:1"},
65 | FSType: "cgroup",
66 | MountSource: "cgroup",
67 | SuperOptions: []string{"rw", "cpu"},
68 | },
69 | },
70 | {
71 | name: "wsl",
72 | line: `560 77 0:138 / /Docker/host rw,noatime - 9p drvfs rw,dirsync,aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/,mmap,access=client,msize=262144,trans=virtio`,
73 | expected: &MountPoint{
74 | MountID: 560,
75 | ParentID: 77,
76 | DeviceID: "0:138",
77 | Root: "/",
78 | MountPoint: "/Docker/host",
79 | Options: []string{"rw", "noatime"},
80 | OptionalFields: []string{},
81 | FSType: "9p",
82 | MountSource: "drvfs",
83 | SuperOptions: []string{
84 | "rw",
85 | "dirsync",
86 | `aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/`,
87 | "mmap",
88 | "access=client",
89 | "msize=262144",
90 | "trans=virtio",
91 | },
92 | },
93 | },
94 | }
95 |
96 | for _, tt := range testTable {
97 | mountPoint, err := NewMountPointFromLine(tt.line)
98 | if assert.NoError(t, err, tt.name) {
99 | assert.Equal(t, tt.expected, mountPoint, tt.name)
100 | }
101 | }
102 | }
103 |
104 | func TestNewMountPointFromLineErr(t *testing.T) {
105 | linesWithInvalidIDs := []string{
106 | "invalidMountID 0 252:0 / / rw,noatime - ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
107 | "1 invalidParentID 252:0 / / rw,noatime - ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
108 | "invalidMountID invalidParentID 252:0 / / rw,noatime - ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
109 | }
110 |
111 | for i, line := range linesWithInvalidIDs {
112 | mountPoint, err := NewMountPointFromLine(line)
113 | assert.Nil(t, mountPoint, "[%d] %q", i, line)
114 | assert.Error(t, err, line)
115 | }
116 |
117 | linesWithInvalidFields := []string{
118 | "1 0 252:0 / / rw,noatime ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
119 | "1 0 252:0 / / rw,noatime shared:1 - ext4 /dev/dm-0",
120 | "1 0 252:0 / / rw,noatime shared:1 ext4 - /dev/dm-0 rw,errors=remount-ro,data=ordered",
121 | "1 0 252:0 / / rw,noatime shared:1 ext4 /dev/dm-0 rw,errors=remount-ro,data=ordered",
122 | "random line",
123 | }
124 |
125 | for i, line := range linesWithInvalidFields {
126 | mountPoint, err := NewMountPointFromLine(line)
127 | errExpected := mountPointFormatInvalidError{line}
128 |
129 | assert.Nil(t, mountPoint, "[%d] %q", i, line)
130 | assert.Equal(t, err, errExpected, "[%d] %q", i, line)
131 | }
132 | }
133 |
134 | func TestMountPointTranslate(t *testing.T) {
135 | line := "31 23 0:24 /docker/0123456789abcdef /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime shared:1 - cgroup cgroup rw,cpu"
136 | cgroupMountPoint, err := NewMountPointFromLine(line)
137 |
138 | assert.NotNil(t, cgroupMountPoint)
139 | assert.NoError(t, err)
140 |
141 | testTable := []struct {
142 | name string
143 | pathToTranslate string
144 | pathTranslated string
145 | }{
146 | {
147 | name: "root",
148 | pathToTranslate: "/docker/0123456789abcdef",
149 | pathTranslated: "/sys/fs/cgroup/cpu",
150 | },
151 | {
152 | name: "root-with-extra-slash",
153 | pathToTranslate: "/docker/0123456789abcdef/",
154 | pathTranslated: "/sys/fs/cgroup/cpu",
155 | },
156 | {
157 | name: "descendant-from-root",
158 | pathToTranslate: "/docker/0123456789abcdef/large/cpu.cfs_quota_us",
159 | pathTranslated: "/sys/fs/cgroup/cpu/large/cpu.cfs_quota_us",
160 | },
161 | }
162 |
163 | for _, tt := range testTable {
164 | path, err := cgroupMountPoint.Translate(tt.pathToTranslate)
165 | assert.Equal(t, tt.pathTranslated, path, tt.name)
166 | assert.NoError(t, err, tt.name)
167 | }
168 | }
169 |
170 | func TestMountPointTranslateError(t *testing.T) {
171 | line := "31 23 0:24 /docker/0123456789abcdef /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime shared:1 - cgroup cgroup rw,cpu"
172 | cgroupMountPoint, err := NewMountPointFromLine(line)
173 |
174 | assert.NotNil(t, cgroupMountPoint)
175 | assert.NoError(t, err)
176 |
177 | inaccessiblePaths := []string{
178 | "/",
179 | "/docker",
180 | "/docker/0123456789abcdef-let-me-hack-this-path",
181 | "/docker/0123456789abcde/abc/../../def",
182 | "/system.slice/docker.service",
183 | }
184 |
185 | for i, path := range inaccessiblePaths {
186 | translated, err := cgroupMountPoint.Translate(path)
187 | errExpected := pathNotExposedFromMountPointError{
188 | mountPoint: cgroupMountPoint.MountPoint,
189 | root: cgroupMountPoint.Root,
190 | path: path,
191 | }
192 |
193 | assert.Equal(t, "", translated, "inaccessiblePaths[%d] == %q", i, path)
194 | assert.Equal(t, errExpected, err, "inaccessiblePaths[%d] == %q", i, path)
195 | }
196 |
197 | relPaths := []string{
198 | "docker",
199 | "docker/0123456789abcde/large",
200 | "system.slice/docker.service",
201 | }
202 |
203 | for i, path := range relPaths {
204 | translated, err := cgroupMountPoint.Translate(path)
205 |
206 | assert.Equal(t, "", translated, "relPaths[%d] == %q", i, path)
207 | assert.Error(t, err, path)
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/internal/cgroups/subsys.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "bufio"
28 | "os"
29 | "strconv"
30 | "strings"
31 | )
32 |
33 | const (
34 | _cgroupSep = ":"
35 | _cgroupSubsysSep = ","
36 | )
37 |
38 | const (
39 | _csFieldIDID = iota
40 | _csFieldIDSubsystems
41 | _csFieldIDName
42 | _csFieldCount
43 | )
44 |
45 | // CGroupSubsys represents the data structure for entities in
46 | // `/proc/$PID/cgroup`. See also proc(5) for more information.
47 | type CGroupSubsys struct {
48 | ID int
49 | Subsystems []string
50 | Name string
51 | }
52 |
53 | // NewCGroupSubsysFromLine returns a new *CGroupSubsys by parsing a string in
54 | // the format of `/proc/$PID/cgroup`
55 | func NewCGroupSubsysFromLine(line string) (*CGroupSubsys, error) {
56 | fields := strings.SplitN(line, _cgroupSep, _csFieldCount)
57 |
58 | if len(fields) != _csFieldCount {
59 | return nil, cgroupSubsysFormatInvalidError{line}
60 | }
61 |
62 | id, err := strconv.Atoi(fields[_csFieldIDID])
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | cgroup := &CGroupSubsys{
68 | ID: id,
69 | Subsystems: strings.Split(fields[_csFieldIDSubsystems], _cgroupSubsysSep),
70 | Name: fields[_csFieldIDName],
71 | }
72 |
73 | return cgroup, nil
74 | }
75 |
76 | // parseCGroupSubsystems parses procPathCGroup (usually at `/proc/$PID/cgroup`)
77 | // and returns a new map[string]*CGroupSubsys.
78 | func parseCGroupSubsystems(procPathCGroup string) (map[string]*CGroupSubsys, error) {
79 | cgroupFile, err := os.Open(procPathCGroup)
80 | if err != nil {
81 | return nil, err
82 | }
83 | defer cgroupFile.Close()
84 |
85 | scanner := bufio.NewScanner(cgroupFile)
86 | subsystems := make(map[string]*CGroupSubsys)
87 |
88 | for scanner.Scan() {
89 | cgroup, err := NewCGroupSubsysFromLine(scanner.Text())
90 | if err != nil {
91 | return nil, err
92 | }
93 | for _, subsys := range cgroup.Subsystems {
94 | subsystems[subsys] = cgroup
95 | }
96 | }
97 |
98 | if err := scanner.Err(); err != nil {
99 | return nil, err
100 | }
101 |
102 | return subsystems, nil
103 | }
104 |
--------------------------------------------------------------------------------
/internal/cgroups/subsys_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "strconv"
28 | "testing"
29 |
30 | "github.com/stretchr/testify/assert"
31 | )
32 |
33 | func TestNewCGroupSubsysFromLine(t *testing.T) {
34 | testTable := []struct {
35 | name string
36 | line string
37 | expectedSubsys *CGroupSubsys
38 | }{
39 | {
40 | name: "single-subsys",
41 | line: "1:cpu:/",
42 | expectedSubsys: &CGroupSubsys{
43 | ID: 1,
44 | Subsystems: []string{"cpu"},
45 | Name: "/",
46 | },
47 | },
48 | {
49 | name: "multi-subsys",
50 | line: "8:cpu,cpuacct,cpuset:/docker/1234567890abcdef",
51 | expectedSubsys: &CGroupSubsys{
52 | ID: 8,
53 | Subsystems: []string{"cpu", "cpuacct", "cpuset"},
54 | Name: "/docker/1234567890abcdef",
55 | },
56 | },
57 | {
58 | name: "multi-subsys",
59 | line: "12:cpu,cpuacct:/system.slice/containerd.service/kubepods-besteffort-podb41662f7_b03a_4c65_8ef9_6e4e55c3cf27.slice:cri-containerd:1753b7cbbf62734d812936961224d5bc0cf8f45214e0d5cdd1a781a053e7c48f",
60 | expectedSubsys: &CGroupSubsys{
61 | ID: 12,
62 | Subsystems: []string{"cpu", "cpuacct"},
63 | Name: "/system.slice/containerd.service/kubepods-besteffort-podb41662f7_b03a_4c65_8ef9_6e4e55c3cf27.slice:cri-containerd:1753b7cbbf62734d812936961224d5bc0cf8f45214e0d5cdd1a781a053e7c48f",
64 | },
65 | },
66 | }
67 |
68 | for _, tt := range testTable {
69 | subsys, err := NewCGroupSubsysFromLine(tt.line)
70 | assert.Equal(t, tt.expectedSubsys, subsys, tt.name)
71 | assert.NoError(t, err, tt.name)
72 | }
73 | }
74 |
75 | func TestNewCGroupSubsysFromLineErr(t *testing.T) {
76 | lines := []string{
77 | "1:cpu",
78 | "not-a-number:cpu:/",
79 | }
80 | _, parseError := strconv.Atoi("not-a-number")
81 |
82 | testTable := []struct {
83 | name string
84 | line string
85 | expectedError error
86 | }{
87 | {
88 | name: "fewer-fields",
89 | line: lines[0],
90 | expectedError: cgroupSubsysFormatInvalidError{lines[0]},
91 | },
92 | {
93 | name: "illegal-id",
94 | line: lines[1],
95 | expectedError: parseError,
96 | },
97 | }
98 |
99 | for _, tt := range testTable {
100 | subsys, err := NewCGroupSubsysFromLine(tt.line)
101 | assert.Nil(t, subsys, tt.name)
102 | assert.Equal(t, tt.expectedError, err, tt.name)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/cpu/cpu.cfs_period_us:
--------------------------------------------------------------------------------
1 | 100000
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/cpu/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
1 | 600000
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/empty/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uber-go/automaxprocs/1ea14c35ce47a73089b824e504d1c92eeb61a5a6/internal/cgroups/testdata/cgroups/empty/cpu.cfs_quota_us
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/invalid/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
1 | non-an-integer
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/undefined-period/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
1 | 800000
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/undefined/cpu.cfs_period_us:
--------------------------------------------------------------------------------
1 | 100000
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/undefined/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
1 | -1
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uber-go/automaxprocs/1ea14c35ce47a73089b824e504d1c92eeb61a5a6/internal/cgroups/testdata/cgroups/v2/empty
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/invalid-max:
--------------------------------------------------------------------------------
1 | asdf 100000
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/invalid-period:
--------------------------------------------------------------------------------
1 | 500000 njn
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/only-max:
--------------------------------------------------------------------------------
1 | 500000
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/set:
--------------------------------------------------------------------------------
1 | 250000 100000
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/too-few-fields:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/too-many-fields:
--------------------------------------------------------------------------------
1 | 250000 100000 100
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/unset:
--------------------------------------------------------------------------------
1 | max 100000
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/v2/zero-period:
--------------------------------------------------------------------------------
1 | 250000 0
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/zero-period/cpu.cfs_period_us:
--------------------------------------------------------------------------------
1 | 0
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/cgroups/zero-period/cpu.cfs_quota_us:
--------------------------------------------------------------------------------
1 | 600000
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/cgroups/cgroup:
--------------------------------------------------------------------------------
1 | 3:memory:/docker/large
2 | 2:cpu,cpuacct:/docker
3 | 1:cpuset:/
4 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/cgroups/mountinfo:
--------------------------------------------------------------------------------
1 | 1 0 8:1 / / rw,noatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro,data=reordered
2 | 2 1 0:1 / /dev rw,relatime shared:2 - devtmpfs udev rw,size=10240k,nr_inodes=16487629,mode=755
3 | 3 1 0:2 / /proc rw,nosuid,nodev,noexec,relatime shared:3 - proc proc rw
4 | 4 1 0:3 / /sys rw,nosuid,nodev,noexec,relatime shared:4 - sysfs sysfs rw
5 | 5 4 0:4 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:5 - tmpfs tmpfs ro,mode=755
6 | 6 5 0:5 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:6 - cgroup cgroup rw,cpuset
7 | 7 5 0:6 /docker /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct
8 | 8 5 0:7 /docker /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:8 - cgroup cgroup rw,memory
9 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/invalid-cgroup/cgroup:
--------------------------------------------------------------------------------
1 | 1:cpu:/cpu
2 | invalid-line:
3 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/invalid-mountinfo/mountinfo:
--------------------------------------------------------------------------------
1 | 1 0 8:1 / / rw,noatime shared:1 - ext4 /dev/sda1
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/untranslatable/cgroup:
--------------------------------------------------------------------------------
1 | 1:cpu:/docker
2 | 2:cpuacct:/docker
3 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/untranslatable/mountinfo:
--------------------------------------------------------------------------------
1 | 31 23 0:24 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime shared:1 - cgroup cgroup rw,cpu
2 | 32 23 0:25 /docker/0123456789abcdef /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime shared:2 - cgroup cgroup rw,cpuacct
3 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/cgroup-invalid:
--------------------------------------------------------------------------------
1 | 0\using\colons\/
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/cgroup-no-match:
--------------------------------------------------------------------------------
1 | 1::/
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/cgroup-root:
--------------------------------------------------------------------------------
1 | 0::/
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/cgroup-subdir:
--------------------------------------------------------------------------------
1 | 0::/Example
2 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/mountinfo:
--------------------------------------------------------------------------------
1 | 1 0 8:1 / / rw,noatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro,data=reordered
2 | 2 1 0:1 / /dev rw,relatime shared:2 - devtmpfs udev rw,size=10240k,nr_inodes=16487629,mode=755
3 | 3 1 0:2 / /proc rw,nosuid,nodev,noexec,relatime shared:3 - proc proc rw
4 | 4 1 0:3 / /sys rw,nosuid,nodev,noexec,relatime shared:4 - sysfs sysfs rw
5 | 5 4 0:4 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:5 - tmpfs tmpfs ro,mode=755
6 | 6 5 0:5 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:6 - cgroup cgroup rw,cpuset
7 | 7 5 0:6 /docker /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct
8 | 8 5 0:7 /docker /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:8 - cgroup cgroup rw,memory
9 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/mountinfo-v1-v2:
--------------------------------------------------------------------------------
1 | 33 24 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755,inode64
2 | 34 33 0:29 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup2 rw,nsdelegate
3 | 35 33 0:30 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,xattr,name=systemd
4 | 39 33 0:34 / /sys/fs/cgroup/misc rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,misc
5 | 40 33 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,net_cls,net_prio
6 | 41 33 0:36 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,rdma
7 | 42 33 0:37 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,memory
8 | 43 33 0:38 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,blkio
9 | 44 33 0:39 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpu,cpuacct
10 | 45 33 0:40 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,pids
11 | 46 33 0:41 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,hugetlb
12 | 47 33 0:42 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,freezer
13 | 48 33 0:43 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,perf_event
14 | 49 33 0:44 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices
15 | 50 33 0:45 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,cpuset
16 |
--------------------------------------------------------------------------------
/internal/cgroups/testdata/proc/v2/mountinfo-v2:
--------------------------------------------------------------------------------
1 | 34 33 0:29 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup rw,nsdelegate
2 | 34 33 0:29 / /sys/fs/foo rw,nosuid,nodev,noexec,relatime shared:10 - foo cgroup rw,nsdelegate
3 |
--------------------------------------------------------------------------------
/internal/cgroups/util_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package cgroups
25 |
26 | import (
27 | "os"
28 | "path/filepath"
29 | )
30 |
31 | var (
32 | pwd = mustGetWd()
33 | testDataPath = filepath.Join(pwd, "testdata")
34 | testDataCGroupsPath = filepath.Join(testDataPath, "cgroups")
35 | testDataProcPath = filepath.Join(testDataPath, "proc")
36 | )
37 |
38 | func mustGetWd() string {
39 | pwd, err := os.Getwd()
40 | if err != nil {
41 | panic(err)
42 | }
43 | return pwd
44 | }
45 |
--------------------------------------------------------------------------------
/internal/runtime/cpu_quota_linux.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package runtime
25 |
26 | import (
27 | "errors"
28 |
29 | cg "go.uber.org/automaxprocs/internal/cgroups"
30 | )
31 |
32 | // CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
33 | // to a valid GOMAXPROCS value. The quota is converted from float to int using round.
34 | // If round == nil, DefaultRoundFunc is used.
35 | func CPUQuotaToGOMAXPROCS(minValue int, round func(v float64) int) (int, CPUQuotaStatus, error) {
36 | if round == nil {
37 | round = DefaultRoundFunc
38 | }
39 | cgroups, err := _newQueryer()
40 | if err != nil {
41 | return -1, CPUQuotaUndefined, err
42 | }
43 |
44 | quota, defined, err := cgroups.CPUQuota()
45 | if !defined || err != nil {
46 | return -1, CPUQuotaUndefined, err
47 | }
48 |
49 | maxProcs := round(quota)
50 | if minValue > 0 && maxProcs < minValue {
51 | return minValue, CPUQuotaMinUsed, nil
52 | }
53 | return maxProcs, CPUQuotaUsed, nil
54 | }
55 |
56 | type queryer interface {
57 | CPUQuota() (float64, bool, error)
58 | }
59 |
60 | var (
61 | _newCgroups2 = cg.NewCGroups2ForCurrentProcess
62 | _newCgroups = cg.NewCGroupsForCurrentProcess
63 | _newQueryer = newQueryer
64 | )
65 |
66 | func newQueryer() (queryer, error) {
67 | cgroups, err := _newCgroups2()
68 | if err == nil {
69 | return cgroups, nil
70 | }
71 | if errors.Is(err, cg.ErrNotV2) {
72 | return _newCgroups()
73 | }
74 | return nil, err
75 | }
76 |
--------------------------------------------------------------------------------
/internal/runtime/cpu_quota_linux_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build linux
22 | // +build linux
23 |
24 | package runtime
25 |
26 | import (
27 | "errors"
28 | "fmt"
29 | "math"
30 | "testing"
31 |
32 | "github.com/prashantv/gostub"
33 | "github.com/stretchr/testify/assert"
34 | "github.com/stretchr/testify/require"
35 | "go.uber.org/automaxprocs/internal/cgroups"
36 | )
37 |
38 | func TestNewQueryer(t *testing.T) {
39 | t.Run("use v2", func(t *testing.T) {
40 | stubs := newStubs(t)
41 |
42 | c2 := new(cgroups.CGroups2)
43 | stubs.StubFunc(&_newCgroups2, c2, nil)
44 |
45 | got, err := newQueryer()
46 | require.NoError(t, err)
47 | assert.Same(t, c2, got)
48 | })
49 |
50 | t.Run("v2 error", func(t *testing.T) {
51 | stubs := newStubs(t)
52 |
53 | giveErr := errors.New("great sadness")
54 | stubs.StubFunc(&_newCgroups2, nil, giveErr)
55 |
56 | _, err := newQueryer()
57 | assert.ErrorIs(t, err, giveErr)
58 | })
59 |
60 | t.Run("use v1", func(t *testing.T) {
61 | stubs := newStubs(t)
62 |
63 | stubs.StubFunc(&_newCgroups2, nil,
64 | fmt.Errorf("not v2: %w", cgroups.ErrNotV2))
65 |
66 | c1 := make(cgroups.CGroups)
67 | stubs.StubFunc(&_newCgroups, c1, nil)
68 |
69 | got, err := newQueryer()
70 | require.NoError(t, err)
71 | assert.IsType(t, c1, got, "must be a v1 cgroup")
72 | })
73 |
74 | t.Run("v1 error", func(t *testing.T) {
75 | stubs := newStubs(t)
76 |
77 | stubs.StubFunc(&_newCgroups2, nil, cgroups.ErrNotV2)
78 |
79 | giveErr := errors.New("great sadness")
80 | stubs.StubFunc(&_newCgroups, nil, giveErr)
81 |
82 | _, err := newQueryer()
83 | assert.ErrorIs(t, err, giveErr)
84 | })
85 |
86 | t.Run("round quota with a nil round function", func(t *testing.T) {
87 | stubs := newStubs(t)
88 |
89 | q := testQueryer{v: 2.7}
90 | stubs.StubFunc(&_newQueryer, q, nil)
91 |
92 | // If round function is nil, CPUQuotaToGOMAXPROCS uses DefaultRoundFunc, which rounds down the value
93 | got, _, err := CPUQuotaToGOMAXPROCS(0, nil)
94 | require.NoError(t, err)
95 | assert.Equal(t, 2, got)
96 | })
97 |
98 | t.Run("round quota with ceil", func(t *testing.T) {
99 | stubs := newStubs(t)
100 |
101 | q := testQueryer{v: 2.7}
102 | stubs.StubFunc(&_newQueryer, q, nil)
103 |
104 | got, _, err := CPUQuotaToGOMAXPROCS(0, func(v float64) int { return int(math.Ceil(v)) })
105 | require.NoError(t, err)
106 | assert.Equal(t, 3, got)
107 | })
108 |
109 | t.Run("round quota with floor", func(t *testing.T) {
110 | stubs := newStubs(t)
111 |
112 | q := testQueryer{v: 2.7}
113 | stubs.StubFunc(&_newQueryer, q, nil)
114 |
115 | got, _, err := CPUQuotaToGOMAXPROCS(0, func(v float64) int { return int(math.Floor(v)) })
116 | require.NoError(t, err)
117 | assert.Equal(t, 2, got)
118 | })
119 | }
120 |
121 | type testQueryer struct {
122 | v float64
123 | }
124 |
125 | func (tq testQueryer) CPUQuota() (float64, bool, error) {
126 | return tq.v, true, nil
127 | }
128 |
129 | func newStubs(t *testing.T) *gostub.Stubs {
130 | stubs := gostub.New()
131 | t.Cleanup(stubs.Reset)
132 | return stubs
133 | }
134 |
--------------------------------------------------------------------------------
/internal/runtime/cpu_quota_unsupported.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build !linux
22 | // +build !linux
23 |
24 | package runtime
25 |
26 | // CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
27 | // to a valid GOMAXPROCS value. This is Linux-specific and not supported in the
28 | // current OS.
29 | func CPUQuotaToGOMAXPROCS(_ int, _ func(v float64) int) (int, CPUQuotaStatus, error) {
30 | return -1, CPUQuotaUndefined, nil
31 | }
32 |
--------------------------------------------------------------------------------
/internal/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package runtime
22 |
23 | import "math"
24 |
25 | // CPUQuotaStatus presents the status of how CPU quota is used
26 | type CPUQuotaStatus int
27 |
28 | const (
29 | // CPUQuotaUndefined is returned when CPU quota is undefined
30 | CPUQuotaUndefined CPUQuotaStatus = iota
31 | // CPUQuotaUsed is returned when a valid CPU quota can be used
32 | CPUQuotaUsed
33 | // CPUQuotaMinUsed is returned when CPU quota is smaller than the min value
34 | CPUQuotaMinUsed
35 | )
36 |
37 | // DefaultRoundFunc is the default function to convert CPU quota from float to int. It rounds the value down (floor).
38 | func DefaultRoundFunc(v float64) int {
39 | return int(math.Floor(v))
40 | }
41 |
--------------------------------------------------------------------------------
/maxprocs/example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package maxprocs_test
22 |
23 | import (
24 | "log"
25 |
26 | "go.uber.org/automaxprocs/maxprocs"
27 | )
28 |
29 | func Example() {
30 | undo, err := maxprocs.Set()
31 | defer undo()
32 | if err != nil {
33 | log.Fatalf("failed to set GOMAXPROCS: %v", err)
34 | }
35 | // Insert your application logic here.
36 | }
37 |
38 | func ExampleLogger() {
39 | // By default, Set doesn't output any logs. You can enable logging by
40 | // supplying a printf implementation.
41 | undo, err := maxprocs.Set(maxprocs.Logger(log.Printf))
42 | defer undo()
43 | if err != nil {
44 | log.Fatalf("failed to set GOMAXPROCS: %v", err)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/maxprocs/maxprocs.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | // Package maxprocs lets Go programs easily configure runtime.GOMAXPROCS to
22 | // match the configured Linux CPU quota. Unlike the top-level automaxprocs
23 | // package, it lets the caller configure logging and handle errors.
24 | package maxprocs // import "go.uber.org/automaxprocs/maxprocs"
25 |
26 | import (
27 | "os"
28 | "runtime"
29 |
30 | iruntime "go.uber.org/automaxprocs/internal/runtime"
31 | )
32 |
33 | const _maxProcsKey = "GOMAXPROCS"
34 |
35 | func currentMaxProcs() int {
36 | return runtime.GOMAXPROCS(0)
37 | }
38 |
39 | type config struct {
40 | printf func(string, ...interface{})
41 | procs func(int, func(v float64) int) (int, iruntime.CPUQuotaStatus, error)
42 | minGOMAXPROCS int
43 | roundQuotaFunc func(v float64) int
44 | }
45 |
46 | func (c *config) log(fmt string, args ...interface{}) {
47 | if c.printf != nil {
48 | c.printf(fmt, args...)
49 | }
50 | }
51 |
52 | // An Option alters the behavior of Set.
53 | type Option interface {
54 | apply(*config)
55 | }
56 |
57 | // Logger uses the supplied printf implementation for log output. By default,
58 | // Set doesn't log anything.
59 | func Logger(printf func(string, ...interface{})) Option {
60 | return optionFunc(func(cfg *config) {
61 | cfg.printf = printf
62 | })
63 | }
64 |
65 | // Min sets the minimum GOMAXPROCS value that will be used.
66 | // Any value below 1 is ignored.
67 | func Min(n int) Option {
68 | return optionFunc(func(cfg *config) {
69 | if n >= 1 {
70 | cfg.minGOMAXPROCS = n
71 | }
72 | })
73 | }
74 |
75 | // RoundQuotaFunc sets the function that will be used to covert the CPU quota from float to int.
76 | func RoundQuotaFunc(rf func(v float64) int) Option {
77 | return optionFunc(func(cfg *config) {
78 | cfg.roundQuotaFunc = rf
79 | })
80 | }
81 |
82 | type optionFunc func(*config)
83 |
84 | func (of optionFunc) apply(cfg *config) { of(cfg) }
85 |
86 | // Set GOMAXPROCS to match the Linux container CPU quota (if any), returning
87 | // any error encountered and an undo function.
88 | //
89 | // Set is a no-op on non-Linux systems and in Linux environments without a
90 | // configured CPU quota.
91 | func Set(opts ...Option) (func(), error) {
92 | cfg := &config{
93 | procs: iruntime.CPUQuotaToGOMAXPROCS,
94 | roundQuotaFunc: iruntime.DefaultRoundFunc,
95 | minGOMAXPROCS: 1,
96 | }
97 | for _, o := range opts {
98 | o.apply(cfg)
99 | }
100 |
101 | undoNoop := func() {
102 | cfg.log("maxprocs: No GOMAXPROCS change to reset")
103 | }
104 |
105 | // Honor the GOMAXPROCS environment variable if present. Otherwise, amend
106 | // `runtime.GOMAXPROCS()` with the current process' CPU quota if the OS is
107 | // Linux, and guarantee a minimum value of 1. The minimum guaranteed value
108 | // can be overridden using `maxprocs.Min()`.
109 | if max, exists := os.LookupEnv(_maxProcsKey); exists {
110 | cfg.log("maxprocs: Honoring GOMAXPROCS=%q as set in environment", max)
111 | return undoNoop, nil
112 | }
113 |
114 | maxProcs, status, err := cfg.procs(cfg.minGOMAXPROCS, cfg.roundQuotaFunc)
115 | if err != nil {
116 | return undoNoop, err
117 | }
118 |
119 | if status == iruntime.CPUQuotaUndefined {
120 | cfg.log("maxprocs: Leaving GOMAXPROCS=%v: CPU quota undefined", currentMaxProcs())
121 | return undoNoop, nil
122 | }
123 |
124 | prev := currentMaxProcs()
125 | undo := func() {
126 | cfg.log("maxprocs: Resetting GOMAXPROCS to %v", prev)
127 | runtime.GOMAXPROCS(prev)
128 | }
129 |
130 | switch status {
131 | case iruntime.CPUQuotaMinUsed:
132 | cfg.log("maxprocs: Updating GOMAXPROCS=%v: using minimum allowed GOMAXPROCS", maxProcs)
133 | case iruntime.CPUQuotaUsed:
134 | cfg.log("maxprocs: Updating GOMAXPROCS=%v: determined from CPU quota", maxProcs)
135 | }
136 |
137 | runtime.GOMAXPROCS(maxProcs)
138 | return undo, nil
139 | }
140 |
--------------------------------------------------------------------------------
/maxprocs/maxprocs_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package maxprocs
22 |
23 | import (
24 | "bytes"
25 | "errors"
26 | "fmt"
27 | "log"
28 | "math"
29 | "os"
30 | "strconv"
31 | "testing"
32 |
33 | iruntime "go.uber.org/automaxprocs/internal/runtime"
34 |
35 | "github.com/stretchr/testify/assert"
36 | "github.com/stretchr/testify/require"
37 | )
38 |
39 | func withMax(t testing.TB, n int, f func()) {
40 | prevStr, ok := os.LookupEnv(_maxProcsKey)
41 | want := strconv.FormatInt(int64(n), 10)
42 | require.NoError(t, os.Setenv(_maxProcsKey, want), "couldn't set GOMAXPROCS")
43 | f()
44 | if ok {
45 | require.NoError(t, os.Setenv(_maxProcsKey, prevStr), "couldn't restore original GOMAXPROCS value")
46 | return
47 | }
48 | require.NoError(t, os.Unsetenv(_maxProcsKey), "couldn't clear GOMAXPROCS")
49 | }
50 |
51 | func testLogger() (*bytes.Buffer, Option) {
52 | buf := bytes.NewBuffer(nil)
53 | printf := func(template string, args ...interface{}) {
54 | fmt.Fprintf(buf, template, args...)
55 | }
56 | return buf, Logger(printf)
57 | }
58 |
59 | func stubProcs(f func(int, func(v float64) int) (int, iruntime.CPUQuotaStatus, error)) Option {
60 | return optionFunc(func(cfg *config) {
61 | cfg.procs = f
62 | })
63 | }
64 |
65 | func TestLogger(t *testing.T) {
66 | t.Run("default", func(t *testing.T) {
67 | // Calling Set without options should be safe.
68 | undo, err := Set()
69 | defer undo()
70 | require.NoError(t, err, "Set failed")
71 | })
72 |
73 | t.Run("override", func(t *testing.T) {
74 | buf, opt := testLogger()
75 | undo, err := Set(opt)
76 | defer undo()
77 | require.NoError(t, err, "Set failed")
78 | assert.True(t, buf.Len() > 0, "didn't capture log output")
79 | })
80 | }
81 |
82 | func TestSet(t *testing.T) {
83 | // Ensure that we've undone any modifications correctly.
84 | prev := currentMaxProcs()
85 | defer func() {
86 | require.Equal(t, prev, currentMaxProcs(), "didn't undo GOMAXPROCS changes")
87 | }()
88 |
89 | t.Run("EnvVarPresent", func(t *testing.T) {
90 | withMax(t, 42, func() {
91 | prev := currentMaxProcs()
92 | undo, err := Set()
93 | defer undo()
94 | require.NoError(t, err, "Set failed")
95 | assert.Equal(t, prev, currentMaxProcs(), "shouldn't alter GOMAXPROCS")
96 | })
97 | })
98 |
99 | t.Run("ErrorReadingQuota", func(t *testing.T) {
100 | opt := stubProcs(func(int, func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
101 | return 0, iruntime.CPUQuotaUndefined, errors.New("failed")
102 | })
103 | prev := currentMaxProcs()
104 | undo, err := Set(opt)
105 | defer undo()
106 | require.Error(t, err, "Set should have failed")
107 | assert.Equal(t, "failed", err.Error(), "should pass errors up the stack")
108 | assert.Equal(t, prev, currentMaxProcs(), "shouldn't alter GOMAXPROCS")
109 | })
110 |
111 | t.Run("QuotaUndefined", func(t *testing.T) {
112 | buf, logOpt := testLogger()
113 | quotaOpt := stubProcs(func(int, func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
114 | return 0, iruntime.CPUQuotaUndefined, nil
115 | })
116 | prev := currentMaxProcs()
117 | undo, err := Set(logOpt, quotaOpt)
118 | defer undo()
119 | require.NoError(t, err, "Set failed")
120 | assert.Equal(t, prev, currentMaxProcs(), "shouldn't alter GOMAXPROCS")
121 | assert.Contains(t, buf.String(), "quota undefined", "unexpected log output")
122 | })
123 |
124 | t.Run("QuotaUndefined return maxProcs=7", func(t *testing.T) {
125 | buf, logOpt := testLogger()
126 | quotaOpt := stubProcs(func(int, func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
127 | return 7, iruntime.CPUQuotaUndefined, nil
128 | })
129 | prev := currentMaxProcs()
130 | undo, err := Set(logOpt, quotaOpt)
131 | defer undo()
132 | require.NoError(t, err, "Set failed")
133 | assert.Equal(t, prev, currentMaxProcs(), "shouldn't alter GOMAXPROCS")
134 | assert.Contains(t, buf.String(), "quota undefined", "unexpected log output")
135 | })
136 |
137 | t.Run("QuotaTooSmall", func(t *testing.T) {
138 | buf, logOpt := testLogger()
139 | quotaOpt := stubProcs(func(min int, round func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
140 | return min, iruntime.CPUQuotaMinUsed, nil
141 | })
142 | undo, err := Set(logOpt, quotaOpt, Min(5))
143 | defer undo()
144 | require.NoError(t, err, "Set failed")
145 | assert.Equal(t, 5, currentMaxProcs(), "should use min allowed GOMAXPROCS")
146 | assert.Contains(t, buf.String(), "using minimum allowed", "unexpected log output")
147 | })
148 |
149 | t.Run("Min unused", func(t *testing.T) {
150 | buf, logOpt := testLogger()
151 | quotaOpt := stubProcs(func(min int, round func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
152 | return min, iruntime.CPUQuotaMinUsed, nil
153 | })
154 | // Min(-1) should be ignored.
155 | undo, err := Set(logOpt, quotaOpt, Min(5), Min(-1))
156 | defer undo()
157 | require.NoError(t, err, "Set failed")
158 | assert.Equal(t, 5, currentMaxProcs(), "should use min allowed GOMAXPROCS")
159 | assert.Contains(t, buf.String(), "using minimum allowed", "unexpected log output")
160 | })
161 |
162 | t.Run("QuotaUsed", func(t *testing.T) {
163 | opt := stubProcs(func(min int, round func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
164 | assert.Equal(t, 1, min, "Default minimum value should be 1")
165 | return 42, iruntime.CPUQuotaUsed, nil
166 | })
167 | undo, err := Set(opt)
168 | defer undo()
169 | require.NoError(t, err, "Set failed")
170 | assert.Equal(t, 42, currentMaxProcs(), "should change GOMAXPROCS to match quota")
171 | })
172 |
173 | t.Run("RoundQuotaSetToCeil", func(t *testing.T) {
174 | opt := stubProcs(func(min int, round func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
175 | assert.Equal(t, round(2.4), 3, "round should be math.Ceil")
176 | return 43, iruntime.CPUQuotaUsed, nil
177 | })
178 | undo, err := Set(opt, RoundQuotaFunc(func(v float64) int { return int(math.Ceil(v)) }))
179 | defer undo()
180 | require.NoError(t, err, "Set failed")
181 | assert.Equal(t, 43, currentMaxProcs(), "should change GOMAXPROCS to match rounded up quota")
182 | })
183 |
184 | t.Run("RoundQuotaSetToFloor", func(t *testing.T) {
185 | opt := stubProcs(func(min int, round func(v float64) int) (int, iruntime.CPUQuotaStatus, error) {
186 | assert.Equal(t, round(2.6), 2, "round should be math.Floor")
187 | return 42, iruntime.CPUQuotaUsed, nil
188 | })
189 | undo, err := Set(opt, RoundQuotaFunc(func(v float64) int { return int(math.Floor(v)) }))
190 | defer undo()
191 | require.NoError(t, err, "Set failed")
192 | assert.Equal(t, 42, currentMaxProcs(), "should change GOMAXPROCS to match rounded up quota")
193 | })
194 | }
195 |
196 | func TestMain(m *testing.M) {
197 | if err := os.Unsetenv(_maxProcsKey); err != nil {
198 | log.Fatalf("Couldn't clear %s: %v\n", _maxProcsKey, err)
199 | }
200 | os.Exit(m.Run())
201 | }
202 |
--------------------------------------------------------------------------------
/maxprocs/version.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2017 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | package maxprocs
22 |
23 | // Version is the current package version.
24 | const Version = "1.6.0"
25 |
--------------------------------------------------------------------------------
/tools/go.mod:
--------------------------------------------------------------------------------
1 | module go.uber.org/automaxprocs/tools
2 |
3 | go 1.20
4 |
5 | require (
6 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
7 | honnef.co/go/tools v0.3.0-0.dev.0.20220306074811-23e1086441d2
8 | )
9 |
10 | require (
11 | github.com/BurntSushi/toml v1.0.0 // indirect
12 | golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
13 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
14 | golang.org/x/sys v0.1.0 // indirect
15 | golang.org/x/tools v0.1.10 // indirect
16 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/tools/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
2 | github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
4 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
5 | golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb h1:fP6C8Xutcp5AlakmT/SkQot0pMicROAsEX7OfNPuG10=
6 | golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
7 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
8 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
9 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
10 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
11 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
12 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
16 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
18 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
20 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
21 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
22 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
23 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
24 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
25 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
26 | honnef.co/go/tools v0.3.0-0.dev.0.20220306074811-23e1086441d2 h1:utiSabORbG/JeX7MlmKMdmsjwom2+v8zmdb6SoBe4UY=
27 | honnef.co/go/tools v0.3.0-0.dev.0.20220306074811-23e1086441d2/go.mod h1:dZI0HmIvwDMW8owtLBJxTHoeX48yuF5p5pDy3y73jGU=
28 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Uber Technologies, Inc.
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy
4 | // of this software and associated documentation files (the "Software"), to deal
5 | // in the Software without restriction, including without limitation the rights
6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | // copies of the Software, and to permit persons to whom the Software is
8 | // furnished to do so, subject to the following conditions:
9 | //
10 | // The above copyright notice and this permission notice shall be included in
11 | // all copies or substantial portions of the Software.
12 | //
13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | // THE SOFTWARE.
20 |
21 | //go:build tools
22 | // +build tools
23 |
24 | package tools
25 |
26 | import (
27 | // Tools we use during development.
28 | _ "golang.org/x/lint/golint"
29 | _ "honnef.co/go/tools/cmd/staticcheck"
30 | )
31 |
--------------------------------------------------------------------------------