├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── backpressure ├── doc.go ├── interface.go ├── mock.go ├── operator.go ├── operator_test.go ├── options.go ├── throttler.go └── throttler_test.go ├── config.go ├── config_test.go ├── constructor.go ├── constructor_test.go ├── controller ├── doc.go ├── interface.go └── nextgc │ ├── component_p.go │ ├── component_p_test.go │ ├── config.go │ ├── config_test.go │ ├── controller.go │ ├── controller_test.go │ └── doc.go ├── doc.go ├── docs ├── architecture.png ├── control_params.png ├── rss.png └── rss_hl.png ├── go.mod ├── go.sum ├── interface.go ├── middleware ├── doc.go ├── grpc.go └── middleware.go ├── options.go ├── requirements.txt ├── service_impl.go ├── service_stub.go ├── stats ├── doc.go ├── memlimiter.go ├── mock.go ├── service.go └── subscription.go ├── test ├── allocator │ ├── Dockerfile │ ├── analyze │ │ ├── compare.py │ │ ├── render.py │ │ ├── report.py │ │ └── testing.py │ ├── app │ │ ├── app.go │ │ ├── doc.go │ │ └── factory.go │ ├── main.go │ ├── perf │ │ ├── client.go │ │ ├── config.go │ │ ├── config.json │ │ ├── config_test.go │ │ └── doc.go │ ├── schema │ │ ├── allocator.pb.go │ │ ├── allocator.proto │ │ ├── allocator_grpc.pb.go │ │ └── generate.sh │ ├── server │ │ ├── config.go │ │ ├── config.json │ │ ├── config_test.go │ │ ├── doc.go │ │ └── server.go │ └── tracker │ │ ├── backend.go │ │ ├── backend_file.go │ │ ├── backend_memory.go │ │ ├── backend_test.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── doc.go │ │ ├── report.go │ │ └── tracker.go └── integration │ └── main_test.go └── utils ├── breaker ├── breaker.go └── doc.go ├── config ├── bytes │ ├── bytes.go │ ├── bytes_test.go │ └── doc.go ├── duration │ ├── doc.go │ ├── duration.go │ └── duration_test.go └── prepare │ ├── doc.go │ └── prepare.go ├── counter.go ├── doc.go ├── math.go └── math_test.go /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, master, develop] 6 | pull_request: 7 | branches: [main, master, develop] 8 | 9 | jobs: 10 | CI: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. 16 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. 17 | 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: '1.18' 21 | 22 | - name: Verify dependencies 23 | run: | 24 | go get -v -t ./... 25 | go mod verify 26 | 27 | - name: Build 28 | run: make build 29 | 30 | - name: SWAP 31 | run: | 32 | sudo swapon -s 33 | export PARTITION=$(sudo swapon -s | tail -n 1 | gawk '{print $1;}') 34 | sudo swapoff $PARTITION 35 | sudo swapon -s 36 | 37 | - name: Test 38 | run: make test_coverage 39 | 40 | - name: Go Coverage Badge # Pass the `coverage.out` output to this action 41 | uses: tj-actions/coverage-badge-go@v1.2 42 | with: 43 | filename: coverage.out 44 | 45 | - name: Verify changed files 46 | uses: tj-actions/verify-changed-files@v9.1 47 | id: verify-changed-files 48 | with: 49 | files: README.md 50 | 51 | - name: Commit changes 52 | if: steps.verify-changed-files.outputs.files_changed == 'true' 53 | run: | 54 | git config --local user.email "action@github.com" 55 | git config --local user.name "GitHub Action" 56 | git add README.md 57 | git commit -m "chore: Updated coverage badge." 58 | 59 | - name: Push changes 60 | if: steps.verify-changed-files.outputs.files_changed == 'true' 61 | uses: ad-m/github-push-action@master 62 | with: 63 | github_token: ${{ github.token }} 64 | branch: ${{ github.head_ref }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | test/infra/allocator/config.json 3 | test/allocator/allocator 4 | test/integration/integration-test 5 | venv 6 | __pycache__ 7 | coverage*out 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # default concurrency is a available CPU number 7 | concurrency: 4 8 | 9 | # timeout for analysis, e.g. 30s, 5m, default is 1m 10 | timeout: 5m 11 | 12 | # exit code when at least one issue was found, default is 1 13 | issues-exit-code: 1 14 | 15 | # include test files or not, default is true 16 | tests: true 17 | 18 | # list of build tags, all linters use it. Default is empty list. 19 | build-tags: 20 | - mytag 21 | 22 | # which dirs to skip: issues from them won't be reported; 23 | # can use regexp here: generated.*, regexp is applied on full path; 24 | # default value is empty list, but default dirs are skipped independently 25 | # from this option's value (see skip-dirs-use-default). 26 | skip-dirs: 27 | - src/external_libs 28 | - autogenerated_by_my_lib 29 | 30 | # default is true. Enables skipping of directories: 31 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 32 | skip-dirs-use-default: true 33 | 34 | # which files to skip: they will be analyzed, but issues from them 35 | # won't be reported. Default value is empty list, but there is 36 | # no need to include all autogenerated files, we confidently recognize 37 | # autogenerated files. If it's not please let us know. 38 | skip-files: 39 | - ".*\\.my\\.go$" 40 | - lib/bad.go 41 | 42 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 43 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 44 | # automatic updating of go.mod described above. Instead, it fails when any changes 45 | # to go.mod are needed. This setting is most useful to check that go.mod does 46 | # not need updates, such as in a continuous integration and testing system. 47 | # If invoked with -mod=vendor, the go command assumes that the vendor 48 | # directory holds the correct copies of dependencies and ignores 49 | # the dependency descriptions in go.mod. 50 | # modules-download-mode: readonly|release|vendor 51 | 52 | 53 | # output configuration options 54 | output: 55 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 56 | format: colored-line-number 57 | 58 | # print lines of code with issue, default is true 59 | print-issued-lines: true 60 | 61 | # print linter name in the end of issue text, default is true 62 | print-linter-name: true 63 | 64 | # make issues output unique by line, default is true 65 | uniq-by-line: true 66 | 67 | 68 | # all available settings of specific linters 69 | linters-settings: 70 | dogsled: 71 | # checks assignments with too many blank identifiers; default is 2 72 | max-blank-identifiers: 2 73 | dupl: 74 | # tokens count to trigger issue, 150 by default 75 | threshold: 100 76 | errcheck: 77 | # report about not checking of errors in type assertions: `a := b.(MyStruct)`; 78 | # default is false: such cases aren't reported by default. 79 | check-type-assertions: false 80 | 81 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 82 | # default is false: such cases aren't reported by default. 83 | check-blank: false 84 | 85 | # [deprecated] comma-separated list of pairs of the form pkg:regex 86 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 87 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 88 | ignore: fmt:.*,io/ioutil:^Read.* 89 | 90 | # path to a file containing a list of functions to exclude from checking 91 | # see https://github.com/kisielk/errcheck#excluding-functions for details 92 | # exclude: /path/to/file.txt 93 | funlen: 94 | lines: 80 95 | statements: 40 96 | gocognit: 97 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 98 | min-complexity: 20 99 | goconst: 100 | # minimal length of string constant, 3 by default 101 | min-len: 3 102 | # minimal occurrences count to trigger, 3 by default 103 | min-occurrences: 3 104 | gocritic: 105 | # Which checks should be enabled; can't be combined with 'disabled-checks'; 106 | # See https://go-critic.github.io/overview#checks-overview 107 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 108 | # By default list of stable checks is used. 109 | # enabled-checks: 110 | # большинство проверок уже включены, сюда можно писать просто дополнительные 111 | 112 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 113 | disabled-checks: 114 | # - regexpMust 115 | - whyNoLint # слишком много писать, увы... 116 | - dupImport # ломается на CGO: https://github.com/go-critic/go-critic/issues/845 117 | - octalLiteral # странный линтер, непонятно что хочет 118 | 119 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 120 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 121 | enabled-tags: 122 | - diagnostic 123 | - style 124 | - performance 125 | - experimental 126 | - opinionated 127 | 128 | settings: # settings passed to gocritic 129 | captLocal: # must be valid enabled check name 130 | paramsOnly: true 131 | rangeValCopy: 132 | sizeThreshold: 32 133 | gocyclo: 134 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 135 | min-complexity: 10 136 | godox: 137 | # report any comments starting with keywords, this is useful for TODO or FIXME comments that 138 | # might be left in the code accidentally and should be resolved before merging 139 | keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 140 | - NOTE 141 | - OPTIMIZE # marks code that should be optimized before merging 142 | - HACK # marks hack-arounds that should be removed before merging 143 | gofmt: 144 | # simplify code: gofmt with `-s` option, true by default 145 | simplify: true 146 | goimports: 147 | # put imports beginning with prefix after 3rd-party packages; 148 | # it's a comma-separated list of prefixes 149 | local-prefixes: github.com/org/project 150 | golint: 151 | # minimal confidence for issues, default is 0.8 152 | min-confidence: 0.21 153 | gomnd: 154 | settings: 155 | mnd: 156 | # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. 157 | checks: argument,case,condition,operation,return,assign 158 | ignored-numbers: 0,1 159 | govet: 160 | # report about shadowed variables 161 | check-shadowing: true 162 | # settings per analyzer 163 | settings: 164 | printf: # analyzer name, run `go tool vet help` to see all analyzers 165 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 166 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 167 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 168 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 169 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 170 | 171 | # enable or disable analyzers by name 172 | enable-all: true 173 | disable-all: false 174 | depguard: 175 | list-type: blacklist 176 | include-go-root: false 177 | packages: 178 | - github.com/sirupsen/logrus 179 | packages-with-error-message: 180 | # specify an error message to output when a blacklisted package is used 181 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 182 | lll: 183 | # max line length, lines longer will be reported. Default is 120. 184 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 185 | line-length: 140 186 | # tab width in spaces. Default to 1. 187 | tab-width: 1 188 | maligned: 189 | # print struct with more effective memory layout or not, false by default 190 | suggest-new: true 191 | misspell: 192 | # Correct spellings using locale preferences for US or UK. 193 | # Default is to use a neutral variety of English. 194 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 195 | locale: US 196 | ignore-words: 197 | - someword 198 | nakedret: 199 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 200 | max-func-lines: 30 201 | prealloc: 202 | # XXX: we don't recommend using this linter before doing performance profiling. 203 | # For most programs usage of prealloc will be a premature optimization. 204 | 205 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 206 | # True by default. 207 | simple: true 208 | range-loops: true # Report preallocation suggestions on range loops, true by default 209 | for-loops: false # Report preallocation suggestions on for loops, false by default 210 | rowserrcheck: 211 | packages: 212 | - github.com/jmoiron/sqlx 213 | tagliatelle: 214 | # check the struck tag name case 215 | case: 216 | # use the struct field name to check the name of the struct tag 217 | use-field-name: true 218 | rules: 219 | # any struct tag type can be used. 220 | # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` 221 | json: snake 222 | yaml: snake 223 | xml: snake 224 | bson: snake 225 | avro: snake 226 | mapstructure: kebab 227 | unparam: 228 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 229 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 230 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 231 | # with golangci-lint call it on a directory with the changed file. 232 | check-exported: false 233 | unused: 234 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 235 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 236 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 237 | # with golangci-lint call it on a directory with the changed file. 238 | check-exported: false 239 | whitespace: 240 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 241 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 242 | wrapcheck: 243 | ignorePackageGlobs: 244 | - gitlab.stageoffice.ru/UCS-COMMON/utils/breaker 245 | - github.com/stretchr/testify* 246 | - google.golang.org/grpc* 247 | wsl: 248 | # If true append is only allowed to be cuddled if appending value is 249 | # matching variables, fields or types on line above. Default is true. 250 | strict-append: true 251 | # Allow calls and assignments to be cuddled as long as the lines have any 252 | # matching variables, fields or types. Default is true. 253 | allow-assign-and-call: false 254 | # Allow multiline assignments to be cuddled. Default is true. 255 | allow-multiline-assign: false 256 | # Allow declarations (var) to be cuddled. 257 | allow-cuddle-declarations: false 258 | # Allow trailing comments in ending of blocks 259 | allow-trailing-comment: false 260 | # Force newlines in end of case at this limit (0 = never). 261 | force-case-trailing-whitespace: 0 262 | 263 | linters: 264 | enable-all: true 265 | fast: false 266 | disable: 267 | - promlinter # too many bugs 268 | - gci # relation with goimports is not clear 269 | - gofumpt # conflicts with wsl 270 | - godox # useless 271 | - exhaustivestruct # bloats code 272 | - paralleltest # it's not clear when and how to use t.Parallel() 273 | - testpackage # seriously limits testing capabilities 274 | 275 | issues: 276 | # List of regexps of issue texts to exclude, empty list by default. 277 | # But independently from this option we use default exclude patterns, 278 | # it can be disabled by `exclude-use-default: false`. To list all 279 | # excluded by default patterns execute `golangci-lint run --help` 280 | exclude: 281 | - abcdef 282 | 283 | # Excluding configuration per-path, per-linter, per-text and per-source 284 | exclude-rules: 285 | - path: _test\.go 286 | linters: 287 | - gocyclo 288 | - dupl 289 | - gosec 290 | - funlen 291 | - gocognit 292 | - gomnd 293 | 294 | - path: "test_.*\\.go$" 295 | linters: 296 | - funlen 297 | - gomnd 298 | - gocognit 299 | 300 | - path: mock.go 301 | text: "exported type" 302 | linters: 303 | - golint 304 | - revive 305 | 306 | - path: mock.go 307 | text: "exported function" 308 | linters: 309 | - golint 310 | - revive 311 | 312 | - path: mock.go 313 | text: "exported method" 314 | linters: 315 | - golint 316 | - revive 317 | 318 | - path: mock.go 319 | linters: 320 | - gomnd 321 | 322 | - path: config.go 323 | text: "fieldalignment" 324 | 325 | # Independently from option `exclude` we use default exclude patterns, 326 | # it can be disabled by this option. To list all 327 | # excluded by default patterns execute `golangci-lint run --help`. 328 | # Default value for this option is true. 329 | exclude-use-default: false 330 | 331 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 332 | max-issues-per-linter: 0 333 | 334 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 335 | max-same-issues: 0 336 | 337 | # Show only new issues: if there are unstaged changes or untracked files, 338 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 339 | # It's a super-useful option for integration of golangci-lint into existing 340 | # large codebase. It's not practical to fix all existing issues at the moment 341 | # of integration: much better don't allow issues in new code. 342 | # Default is false. 343 | new: false 344 | 345 | # Show only new issues created after git revision `REV` 346 | # new-from-rev: REV 347 | 348 | # Show only new issues created in git patch with set file path. 349 | # new-from-patch: path/to/patch/file 350 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 New Cloud Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build ./... 3 | cd ./test/allocator && go build . && cd - 4 | 5 | #UNIT_TEST_PACKAGES=$(shell go list ./... | grep -v test/integration test/allocator/app) 6 | UNIT_TEST_PACKAGES=$(shell go list ./...) 7 | unit_test: 8 | go test -v -count=1 -cover $(UNIT_TEST_PACKAGES) -coverprofile=coverage.unit.out -coverpkg ./... 9 | 10 | integration_test: 11 | go test -c ./test/integration/main_test.go -o ./test/integration/integration-test -coverpkg ./... 12 | ./test/integration/integration-test -test.v -test.coverprofile=coverage.integration.out 13 | 14 | test_coverage: unit_test integration_test 15 | # merge outputs from unit and integration testing 16 | cp coverage.unit.out coverage.overall.out 17 | tail --lines=+2 coverage.integration.out >> coverage.overall.out 18 | # cannot cover main function and CLI package 19 | sed -i '/test\/allocator\/app/d' ./coverage.overall.out 20 | sed -i '/test\/allocator\/main.go/d' ./coverage.overall.out 21 | # final report 22 | go tool cover -func=coverage.overall.out -o=coverage.out 23 | 24 | fix: 25 | go fmt . 26 | go mod tidy 27 | 28 | lint: 29 | golangci-lint run -c .golangci.yml ./... 30 | 31 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/newcloudtechnologies/memlimiter.svg)](https://pkg.go.dev/github.com/newcloudtechnologies/memlimiter) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/newcloudtechnologies/memlimiter)](https://goreportcard.com/report/github.com/newcloudtechnologies/memlimiter) 3 | ![Coverage](https://img.shields.io/badge/Coverage-80.1%25-brightgreen) 4 | ![CI](https://github.com/newcloudtechnologies/memlimiter/actions/workflows/CI.yml/badge.svg) 5 | 6 | # MemLimiter 7 | 8 | Library that helps to limit memory consumption of your Go service. 9 | 10 | ## Working principles 11 | As of today (Go 1.18), there is a possibility for any Go application to be eventually stopped by OOM killer. The memory leak is because Go runtime knows nothing about the limitations imposed on the process by the operating system (for instance, using `cgroups`). However, an unexpected termination of a process because of OOM is highly undesirable, as it can lead to cache resetting, data integrity violation, distributed transaction hanging and even cascading failure of a distributed backend. Therefore, services should degrade gracefully instead of immediate stop due to `SIGKILL`. 12 | 13 | A universal solution for programming languages with automatic memory management comprises two parts: 14 | 15 | 1. **Garbage collection intensification**. The more often GC starts, the more garbage will be collected, the fewer new physical memory allocations we have to make for the service’s business logic. 16 | 2. **Request throttling**. By suppressing some of the incoming requests, we implement the backpressure: the middleware simply cuts off part of the load coming from the client in order to avoid too many memory allocations. 17 | 18 | MemLimiter represents a memory budget [automated control system](https://en.wikipedia.org/wiki/Control_system) that helps to keep the memory consumption of a Go service within a predefined limit. 19 | 20 | ### Memory budget utilization 21 | 22 | The core of the MemLimiter is a special object quite similar to [P-controller](https://en.wikipedia.org/wiki/PID_controller), but with certain specifics (more on that below). Memory budget utilization value acts as an input signal for the controller. We define the $Utilization$ as follows: 23 | 24 | $$ Utilization = \frac {NextGC} {RSS_{limit} - CGO} $$ 25 | 26 | where: 27 | * $NextGC$ ([from here](https://pkg.go.dev/runtime#MemStats)) is a target size for heap, upon reaching which the Go runtime will launch the GC next time; 28 | * $RSS_{limit}$ is a hard limit for service's physical memory (`RSS`) consumption (so that exceeding this limit will highly likely result in OOM); 29 | * $CGO$ is a total size of heap allocations made beyond `Cgo` borders (within `C`/`C++`/.... libraries). 30 | 31 | A few notes about $CGO$ component. Allocations made outside of the Go allocator, of course, are not controlled by the Go runtime in any way. At the same time, the memory consumption limit is common for both Go and non-Go allocators. Therefore, if non-Go allocations grow, all we can do is shrink the memory budget for Go allocations (which is why we subtract $CGO$ from the denominator of the previous expression). If your service uses `Cgo`, you need to figure out how much memory is allocated “on the other side” – **otherwise MemLimiter won’t be able to save your service from OOM**. 32 | 33 | If the service doesn't use `Cgo`, the $Utilization$ formula is simplified to: 34 | $$Utilization = \frac {NextGC} {RSS_{limit}}$$ 35 | 36 | ### Control function 37 | 38 | The controller converts the input signal into the control signal according to the following formula: 39 | 40 | $$ K_{p} = C_{p} \cdot \frac {1} {1 - Utilization} $$ 41 | 42 | This is not an ordinary definition for a proportional component of the PID-controller, but still the direct proportionality is preserved: the closer the $Utilization$ is to 1 (or 100%), the higher the control signal value. The main purpose of the controller is to prevent a situation in which the next GC launch will be scheduled when the memory consumption exceeds the hard limit (and this will cause OOM). 43 | 44 | You can adjust the proportional component control signal strength using a coefficient $C_{p}$. In addition, there is optional [exponential averaging](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) of the control signal. This helps to smooth out high-frequency fluctuations of the control signal (but it hardly eliminates [self-oscillations](https://en.wikipedia.org/wiki/Self-oscillation)). 45 | 46 | The control signal is always saturated to prevent extremal values: 47 | 48 | $$ Output = \begin{cases} 49 | \displaystyle 100 \ \ \ K_{p} \gt 100 \\ 50 | \displaystyle 0 \ \ \ \ \ \ \ K_{p} \lt 100 \\ 51 | \displaystyle K_{p} \ \ \ \ otherwise \\ 52 | \end{cases}$$ 53 | 54 | Finally we convert the dimensionless quantity $Output$ into specific $GOGC$ (for the further use in [`debug.SetGCPercent`](https://pkg.go.dev/runtime/debug#SetGCPercent)) and $Throttling$ (percentage of suppressed requests) values, however, only if the $Utilization$ exceeds the specified limits: 55 | 56 | 57 | $$ GC = \begin{cases} 58 | \displaystyle Output \ \ \ Utilization \gt DangerZoneGC \\ 59 | \displaystyle 100 \ \ \ \ \ \ \ \ \ \ otherwise \\ 60 | \end{cases}$$ 61 | 62 | $$ Throttling = \begin{cases} 63 | \displaystyle Output \ \ \ Utilization \gt DangerZoneThrottling \\ 64 | \displaystyle 0 \ \ \ \ \ \ \ \ \ \ \ \ \ \ otherwise \\ 65 | \end{cases}$$ 66 | 67 | ## Architecture 68 | 69 | The MemLimiter comprises two main parts: 70 | 71 | 1. **Core** implementing the memory budget controller and backpressure subsystems. Core relies on actual statistics provided by `stats.ServiceStatsSubscription`. In a critical situation, core may gracefully terminate the application with `utils.ApplicationTerminator`. 72 | 2. **Middleware** providing request throttling feature for various web frameworks. Every time the server receives a request, it uses middleware to ask the MemLimiter’s core for permission to process this request. Currently, only `GRPC` is supported, but `Middleware` is an easily extensible interface, and PRs are welcome. 73 | 74 | ![Architecture](docs/architecture.png) 75 | 76 | ## Quick start guide 77 | 78 | ### Services without `Cgo` 79 | 80 | Refer to the [example service](test/allocator/server/server.go). 81 | 82 | ### Services with `Cgo` 83 | 84 | Refer to the [example service](test/allocator/server/server.go). 85 | 86 | You must also provide your own `stats.ServiceStatsSubscription` and `stats.ServiceStats` implementations. The latter one must return non-nil `stats.ConsumptionReport` instances if you want MemLimiter to consider allocations made outside of Go runtime allocator and estimate memory utilization correctly. 87 | 88 | ### Tuning 89 | 90 | There are several key settings in MemLimiter [configuration](controller/nextgc/config.go): 91 | 92 | * `RSSLimit` 93 | * `DangerZoneGC` 94 | * `DangerZoneThrottling` 95 | * `Period` 96 | * `WindowSize` 97 | * `Coefficient` ($C_{p}$) 98 | 99 | You have to pick them empirically for your service. The settings must correspond to the business logic features of a particular service and to the workload expected. 100 | 101 | We made a series of performance tests with [Allocator][test/allocator] - an example service which does nothing but allocations that reside in memory for some time. We used different settings, applied the same load and tracked the RSS of a process. 102 | 103 | Settings ranges: 104 | * $RSS_{limit} = {1G}$ 105 | * $DangerZoneGC = 50%$ 106 | * $DangerZoneThrottling = 90%$ 107 | * $Period = 100ms$ 108 | * $WindowSize = 20$ 109 | * $C_{p} \in \\{0, 0.5, 1, 5, 10, 50, 100\\}$ 110 | 111 | These plots may give you some inspiration on how $C_{p}$ value affects the physical memory consumption other things being equal: 112 | 113 | ![Control params](docs/control_params.png) 114 | 115 | And the summary plot with RSS consumption dependence on $C_{p}$ value: 116 | 117 | ![RSS](docs/rss_hl.png) 118 | 119 | The general conclusion is that: 120 | * The higher the $C_{p}$ is, the lower the $RSS$ consumption. 121 | * Too low and too high $C_{p}$ values cause self-oscillation of control parameters. 122 | * Disabling MemLimiter causes OOM. 123 | 124 | ## TODO 125 | 126 | * Extend middleware.Middleware to support more frameworks. 127 | * Add GOGC limitations to prevent death spirals. 128 | * Support popular Cgo allocators like Jemalloc or TCMalloc, parse their stats to provide information about Cgo memory consumption. 129 | 130 | Your PRs are welcome! 131 | 132 | ## Publications 133 | 134 | * Isaev V. A. Go runtime high memory consumption (in Russian). Evrone Go meetup. 2022. 135 | [![Preview](https://yt-embed.herokuapp.com/embed?v=_BbhmaZupqs)]( 136 | https://www.youtube.com/watch?v=_BbhmaZupqs 137 | ) 138 | -------------------------------------------------------------------------------- /backpressure/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package backpressure contains code applying control signals issued by controller to Go runtime and 8 | // and to gRPC server. 9 | package backpressure 10 | -------------------------------------------------------------------------------- /backpressure/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/stats" 11 | ) 12 | 13 | const ( 14 | // DefaultGOGC - default GOGC value. 15 | DefaultGOGC = 100 16 | // NoThrottling - allow execution of all GRPC requests. 17 | NoThrottling = 0 18 | // FullThrottling - a complete ban on GRPC request execution. 19 | FullThrottling = 100 20 | ) 21 | 22 | // Operator applies control signals to Go runtime and GRPC server. 23 | type Operator interface { 24 | // SetControlParameters registers the actual value of control parameters. 25 | SetControlParameters(value *stats.ControlParameters) error 26 | // AllowRequest can be used by server middleware to check if it's possible to execute 27 | // a particular request. 28 | AllowRequest() bool 29 | // GetStats returns statistics of Backpressure subsystem. 30 | GetStats() (*stats.BackpressureStats, error) 31 | } 32 | -------------------------------------------------------------------------------- /backpressure/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/stats" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | var _ Operator = (*OperatorMock)(nil) 15 | 16 | type OperatorMock struct { 17 | Operator 18 | mock.Mock 19 | } 20 | 21 | func (m *OperatorMock) SetControlParameters(value *stats.ControlParameters) error { 22 | args := m.Called(value) 23 | 24 | return args.Error(0) 25 | } 26 | -------------------------------------------------------------------------------- /backpressure/operator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "runtime/debug" 11 | "sync/atomic" 12 | 13 | "github.com/go-logr/logr" 14 | "github.com/newcloudtechnologies/memlimiter/stats" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | var _ Operator = (*operatorImpl)(nil) 19 | 20 | type operatorImpl struct { 21 | *throttler 22 | notificationChan chan<- *stats.MemLimiterStats 23 | lastControlParameters atomic.Value 24 | logger logr.Logger 25 | } 26 | 27 | func (b *operatorImpl) GetStats() (*stats.BackpressureStats, error) { 28 | result := &stats.BackpressureStats{ 29 | Throttling: b.throttler.getStats(), 30 | } 31 | 32 | lastControlParameters := b.lastControlParameters.Load() 33 | if lastControlParameters != nil { 34 | var ok bool 35 | 36 | result.ControlParameters, ok = lastControlParameters.(*stats.ControlParameters) 37 | if !ok { 38 | return nil, errors.Errorf("ivalid type cast (%T)", lastControlParameters) 39 | } 40 | } 41 | 42 | return result, nil 43 | } 44 | 45 | func (b *operatorImpl) SetControlParameters(value *stats.ControlParameters) error { 46 | old := b.lastControlParameters.Swap(value) 47 | if old != nil { 48 | // if control parameters didn't change, we do nothing 49 | if value.EqualsTo(old.(*stats.ControlParameters)) { 50 | return nil 51 | } 52 | } 53 | 54 | // set the share of the requests that have to be throttled 55 | if err := b.throttler.setThreshold(value.ThrottlingPercentage); err != nil { 56 | return errors.Wrap(err, "throttler set threshold") 57 | } 58 | 59 | // tune GC pace 60 | debug.SetGCPercent(value.GOGC) 61 | 62 | b.logger.Info("control parameters changed", value.ToKeysAndValues()...) 63 | 64 | // notify client about statistics change 65 | if b.notificationChan != nil { 66 | backpressureStats, err := b.GetStats() 67 | if err != nil { 68 | return errors.Wrap(err, "get stats") 69 | } 70 | 71 | memLimiterStats := &stats.MemLimiterStats{ 72 | Controller: value.ControllerStats, 73 | Backpressure: backpressureStats, 74 | } 75 | 76 | // if client's not ready to read, omit it 77 | select { 78 | case b.notificationChan <- memLimiterStats: 79 | default: 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // NewOperator builds new Operator. 87 | func NewOperator(logger logr.Logger, options ...Option) Operator { 88 | out := &operatorImpl{ 89 | logger: logger, 90 | throttler: newThrottler(), 91 | } 92 | 93 | //nolint:gocritic 94 | for _, op := range options { 95 | switch t := op.(type) { 96 | case *notificationsOption: 97 | out.notificationChan = t.val 98 | } 99 | } 100 | 101 | return out 102 | } 103 | -------------------------------------------------------------------------------- /backpressure/operator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/go-logr/logr/testr" 13 | "github.com/newcloudtechnologies/memlimiter/stats" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestOperator(t *testing.T) { 18 | logger := testr.New(t) 19 | notifications := make(chan *stats.MemLimiterStats, 1) 20 | 21 | op := NewOperator(logger, WithNotificationsOption(notifications)) 22 | 23 | params := &stats.ControlParameters{ 24 | GOGC: 20, 25 | ThrottlingPercentage: 80, 26 | } 27 | 28 | err := op.SetControlParameters(params) 29 | require.NoError(t, err) 30 | 31 | notification := <-notifications 32 | 33 | require.Equal(t, params, notification.Backpressure.ControlParameters) 34 | } 35 | -------------------------------------------------------------------------------- /backpressure/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/stats" 11 | ) 12 | 13 | // Option - backpressure operator options. 14 | type Option interface { 15 | anchor() 16 | } 17 | 18 | type notificationsOption struct { 19 | val chan<- *stats.MemLimiterStats 20 | } 21 | 22 | func (o notificationsOption) anchor() {} 23 | 24 | // WithNotificationsOption makes it possible for a client to implement application-specific 25 | // backpressure logic. Client may subscribe for the operative MemLimiter telemetry and make 26 | // own decisions (taking into account metrics like RSS etc.). 27 | // This is inspired by the channel that was attached to SetMaxHeap function 28 | // (see https://github.com/golang/proposal/blob/master/design/48409-soft-memory-limit.md#setmaxheap) 29 | func WithNotificationsOption(notifications chan<- *stats.MemLimiterStats) Option { 30 | return ¬ificationsOption{ 31 | val: notifications, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backpressure/throttler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "sync/atomic" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/stats" 13 | "github.com/newcloudtechnologies/memlimiter/utils" 14 | "github.com/villenny/fastrand64-go" 15 | 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | type throttler struct { 20 | // group of request counters 21 | requestsTotal utils.Counter 22 | requestsPassed utils.Counter 23 | requestsThrottled utils.Counter 24 | 25 | // The following features of RNG are crucial for the backpressure subsystem: 26 | // 1. uniform distribution of the output; 27 | // 2. thread-safety; 28 | 29 | // In order to save time, we use a third party RNG library which is thread-safe, 30 | // however, there are some concerns on the distribution uniformity. 31 | // There are indicators that it's truly uniform because this RNG (providing only integer numbers) 32 | // was used in the uniformly distributed float RNG implementation: https://prng.di.unimi.it/ 33 | 34 | // Here are the posts stating that (at least empirically) the distribution uniformity is 35 | // observed for all RNGs belonging to this family but one: 36 | // https://stackoverflow.com/questions/71050149/does-xoshiro-xoroshiro-prngs-provide-uniform-distribution 37 | // https://crypto.stackexchange.com/questions/98597 38 | rng *fastrand64.ThreadsafePoolRNG 39 | 40 | // число в диапазоне [0; 100], показывающее, какой процент запросов должен быть отбит 41 | threshold uint32 42 | } 43 | 44 | func (t *throttler) setThreshold(value uint32) error { 45 | if value > FullThrottling { 46 | return errors.New("implementation error: threshold value must belong to [0;100]") 47 | } 48 | 49 | atomic.StoreUint32(&t.threshold, value) 50 | 51 | return nil 52 | } 53 | 54 | func (t *throttler) getStats() *stats.ThrottlingStats { 55 | return &stats.ThrottlingStats{ 56 | Total: uint64(t.requestsTotal.Count()), 57 | Passed: uint64(t.requestsPassed.Count()), 58 | Throttled: uint64(t.requestsThrottled.Count()), 59 | } 60 | } 61 | 62 | func (t *throttler) AllowRequest() bool { 63 | threshold := atomic.LoadUint32(&t.threshold) 64 | 65 | // if throttling is disabled, allow any request 66 | if threshold == 0 { 67 | t.requestsPassed.Inc(1) 68 | 69 | return true 70 | } 71 | 72 | // flip a coin in the range [0; 100]; if the actual value is less than the threshold value, 73 | // throttle the request, otherwise allow it. 74 | value := t.rng.Uint32n(FullThrottling) 75 | 76 | allowed := value >= threshold 77 | 78 | if allowed { 79 | t.requestsPassed.Inc(1) 80 | } else { 81 | t.requestsThrottled.Inc(1) 82 | } 83 | 84 | return allowed 85 | } 86 | 87 | func newThrottler() *throttler { 88 | requestsTotal := utils.NewCounter(nil) 89 | 90 | return &throttler{ 91 | rng: fastrand64.NewSyncPoolXoshiro256ssRNG(), 92 | requestsTotal: requestsTotal, 93 | requestsPassed: utils.NewCounter(requestsTotal), 94 | requestsThrottled: utils.NewCounter(requestsTotal), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /backpressure/throttler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package backpressure 8 | 9 | import ( 10 | "fmt" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/newcloudtechnologies/memlimiter/utils" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestThrottler(t *testing.T) { 19 | // Launch 1000 requests concurrently in each test. Some of them must be throttled, others should be allowed. 20 | const ( 21 | requests = 1000 22 | ) 23 | 24 | // Check different throttling levels with 10% step. 25 | for i := 0; i < 10; i++ { 26 | throttlingLevel := uint32(i) * 10 27 | 28 | t.Run(fmt.Sprintf("throttling level = %v", throttlingLevel), func(t *testing.T) { 29 | th := newThrottler() 30 | 31 | err := th.setThreshold(throttlingLevel) 32 | require.NoError(t, err) 33 | 34 | wg := &sync.WaitGroup{} 35 | wg.Add(requests) 36 | 37 | failed := utils.NewCounter(nil) 38 | succeeded := utils.NewCounter(nil) 39 | 40 | for i := 0; i < requests; i++ { 41 | go func() { 42 | defer wg.Done() 43 | 44 | ok := th.AllowRequest() 45 | if ok { 46 | succeeded.Inc(1) 47 | } else { 48 | failed.Inc(1) 49 | } 50 | }() 51 | } 52 | 53 | wg.Wait() 54 | 55 | total := failed.Count() + succeeded.Count() 56 | require.Equal(t, int64(requests), total) 57 | 58 | failedShareExpected := float64(throttlingLevel) / float64(100) 59 | failedShareActual := float64(failed.Count()) / float64(total) 60 | succeededShareExpected := 1 - failedShareExpected 61 | succeededShareActual := float64(succeeded.Count()) / float64(total) 62 | 63 | // Either sample length is not sufficient, or RNG distribution is not exactly uniform 64 | // (look through the comments in throttler.go). 65 | // We cannot increase sample length, because this is unit rather than performance tests, 66 | // so we introduce small inaccuracy. 67 | require.InDelta( 68 | t, 69 | failedShareExpected, 70 | failedShareActual, 71 | 0.055, 72 | fmt.Sprintf("expected = %v, actual = %v", failedShareExpected, failedShareActual), 73 | ) 74 | require.InDelta( 75 | t, 76 | succeededShareExpected, 77 | succeededShareActual, 78 | 0.055, 79 | fmt.Sprintf("expected = %v, actual = %v", succeededShareExpected, succeededShareActual), 80 | ) 81 | 82 | // Check internal counters. 83 | require.Equal(t, total, th.requestsTotal.Count()) 84 | require.Equal(t, failed.Count(), th.requestsThrottled.Count()) 85 | require.Equal(t, succeeded.Count(), th.requestsPassed.Count()) 86 | }) 87 | } 88 | } 89 | 90 | /* 91 | go test -bench=. -benchtime=10s ./backpressure 92 | goos: linux 93 | goarch: amd64 consumption_reporter.go doc.go mock.go 94 | pkg: github.com/newcloudtechnologies/memlimiter/backpressure 95 | cpu: AMD Ryzen 7 2700X Eight-Core Processor 96 | BenchmarkThrottler/throttling_level_=_0-16 22977 542772 ns/op 97 | BenchmarkThrottler/throttling_level_=_50-16 22722 508701 ns/op 98 | BenchmarkThrottler/throttling_level_=_100-16 22220 488162 ns/op 99 | PASS 100 | ok github.com/newcloudtechnologies/memlimiter/backpressure 57.747s. 101 | */ 102 | func BenchmarkThrottler(b *testing.B) { 103 | const requests = 1000 104 | 105 | for _, throttlingLevel := range []uint32{0, 50, 100} { 106 | throttlingLevel := throttlingLevel 107 | 108 | b.Run(fmt.Sprintf("throttling level = %v", throttlingLevel), func(b *testing.B) { 109 | th := newThrottler() 110 | 111 | err := th.setThreshold(throttlingLevel) 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | 116 | var allowed bool 117 | 118 | for k := 0; k < b.N; k++ { 119 | wg := &sync.WaitGroup{} 120 | 121 | b.StartTimer() 122 | 123 | for i := 0; i < requests; i++ { 124 | wg.Add(1) 125 | 126 | go func() { 127 | defer wg.Done() 128 | 129 | // assign result to fictive variable to disallow compiler to optimize out function call 130 | allowed = th.AllowRequest() 131 | }() 132 | } 133 | wg.Wait() 134 | 135 | b.StopTimer() 136 | } 137 | 138 | _ = allowed 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "github.com/pkg/errors" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/controller/nextgc" 13 | ) 14 | 15 | // Config - high-level MemLimiter config. 16 | type Config struct { 17 | // ControllerNextGC - NextGC-based controller 18 | ControllerNextGC *nextgc.ControllerConfig `json:"controller_nextgc"` //nolint:tagliatelle 19 | // TODO: 20 | // if new controller implementation appears, put its config here and make switch in Prepare() 21 | // (only one subsection must be not nil). 22 | } 23 | 24 | // Prepare validates config. 25 | func (c *Config) Prepare() error { 26 | if c == nil { 27 | // This means that user wants to use stub instead of real memlimiter 28 | return nil 29 | } 30 | 31 | if c.ControllerNextGC == nil { 32 | return errors.New("empty ControllerNextGC") 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestConfig(t *testing.T) { 16 | t.Run("config nil", func(t *testing.T) { 17 | var c *Config 18 | require.NoError(t, c.Prepare()) 19 | }) 20 | 21 | t.Run("controller config nil", func(t *testing.T) { 22 | c := &Config{ControllerNextGC: nil} 23 | require.Error(t, c.Prepare()) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /constructor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/go-logr/logr" 13 | "github.com/newcloudtechnologies/memlimiter/backpressure" 14 | "github.com/newcloudtechnologies/memlimiter/stats" 15 | ) 16 | 17 | // NewServiceFromConfig - main entrypoint for MemLimiter. 18 | func NewServiceFromConfig( 19 | logger logr.Logger, 20 | cfg *Config, 21 | options ...Option, 22 | ) (Service, error) { 23 | var ( 24 | serviceStatsSubscription stats.ServiceStatsSubscription 25 | backpressureOperator backpressure.Operator 26 | ) 27 | 28 | for _, op := range options { 29 | switch t := (op).(type) { 30 | case *serviceStatsSubscriptionOption: 31 | serviceStatsSubscription = t.val 32 | case *backpressureOperatorOption: 33 | backpressureOperator = t.val 34 | } 35 | } 36 | 37 | // make defaults 38 | if serviceStatsSubscription == nil { 39 | serviceStatsSubscription = stats.NewSubscriptionDefault(logger, time.Second) 40 | } 41 | 42 | if backpressureOperator == nil { 43 | backpressureOperator = backpressure.NewOperator(logger) 44 | } 45 | 46 | if cfg == nil { 47 | return newServiceStub(serviceStatsSubscription), nil 48 | } 49 | 50 | return newServiceImpl(logger, cfg, serviceStatsSubscription, backpressureOperator) 51 | } 52 | -------------------------------------------------------------------------------- /constructor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | "github.com/go-logr/logr/testr" 14 | "github.com/newcloudtechnologies/memlimiter/stats" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestConstructor(t *testing.T) { 19 | t.Run("stub", func(t *testing.T) { 20 | logger := testr.New(t) 21 | 22 | const delay = 10 * time.Millisecond 23 | 24 | subscription := stats.NewSubscriptionDefault(logger, delay) 25 | defer subscription.Quit() 26 | 27 | service, err := NewServiceFromConfig( 28 | logger, 29 | nil, // use stub instead of real service 30 | WithServiceStatsSubscription(subscription), 31 | ) 32 | require.NoError(t, err) 33 | 34 | defer service.Quit() 35 | 36 | ss, err := service.GetStats() 37 | require.NoError(t, err) 38 | require.Nil(t, ss) 39 | 40 | time.Sleep(2 * delay) 41 | 42 | ss, err = service.GetStats() 43 | require.NoError(t, err) 44 | require.NotNil(t, ss) 45 | 46 | require.Nil(t, service.Middleware()) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /controller/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package controller contains common types for different possible implementations 8 | // of memory usage controller. 9 | package controller 10 | -------------------------------------------------------------------------------- /controller/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package controller 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/stats" 11 | ) 12 | 13 | // Controller - generic memory consumption controller interface. 14 | type Controller interface { 15 | GetStats() (*stats.ControllerStats, error) 16 | Quit() 17 | } 18 | -------------------------------------------------------------------------------- /controller/nextgc/component_p.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "math" 11 | 12 | "github.com/go-logr/logr" 13 | metrics "github.com/rcrowley/go-metrics" 14 | 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // The proportional component of the controller. 19 | type componentP struct { 20 | lastValues metrics.Sample 21 | cfg *ComponentProportionalConfig 22 | logger logr.Logger 23 | } 24 | 25 | func (c *componentP) value(utilization float64) (float64, error) { 26 | if c.lastValues != nil { 27 | valueEMA, err := c.valueEMA(utilization) 28 | if err != nil { 29 | return math.NaN(), errors.Wrap(err, "value EMA") 30 | } 31 | 32 | return valueEMA, nil 33 | } 34 | 35 | valueRaw, err := c.valueRaw(utilization) 36 | if err != nil { 37 | return math.NaN(), errors.Wrap(err, "value raw") 38 | } 39 | 40 | return valueRaw, nil 41 | } 42 | 43 | func (c *componentP) valueRaw(utilization float64) (float64, error) { 44 | if utilization < 0 { 45 | return math.NaN(), errors.Errorf("value is undefined if memory usage = %v", utilization) 46 | } 47 | 48 | if utilization >= 1 { 49 | // In theory, values >= 1 are impossible, but in practice sometimes we face small exceeding of the upper bound (< 1.1). 50 | // This needs to be investigated later. 51 | const maxReasonableOutput = 100 52 | 53 | c.logger.Info( 54 | "not a good value for memory usage, cutting output value", 55 | "memory_usage", utilization, 56 | "output_value", maxReasonableOutput, 57 | ) 58 | 59 | return maxReasonableOutput, nil 60 | } 61 | 62 | // The closer the memory usage value is to 100%, the stronger the control signal. 63 | return c.cfg.Coefficient * (1 / (1 - utilization)), nil 64 | } 65 | 66 | func (c *componentP) valueEMA(utilization float64) (float64, error) { 67 | valueRaw, err := c.valueRaw(utilization) 68 | if err != nil { 69 | return 0, errors.Wrap(err, "value raw") 70 | } 71 | 72 | // TODO: need to find statistical library working with floats to make this conversion unnecessary 73 | const reductionFactor = 100 74 | 75 | c.lastValues.Update(int64(valueRaw * reductionFactor)) 76 | 77 | return c.lastValues.Mean() / reductionFactor, nil 78 | } 79 | 80 | func newComponentP(logger logr.Logger, cfg *ComponentProportionalConfig) *componentP { 81 | out := &componentP{ 82 | logger: logger, 83 | cfg: cfg, 84 | } 85 | 86 | if cfg.WindowSize != 0 { 87 | // alpha is a smoothing coefficient describing the degree of weighting decrease; 88 | // the lesser the alpha is, the higher the impact of the elder historical values on the resulting value. 89 | // alpha is choosed empirically, but can depend on a window size, like here: 90 | // https://en.wikipedia.org/wiki/Moving_average#Relationship_between_SMA_and_EMA 91 | //nolint:gomnd 92 | alpha := 2 / (float64(cfg.WindowSize + 1)) 93 | 94 | out.lastValues = metrics.NewExpDecaySample(int(cfg.WindowSize), alpha) 95 | } 96 | 97 | return out 98 | } 99 | -------------------------------------------------------------------------------- /controller/nextgc/component_p_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/go-logr/logr/testr" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestComponentP(t *testing.T) { 17 | logger := testr.New(t) 18 | cmp := &componentP{logger: logger} 19 | 20 | _, err := cmp.value(-1) 21 | require.Error(t, err) 22 | 23 | _, err = cmp.value(2) 24 | require.NoError(t, err) 25 | } 26 | -------------------------------------------------------------------------------- /controller/nextgc/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 11 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // ControllerConfig - controller configuration. 16 | type ControllerConfig struct { 17 | // RSSLimit - physical memory (RSS) consumption hard limit for a process. 18 | RSSLimit bytes.Bytes `json:"rss_limit"` 19 | // DangerZoneGOGC - RSS utilization threshold that triggers controller to 20 | // set more conservative parameters for GC. 21 | // Possible values are in range (0; 100). 22 | DangerZoneGOGC uint32 `json:"danger_zone_gogc"` 23 | // DangerZoneGOGC - RSS utilization threshold that triggers controller to 24 | // throttle incoming requests. 25 | // Possible values are in range (0; 100). 26 | DangerZoneThrottling uint32 `json:"danger_zone_throttling"` 27 | // Period - the periodicity of control parameters computation. 28 | Period duration.Duration `json:"period"` 29 | // ComponentProportional - controller's proportional component configuration 30 | ComponentProportional *ComponentProportionalConfig `json:"component_proportional"` 31 | // TODO: 32 | // if some other components will appear in future, put their configs here. 33 | } 34 | 35 | // Prepare - config validator. 36 | func (c *ControllerConfig) Prepare() error { 37 | if c.RSSLimit.Value == 0 { 38 | return errors.New("empty RSSLimit") 39 | } 40 | 41 | if c.DangerZoneGOGC == 0 || c.DangerZoneGOGC > 100 { 42 | return errors.New("invalid DangerZoneGOGC value (must belong to [0; 100])") 43 | } 44 | 45 | if c.DangerZoneThrottling == 0 || c.DangerZoneThrottling > 100 { 46 | return errors.Errorf("invalid DangerZoneThrottling value (must belong to [0; 100])") 47 | } 48 | 49 | if c.Period.Duration == 0 { 50 | return errors.New("empty Period") 51 | } 52 | 53 | if c.ComponentProportional == nil { 54 | return errors.New("empty ComponentProportional") 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // ComponentProportionalConfig - controller's proportional component configuration. 61 | type ComponentProportionalConfig struct { 62 | // Coefficient - coefficient used to computed weighted sum of in the controller equation 63 | Coefficient float64 `json:"coefficient"` 64 | // WindowSize - averaging window size for the EMA. Averaging is disabled if WindowSize is zero. 65 | WindowSize uint `json:"window_size"` 66 | } 67 | 68 | // Prepare - config validator. 69 | func (c *ComponentProportionalConfig) Prepare() error { 70 | if c.Coefficient == 0 { 71 | return errors.New("empty Coefficient makes no sense") 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /controller/nextgc/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 13 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestComponentConfig(t *testing.T) { 18 | t.Run("bad RSS limit", func(t *testing.T) { 19 | c := &ControllerConfig{RSSLimit: bytes.Bytes{Value: 0}} 20 | require.Error(t, c.Prepare()) 21 | }) 22 | 23 | t.Run("bad danger zone GOGC", func(t *testing.T) { 24 | c := &ControllerConfig{ 25 | RSSLimit: bytes.Bytes{Value: 1}, 26 | DangerZoneGOGC: 120, 27 | } 28 | require.Error(t, c.Prepare()) 29 | }) 30 | 31 | t.Run("bad danger zone throttling", func(t *testing.T) { 32 | c := &ControllerConfig{ 33 | RSSLimit: bytes.Bytes{Value: 1}, 34 | DangerZoneGOGC: 50, 35 | DangerZoneThrottling: 120, 36 | } 37 | require.Error(t, c.Prepare()) 38 | }) 39 | 40 | t.Run("bad period", func(t *testing.T) { 41 | c := &ControllerConfig{ 42 | RSSLimit: bytes.Bytes{Value: 1}, 43 | DangerZoneGOGC: 50, 44 | DangerZoneThrottling: 90, 45 | Period: duration.Duration{Duration: 0}, 46 | } 47 | require.Error(t, c.Prepare()) 48 | }) 49 | 50 | t.Run("bad component proportional", func(t *testing.T) { 51 | c := &ControllerConfig{ 52 | RSSLimit: bytes.Bytes{Value: 1}, 53 | DangerZoneGOGC: 50, 54 | DangerZoneThrottling: 90, 55 | Period: duration.Duration{Duration: 1}, 56 | } 57 | require.Error(t, c.Prepare()) 58 | }) 59 | } 60 | 61 | func TestComponentProportionalConfig(t *testing.T) { 62 | t.Run("invalid proportional config", func(t *testing.T) { 63 | c := &ComponentProportionalConfig{ 64 | Coefficient: 0, 65 | } 66 | require.Error(t, c.Prepare()) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /controller/nextgc/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "math" 11 | "time" 12 | 13 | "github.com/go-logr/logr" 14 | "github.com/newcloudtechnologies/memlimiter/stats" 15 | "github.com/newcloudtechnologies/memlimiter/utils/breaker" 16 | "github.com/pkg/errors" 17 | 18 | "github.com/newcloudtechnologies/memlimiter/backpressure" 19 | "github.com/newcloudtechnologies/memlimiter/controller" 20 | memlimiter_utils "github.com/newcloudtechnologies/memlimiter/utils" 21 | ) 22 | 23 | // controllerImpl - in some early versions this class was designed to be used as a classical PID-controller 24 | // described in control theory. But currently it has only proportional (P) component, and the proportionality 25 | // is non-linear (see component_p.go). It looks like integral (I) component will never be implemented. 26 | // But the differential controller (D) still may be implemented in future if we face self-oscillation. 27 | // 28 | //nolint:govet 29 | type controllerImpl struct { 30 | input stats.ServiceStatsSubscription // input: service tracker subscription. 31 | output backpressure.Operator // output: write control parameters here 32 | 33 | // Controller components: 34 | // 1. proportional component. 35 | componentP *componentP 36 | 37 | // cached values, describing the actual state of the controller: 38 | pValue float64 // proportional component's output 39 | sumValue float64 // final output 40 | goAllocLimit uint64 // memory budget [bytes] 41 | utilization float64 // memory budget utilization [percents] 42 | rss uint64 // physical memory actual consumption 43 | consumptionReport *stats.ConsumptionReport // latest special memory consumers report 44 | controlParameters *stats.ControlParameters // latest control parameters value 45 | 46 | getStatsChan chan *getStatsRequest 47 | 48 | cfg *ControllerConfig 49 | logger logr.Logger 50 | breaker *breaker.Breaker 51 | } 52 | 53 | type getStatsRequest struct { 54 | result chan *stats.ControllerStats 55 | } 56 | 57 | func (r *getStatsRequest) respondWith(resp *stats.ControllerStats) { 58 | r.result <- resp 59 | } 60 | 61 | func (c *controllerImpl) GetStats() (*stats.ControllerStats, error) { 62 | req := &getStatsRequest{result: make(chan *stats.ControllerStats, 1)} 63 | 64 | select { 65 | case c.getStatsChan <- req: 66 | case <-c.breaker.Done(): 67 | return nil, errors.Wrap(c.breaker.Err(), "breaker err") 68 | } 69 | 70 | select { 71 | case resp := <-req.result: 72 | return resp, nil 73 | case <-c.breaker.Done(): 74 | return nil, errors.Wrap(c.breaker.Err(), "breaker err") 75 | } 76 | } 77 | 78 | func (c *controllerImpl) loop() { 79 | defer c.breaker.Dec() 80 | 81 | ticker := time.NewTicker(c.cfg.Period.Duration) 82 | defer ticker.Stop() 83 | 84 | for { 85 | select { 86 | case serviceStats := <-c.input.Updates(): 87 | // Update controller state every time we receive the actual tracker about the process. 88 | if err := c.updateState(serviceStats); err != nil { 89 | c.logger.Error(err, "update state") 90 | } 91 | case <-ticker.C: 92 | // Generate control parameters based on the most recent state and send it to the backpressure operator. 93 | if err := c.applyControlValue(); err != nil { 94 | c.logger.Error(err, "apply control value") 95 | } 96 | case req := <-c.getStatsChan: 97 | req.respondWith(c.aggregateStats()) 98 | case <-c.breaker.Done(): 99 | return 100 | } 101 | } 102 | } 103 | 104 | func (c *controllerImpl) updateState(serviceStats stats.ServiceStats) error { 105 | // Extract the latest report on special memory consumers if there are any. 106 | c.consumptionReport = serviceStats.ConsumptionReport() 107 | 108 | c.updateUtilization(serviceStats) 109 | 110 | if err := c.updateControlValues(); err != nil { 111 | return errors.Wrap(err, "update control values") 112 | } 113 | 114 | c.updateControlParameters() 115 | 116 | return nil 117 | } 118 | 119 | func (c *controllerImpl) updateUtilization(serviceStats stats.ServiceStats) { 120 | // The process memory (roughly) consists of two main parts: 121 | // 1. Allocations managed by Go runtime. 122 | // 2. Allocations made beyond CGO border. 123 | // 124 | // We can only affect the Go allocation. CGO allocations are out of the scope. 125 | // To compute the amount of memory available for allocations in Go, 126 | // we subtract known CGO allocations from the common memory bugdet. 127 | // If CGO allocations grow, Go allocation have to shrink. 128 | var cgoAllocs uint64 129 | 130 | if c.consumptionReport != nil { 131 | for _, value := range c.consumptionReport.Cgo { 132 | cgoAllocs += value 133 | } 134 | } 135 | 136 | c.goAllocLimit = c.cfg.RSSLimit.Value - cgoAllocs 137 | 138 | // Memory utilization is defined as the relation of NextGC value to the Go allocation limit. 139 | // If NextGC becomes higher than the allocation limit, the GC will never run, because 140 | // OOM will happen first. That's why we need to push away Go process from the allocation limit. 141 | c.utilization = float64(serviceStats.NextGC()) / float64(c.goAllocLimit) 142 | 143 | // Just for the history, save actual RSS value 144 | c.rss = serviceStats.RSS() 145 | } 146 | 147 | func (c *controllerImpl) updateControlValues() error { 148 | var err error 149 | 150 | c.pValue, err = c.componentP.value(c.utilization) 151 | if err != nil { 152 | return errors.Wrap(err, "component proportional value") 153 | } 154 | 155 | // TODO: if new components appear, summarize their outputs here: 156 | c.sumValue = c.pValue 157 | 158 | // Saturate controller output so that the control parameters are not too radical. 159 | // Details: 160 | // https://en.wikipedia.org/wiki/Saturation_arithmetic 161 | // https://habr.com/ru/post/345972/ 162 | const ( 163 | lowerBound = 0 164 | upperBound = 99 // this otherwise GOGC will turn to zero 165 | ) 166 | 167 | c.sumValue = memlimiter_utils.ClampFloat64(c.sumValue, lowerBound, upperBound) 168 | 169 | return nil 170 | } 171 | 172 | func (c *controllerImpl) updateControlParameters() { 173 | c.controlParameters = &stats.ControlParameters{} 174 | c.updateControlParameterGOGC() 175 | c.updateControlParameterThrottling() 176 | 177 | c.controlParameters.ControllerStats = c.aggregateStats() 178 | } 179 | 180 | const percents = 100 181 | 182 | func (c *controllerImpl) updateControlParameterGOGC() { 183 | // Control parameters are set to defaults in the "green zone". 184 | if uint32(c.utilization*percents) < c.cfg.DangerZoneGOGC { 185 | c.controlParameters.GOGC = backpressure.DefaultGOGC 186 | 187 | return 188 | } 189 | 190 | // Control parameters are more conservative in the "red zone". 191 | roundedValue := uint32(math.Round(c.sumValue)) 192 | c.controlParameters.GOGC = int(backpressure.DefaultGOGC - roundedValue) 193 | } 194 | 195 | func (c *controllerImpl) updateControlParameterThrottling() { 196 | // Disable throttling in the "green zone". 197 | if uint32(c.utilization*percents) < c.cfg.DangerZoneThrottling { 198 | c.controlParameters.ThrottlingPercentage = backpressure.NoThrottling 199 | 200 | return 201 | } 202 | 203 | // Control parameters are more conservative in the "red zone". 204 | roundedValue := uint32(math.Round(c.sumValue)) 205 | c.controlParameters.ThrottlingPercentage = roundedValue 206 | } 207 | 208 | func (c *controllerImpl) applyControlValue() error { 209 | if err := c.output.SetControlParameters(c.controlParameters); err != nil { 210 | return errors.Wrapf(err, "set control parameters: %v", c.controlParameters) 211 | } 212 | 213 | return nil 214 | } 215 | 216 | func (c *controllerImpl) aggregateStats() *stats.ControllerStats { 217 | res := &stats.ControllerStats{ 218 | MemoryBudget: &stats.MemoryBudgetStats{ 219 | RSSActual: c.rss, 220 | RSSLimit: c.cfg.RSSLimit.Value, 221 | GoAllocLimit: c.goAllocLimit, 222 | Utilization: c.utilization, 223 | }, 224 | NextGC: &stats.ControllerNextGCStats{ 225 | P: c.pValue, 226 | Output: c.sumValue, 227 | }, 228 | } 229 | 230 | if c.consumptionReport != nil { 231 | res.MemoryBudget.SpecialConsumers = &stats.SpecialConsumersStats{} 232 | res.MemoryBudget.SpecialConsumers.Go = c.consumptionReport.Go 233 | res.MemoryBudget.SpecialConsumers.Cgo = c.consumptionReport.Cgo 234 | } 235 | 236 | return res 237 | } 238 | 239 | // Quit gracefully stops the controller. 240 | func (c *controllerImpl) Quit() { 241 | c.breaker.ShutdownAndWait() 242 | } 243 | 244 | // NewControllerFromConfig builds new controller. 245 | func NewControllerFromConfig( 246 | logger logr.Logger, 247 | cfg *ControllerConfig, 248 | serviceStatsSubscription stats.ServiceStatsSubscription, 249 | backpressureOperator backpressure.Operator, 250 | ) (controller.Controller, error) { 251 | c := &controllerImpl{ 252 | input: serviceStatsSubscription, 253 | output: backpressureOperator, 254 | componentP: newComponentP(logger, cfg.ComponentProportional), 255 | pValue: 0, 256 | sumValue: 0, 257 | controlParameters: &stats.ControlParameters{ 258 | GOGC: backpressure.DefaultGOGC, 259 | ThrottlingPercentage: backpressure.NoThrottling, 260 | }, 261 | getStatsChan: make(chan *getStatsRequest), 262 | cfg: cfg, 263 | logger: logger, 264 | breaker: breaker.NewBreakerWithInitValue(1), 265 | } 266 | 267 | // initialize backpressure operator with default control signal 268 | if err := c.applyControlValue(); err != nil { 269 | return nil, errors.Wrap(err, "apply control value") 270 | } 271 | 272 | go c.loop() 273 | 274 | return c, nil 275 | } 276 | -------------------------------------------------------------------------------- /controller/nextgc/controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package nextgc 8 | 9 | import ( 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | 14 | "code.cloudfoundry.org/bytefmt" 15 | "github.com/go-logr/logr/testr" 16 | "github.com/newcloudtechnologies/memlimiter/stats" 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/newcloudtechnologies/memlimiter/backpressure" 20 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 21 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 22 | "github.com/stretchr/testify/mock" 23 | ) 24 | 25 | func TestController(t *testing.T) { 26 | logger := testr.New(t) 27 | 28 | const servusPeriod = 100 * time.Millisecond 29 | 30 | controllerPeriod := 2 * servusPeriod 31 | 32 | cfg := &ControllerConfig{ 33 | // We cannot exceed 1000M RSS threshold 34 | RSSLimit: bytes.Bytes{Value: 1000 * bytefmt.MEGABYTE}, 35 | // When memory budget utilization reaches 50%, the controller will start GOGC altering. 36 | DangerZoneGOGC: 50, 37 | // When memory budget utilization reaches 90%, the controller will start request throttling. 38 | DangerZoneThrottling: 90, 39 | Period: duration.Duration{Duration: controllerPeriod}, 40 | ComponentProportional: &ComponentProportionalConfig{ 41 | Coefficient: 1, 42 | WindowSize: 0, // just for simplicity disable the smoothing 43 | }, 44 | } 45 | 46 | // First ServiceStats instance describes the situation, when the memory budget utilization 47 | // is very close to the limits. 48 | memoryBudgetExhausted := &stats.ServiceStatsMock{} 49 | memoryBudgetExhausted.On("NextGC").Return(uint64(950 * bytefmt.MEGABYTE)) 50 | memoryBudgetExhausted.On("RSS").Return(uint64(900 * bytefmt.MEGABYTE)) 51 | 52 | cr1 := &stats.ConsumptionReport{ 53 | Cgo: map[string]uint64{"some_important_cache": 5 * bytefmt.MEGABYTE}, 54 | } 55 | memoryBudgetExhausted.On("ConsumptionReport").Return(cr1) 56 | 57 | // In the second case the memory budget utilization returns to the ordinary values. 58 | memoryBudgetNormal := &stats.ServiceStatsMock{} 59 | memoryBudgetNormal.On("NextGC").Return(uint64(300 * bytefmt.MEGABYTE)) 60 | memoryBudgetNormal.On("RSS").Return(uint64(500 * bytefmt.MEGABYTE)) 61 | 62 | cr2 := &stats.ConsumptionReport{ 63 | Cgo: map[string]uint64{"some_important_cache": 1 * bytefmt.MEGABYTE}, 64 | } 65 | memoryBudgetNormal.On("ConsumptionReport").Return(cr2) 66 | 67 | subscriptionMock := &stats.ServiceStatsSubscriptionMock{ 68 | Chan: make(chan stats.ServiceStats), 69 | } 70 | 71 | // this channel is closed when backpressure.Operator receives all required actions 72 | terminateChan := make(chan struct{}) 73 | 74 | var serviceStatsContainer atomic.Value 75 | 76 | // The stream of tracker.ServiceStats instances 77 | go func() { 78 | ticker := time.NewTicker(servusPeriod) 79 | 80 | for { 81 | select { 82 | case <-ticker.C: 83 | serviceStats, ok := serviceStatsContainer.Load().(stats.ServiceStats) 84 | if ok { 85 | subscriptionMock.Chan <- serviceStats 86 | } 87 | case <-terminateChan: 88 | return 89 | } 90 | } 91 | }() 92 | 93 | backpressureOperatorMock := &backpressure.OperatorMock{} 94 | 95 | // first initialization within NewController constructor 96 | backpressureOperatorMock.On( 97 | "SetControlParameters", 98 | &stats.ControlParameters{ 99 | GOGC: backpressure.DefaultGOGC, 100 | ThrottlingPercentage: backpressure.NoThrottling, 101 | }, 102 | ).Return(nil).Once() 103 | 104 | // Here we model the situation of memory exhaustion. 105 | serviceStatsContainer.Store(memoryBudgetExhausted) 106 | 107 | backpressureOperatorMock.On( 108 | "SetControlParameters", 109 | mock.MatchedBy(func(val *stats.ControlParameters) bool { 110 | return val.GOGC == 78 && val.ThrottlingPercentage == 22 111 | }), 112 | ).Return(nil).Once().Run( 113 | func(args mock.Arguments) { 114 | // As soon as the control signal is delivered to the backpressure.Operator, 115 | // replace the ServiceStats instance to make controller think that memory 116 | // consumption returned to normal. 117 | serviceStatsContainer.Store(memoryBudgetNormal) 118 | }, 119 | ).On( 120 | "SetControlParameters", 121 | mock.MatchedBy(func(val *stats.ControlParameters) bool { 122 | return val.GOGC == backpressure.DefaultGOGC && val.ThrottlingPercentage == backpressure.NoThrottling 123 | }), 124 | ).Return(nil).Once().Run( 125 | func(args mock.Arguments) { 126 | close(terminateChan) 127 | }, 128 | ) 129 | 130 | c, err := NewControllerFromConfig(logger, cfg, subscriptionMock, backpressureOperatorMock) 131 | require.NoError(t, err) 132 | 133 | <-terminateChan 134 | 135 | c.Quit() 136 | 137 | mock.AssertExpectationsForObjects(t, subscriptionMock, backpressureOperatorMock) 138 | } 139 | -------------------------------------------------------------------------------- /controller/nextgc/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package nextgc provides the implementation of memory usage controller, which aims 8 | // to keep Go Runtime NextGC value lower than the RSS consumption hard limit to prevent OOM errors. 9 | package nextgc 10 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package memlimiter - memory budget control subsystem for Go services. 8 | // It tracks memory budget utilization and tries to stabilize memory usage with 9 | // backpressure (GC and request throttling) techniques. 10 | package memlimiter 11 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcloudtechnologies/memlimiter/e06a8d27b30b6114fcdf755db81531da2e5484f6/docs/architecture.png -------------------------------------------------------------------------------- /docs/control_params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcloudtechnologies/memlimiter/e06a8d27b30b6114fcdf755db81531da2e5484f6/docs/control_params.png -------------------------------------------------------------------------------- /docs/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcloudtechnologies/memlimiter/e06a8d27b30b6114fcdf755db81531da2e5484f6/docs/rss.png -------------------------------------------------------------------------------- /docs/rss_hl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcloudtechnologies/memlimiter/e06a8d27b30b6114fcdf755db81531da2e5484f6/docs/rss_hl.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/newcloudtechnologies/memlimiter 2 | 3 | go 1.17 4 | 5 | require ( 6 | code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5 7 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 8 | github.com/go-logr/logr v1.2.3 9 | github.com/go-logr/stdr v1.2.2 10 | github.com/golang/protobuf v1.5.2 11 | github.com/pkg/errors v0.9.1 12 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 13 | github.com/shirou/gopsutil/v3 v3.22.5 14 | github.com/stretchr/testify v1.7.1 15 | github.com/urfave/cli/v2 v2.8.1 16 | github.com/villenny/fastrand64-go v0.0.0-20201008161821-3d8fa521c558 17 | golang.org/x/time v0.0.0-20220411224347-583f2d630306 18 | google.golang.org/grpc v1.38.0 19 | google.golang.org/protobuf v1.26.0 20 | ) 21 | 22 | require ( 23 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/go-ole/go-ole v1.2.6 // indirect 26 | github.com/kr/pretty v0.3.0 // indirect 27 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/stretchr/objx v0.2.0 // indirect 32 | github.com/tklauser/go-sysconf v0.3.10 // indirect 33 | github.com/tklauser/numcpus v0.4.0 // indirect 34 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 35 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 36 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect 37 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf // indirect 38 | golang.org/x/text v0.3.7 // indirect 39 | google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d // indirect 40 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 41 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5 h1:tM5+dn2C9xZw1RzgI6WTQW1rGqdUimKB3RFbyu4h6Hc= 3 | code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5/go.mod h1:v4VVB6oBMz/c9fRY6vZrwr5xKRWOH5NPDjQZlPk0Gbs= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 6 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= 7 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 10 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 18 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 19 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 20 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 23 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 24 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 25 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 26 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 27 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 28 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 29 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 30 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 31 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 32 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 33 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 34 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 37 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 38 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 39 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 40 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 41 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 42 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 43 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 44 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 45 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 46 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 47 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 48 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 54 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 55 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 56 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 57 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 59 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 60 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 61 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 62 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 63 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 64 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 65 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 66 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 67 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 68 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 69 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 70 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 71 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 72 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 73 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 74 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 75 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 76 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 77 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 78 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 79 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 83 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 84 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 85 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 86 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 87 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 88 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 89 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 90 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 91 | github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA= 92 | github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA= 93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 95 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 96 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 97 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 98 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 99 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 100 | github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= 101 | github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= 102 | github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= 103 | github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= 104 | github.com/tsuna/endian v0.0.0-20151020052604-29b3a4178852 h1:/HMzghBx/U8ZTQ+CCKRAsjeNNV12OCG3PfJcthNMBU0= 105 | github.com/tsuna/endian v0.0.0-20151020052604-29b3a4178852/go.mod h1:7SvkOZYNBtjd5XUi2fuPMvAZS8rlCMaU69hj/3joIsE= 106 | github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4= 107 | github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= 108 | github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI= 109 | github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 110 | github.com/villenny/fastrand64-go v0.0.0-20201008161821-3d8fa521c558 h1:oNwFCUPi4ns2fMuaBtzMdQImdt25neDPJPBTNprmdF8= 111 | github.com/villenny/fastrand64-go v0.0.0-20201008161821-3d8fa521c558/go.mod h1:0KogUQQf0cFYfgnOpYJqw1RnSb4S1oJwUb1CEpGJLJ4= 112 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 113 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 114 | github.com/yalue/native_endian v0.0.0-20180607135909-51013b03be4f h1:nsQCScpQ8RRf+wIooqfyyEUINV2cAPuo2uVtHSBbA4M= 115 | github.com/yalue/native_endian v0.0.0-20180607135909-51013b03be4f/go.mod h1:1cm5YQZdnDQBZVtFG2Ip8sFVN0eYZ8OFkCT2kIVl9mw= 116 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 117 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 118 | github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= 119 | github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 120 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 122 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 123 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 124 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 125 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 126 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 127 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 128 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 129 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 130 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 131 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 133 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 135 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 136 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 138 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 139 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 140 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 141 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 142 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= 143 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 144 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 145 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 146 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 147 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 148 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 149 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 151 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 166 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 167 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf h1:Fm4IcnUL803i92qDlmB0obyHmosDrxZWxJL3gIeNqOw= 168 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 170 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 171 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 172 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 173 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 174 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 175 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 176 | golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= 177 | golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 178 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 179 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 180 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 181 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 182 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 183 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 184 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 185 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 186 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 187 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 192 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 193 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 194 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 195 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 196 | google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d h1:KzwjikDymrEmYYbdyfievTwjEeGlu+OM6oiKBkF3Jfg= 197 | google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 198 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 199 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 200 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 201 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 202 | google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= 203 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 204 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 205 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 206 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 207 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 208 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 209 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 210 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 211 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 212 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 213 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 214 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 215 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 216 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 217 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 219 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 220 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 221 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 222 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 223 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 224 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 225 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 226 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 227 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 228 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 229 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 230 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 231 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 232 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 233 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 234 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/middleware" 11 | "github.com/newcloudtechnologies/memlimiter/stats" 12 | ) 13 | 14 | // Service - a high-level interface for a memory usage control subsystem. 15 | type Service interface { 16 | Middleware() middleware.Middleware 17 | GetStats() (*stats.MemLimiterStats, error) 18 | // Quit terminates service gracefully. 19 | Quit() 20 | } 21 | -------------------------------------------------------------------------------- /middleware/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package middleware provides code that helps to integrate MemLimiter's backpressure subsystem 8 | // with modern web frameworks. 9 | package middleware 10 | -------------------------------------------------------------------------------- /middleware/grpc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package middleware 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/go-logr/logr" 13 | "github.com/newcloudtechnologies/memlimiter/backpressure" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/codes" 16 | "google.golang.org/grpc/status" 17 | ) 18 | 19 | // GRPC provides server-side interceptors that must be used 20 | // at the time of GRPC server construction. 21 | type GRPC interface { 22 | // MakeUnaryServerInterceptor returns unary server interceptor. 23 | MakeUnaryServerInterceptor() grpc.UnaryServerInterceptor 24 | // MakeStreamServerInterceptor returns stream server interceptor. 25 | MakeStreamServerInterceptor() grpc.StreamServerInterceptor 26 | } 27 | 28 | type grpcImpl struct { 29 | backpressureOperator backpressure.Operator 30 | logger logr.Logger 31 | } 32 | 33 | func (g *grpcImpl) MakeUnaryServerInterceptor() grpc.UnaryServerInterceptor { 34 | return func( 35 | ctx context.Context, 36 | req interface{}, 37 | info *grpc.UnaryServerInfo, 38 | handler grpc.UnaryHandler, 39 | ) (interface{}, error) { 40 | allowed := g.backpressureOperator.AllowRequest() 41 | if allowed { 42 | return handler(ctx, req) 43 | } 44 | 45 | logger, err := logr.FromContext(ctx) 46 | if err != nil { 47 | logger = g.logger 48 | } 49 | 50 | logger.Info("request has been throttled") 51 | 52 | return nil, status.Error(codes.ResourceExhausted, "request has been throttled") 53 | } 54 | } 55 | 56 | func (g *grpcImpl) MakeStreamServerInterceptor() grpc.StreamServerInterceptor { 57 | return func( 58 | srv interface{}, 59 | ss grpc.ServerStream, 60 | info *grpc.StreamServerInfo, 61 | handler grpc.StreamHandler, 62 | ) error { 63 | allowed := g.backpressureOperator.AllowRequest() 64 | if allowed { 65 | return handler(srv, ss) 66 | } 67 | 68 | logger, err := logr.FromContext(ss.Context()) 69 | if err != nil { 70 | logger = g.logger 71 | } 72 | 73 | logger.Info("request has been throttled") 74 | 75 | return status.Error(codes.ResourceExhausted, "request has been throttled") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package middleware 8 | 9 | import ( 10 | "github.com/go-logr/logr" 11 | "github.com/newcloudtechnologies/memlimiter/backpressure" 12 | ) 13 | 14 | // Middleware - extendable type responsible for MemLimiter integration with 15 | // various web and microservice frameworks. 16 | type Middleware interface { 17 | GRPC() GRPC 18 | // TODO: add new frameworks here 19 | } 20 | 21 | type middlewareImpl struct { 22 | backpressureOperator backpressure.Operator 23 | logger logr.Logger 24 | } 25 | 26 | func (m *middlewareImpl) GRPC() GRPC { 27 | return &grpcImpl{ 28 | logger: m.logger, 29 | backpressureOperator: m.backpressureOperator, 30 | } 31 | } 32 | 33 | // NewMiddleware creates new middleware instance. 34 | func NewMiddleware(logger logr.Logger, operator backpressure.Operator) Middleware { 35 | return &middlewareImpl{ 36 | logger: logger, 37 | backpressureOperator: operator, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/backpressure" 11 | "github.com/newcloudtechnologies/memlimiter/stats" 12 | ) 13 | 14 | // Option - MemLimiter constructor options. 15 | type Option interface { 16 | anchor() 17 | } 18 | 19 | type backpressureOperatorOption struct { 20 | val backpressure.Operator 21 | } 22 | 23 | func (b *backpressureOperatorOption) anchor() {} 24 | 25 | // WithBackpressureOperator allows client to provide customized backpressure.Operator; 26 | // that's especially useful when implementing backpressure logic on the application side. 27 | func WithBackpressureOperator(val backpressure.Operator) Option { 28 | return &backpressureOperatorOption{val: val} 29 | } 30 | 31 | type serviceStatsSubscriptionOption struct { 32 | val stats.ServiceStatsSubscription 33 | } 34 | 35 | func (s serviceStatsSubscriptionOption) anchor() { 36 | } 37 | 38 | // WithServiceStatsSubscription allows client to provide own implementation of service stats subscription. 39 | func WithServiceStatsSubscription(val stats.ServiceStatsSubscription) Option { 40 | return &serviceStatsSubscriptionOption{val: val} 41 | } 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2022.5.18.1 2 | charset-normalizer==2.0.12 3 | cycler==0.11.0 4 | docker==5.0.3 5 | fonttools==4.33.3 6 | humanize==4.1.0 7 | idna==3.3 8 | Jinja2==3.1.2 9 | kiwisolver==1.4.3 10 | MarkupSafe==2.1.1 11 | matplotlib==3.5.2 12 | numpy==1.22.4 13 | packaging==21.3 14 | pandas==1.4.2 15 | Pillow==9.1.1 16 | pyparsing==3.0.9 17 | python-dateutil==2.8.2 18 | pytz==2022.1 19 | requests==2.28.0 20 | six==1.16.0 21 | urllib3==1.26.9 22 | websocket-client==1.3.2 23 | -------------------------------------------------------------------------------- /service_impl.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "github.com/go-logr/logr" 11 | "github.com/newcloudtechnologies/memlimiter/backpressure" 12 | "github.com/newcloudtechnologies/memlimiter/controller" 13 | "github.com/newcloudtechnologies/memlimiter/controller/nextgc" 14 | "github.com/newcloudtechnologies/memlimiter/middleware" 15 | "github.com/newcloudtechnologies/memlimiter/stats" 16 | "github.com/newcloudtechnologies/memlimiter/utils/config/prepare" 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | var _ Service = (*serviceImpl)(nil) 21 | 22 | type serviceImpl struct { 23 | middleware middleware.Middleware 24 | backpressureOperator backpressure.Operator 25 | statsSubscription stats.ServiceStatsSubscription 26 | controller controller.Controller 27 | logger logr.Logger 28 | } 29 | 30 | func (s *serviceImpl) Middleware() middleware.Middleware { return s.middleware } 31 | 32 | func (s *serviceImpl) GetStats() (*stats.MemLimiterStats, error) { 33 | controllerStats, err := s.controller.GetStats() 34 | if err != nil { 35 | return nil, errors.Wrap(err, "controller tracker") 36 | } 37 | 38 | backpressureStats, err := s.backpressureOperator.GetStats() 39 | if err != nil { 40 | return nil, errors.Wrap(err, "backpressure tracker") 41 | } 42 | 43 | return &stats.MemLimiterStats{ 44 | Controller: controllerStats, 45 | Backpressure: backpressureStats, 46 | }, nil 47 | } 48 | 49 | func (s *serviceImpl) Quit() { 50 | s.logger.Info("terminating MemLimiter service") 51 | s.controller.Quit() 52 | s.statsSubscription.Quit() 53 | } 54 | 55 | // newServiceImpl - main entrypoint for MemLimiter. 56 | func newServiceImpl( 57 | logger logr.Logger, 58 | cfg *Config, 59 | statsSubscription stats.ServiceStatsSubscription, 60 | backpressureOperator backpressure.Operator, 61 | ) (Service, error) { 62 | if err := prepare.Prepare(cfg); err != nil { 63 | return nil, errors.Wrap(err, "prepare config") 64 | } 65 | 66 | if statsSubscription == nil { 67 | return nil, errors.New("nil tracker subscription passed") 68 | } 69 | 70 | logger.Info("starting MemLimiter service") 71 | 72 | c, err := nextgc.NewControllerFromConfig( 73 | logger, 74 | cfg.ControllerNextGC, 75 | statsSubscription, 76 | backpressureOperator, 77 | ) 78 | 79 | if err != nil { 80 | return nil, errors.Wrap(err, "new controller from config") 81 | } 82 | 83 | return &serviceImpl{ 84 | middleware: middleware.NewMiddleware(logger, backpressureOperator), 85 | backpressureOperator: backpressureOperator, 86 | statsSubscription: statsSubscription, 87 | controller: c, 88 | logger: logger, 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /service_stub.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package memlimiter 8 | 9 | import ( 10 | "sync/atomic" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/middleware" 13 | "github.com/newcloudtechnologies/memlimiter/stats" 14 | "github.com/newcloudtechnologies/memlimiter/utils/breaker" 15 | ) 16 | 17 | var _ Service = (*serviceStub)(nil) 18 | 19 | // serviceStub doesn't perform active memory management, it just caches the latest statistics. 20 | type serviceStub struct { 21 | latestStats atomic.Value 22 | statsSubscription stats.ServiceStatsSubscription 23 | breaker *breaker.Breaker 24 | } 25 | 26 | func (s *serviceStub) loop() { 27 | defer s.breaker.Dec() 28 | 29 | for { 30 | select { 31 | case record := <-s.statsSubscription.Updates(): 32 | s.latestStats.Store(record) 33 | case <-s.breaker.Done(): 34 | return 35 | } 36 | } 37 | } 38 | 39 | func (s *serviceStub) Middleware() middleware.Middleware { 40 | // TODO: return stub 41 | return nil 42 | } 43 | 44 | func (s *serviceStub) GetStats() (*stats.MemLimiterStats, error) { 45 | if val := s.latestStats.Load(); val != nil { 46 | //nolint:forcetypeassert 47 | ss := val.(stats.ServiceStats) 48 | 49 | out := &stats.MemLimiterStats{ 50 | Controller: &stats.ControllerStats{ 51 | MemoryBudget: &stats.MemoryBudgetStats{ 52 | RSSActual: ss.RSS(), 53 | }, 54 | }, 55 | } 56 | 57 | return out, nil 58 | } 59 | 60 | return nil, nil 61 | } 62 | 63 | func (s *serviceStub) Quit() { 64 | s.breaker.Shutdown() 65 | s.statsSubscription.Quit() 66 | } 67 | 68 | func newServiceStub(statsSubscription stats.ServiceStatsSubscription) Service { 69 | if statsSubscription == nil { 70 | return &serviceStub{ 71 | breaker: breaker.NewBreaker(), 72 | } 73 | } 74 | 75 | out := &serviceStub{ 76 | statsSubscription: statsSubscription, 77 | breaker: breaker.NewBreakerWithInitValue(1), 78 | } 79 | 80 | go out.loop() 81 | 82 | return out 83 | } 84 | -------------------------------------------------------------------------------- /stats/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package stats contains various data types describing service statistics 8 | // MemLimiter relies on, as well as its own statistics. 9 | package stats 10 | -------------------------------------------------------------------------------- /stats/memlimiter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package stats 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | // MemLimiterStats - top-level MemLimiter statistics data type. 14 | type MemLimiterStats struct { 15 | // ControllerStats - memory budget controller statistics 16 | Controller *ControllerStats 17 | // Backpressure - backpressure subsystem statistics 18 | Backpressure *BackpressureStats 19 | } 20 | 21 | // ControllerStats - memory budget controller tracker. 22 | type ControllerStats struct { 23 | // MemoryBudget - common memory budget information 24 | MemoryBudget *MemoryBudgetStats 25 | // NextGC - NextGC-aware controller statistics 26 | NextGC *ControllerNextGCStats 27 | } 28 | 29 | // MemoryBudgetStats - memory budget tracker. 30 | type MemoryBudgetStats struct { 31 | // SpecialConsumers - specialized memory consumers (like CGO) statistics. 32 | SpecialConsumers *SpecialConsumersStats 33 | // RSSActual - physical memory (RSS) current consumption [bytes]. 34 | RSSActual uint64 35 | // RSSLimit - physical memory (RSS) consumption limit [bytes]. 36 | RSSLimit uint64 37 | // GoAllocLimit - allocation limit for Go Runtime (with the except of CGO) [bytes]. 38 | GoAllocLimit uint64 39 | // Utilization - memory budget utilization [percents] 40 | // (definition depends on a particular controller implementation). 41 | Utilization float64 42 | } 43 | 44 | // SpecialConsumersStats - specialized memory consumers statistics. 45 | type SpecialConsumersStats struct { 46 | // Go - Go runtime managed consumers. 47 | Go map[string]uint64 48 | // Cgo - consumers residing beyond the Cgo border. 49 | Cgo map[string]uint64 50 | } 51 | 52 | // ControllerNextGCStats - NextGC-aware controller statistics. 53 | type ControllerNextGCStats struct { 54 | // P - proportional component's output 55 | P float64 56 | // Output - final output 57 | Output float64 58 | } 59 | 60 | // BackpressureStats - backpressure subsystem statistics. 61 | type BackpressureStats struct { 62 | // Throttling - throttling subsystem statistics. 63 | Throttling *ThrottlingStats 64 | // ControlParameters - control signal received from controller. 65 | ControlParameters *ControlParameters 66 | } 67 | 68 | // ThrottlingStats - throttling subsystem statistics. 69 | type ThrottlingStats struct { 70 | // Passed - number of allowed requests. 71 | Passed uint64 72 | // Throttled - number of throttled requests. 73 | Throttled uint64 74 | // Total - total number of received requests (Passed + Throttled) 75 | Total uint64 76 | } 77 | 78 | // ControlParameters - вектор управляющих сигналов для системы. 79 | type ControlParameters struct { 80 | // ControllerStats - internal telemetry that may be useful for 81 | // implementation of application-specific backpressure actors. 82 | ControllerStats *ControllerStats 83 | // GOGC - value that will be used as a parameter for debug.SetGCPercent 84 | GOGC int 85 | // ThrottlingPercentage - percentage of requests that must be throttled on the middleware level (in range [0; 100]) 86 | ThrottlingPercentage uint32 87 | } 88 | 89 | func (cp *ControlParameters) String() string { 90 | return fmt.Sprintf("gogc = %v, throttling_percentage = %v", cp.GOGC, cp.ThrottlingPercentage) 91 | } 92 | 93 | // ToKeysAndValues serializes struct for use in logr.Logger. 94 | func (cp *ControlParameters) ToKeysAndValues() []interface{} { 95 | return []interface{}{ 96 | "gogc", cp.GOGC, 97 | "throttling_percentage", cp.ThrottlingPercentage, 98 | } 99 | } 100 | 101 | // EqualsTo - comparator. 102 | func (cp *ControlParameters) EqualsTo(other *ControlParameters) bool { 103 | return cp.GOGC == other.GOGC && cp.ThrottlingPercentage == other.ThrottlingPercentage 104 | } 105 | -------------------------------------------------------------------------------- /stats/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package stats 8 | 9 | import ( 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | var _ ServiceStats = (*ServiceStatsMock)(nil) 14 | 15 | // ServiceStatsMock mocks ServiceStatsSubscription. 16 | type ServiceStatsMock struct { 17 | mock.Mock 18 | } 19 | 20 | func (m *ServiceStatsMock) RSS() uint64 { 21 | return m.Called().Get(0).(uint64) 22 | } 23 | 24 | func (m *ServiceStatsMock) NextGC() uint64 { 25 | return m.Called().Get(0).(uint64) 26 | } 27 | 28 | func (m *ServiceStatsMock) ConsumptionReport() *ConsumptionReport { 29 | args := m.Called() 30 | 31 | return args.Get(0).(*ConsumptionReport) 32 | } 33 | 34 | var _ ServiceStatsSubscription = (*ServiceStatsSubscriptionMock)(nil) 35 | 36 | // ServiceStatsSubscriptionMock mocks ServiceStatsSubscription. 37 | type ServiceStatsSubscriptionMock struct { 38 | ServiceStatsSubscription 39 | Chan chan ServiceStats 40 | mock.Mock 41 | } 42 | 43 | func (m *ServiceStatsSubscriptionMock) Updates() <-chan ServiceStats { 44 | return m.Chan 45 | } 46 | -------------------------------------------------------------------------------- /stats/service.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package stats 8 | 9 | // ServiceStats represents the actual process statistics. 10 | type ServiceStats interface { 11 | // RSS returns current RSS value [bytes] 12 | RSS() uint64 13 | // NextGC returns current NextGC value [bytes] 14 | NextGC() uint64 15 | // ConsumptionReport provides statistical information about the predefined memory consumers that contribute 16 | // significant part in process' overall memory consumption (caches, memory pools and other large structures). 17 | // It's mandatory to fill this report if you have large caches on Go side or if you allocate a lot beyond Cgo borders. 18 | // But if your service is simple, feel free to return nil. 19 | ConsumptionReport() *ConsumptionReport 20 | } 21 | 22 | // ConsumptionReport - report on memory consumption contributed by predefined data structures living during the 23 | // whole application life-time (caches, memory pools and other large structures). 24 | type ConsumptionReport struct { 25 | // Go - memory consumption contributed by structures managed by Go allocator. 26 | // [key - arbitrary string, value - bytes] 27 | Go map[string]uint64 28 | // Cgo - memory consumption contributed by structures managed by Cgo allocator. 29 | // [key - arbitrary string, value - bytes] 30 | Cgo map[string]uint64 31 | } 32 | 33 | var _ ServiceStats = (*serviceStatsDefault)(nil) 34 | 35 | type serviceStatsDefault struct { 36 | rss uint64 37 | nextGC uint64 38 | } 39 | 40 | func (s serviceStatsDefault) RSS() uint64 { return s.rss } 41 | 42 | func (s serviceStatsDefault) NextGC() uint64 { return s.nextGC } 43 | 44 | func (s serviceStatsDefault) ConsumptionReport() *ConsumptionReport { 45 | // don't forget to put real report of your service's memory consumption in your own implementation 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /stats/subscription.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package stats 8 | 9 | import ( 10 | "os" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/go-logr/logr" 15 | "github.com/newcloudtechnologies/memlimiter/utils/breaker" 16 | "github.com/pkg/errors" 17 | "github.com/shirou/gopsutil/v3/process" 18 | ) 19 | 20 | // ServiceStatsSubscription - service tracker subscription interface. 21 | // There is a default implementation, but if you use Cgo in your application, 22 | // it's strongly recommended to implement this interface on your own, because 23 | // you need to provide custom tracker containing information on Cgo memory consumption. 24 | type ServiceStatsSubscription interface { 25 | // Updates returns outgoing stream of service tracker. 26 | Updates() <-chan ServiceStats 27 | // Quit terminates program. 28 | Quit() 29 | } 30 | 31 | type subscriptionDefault struct { 32 | outChan chan ServiceStats 33 | breaker *breaker.Breaker 34 | logger logr.Logger 35 | period time.Duration 36 | pid int32 37 | } 38 | 39 | func (s *subscriptionDefault) Updates() <-chan ServiceStats { return s.outChan } 40 | 41 | func (s *subscriptionDefault) Quit() { 42 | s.breaker.ShutdownAndWait() 43 | } 44 | 45 | func (s *subscriptionDefault) makeServiceStats() (ServiceStats, error) { 46 | ms := &runtime.MemStats{} 47 | runtime.ReadMemStats(ms) 48 | 49 | pr, err := process.NewProcess(s.pid) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "new pr") 52 | } 53 | 54 | processMemoryInfo, err := pr.MemoryInfoEx() 55 | if err != nil { 56 | return nil, errors.Wrap(err, "process memory info ex") 57 | } 58 | 59 | return serviceStatsDefault{ 60 | rss: processMemoryInfo.RSS, 61 | nextGC: ms.NextGC, 62 | }, nil 63 | } 64 | 65 | // NewSubscriptionDefault - default implementation of service tracker subscription. 66 | func NewSubscriptionDefault(logger logr.Logger, period time.Duration) ServiceStatsSubscription { 67 | ss := &subscriptionDefault{ 68 | outChan: make(chan ServiceStats), 69 | period: period, 70 | breaker: breaker.NewBreakerWithInitValue(1), 71 | pid: int32(os.Getpid()), 72 | logger: logger, 73 | } 74 | 75 | go func() { 76 | ticker := time.NewTicker(period) 77 | defer ticker.Stop() 78 | 79 | defer ss.breaker.Dec() 80 | 81 | for { 82 | select { 83 | case <-ticker.C: 84 | out, err := ss.makeServiceStats() 85 | if err != nil { 86 | logger.Error(err, "make service stats") 87 | 88 | break 89 | } 90 | 91 | select { 92 | case ss.outChan <- out: 93 | case <-ss.breaker.Done(): 94 | return 95 | } 96 | case <-ss.breaker.Done(): 97 | return 98 | } 99 | } 100 | }() 101 | 102 | return ss 103 | } 104 | -------------------------------------------------------------------------------- /test/allocator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:36 2 | 3 | ADD allocator /usr/local/bin 4 | 5 | CMD /usr/local/bin/allocator server -c /etc/allocator/server_config.json -------------------------------------------------------------------------------- /test/allocator/analyze/compare.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 2 | # Author: Vitaly Isaev 3 | # License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 4 | import os 5 | from datetime import datetime 6 | from pathlib import Path 7 | from typing import Final 8 | 9 | import docker 10 | import jinja2 11 | 12 | import render 13 | from report import Report 14 | from testing import Session, make_sessions, Params 15 | 16 | image_tag: Final = 'allocator' 17 | dockerfile_path: Final = 'test/allocator' 18 | container_name: Final = 'allocator' 19 | 20 | 21 | class PerfConfigRenderer: 22 | __t: Final = ''' 23 | { 24 | "endpoint": "localhost:1988", 25 | "rps": 100, 26 | "load_duration": "{{ load_duration }}", 27 | "allocation_size": "1M", 28 | "pause_duration": "5s", 29 | "request_timeout": "1m" 30 | } 31 | ''' 32 | __template: jinja2.Template 33 | 34 | def __init__(self): 35 | self.__template = jinja2.Template(self.__t) 36 | 37 | def render(self, 38 | path: os.PathLike, 39 | params: Params, 40 | ): 41 | out = self.__template.render(load_duration=params.load_duration) 42 | 43 | with open(path, "w") as f: 44 | f.write(out) 45 | 46 | 47 | class ServerConfigRenderer: 48 | __t: Final = ''' 49 | { 50 | {% if not unlimited %} 51 | "memlimiter": { 52 | "controller_nextgc": { 53 | "rss_limit": "{{ rss_limit }}", 54 | "danger_zone_gogc": 50, 55 | "danger_zone_throttling": 90, 56 | "period": "100ms", 57 | "component_proportional": { 58 | "coefficient": {{ coefficient }}, 59 | "window_size": 20 60 | } 61 | } 62 | }, 63 | {% endif %} 64 | "listen_endpoint": "0.0.0.0:1988", 65 | "tracker": { 66 | "path": "/etc/allocator/tracker.csv", 67 | "period": "10ms" 68 | } 69 | } 70 | ''' 71 | __template: jinja2.Template 72 | 73 | def __init__(self): 74 | self.__template = jinja2.Template(self.__t) 75 | 76 | def render(self, 77 | path: os.PathLike, 78 | params: Params, 79 | ): 80 | out = self.__template.render( 81 | unlimited=params.unlimited, 82 | rss_limit=params.rss_limit_str, 83 | coefficient=params.coefficient, 84 | ) 85 | 86 | with open(path, "w") as f: 87 | f.write(out) 88 | 89 | 90 | class DockerClient: 91 | client: docker.client.DockerClient 92 | 93 | def __init__(self): 94 | self.client = docker.client.from_env() 95 | self.__build_image() 96 | 97 | def __build_image(self): 98 | image, logs = self.client.images.build(path=dockerfile_path, tag=image_tag) 99 | for log in logs: 100 | print(log) 101 | 102 | def execute(self, mem_limit: str, session_dir_path: os.PathLike): 103 | try: 104 | # drop container if exists 105 | container = self.client.containers.get(container_name) 106 | container.remove(force=True) 107 | except docker.errors.NotFound: 108 | pass 109 | 110 | container = self.client.containers.run( 111 | name=container_name, 112 | image=image_tag, 113 | mem_limit=mem_limit, 114 | volumes={ 115 | str(session_dir_path): { 116 | 'bind': '/etc/allocator', 117 | 'mode': 'rw', 118 | } 119 | }, 120 | detach=True, 121 | ) 122 | 123 | _, logs = container.exec_run( 124 | cmd='/usr/local/bin/allocator perf -c /etc/allocator/perf_config.json', 125 | stream=True, 126 | ) 127 | 128 | for log in logs: 129 | print(log) 130 | 131 | container.stop() 132 | 133 | 134 | def run_session( 135 | docker_client: DockerClient, 136 | server_config_renderer: ServerConfigRenderer, 137 | perf_config_renderer: PerfConfigRenderer, 138 | session: Session, 139 | ) -> Report: 140 | print(f">>> Start case: {session.params}") 141 | 142 | server_config_path = Path(session.dir_path, "server_config.json") 143 | server_config_renderer.render(path=server_config_path, 144 | params=session.params) 145 | 146 | perf_config_path = Path(session.dir_path, "perf_config.json") 147 | perf_config_renderer.render(path=perf_config_path, 148 | params=session.params) 149 | 150 | # run test session within Docker container 151 | docker_client.execute( 152 | mem_limit=session.params.rss_limit_str, 153 | session_dir_path=session.dir_path, 154 | ) 155 | 156 | # parse output 157 | tracker_path = Path(session.dir_path, 'tracker.csv') 158 | return Report.from_file(session=session, path=tracker_path) 159 | 160 | 161 | def main(): 162 | docker_client = DockerClient() 163 | server_config_renderer = ServerConfigRenderer() 164 | perf_config_renderer = PerfConfigRenderer() 165 | 166 | now = datetime.now() 167 | root_dir = Path('/tmp/allocator', f'allocator_{now.hour}{now.minute}{now.second}') 168 | 169 | sessions = make_sessions(root_dir) 170 | reports = [ 171 | run_session( 172 | docker_client=docker_client, 173 | server_config_renderer=server_config_renderer, 174 | perf_config_renderer=perf_config_renderer, 175 | session=ss) 176 | for ss in sessions 177 | ] 178 | 179 | render.control_params_subplots(reports, Path(root_dir, "control_params.png")) 180 | render.rss_pivot(reports, Path(root_dir, 'rss.png')) 181 | 182 | 183 | if __name__ == '__main__': 184 | main() 185 | -------------------------------------------------------------------------------- /test/allocator/analyze/render.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 2 | # Author: Vitaly Isaev 3 | # License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 4 | import os 5 | from typing import List 6 | 7 | import humanize as humanize 8 | import matplotlib.pyplot as plt 9 | import matplotlib.ticker 10 | import numpy as np 11 | 12 | from report import Report 13 | 14 | 15 | @matplotlib.ticker.FuncFormatter 16 | def bytes_major_formatter(x, pos): 17 | return humanize.naturalsize(int(x), binary=True).replace(".0", "") 18 | 19 | 20 | def control_params_subplots(reports: List[Report], path: os.PathLike): 21 | ncols = 2 22 | nrows = 3 23 | if len(reports) != ncols * nrows: 24 | raise Exception("columns and rows mismatch") 25 | 26 | fig, axes = plt.subplots(ncols=2, nrows=3, figsize=(12, 15)) 27 | 28 | ls, labels = None, None 29 | 30 | for i in range(nrows): 31 | for j in range(ncols): 32 | ix = i * ncols + j 33 | 34 | report = reports[ix] 35 | df = report.df 36 | ax = axes[i][j] 37 | 38 | ax.set_xlim(0, 60) 39 | 40 | ax.set_xlabel('Time, seconds') 41 | 42 | # RSS plot 43 | color = 'tab:red' 44 | l0 = ax.plot(df['elapsed_time'], df['rss'], color=color, label='RSS') 45 | ax.set_ylabel('RSS, bytes') 46 | ax.set_ylim(0, 1024 * 1024 * 1024) 47 | ax.set_yticks([ml * 1024 * 1024 for ml in (256, 512, 512 + 256, 1024)]) 48 | ax.yaxis.set_major_formatter(bytes_major_formatter) 49 | 50 | # GOGC plot 51 | color = 'tab:blue' 52 | twin1 = ax.twinx() 53 | l1 = twin1.plot(df['elapsed_time'], df['gogc'], color=color, label='GOGC') 54 | twin1.set_ylabel('GOGC') 55 | twin1.set_ylim(-5, 105) 56 | 57 | # Throttling plot 58 | color = 'tab:green' 59 | twin2 = ax.twinx() 60 | twin2.spines.right.set_position(("axes", 1.2)) 61 | l2 = twin2.plot(df['elapsed_time'], df['throttling'], color=color, label='Throttling') 62 | twin2.set_ylabel('Throttling') 63 | twin2.set_ylim(-5, 105) 64 | 65 | # legend 66 | if not ls or not labels: 67 | ls = l0 + l1 + l2 68 | labels = [l.get_label() for l in ls] 69 | 70 | # title 71 | if report.session.params.unlimited: 72 | title = "MemLimiter disabled" 73 | else: 74 | coefficient = report.session.params.coefficient_str 75 | title = f'MemLimiter enabled, $C_{{p}} = {coefficient}$' 76 | ax.title.set_text(title) 77 | 78 | fig.legend(ls, labels) 79 | fig.tight_layout() 80 | fig.savefig(path, transparent=False) 81 | 82 | 83 | def rss_pivot(reports: List[Report], path: os.PathLike): 84 | fig, ax = plt.subplots(figsize=(8, 6)) 85 | ax.set_xlim(0, 60) 86 | ax.set_xlabel('Time, seconds') 87 | ax.set_ylabel('RSS, bytes') 88 | ax.set_ylim(0, 1024 * 1024 * 1024) 89 | ax.set_yticks([ml * 1024 * 1024 for ml in (256, 512, 512 + 256, 1024)]) 90 | ax.yaxis.set_major_formatter(bytes_major_formatter) 91 | 92 | n = len(reports) 93 | 94 | colors = plt.cm.turbo(np.linspace(0, 1, n)) 95 | for i in range(n): 96 | report = reports[n - 1 - i] 97 | if report.session.params.unlimited: 98 | label = 'No limits' 99 | else: 100 | label = f'$C_{{p}} = {report.session.params.coefficient_str}$' 101 | 102 | ax.plot(report.df['elapsed_time'], report.df['rss'], color=colors[i], label=label) 103 | 104 | ax.legend() 105 | ax.title.set_text('RSS consumption dependence on $C_{{p}}$') 106 | fig.tight_layout() 107 | fig.savefig(path, transparent=False) 108 | -------------------------------------------------------------------------------- /test/allocator/analyze/report.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 2 | # Author: Vitaly Isaev 3 | # License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 4 | import dataclasses 5 | import os 6 | 7 | import pandas as pd 8 | 9 | from testing import Session 10 | 11 | 12 | @dataclasses.dataclass 13 | class Report: 14 | df: pd.DataFrame 15 | session: Session 16 | 17 | @classmethod 18 | def from_file(cls, path: os.PathLike, session: Session): 19 | out = Report( 20 | df=Report.__parse_tracker_stats(path), 21 | session=session, 22 | ) 23 | 24 | return out 25 | 26 | @staticmethod 27 | def __parse_tracker_stats(path: os.PathLike) -> pd.DataFrame: 28 | df = pd.read_csv(path) 29 | df['timestamp'] = pd.to_datetime(df['timestamp']) 30 | df['utilization'] *= 100 31 | return df 32 | 33 | def __post_init__(self): 34 | # Emulate OOM event for unconstrained process 35 | if self.session.params.unlimited: 36 | last_ts, last_but_one_ts = self.df['timestamp'].iloc[-1], self.df['timestamp'].iloc[-2] 37 | delta = last_ts - last_but_one_ts 38 | self.df.loc[len(self.df)] = [ 39 | last_ts + delta, 40 | self.session.params.rss_limit, 41 | 0, 0, 0, 42 | ] 43 | 44 | # compute elapsed time 45 | self.df['elapsed_time'] = (self.df['timestamp'] - self.df['timestamp'].min()).apply( 46 | lambda x: x.seconds + x.microseconds / 1000000) 47 | -------------------------------------------------------------------------------- /test/allocator/analyze/testing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 2 | # Author: Vitaly Isaev 3 | # License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 4 | import dataclasses 5 | import os 6 | from pathlib import Path 7 | from typing import Iterable, Final 8 | 9 | GIGABYTE: Final = 1024 * 1024 * 1024 10 | 11 | 12 | @dataclasses.dataclass 13 | class Params: 14 | unlimited: bool 15 | rss_limit: int = GIGABYTE 16 | coefficient: float = 20.0 17 | load_duration: str = '60s' 18 | 19 | def __str__(self) -> str: 20 | return f"unlimited_{self.unlimited}_rss_limit_{self.rss_limit}_coefficient_{self.coefficient_str}" 21 | 22 | @property 23 | def rss_limit_str(self): 24 | return f'{self.rss_limit}b' 25 | 26 | @property 27 | def coefficient_str(self): 28 | if type(self.coefficient) == float and self.coefficient.is_integer(): 29 | return str(int(self.coefficient)) 30 | else: 31 | return str(self.coefficient) 32 | 33 | 34 | class Session: 35 | params: Params 36 | dir_path: os.PathLike 37 | 38 | def __init__(self, case: Params, root_dir: os.PathLike): 39 | self.params = case 40 | self.dir_path = Path(root_dir, str(case)) 41 | os.makedirs(self.dir_path, 0o777) 42 | 43 | 44 | def make_sessions(root_dir: os.PathLike) -> Iterable[Session]: 45 | duration = "60s" 46 | cases = ( 47 | Params(unlimited=True, load_duration=duration, rss_limit=GIGABYTE), 48 | Params(unlimited=False, load_duration=duration, rss_limit=GIGABYTE, coefficient=0.5), 49 | Params(unlimited=False, load_duration=duration, rss_limit=GIGABYTE, coefficient=1), 50 | Params(unlimited=False, load_duration=duration, rss_limit=GIGABYTE, coefficient=5), 51 | Params(unlimited=False, load_duration=duration, rss_limit=GIGABYTE, coefficient=10), 52 | Params(unlimited=False, load_duration=duration, rss_limit=GIGABYTE, coefficient=50), 53 | ) 54 | 55 | # FIXME: remove after debug 56 | # cases = ( 57 | # Params(unlimited=True, load_duration="10s", rss_limit=GIGABYTE), 58 | # ) 59 | 60 | return (Session(case=tc, root_dir=root_dir) for tc in cases) 61 | -------------------------------------------------------------------------------- /test/allocator/app/app.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package app 8 | 9 | import ( 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | 14 | "github.com/go-logr/logr" 15 | "github.com/pkg/errors" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | // App - CLI application. 20 | type App struct { 21 | factory Factory 22 | logger logr.Logger 23 | } 24 | 25 | // Run launches the application. 26 | func (a *App) Run() { 27 | app := &cli.App{ 28 | Name: "allocator", 29 | Usage: "test application for memlimiter", 30 | Commands: cli.Commands{ 31 | &cli.Command{ 32 | Name: "server", 33 | Usage: "allocator server app", 34 | Flags: []cli.Flag{ 35 | &cli.StringFlag{ 36 | Name: "config", 37 | Usage: "configuration file", 38 | Aliases: []string{"c"}, 39 | Required: true, 40 | }, 41 | }, 42 | Action: func(context *cli.Context) error { 43 | r, err := a.factory.MakeServer(context) 44 | if err != nil { 45 | return errors.Wrap(err, "make server") 46 | } 47 | 48 | return runAndWaitSignal(r) 49 | }, 50 | }, 51 | &cli.Command{ 52 | Name: "perf", 53 | Usage: "allocator perf client", 54 | Flags: []cli.Flag{ 55 | &cli.StringFlag{ 56 | Name: "config", 57 | Usage: "configuration file", 58 | Aliases: []string{"c"}, 59 | Required: true, 60 | }, 61 | }, 62 | Action: func(context *cli.Context) error { 63 | r, err := a.factory.MakePerfClient(context) 64 | if err != nil { 65 | return errors.Wrap(err, "make perf client") 66 | } 67 | 68 | return runAndWaitSignal(r) 69 | }, 70 | }, 71 | }, 72 | } 73 | 74 | if err := app.Run(os.Args); err != nil { 75 | a.logger.Error(err, "application run") 76 | os.Exit(1) 77 | } 78 | } 79 | 80 | func runAndWaitSignal(r Runnable) error { 81 | signalChan := make(chan os.Signal, 1) 82 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 83 | 84 | errChan := make(chan error, 1) 85 | 86 | go func() { errChan <- r.Run() }() 87 | 88 | defer r.Quit() 89 | 90 | select { 91 | case err := <-errChan: 92 | return errors.Wrap(err, "run error") 93 | case <-signalChan: 94 | return nil 95 | } 96 | } 97 | 98 | // NewApp prepares new application. 99 | func NewApp(logger logr.Logger, factory Factory) *App { 100 | return &App{ 101 | logger: logger, 102 | factory: factory, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/allocator/app/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package app contains all the necessary things to build simple web application using MemLimiter. 8 | package app 9 | -------------------------------------------------------------------------------- /test/allocator/app/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package app 8 | 9 | import ( 10 | "encoding/json" 11 | "io/ioutil" 12 | "path/filepath" 13 | 14 | "github.com/go-logr/logr" 15 | "github.com/newcloudtechnologies/memlimiter/test/allocator/perf" 16 | "github.com/newcloudtechnologies/memlimiter/test/allocator/server" 17 | "github.com/pkg/errors" 18 | "github.com/urfave/cli/v2" 19 | ) 20 | 21 | // Runnable represents some task that can be run. 22 | type Runnable interface { 23 | // Run - a blocking call. 24 | Run() error 25 | // Quit terminates process. 26 | Quit() 27 | } 28 | 29 | // Factory builds runnable tasks. 30 | type Factory interface { 31 | // MakeServer creates a server. 32 | MakeServer(c *cli.Context) (Runnable, error) 33 | // MakePerfClient creates a client for performance tests. 34 | MakePerfClient(c *cli.Context) (Runnable, error) 35 | } 36 | 37 | type factoryDefault struct { 38 | logger logr.Logger 39 | } 40 | 41 | //nolint:dupl 42 | func (f *factoryDefault) MakeServer(c *cli.Context) (Runnable, error) { 43 | filename := c.String("config") 44 | 45 | data, err := ioutil.ReadFile(filepath.Clean(filename)) 46 | if err != nil { 47 | return nil, errors.Wrap(err, "ioutil readfile") 48 | } 49 | 50 | cfg := &server.Config{} 51 | 52 | if err = json.Unmarshal(data, cfg); err != nil { 53 | return nil, errors.Wrap(err, "unmarshal") 54 | } 55 | 56 | srv, err := server.NewServer(f.logger, cfg) 57 | if err != nil { 58 | return nil, errors.Wrap(err, "new allocator server") 59 | } 60 | 61 | return srv, nil 62 | } 63 | 64 | //nolint:dupl 65 | func (f *factoryDefault) MakePerfClient(c *cli.Context) (Runnable, error) { 66 | filename := c.String("config") 67 | 68 | data, err := ioutil.ReadFile(filepath.Clean(filename)) 69 | if err != nil { 70 | return nil, errors.Wrap(err, "ioutil readfile") 71 | } 72 | 73 | cfg := &perf.Config{} 74 | 75 | if err = json.Unmarshal(data, cfg); err != nil { 76 | return nil, errors.Wrap(err, "unmarshal") 77 | } 78 | 79 | cl, err := perf.NewClient(f.logger, cfg) 80 | if err != nil { 81 | return nil, errors.Wrap(err, "new allocator server") 82 | } 83 | 84 | return cl, nil 85 | } 86 | 87 | // NewFactory makes new default factory. 88 | func NewFactory(logger logr.Logger) Factory { 89 | return &factoryDefault{logger: logger} 90 | } 91 | -------------------------------------------------------------------------------- /test/allocator/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "log" 11 | "os" 12 | 13 | "github.com/go-logr/stdr" 14 | "github.com/newcloudtechnologies/memlimiter/test/allocator/app" 15 | ) 16 | 17 | func main() { 18 | logger := stdr.NewWithOptions( 19 | log.New(os.Stdout, "", log.LstdFlags), 20 | stdr.Options{LogCaller: stdr.All}, 21 | ) 22 | 23 | a := app.NewApp(logger, app.NewFactory(logger)) 24 | a.Run() 25 | } 26 | -------------------------------------------------------------------------------- /test/allocator/perf/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package perf 8 | 9 | import ( 10 | "context" 11 | "time" 12 | 13 | "github.com/go-logr/logr" 14 | "github.com/newcloudtechnologies/memlimiter/utils/config/prepare" 15 | "github.com/rcrowley/go-metrics" 16 | "golang.org/x/time/rate" 17 | "google.golang.org/grpc" 18 | "google.golang.org/protobuf/types/known/durationpb" 19 | 20 | "github.com/newcloudtechnologies/memlimiter/test/allocator/schema" 21 | "github.com/newcloudtechnologies/memlimiter/utils/breaker" 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | // Client - client for performance testing. 26 | type Client struct { 27 | startTime time.Time 28 | client schema.AllocatorClient 29 | requestsInFlight metrics.Counter 30 | grpcConn *grpc.ClientConn 31 | breaker *breaker.Breaker 32 | cfg *Config 33 | logger logr.Logger 34 | } 35 | 36 | // Run starts load session. 37 | func (p *Client) Run() error { 38 | if err := p.breaker.Inc(); err != nil { 39 | return errors.Wrap(err, "breaker inc") 40 | } 41 | 42 | defer p.breaker.Dec() 43 | 44 | monitoringTicker := time.NewTicker(time.Second) 45 | defer monitoringTicker.Stop() 46 | 47 | timer := time.NewTimer(p.cfg.LoadDuration.Duration) 48 | defer timer.Stop() 49 | 50 | limiter := rate.NewLimiter(p.cfg.RPS, 1) 51 | 52 | // single threaded for simplicity 53 | for { 54 | // wait till limiter allows to fire a request 55 | if err := limiter.Wait(p.breaker); err != nil { 56 | return errors.Wrap(err, "limiter wait") 57 | } 58 | 59 | // increment request copies 60 | if err := p.breaker.Inc(); err != nil { 61 | return errors.Wrap(err, "breaker inc") 62 | } 63 | 64 | go p.makeRequest() 65 | 66 | select { 67 | case <-monitoringTicker.C: 68 | // print progress periodically 69 | p.printProgress() 70 | case <-timer.C: 71 | // terminate load 72 | return nil 73 | default: 74 | } 75 | } 76 | } 77 | 78 | func (p *Client) makeRequest() { 79 | defer p.breaker.Dec() 80 | 81 | // update in-flight request counter 82 | p.requestsInFlight.Inc(1) 83 | defer p.requestsInFlight.Dec(1) 84 | 85 | ctx, cancel := context.WithTimeout(p.breaker, p.cfg.RequestTimeout.Duration) 86 | defer cancel() 87 | 88 | request := &schema.MakeAllocationRequest{ 89 | Size: p.cfg.AllocationSize.Value, 90 | } 91 | 92 | if p.cfg.PauseDuration.Duration != 0 { 93 | request.Duration = durationpb.New(p.cfg.PauseDuration.Duration) 94 | } 95 | 96 | _, err := p.client.MakeAllocation(ctx, request) 97 | if err != nil && p.breaker.IsOperational() { 98 | p.logger.Error(err, "make allocation request") 99 | } 100 | } 101 | 102 | func (p *Client) printProgress() { 103 | p.logger.Info( 104 | "progress", 105 | "elapsed_time", time.Since(p.startTime), 106 | "in_flight", p.requestsInFlight.Count(), 107 | ) 108 | } 109 | 110 | // Quit terminates perf client gracefully. 111 | func (p *Client) Quit() { 112 | p.breaker.ShutdownAndWait() 113 | 114 | if err := p.grpcConn.Close(); err != nil { 115 | p.logger.Error(err, "gprc connection close") 116 | } 117 | } 118 | 119 | // NewClient creates new client for performance tests. 120 | func NewClient(logger logr.Logger, cfg *Config) (*Client, error) { 121 | if err := prepare.Prepare(cfg); err != nil { 122 | return nil, errors.Wrap(err, "configs prepare") 123 | } 124 | 125 | grpcConn, err := grpc.Dial(cfg.Endpoint, grpc.WithInsecure()) 126 | if err != nil { 127 | return nil, errors.Wrap(err, "dial error") 128 | } 129 | 130 | client := schema.NewAllocatorClient(grpcConn) 131 | 132 | return &Client{ 133 | grpcConn: grpcConn, 134 | logger: logger, 135 | client: client, 136 | startTime: time.Now(), 137 | cfg: cfg, 138 | requestsInFlight: metrics.NewCounter(), 139 | breaker: breaker.NewBreaker(), 140 | }, nil 141 | } 142 | -------------------------------------------------------------------------------- /test/allocator/perf/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package perf 8 | 9 | import ( 10 | "golang.org/x/time/rate" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 13 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Config - performance client configuration. 18 | type Config struct { 19 | // Endpoint server address 20 | Endpoint string `json:"endpoint"` 21 | // RPS - target load [issued requests per second] 22 | RPS rate.Limit `json:"rps"` 23 | // LoadDuration - duration of the performance test session 24 | LoadDuration duration.Duration `json:"load_duration"` 25 | // AllocationSize - the size of allocations made on the server side during each request 26 | AllocationSize bytes.Bytes `json:"allocation_size"` 27 | // PauseDuration - duration of the pause in the request handler 28 | // on the server-side (to help allocations reside in server memory for a long time). 29 | PauseDuration duration.Duration `json:"pause_duration"` 30 | // RequestTimeout - server request timeout 31 | RequestTimeout duration.Duration `json:"request_timeout"` 32 | } 33 | 34 | // Prepare validates configuration. 35 | func (c *Config) Prepare() error { 36 | if c.Endpoint == "" { 37 | return errors.New("empty endpoint") 38 | } 39 | 40 | if c.RPS == 0 { 41 | return errors.New("empty rps") 42 | } 43 | 44 | if c.LoadDuration.Duration == 0 { 45 | return errors.New("empty load duration") 46 | } 47 | 48 | if c.AllocationSize.Value == 0 { 49 | return errors.New("empty allocation size") 50 | } 51 | 52 | if c.PauseDuration.Duration == 0 { 53 | return errors.New("empty pause duration") 54 | } 55 | 56 | if c.RequestTimeout.Duration == 0 { 57 | return errors.New("empty request timeout") 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /test/allocator/perf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": "localhost:1988", 3 | "rps": 100, 4 | "load_duration": "20s", 5 | "allocation_size": "1M", 6 | "pause_duration": "10s", 7 | "request_timeout": "1m" 8 | } 9 | -------------------------------------------------------------------------------- /test/allocator/perf/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package perf 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 13 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestConfig(t *testing.T) { 18 | t.Run("invalid endpoint", func(t *testing.T) { 19 | c := &Config{} 20 | require.Error(t, c.Prepare()) 21 | }) 22 | 23 | t.Run("invalid rps", func(t *testing.T) { 24 | c := &Config{ 25 | Endpoint: "localhost:8080", 26 | } 27 | require.Error(t, c.Prepare()) 28 | }) 29 | 30 | t.Run("invalid load duration", func(t *testing.T) { 31 | c := &Config{ 32 | Endpoint: "localhost:8080", 33 | RPS: 100, 34 | } 35 | require.Error(t, c.Prepare()) 36 | }) 37 | 38 | t.Run("invalid allocation size", func(t *testing.T) { 39 | c := &Config{ 40 | Endpoint: "localhost:8080", 41 | RPS: 100, 42 | LoadDuration: duration.Duration{Duration: 1}, 43 | } 44 | require.Error(t, c.Prepare()) 45 | }) 46 | 47 | t.Run("invalid pause duration", func(t *testing.T) { 48 | c := &Config{ 49 | Endpoint: "localhost:8080", 50 | RPS: 100, 51 | LoadDuration: duration.Duration{Duration: 1}, 52 | AllocationSize: bytes.Bytes{Value: 100}, 53 | } 54 | require.Error(t, c.Prepare()) 55 | }) 56 | 57 | t.Run("invalid request timeout duration", func(t *testing.T) { 58 | c := &Config{ 59 | Endpoint: "localhost:8080", 60 | RPS: 100, 61 | LoadDuration: duration.Duration{Duration: 1}, 62 | AllocationSize: bytes.Bytes{Value: 100}, 63 | PauseDuration: duration.Duration{Duration: 1}, 64 | } 65 | require.Error(t, c.Prepare()) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /test/allocator/perf/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package perf contains performance client. 8 | package perf 9 | -------------------------------------------------------------------------------- /test/allocator/schema/allocator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.14.0 5 | // source: allocator.proto 6 | 7 | package schema 8 | 9 | import ( 10 | reflect "reflect" 11 | sync "sync" 12 | 13 | proto "github.com/golang/protobuf/proto" 14 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 15 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 16 | durationpb "google.golang.org/protobuf/types/known/durationpb" 17 | ) 18 | 19 | const ( 20 | // Verify that this generated code is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 22 | // Verify that runtime/protoimpl is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 24 | ) 25 | 26 | // This is a compile-time assertion that a sufficiently up-to-date version 27 | // of the legacy proto package is being used. 28 | const _ = proto.ProtoPackageIsVersion4 29 | 30 | // MakeAllocationRequest - запрос на аллокацию. 31 | type MakeAllocationRequest struct { 32 | state protoimpl.MessageState 33 | sizeCache protoimpl.SizeCache 34 | unknownFields protoimpl.UnknownFields 35 | 36 | // size - размер аллокации 37 | Size uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` 38 | // duration - продолжительность времени, на которое надо заблокировать запрос после аллокации 39 | Duration *durationpb.Duration `protobuf:"bytes,2,opt,name=duration,proto3" json:"duration,omitempty"` 40 | } 41 | 42 | func (x *MakeAllocationRequest) Reset() { 43 | *x = MakeAllocationRequest{} 44 | if protoimpl.UnsafeEnabled { 45 | mi := &file_allocator_proto_msgTypes[0] 46 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 47 | ms.StoreMessageInfo(mi) 48 | } 49 | } 50 | 51 | func (x *MakeAllocationRequest) String() string { 52 | return protoimpl.X.MessageStringOf(x) 53 | } 54 | 55 | func (*MakeAllocationRequest) ProtoMessage() {} 56 | 57 | func (x *MakeAllocationRequest) ProtoReflect() protoreflect.Message { 58 | mi := &file_allocator_proto_msgTypes[0] 59 | if protoimpl.UnsafeEnabled && x != nil { 60 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 61 | if ms.LoadMessageInfo() == nil { 62 | ms.StoreMessageInfo(mi) 63 | } 64 | return ms 65 | } 66 | return mi.MessageOf(x) 67 | } 68 | 69 | // Deprecated: Use MakeAllocationRequest.ProtoReflect.Descriptor instead. 70 | func (*MakeAllocationRequest) Descriptor() ([]byte, []int) { 71 | return file_allocator_proto_rawDescGZIP(), []int{0} 72 | } 73 | 74 | func (x *MakeAllocationRequest) GetSize() uint64 { 75 | if x != nil { 76 | return x.Size 77 | } 78 | return 0 79 | } 80 | 81 | func (x *MakeAllocationRequest) GetDuration() *durationpb.Duration { 82 | if x != nil { 83 | return x.Duration 84 | } 85 | return nil 86 | } 87 | 88 | // MakeAllocationResponse - ответ на запрос на аллокацию. 89 | type MakeAllocationResponse struct { 90 | state protoimpl.MessageState 91 | sizeCache protoimpl.SizeCache 92 | unknownFields protoimpl.UnknownFields 93 | 94 | // value - просто некоторое значение 95 | Value uint64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` 96 | } 97 | 98 | func (x *MakeAllocationResponse) Reset() { 99 | *x = MakeAllocationResponse{} 100 | if protoimpl.UnsafeEnabled { 101 | mi := &file_allocator_proto_msgTypes[1] 102 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 103 | ms.StoreMessageInfo(mi) 104 | } 105 | } 106 | 107 | func (x *MakeAllocationResponse) String() string { 108 | return protoimpl.X.MessageStringOf(x) 109 | } 110 | 111 | func (*MakeAllocationResponse) ProtoMessage() {} 112 | 113 | func (x *MakeAllocationResponse) ProtoReflect() protoreflect.Message { 114 | mi := &file_allocator_proto_msgTypes[1] 115 | if protoimpl.UnsafeEnabled && x != nil { 116 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 117 | if ms.LoadMessageInfo() == nil { 118 | ms.StoreMessageInfo(mi) 119 | } 120 | return ms 121 | } 122 | return mi.MessageOf(x) 123 | } 124 | 125 | // Deprecated: Use MakeAllocationResponse.ProtoReflect.Descriptor instead. 126 | func (*MakeAllocationResponse) Descriptor() ([]byte, []int) { 127 | return file_allocator_proto_rawDescGZIP(), []int{1} 128 | } 129 | 130 | func (x *MakeAllocationResponse) GetValue() uint64 { 131 | if x != nil { 132 | return x.Value 133 | } 134 | return 0 135 | } 136 | 137 | var File_allocator_proto protoreflect.FileDescriptor 138 | 139 | var file_allocator_proto_rawDesc = []byte{ 140 | 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 141 | 0x6f, 0x12, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 142 | 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 143 | 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x62, 0x0a, 0x15, 0x4d, 0x61, 0x6b, 144 | 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 145 | 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 146 | 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 147 | 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 148 | 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 149 | 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 150 | 0x16, 0x4d, 0x61, 0x6b, 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 151 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 152 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x5e, 0x0a, 153 | 0x09, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x51, 0x0a, 0x0e, 0x4d, 0x61, 154 | 0x6b, 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x73, 155 | 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4d, 0x61, 0x6b, 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 156 | 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x63, 157 | 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4d, 0x61, 0x6b, 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 158 | 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x43, 0x5a, 159 | 0x41, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x73, 0x74, 0x61, 0x67, 0x65, 0x6f, 0x66, 0x66, 160 | 0x69, 0x63, 0x65, 0x2e, 0x72, 0x75, 0x2f, 0x55, 0x43, 0x53, 0x2d, 0x43, 0x4f, 0x4d, 0x4d, 0x4f, 161 | 0x4e, 0x2f, 0x6d, 0x65, 0x6d, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x2f, 0x74, 0x65, 0x73, 162 | 0x74, 0x2f, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x73, 0x63, 0x68, 0x65, 163 | 0x6d, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 164 | } 165 | 166 | var ( 167 | file_allocator_proto_rawDescOnce sync.Once 168 | file_allocator_proto_rawDescData = file_allocator_proto_rawDesc 169 | ) 170 | 171 | func file_allocator_proto_rawDescGZIP() []byte { 172 | file_allocator_proto_rawDescOnce.Do(func() { 173 | file_allocator_proto_rawDescData = protoimpl.X.CompressGZIP(file_allocator_proto_rawDescData) 174 | }) 175 | return file_allocator_proto_rawDescData 176 | } 177 | 178 | var file_allocator_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 179 | var file_allocator_proto_goTypes = []interface{}{ 180 | (*MakeAllocationRequest)(nil), // 0: schema.MakeAllocationRequest 181 | (*MakeAllocationResponse)(nil), // 1: schema.MakeAllocationResponse 182 | (*durationpb.Duration)(nil), // 2: google.protobuf.Duration 183 | } 184 | var file_allocator_proto_depIdxs = []int32{ 185 | 2, // 0: schema.MakeAllocationRequest.duration:type_name -> google.protobuf.Duration 186 | 0, // 1: schema.Allocator.MakeAllocation:input_type -> schema.MakeAllocationRequest 187 | 1, // 2: schema.Allocator.MakeAllocation:output_type -> schema.MakeAllocationResponse 188 | 2, // [2:3] is the sub-list for method output_type 189 | 1, // [1:2] is the sub-list for method input_type 190 | 1, // [1:1] is the sub-list for extension type_name 191 | 1, // [1:1] is the sub-list for extension extendee 192 | 0, // [0:1] is the sub-list for field type_name 193 | } 194 | 195 | func init() { file_allocator_proto_init() } 196 | func file_allocator_proto_init() { 197 | if File_allocator_proto != nil { 198 | return 199 | } 200 | if !protoimpl.UnsafeEnabled { 201 | file_allocator_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 202 | switch v := v.(*MakeAllocationRequest); i { 203 | case 0: 204 | return &v.state 205 | case 1: 206 | return &v.sizeCache 207 | case 2: 208 | return &v.unknownFields 209 | default: 210 | return nil 211 | } 212 | } 213 | file_allocator_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 214 | switch v := v.(*MakeAllocationResponse); i { 215 | case 0: 216 | return &v.state 217 | case 1: 218 | return &v.sizeCache 219 | case 2: 220 | return &v.unknownFields 221 | default: 222 | return nil 223 | } 224 | } 225 | } 226 | type x struct{} 227 | out := protoimpl.TypeBuilder{ 228 | File: protoimpl.DescBuilder{ 229 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 230 | RawDescriptor: file_allocator_proto_rawDesc, 231 | NumEnums: 0, 232 | NumMessages: 2, 233 | NumExtensions: 0, 234 | NumServices: 1, 235 | }, 236 | GoTypes: file_allocator_proto_goTypes, 237 | DependencyIndexes: file_allocator_proto_depIdxs, 238 | MessageInfos: file_allocator_proto_msgTypes, 239 | }.Build() 240 | File_allocator_proto = out.File 241 | file_allocator_proto_rawDesc = nil 242 | file_allocator_proto_goTypes = nil 243 | file_allocator_proto_depIdxs = nil 244 | } 245 | -------------------------------------------------------------------------------- /test/allocator/schema/allocator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package schema; 4 | 5 | option go_package = "gitlab.stageoffice.ru/UCS-COMMON/memlimiter/test/allocator/schema"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Allocator - тестовый сервис, который просто делает аллокации во время обработки запроса 10 | service Allocator { 11 | rpc MakeAllocation (MakeAllocationRequest) returns (MakeAllocationResponse) {} 12 | } 13 | 14 | // MakeAllocationRequest - запрос на аллокацию 15 | message MakeAllocationRequest { 16 | // size - размер аллокации 17 | uint64 size = 1; 18 | // duration - продолжительность времени, на которое надо заблокировать запрос после аллокации 19 | google.protobuf.Duration duration = 2; 20 | } 21 | 22 | // MakeAllocationResponse - ответ на запрос на аллокацию 23 | message MakeAllocationResponse { 24 | // value - просто некоторое значение 25 | uint64 value = 1; 26 | } -------------------------------------------------------------------------------- /test/allocator/schema/allocator_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package schema 4 | 5 | import ( 6 | context "context" 7 | 8 | grpc "google.golang.org/grpc" 9 | codes "google.golang.org/grpc/codes" 10 | status "google.golang.org/grpc/status" 11 | ) 12 | 13 | // This is a compile-time assertion to ensure that this generated file 14 | // is compatible with the grpc package it is being compiled against. 15 | // Requires gRPC-Go v1.32.0 or later. 16 | const _ = grpc.SupportPackageIsVersion7 17 | 18 | // AllocatorClient is the client API for Allocator service. 19 | // 20 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 21 | type AllocatorClient interface { 22 | MakeAllocation(ctx context.Context, in *MakeAllocationRequest, opts ...grpc.CallOption) (*MakeAllocationResponse, error) 23 | } 24 | 25 | type allocatorClient struct { 26 | cc grpc.ClientConnInterface 27 | } 28 | 29 | func NewAllocatorClient(cc grpc.ClientConnInterface) AllocatorClient { 30 | return &allocatorClient{cc} 31 | } 32 | 33 | func (c *allocatorClient) MakeAllocation(ctx context.Context, in *MakeAllocationRequest, opts ...grpc.CallOption) (*MakeAllocationResponse, error) { 34 | out := new(MakeAllocationResponse) 35 | err := c.cc.Invoke(ctx, "/schema.Allocator/MakeAllocation", in, out, opts...) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return out, nil 40 | } 41 | 42 | // AllocatorServer is the server API for Allocator service. 43 | // All implementations must embed UnimplementedAllocatorServer 44 | // for forward compatibility. 45 | type AllocatorServer interface { 46 | MakeAllocation(context.Context, *MakeAllocationRequest) (*MakeAllocationResponse, error) 47 | mustEmbedUnimplementedAllocatorServer() 48 | } 49 | 50 | // UnimplementedAllocatorServer must be embedded to have forward compatible implementations. 51 | type UnimplementedAllocatorServer struct { 52 | } 53 | 54 | func (UnimplementedAllocatorServer) MakeAllocation(context.Context, *MakeAllocationRequest) (*MakeAllocationResponse, error) { 55 | return nil, status.Errorf(codes.Unimplemented, "method MakeAllocation not implemented") 56 | } 57 | func (UnimplementedAllocatorServer) mustEmbedUnimplementedAllocatorServer() {} 58 | 59 | // UnsafeAllocatorServer may be embedded to opt out of forward compatibility for this service. 60 | // Use of this interface is not recommended, as added methods to AllocatorServer will 61 | // result in compilation errors. 62 | type UnsafeAllocatorServer interface { 63 | mustEmbedUnimplementedAllocatorServer() 64 | } 65 | 66 | func RegisterAllocatorServer(s grpc.ServiceRegistrar, srv AllocatorServer) { 67 | s.RegisterService(&Allocator_ServiceDesc, srv) 68 | } 69 | 70 | func _Allocator_MakeAllocation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 71 | in := new(MakeAllocationRequest) 72 | if err := dec(in); err != nil { 73 | return nil, err 74 | } 75 | if interceptor == nil { 76 | return srv.(AllocatorServer).MakeAllocation(ctx, in) 77 | } 78 | info := &grpc.UnaryServerInfo{ 79 | Server: srv, 80 | FullMethod: "/schema.Allocator/MakeAllocation", 81 | } 82 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 83 | return srv.(AllocatorServer).MakeAllocation(ctx, req.(*MakeAllocationRequest)) 84 | } 85 | return interceptor(ctx, in, info, handler) 86 | } 87 | 88 | // Allocator_ServiceDesc is the grpc.ServiceDesc for Allocator service. 89 | // It's only intended for direct use with grpc.RegisterService, 90 | // and not to be introspected or modified (even as a copy). 91 | var Allocator_ServiceDesc = grpc.ServiceDesc{ 92 | ServiceName: "schema.Allocator", 93 | HandlerType: (*AllocatorServer)(nil), 94 | Methods: []grpc.MethodDesc{ 95 | { 96 | MethodName: "MakeAllocation", 97 | Handler: _Allocator_MakeAllocation_Handler, 98 | }, 99 | }, 100 | Streams: []grpc.StreamDesc{}, 101 | Metadata: "allocator.proto", 102 | } 103 | -------------------------------------------------------------------------------- /test/allocator/schema/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 5 | # Author: Vitaly Isaev 6 | # License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 7 | # 8 | 9 | set -e 10 | set -x 11 | 12 | protoc -I/usr/include \ 13 | -I. \ 14 | --go_out=. \ 15 | --go_opt=paths=source_relative \ 16 | --go-grpc_out=. \ 17 | --go-grpc_opt=paths=source_relative \ 18 | allocator.proto 19 | -------------------------------------------------------------------------------- /test/allocator/server/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package server 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter" 11 | "github.com/newcloudtechnologies/memlimiter/test/allocator/tracker" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Config - a top-level service configuration. 16 | type Config struct { 17 | MemLimiter *memlimiter.Config `json:"memLimiter"` //nolint:tagliatelle 18 | Tracker *tracker.Config `json:"tracker"` 19 | ListenEndpoint string `json:"listen_endpoint"` 20 | } 21 | 22 | // Prepare validates config. 23 | func (c *Config) Prepare() error { 24 | if c.ListenEndpoint == "" { 25 | return errors.New("listen endpoint is empty") 26 | } 27 | 28 | if c.Tracker == nil { 29 | return errors.New("empty tracker") 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /test/allocator/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "memlimiter": { 3 | "controller_nextgc": { 4 | "rss_limit": "1G", 5 | "danger_zone_gogc": 50, 6 | "danger_zone_throttling": 90, 7 | "period": "1s", 8 | "component_proportional": { 9 | "coefficient": 20, 10 | "window_size": 20 11 | } 12 | } 13 | }, 14 | "listen_endpoint": "0.0.0.0:1988", 15 | "tracker": { 16 | "backend_file": { 17 | "path": "/tmp/tracker.csv" 18 | }, 19 | "period": "1s" 20 | } 21 | } -------------------------------------------------------------------------------- /test/allocator/server/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package server 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestConfig(t *testing.T) { 16 | t.Run("empty endpoint", func(t *testing.T) { 17 | c := &Config{} 18 | require.Error(t, c.Prepare()) 19 | }) 20 | 21 | t.Run("empty tracker", func(t *testing.T) { 22 | c := &Config{ 23 | Tracker: nil, 24 | ListenEndpoint: "localhost:80", 25 | } 26 | require.Error(t, c.Prepare()) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /test/allocator/server/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package server is a simple GRPC service performing useless memory allocations. 8 | // Consider it as en example of MemLimiter integration. 9 | package server 10 | -------------------------------------------------------------------------------- /test/allocator/server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package server 8 | 9 | import ( 10 | "context" 11 | "math/rand" 12 | "net" 13 | "time" 14 | 15 | "github.com/go-logr/logr" 16 | "github.com/newcloudtechnologies/memlimiter" 17 | "github.com/newcloudtechnologies/memlimiter/test/allocator/schema" 18 | "github.com/newcloudtechnologies/memlimiter/test/allocator/tracker" 19 | "github.com/newcloudtechnologies/memlimiter/utils/config/prepare" 20 | "github.com/pkg/errors" 21 | "google.golang.org/grpc" 22 | ) 23 | 24 | // Server represents Allocator service interface. 25 | type Server interface { 26 | schema.AllocatorServer 27 | // Run starts service (a blocking call). 28 | Run() error 29 | // Quit terminates service gracefully. 30 | Quit() 31 | // GRPCServer returns underlying server implementation. Only for testing purposes. 32 | GRPCServer() *grpc.Server 33 | // MemLimiter returns internal MemLimiter object. Only for testing purposes. 34 | MemLimiter() memlimiter.Service 35 | // Tracker returns statistics tracker. Only for testing purposes. 36 | Tracker() *tracker.Tracker 37 | } 38 | 39 | var _ Server = (*serverImpl)(nil) 40 | 41 | type serverImpl struct { 42 | schema.UnimplementedAllocatorServer 43 | memLimiter memlimiter.Service 44 | tracker *tracker.Tracker 45 | cfg *Config 46 | grpcServer *grpc.Server 47 | logger logr.Logger 48 | } 49 | 50 | func (srv *serverImpl) MakeAllocation(_ context.Context, request *schema.MakeAllocationRequest) (*schema.MakeAllocationResponse, error) { 51 | var slice []byte 52 | 53 | // allocate slice 54 | if request.Size != 0 { 55 | slice = make([]byte, int(request.Size)) 56 | //nolint:gosec 57 | if _, err := rand.Read(slice); err != nil { 58 | return nil, errors.Wrap(err, "rand read") 59 | } 60 | } 61 | 62 | // Wait some time to make slice reside in the RSS (otherwise it could be immediately collected by GC). 63 | // This is a trivial imitation of a real-world service business logic. 64 | duration := request.Duration.AsDuration() 65 | if duration != 0 { 66 | time.Sleep(duration) 67 | } 68 | 69 | // Imitate some work with slice to prevent compiler from optimizing out the slice. 70 | x := uint64(0) 71 | for i := 0; i < len(slice); i++ { 72 | x += uint64(slice[i]) 73 | } 74 | 75 | return &schema.MakeAllocationResponse{Value: x}, nil 76 | } 77 | 78 | func (srv *serverImpl) Run() error { 79 | endpoint := srv.cfg.ListenEndpoint 80 | 81 | listener, err := net.Listen("tcp", endpoint) 82 | if err != nil { 83 | return errors.Wrap(err, "net listen") 84 | } 85 | 86 | srv.logger.Info("starting listening", "endpoint", endpoint) 87 | 88 | if err = srv.grpcServer.Serve(listener); err != nil { 89 | return errors.Wrap(err, "grpc server serve") 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (srv *serverImpl) GRPCServer() *grpc.Server { return srv.grpcServer } 96 | 97 | func (srv *serverImpl) MemLimiter() memlimiter.Service { return srv.memLimiter } 98 | 99 | func (srv *serverImpl) Tracker() *tracker.Tracker { return srv.tracker } 100 | 101 | func (srv *serverImpl) Quit() { 102 | srv.logger.Info("terminating server") 103 | srv.grpcServer.Stop() 104 | srv.memLimiter.Quit() 105 | } 106 | 107 | // NewServer - server constructor. 108 | func NewServer(logger logr.Logger, cfg *Config, options ...grpc.ServerOption) (Server, error) { 109 | if err := prepare.Prepare(cfg); err != nil { 110 | return nil, errors.Wrap(err, "configs prepare") 111 | } 112 | 113 | memLimiter, err := memlimiter.NewServiceFromConfig(logger, cfg.MemLimiter) 114 | if err != nil { 115 | return nil, errors.Wrap(err, "new MemLimiter from config") 116 | } 117 | 118 | tr, err := tracker.NewTrackerFromConfig(logger, cfg.Tracker, memLimiter) 119 | if err != nil { 120 | return nil, errors.Wrap(err, "new tracker from config") 121 | } 122 | 123 | if cfg.MemLimiter != nil { 124 | options = append(options, 125 | grpc.UnaryInterceptor(memLimiter.Middleware().GRPC().MakeUnaryServerInterceptor()), 126 | grpc.StreamInterceptor(memLimiter.Middleware().GRPC().MakeStreamServerInterceptor()), 127 | ) 128 | } 129 | 130 | srv := &serverImpl{ 131 | logger: logger, 132 | cfg: cfg, 133 | memLimiter: memLimiter, 134 | grpcServer: grpc.NewServer(options...), 135 | tracker: tr, 136 | } 137 | 138 | schema.RegisterAllocatorServer(srv.grpcServer, srv) 139 | 140 | return srv, nil 141 | } 142 | -------------------------------------------------------------------------------- /test/allocator/tracker/backend.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | type backend interface { 10 | saveReport(*Report) error 11 | getReports() ([]*Report, error) 12 | quit() 13 | } 14 | -------------------------------------------------------------------------------- /test/allocator/tracker/backend_file.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "encoding/csv" 11 | "os" 12 | 13 | "github.com/go-logr/logr" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | var _ backend = (*backendFile)(nil) 18 | 19 | type backendFile struct { 20 | fd *os.File 21 | writer *csv.Writer 22 | logger logr.Logger 23 | } 24 | 25 | func (b *backendFile) saveReport(r *Report) error { 26 | if err := b.writer.Write(r.toCsv()); err != nil { 27 | return errors.Wrap(err, "csv write") 28 | } 29 | 30 | b.writer.Flush() 31 | 32 | if err := b.writer.Error(); err != nil { 33 | return errors.Wrap(err, "csv flush") 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (b *backendFile) getReports() ([]*Report, error) { 40 | return nil, errors.New("all reports are dumped to file immediately") 41 | } 42 | 43 | func (b *backendFile) quit() { 44 | if err := b.fd.Close(); err != nil { 45 | b.logger.Error(err, "close file") 46 | } 47 | } 48 | 49 | func newBackendFile(logger logr.Logger, cfg *ConfigBackendFile) (backend, error) { 50 | const perm = 0600 51 | 52 | fd, err := os.OpenFile(cfg.Path, os.O_CREATE|os.O_APPEND|os.O_WRONLY|os.O_SYNC|os.O_TRUNC, perm) 53 | if err != nil { 54 | return nil, errors.Wrap(err, "open file") 55 | } 56 | 57 | wr := csv.NewWriter(fd) 58 | 59 | if err := wr.Write(new(Report).headers()); err != nil { 60 | return nil, errors.Wrap(err, "write header") 61 | } 62 | 63 | return &backendFile{logger: logger, writer: wr, fd: fd}, nil 64 | } 65 | -------------------------------------------------------------------------------- /test/allocator/tracker/backend_memory.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | type backendMemory struct { 10 | reports []*Report 11 | } 12 | 13 | func (b *backendMemory) saveReport(r *Report) error { 14 | b.reports = append(b.reports, r) 15 | 16 | return nil 17 | } 18 | 19 | func (b *backendMemory) getReports() ([]*Report, error) { 20 | out := make([]*Report, len(b.reports)) 21 | copy(out, b.reports) 22 | 23 | return out, nil 24 | } 25 | 26 | func (b *backendMemory) quit() {} 27 | 28 | func newBackendMemory() backend { 29 | return &backendMemory{} 30 | } 31 | -------------------------------------------------------------------------------- /test/allocator/tracker/backend_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | "github.com/go-logr/logr/testr" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestBackend(t *testing.T) { 18 | logger := testr.New(t) 19 | 20 | cfg := &ConfigBackendFile{Path: "/tmp/backend.csv"} 21 | 22 | back, err := newBackendFile(logger, cfg) 23 | require.NoError(t, err) 24 | 25 | defer back.quit() 26 | 27 | reportsIn := []*Report{ 28 | { 29 | Timestamp: time.Now().String(), 30 | RSS: 1, 31 | Utilization: 2, 32 | GOGC: 3, 33 | Throttling: 4, 34 | }, 35 | { 36 | Timestamp: time.Now().String(), 37 | RSS: 2, 38 | Utilization: 3, 39 | GOGC: 4, 40 | Throttling: 5, 41 | }, 42 | } 43 | 44 | for _, rep := range reportsIn { 45 | err = back.saveReport(rep) 46 | require.NoError(t, err) 47 | } 48 | 49 | reportsOut, err := back.getReports() 50 | require.Len(t, reportsOut, 0) 51 | require.Error(t, err) 52 | } 53 | -------------------------------------------------------------------------------- /test/allocator/tracker/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ConfigBackendFile configures file backend of a Tracker. 15 | type ConfigBackendFile struct { 16 | Path string `json:"path"` 17 | } 18 | 19 | // Prepare validates config. 20 | func (c *ConfigBackendFile) Prepare() error { 21 | if c.Path == "" { 22 | return errors.New("empty path") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // ConfigBackendMemory configures memory backend of a Tracker. 29 | type ConfigBackendMemory struct { 30 | } 31 | 32 | // Config is a configuration of a tracker. 33 | type Config struct { 34 | BackendFile *ConfigBackendFile `json:"backend_file"` 35 | BackendMemory *ConfigBackendMemory `json:"backend_memory"` 36 | Period duration.Duration `json:"period"` 37 | } 38 | 39 | // Prepare validates config. 40 | func (c *Config) Prepare() error { 41 | if c.BackendFile == nil && c.BackendMemory == nil { 42 | return errors.New("empty backend sections") 43 | } 44 | 45 | if c.BackendFile != nil && c.BackendMemory != nil { 46 | return errors.New("more than one non-empty backend section") 47 | } 48 | 49 | if c.Period.Duration == 0 { 50 | return errors.New("empty period") 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /test/allocator/tracker/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 13 | ) 14 | 15 | func TestConfigBackendFile_Prepare(t *testing.T) { 16 | type fields struct { 17 | Path string 18 | } 19 | 20 | tests := []struct { 21 | name string 22 | fields fields 23 | wantErr bool 24 | }{ 25 | { 26 | name: "empty path", 27 | fields: fields{Path: ""}, 28 | wantErr: true, 29 | }, 30 | } 31 | 32 | for _, tt := range tests { 33 | tt := tt 34 | t.Run(tt.name, func(t *testing.T) { 35 | c := &ConfigBackendFile{ 36 | Path: tt.fields.Path, 37 | } 38 | if err := c.Prepare(); (err != nil) != tt.wantErr { 39 | t.Errorf("Prepare() error = %v, wantErr %v", err, tt.wantErr) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestConfig_Prepare(t *testing.T) { 46 | type fields struct { 47 | BackendFile *ConfigBackendFile 48 | BackendMemory *ConfigBackendMemory 49 | Period duration.Duration 50 | } 51 | 52 | tests := []struct { 53 | name string 54 | fields fields 55 | wantErr bool 56 | }{ 57 | { 58 | name: "empty backends", 59 | fields: fields{}, 60 | wantErr: true, 61 | }, 62 | { 63 | name: "non-empty backends", 64 | fields: fields{ 65 | BackendFile: new(ConfigBackendFile), 66 | BackendMemory: new(ConfigBackendMemory), 67 | }, 68 | wantErr: true, 69 | }, 70 | { 71 | name: "invalid duration", 72 | fields: fields{ 73 | BackendFile: new(ConfigBackendFile), 74 | Period: duration.Duration{Duration: 0}, 75 | }, 76 | wantErr: true, 77 | }, 78 | } 79 | 80 | for _, tt := range tests { 81 | tt := tt 82 | t.Run(tt.name, func(t *testing.T) { 83 | c := &Config{ 84 | BackendFile: tt.fields.BackendFile, 85 | BackendMemory: tt.fields.BackendMemory, 86 | Period: tt.fields.Period, 87 | } 88 | if err := c.Prepare(); (err != nil) != tt.wantErr { 89 | t.Errorf("Prepare() error = %v, wantErr %v", err, tt.wantErr) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/allocator/tracker/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package tracker contains logic of service stats persistence. This is for the sake of making graphs for README. 8 | package tracker 9 | -------------------------------------------------------------------------------- /test/allocator/tracker/report.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | // Report is a memory consumption report (used only for tests). 14 | type Report struct { 15 | Timestamp string 16 | RSS uint64 17 | Utilization float64 18 | GOGC int 19 | Throttling uint32 20 | } 21 | 22 | func (r *Report) headers() []string { 23 | return []string{ 24 | "timestamp", 25 | "rss", 26 | "utilization", 27 | "gogc", 28 | "throttling", 29 | } 30 | } 31 | 32 | func (r *Report) toCsv() []string { 33 | return []string{ 34 | r.Timestamp, 35 | fmt.Sprint(r.RSS), 36 | fmt.Sprint(r.Utilization), 37 | fmt.Sprint(r.GOGC), 38 | fmt.Sprint(r.Throttling), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/allocator/tracker/tracker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package tracker 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/go-logr/logr" 13 | "github.com/newcloudtechnologies/memlimiter" 14 | "github.com/newcloudtechnologies/memlimiter/utils/breaker" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Tracker is responsible for service stats persistence. 19 | type Tracker struct { 20 | backend backend 21 | memLimiter memlimiter.Service 22 | cfg *Config 23 | breaker *breaker.Breaker 24 | logger logr.Logger 25 | } 26 | 27 | func (tr *Tracker) makeReport() (*Report, error) { 28 | out := &Report{} 29 | 30 | out.Timestamp = time.Now().Format(time.RFC3339Nano) 31 | 32 | mlStats, err := tr.memLimiter.GetStats() 33 | if err != nil { 34 | return nil, errors.Wrap(err, "memlimiter stats") 35 | } 36 | 37 | if mlStats != nil { 38 | out.RSS = mlStats.Controller.MemoryBudget.RSSActual 39 | out.Utilization = mlStats.Controller.MemoryBudget.Utilization 40 | 41 | if mlStats.Backpressure != nil { 42 | out.GOGC = mlStats.Backpressure.ControlParameters.GOGC 43 | out.Throttling = mlStats.Backpressure.ControlParameters.ThrottlingPercentage 44 | } 45 | } 46 | 47 | return out, nil 48 | } 49 | 50 | func (tr *Tracker) dumpReport() error { 51 | r, err := tr.makeReport() 52 | if err != nil { 53 | return errors.Wrap(err, "dump Report") 54 | } 55 | 56 | if err = tr.backend.saveReport(r); err != nil { 57 | return errors.Wrap(err, "backend save Report") 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (tr *Tracker) loop() { 64 | defer tr.breaker.Dec() 65 | 66 | ticker := time.NewTicker(tr.cfg.Period.Duration) 67 | defer ticker.Stop() 68 | 69 | for { 70 | select { 71 | case <-ticker.C: 72 | if err := tr.dumpReport(); err != nil { 73 | tr.logger.Error(err, "dump Report") 74 | } 75 | case <-tr.breaker.Done(): 76 | return 77 | } 78 | } 79 | } 80 | 81 | // GetReports returns the accumulated reports. 82 | func (tr *Tracker) GetReports() ([]*Report, error) { 83 | out, err := tr.backend.getReports() 84 | if err != nil { 85 | return nil, errors.Wrap(err, "backend get reports") 86 | } 87 | 88 | return out, nil 89 | } 90 | 91 | // Quit gracefully terminates tracker. 92 | func (tr *Tracker) Quit() { 93 | tr.breaker.ShutdownAndWait() 94 | tr.backend.quit() 95 | } 96 | 97 | // NewTrackerFromConfig is a constructor of a Tracker. 98 | func NewTrackerFromConfig(logger logr.Logger, cfg *Config, memLimiter memlimiter.Service) (*Tracker, error) { 99 | var ( 100 | back backend 101 | err error 102 | ) 103 | 104 | switch { 105 | case cfg.BackendFile != nil: 106 | back, err = newBackendFile(logger, cfg.BackendFile) 107 | case cfg.BackendMemory != nil: 108 | back = newBackendMemory() 109 | default: 110 | return nil, errors.New("unexpected backend type") 111 | } 112 | 113 | if err != nil { 114 | return nil, errors.Wrap(err, "new backend") 115 | } 116 | 117 | tr := &Tracker{ 118 | backend: back, 119 | logger: logger, 120 | cfg: cfg, 121 | memLimiter: memLimiter, 122 | breaker: breaker.NewBreakerWithInitValue(1), 123 | } 124 | 125 | go tr.loop() 126 | 127 | return tr, nil 128 | } 129 | -------------------------------------------------------------------------------- /test/integration/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package integration 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | "code.cloudfoundry.org/bytefmt" 14 | "github.com/aclements/go-moremath/stats" 15 | "github.com/go-logr/logr" 16 | "github.com/go-logr/logr/testr" 17 | "github.com/newcloudtechnologies/memlimiter" 18 | "github.com/newcloudtechnologies/memlimiter/controller/nextgc" 19 | "github.com/newcloudtechnologies/memlimiter/test/allocator/perf" 20 | "github.com/newcloudtechnologies/memlimiter/test/allocator/server" 21 | "github.com/newcloudtechnologies/memlimiter/test/allocator/tracker" 22 | "github.com/newcloudtechnologies/memlimiter/utils/config/bytes" 23 | "github.com/newcloudtechnologies/memlimiter/utils/config/duration" 24 | "github.com/pkg/errors" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestComponent(t *testing.T) { 29 | const endpoint = "0.0.0.0:1988" 30 | 31 | logger := testr.New(t) 32 | 33 | const rssLimit = bytefmt.GIGABYTE 34 | 35 | allocatorServer, err := makeServer(logger, endpoint, rssLimit) 36 | require.NoError(t, err) 37 | 38 | defer allocatorServer.Quit() 39 | 40 | go func() { 41 | if errRun := allocatorServer.Run(); errRun != nil { 42 | logger.Error(errRun, "server run") 43 | } 44 | }() 45 | 46 | // wait for a while to make server run asynchronously 47 | time.Sleep(time.Second) 48 | 49 | perfClient, err := makePerfClient(logger, endpoint) 50 | require.NoError(t, err) 51 | 52 | // perform load 53 | err = perfClient.Run() 54 | require.NoError(t, err) 55 | 56 | defer perfClient.Quit() 57 | 58 | // collect reports 59 | reports, err := allocatorServer.Tracker().GetReports() 60 | require.NoError(t, err) 61 | require.NotEmpty(t, reports) 62 | 63 | analyzeReports(t, reports, rssLimit) 64 | } 65 | 66 | func makeServer(logger logr.Logger, endpoint string, rssLimit uint64) (server.Server, error) { 67 | cfg := &server.Config{ 68 | MemLimiter: &memlimiter.Config{ControllerNextGC: &nextgc.ControllerConfig{ 69 | RSSLimit: bytes.Bytes{Value: rssLimit}, 70 | DangerZoneGOGC: 50, 71 | DangerZoneThrottling: 90, 72 | Period: duration.Duration{Duration: time.Second}, 73 | ComponentProportional: &nextgc.ComponentProportionalConfig{ 74 | Coefficient: 20, 75 | WindowSize: 20, 76 | }, 77 | }}, 78 | Tracker: &tracker.Config{ 79 | BackendMemory: &tracker.ConfigBackendMemory{}, 80 | Period: duration.Duration{Duration: time.Second}, 81 | }, 82 | ListenEndpoint: endpoint, 83 | } 84 | 85 | allocatorServer, err := server.NewServer(logger, cfg) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "perf client") 88 | } 89 | 90 | return allocatorServer, nil 91 | } 92 | 93 | func makePerfClient(logger logr.Logger, endpoint string) (*perf.Client, error) { 94 | cfg := &perf.Config{ 95 | Endpoint: endpoint, 96 | RPS: 100, 97 | LoadDuration: duration.Duration{Duration: 20 * time.Second}, 98 | AllocationSize: bytes.Bytes{Value: bytefmt.MEGABYTE}, 99 | PauseDuration: duration.Duration{Duration: 5 * time.Second}, 100 | RequestTimeout: duration.Duration{Duration: 1 * time.Minute}, 101 | } 102 | 103 | perfClient, err := perf.NewClient(logger, cfg) 104 | if err != nil { 105 | return nil, errors.Wrap(err, "perf client") 106 | } 107 | 108 | return perfClient, nil 109 | } 110 | 111 | func analyzeReports(t *testing.T, reports []*tracker.Report, rssLimit float64) { 112 | t.Helper() 113 | 114 | sample := &stats.Sample{} 115 | 116 | // take only the second half of observations as we expect memory consumption to be stable here due to MemLimiter work 117 | reports = reports[len(reports)/2:] 118 | 119 | for _, r := range reports { 120 | sample.Xs = append(sample.Xs, float64(r.RSS)) 121 | } 122 | 123 | // We can only expect that the memory consumption wouldn't be greater than an 124 | // upper consumption limit (RSSLimit), but we cannot predict the exact value 125 | // because of possible existence of a SWAP partition. 126 | actualRSS := sample.Mean() 127 | 128 | // but since this is a soft limit, we allow small exceeding of it 129 | require.Less(t, actualRSS, 1.10*rssLimit) 130 | } 131 | -------------------------------------------------------------------------------- /utils/breaker/breaker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package breaker 8 | 9 | import ( 10 | "runtime" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | const ( 18 | operational int32 = iota + 1 19 | shutdown 20 | ) 21 | 22 | // Breaker can be used to stop any subsystem with background tasks gracefully. 23 | type Breaker struct { 24 | exitChan chan struct{} 25 | count int64 26 | mode int32 27 | } 28 | 29 | // Inc increments number of tasks. 30 | func (b *Breaker) Inc() error { 31 | if !b.IsOperational() { 32 | return errors.New("shutdown in progress") 33 | } 34 | 35 | atomic.AddInt64(&b.count, 1) 36 | 37 | return nil 38 | } 39 | 40 | // Dec decrements number of tasks. 41 | func (b *Breaker) Dec() { 42 | atomic.AddInt64(&b.count, -1) 43 | } 44 | 45 | // IsOperational checks whether breaker is in operational mode. 46 | func (b *Breaker) IsOperational() bool { return atomic.LoadInt32(&b.mode) == operational } 47 | 48 | // Wait blocks until the number of tasks becomes equal to zero. 49 | func (b *Breaker) Wait() { 50 | if atomic.LoadInt32(&b.mode) != shutdown { 51 | panic("cannot wait on operational Breaker, turn it off first") 52 | } 53 | 54 | for { 55 | if atomic.LoadInt64(&b.count) == 0 { 56 | break 57 | } 58 | 59 | runtime.Gosched() 60 | } 61 | } 62 | 63 | // Shutdown switches breaker in shutdown mode. 64 | func (b *Breaker) Shutdown() { 65 | if atomic.CompareAndSwapInt32(&b.mode, operational, shutdown) { 66 | // notify channel subscribers about termination 67 | close(b.exitChan) 68 | } 69 | } 70 | 71 | // ShutdownAndWait switches breakers in shutdown mode and 72 | // waits for all background tasks to terminate. 73 | func (b *Breaker) ShutdownAndWait() { 74 | b.Shutdown() 75 | b.Wait() 76 | } 77 | 78 | // Deadline implemented for the sake of compatibility with context.Context. 79 | func (b *Breaker) Deadline() (deadline time.Time, ok bool) { 80 | return time.Time{}, false 81 | } 82 | 83 | // Value implemented for the sake of compatibility with context.Context. 84 | func (b *Breaker) Value(key interface{}) interface{} { return nil } 85 | 86 | // Done returns channel which can be used in a manner similar to context.Context.Done(). 87 | func (b *Breaker) Done() <-chan struct{} { return b.exitChan } 88 | 89 | // ErrNotOperational tells that Breaker has been shut down. 90 | var ErrNotOperational = errors.New("breaker is not operational") 91 | 92 | // Err returns error which can be used in a manner similar to context.Context.Done(). 93 | func (b *Breaker) Err() error { 94 | if b.IsOperational() { 95 | return nil 96 | } 97 | 98 | return ErrNotOperational 99 | } 100 | 101 | // NewBreaker - default breaker constructor. 102 | func NewBreaker() *Breaker { 103 | return &Breaker{ 104 | count: 0, 105 | mode: operational, 106 | exitChan: make(chan struct{}), 107 | } 108 | } 109 | 110 | // NewBreakerWithInitValue - alternative breaker constructor convenient for usage 111 | // in pools and actors, when you know how many goroutines will work from the very beginning. 112 | func NewBreakerWithInitValue(value int64) *Breaker { 113 | b := NewBreaker() 114 | b.count = value 115 | 116 | return b 117 | } 118 | -------------------------------------------------------------------------------- /utils/breaker/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package breaker contains useful thread-safe abstraction the helps to control lifetime 8 | // of actors, background tasks, pools etc. 9 | package breaker 10 | -------------------------------------------------------------------------------- /utils/config/bytes/bytes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package bytes 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | 13 | "code.cloudfoundry.org/bytefmt" 14 | ) 15 | 16 | // Bytes helps to represent human-readable size values in JSON. 17 | type Bytes struct { 18 | Value uint64 19 | } 20 | 21 | // UnmarshalJSON - JSON deserializer. 22 | func (b *Bytes) UnmarshalJSON(data []byte) (err error) { 23 | var s string 24 | 25 | if err = json.Unmarshal(data, &s); err != nil { 26 | return 27 | } 28 | 29 | if s == "0" { 30 | return 31 | } 32 | 33 | b.Value, err = bytefmt.ToBytes(s) 34 | 35 | return 36 | } 37 | 38 | // MarshalJSON - JSON serializer. 39 | func (b Bytes) MarshalJSON() ([]byte, error) { 40 | str := fmt.Sprintf("\"%s\"", bytefmt.ByteSize(b.Value)) 41 | 42 | return []byte(str), nil 43 | } 44 | -------------------------------------------------------------------------------- /utils/config/bytes/bytes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package bytes 8 | 9 | import ( 10 | "encoding/json" 11 | "testing" 12 | 13 | "code.cloudfoundry.org/bytefmt" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type testStruct struct { 18 | Size Bytes `json:"size"` 19 | } 20 | 21 | func TestSize_UnmarshalJSON(t *testing.T) { 22 | var ts testStruct 23 | 24 | data := []byte(`{"size": "20M"}`) 25 | assert.NoError(t, json.Unmarshal(data, &ts)) 26 | assert.Equal(t, uint64(20*bytefmt.MEGABYTE), ts.Size.Value) 27 | 28 | data = []byte(`{"size":"invalid"}`) 29 | assert.Error(t, json.Unmarshal(data, &ts)) 30 | 31 | data = []byte(`{"size":"30MB"}`) 32 | assert.NoError(t, json.Unmarshal(data, &ts)) 33 | assert.Equal(t, uint64(30*bytefmt.MEGABYTE), ts.Size.Value) 34 | 35 | data = []byte(`{"size":"40K"}`) 36 | assert.NoError(t, json.Unmarshal(data, &ts)) 37 | assert.Equal(t, uint64(40*bytefmt.KILOBYTE), ts.Size.Value) 38 | 39 | data = []byte(`{"size":"50KB"}`) 40 | assert.NoError(t, json.Unmarshal(data, &ts)) 41 | assert.Equal(t, uint64(50*bytefmt.KILOBYTE), ts.Size.Value) 42 | 43 | // also check lowercase 44 | data = []byte(`{"size":"50kb"}`) 45 | assert.NoError(t, json.Unmarshal(data, &ts)) 46 | assert.Equal(t, uint64(50*bytefmt.KILOBYTE), ts.Size.Value) 47 | } 48 | 49 | func TestSize_MarshalJSON(t *testing.T) { 50 | var ts testStruct 51 | 52 | ts.Size = Bytes{Value: 20 * bytefmt.MEGABYTE} 53 | data, err := json.Marshal(&ts) 54 | assert.NoError(t, err) 55 | assert.Equal(t, []byte(`{"size":"20M"}`), data) 56 | 57 | ts.Size = Bytes{Value: 40 * bytefmt.KILOBYTE} 58 | data, err = json.Marshal(&ts) 59 | assert.NoError(t, err) 60 | assert.Equal(t, []byte(`{"size":"40K"}`), data) 61 | 62 | ts.Size = Bytes{Value: 1 * bytefmt.BYTE} 63 | data, err = json.Marshal(&ts) 64 | assert.NoError(t, err) 65 | assert.Equal(t, []byte(`{"size":"1B"}`), data) 66 | } 67 | 68 | func TestBytesByValue(t *testing.T) { 69 | type masterStructVal struct { 70 | T testStruct `json:"t"` 71 | } 72 | 73 | var ms masterStructVal 74 | 75 | data := []byte(`{"t":{"size":"20M"}}`) 76 | assert.NoError(t, json.Unmarshal(data, &ms)) 77 | assert.Equal(t, uint64(20*bytefmt.MEGABYTE), ms.T.Size.Value) 78 | 79 | dump, err := json.Marshal(&ms) 80 | assert.NoError(t, err) 81 | assert.Equal(t, []byte(`{"t":{"size":"20M"}}`), dump) 82 | } 83 | 84 | func TestBytesByPointer(t *testing.T) { 85 | type masterStructPtr struct { 86 | T *testStruct `json:"t"` 87 | } 88 | 89 | var ms masterStructPtr 90 | 91 | data := []byte(`{"t":{"size":"20M"}}`) 92 | assert.NoError(t, json.Unmarshal(data, &ms)) 93 | assert.Equal(t, uint64(20*bytefmt.MEGABYTE), ms.T.Size.Value) 94 | 95 | dump, err := json.Marshal(&ms) 96 | assert.NoError(t, err) 97 | assert.Equal(t, []byte(`{"t":{"size":"20M"}}`), dump) 98 | } 99 | 100 | func TestBytesZeroValue(t *testing.T) { 101 | var ts testStruct 102 | 103 | data := []byte(`{"size": "0"}`) 104 | assert.NoError(t, json.Unmarshal(data, &ts)) 105 | assert.Equal(t, uint64(0*bytefmt.MEGABYTE), ts.Size.Value) 106 | } 107 | -------------------------------------------------------------------------------- /utils/config/bytes/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package bytes helps to represent human-readable size values in JSON. 8 | package bytes 9 | -------------------------------------------------------------------------------- /utils/config/duration/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package duration helps to represent human-readable duration values in JSON. 8 | package duration 9 | -------------------------------------------------------------------------------- /utils/config/duration/duration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package duration 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "time" 13 | ) 14 | 15 | // Duration helps to represent human-readable duration values in JSON. 16 | type Duration struct { 17 | time.Duration 18 | } 19 | 20 | // UnmarshalJSON - JSON deserializer. 21 | func (d *Duration) UnmarshalJSON(data []byte) (err error) { 22 | var s string 23 | 24 | if err = json.Unmarshal(data, &s); err != nil { 25 | return 26 | } 27 | 28 | d.Duration, err = time.ParseDuration(s) 29 | 30 | return 31 | } 32 | 33 | // MarshalJSON - JSON serializer. 34 | func (d Duration) MarshalJSON() ([]byte, error) { 35 | s := fmt.Sprintf("\"%s\"", d.Duration.String()) 36 | 37 | return []byte(s), nil 38 | } 39 | -------------------------------------------------------------------------------- /utils/config/duration/duration_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package duration 8 | 9 | import ( 10 | "encoding/json" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type testStruct struct { 18 | Timeout Duration `json:"timeout"` 19 | } 20 | 21 | func TestDuration_UnmarshalJSON(t *testing.T) { 22 | var ts testStruct 23 | 24 | data := []byte(`{ "timeout": "2ns" }`) 25 | assert.NoError(t, json.Unmarshal(data, &ts)) 26 | assert.Equal(t, 2*time.Nanosecond, ts.Timeout.Duration) 27 | 28 | data = []byte(`{ "timeout": "2ms" }`) 29 | assert.NoError(t, json.Unmarshal(data, &ts)) 30 | assert.Equal(t, 2*time.Millisecond, ts.Timeout.Duration) 31 | 32 | data = []byte(`{ "timeout": "2s" }`) 33 | assert.NoError(t, json.Unmarshal(data, &ts)) 34 | assert.Equal(t, 2*time.Second, ts.Timeout.Duration) 35 | 36 | data = []byte(`{ "timeout": "2m" }`) 37 | assert.NoError(t, json.Unmarshal(data, &ts)) 38 | assert.Equal(t, 2*time.Minute, ts.Timeout.Duration) 39 | 40 | data = []byte(`{ "timeout": "2h" }`) 41 | assert.NoError(t, json.Unmarshal(data, &ts)) 42 | assert.Equal(t, 2*time.Hour, ts.Timeout.Duration) 43 | 44 | data = []byte(`{ "timeout": "invalid" }`) 45 | assert.Error(t, json.Unmarshal(data, &ts)) 46 | } 47 | 48 | func TestDuration_MarshalJSON(t *testing.T) { 49 | var ( 50 | ts testStruct 51 | dump []byte 52 | err error 53 | ) 54 | 55 | ts.Timeout = Duration{Duration: 2 * time.Nanosecond} 56 | dump, err = json.Marshal(&ts) 57 | assert.NoError(t, err) 58 | assert.Equal(t, []byte(`{"timeout":"2ns"}`), dump) 59 | 60 | ts.Timeout = Duration{Duration: 2 * time.Millisecond} 61 | dump, err = json.Marshal(&ts) 62 | assert.NoError(t, err) 63 | assert.Equal(t, []byte(`{"timeout":"2ms"}`), dump) 64 | 65 | ts.Timeout = Duration{Duration: 2 * time.Second} 66 | dump, err = json.Marshal(&ts) 67 | assert.NoError(t, err) 68 | assert.Equal(t, []byte(`{"timeout":"2s"}`), dump) 69 | 70 | ts.Timeout = Duration{Duration: 2 * time.Minute} 71 | dump, err = json.Marshal(&ts) 72 | assert.NoError(t, err) 73 | assert.Equal(t, []byte(`{"timeout":"2m0s"}`), dump) 74 | 75 | ts.Timeout = Duration{Duration: 2 * time.Hour} 76 | dump, err = json.Marshal(&ts) 77 | assert.NoError(t, err) 78 | assert.Equal(t, []byte(`{"timeout":"2h0m0s"}`), dump) 79 | } 80 | 81 | func TestDurationByValue(t *testing.T) { 82 | type masterStructVal struct { 83 | T testStruct `json:"t"` 84 | } 85 | 86 | var ms masterStructVal 87 | 88 | data := []byte(`{"t":{"timeout":"2ns"}}`) 89 | assert.NoError(t, json.Unmarshal(data, &ms)) 90 | assert.Equal(t, 2*time.Nanosecond, ms.T.Timeout.Duration) 91 | 92 | dump, err := json.Marshal(&ms) 93 | assert.NoError(t, err) 94 | assert.Equal(t, []byte(`{"t":{"timeout":"2ns"}}`), dump) 95 | } 96 | 97 | func TestDurationByPointer(t *testing.T) { 98 | type masterStructPtr struct { 99 | T *testStruct `json:"t"` 100 | } 101 | 102 | var ms masterStructPtr 103 | 104 | data := []byte(`{"t":{"timeout":"2ns"}}`) 105 | assert.NoError(t, json.Unmarshal(data, &ms)) 106 | assert.Equal(t, 2*time.Nanosecond, ms.T.Timeout.Duration) 107 | 108 | dump, err := json.Marshal(&ms) 109 | assert.NoError(t, err) 110 | assert.Equal(t, []byte(`{"t":{"timeout":"2ns"}}`), dump) 111 | } 112 | 113 | func TestDurationZeroValue(t *testing.T) { 114 | var ts testStruct 115 | 116 | data := []byte(`{"size": "0"}`) 117 | assert.NoError(t, json.Unmarshal(data, &ts)) 118 | assert.Equal(t, 0*time.Second, ts.Timeout.Duration) 119 | } 120 | -------------------------------------------------------------------------------- /utils/config/prepare/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package prepare provides a function to validate configuration recursively. 8 | package prepare 9 | -------------------------------------------------------------------------------- /utils/config/prepare/prepare.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package prepare 8 | 9 | import ( 10 | "reflect" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | tagName = "json" 17 | prepareTagName = "prepare" 18 | optValue = "optional" 19 | ) 20 | 21 | // Preparer is used for recursive validation of configuration structures. 22 | type Preparer interface { 23 | // Prepare validates something. 24 | Prepare() error 25 | } 26 | 27 | // Prepare calls Prepare() method on the object and its fields recursively. 28 | func Prepare(src interface{}) error { 29 | if src == nil { 30 | return nil 31 | } 32 | 33 | v := reflect.ValueOf(src) 34 | 35 | pr, ok := src.(Preparer) 36 | if ok { 37 | err := pr.Prepare() 38 | if err != nil { 39 | return errors.Wrap(err, "prepare error") 40 | } 41 | } 42 | 43 | return traverse(v, true) 44 | } 45 | 46 | //nolint:gocognit,gocyclo,exhaustive,cyclop 47 | func traverse(v reflect.Value, parentTraversed bool) (err error) { 48 | switch v.Kind() { 49 | case reflect.Interface, reflect.Ptr: 50 | if !v.IsNil() && v.CanInterface() { 51 | if err := tryPrepareInterface(v.Interface()); err != nil { 52 | return err 53 | } 54 | 55 | if err := traverse(v.Elem(), true); err != nil { 56 | return err 57 | } 58 | } 59 | case reflect.Struct: 60 | if !parentTraversed && v.CanInterface() { 61 | if err := tryPrepareInterface(v.Interface()); err != nil { 62 | return err 63 | } 64 | } 65 | 66 | for j := 0; j < v.NumField(); j++ { 67 | optTag := v.Type().Field(j).Tag.Get(prepareTagName) 68 | if optTag == optValue && v.Field(j).IsNil() { 69 | continue 70 | } 71 | 72 | err := traverse(v.Field(j), false) 73 | if err != nil { 74 | tagValue := v.Type().Field(j).Tag.Get(tagName) 75 | 76 | return errors.Errorf("invalid section '%s': %v", tagValue, err) 77 | } 78 | 79 | // call Prepare() on children. 80 | child := v.Field(j) 81 | if child.CanAddr() { 82 | if child.Addr().MethodByName("Prepare").Kind() != reflect.Invalid { 83 | child.Addr().MethodByName("Prepare").Call([]reflect.Value{}) 84 | } 85 | } 86 | } 87 | default: 88 | if v.CanInterface() { 89 | return tryPrepareInterface(v.Interface()) 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func tryPrepareInterface(v interface{}) (err error) { 97 | pr, ok := v.(Preparer) 98 | if ok { 99 | err = pr.Prepare() 100 | } 101 | 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /utils/counter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | go_metrics "github.com/rcrowley/go-metrics" 11 | ) 12 | 13 | var _ Counter = (*childCounter)(nil) 14 | 15 | // Counter - thread-safe metrics counter. 16 | type Counter interface { 17 | go_metrics.Counter 18 | } 19 | 20 | // childCounter allows to construct hierarchical counters. 21 | type childCounter struct { 22 | Counter 23 | parent Counter 24 | } 25 | 26 | func (counter *childCounter) Dec(i int64) { 27 | counter.parent.Dec(i) 28 | counter.Counter.Dec(i) 29 | } 30 | 31 | func (counter *childCounter) Inc(i int64) { 32 | counter.parent.Inc(i) 33 | counter.Counter.Inc(i) 34 | } 35 | 36 | // NewCounter creates counter referring to parent counter. 37 | // If parent is nil, the root in hierarchy is created. 38 | func NewCounter(parent Counter) Counter { 39 | if parent == nil { 40 | return go_metrics.NewCounter() 41 | } 42 | 43 | return &childCounter{Counter: go_metrics.NewCounter(), parent: parent} 44 | } 45 | -------------------------------------------------------------------------------- /utils/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | // Package utils provides various utilities and helpers. 8 | package utils 9 | -------------------------------------------------------------------------------- /utils/math.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package utils 8 | 9 | // ClampFloat64 limits the provided value according to the given range. 10 | // Origin: https://docs.unity3d.com/ScriptReference/Mathf.Clamp.html. 11 | func ClampFloat64(value, min, max float64) float64 { 12 | if value < min { 13 | return min 14 | } 15 | 16 | if value > max { 17 | return max 18 | } 19 | 20 | return value 21 | } 22 | -------------------------------------------------------------------------------- /utils/math_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) New Cloud Technologies, Ltd. 2013-2022. 3 | * Author: Vitaly Isaev 4 | * License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE 5 | */ 6 | 7 | package utils 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestClampFloat64(t *testing.T) { 14 | type args struct { 15 | value float64 16 | min float64 17 | max float64 18 | } 19 | 20 | tests := []struct { 21 | name string 22 | args args 23 | want float64 24 | }{ 25 | { 26 | name: "less", 27 | args: args{ 28 | value: -1, 29 | min: 0, 30 | max: 100, 31 | }, 32 | want: 0, 33 | }, 34 | { 35 | name: "middle", 36 | args: args{ 37 | value: 50, 38 | min: 0, 39 | max: 100, 40 | }, 41 | want: 50, 42 | }, 43 | { 44 | name: "greater", 45 | args: args{ 46 | value: 101, 47 | min: 0, 48 | max: 100, 49 | }, 50 | want: 100, 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | tt := tt 56 | t.Run(tt.name, func(t *testing.T) { 57 | if got := ClampFloat64(tt.args.value, tt.args.min, tt.args.max); got != tt.want { 58 | t.Errorf("ClampFloat64() = %v, want %v", got, tt.want) 59 | } 60 | }) 61 | } 62 | } 63 | --------------------------------------------------------------------------------