├── .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 | --------------------------------------------------------------------------------