├── .dockerignore ├── .github └── workflows │ ├── go.yaml │ ├── golangci-ling.yaml │ └── release.yaml ├── .gitignore ├── .golangci.pipeline.yaml ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── benchmark_chart.png └── logo.png ├── benchmarks ├── dumb-server │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── api │ │ └── api.proto │ ├── docker-compose.yaml │ ├── main.go │ └── pb │ │ ├── api.pb.go │ │ └── api_grpc.pb.go ├── framer │ ├── README.md │ ├── docker-compose.yaml │ ├── requests.bin │ └── run.sh ├── gatling │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── build.gradle.kts │ ├── docker-compose.yaml │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── run.sh │ └── src │ │ ├── gatling │ │ ├── kotlin │ │ │ └── bench │ │ │ │ └── BenchKt.kt │ │ └── resources │ │ │ └── logback.xml │ │ └── main │ │ └── proto │ │ └── benchmark.proto ├── ghz │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yaml │ ├── ghz-data.bin │ └── run.sh ├── h2load │ ├── Dockerfile │ ├── README.md │ ├── data │ ├── docker-compose.yaml │ └── run.sh ├── jmeter-java-dsl │ ├── .gitignore │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── Dockerfile │ ├── Makefile │ ├── REAME.md │ ├── docker-compose.yaml │ ├── mvnw │ ├── pom.xml │ ├── run.sh │ └── src │ │ ├── main │ │ └── resources │ │ │ └── api │ │ │ └── api.proto │ │ └── test │ │ └── java │ │ └── PerformanceIT.java ├── k6 │ ├── README.md │ ├── docker-compose.yaml │ ├── run.sh │ └── script.js └── pandora │ ├── Dockerfile │ ├── README.md │ ├── config.yaml │ ├── docker-compose.yaml │ ├── requests.json │ └── run.sh ├── cmd └── framer │ ├── cmd_convert.go │ ├── cmd_load.go │ ├── main.go │ └── main_benchmark_test.go ├── consts └── consts.go ├── datasource ├── cyclic-reader.go ├── cyclic-reader_test.go ├── decoder │ ├── decoder.go │ ├── jsonkv_safe.go │ └── model.go ├── file.go ├── file_benchmark_test.go ├── inmem.go ├── inmem_benchmark_test.go ├── meta_middleware.go ├── request.go └── request_test.go ├── examples └── requestsgen │ ├── go.mod │ ├── go.sum │ └── main.go ├── formats ├── converter │ └── processor.go ├── grpc │ ├── convert_strategy.go │ ├── convert_test.go │ ├── middleware.go │ ├── ozon │ │ ├── binary │ │ │ ├── encoding │ │ │ │ ├── binary_test.go │ │ │ │ ├── decoder.go │ │ │ │ └── encoder.go │ │ │ ├── format.go │ │ │ └── io │ │ │ │ ├── io.go │ │ │ │ └── io_test.go │ │ └── json │ │ │ ├── encoding │ │ │ ├── decoder.go │ │ │ ├── encoder.go │ │ │ ├── json_test.go │ │ │ ├── reflection │ │ │ │ ├── reflector.go │ │ │ │ ├── store.go │ │ │ │ └── store_test.go │ │ │ └── testproto │ │ │ │ ├── service.pb.go │ │ │ │ └── service.proto │ │ │ ├── format.go │ │ │ └── io │ │ │ ├── io.go │ │ │ └── io_test.go │ ├── pandora │ │ └── json │ │ │ └── format.go │ └── test_files │ │ ├── requests.ozon.binary │ │ ├── requests.ozon.json │ │ └── requests.pandora.json ├── internal │ ├── json │ │ ├── escape.go │ │ └── escape_test.go │ ├── kv │ │ ├── encoding.go │ │ └── json │ │ │ ├── jsonkv.go │ │ │ ├── jsonkv_multi_benchmark_test.go │ │ │ ├── jsonkv_multi_test.go │ │ │ ├── jsonkv_single_benchmark_test.go │ │ │ └── jsonkv_single_test.go │ └── pooledreader │ │ └── pooledreader.go └── model │ └── model.go ├── frameheader └── frameheader.go ├── go.mod ├── go.sum ├── loader ├── e2e_test.go ├── flowcontrol │ └── flowcontrol.go ├── loader.go ├── reciever │ ├── framer.go │ ├── framer_test.go │ ├── mock_generate_test.go │ ├── mock_loader_types_test.go │ ├── mock_reciever_test.go │ ├── processor.go │ ├── processor_benchamrk_test.go │ ├── processor_test.go │ └── reciever.go ├── sender │ └── sender.go ├── streams │ ├── limiter │ │ └── limiter.go │ ├── pool │ │ ├── pool.go │ │ └── pool_benchmark_test.go │ └── store │ │ └── store.go ├── timeout_queue.go └── types │ ├── flow_control.go │ ├── req.go │ └── stream.go ├── report ├── multi │ └── multi.go ├── noop │ └── noop.go ├── phout │ ├── phout.go │ └── phout_test.go ├── simple │ └── simple.go └── supersimple │ └── supersimple.go ├── scheduler └── scheduler.go ├── test_files └── requests └── utils ├── grpc ├── encode_duration.go └── encode_duration_test.go ├── hpack_wrapper └── wrapper.go ├── lru ├── lru.go └── lru_test.go └── pool └── pool.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | LICENSE 3 | docker-compose.yaml 4 | *.md 5 | assets 6 | Dockerfile 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.22.x' 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/golangci-ling.yaml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-20.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v6.1.0 25 | with: 26 | version: v1.60.1 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: '1.22.X' 24 | cache: true 25 | 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v4 28 | with: 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | phout.log 3 | *.test 4 | *.out 5 | 6 | dist/ 7 | -------------------------------------------------------------------------------- /.golangci.pipeline.yaml: -------------------------------------------------------------------------------- 1 | # More info on config here: https://golangci-lint.run/usage/configuration/#config-file 2 | run: 3 | concurrency: 8 4 | timeout: 10m 5 | issues-exit-code: 1 6 | tests: true 7 | skip-dirs: 8 | - bin 9 | - vendor 10 | - var 11 | - tmp 12 | skip-files: 13 | - \.pb\.go$ 14 | - \.pb\.gw\.go$ 15 | - \.pb\.scratch\.go$ 16 | - \.pb\.goclay\.go$ 17 | 18 | output: 19 | format: colored-line-number 20 | print-issued-lines: true 21 | print-linter-name: true 22 | 23 | linters-settings: 24 | dupl: 25 | threshold: 100 26 | goconst: 27 | min-len: 2 28 | min-occurrences: 3 29 | govet: 30 | check-shadowing: true 31 | shadow: 32 | strict: false 33 | 34 | linters: 35 | disable-all: true 36 | enable: 37 | # - dupl - it's very slow, enable if you really know why you need it 38 | - errcheck 39 | - goconst 40 | - goimports 41 | - gosec 42 | - govet 43 | - ineffassign 44 | - megacheck # (staticcheck + gosimple + unused in one linter) 45 | - revive 46 | - typecheck 47 | - unused # will be used insted of varcheck + deadcode + structcheck. More info https://github.com/golangci/golangci-lint/issues/1841 48 | - paralleltest 49 | 50 | issues: 51 | exclude-use-default: false 52 | exclude: 53 | - 'exported: exported .* should have comment' 54 | - 'package-comments: .*' 55 | - '^shadow: declaration of "err" shadows declaration at line' 56 | # _ instead of err checks 57 | - G104 58 | # for "public interface + private struct implementation" cases only! 59 | - exported func .* returns unexported type .*, which can be annoying to use 60 | # can be removed in the development phase 61 | # - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) 62 | # not for the active development - can be removed in the stable phase 63 | - should have a package comment, unless it's in another file for this package 64 | - don't use an underscore in package name 65 | # EXC0001 errcheck: Almost all programs ignore errors on these functions and in most cases it's ok 66 | - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked 67 | - should check returned error before deferring 68 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | 16 | builds: 17 | - main: ./cmd/framer 18 | env: 19 | - CGO_ENABLED=0 20 | goos: 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - arm64 26 | ldflags: 27 | - -X main.Version={{.Version}} 28 | 29 | archives: 30 | - format: tar.gz 31 | # this name template makes the OS and Arch compatible with the results of `uname`. 32 | name_template: >- 33 | {{ .ProjectName }}_ 34 | {{- title .Os }}_ 35 | {{- if eq .Arch "amd64" }}x86_64 36 | {{- else if eq .Arch "386" }}i386 37 | {{- else }}{{ .Arch }}{{ end }} 38 | {{- if .Arm }}v{{ .Arm }}{{ end }} 39 | # use zip for windows archives 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | 44 | changelog: 45 | sort: asc 46 | filters: 47 | exclude: 48 | - "^docs:" 49 | - "^test:" 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS build-stage 2 | WORKDIR /app 3 | RUN apk add make 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | RUN make build 8 | 9 | FROM alpine 10 | WORKDIR / 11 | COPY --from=build-stage /tmp/bin/framer /framer 12 | ENTRYPOINT ["/framer"] 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAIN_PACKAGE_PATH := ./cmd/framer 2 | BINARY_NAME := framer 3 | 4 | ## test: run all tests 5 | .PHONY: test 6 | test: 7 | go test -v -race -buildvcs ./... 8 | 9 | ## test/cover: run all tests and display coverage 10 | .PHONY: test/cover 11 | test/cover: 12 | go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... 13 | go tool cover -html=/tmp/coverage.out 14 | 15 | ## build: build the application 16 | .PHONY: build 17 | build: 18 | go build -o=/tmp/bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} 19 | 20 | ## lint: lint code 21 | .PHONY: lint 22 | lint: 23 | golangci-lint run --config=.golangci.pipeline.yaml --sort-results ./... 24 | 25 | ## help: print this help message 26 | .PHONY: help 27 | help: 28 | @echo 'Usage:' 29 | @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](./assets/logo.png) 2 | # framer by Ozon Tech 3 | framer is the most performant gRPC-load generator 4 | 5 | ## Performance 6 | ![benchmark chart](./assets/benchmark_chart.png) 7 | 8 | Benchmarks are done with `11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz`. 9 | Load generators was limited in 2 CPU. 10 | Load generators configurations are available in [benchmarks directory](./benchmarks) 11 | 12 | ### How we achive this performance values? 13 | * We have created simple http/2 & grpc client from scratch with performance in mind; 14 | * Project follows zero-allocation concept; 15 | * Syscalls count minimized; 16 | * Data copying minimized; 17 | 18 | ## Disclaimer 19 | This is alpha version. Public api and request file format may be changed. 20 | 21 | ## Install 22 | Download binary from [github release page](https://github.com/ozontech/framer/releases/latest) and place it in your PATH. 23 | 24 | ### Compile 25 | **Build using go** 26 | 27 | ```sh 28 | git clone https://github.com/ozontech/framer 29 | cd framer/cmd/framer 30 | go build . -o framer 31 | ./framer --help 32 | ``` 33 | 34 | **Install using go** 35 | ```sh 36 | go install github.com/ozontech/framer/cmd/framer@latest 37 | ``` 38 | 39 | **Run using docker** 40 | ```sh 41 | git clone https://github.com/ozontech/framer 42 | cd framer 43 | docker -v build --tag framer . 44 | docker run --network host -v $(pwd)/test_files/requests:/requests framer load --addr=localhost:9090 --requests-file=test_files/requests --clients 10 const 10 --duration 10s 45 | ``` 46 | 47 | ## Load generation 48 | ### Usage 49 | ``` 50 | Usage: framer load --addr=STRING --requests-file=REQUESTS-FILE [flags] 51 | 52 | Starting load generation. 53 | 54 | Flags: 55 | -h, --help Show context-sensitive help. 56 | 57 | --addr=STRING Address of system under test 58 | --requests-file=REQUESTS-FILE File of requests in ozon.binary format (see convert command to generate) 59 | --inmem-requests Load whole requests file in memory. 60 | --clients=1 Clients count. 61 | --phout=STRING Phout report file. 62 | 63 | RPs: 64 | const Const rps. 65 | Value req/s. 66 | 67 | line Linear rps. 68 | Starting req/s. 69 | Ending req/s. 70 | Duration (10s, 2h...). 71 | 72 | unlimited Unlimited rps (default one). 73 | ``` 74 | 75 | ### Example 76 | ```sh 77 | framer load --addr=localhost:9090 --requests-file=test_files/requests --clients 10 const 10 --duration 10s 78 | ``` 79 | It makes 10 rps from 10 clients in 10 second. 80 | 81 | ## Converter 82 | `framer convert` command may be used to convert requests file between different formats. 83 | Now is supported next formats: 84 | * ozon.binary - [see format description above](#ozon.binary-file-format); 85 | * pandora.json - grpc json format of pandora load generator. [See documentation](https://yandex.cloud/ru/docs/load-testing/concepts/payloads/grpc-json); 86 | * ozon.json - same as pandora.json, but has ability to store repeatable meta value. 87 | 88 | ### Supported formats 89 | ### Ozon.binary file format 90 | Rules are using [ABNF syntax](https://tools.ietf.org/html/rfc5234). 91 | 92 | ```abnf 93 | Requests = 1*Request 94 | Request = Length LF RequestPayload LF 95 | Length = 1*DIGIT; Size of RequestPayload in bytes, represented as ascii string 96 | RequestPayload = TagLine PathLine HeadersLine Body 97 | TagLine = 1*CHAR LF 98 | PathLine = 1*CHAR LF 99 | HeadersLine = Headers LF 100 | Headers = {json encoded object with followed structure {"key1": ["val1.1", "val1.2"], "key2": ["val2.1"]}}; keys started with ":" will be ignored 101 | Body = 1*({any byte}) 102 | ``` 103 | 104 | [Example requests file](https://github.com/ozontech/framer/-/blob/master/test_files/requests) 105 | 106 | #### Programatic ozon.binary generation example 107 | [Full example](./examples/requestsgen) 108 | 109 | ### Usage 110 | ``` 111 | Usage: framer convert --help 112 | Usage: framer convert --from=ozon.json --to=ozon.binary [ []] [flags] 113 | 114 | Converting request files. 115 | 116 | Arguments: 117 | [] Input file (default is stdin) 118 | [] Input file (default is stdout) 119 | 120 | Flags: 121 | -h, --help Show context-sensitive help. 122 | 123 | --from=ozon.json Input format. Available types: ozon.json, ozon.binary, pandora.json 124 | --to=ozon.binary Output format. Available types: ozon.json, ozon.binary 125 | 126 | Reflection flags: 127 | --reflection-addr=my-service:9090 Address of reflection api 128 | --reflection-proto=service1.proto,service2.proto,... Proto files 129 | --reflection-import-path=./api/,./vendor/,... Proto import paths 130 | ``` 131 | 132 | ### Example 133 | ```sh 134 | framer convert --from=ozon.json --to=ozon.binary --reflection-proto=formats/grpc/ozon/json/encoding/testproto/service.proto formats/grpc/test_files/requests.ozon.json 135 | ``` 136 | It converts requests file from ozon.json format to ozon.binary format using protofile. 137 | 138 | ## TODO 139 | - [ ] Installation 140 | - [ ] Homebrew suport for macOS; 141 | - [ ] Publish to dockerhub; 142 | - [ ] Configuration file support; 143 | - [ ] Requests scheduling strategys combination; 144 | - [ ] More reporting variants; 145 | - [ ] More performance: 146 | - [ ] Batch frames to multiple tcp connections using linux aio.h (minimize syscalls); May help keep performance with hundreds connections; 147 | - [ ] Faster hpack encoding/decoding; 148 | - [ ] More performant requests file structure; 149 | - [ ] Extensibility: 150 | - [ ] Requests middleware (for injecting token, for example); 151 | - [ ] Ability to use custom client-side load balancer and discoverer; 152 | - [ ] Custom reporters; 153 | - [ ] Plugin system 154 | -------------------------------------------------------------------------------- /assets/benchmark_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozontech/framer/ae45a6eb6a20f744f5fcc7d606e7a8c66c591d2c/assets/benchmark_chart.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozontech/framer/ae45a6eb6a20f744f5fcc7d606e7a8c66c591d2c/assets/logo.png -------------------------------------------------------------------------------- /benchmarks/dumb-server/.gitignore: -------------------------------------------------------------------------------- 1 | debug 2 | -------------------------------------------------------------------------------- /benchmarks/dumb-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS build-stage 2 | WORKDIR /app 3 | RUN apk add make 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | WORKDIR /app/benchmarks/dumb-server 8 | RUN make build 9 | 10 | FROM alpine 11 | WORKDIR / 12 | COPY --from=build-stage /tmp/bin/dumb-server /dumb-server 13 | ENTRYPOINT ["/dumb-server"] 14 | -------------------------------------------------------------------------------- /benchmarks/dumb-server/Makefile: -------------------------------------------------------------------------------- 1 | MAIN_PACKAGE_PATH := ./ 2 | BINARY_NAME := dumb-server 3 | 4 | ## build: build the application 5 | .PHONY: build 6 | build: 7 | go build -o=/tmp/bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} 8 | 9 | 10 | ## run: build and run the application 11 | .PHONY: run 12 | run: | build 13 | /tmp/bin/${BINARY_NAME} 14 | 15 | .PHONY: pb 16 | pb: 17 | protoc --go_out=./pb/ --go_opt=paths=source_relative \ 18 | --go-grpc_out=./pb/ --go-grpc_opt=paths=source_relative \ 19 | -I=./api api.proto \ 20 | --go_opt=Mapi.proto=github.com/rapthead/dumb-grpc-server/pb \ 21 | --go-grpc_opt=Mapi.proto=github.com/rapthead/dumb-grpc-server/pb 22 | -------------------------------------------------------------------------------- /benchmarks/dumb-server/api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package test.api; 4 | 5 | service TestApi { 6 | rpc Test (TestRequest) returns (EmptyResponse); 7 | } 8 | 9 | message TestRequest { 10 | string field = 1; 11 | } 12 | 13 | message EmptyResponse {} 14 | -------------------------------------------------------------------------------- /benchmarks/dumb-server/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | server: 4 | container_name: server-${COMPOSE_PROJECT_NAME} 5 | image: server 6 | build: 7 | context: ../.. 8 | dockerfile: benchmarks/dumb-server/Dockerfile 9 | network_mode: host 10 | -------------------------------------------------------------------------------- /benchmarks/dumb-server/pb/api_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.21.12 5 | // source: api.proto 6 | 7 | package pb 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | TestApi_Test_FullMethodName = "/test.api.TestApi/Test" 23 | ) 24 | 25 | // TestApiClient is the client API for TestApi service. 26 | // 27 | // 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. 28 | type TestApiClient interface { 29 | Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*EmptyResponse, error) 30 | } 31 | 32 | type testApiClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewTestApiClient(cc grpc.ClientConnInterface) TestApiClient { 37 | return &testApiClient{cc} 38 | } 39 | 40 | func (c *testApiClient) Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*EmptyResponse, error) { 41 | out := new(EmptyResponse) 42 | err := c.cc.Invoke(ctx, TestApi_Test_FullMethodName, in, out, opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return out, nil 47 | } 48 | 49 | // TestApiServer is the server API for TestApi service. 50 | // All implementations must embed UnimplementedTestApiServer 51 | // for forward compatibility 52 | type TestApiServer interface { 53 | Test(context.Context, *TestRequest) (*EmptyResponse, error) 54 | mustEmbedUnimplementedTestApiServer() 55 | } 56 | 57 | // UnimplementedTestApiServer must be embedded to have forward compatible implementations. 58 | type UnimplementedTestApiServer struct { 59 | } 60 | 61 | func (UnimplementedTestApiServer) Test(context.Context, *TestRequest) (*EmptyResponse, error) { 62 | return nil, status.Errorf(codes.Unimplemented, "method Test not implemented") 63 | } 64 | func (UnimplementedTestApiServer) mustEmbedUnimplementedTestApiServer() {} 65 | 66 | // UnsafeTestApiServer may be embedded to opt out of forward compatibility for this service. 67 | // Use of this interface is not recommended, as added methods to TestApiServer will 68 | // result in compilation errors. 69 | type UnsafeTestApiServer interface { 70 | mustEmbedUnimplementedTestApiServer() 71 | } 72 | 73 | func RegisterTestApiServer(s grpc.ServiceRegistrar, srv TestApiServer) { 74 | s.RegisterService(&TestApi_ServiceDesc, srv) 75 | } 76 | 77 | func _TestApi_Test_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 78 | in := new(TestRequest) 79 | if err := dec(in); err != nil { 80 | return nil, err 81 | } 82 | if interceptor == nil { 83 | return srv.(TestApiServer).Test(ctx, in) 84 | } 85 | info := &grpc.UnaryServerInfo{ 86 | Server: srv, 87 | FullMethod: TestApi_Test_FullMethodName, 88 | } 89 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 90 | return srv.(TestApiServer).Test(ctx, req.(*TestRequest)) 91 | } 92 | return interceptor(ctx, in, info, handler) 93 | } 94 | 95 | // TestApi_ServiceDesc is the grpc.ServiceDesc for TestApi service. 96 | // It's only intended for direct use with grpc.RegisterService, 97 | // and not to be introspected or modified (even as a copy) 98 | var TestApi_ServiceDesc = grpc.ServiceDesc{ 99 | ServiceName: "test.api.TestApi", 100 | HandlerType: (*TestApiServer)(nil), 101 | Methods: []grpc.MethodDesc{ 102 | { 103 | MethodName: "Test", 104 | Handler: _TestApi_Test_Handler, 105 | }, 106 | }, 107 | Streams: []grpc.StreamDesc{}, 108 | Metadata: "api.proto", 109 | } 110 | -------------------------------------------------------------------------------- /benchmarks/framer/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * Install [framer](../../README.md#install) 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/framer/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: framer-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | framer: 6 | container_name: framer 7 | image: framer 8 | build: 9 | context: ../.. 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | volumes: 13 | - ./requests.bin:/tmp/requests.bin 14 | command: > 15 | load --addr=localhost:9090 --inmem-requests 16 | --requests-file=/tmp/requests.bin --clients 10 17 | unlimited --duration 1m 18 | deploy: 19 | resources: 20 | limits: 21 | cpus: '2' 22 | memory: 2G 23 | -------------------------------------------------------------------------------- /benchmarks/framer/requests.bin: -------------------------------------------------------------------------------- 1 | 113 2 | /Test 3 | /test.api.TestApi/Test 4 | {"x-my-header-key1":["my-header-val1"],"x-my-header-key2":["my-header-val2"]} 5 | 6 | ping 7 | -------------------------------------------------------------------------------- /benchmarks/framer/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | framer load --addr=localhost:9090 \ 3 | --inmem-requests --requests-file=./requests.bin --clients 10 \ 4 | unlimited --count=10000000 5 | -------------------------------------------------------------------------------- /benchmarks/gatling/.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /benchmarks/gatling/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | *.hprof 4 | -------------------------------------------------------------------------------- /benchmarks/gatling/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-oracle 2 | WORKDIR /app 3 | COPY . /app 4 | RUN ./gradlew 5 | ENTRYPOINT ["./gradlew", "gatlingRun-bench.BenchKt"] 6 | -------------------------------------------------------------------------------- /benchmarks/gatling/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * Install java and gradle 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/gatling/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.* 2 | 3 | plugins { 4 | java 5 | idea 6 | 7 | kotlin("jvm") version "1.8.10" 8 | 9 | application 10 | 11 | id("com.google.protobuf") version "0.9.2" 12 | id("io.gatling.gradle") version "3.9.2.1" 13 | } 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | fun add(s: String) { 21 | implementation(s) 22 | gatling(s) 23 | } 24 | 25 | add("com.google.protobuf:protobuf-java:3.22.2") 26 | add("io.grpc:grpc-netty-shaded:1.53.0") 27 | add("io.grpc:grpc-protobuf:1.53.0") 28 | add("io.grpc:grpc-stub:1.53.0") 29 | 30 | implementation("javax.annotation:javax.annotation-api:1.3.2") 31 | 32 | gatling("com.github.phisgr:gatling-grpc:0.16.0") 33 | // for Scala Gatling tests 34 | gatling("com.github.phisgr:gatling-javapb:1.3.0") 35 | // for Kotlin/Java Gatling tests 36 | gatling("com.github.phisgr:gatling-grpc-kt:0.15.1") 37 | } 38 | 39 | protobuf { 40 | protoc { 41 | artifact = "com.google.protobuf:protoc:3.22.2" 42 | } 43 | plugins { 44 | id("grpc") { 45 | artifact = "io.grpc:protoc-gen-grpc-java:1.53.0" 46 | } 47 | } 48 | generateProtoTasks { 49 | ofSourceSet("main").forEach { 50 | it.plugins { 51 | id("grpc") 52 | } 53 | } 54 | } 55 | } 56 | 57 | application { 58 | mainClass.set("bench.DemoServer") 59 | } 60 | -------------------------------------------------------------------------------- /benchmarks/gatling/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: gatling-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | gatling: 6 | container_name: gatling 7 | image: gatling 8 | build: 9 | context: ./ 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | working_dir: /app 13 | volumes: 14 | - ./:/app 15 | deploy: 16 | resources: 17 | limits: 18 | cpus: '2' 19 | memory: 2G 20 | -------------------------------------------------------------------------------- /benchmarks/gatling/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozontech/framer/ae45a6eb6a20f744f5fcc7d606e7a8c66c591d2c/benchmarks/gatling/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /benchmarks/gatling/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /benchmarks/gatling/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /benchmarks/gatling/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /benchmarks/gatling/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) 3 | export PATH=$JAVA_HOME/bin:$PATH 4 | ./gradlew gatlingRun-bench.BenchKt 5 | -------------------------------------------------------------------------------- /benchmarks/gatling/src/gatling/kotlin/bench/BenchKt.kt: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import com.github.phisgr.gatling.kt.grpc.* 4 | import bench.BenchmarkGrpc 5 | import bench.BenchmarkOuterClass.BlankGetRequest 6 | import io.gatling.javaapi.core.CoreDsl.* 7 | import io.gatling.javaapi.core.* 8 | import io.grpc.ManagedChannelBuilder 9 | 10 | class BenchKt : Simulation() { 11 | 12 | private val grpcConf = grpc(ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext()) 13 | 14 | private fun request(name: String) = grpc(name) 15 | .rpc(BenchmarkGrpc.getBlankGetMethod()) 16 | .payload(BlankGetRequest::newBuilder) { 17 | build() 18 | } 19 | 20 | private val scn = scenario("Play Ping Pong").exec( 21 | request("Send message") 22 | ) 23 | 24 | init { 25 | setUp( 26 | scn.injectOpen( 27 | rampUsersPerSec(10.0).to(100000.0).during(40), //line 28 | constantUsersPerSec(100000.0).during(20), //const 29 | ).protocols(grpcConf.shareChannel()) 30 | ).maxDuration(62) //limit 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /benchmarks/gatling/src/gatling/resources/logback.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /benchmarks/ghz/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | WORKDIR /app 3 | ADD https://github.com/bojand/ghz/releases/download/v0.120.0/ghz-linux-x86_64.tar.gz ./ 4 | RUN tar xvf ghz-linux-x86_64.tar.gz 5 | ENTRYPOINT ["/app/ghz"] 6 | -------------------------------------------------------------------------------- /benchmarks/ghz/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | - Install [ghz](https://ghz.sh/docs/install) 5 | - Run dumb server: `(cd ../dumb-server && make run)` 6 | - Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/ghz/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: ghz-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | ghz: 6 | container_name: ghz 7 | image: ghz 8 | build: 9 | context: ./ 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | volumes: 13 | - ../dumb-server/api/api.proto:/tmp/api.proto 14 | - ./ghz-data.bin:/tmp/ghz-data.bin 15 | command: > 16 | --insecure --async 17 | --proto /tmp/api.proto 18 | --call test.api.TestApi/Test 19 | -c 10 --total 100000 20 | -B /tmp/ghz-data.bin localhost:9090 21 | deploy: 22 | resources: 23 | limits: 24 | cpus: '2' 25 | memory: 2G 26 | -------------------------------------------------------------------------------- /benchmarks/ghz/ghz-data.bin: -------------------------------------------------------------------------------- 1 | 2 | ping -------------------------------------------------------------------------------- /benchmarks/ghz/run.sh: -------------------------------------------------------------------------------- 1 | ghz --insecure --async \ 2 | --proto ../dumb-server/api/api.proto \ 3 | --call test.api.TestApi/Test \ 4 | -c 10 --total 100000 \ 5 | -B ./ghz-data.bin localhost:9090 6 | -------------------------------------------------------------------------------- /benchmarks/h2load/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | RUN apt-get update && apt-get install -y nghttp2-client 3 | ENTRYPOINT ["h2load"] 4 | -------------------------------------------------------------------------------- /benchmarks/h2load/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * Install h2load: `sudo apt install nghttp2-client` 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/h2load/data: -------------------------------------------------------------------------------- 1 |  2 | ping -------------------------------------------------------------------------------- /benchmarks/h2load/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: h2load-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | h2load: 6 | container_name: h2load 7 | image: h2load 8 | build: 9 | context: ./ 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | volumes: 13 | - ./data:/tmp/data 14 | command: > 15 | http://localhost:9090/test.api.TestService/Test 16 | -n 6000000 -c 10 -m 200 17 | --data=/tmp/data 18 | -H 'x-my-header-key1: my-header-val1' -H 'x-my-header-key2: my-header-val2' 19 | -H 'te: trailers' -H 'content-type: application/grpc' 20 | deploy: 21 | resources: 22 | limits: 23 | cpus: '2' 24 | memory: 2G 25 | -------------------------------------------------------------------------------- /benchmarks/h2load/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | cpulimit -f -l 200 -- h2load http://localhost:9090/test.api.TestService/Test \ 3 | -n 6000000 -c 10 -m 200 \ 4 | --data=./data \ 5 | -H 'x-my-header-key1: my-header-val1' -H 'x-my-header-key2: my-header-val2' \ 6 | -H 'te: trailers' -H 'content-type: application/grpc' 7 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/.gitignore: -------------------------------------------------------------------------------- 1 | report.jtl 2 | target 3 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozontech/framer/ae45a6eb6a20f744f5fcc7d606e7a8c66c591d2c/benchmarks/jmeter-java-dsl/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:20-oracle 2 | WORKDIR /app 3 | COPY . /app 4 | ENTRYPOINT ["./mvnw", "verify" ] 5 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | .PHONY: verify 4 | verify: 5 | ./mvnw verify 6 | 7 | 8 | 9 | .PHONY: run-dumb 10 | run-dumb: 11 | cd ../dumb-server && make run -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/REAME.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * Install JDK > 20, maven 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: jmeter-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | jmeter: 6 | container_name: jmeter 7 | image: jmeter 8 | build: 9 | context: ./ 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | working_dir: /app 13 | tty: true 14 | volumes: 15 | - ./:/app 16 | deploy: 17 | resources: 18 | limits: 19 | cpus: '2' 20 | memory: 2G 21 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | make verify 3 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/src/main/resources/api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package test.api; 4 | 5 | service TestApi { 6 | rpc Test (TestRequest) returns (EmptyResponse); 7 | } 8 | 9 | message TestRequest { 10 | string field = 1; 11 | } 12 | 13 | message EmptyResponse {} 14 | -------------------------------------------------------------------------------- /benchmarks/jmeter-java-dsl/src/test/java/PerformanceIT.java: -------------------------------------------------------------------------------- 1 | import static us.abstracta.jmeter.javadsl.JmeterDsl.*; 2 | import static us.abstracta.jmeter.javadsl.dashboard.DashboardVisualizer.dashboardVisualizer; 3 | 4 | import java.io.IOException; 5 | import java.time.Duration; 6 | import java.util.*; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | //import benchmark.BenchmarkGrpc; 11 | //import benchmark.BenchmarkOuterClass; 12 | import io.grpc.ManagedChannel; 13 | import io.grpc.ManagedChannelBuilder; 14 | import io.netty.channel.nio.NioEventLoopGroup; 15 | import org.junit.jupiter.api.Test; 16 | import test.api.Api; 17 | import test.api.TestApiGrpc; 18 | import us.abstracta.jmeter.javadsl.core.TestPlanStats; 19 | import us.abstracta.jmeter.javadsl.core.threadgroups.RpsThreadGroup; 20 | 21 | public class PerformanceIT { 22 | // BenchmarkGrpc.TestApiBlockingStub. 23 | 24 | @Test 25 | public void testPerformance() throws IOException { 26 | String host = System.getProperty("target.host"); 27 | System.out.println(host); 28 | String getenv = System.getProperty("target.port"); 29 | System.out.println(getenv); 30 | int port = Integer.parseInt(getenv); 31 | int maxThreads = Integer.parseInt(System.getProperty("max.threads")); 32 | int targetRPS = Integer.parseInt(System.getProperty("target.rps")); 33 | int numClients = Integer.parseInt(System.getProperty("grpc.numclients")); 34 | 35 | Map stubs = new HashMap<>(); 36 | for (int i = 0; i < numClients; i++) { 37 | ManagedChannel client = ManagedChannelBuilder 38 | .forAddress(host, port) 39 | .keepAliveTime(5, TimeUnit.SECONDS) 40 | .usePlaintext().build(); 41 | TestApiGrpc.TestApiBlockingStub stub = TestApiGrpc.newBlockingStub(client); 42 | stub.withCompression(System.getProperty("grpc.compression")); 43 | stub.withExecutor(Executors.newCachedThreadPool()); 44 | stubs.put(String.valueOf(i), stub); 45 | } 46 | Random generator = new Random(); 47 | Object[] clients = stubs.values().toArray(); 48 | 49 | Api.TestRequest trueRequest = Api.TestRequest.newBuilder().setField("true").build(); 50 | 51 | RpsThreadGroup children = rpsThreadGroup() 52 | .maxThreads(maxThreads) 53 | .rampTo(targetRPS, Duration.ofSeconds(120)) 54 | .rampToAndHold(targetRPS, Duration.ofSeconds(120), Duration.ofSeconds(120)) 55 | .children( 56 | jsr223Sampler(vars -> { 57 | try { 58 | TestApiGrpc.TestApiBlockingStub value = 59 | (TestApiGrpc.TestApiBlockingStub) clients[generator.nextInt(clients.length)]; 60 | Api.EmptyResponse resp = value.test(trueRequest); 61 | vars.sampleResult.setSentBytes(trueRequest.getSerializedSize()); 62 | vars.sampleResult.setBytes(resp.getSerializedSize()); 63 | } catch (Exception e) { 64 | vars.sampleResult.setResponseCode("500"); 65 | } 66 | }) 67 | ); 68 | TestPlanStats stats = testPlan( 69 | children, 70 | // dashboardVisualizer(), 71 | 72 | htmlReporter("./")).run(); 73 | 74 | 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /benchmarks/k6/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * [Install k6](https://k6.io/docs/get-started/installation/) 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/k6/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: k6-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | k6: 6 | container_name: k6 7 | image: grafana/k6:0.51.0 8 | network_mode: host 9 | volumes: 10 | - ./script.js:/app/k6/script.js 11 | - ../dumb-server/api/api.proto:/app/dumb-server/api/api.proto 12 | working_dir: /app 13 | command: > 14 | run /app/k6/script.js 15 | deploy: 16 | resources: 17 | limits: 18 | cpus: '2' 19 | memory: 2G 20 | -------------------------------------------------------------------------------- /benchmarks/k6/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | k6 run script.js 3 | -------------------------------------------------------------------------------- /benchmarks/k6/script.js: -------------------------------------------------------------------------------- 1 | import { check, sleep } from 'k6'; 2 | import { Client, StatusOK } from 'k6/net/grpc'; 3 | 4 | export const options = { 5 | vus: 10, 6 | duration: '20s', 7 | }; 8 | 9 | const client = new Client(); 10 | client.load([], '../dumb-server/api/api.proto'); 11 | 12 | export default function() { 13 | if (__ITER == 0) { 14 | client.connect('127.0.0.1:9090', { plaintext: true }); 15 | } 16 | 17 | const data = { field: 'ping' }; 18 | const params = { 19 | metadata: { 20 | 'x-my-header-key1': 'my-header-val1', 21 | 'x-my-header-key2': 'my-header-val2', 22 | }, 23 | }; 24 | const response = client.invoke('test.api.TestApi/Test', data); 25 | check(response, { 'status is OK': (r) => r && r.status === StatusOK }) 26 | } 27 | -------------------------------------------------------------------------------- /benchmarks/pandora/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | WORKDIR /app 3 | ADD https://github.com/yandex/pandora/releases/download/v0.5.26/pandora_0.5.26_linux_amd64 /usr/local/bin/pandora 4 | RUN chmod +x /usr/local/bin/pandora 5 | ENTRYPOINT ["pandora"] 6 | -------------------------------------------------------------------------------- /benchmarks/pandora/README.md: -------------------------------------------------------------------------------- 1 | How to run 2 | - Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` 3 | - Run local: 4 | * Install [pandora](https://github.com/yandex/pandora#how-to-start) 5 | * Run dumb server: `(cd ../dumb-server && make run)` 6 | * Execute `./run.sh` 7 | -------------------------------------------------------------------------------- /benchmarks/pandora/config.yaml: -------------------------------------------------------------------------------- 1 | pools: 2 | - id: GRPC 3 | gun: 4 | type: grpc 5 | target: localhost:9090 6 | reflect_port: 9091 7 | tls: false 8 | ammo: 9 | type: grpc/json 10 | file: requests.json 11 | result: 12 | type: discard 13 | rps: 14 | - duration: 1m 15 | ops: 200_000 16 | type: const # type unlimited has bug 17 | discard_overflow: false 18 | startup: 19 | type: once 20 | # Clients count. Yandex pandora don't use http/2 multiplexing, 21 | # so we must use huge amount of connections 22 | times: 1500 23 | -------------------------------------------------------------------------------- /benchmarks/pandora/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: pandora-benchmark 2 | include: 3 | - ../dumb-server/docker-compose.yaml 4 | services: 5 | pandora: 6 | container_name: pandora 7 | image: pandora 8 | build: 9 | context: ./ 10 | dockerfile: Dockerfile 11 | network_mode: host 12 | volumes: 13 | - ./:/app/ 14 | command: > 15 | config.yaml 16 | deploy: 17 | resources: 18 | limits: 19 | cpus: '2' 20 | memory: 2G 21 | -------------------------------------------------------------------------------- /benchmarks/pandora/requests.json: -------------------------------------------------------------------------------- 1 | {"tag":"/Test","call":"test.api.TestApi.Test","metadata":{"x-my-header-key1": "my-header-val1", "x-my-header-key2": "my-header-val2"},"payload":{"field":"ping"}} 2 | -------------------------------------------------------------------------------- /benchmarks/pandora/run.sh: -------------------------------------------------------------------------------- 1 | #!env sh 2 | pandora config.yaml 3 | -------------------------------------------------------------------------------- /cmd/framer/cmd_convert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/ozontech/framer/formats/converter" 12 | formatsGRPC "github.com/ozontech/framer/formats/grpc" 13 | ozonBinary "github.com/ozontech/framer/formats/grpc/ozon/binary" 14 | ozonJson "github.com/ozontech/framer/formats/grpc/ozon/json" 15 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 16 | pandoryJson "github.com/ozontech/framer/formats/grpc/pandora/json" 17 | "github.com/ozontech/framer/formats/model" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/credentials/insecure" 20 | ) 21 | 22 | type format string 23 | 24 | const ( 25 | formatOzonJSON format = "ozon.json" 26 | formatOzonBinary format = "ozon.binary" 27 | formatPandoraJSON format = "pandora.json" 28 | ) 29 | 30 | type ConvertCommand struct { 31 | In *os.File `arg:"" required:"" default:"-" help:"Input file (default is stdin)"` 32 | Out string `arg:"" required:"" default:"-" help:"Input file (default is stdout)" type:"path"` 33 | 34 | From format `enum:"ozon.json, ozon.binary, pandora.json" required:"" placeholder:"ozon/json" help:"Input format. Available types: ${enum}"` 35 | To format `enum:"ozon.json, ozon.binary" required:"" placeholder:"ozon/binary" help:"Output format. Available types: ${enum}"` 36 | 37 | ReflectionAddr string `group:"reflection" xor:"reflection" placeholder:"my-service:9090" help:"Address of reflection api"` 38 | ReflectionProto []string `group:"reflection" xor:"reflection" placeholder:"service1.proto,service2.proto" help:"Proto files"` 39 | ReflectionImportPath []string `group:"reflection" type:"existingdir" placeholder:"./api/,./vendor/" help:"Proto import paths"` 40 | } 41 | 42 | func (c *ConvertCommand) Validate() error { 43 | if c.From == c.To { 44 | return errors.New("--from and --to flags must have different values") 45 | } 46 | hasReflect := len(c.ReflectionProto) != 0 || c.ReflectionAddr != "" 47 | if (c.From != formatOzonBinary || c.To != formatOzonBinary) && !hasReflect { 48 | return errors.New("--reflection-addr or --reflection-proto is required for this conversion type") 49 | } 50 | return nil 51 | } 52 | 53 | func (c *ConvertCommand) Run(ctx context.Context) error { 54 | var outF *os.File 55 | if c.Out == "-" { 56 | outF = os.Stdout 57 | } else { 58 | var err error 59 | outF, err = os.Create(c.Out) 60 | if err != nil { 61 | return fmt.Errorf("output file creation: %w", err) 62 | } 63 | } 64 | 65 | var r8n reflection.DynamicMessagesStore 66 | if c.ReflectionAddr != "" { 67 | conn, err := grpc.Dial( 68 | c.ReflectionAddr, 69 | grpc.WithTransportCredentials(insecure.NewCredentials()), 70 | grpc.WithUserAgent("framer"), 71 | ) 72 | if err != nil { 73 | return fmt.Errorf("create reflection conn: %w", err) 74 | } 75 | 76 | fetchCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) 77 | fetcher := reflection.NewRemoteFetcher(conn) 78 | r8n, err = fetcher.Fetch(fetchCtx) 79 | cancel() 80 | if err != nil { 81 | return fmt.Errorf("remote reflection fetching: %w", err) 82 | } 83 | for _, warn := range fetcher.Warnings() { 84 | log.Println("remote reflection fetching: ", warn) 85 | } 86 | } else { 87 | fetchCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) 88 | defer cancel() 89 | 90 | var err error 91 | r8n, err = reflection.NewLocalFetcher(c.ReflectionProto, c.ReflectionImportPath).Fetch(fetchCtx) 92 | if err != nil { 93 | return fmt.Errorf("local reflection fetching: %w", err) 94 | } 95 | } 96 | 97 | var inputFormat *model.InputFormat 98 | switch c.From { 99 | case formatOzonBinary: 100 | inputFormat = ozonBinary.NewInput(c.In) 101 | case formatOzonJSON: 102 | inputFormat = ozonJson.NewInput(c.In, r8n) 103 | case formatPandoraJSON: 104 | inputFormat = pandoryJson.NewInput(c.In, r8n) 105 | default: 106 | panic("assertion error") 107 | } 108 | 109 | var outputFormat *model.OutputFormat 110 | switch c.To { 111 | case formatOzonBinary: 112 | outputFormat = ozonBinary.NewOutput(outF) 113 | case formatOzonJSON: 114 | outputFormat = ozonJson.NewOutput(outF, r8n) 115 | default: 116 | panic("assertion error") 117 | } 118 | 119 | return converter.NewProcessor(formatsGRPC.NewConvertStrategy( 120 | inputFormat, 121 | outputFormat, 122 | )).Process(ctx) 123 | } 124 | -------------------------------------------------------------------------------- /cmd/framer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "net/http" 8 | _ "net/http/pprof" //nolint:gosec 9 | "runtime/debug" 10 | 11 | "github.com/alecthomas/kong" 12 | mangokong "github.com/alecthomas/mango-kong" 13 | ) 14 | 15 | var CLI struct { 16 | Load LoadCommand `cmd:"" help:"Starting load generation."` 17 | Convert ConvertCommand `cmd:"" help:"Converting request files."` 18 | Man mangokong.ManFlag `help:"Write man page." hidden:""` 19 | Version VersionFlag `name:"version" help:"Print version information and quit"` 20 | DebugServer bool `help:"Enable debug server."` 21 | } 22 | 23 | type VersionFlag string 24 | 25 | func (v VersionFlag) Decode(_ *kong.DecodeContext) error { return nil } 26 | func (v VersionFlag) IsBool() bool { return true } 27 | func (v VersionFlag) BeforeApply(app *kong.Kong, _ kong.Vars) error { 28 | fmt.Println(getVersion()) 29 | app.Exit(0) 30 | return nil 31 | } 32 | 33 | func main() { 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | 37 | go func() { 38 | // runtime.SetBlockProfileRate(1) 39 | http.ListenAndServe(":8081", nil) //nolint:errcheck,gosec 40 | }() 41 | 42 | // sigs := make(chan os.Signal, 1) 43 | // signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 44 | // go func() { 45 | // <-sigs 46 | // cancel() 47 | // }() 48 | 49 | kongCtx := kong.Parse( 50 | &CLI, 51 | kong.BindTo(ctx, (*context.Context)(nil)), 52 | kong.Bind(DurationLimit{Duration: math.MaxInt64}), 53 | kong.Groups(map[string]string{ 54 | "reflection": `Reflection flags:`, 55 | }), 56 | kong.ConfigureHelp(kong.HelpOptions{ 57 | Tree: true, 58 | Compact: true, 59 | }), 60 | kong.Description(`the most performant grpc load generator 61 | 62 | The framer is used to generate test requests to grpc servers and measure codes and response times in a most effective way. 63 | `), 64 | ) 65 | err := kongCtx.Run() 66 | kongCtx.FatalIfErrorf(err) 67 | } 68 | 69 | const unknownVersion = "unknown" 70 | 71 | var Version = unknownVersion 72 | 73 | func getVersion() string { 74 | if Version != unknownVersion { 75 | return Version 76 | } 77 | 78 | info, ok := debug.ReadBuildInfo() 79 | if !ok { 80 | return Version 81 | } 82 | 83 | for _, kv := range info.Settings { 84 | if kv.Value == "" { 85 | continue 86 | } 87 | if kv.Key == "vcs.revision" && kv.Value != "" { 88 | return kv.Value 89 | } 90 | } 91 | 92 | return Version 93 | } 94 | -------------------------------------------------------------------------------- /cmd/framer/main_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ozontech/framer/consts" 11 | "github.com/ozontech/framer/datasource" 12 | "github.com/ozontech/framer/loader" 13 | "github.com/ozontech/framer/report/simple" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | "go.uber.org/zap/zaptest" 17 | "golang.org/x/sync/errgroup" 18 | ) 19 | 20 | // func BenchmarkDataSource(b *testing.B) { 21 | // f, err := os.Open("../test_files/requests") 22 | // if err != nil { 23 | // b.Fatal(err) 24 | // } 25 | // 26 | // if false { 27 | // go func() { 28 | // log.Println(http.ListenAndServe("localhost:6060", nil)) 29 | // }() 30 | // } 31 | // 32 | // ctx, cancel := context.WithCancel(context.Background()) 33 | // defer cancel() 34 | // 35 | // b.ResetTimer() 36 | // err = (&LoadCommand{ 37 | // Clients: 1, 38 | // RequestsCount: b.N, 39 | // RequestsFile: f, 40 | // Addr: "localhost:9090", 41 | // }).Run(ctx) 42 | // if err != nil { 43 | // b.Fatal(err) 44 | // } 45 | // } 46 | 47 | func BenchmarkE2E(b *testing.B) { 48 | f, err := os.Open("../../test_files/requests") 49 | if err != nil { 50 | b.Fatal(err) 51 | } 52 | 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | defer cancel() 55 | 56 | conn, err := createConn(ctx, consts.DefaultTimeout, "localhost:9090") 57 | if err != nil { 58 | b.Fatal(err) 59 | } 60 | reporter := simple.New() 61 | a := assert.New(b) 62 | go func() { a.NoError(reporter.Run()) }() 63 | defer func() { a.NoError(reporter.Close()) }() 64 | 65 | l, err := loader.NewLoader( 66 | conn, 67 | reporter, 68 | consts.DefaultTimeout, 69 | false, 70 | zaptest.NewLogger(b), 71 | ) 72 | if err != nil { 73 | b.Fatal(fmt.Errorf("loader setup: %w", err)) 74 | } 75 | 76 | b.ResetTimer() 77 | 78 | g, ctx := errgroup.WithContext(ctx) 79 | runCtx, runCancel := context.WithCancel(ctx) 80 | g.Go(func() error { 81 | return l.Run(runCtx) 82 | }) 83 | 84 | dataSource := datasource.NewFileDataSource(datasource.NewCyclicReader(f)) 85 | g.Go(func() error { 86 | r, err := dataSource.Fetch() 87 | if err != nil { 88 | return err 89 | } 90 | l.DoRequest(r) 91 | b.ResetTimer() 92 | 93 | r, err = dataSource.Fetch() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | for i := 0; i < b.N; i++ { 99 | l.DoRequest(r) 100 | 101 | r, err = dataSource.Fetch() 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | l.WaitResponses(ctx) 107 | runCancel() 108 | return nil 109 | }) 110 | 111 | err = g.Wait() 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | } 116 | 117 | func BenchmarkE2EInMemDatasource(b *testing.B) { 118 | a := assert.New(b) 119 | ctx, cancel := context.WithCancel(context.Background()) 120 | defer cancel() 121 | 122 | conn, err := createConn(ctx, 5*time.Second, "localhost:9090") 123 | a.NoError(err) 124 | reporter := simple.New() 125 | 126 | reportErr := make(chan error) 127 | go func() { reportErr <- reporter.Run() }() 128 | 129 | l, err := loader.NewLoader( 130 | conn, 131 | reporter, 132 | consts.DefaultTimeout, 133 | false, 134 | zaptest.NewLogger(b), 135 | ) 136 | a.NoError(err) 137 | 138 | b.ResetTimer() 139 | 140 | g, ctx := errgroup.WithContext(ctx) 141 | runCtx, runCancel := context.WithCancel(ctx) 142 | g.Go(func() error { 143 | return l.Run(runCtx) 144 | }) 145 | 146 | requestsFile, err := os.Open("../test_files/requests") 147 | if err != nil { 148 | require.NoError(b, err) 149 | } 150 | dataSource := datasource.NewInmemDataSource(requestsFile) 151 | require.NoError(b, dataSource.Init()) 152 | g.Go(func() error { 153 | r, err := dataSource.Fetch() 154 | if err != nil { 155 | return err 156 | } 157 | l.DoRequest(r) 158 | b.ResetTimer() 159 | 160 | r, err = dataSource.Fetch() 161 | if err != nil { 162 | return err 163 | } 164 | 165 | for i := 0; i < b.N; i++ { 166 | l.DoRequest(r) 167 | 168 | r, err = dataSource.Fetch() 169 | if err != nil { 170 | return err 171 | } 172 | } 173 | l.WaitResponses(ctx) 174 | runCancel() 175 | return nil 176 | }) 177 | 178 | a.NoError(g.Wait()) 179 | a.NoError(reporter.Close()) 180 | a.NoError(<-reportErr) 181 | } 182 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | const ( 9 | RecieveBufferSize = 2048 10 | SendBatchTimeout = time.Millisecond 11 | RecieveBatchTimeout = time.Millisecond 12 | 13 | DefaultInitialWindowSize = 65_535 14 | DefaultTimeout = 11 * time.Second 15 | DefaultMaxFrameSize = 16384 // DefaultMaxFrameSize - максимальная длина пейлоада фрейма в grpc. У http2 ограничение больше. 16 | DefaultMaxHeaderListSize = math.MaxUint32 17 | ) 18 | -------------------------------------------------------------------------------- /datasource/cyclic-reader.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type CyclicReader struct { 9 | rs io.ReadSeeker 10 | } 11 | 12 | func NewCyclicReader(rs io.ReadSeeker) *CyclicReader { 13 | return &CyclicReader{rs} 14 | } 15 | 16 | func (r *CyclicReader) Read(b []byte) (int, error) { 17 | n, err := r.rs.Read(b) 18 | if err == nil { 19 | return n, nil 20 | } 21 | 22 | if err != io.EOF { 23 | return n, fmt.Errorf("read: %w", err) 24 | } 25 | 26 | _, err = r.rs.Seek(0, io.SeekStart) 27 | if err != nil { 28 | return n, fmt.Errorf("rewind: %w", err) 29 | } 30 | 31 | return n, nil 32 | } 33 | -------------------------------------------------------------------------------- /datasource/cyclic-reader_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type errReader struct { 13 | seekErr error 14 | readErr error 15 | } 16 | 17 | func (r errReader) Seek(int64, int) (int64, error) { 18 | return 0, r.seekErr 19 | } 20 | 21 | func (r errReader) Read([]byte) (int, error) { 22 | return 0, r.readErr 23 | } 24 | 25 | var errBrand = errors.New("brand error") 26 | 27 | func TestCyclicReaderErrors(t *testing.T) { 28 | t.Parallel() 29 | assert := assert.New(t) 30 | buf := make([]byte, 1024) 31 | 32 | cr := NewCyclicReader(errReader{nil, errBrand}) 33 | _, err := cr.Read(buf) 34 | assert.ErrorIs(err, errBrand) 35 | 36 | cr = NewCyclicReader(errReader{errBrand, io.EOF}) 37 | _, err = cr.Read(buf) 38 | assert.ErrorIs(err, errBrand) 39 | } 40 | 41 | func TestCyclicReader(t *testing.T) { 42 | t.Parallel() 43 | assert := assert.New(t) 44 | buf := make([]byte, 1024) 45 | 46 | in := []byte("1234567890") 47 | cr := NewCyclicReader(bytes.NewReader(in)) 48 | 49 | n, err := cr.Read(buf) 50 | assert.NoError(err) 51 | assert.Equal(in, buf[:n]) 52 | 53 | n, err = cr.Read(buf) 54 | assert.NoError(err) 55 | assert.Equal([]byte{}, buf[:n]) 56 | 57 | n, err = cr.Read(buf) 58 | assert.NoError(err) 59 | assert.Equal(in, buf[:n]) 60 | } 61 | -------------------------------------------------------------------------------- /datasource/decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/ozontech/framer/utils/lru" 8 | ) 9 | 10 | type Decoder struct { 11 | metaUnmarshaler *SafeMultiValDecoder 12 | 13 | tagsLRU *lru.LRU 14 | methodsLRU *lru.LRU 15 | } 16 | 17 | func NewDecoder() *Decoder { 18 | return &Decoder{ 19 | metaUnmarshaler: NewSafeMultiValDecoder(), 20 | 21 | tagsLRU: lru.New(1024), 22 | methodsLRU: lru.New(1024), 23 | } 24 | } 25 | 26 | func (decoder *Decoder) Unmarshal(d *Data, b []byte) error { 27 | d.Reset() 28 | 29 | tagB, b := nextLine(b) 30 | d.Tag = decoder.tagsLRU.GetOrAdd(tagB) 31 | 32 | methodB, b := nextLine(b) 33 | d.Method = decoder.methodsLRU.GetOrAdd(methodB) 34 | 35 | metaBytes, b := nextLine(b) 36 | var err error 37 | d.Metadata, err = decoder.metaUnmarshaler.UnmarshalAppend(d.Metadata, metaBytes) 38 | if err != nil { 39 | return fmt.Errorf("meta unmarshal error: %w", err) 40 | } 41 | 42 | d.Message = b 43 | return nil 44 | } 45 | 46 | func nextLine(in []byte) ([]byte, []byte) { 47 | index := bytes.IndexByte(in, '\n') 48 | if index == -1 { 49 | return []byte{}, []byte{} 50 | } 51 | return in[:index], in[index+1:] 52 | } 53 | -------------------------------------------------------------------------------- /datasource/decoder/jsonkv_safe.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/mailru/easyjson/jlexer" 5 | "github.com/ozontech/framer/utils/lru" 6 | ) 7 | 8 | const ( 9 | kLRUSize = 1 << 10 10 | vLRUSize = 1 << 16 11 | ) 12 | 13 | type SafeMultiValDecoder struct { 14 | kLRU *lru.LRU 15 | vLRU *lru.LRU 16 | } 17 | 18 | func NewSafeMultiValDecoder() *SafeMultiValDecoder { 19 | return &SafeMultiValDecoder{ 20 | lru.New(kLRUSize), 21 | lru.New(vLRUSize), 22 | } 23 | } 24 | 25 | func (d *SafeMultiValDecoder) UnmarshalAppend(buf []Meta, bytes []byte) ([]Meta, error) { 26 | in := jlexer.Lexer{Data: bytes} 27 | 28 | in.Delim('{') 29 | for !in.IsDelim('}') { 30 | key := d.kLRU.GetOrAdd(in.UnsafeBytes()) 31 | in.WantColon() 32 | 33 | in.Delim('[') 34 | for !in.IsDelim(']') { 35 | val := d.kLRU.GetOrAdd(in.UnsafeBytes()) 36 | buf = append(buf, Meta{Name: key, Value: val}) 37 | in.WantComma() 38 | } 39 | in.Delim(']') 40 | 41 | in.WantComma() 42 | } 43 | in.Delim('}') 44 | in.Consumed() 45 | 46 | return buf, in.Error() 47 | } 48 | -------------------------------------------------------------------------------- /datasource/decoder/model.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | type Meta struct { 4 | Name string 5 | Value string 6 | } 7 | 8 | type Data struct { 9 | Tag string 10 | Method string // "/" {service name} "/" {method name} 11 | Metadata []Meta 12 | Message []byte 13 | } 14 | 15 | func (d *Data) Reset() { 16 | d.Tag = "" 17 | d.Method = "" 18 | d.Metadata = d.Metadata[:0] 19 | d.Message = d.Message[:0] 20 | } 21 | -------------------------------------------------------------------------------- /datasource/file.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | 8 | "github.com/ozontech/framer/datasource/decoder" 9 | "github.com/ozontech/framer/formats/grpc/ozon/binary" 10 | "github.com/ozontech/framer/formats/model" 11 | "github.com/ozontech/framer/loader/types" 12 | "github.com/ozontech/framer/utils/pool" 13 | ) 14 | 15 | type FileDataSource struct { 16 | reader model.PooledRequestReader 17 | decoder *decoder.Decoder 18 | 19 | pool *pool.SlicePool[*fileRequest] 20 | factory *RequestAdapterFactory 21 | mu sync.Mutex 22 | } 23 | 24 | func NewFileDataSource(r io.Reader, factoryOptions ...Option) *FileDataSource { 25 | ds := &FileDataSource{ 26 | binary.NewInput(r).Reader, 27 | decoder.NewDecoder(), 28 | 29 | pool.NewSlicePoolSize[*fileRequest](100), 30 | NewRequestAdapterFactory(factoryOptions...), 31 | sync.Mutex{}, 32 | } 33 | return ds 34 | } 35 | 36 | func (ds *FileDataSource) Fetch() (types.Req, error) { 37 | r, ok := ds.pool.Acquire() 38 | if !ok { 39 | r = &fileRequest{ 40 | bytesPool: ds.reader, 41 | pool: ds.pool, 42 | RequestAdapter: ds.factory.Build(), 43 | } 44 | } 45 | 46 | var err error 47 | ds.mu.Lock() 48 | r.bytes, err = ds.reader.ReadNext() 49 | ds.mu.Unlock() 50 | if err != nil { 51 | return nil, fmt.Errorf("read next request: %w", err) 52 | } 53 | 54 | return r, ds.decoder.Unmarshal(&r.data, r.bytes) 55 | } 56 | 57 | type fileRequest struct { 58 | bytesPool model.PooledRequestReader 59 | bytes []byte 60 | pool *pool.SlicePool[*fileRequest] 61 | *RequestAdapter 62 | } 63 | 64 | func (r *fileRequest) Release() { 65 | r.bytesPool.Release(r.bytes) 66 | r.pool.Release(r) 67 | } 68 | -------------------------------------------------------------------------------- /datasource/file_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/ozontech/framer/consts" 9 | "github.com/ozontech/framer/loader/types" 10 | hpackwrapper "github.com/ozontech/framer/utils/hpack_wrapper" 11 | ) 12 | 13 | type noopHpackFieldWriter struct{} 14 | 15 | func (*noopHpackFieldWriter) WriteField(string, string) {} 16 | func (*noopHpackFieldWriter) SetWriter(io.Writer) {} 17 | 18 | func BenchmarkFileDataSource(b *testing.B) { 19 | f, err := os.Open("../test_files/requests") 20 | if err != nil { 21 | b.Fatal(err) 22 | } 23 | defer f.Close() 24 | ds := NewFileDataSource(NewCyclicReader(f)) 25 | 26 | rr := make(chan types.Req, 100) 27 | done := make(chan struct{}) 28 | go func() { 29 | defer close(done) 30 | for r := range rr { 31 | _, err := r.SetUp(consts.DefaultMaxFrameSize, consts.DefaultMaxHeaderListSize, 0, &noopHpackFieldWriter{}) 32 | if err != nil { 33 | b.Error(err) 34 | return 35 | } 36 | b.SetBytes(int64(r.Size())) 37 | r.Release() 38 | } 39 | }() 40 | 41 | b.ResetTimer() 42 | for i := 0; i < b.N; i++ { 43 | r, err := ds.Fetch() 44 | if err != nil { 45 | b.Fatal(err) 46 | } 47 | rr <- r 48 | } 49 | close(rr) 50 | <-done 51 | } 52 | 53 | func BenchmarkRequestSetupNoop(b *testing.B) { 54 | f, err := os.Open("../test_files/requests") 55 | if err != nil { 56 | b.Fatal(err) 57 | } 58 | defer f.Close() 59 | ds := NewFileDataSource(f) 60 | 61 | r, err := ds.Fetch() 62 | if err != nil { 63 | b.Fatal(err) 64 | } 65 | 66 | b.ResetTimer() 67 | for i := 0; i < b.N; i++ { 68 | _, err := r.SetUp(consts.DefaultMaxFrameSize, consts.DefaultMaxHeaderListSize, 0, &noopHpackFieldWriter{}) 69 | if err != nil { 70 | b.Error(err) 71 | return 72 | } 73 | b.SetBytes(int64(r.Size())) 74 | } 75 | } 76 | 77 | func BenchmarkRequestSetupHpack(b *testing.B) { 78 | f, err := os.Open("../test_files/requests") 79 | if err != nil { 80 | b.Fatal(err) 81 | } 82 | defer f.Close() 83 | ds := NewFileDataSource(f) 84 | 85 | r, err := ds.Fetch() 86 | if err != nil { 87 | b.Fatal(err) 88 | } 89 | 90 | hpackwrapper := hpackwrapper.NewWrapper() 91 | b.ResetTimer() 92 | for i := 0; i < b.N; i++ { 93 | _, err := r.SetUp(consts.DefaultMaxFrameSize, consts.DefaultMaxHeaderListSize, 0, hpackwrapper) 94 | if err != nil { 95 | b.Error(err) 96 | return 97 | } 98 | b.SetBytes(int64(r.Size())) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /datasource/inmem.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "sync/atomic" 8 | 9 | "github.com/ozontech/framer/datasource/decoder" 10 | "github.com/ozontech/framer/formats/grpc/ozon/binary" 11 | "github.com/ozontech/framer/formats/model" 12 | "github.com/ozontech/framer/loader/types" 13 | "github.com/ozontech/framer/utils/pool" 14 | ) 15 | 16 | type InmemDataSource struct { 17 | reader model.PooledRequestReader 18 | decoder *decoder.Decoder 19 | 20 | factory *RequestAdapterFactory 21 | pool *pool.SlicePool[*memRequest] 22 | i atomic.Int32 23 | datas []decoder.Data 24 | } 25 | 26 | func NewInmemDataSource(r io.Reader, factoryOptions ...Option) *InmemDataSource { 27 | return &InmemDataSource{ 28 | binary.NewInput(r).Reader, 29 | decoder.NewDecoder(), 30 | 31 | NewRequestAdapterFactory(factoryOptions...), 32 | pool.NewSlicePoolSize[*memRequest](100), 33 | atomic.Int32{}, 34 | nil, 35 | } 36 | } 37 | 38 | func (ds *InmemDataSource) Init() error { 39 | var b []byte 40 | var err error 41 | for { 42 | b, err = ds.reader.ReadNext() 43 | if err != nil { 44 | if errors.Is(err, io.EOF) { 45 | break 46 | } 47 | return fmt.Errorf("read next request: %w", err) 48 | } 49 | var data decoder.Data 50 | err = ds.decoder.Unmarshal(&data, b) 51 | if err != nil { 52 | return fmt.Errorf("read next request: %w", err) 53 | } 54 | ds.datas = append(ds.datas, data) 55 | } 56 | if len(ds.datas) == 0 { 57 | return errors.New("request file is empty") 58 | } 59 | return nil 60 | } 61 | 62 | func (ds *InmemDataSource) Fetch() (types.Req, error) { 63 | r, ok := ds.pool.Acquire() 64 | if !ok { 65 | adapter := ds.factory.Build() 66 | r = &memRequest{ 67 | pool: ds.pool, 68 | RequestAdapter: adapter, 69 | } 70 | } 71 | 72 | i := int(ds.i.Add(1)) 73 | r.setData(ds.datas[i%len(ds.datas)]) 74 | return r, nil 75 | } 76 | 77 | type memRequest struct { 78 | pool *pool.SlicePool[*memRequest] 79 | *RequestAdapter 80 | } 81 | 82 | func (r *memRequest) Release() { r.pool.Release(r) } 83 | -------------------------------------------------------------------------------- /datasource/inmem_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/ozontech/framer/consts" 10 | "github.com/ozontech/framer/loader/types" 11 | ) 12 | 13 | func BenchmarkInmemDataSource(b *testing.B) { 14 | f, err := os.Open("../test_files/requests") 15 | if err != nil { 16 | b.Fatal(err) 17 | } 18 | defer f.Close() 19 | ds := NewInmemDataSource(f) 20 | assert.NoError(b, ds.Init()) 21 | 22 | rr := make(chan types.Req, 100) 23 | b.ResetTimer() 24 | go func() { 25 | defer close(rr) 26 | for i := 0; i < b.N; i++ { 27 | r, err := ds.Fetch() 28 | if err != nil { 29 | b.Error(err) 30 | return 31 | } 32 | rr <- r 33 | } 34 | }() 35 | 36 | for r := range rr { 37 | _, err := r.SetUp(consts.DefaultMaxFrameSize, consts.DefaultMaxHeaderListSize, 0, &noopHpackFieldWriter{}) 38 | if err != nil { 39 | b.Error(err) 40 | return 41 | } 42 | b.SetBytes(int64(r.Size())) 43 | r.Release() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /datasource/meta_middleware.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ozontech/framer/loader/types" 7 | ) 8 | 9 | type MetaMiddleware interface { 10 | IsAllowed(key string) bool 11 | WriteAdditional(types.HPackFieldWriter) 12 | } 13 | 14 | type noopMiddleware struct{} 15 | 16 | func (noopMiddleware) IsAllowed(string) bool { return true } 17 | func (noopMiddleware) WriteAdditional(types.HPackFieldWriter) {} 18 | 19 | type defaultMiddleware struct { 20 | staticPseudo []string 21 | staticRegular []string 22 | next MetaMiddleware 23 | } 24 | 25 | func newDefaultMiddleware(next MetaMiddleware, additionalHeaders ...string) *defaultMiddleware { 26 | m := &defaultMiddleware{ 27 | staticPseudo: []string{ 28 | ":method", "POST", 29 | ":scheme", "http", 30 | }, 31 | staticRegular: []string{ 32 | "content-type", "application/grpc", 33 | "te", "trailers", 34 | }, 35 | next: noopMiddleware{}, 36 | } 37 | 38 | for i := 0; i < len(additionalHeaders); i += 2 { 39 | k, v := additionalHeaders[i], additionalHeaders[i+1] 40 | if strings.HasPrefix(k, ":") { 41 | m.staticPseudo = append(m.staticPseudo, k, v) 42 | } else { 43 | m.staticRegular = append(m.staticRegular, k, v) 44 | } 45 | } 46 | 47 | return m 48 | } 49 | 50 | func (m *defaultMiddleware) IsAllowed(k string) (allowed bool) { 51 | if k == "" { 52 | return false 53 | } 54 | // отфильтровываем псевдохедеры из меты т.к. 55 | // стандартный клиент также псевдохедеры не пропускает 56 | // и псевдохедерами можно легко сломать стрельбу 57 | if k[0] == ':' { 58 | return false 59 | } 60 | switch k { 61 | case "content-type", "te", "grpc-timeout": 62 | return false 63 | } 64 | return m.next.IsAllowed(k) 65 | } 66 | 67 | func (m *defaultMiddleware) WriteAdditional(hpack types.HPackFieldWriter) { 68 | // добавляем статичные псевдохедеры 69 | staticPseudo := m.staticPseudo 70 | for i := 0; i < len(staticPseudo); i += 2 { 71 | //nolint:errcheck // пишем в буфер, это безопасно 72 | hpack.WriteField(staticPseudo[i], staticPseudo[i+1]) 73 | } 74 | 75 | m.next.WriteAdditional(hpack) 76 | 77 | // добавляем статичные хедеры 78 | staticRegular := m.staticRegular 79 | for i := 0; i < len(staticRegular); i += 2 { 80 | //nolint:errcheck // пишем в буфер, это безопасно 81 | hpack.WriteField(staticRegular[i], staticRegular[i+1]) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /datasource/request_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/net/http2" 10 | "golang.org/x/net/http2/hpack" 11 | 12 | "github.com/ozontech/framer/consts" 13 | "github.com/ozontech/framer/datasource/decoder" 14 | "github.com/ozontech/framer/loader/types" 15 | hpackwrapper "github.com/ozontech/framer/utils/hpack_wrapper" 16 | ) 17 | 18 | func TestFrameHeadersPool(t *testing.T) { 19 | t.Parallel() 20 | a := assert.New(t) 21 | frameHeaders := newFrameHeaders() 22 | buf1 := frameHeaders.Get() 23 | a.Len(buf1, 9) 24 | for i := range buf1 { 25 | buf1[i] = 1 26 | } 27 | 28 | buf2 := frameHeaders.Get() 29 | a.Len(buf2, 9) 30 | for i := range buf2 { 31 | buf2[i] = 2 32 | } 33 | 34 | for i := range buf1 { 35 | a.Equal(byte(1), buf1[i]) 36 | } 37 | 38 | frameHeaders.Reset() 39 | 40 | buf3 := frameHeaders.Get() 41 | a.Len(buf3, 9) 42 | for i := range buf3 { 43 | buf3[i] = 3 44 | } 45 | 46 | // It reuses buf after reset 47 | for i := range buf1 { 48 | a.Equal(byte(3), buf1[i]) 49 | } 50 | } 51 | 52 | type metaMW struct{} 53 | 54 | func (metaMW) IsAllowed(string) bool { return true } 55 | func (metaMW) WriteAdditional(fw types.HPackFieldWriter) { 56 | fw.WriteField(":method", "POST") 57 | fw.WriteField(":authority", ":authority-v") 58 | fw.WriteField("regular-k", "regular-v") 59 | } 60 | 61 | func TestRequest1(t *testing.T) { 62 | t.Parallel() 63 | a := assert.New(t) 64 | r := NewRequestAdapter(metaMW{}) 65 | buf := bytes.NewBuffer(nil) 66 | framer := http2.NewFramer(nil, buf) 67 | framer.ReadMetaHeaders = hpack.NewDecoder(4098, nil) 68 | 69 | const interations = 10 70 | for i := 0; i < interations; i++ { 71 | message := []byte("this is message") 72 | r.data = decoder.Data{ 73 | Tag: "tag", 74 | Method: "/method", 75 | Metadata: []decoder.Meta{ 76 | {Name: "k1", Value: "v1"}, 77 | {Name: "k2", Value: "v2"}, 78 | }, 79 | Message: message, 80 | } 81 | a.Equal(r.FullMethodName(), "/method") 82 | a.Equal(r.Tag(), "tag") 83 | 84 | hpw := hpackwrapper.NewWrapper() 85 | const streamID uint32 = 123 86 | frames, err := r.SetUp(consts.DefaultMaxFrameSize, consts.DefaultMaxHeaderListSize, streamID, hpw) 87 | a.NoError(err) 88 | a.Len(frames, 2) 89 | for _, f := range frames { 90 | for _, c := range f.Chunks { 91 | if c != nil { 92 | buf.Write(c) 93 | } 94 | } 95 | } 96 | 97 | // headers 98 | { 99 | f := frames[0] 100 | a.Zero(f.FlowControlPrice) 101 | 102 | expectedHeaders := []hpack.HeaderField{ 103 | {Name: ":path", Value: "/method"}, 104 | {Name: ":method", Value: "POST"}, 105 | {Name: ":authority", Value: ":authority-v"}, 106 | {Name: "regular-k", Value: "regular-v"}, 107 | {Name: "k1", Value: "v1"}, 108 | {Name: "k2", Value: "v2"}, 109 | } 110 | http2Frame, err := framer.ReadFrame() 111 | a.NoError(err) 112 | mhf := http2Frame.(*http2.MetaHeadersFrame) 113 | 114 | a.Equal(expectedHeaders, mhf.Fields) 115 | header := mhf.Header() 116 | a.Equal(http2.FrameHeaders, header.Type) 117 | a.Equal(http2.FlagHeadersEndHeaders, header.Flags) 118 | a.Equal(streamID, header.StreamID) 119 | } 120 | 121 | // data 122 | { 123 | data := []byte{0} 124 | data = binary.BigEndian.AppendUint32(data, uint32(len(message))) 125 | data = append(data, message...) 126 | 127 | f := frames[0] 128 | a.Zero(f.FlowControlPrice) 129 | 130 | http2Frame, err := framer.ReadFrame() 131 | a.NoError(err) 132 | df := http2Frame.(*http2.DataFrame) 133 | 134 | header := df.Header() 135 | a.Equal(http2.FrameData, header.Type) 136 | a.Equal(http2.FlagDataEndStream, header.Flags) 137 | a.Equal(uint32(len(data)), header.Length) 138 | a.Equal(streamID, header.StreamID) 139 | a.Equal(data, df.Data()) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /examples/requestsgen/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ozontech/framer/examples/requestsgen 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.4 7 | google.golang.org/protobuf v1.34.1 8 | ) 9 | -------------------------------------------------------------------------------- /examples/requestsgen/go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 2 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 3 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 6 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 7 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 8 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 9 | -------------------------------------------------------------------------------- /examples/requestsgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "google.golang.org/protobuf/proto" 10 | "google.golang.org/protobuf/types/known/wrapperspb" // here must be your generated protobuf 11 | ) 12 | 13 | type requestsWriter struct { 14 | w io.Writer 15 | buf []byte 16 | } 17 | 18 | func newRequestsWriter(w io.Writer) *requestsWriter { 19 | return &requestsWriter{w, make([]byte, 0, 1024)} 20 | } 21 | 22 | func (w *requestsWriter) WriteRequest(tag, path string, message []byte) error { 23 | w.buf = w.buf[:10] 24 | w.buf = append(w.buf, '\n') 25 | w.buf = append(w.buf, []byte(path)...) 26 | w.buf = append(w.buf, '\n') 27 | w.buf = append(w.buf, []byte(tag)...) 28 | w.buf = append(w.buf, '\n') 29 | w.buf = append(w.buf, message...) 30 | w.buf = append(w.buf, '\n') 31 | 32 | l := []byte(strconv.Itoa(len(w.buf) - 10)) 33 | b := w.buf[10-len(l):] 34 | copy(b, l) 35 | _, err := w.w.Write(b) 36 | return err 37 | } 38 | 39 | func run(w *requestsWriter) error { 40 | for i := 0; i < 10_000; i++ { 41 | pl, err := proto.Marshal(wrapperspb.String("i am your request #" + strconv.Itoa(i))) 42 | if err != nil { 43 | return err 44 | } 45 | err = w.WriteRequest("/my.service/Method", "request tag", pl) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func main() { 54 | err := run(newRequestsWriter(os.Stdout)) 55 | if err != nil { 56 | log.Fatal(err.Error()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /formats/converter/processor.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "runtime" 10 | 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | type DataHolder = interface{} 15 | 16 | type convertItem struct { 17 | message DataHolder 18 | convertErr error 19 | } 20 | 21 | type Strategy interface { 22 | Read() (DataHolder, error) 23 | Decode(DataHolder) error 24 | 25 | Encode(DataHolder) error 26 | Write(DataHolder) error 27 | } 28 | 29 | type Processor struct { 30 | conf conf 31 | strategy Strategy 32 | } 33 | 34 | func NewProcessor(strategy Strategy, opts ...Option) *Processor { 35 | conf := newDefaultConf() //nolint:govet 36 | for _, o := range opts { 37 | if o != nil { 38 | o(&conf) 39 | } 40 | } 41 | 42 | return &Processor{ 43 | conf: conf, 44 | strategy: strategy, 45 | } 46 | } 47 | 48 | func (p *Processor) runRead(ctx context.Context, readChans []chan convertItem) error { 49 | defer func() { 50 | for _, readChan := range readChans { 51 | close(readChan) 52 | } 53 | }() 54 | 55 | s := p.strategy 56 | threads := p.conf.threads 57 | 58 | var i int 59 | for { 60 | message, err := s.Read() 61 | if err != nil { 62 | if errors.Is(err, io.EOF) { 63 | return nil 64 | } 65 | return fmt.Errorf("read #%d message error: %w", i+1, err) 66 | } 67 | 68 | select { 69 | case readChans[i%threads] <- convertItem{message, nil}: 70 | case <-ctx.Done(): 71 | return ctx.Err() 72 | } 73 | i++ 74 | } 75 | } 76 | 77 | func (p *Processor) runConvert(ctx context.Context, convertChan <-chan convertItem, writeChan chan<- convertItem) error { 78 | defer close(writeChan) 79 | 80 | s := p.strategy 81 | 82 | for { 83 | select { 84 | case item, ok := <-convertChan: 85 | if !ok { 86 | return nil 87 | } 88 | 89 | if err := s.Decode(item.message); err != nil { 90 | item.convertErr = fmt.Errorf("decode error: %w", err) 91 | } else if err = s.Encode(item.message); err != nil { 92 | item.convertErr = fmt.Errorf("encode error: %w", err) 93 | } 94 | 95 | select { 96 | case writeChan <- item: 97 | case <-ctx.Done(): 98 | return ctx.Err() 99 | } 100 | case <-ctx.Done(): 101 | return ctx.Err() 102 | } 103 | } 104 | } 105 | 106 | func (p *Processor) runWrite(ctx context.Context, writeChans []chan convertItem) error { 107 | threads := p.conf.threads 108 | errWriter := p.conf.errWriter 109 | s := p.strategy 110 | 111 | var ( 112 | i int 113 | closedChans int 114 | ) 115 | for { 116 | select { 117 | case item, ok := <-writeChans[i%threads]: 118 | if !ok { 119 | closedChans++ 120 | if closedChans == threads { 121 | return nil 122 | } 123 | continue 124 | } 125 | 126 | i++ 127 | if item.convertErr != nil { 128 | err := fmt.Errorf("message #%d: %w", i, item.convertErr) 129 | if p.conf.failOnConvertErrors { 130 | return err 131 | } 132 | _, err = errWriter.Write([]byte(err.Error() + "\n")) 133 | if err != nil { 134 | return fmt.Errorf("errWriter: %w", err) 135 | } 136 | continue 137 | } 138 | err := s.Write(item.message) 139 | if err != nil { 140 | return fmt.Errorf("write: %w", err) 141 | } 142 | case <-ctx.Done(): 143 | return ctx.Err() 144 | } 145 | } 146 | } 147 | 148 | func (p *Processor) Process(ctx context.Context) error { 149 | threads := p.conf.threads 150 | readChans := make([]chan convertItem, threads) 151 | for i := range readChans { 152 | readChans[i] = make(chan convertItem, p.conf.threadBuffer) 153 | } 154 | writeChans := make([]chan convertItem, threads) 155 | for i := range readChans { 156 | writeChans[i] = make(chan convertItem) 157 | } 158 | 159 | g, ctx := errgroup.WithContext(ctx) 160 | 161 | g.Go(func() (err error) { return p.runRead(ctx, readChans) }) 162 | 163 | for i := 0; i < threads; i++ { 164 | i := i 165 | g.Go(func() error { return p.runConvert(ctx, readChans[i], writeChans[i]) }) 166 | } 167 | 168 | g.Go(func() error { return p.runWrite(ctx, writeChans) }) 169 | 170 | return g.Wait() 171 | } 172 | 173 | type conf struct { 174 | threads int 175 | threadBuffer int 176 | errWriter io.Writer 177 | failOnConvertErrors bool 178 | } 179 | 180 | func newDefaultConf() conf { 181 | return conf{ 182 | threads: runtime.GOMAXPROCS(-1), 183 | threadBuffer: 100, 184 | errWriter: os.Stderr, 185 | } 186 | } 187 | 188 | type Option func(*conf) 189 | 190 | func WithThreads(threads int) Option { 191 | return func(r *conf) { 192 | r.threads = threads 193 | } 194 | } 195 | 196 | func WithThreadsBuffer(threadBuffer int) Option { 197 | return func(c *conf) { 198 | c.threadBuffer = threadBuffer 199 | } 200 | } 201 | 202 | func WithErrWriter(errWriter io.Writer) Option { 203 | return func(c *conf) { 204 | c.errWriter = errWriter 205 | } 206 | } 207 | 208 | func WithFailOnConvertErrors() Option { 209 | return func(r *conf) { 210 | r.failOnConvertErrors = true 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /formats/grpc/convert_strategy.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/ozontech/framer/formats/model" 5 | "github.com/ozontech/framer/utils/pool" 6 | ) 7 | 8 | type DataHolder struct { 9 | data model.Data 10 | readBuf []byte 11 | writeBuf []byte 12 | } 13 | 14 | type ConvertStrategy struct { 15 | pool *pool.SlicePool[*DataHolder] 16 | 17 | in *model.InputFormat 18 | out *model.OutputFormat 19 | } 20 | 21 | func NewConvertStrategy(in *model.InputFormat, out *model.OutputFormat) *ConvertStrategy { 22 | return &ConvertStrategy{ 23 | pool: pool.NewSlicePool[*DataHolder](), 24 | in: in, 25 | out: out, 26 | } 27 | } 28 | 29 | func (s *ConvertStrategy) Read() (interface{}, error) { 30 | dh, ok := s.pool.Acquire() 31 | if !ok { 32 | dh = new(DataHolder) 33 | } 34 | 35 | var err error 36 | dh.readBuf, err = s.in.Reader.ReadNext() 37 | return dh, err 38 | } 39 | 40 | func (s *ConvertStrategy) Decode(dataHolder interface{}) error { 41 | dh := dataHolder.(*DataHolder) 42 | return s.in.Decoder.Unmarshal(&dh.data, dh.readBuf) 43 | } 44 | 45 | func (s *ConvertStrategy) Encode(dataHolder interface{}) error { 46 | dh := dataHolder.(*DataHolder) 47 | var err error 48 | dh.writeBuf, err = s.out.Encoder.MarshalAppend(dh.writeBuf, &dh.data) 49 | return err 50 | } 51 | 52 | func (s *ConvertStrategy) Write(dataHolder interface{}) error { 53 | dh := dataHolder.(*DataHolder) 54 | defer s.in.Reader.Release(dh.readBuf) 55 | return s.out.Writer.WriteNext(dh.writeBuf) 56 | } 57 | -------------------------------------------------------------------------------- /formats/grpc/convert_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/ozontech/framer/formats/converter" 17 | formatsGRPC "github.com/ozontech/framer/formats/grpc" 18 | ozonBinary "github.com/ozontech/framer/formats/grpc/ozon/binary" 19 | ozonJson "github.com/ozontech/framer/formats/grpc/ozon/json" 20 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 21 | pandoryJson "github.com/ozontech/framer/formats/grpc/pandora/json" 22 | model "github.com/ozontech/framer/formats/model" 23 | ) 24 | 25 | func testGRPCConvert( 26 | ctx context.Context, 27 | t *testing.T, 28 | pathFrom, pathTo string, 29 | from grpcIN, to grpcOUT, 30 | ) { 31 | ctx, cancel := context.WithCancel(ctx) 32 | defer cancel() 33 | 34 | f, err := os.Open(filepath.Join("./test_files/", filepath.Clean(pathFrom))) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | outBuf := new(bytes.Buffer) 40 | 41 | processor := converter.NewProcessor(formatsGRPC.NewConvertStrategy( 42 | from.Factory(f), 43 | to.Factory(outBuf), 44 | )) 45 | 46 | r := require.New(t) 47 | r.NoError(processor.Process(ctx)) 48 | 49 | expectedB, err := os.ReadFile(filepath.Join("./test_files/", filepath.Clean(pathTo))) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | assert.Equal(t, string(expectedB), outBuf.String()) 55 | } 56 | 57 | func TestGRPCConverts(t *testing.T) { 58 | reflection := makeReflection(t) 59 | 60 | t.Parallel() 61 | 62 | inFormats := []grpcIN{ 63 | { 64 | Ext: "ozon.json", 65 | Factory: func(r io.Reader) *model.InputFormat { 66 | return ozonJson.NewInput(r, reflection) 67 | }, 68 | }, 69 | { 70 | Ext: "pandora.json", 71 | Factory: func(r io.Reader) *model.InputFormat { 72 | return pandoryJson.NewInput(r, reflection) 73 | }, 74 | }, 75 | { 76 | Ext: "ozon.binary", 77 | Factory: ozonBinary.NewInput, 78 | }, 79 | } 80 | outFormats := []grpcOUT{ 81 | { 82 | Ext: "ozon.json", 83 | Factory: func(w io.Writer) *model.OutputFormat { 84 | return ozonJson.NewOutput(w, reflection) 85 | }, 86 | }, 87 | { 88 | Ext: "ozon.binary", 89 | Factory: ozonBinary.NewOutput, 90 | }, 91 | } 92 | 93 | for _, inFormat := range inFormats { 94 | for _, outFormat := range outFormats { 95 | for _, fileName := range []string{"requests"} { 96 | inFormat := inFormat 97 | outFormat := outFormat 98 | 99 | t.Run(fmt.Sprintf("convert_%s_2_%s(%s)", inFormat.Ext, outFormat.Ext, fileName), func(t *testing.T) { 100 | var ( 101 | inFile = fmt.Sprintf("%s.%s", fileName, inFormat.Ext) 102 | outFile = fmt.Sprintf("%s.%s", fileName, outFormat.Ext) 103 | ) 104 | t.Parallel() 105 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 106 | defer cancel() 107 | testGRPCConvert(ctx, t, inFile, outFile, inFormat, outFormat) 108 | }) 109 | } 110 | } 111 | } 112 | } 113 | 114 | type grpcIN struct { 115 | Ext string 116 | Factory func(io.Reader) *model.InputFormat 117 | } 118 | 119 | type grpcOUT struct { 120 | Ext string 121 | Factory func(io.Writer) *model.OutputFormat 122 | } 123 | 124 | func makeReflection(t *testing.T) reflection.DynamicMessagesStore { 125 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 126 | defer cancel() 127 | 128 | store, err := reflection.NewLocalFetcher([]string{"./ozon/json/encoding/testproto/service.proto"}, nil).Fetch(ctx) 129 | if err != nil { 130 | t.Fatalf("can't get reflection: %s", err) 131 | } 132 | 133 | return store 134 | } 135 | -------------------------------------------------------------------------------- /formats/grpc/middleware.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/ozontech/framer/formats/model" 5 | ) 6 | 7 | // MiddlewareFunc позволяет выполнять модификации над запросами во время кодирования/декодирования. 8 | type MiddlewareFunc func(*model.Data) *model.Data 9 | 10 | // WrapEncoder оборачивает Marshaler, выполняя заданные модификации над запросами перед каждым вызовом MarshalAppend. 11 | func WrapEncoder(enc model.Marshaler, mw ...MiddlewareFunc) model.Marshaler { 12 | if len(mw) == 0 { 13 | return enc 14 | } 15 | return &middlewareEncoder{ 16 | enc: enc, 17 | mws: mw, 18 | } 19 | } 20 | 21 | type middlewareEncoder struct { 22 | enc model.Marshaler 23 | mws []MiddlewareFunc 24 | } 25 | 26 | func (e *middlewareEncoder) MarshalAppend(b []byte, data *model.Data) ([]byte, error) { 27 | for _, mw := range e.mws { 28 | data = mw(data) 29 | } 30 | return e.enc.MarshalAppend(b, data) 31 | } 32 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/encoding/binary_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ozontech/framer/formats/grpc/ozon/binary/encoding" 7 | "github.com/ozontech/framer/formats/model" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type test struct { 13 | name string 14 | bytes []byte 15 | data *model.Data 16 | } 17 | 18 | func makeTests() []test { 19 | return []test{{ 20 | "simple", 21 | []byte( 22 | "/Test\n" + 23 | "/test.api.TestApi/Test\n" + 24 | `{"key":["val1","val2"]}` + "\n" + 25 | "This is body\r\nmultiline\r\nbody\r\n\r\n", 26 | ), 27 | &model.Data{ 28 | Tag: []byte("/Test"), 29 | Method: []byte("/test.api.TestApi/Test"), 30 | Metadata: []model.Meta{ 31 | {Name: []byte("key"), Value: []byte("val1")}, 32 | {Name: []byte("key"), Value: []byte("val2")}, 33 | }, 34 | Message: []byte("This is body\r\nmultiline\r\nbody\r\n\r\n"), 35 | }, 36 | }} 37 | } 38 | 39 | func TestDecoder(t *testing.T) { 40 | t.Parallel() 41 | d := encoding.NewDecoder() 42 | for _, tc := range makeTests() { 43 | tc := tc 44 | 45 | t.Run(tc.name, func(t *testing.T) { 46 | t.Parallel() 47 | data := new(model.Data) 48 | require.NoError(t, d.Unmarshal(data, tc.bytes)) 49 | assert.Equal(t, tc.data, data) 50 | }) 51 | } 52 | } 53 | 54 | func TestEncoder(t *testing.T) { 55 | t.Parallel() 56 | e := encoding.NewEncoder() 57 | for _, tc := range makeTests() { 58 | tc := tc 59 | 60 | t.Run(tc.name, func(t *testing.T) { 61 | t.Parallel() 62 | a := assert.New(t) 63 | b, err := e.MarshalAppend(nil, tc.data) 64 | a.NoError(err) 65 | a.Equal(string(tc.bytes), string(b)) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/encoding/decoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | kv "github.com/ozontech/framer/formats/internal/kv" 8 | jsonkv "github.com/ozontech/framer/formats/internal/kv/json" 9 | "github.com/ozontech/framer/formats/model" 10 | ) 11 | 12 | type Decoder struct { 13 | metaUnmarshaler kv.Unmarshaler 14 | } 15 | 16 | func NewDecoder() *Decoder { 17 | return &Decoder{ 18 | metaUnmarshaler: jsonkv.NewMultiVal(), 19 | } 20 | } 21 | 22 | func (decoder *Decoder) Unmarshal(d *model.Data, b []byte) error { 23 | d.Reset() 24 | 25 | d.Tag, b = nextLine(b) 26 | d.Method, b = nextLine(b) 27 | 28 | metaBytes, b := nextLine(b) 29 | var err error 30 | d.Metadata, err = decoder.metaUnmarshaler.UnmarshalAppend(d.Metadata, metaBytes) 31 | if err != nil { 32 | return fmt.Errorf("meta unmarshal error: %w", err) 33 | } 34 | 35 | d.Message = b 36 | return nil 37 | } 38 | 39 | func nextLine(in []byte) ([]byte, []byte) { 40 | index := bytes.IndexByte(in, '\n') 41 | if index == -1 { 42 | return []byte{}, []byte{} 43 | } 44 | return in[:index], in[index+1:] 45 | } 46 | 47 | var _ model.Unmarshaler = &Decoder{} 48 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/encoding/encoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | kv "github.com/ozontech/framer/formats/internal/kv" 5 | jsonkv "github.com/ozontech/framer/formats/internal/kv/json" 6 | "github.com/ozontech/framer/formats/model" 7 | ) 8 | 9 | type Encoder struct { 10 | metaMarshaler kv.Marshaler 11 | } 12 | 13 | func NewEncoder() *Encoder { 14 | return &Encoder{jsonkv.NewMultiVal()} 15 | } 16 | 17 | func (encoder *Encoder) MarshalAppend(b []byte, d *model.Data) ([]byte, error) { 18 | b = append(b, d.Tag...) 19 | b = append(b, '\n') 20 | 21 | b = append(b, d.Method...) 22 | b = append(b, '\n') 23 | 24 | b = encoder.metaMarshaler.MarshalAppend(b, d.Metadata) 25 | b = append(b, '\n') 26 | 27 | b = append(b, d.Message...) 28 | 29 | return b, nil 30 | } 31 | 32 | var _ model.Marshaler = &Encoder{} 33 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/format.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ozontech/framer/formats/grpc/ozon/binary/encoding" 7 | binaryIO "github.com/ozontech/framer/formats/grpc/ozon/binary/io" 8 | "github.com/ozontech/framer/formats/internal/pooledreader" 9 | "github.com/ozontech/framer/formats/model" 10 | ) 11 | 12 | func NewInput(r io.Reader) *model.InputFormat { 13 | return &model.InputFormat{ 14 | Reader: pooledreader.New(binaryIO.NewReader(r)), 15 | Decoder: encoding.NewDecoder(), 16 | } 17 | } 18 | 19 | func NewOutput(w io.Writer) *model.OutputFormat { 20 | return &model.OutputFormat{ 21 | Writer: binaryIO.NewWriter(w), 22 | Encoder: encoding.NewEncoder(), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/io/io.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | ) 8 | 9 | const nChar = '\n' 10 | 11 | type Reader struct { 12 | buf []byte 13 | unprocessed []byte 14 | 15 | r io.Reader 16 | } 17 | 18 | func NewReader(r io.Reader, bufSize ...int) *Reader { 19 | size := 4096 20 | if len(bufSize) > 0 { 21 | size = bufSize[0] 22 | } 23 | return &Reader{ 24 | buf: make([]byte, size), 25 | r: r, 26 | } 27 | } 28 | 29 | func (r *Reader) fillUnprocessed() error { 30 | n, err := r.r.Read(r.buf[:]) 31 | if err != nil { 32 | return err 33 | } 34 | r.unprocessed = r.buf[:n] 35 | return nil 36 | } 37 | 38 | // ReadNext читает контейнер запроса 39 | func (r *Reader) ReadNext(b []byte) ([]byte, error) { 40 | var ( 41 | size int 42 | n int 43 | err error 44 | ) 45 | 46 | for { 47 | size, n, err = fillSize(size, r.unprocessed) 48 | r.unprocessed = r.unprocessed[n:] 49 | if err == nil { 50 | break 51 | } 52 | 53 | if err != io.ErrUnexpectedEOF { 54 | return nil, err 55 | } 56 | 57 | err = r.fillUnprocessed() 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | 63 | if cap(b) < size { 64 | b = append(b, make([]byte, size-len(b))...) 65 | } else { 66 | b = b[:size] 67 | } 68 | var filled int 69 | for filled < size { 70 | if len(r.unprocessed) == 0 { 71 | err = r.fillUnprocessed() 72 | if err != nil { 73 | return nil, err 74 | } 75 | } 76 | 77 | n = copy(b[filled:], r.unprocessed) 78 | filled += n 79 | r.unprocessed = r.unprocessed[n:] 80 | } 81 | 82 | for { 83 | if len(r.unprocessed) == 0 { 84 | err = r.fillUnprocessed() 85 | if err == io.EOF { 86 | break 87 | } 88 | if err != nil { 89 | return nil, err 90 | } 91 | } else { 92 | if r.unprocessed[0] != nChar { 93 | break 94 | } 95 | r.unprocessed = r.unprocessed[1:] 96 | } 97 | } 98 | 99 | return b, nil 100 | } 101 | 102 | func fillSize(accIn int, in []byte) (acc int, consumed int, err error) { 103 | for i, b := range in { 104 | var d int 105 | switch { 106 | case '0' <= b && b <= '9': 107 | d = int(b - '0') 108 | case b == '\n': 109 | return accIn, i + 1, nil 110 | default: 111 | return accIn, i + 1, fmt.Errorf("unexpected char: '%c'", b) 112 | } 113 | accIn = accIn*10 + d 114 | } 115 | return accIn, len(in), io.ErrUnexpectedEOF 116 | } 117 | 118 | type Writer struct { 119 | w io.Writer 120 | prefixBuf []byte 121 | } 122 | 123 | func NewWriter(w io.Writer) *Writer { 124 | return &Writer{w: w} 125 | } 126 | 127 | func (w *Writer) WriteNext(p []byte) error { 128 | w.prefixBuf = strconv.AppendUint(w.prefixBuf[:0], uint64(len(p)), 10) 129 | w.prefixBuf = append(w.prefixBuf, '\n') 130 | _, err := w.w.Write(w.prefixBuf) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | p = append(p, '\n') 136 | _, err = w.w.Write(p) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /formats/grpc/ozon/binary/io/io_test.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type test struct { 14 | name string 15 | bytes []byte 16 | data [][]byte 17 | } 18 | 19 | func makeTests() []test { 20 | return []test{ 21 | { 22 | "simple", 23 | []byte(`305 24 | /test.api.TestApi/Test 25 | /test.api.TestApi/Test 26 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 27 | 28 | 123456 29 | 305 30 | /test.api.TestApi/Test 31 | /test.api.TestApi/Test 32 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 33 | 34 | 123456 35 | `), 36 | [][]byte{ 37 | []byte(`/test.api.TestApi/Test 38 | /test.api.TestApi/Test 39 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 40 | 41 | 123456`), 42 | []byte(`/test.api.TestApi/Test 43 | /test.api.TestApi/Test 44 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 45 | 46 | 123456`), 47 | }, 48 | }, 49 | } 50 | } 51 | 52 | func TestReader(t *testing.T) { 53 | t.Parallel() 54 | 55 | runTest := func(tc test) { 56 | t.Run(tc.name, func(t *testing.T) { 57 | t.Parallel() 58 | a := assert.New(t) 59 | 60 | r := NewReader(bytes.NewReader(tc.bytes)) 61 | var i int 62 | for { 63 | d, err := r.ReadNext(nil) 64 | if errors.Is(err, io.EOF) { 65 | break 66 | } 67 | a.NoError(err) 68 | 69 | var expected string 70 | if i < len(tc.data) { 71 | expected = string(tc.data[i]) 72 | } 73 | assert.Equal(t, expected, string(d), fmt.Sprintf("read %d request", i)) 74 | i++ 75 | } 76 | assert.Equal(t, len(tc.data), i) 77 | }) 78 | } 79 | 80 | for _, tc := range makeTests() { 81 | runTest(tc) 82 | } 83 | 84 | runTest(test{ 85 | "without last \\n", 86 | []byte(`305 87 | /test.api.TestApi/Test 88 | /test.api.TestApi/Test 89 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 90 | 91 | 123456 92 | 305 93 | /test.api.TestApi/Test 94 | /test.api.TestApi/Test 95 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 96 | 97 | 123456`), 98 | [][]byte{ 99 | []byte(`/test.api.TestApi/Test 100 | /test.api.TestApi/Test 101 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 102 | 103 | 123456`), 104 | []byte(`/test.api.TestApi/Test 105 | /test.api.TestApi/Test 106 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 107 | 108 | 123456`), 109 | }, 110 | }) 111 | } 112 | 113 | func TestWriter(t *testing.T) { 114 | t.Parallel() 115 | for _, tc := range makeTests() { 116 | tc := tc 117 | 118 | bb := new(bytes.Buffer) 119 | t.Run(tc.name, func(t *testing.T) { 120 | a := assert.New(t) 121 | t.Parallel() 122 | bb.Reset() 123 | w := NewWriter(bb) 124 | for _, d := range tc.data { 125 | a.NoError(w.WriteNext(d)) 126 | } 127 | assert.Equal(t, string(tc.bytes), bb.String()) 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/decoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/jhump/protoreflect/dynamic" 9 | "github.com/mailru/easyjson/jlexer" 10 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 11 | "github.com/ozontech/framer/formats/internal/kv" 12 | jsonkv "github.com/ozontech/framer/formats/internal/kv/json" 13 | "github.com/ozontech/framer/formats/model" 14 | ) 15 | 16 | type DecoderOption func(*Decoder) 17 | 18 | func WithSingleMetaValue() DecoderOption { 19 | return func(r *Decoder) { 20 | r.metaUnmarshaler = jsonkv.NewSingleVal() 21 | } 22 | } 23 | 24 | type Decoder struct { 25 | desriptors reflection.DynamicMessagesStore 26 | metaUnmarshaler kv.Unmarshaler 27 | } 28 | 29 | func NewDecoder( 30 | desriptors reflection.DynamicMessagesStore, 31 | opts ...DecoderOption, 32 | ) *Decoder { 33 | d := &Decoder{desriptors, jsonkv.NewMultiVal()} 34 | for _, o := range opts { 35 | o(d) 36 | } 37 | return d 38 | } 39 | 40 | // Unmarshal unmarshal bytes to *grpc.Data struct 41 | func (decoder *Decoder) Unmarshal(d *model.Data, b []byte) error { 42 | d.Reset() 43 | var err error 44 | 45 | in := jlexer.Lexer{Data: b} 46 | 47 | var ( 48 | jsonPayload []byte 49 | dynamicMessage *dynamic.Message 50 | ) 51 | in.Delim('{') 52 | for !in.IsDelim('}') { 53 | key := in.UnsafeFieldName(true) 54 | in.WantColon() 55 | switch key { 56 | case "tag": 57 | d.Tag = in.UnsafeBytes() 58 | case "call": 59 | d.Method = methodFromJSON(in.UnsafeBytes()) 60 | var release func() 61 | dynamicMessage, release = decoder.desriptors.Get(d.Method) 62 | if dynamicMessage == nil { 63 | return errors.New("no such method: " + string(d.Method)) 64 | } 65 | defer release() 66 | case "metadata": 67 | d.Metadata, err = decoder.metaUnmarshaler.UnmarshalAppend( 68 | d.Metadata, in.Raw(), 69 | ) 70 | if err != nil { 71 | return fmt.Errorf("unmarshal meta: %w", err) 72 | } 73 | case "payload": 74 | jsonPayload = in.Raw() 75 | default: 76 | return fmt.Errorf("unknown field: %s", key) 77 | } 78 | in.WantComma() 79 | } 80 | in.Delim('}') 81 | in.Consumed() 82 | 83 | if dynamicMessage == nil { 84 | return fmt.Errorf(`"call" is required`) 85 | } 86 | 87 | if err = dynamicMessage.UnmarshalJSON(jsonPayload); err != nil { 88 | return fmt.Errorf( 89 | "unmarshalling json payload for %s: %w", 90 | d.Method, err, 91 | ) 92 | } 93 | d.Message, err = dynamicMessage.MarshalAppend(d.Message) 94 | if err != nil { 95 | return fmt.Errorf("marshaling grpc message into binary: %w", err) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // methodFromJSON - приводит метод к стандартному виду '/package.Service/Call' 102 | // старый формат - 'package.Service.Call'. 103 | func methodFromJSON(method []byte) []byte { 104 | if len(method) == 0 || method[0] == '/' { 105 | return method 106 | } 107 | 108 | ind := bytes.LastIndexByte(method, '.') 109 | if ind != -1 { 110 | method[ind] = '/' 111 | } 112 | method = append(method, 0x0) 113 | copy(method[1:], method) 114 | method[0] = '/' 115 | 116 | return method 117 | } 118 | 119 | var _ model.Unmarshaler = &Decoder{} 120 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/encoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 8 | "github.com/ozontech/framer/formats/internal/json" 9 | "github.com/ozontech/framer/formats/internal/kv" 10 | jsonkv "github.com/ozontech/framer/formats/internal/kv/json" 11 | "github.com/ozontech/framer/formats/model" 12 | ) 13 | 14 | type Encoder struct { 15 | desriptors reflection.DynamicMessagesStore 16 | metaMarshaler kv.Marshaler 17 | } 18 | 19 | func NewEncoder(desriptors reflection.DynamicMessagesStore) *Encoder { 20 | return &Encoder{desriptors, jsonkv.NewMultiVal()} 21 | } 22 | 23 | func (encoder *Encoder) MarshalAppend(b []byte, d *model.Data) ([]byte, error) { 24 | message, release := encoder.desriptors.Get(d.Method) 25 | if message == nil { 26 | return nil, fmt.Errorf("no such method: %s", d.Method) 27 | } 28 | defer release() 29 | 30 | b = append(b, `{"tag":`...) 31 | b = json.EscapeStringAppend(b, d.Tag) 32 | b = append(b, `,"call":`...) 33 | b = json.EscapeStringAppend(b, methodToJSON(d.Method)) 34 | b = append(b, `,"metadata":`...) 35 | b = encoder.metaMarshaler.MarshalAppend(b, d.Metadata) 36 | b = append(b, `,"payload":`...) 37 | 38 | if err := message.Unmarshal(d.Message); err != nil { 39 | return b, fmt.Errorf("unmarshaling binary payload for %s: %w", d.Method, err) 40 | } 41 | jsonBody, err := message.MarshalJSON() 42 | if err != nil { 43 | return b, fmt.Errorf("protojson marshal: %w", err) 44 | } 45 | b = append(b, jsonBody...) 46 | return append(b, '}'), nil 47 | } 48 | 49 | // Приводит формат имени метода к виду используемому в либе dynamic - package.Service.Call 50 | // в запросах используется каноничный fqn - /package.Service/Call. 51 | func methodToJSON(method []byte) []byte { 52 | if len(method) == 0 { 53 | return method 54 | } 55 | 56 | if method[0] == '/' { 57 | method = method[1:] 58 | } 59 | 60 | ind := bytes.LastIndexByte(method, '/') 61 | if ind != -1 { 62 | method[ind] = '.' 63 | } 64 | 65 | return method 66 | } 67 | 68 | var _ model.Marshaler = &Encoder{} 69 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/json_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/jhump/protoreflect/desc" 8 | "github.com/jhump/protoreflect/desc/protoparse" 9 | "github.com/stretchr/testify/assert" 10 | "google.golang.org/protobuf/proto" 11 | 12 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding" 13 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 14 | tesproto "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/testproto" 15 | "github.com/ozontech/framer/formats/model" 16 | ) 17 | 18 | func mustMakeStore() reflection.DynamicMessagesStore { 19 | fds, err := protoparse.Parser{ 20 | LookupImport: desc.LoadFileDescriptor, 21 | ImportPaths: []string{"./testproto"}, 22 | }.ParseFiles("service.proto") 23 | if err != nil { 24 | panic(fmt.Errorf("can't parse proto files: %w", err)) 25 | } 26 | 27 | var methods []*desc.MethodDescriptor 28 | for _, fd := range fds { 29 | services := fd.GetServices() 30 | for _, service := range services { 31 | methods = append(methods, service.GetMethods()...) 32 | } 33 | } 34 | return reflection.NewDynamicMessagesStore(methods) 35 | } 36 | 37 | type test struct { 38 | name string 39 | bytes []byte 40 | data *model.Data 41 | } 42 | 43 | func makeTests() []test { 44 | return []test{{ 45 | "simple", 46 | []byte( 47 | "{" + 48 | `"tag":"tag",` + 49 | `"call":"testpackage.TestService.TestMethod5",` + 50 | `"metadata":{"key":["val1","val2"]},` + 51 | `"payload":{"request":"12345"}` + 52 | "}", 53 | ), 54 | &model.Data{ 55 | Tag: []byte("tag"), 56 | Method: []byte("/testpackage.TestService/TestMethod5"), 57 | Metadata: []model.Meta{ 58 | {Name: []byte("key"), Value: []byte("val1")}, 59 | {Name: []byte("key"), Value: []byte("val2")}, 60 | }, 61 | Message: mustMarshal(&tesproto.TestRequest5{ 62 | Request: "12345", 63 | }), 64 | }, 65 | }} 66 | } 67 | 68 | func mustMarshal(m proto.Message) []byte { 69 | b, err := proto.Marshal(m) 70 | if err != nil { 71 | panic(err) 72 | } 73 | return b 74 | } 75 | 76 | func TestDecoder(t *testing.T) { 77 | t.Parallel() 78 | d := encoding.NewDecoder(mustMakeStore()) 79 | for _, tc := range makeTests() { 80 | tc := tc 81 | 82 | t.Run(tc.name, func(t *testing.T) { 83 | t.Parallel() 84 | data := new(model.Data) 85 | in := append([]byte{}, tc.bytes...) // bytes.Clone 86 | err := d.Unmarshal(data, in) 87 | if err != nil { 88 | t.Fatal(err) 89 | return 90 | } 91 | if !assert.Equal(t, tc.data, data) { 92 | t.Logf("%s\n", tc.data.Method) 93 | } 94 | }) 95 | } 96 | 97 | t.Run("garbage in root", func(t *testing.T) { 98 | t.Parallel() 99 | data := new(model.Data) 100 | in := []byte("garbage") 101 | assert.Error(t, d.Unmarshal(data, in)) 102 | }) 103 | 104 | t.Run("garbage in payload", func(t *testing.T) { 105 | t.Parallel() 106 | data := new(model.Data) 107 | in := []byte( 108 | "{" + 109 | `"tag":"tag",` + 110 | `"call":"testpackage.TestService.TestMethod5",` + 111 | `"metadata":{"key":["val1","val2"]},` + 112 | `"payload":"garbage"` + 113 | "}", 114 | ) 115 | assert.Error(t, d.Unmarshal(data, in)) 116 | }) 117 | } 118 | 119 | func TestEncoder(t *testing.T) { 120 | t.Parallel() 121 | e := encoding.NewEncoder(mustMakeStore()) 122 | for _, tc := range makeTests() { 123 | tc := tc 124 | 125 | t.Run(tc.name, func(t *testing.T) { 126 | t.Parallel() 127 | b, err := e.MarshalAppend(nil, tc.data) 128 | if err != nil { 129 | t.Fatal(err) 130 | return 131 | } 132 | assert.Equal(t, string(tc.bytes), string(b)) 133 | }) 134 | } 135 | 136 | t.Run("garbage in payload", func(t *testing.T) { 137 | t.Parallel() 138 | _, err := e.MarshalAppend(nil, &model.Data{ 139 | Tag: []byte("tag"), 140 | Method: []byte("/testpackage.TestService/TestMethod5"), 141 | Metadata: []model.Meta{ 142 | {Name: []byte("key"), Value: []byte("val1")}, 143 | {Name: []byte("key"), Value: []byte("val2")}, 144 | }, 145 | Message: []byte("garbagebytes"), 146 | }) 147 | assert.Error(t, err) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/reflection/reflector.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/jhump/protoreflect/desc" 10 | "github.com/jhump/protoreflect/desc/protoparse" 11 | "github.com/jhump/protoreflect/grpcreflect" 12 | "google.golang.org/grpc" 13 | reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 14 | ) 15 | 16 | type Fetcher interface { 17 | Fetch(ctx context.Context) (DynamicMessagesStore, error) 18 | } 19 | 20 | type ErrFetcher struct { 21 | err error 22 | } 23 | 24 | func NewErrFetcher(err error) *ErrFetcher { 25 | return &ErrFetcher{err} 26 | } 27 | 28 | func (f *ErrFetcher) Fetch(context.Context) (DynamicMessagesStore, error) { 29 | return nil, f.err 30 | } 31 | 32 | type CachedFetcher struct { 33 | next Fetcher 34 | once *sync.Once 35 | store DynamicMessagesStore 36 | err error 37 | } 38 | 39 | func NewCachedFetcher(next Fetcher) *CachedFetcher { 40 | return &CachedFetcher{next: next, once: new(sync.Once)} 41 | } 42 | 43 | func (f *CachedFetcher) Fetch(ctx context.Context) (DynamicMessagesStore, error) { 44 | f.once.Do(func() { 45 | f.store, f.err = f.next.Fetch(ctx) 46 | }) 47 | return f.store, f.err 48 | } 49 | 50 | type LocalFetcher struct { 51 | filenames, importPaths []string 52 | } 53 | 54 | func NewLocalFetcher(filenames, importPaths []string) LocalFetcher { 55 | return LocalFetcher{filenames, importPaths} 56 | } 57 | 58 | func (f LocalFetcher) Fetch(context.Context) (DynamicMessagesStore, error) { 59 | fds, err := protoparse.Parser{ 60 | LookupImport: desc.LoadFileDescriptor, 61 | ImportPaths: f.importPaths, 62 | }.ParseFiles(f.filenames...) 63 | if err != nil { 64 | return nil, fmt.Errorf("can't parse proto files: %w", err) 65 | } 66 | 67 | var methods []*desc.MethodDescriptor 68 | for _, fs := range fds { 69 | services := fs.GetServices() 70 | for _, service := range services { 71 | methods = append(methods, service.GetMethods()...) 72 | } 73 | } 74 | return NewDynamicMessagesStore(methods), nil 75 | } 76 | 77 | type WarnLogger interface { 78 | Println(string) 79 | } 80 | 81 | type RemoteFetcher struct { 82 | conn *grpc.ClientConn 83 | timeout time.Duration 84 | warns []string 85 | } 86 | 87 | func NewRemoteFetcher(conn *grpc.ClientConn) *RemoteFetcher { 88 | return &RemoteFetcher{conn, 5 * time.Second, nil} 89 | } 90 | 91 | func (f *RemoteFetcher) Warnings() []string { 92 | return f.warns 93 | } 94 | 95 | func (f *RemoteFetcher) Fetch(ctx context.Context) (DynamicMessagesStore, error) { 96 | refClient := grpcreflect.NewClient(ctx, reflectpb.NewServerReflectionClient(f.conn)) 97 | listServices, err := refClient.ListServices() 98 | if err != nil { 99 | return nil, fmt.Errorf("reflection fetching: %w", err) 100 | } 101 | 102 | var methods []*desc.MethodDescriptor 103 | for _, s := range listServices { 104 | service, err := refClient.ResolveService(s) 105 | if err != nil { 106 | f.warns = append(f.warns, "service not found: "+s) 107 | continue 108 | } 109 | methods = append(methods, service.GetMethods()...) 110 | } 111 | 112 | return NewDynamicMessagesStore(methods), nil 113 | } 114 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/reflection/store.go: -------------------------------------------------------------------------------- 1 | package reflection 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | 7 | "github.com/jhump/protoreflect/desc" 8 | "github.com/jhump/protoreflect/dynamic" 9 | ) 10 | 11 | type DynamicMessagesStore interface { 12 | // must return nil if not found 13 | Get(methodName []byte) (message *dynamic.Message, release func()) 14 | } 15 | 16 | type dynamicMessagesStore struct { 17 | items map[string]*sync.Pool 18 | } 19 | 20 | func (s *dynamicMessagesStore) Get(methodName []byte) (*dynamic.Message, func()) { 21 | pool, ok := s.items[string(methodName)] 22 | if !ok { 23 | return nil, nil 24 | } 25 | message := pool.Get().(*dynamic.Message) 26 | return message, func() { pool.Put(message) } 27 | } 28 | 29 | func NewDynamicMessagesStore(descriptors []*desc.MethodDescriptor) DynamicMessagesStore { 30 | items := make(map[string]*sync.Pool, len(descriptors)) 31 | for _, descriptor := range descriptors { 32 | descriptor := descriptor 33 | 34 | fqn := string(NormalizeMethod([]byte(descriptor.GetFullyQualifiedName()))) 35 | items[fqn] = &sync.Pool{New: func() interface{} { 36 | return dynamic.NewMessage(descriptor.GetInputType()) 37 | }} 38 | } 39 | return &dynamicMessagesStore{items} 40 | } 41 | 42 | // NormalizeMethod - приводит метод к стандартному виду '/package.Service/Call' 43 | // из 'package.Service.Call'. 44 | func NormalizeMethod(method []byte) []byte { 45 | if len(method) == 0 || method[0] == '/' { 46 | return method 47 | } 48 | 49 | ind := bytes.LastIndexByte(method, '.') 50 | if ind != -1 { 51 | method[ind] = '/' 52 | } 53 | method = append(method, 0x0) 54 | copy(method[1:], method) 55 | method[0] = '/' 56 | 57 | return method 58 | } 59 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/reflection/store_test.go: -------------------------------------------------------------------------------- 1 | //nolint:goconst 2 | package reflection 3 | 4 | import ( 5 | "bytes" 6 | "sort" 7 | "strconv" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkMapStore(b *testing.B) { 13 | for _, n := range []int{1, 10, 100, 1000} { 14 | m := make(map[string][]byte) 15 | var sm sync.Map 16 | items := []storeItem{} 17 | for i := 0; i < n; i++ { 18 | k := "/test.api.TestApi/Test" + strconv.Itoa(i) 19 | v := []byte(k) 20 | 21 | m[k] = v 22 | sm.Store(k, v) 23 | items = append(items, storeItem{ 24 | k: []byte(k), 25 | v: v, 26 | }) 27 | } 28 | s := newDynamicMessagesStore(items) 29 | 30 | b.Run("map"+strconv.Itoa(n), func(b *testing.B) { 31 | k := []byte("/test.api.TestApi/Test" + strconv.Itoa(b.N%n)) 32 | for i := 0; i < b.N; i++ { 33 | _ = m[string(k)] 34 | } 35 | }) 36 | 37 | b.Run("binSearchStore"+strconv.Itoa(n), func(b *testing.B) { 38 | k := []byte("/test.api.TestApi/Test" + strconv.Itoa(b.N%n)) 39 | for i := 0; i < b.N; i++ { 40 | s.Get(k) 41 | } 42 | }) 43 | 44 | b.Run("syncmap"+strconv.Itoa(n), func(b *testing.B) { 45 | k := []byte("/test.api.TestApi/Test" + strconv.Itoa(b.N%n)) 46 | for i := 0; i < b.N; i++ { 47 | v, ok := sm.Load(string(k)) 48 | if ok { 49 | _ = v.([]byte) 50 | } 51 | } 52 | }) 53 | } 54 | } 55 | 56 | type storeItem struct { 57 | k []byte 58 | v []byte 59 | } 60 | 61 | type binSearchStore struct { 62 | items []storeItem 63 | } 64 | 65 | func (s *binSearchStore) Len() int { return len(s.items) } 66 | func (s *binSearchStore) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } 67 | func (s *binSearchStore) Less(i, j int) bool { 68 | return bytes.Compare(s.items[i].k, s.items[j].k) < 0 69 | } 70 | 71 | func (s *binSearchStore) Get(k []byte) []byte { 72 | i := sort.Search(len(s.items), func(i int) bool { 73 | return bytes.Compare(s.items[i].k, k) >= 0 74 | }) 75 | if i == len(s.items) { 76 | return nil 77 | } 78 | item := s.items[i] 79 | if bytes.Equal(item.k, k) { 80 | return item.v 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func newDynamicMessagesStore(items []storeItem) *binSearchStore { 87 | store := &binSearchStore{items} 88 | sort.Sort(store) 89 | return store 90 | } 91 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/encoding/testproto/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "./;tesproto"; 4 | 5 | package testpackage; 6 | 7 | service TestService { 8 | rpc TestMethod0 (TestRequest0) returns (TestResponse0) {} 9 | rpc TestMethod1 (TestRequest1) returns (TestResponse1) {} 10 | rpc TestMethod2 (TestRequest2) returns (TestResponse2) {} 11 | rpc TestMethod3 (TestRequest3) returns (TestResponse3) {} 12 | rpc TestMethod4 (TestRequest4) returns (TestResponse4) {} 13 | rpc TestMethod5 (TestRequest5) returns (TestResponse5) {} 14 | rpc TestMethod6 (TestRequest6) returns (TestResponse6) {} 15 | rpc TestMethod7 (TestRequest7) returns (TestResponse7) {} 16 | rpc TestMethod8 (TestRequest8) returns (TestResponse8) {} 17 | rpc TestMethod9 (TestRequest9) returns (TestResponse9) {} 18 | } 19 | 20 | message TestRequest0 { string request = 1; } 21 | message TestRequest1 { string request = 1; } 22 | message TestRequest2 { string request = 1; } 23 | message TestRequest3 { string request = 1; } 24 | message TestRequest4 { string request = 1; } 25 | message TestRequest5 { string request = 1; } 26 | message TestRequest6 { string request = 1; } 27 | message TestRequest7 { string request = 1; } 28 | message TestRequest8 { string request = 1; } 29 | message TestRequest9 { string request = 1; } 30 | message TestResponse0 { string answer = 1; } 31 | message TestResponse1 { string answer = 1; } 32 | message TestResponse2 { string answer = 1; } 33 | message TestResponse3 { string answer = 1; } 34 | message TestResponse4 { string answer = 1; } 35 | message TestResponse5 { string answer = 1; } 36 | message TestResponse6 { string answer = 1; } 37 | message TestResponse7 { string answer = 1; } 38 | message TestResponse8 { string answer = 1; } 39 | message TestResponse9 { string answer = 1; } 40 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/format.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding" 7 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 8 | formatIO "github.com/ozontech/framer/formats/grpc/ozon/json/io" 9 | "github.com/ozontech/framer/formats/internal/pooledreader" 10 | "github.com/ozontech/framer/formats/model" 11 | ) 12 | 13 | func NewInput( 14 | r io.Reader, 15 | store reflection.DynamicMessagesStore, 16 | ) *model.InputFormat { 17 | return &model.InputFormat{ 18 | Reader: pooledreader.New(formatIO.NewReader(r)), 19 | Decoder: encoding.NewDecoder(store), 20 | } 21 | } 22 | 23 | func NewOutput( 24 | w io.Writer, 25 | store reflection.DynamicMessagesStore, 26 | ) *model.OutputFormat { 27 | return &model.OutputFormat{ 28 | Writer: formatIO.NewWriter(w), 29 | Encoder: encoding.NewEncoder(store), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/io/io.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type Reader struct { 9 | buf [4096]byte 10 | unprocessed []byte 11 | 12 | r io.Reader 13 | } 14 | 15 | func NewReader(r io.Reader) *Reader { 16 | return &Reader{r: r} 17 | } 18 | 19 | func (r *Reader) fillUnprocessed() error { 20 | n, err := r.r.Read(r.buf[:]) 21 | if err != nil { 22 | return err 23 | } 24 | r.unprocessed = r.buf[:n] 25 | return nil 26 | } 27 | 28 | // ReadNext использует p как буфер для чтения, при необходимости выделяя новую память. 29 | func (r *Reader) ReadNext(p []byte) ([]byte, error) { 30 | for { 31 | if len(r.unprocessed) == 0 { 32 | err := r.fillUnprocessed() 33 | if err == io.EOF && len(p) > 0 { 34 | return p, nil 35 | } 36 | if err != nil { 37 | return p, err 38 | } 39 | } 40 | 41 | index := bytes.IndexByte(r.unprocessed, '\n') 42 | if index != -1 { 43 | p = append(p, r.unprocessed[:index]...) 44 | r.unprocessed = r.unprocessed[index+1:] 45 | return p, nil 46 | } 47 | 48 | p = append(p, r.unprocessed...) 49 | r.unprocessed = nil 50 | } 51 | } 52 | 53 | type Writer struct { 54 | w io.Writer 55 | } 56 | 57 | func NewWriter(w io.Writer) *Writer { 58 | return &Writer{w: w} 59 | } 60 | 61 | func (w *Writer) WriteNext(p []byte) error { 62 | _, err := w.w.Write(append(p, '\n')) 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /formats/grpc/ozon/json/io/io_test.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type test struct { 14 | name string 15 | bytes []byte 16 | data [][]byte 17 | } 18 | 19 | //nolint:goconst 20 | func makeTests() []test { 21 | return []test{{ 22 | "simple", 23 | []byte( 24 | "line1\n" + 25 | "line2\n" + 26 | "line3\n" + 27 | "line4\n" + 28 | "line5\n" + 29 | "line6\n", 30 | ), 31 | [][]byte{ 32 | []byte("line1"), 33 | []byte("line2"), 34 | []byte("line3"), 35 | []byte("line4"), 36 | []byte("line5"), 37 | []byte("line6"), 38 | }, 39 | }} 40 | } 41 | 42 | func TestReader(t *testing.T) { 43 | t.Parallel() 44 | tests := append(makeTests(), test{ 45 | "without last \\n", 46 | []byte( 47 | "line1\n" + 48 | "line2\n" + 49 | "line3\n" + 50 | "line4\n" + 51 | "line5\n" + 52 | "line6", 53 | ), 54 | [][]byte{ 55 | []byte("line1"), 56 | []byte("line2"), 57 | []byte("line3"), 58 | []byte("line4"), 59 | []byte("line5"), 60 | []byte("line6"), 61 | }, 62 | }) 63 | for _, tc := range tests { 64 | tc := tc 65 | 66 | r := NewReader(bufio.NewReader(bytes.NewReader(tc.bytes))) 67 | t.Run(tc.name, func(t *testing.T) { 68 | t.Parallel() 69 | a := assert.New(t) 70 | var i int 71 | for { 72 | d, err := r.ReadNext(nil) 73 | if errors.Is(err, io.EOF) { 74 | break 75 | } 76 | a.NoError(err) 77 | var data string 78 | if i < len(tc.data) { 79 | data = string(tc.data[i]) 80 | } 81 | a.Equal(data, string(d)) 82 | i++ 83 | } 84 | a.Equal(len(tc.data), i) 85 | }) 86 | } 87 | } 88 | 89 | func TestWriter(t *testing.T) { 90 | t.Parallel() 91 | for _, tc := range makeTests() { 92 | tc := tc 93 | 94 | t.Run(tc.name, func(t *testing.T) { 95 | t.Parallel() 96 | a := assert.New(t) 97 | 98 | bb := new(bytes.Buffer) 99 | w := NewWriter(bb) 100 | for _, d := range tc.data { 101 | a.NoError(w.WriteNext(d)) 102 | } 103 | 104 | assert.Equal(t, string(tc.bytes), bb.String()) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /formats/grpc/pandora/json/format.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding" 7 | "github.com/ozontech/framer/formats/grpc/ozon/json/encoding/reflection" 8 | retuestIO "github.com/ozontech/framer/formats/grpc/ozon/json/io" 9 | "github.com/ozontech/framer/formats/internal/pooledreader" 10 | "github.com/ozontech/framer/formats/model" 11 | ) 12 | 13 | func NewInput(r io.Reader, store reflection.DynamicMessagesStore) *model.InputFormat { 14 | return &model.InputFormat{ 15 | Reader: pooledreader.New(retuestIO.NewReader(r)), 16 | Decoder: encoding.NewDecoder(store, encoding.WithSingleMetaValue()), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /formats/grpc/test_files/requests.ozon.binary: -------------------------------------------------------------------------------- 1 | 332 2 | /testpackage.TestService/TestMethod0 3 | /testpackage.TestService/TestMethod0 4 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 5 | 6 | ping 7 | 332 8 | /testpackage.TestService/TestMethod0 9 | /testpackage.TestService/TestMethod0 10 | {"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]} 11 | 12 | ping 13 | -------------------------------------------------------------------------------- /formats/grpc/test_files/requests.ozon.json: -------------------------------------------------------------------------------- 1 | {"tag":"/testpackage.TestService/TestMethod0","call":"testpackage.TestService.TestMethod0","metadata":{"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]},"payload":{"request":"ping"}} 2 | {"tag":"/testpackage.TestService/TestMethod0","call":"testpackage.TestService.TestMethod0","metadata":{"user-agent":["grpcurl/v1.8.6 grpc-go/1.44.1-dev"],"x-forwarded-for":["1.2.3.4"],"x-forwarded-host":["myservice:9090"],"x-forwarded-port":["9090"],"x-forwarded-proto":["http"],"x-forwarded-scheme":["http"],"x-real-ip":["1.2.3.4"],"x-scheme":["http"]},"payload":{"request":"ping"}} 3 | -------------------------------------------------------------------------------- /formats/grpc/test_files/requests.pandora.json: -------------------------------------------------------------------------------- 1 | {"tag":"/testpackage.TestService/TestMethod0","call":"testpackage.TestService.TestMethod0","metadata":{"user-agent":"grpcurl/v1.8.6 grpc-go/1.44.1-dev","x-forwarded-for":"1.2.3.4","x-forwarded-host":"myservice:9090","x-forwarded-port":"9090","x-forwarded-proto":"http","x-forwarded-scheme":"http","x-real-ip":"1.2.3.4","x-scheme":"http"},"payload":{"request":"ping"}} 2 | {"tag":"/testpackage.TestService/TestMethod0","call":"testpackage.TestService.TestMethod0","metadata":{"user-agent":"grpcurl/v1.8.6 grpc-go/1.44.1-dev","x-forwarded-for":"1.2.3.4","x-forwarded-host":"myservice:9090","x-forwarded-port":"9090","x-forwarded-proto":"http","x-forwarded-scheme":"http","x-real-ip":"1.2.3.4","x-scheme":"http"},"payload":{"request":"ping"}} 3 | -------------------------------------------------------------------------------- /formats/internal/json/escape_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEscapeString(t *testing.T) { 8 | t.Parallel() 9 | 10 | b := EscapeStringAppend(nil, []byte("foo")) 11 | if string(b) != `"foo"` { 12 | t.Fatalf("Expected: %v\nGot: %v", `"foo"`, b) 13 | } 14 | 15 | b = EscapeStringAppend(nil, []byte(`f"oo`)) 16 | if string(b) != `"f\"oo"` { 17 | t.Fatalf("Expected: %v\nGot: %v", `"f\"oo"`, b) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /formats/internal/kv/encoding.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "github.com/ozontech/framer/formats/model" 5 | ) 6 | 7 | type Marshaler interface { 8 | MarshalAppend(b []byte, meta []model.Meta) []byte 9 | } 10 | 11 | type Unmarshaler interface { 12 | UnmarshalAppend(buf []model.Meta, bytes []byte) ([]model.Meta, error) 13 | } 14 | -------------------------------------------------------------------------------- /formats/internal/kv/json/jsonkv.go: -------------------------------------------------------------------------------- 1 | package jsonkv 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | 7 | "github.com/mailru/easyjson/jlexer" 8 | "github.com/ozontech/framer/formats/internal/json" 9 | "github.com/ozontech/framer/formats/model" 10 | ) 11 | 12 | type metaSorter []model.Meta 13 | 14 | func (m metaSorter) Len() int { return len(m) } 15 | func (m metaSorter) Less(i, j int) bool { return bytes.Compare(m[i].Name, m[j].Name) < 0 } 16 | func (m metaSorter) Swap(i, j int) { 17 | m[i], m[j] = m[j], m[i] 18 | } 19 | 20 | type MultiVal struct{} 21 | 22 | func NewMultiVal() MultiVal { 23 | return MultiVal{} 24 | } 25 | 26 | func (MultiVal) MarshalAppend(b []byte, meta []model.Meta) []byte { 27 | if len(meta) == 0 { 28 | return append(b, "{}"...) 29 | } 30 | 31 | sort.Sort(metaSorter(meta)) 32 | 33 | b = append(b, '{') 34 | if len(meta) > 0 { 35 | m := meta[0] 36 | b = json.EscapeStringAppend(b, m.Name) 37 | b = append(b, ":["...) 38 | b = json.EscapeStringAppend(b, m.Value) 39 | } 40 | for i := 1; i < len(meta); i++ { 41 | m := meta[i] 42 | isNew := !bytes.Equal(m.Name, meta[i-1].Name) 43 | if !isNew { 44 | b = append(b, ',') 45 | b = json.EscapeStringAppend(b, m.Value) 46 | continue 47 | } 48 | 49 | b = append(b, "],"...) 50 | b = json.EscapeStringAppend(b, m.Name) 51 | b = append(b, ":["...) 52 | b = json.EscapeStringAppend(b, m.Value) 53 | } 54 | return append(b, "]}"...) 55 | } 56 | 57 | func (MultiVal) UnmarshalAppend(buf []model.Meta, bytes []byte) ([]model.Meta, error) { 58 | in := jlexer.Lexer{Data: bytes} 59 | 60 | in.Delim('{') 61 | for !in.IsDelim('}') { 62 | key := in.UnsafeBytes() 63 | 64 | in.WantColon() 65 | 66 | in.Delim('[') 67 | for !in.IsDelim(']') { 68 | buf = append(buf, model.Meta{Name: key, Value: in.UnsafeBytes()}) 69 | in.WantComma() 70 | } 71 | in.Delim(']') 72 | 73 | in.WantComma() 74 | } 75 | in.Delim('}') 76 | in.Consumed() 77 | 78 | return buf, in.Error() 79 | } 80 | 81 | type SingleVal struct { 82 | opts *options 83 | } 84 | 85 | func NewSingleVal(optFns ...Option) SingleVal { 86 | return SingleVal{applyOptions(optFns...)} 87 | } 88 | 89 | func (SingleVal) MarshalAppend(b []byte, meta []model.Meta) []byte { 90 | b = append(b, '{') 91 | i := 0 92 | for ; i < len(meta)-1; i++ { 93 | k, v := meta[i].Name, meta[i].Value 94 | b = json.EscapeStringAppend(b, k) 95 | b = append(b, ':') 96 | b = json.EscapeStringAppend(b, v) 97 | b = append(b, ',') 98 | } 99 | for ; i < len(meta); i += 2 { 100 | k, v := meta[i].Name, meta[i].Value 101 | b = json.EscapeStringAppend(b, k) 102 | b = append(b, ':') 103 | b = json.EscapeStringAppend(b, v) 104 | } 105 | return append(b, '}') 106 | } 107 | 108 | func (SingleVal) UnmarshalAppend(buf []model.Meta, bytes []byte) ([]model.Meta, error) { 109 | in := jlexer.Lexer{Data: bytes} 110 | 111 | in.Delim('{') 112 | for !in.IsDelim('}') { 113 | k := in.UnsafeBytes() 114 | in.WantColon() 115 | v := in.UnsafeBytes() 116 | 117 | buf = append(buf, model.Meta{Name: k, Value: v}) 118 | 119 | in.WantComma() 120 | } 121 | in.Delim('}') 122 | in.Consumed() 123 | 124 | return buf, in.Error() 125 | } 126 | 127 | type options struct { 128 | filter func(k []byte) (allowed bool) 129 | } 130 | 131 | func applyOptions(optFns ...Option) *options { 132 | opts := &options{ 133 | filter: func([]byte) (allowed bool) { return true }, 134 | } 135 | 136 | for _, o := range optFns { 137 | o(opts) 138 | } 139 | 140 | return opts 141 | } 142 | 143 | type Option func(*options) 144 | 145 | func WithFilter(filter func(k []byte) (allowed bool)) Option { 146 | return func(o *options) { o.filter = filter } 147 | } 148 | -------------------------------------------------------------------------------- /formats/internal/kv/json/jsonkv_multi_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package jsonkv 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/mailru/easyjson/jlexer" 8 | "github.com/ozontech/framer/formats/model" 9 | ) 10 | 11 | // BenchmarkMultiValEncoding-8 6392497 180.7 ns/op 24 B/op 1 allocs/op 12 | // BenchmarkLegacyMultiValEncoding-8 908154 1183 ns/op 1112 B/op 19 allocs/op 13 | // BenchmarkMultiValJsonDecoding-8 4344513 277.3 ns/op 0 B/op 0 allocs/op 14 | // BenchmarkLegacyMultiValDecoding-8 1857330 641.0 ns/op 560 B/op 14 allocs/op 15 | 16 | func BenchmarkMultiValEncoding(b *testing.B) { 17 | meta := []model.Meta{ 18 | {Name: []byte("header1"), Value: []byte("value1")}, 19 | {Name: []byte("header2"), Value: []byte("value2")}, 20 | {Name: []byte("header3"), Value: []byte("value3")}, 21 | {Name: []byte("header4"), Value: []byte("value4")}, 22 | } 23 | 24 | encoder := NewMultiVal() 25 | buf := make([]byte, 0, 1024) 26 | for i := 0; i < b.N; i++ { 27 | buf = encoder.MarshalAppend(buf[:0], meta) 28 | } 29 | } 30 | 31 | func BenchmarkLegacyMultiValEncoding(b *testing.B) { 32 | s := []string{ 33 | "header1", "value1", 34 | "header2", "value2", 35 | "header3", "value3", 36 | "header4", "value4", 37 | } 38 | 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | m := make(map[string][]string, len(s)/2) 42 | for i := 0; i < len(s); i += 2 { 43 | m[s[i]] = append(m[s[i]], s[i+1]) 44 | } 45 | _, err := json.Marshal(m) 46 | if err != nil { 47 | b.Fatal(err) 48 | } 49 | } 50 | } 51 | 52 | func BenchmarkMultiValJsonDecoding(b *testing.B) { 53 | in := []byte(`{ "header1": ["value1"], "header2": ["value2"], "header3": ["value3"], "header4": ["value4"], "header5": ["value5"] }`) 54 | decoder := MultiVal{} 55 | buf := make([]model.Meta, 10) 56 | 57 | b.ResetTimer() 58 | for i := 0; i < b.N; i++ { 59 | var err error 60 | buf, err = decoder.UnmarshalAppend(buf[:0], in) 61 | if err != nil { 62 | b.Fatal(err) 63 | } 64 | } 65 | } 66 | 67 | func BenchmarkLegacyMultiValDecoding(b *testing.B) { 68 | in := []byte(`{ "header1": ["value1"], "header2": ["value2"], "header3": ["value3"], "header4": ["value4"], "header5": ["value5"] }`) 69 | 70 | b.ResetTimer() 71 | for i := 0; i < b.N; i++ { 72 | _, err := unmarshalJSONMeta(in) 73 | if err != nil { 74 | b.Fatal(err) 75 | } 76 | } 77 | } 78 | 79 | func unmarshalJSONMeta(b []byte) ([]string, error) { 80 | r := []string{} 81 | 82 | in := jlexer.Lexer{Data: b} 83 | 84 | in.Delim('{') 85 | for !in.IsDelim('}') { 86 | // key := in.String() 87 | key := in.String() 88 | 89 | in.WantColon() 90 | 91 | in.Delim('[') 92 | for !in.IsDelim(']') { 93 | r = append(r, key, in.String()) 94 | in.WantComma() 95 | } 96 | in.Delim(']') 97 | 98 | in.WantComma() 99 | } 100 | in.Delim('}') 101 | in.Consumed() 102 | 103 | return r, in.Error() 104 | } 105 | -------------------------------------------------------------------------------- /formats/internal/kv/json/jsonkv_multi_test.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl 2 | package jsonkv 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/ozontech/framer/formats/model" 10 | ) 11 | 12 | var multiValTests = []struct { 13 | name string 14 | bytes []byte 15 | data []model.Meta 16 | }{ 17 | { 18 | "simple", 19 | []byte(`{"key":["val"]}`), 20 | []model.Meta{ 21 | {Name: []byte("key"), Value: []byte("val")}, 22 | }, 23 | }, 24 | { 25 | "json encoded strings", 26 | []byte(`{"key\n":["val\n"]}`), 27 | []model.Meta{ 28 | {Name: []byte("key\n"), Value: []byte("val\n")}, 29 | }, 30 | }, 31 | { 32 | "duplicate vals", 33 | []byte(`{"key":["val","val"]}`), 34 | []model.Meta{ 35 | {Name: []byte("key"), Value: []byte("val")}, 36 | {Name: []byte("key"), Value: []byte("val")}, 37 | }, 38 | }, 39 | } 40 | 41 | func TestMultiValDecoder(t *testing.T) { 42 | t.Parallel() 43 | d := NewMultiVal() 44 | 45 | for _, tc := range multiValTests { 46 | tc := tc 47 | 48 | t.Run(tc.name, func(t *testing.T) { 49 | t.Parallel() 50 | out, err := d.UnmarshalAppend(nil, tc.bytes) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | assert.Equal(t, tc.data, out) 55 | }) 56 | 57 | t.Run("append/"+tc.name, func(t *testing.T) { 58 | t.Parallel() 59 | m := model.Meta{Name: []byte("exist key"), Value: []byte("exist val")} 60 | buf := []model.Meta{m} 61 | out, err := d.UnmarshalAppend(buf, tc.bytes) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | assert.Equal(t, append( 66 | []model.Meta{{Name: []byte("exist key"), Value: []byte("exist val")}}, 67 | tc.data..., 68 | ), out) 69 | }) 70 | } 71 | 72 | t.Run("error", func(t *testing.T) { 73 | t.Parallel() 74 | _, err := d.UnmarshalAppend(nil, []byte(`some garbage{///]`)) 75 | assert.Error(t, err) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /formats/internal/kv/json/jsonkv_single_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package jsonkv 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/ozontech/framer/formats/model" 8 | ) 9 | 10 | // BenchmarkSingleValEncoding-8 9681330 126.0 ns/op 0 B/op 0 allocs/op 11 | // BenchmarkLegacySingleValEncoding-8 1222810 1631 ns/op 936 B/op 15 allocs/op 12 | // BenchmarkSingleValJsonKVDecoding-8 3303810 364.9 ns/op 0 B/op 0 allocs/op 13 | // BenchmarkLegacySingleValDecoding-8 357715 3251 ns/op 792 B/op 26 allocs/op 14 | 15 | func BenchmarkSingleValEncoding(b *testing.B) { 16 | headers := []model.Meta{ 17 | {Name: []byte("header1"), Value: []byte("value1")}, 18 | {Name: []byte("header2"), Value: []byte("value2")}, 19 | {Name: []byte("header3"), Value: []byte("value3")}, 20 | {Name: []byte("header4"), Value: []byte("value4")}, 21 | } 22 | 23 | e := NewSingleVal() 24 | bb := make([]byte, 0, 1024) 25 | 26 | b.ResetTimer() 27 | for i := 0; i < b.N; i++ { 28 | bb = e.MarshalAppend(bb, headers) 29 | } 30 | } 31 | 32 | func BenchmarkLegacySingleValEncoding(b *testing.B) { 33 | s := []string{ 34 | "header1", "value1", 35 | "header2", "value2", 36 | "header3", "value3", 37 | "header4", "value4", 38 | } 39 | 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | m := make(map[string]string, len(s)/2) 43 | for i := 0; i < len(s); i += 2 { 44 | m[s[i]] = s[i+1] 45 | } 46 | _, err := json.Marshal(m) 47 | if err != nil { 48 | b.Fatal(err) 49 | } 50 | } 51 | } 52 | 53 | func BenchmarkSingleValJsonKVDecoding(b *testing.B) { 54 | in := []byte(`{ "header1": "value1", "header2": "value2", "header3": "value3", "header4": "value4", "header5": "value5" }`) 55 | d := SingleVal{} 56 | buf := make([]model.Meta, 10) 57 | 58 | b.ResetTimer() 59 | for i := 0; i < b.N; i++ { 60 | var err error 61 | buf, err = d.UnmarshalAppend(buf[:0], in) 62 | if err != nil { 63 | b.Fatal(err) 64 | } 65 | } 66 | } 67 | 68 | func BenchmarkLegacySingleValDecoding(b *testing.B) { 69 | in := []byte(`{ "header1": "value1", "header2": "value2", "header3": "value3", "header4": "value4", "header5": "value5" }`) 70 | 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | headers := make(map[string]string) 74 | err := json.Unmarshal(in, &headers) 75 | if err != nil { 76 | b.Fatal(err) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /formats/internal/kv/json/jsonkv_single_test.go: -------------------------------------------------------------------------------- 1 | //nolint:dupl 2 | package jsonkv 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/ozontech/framer/formats/model" 10 | ) 11 | 12 | var singleValTests = []struct { 13 | name string 14 | bytes []byte 15 | data []model.Meta 16 | }{ 17 | { 18 | "simple", 19 | []byte(`{"key":"val"}`), 20 | []model.Meta{ 21 | {Name: []byte("key"), Value: []byte("val")}, 22 | }, 23 | }, 24 | { 25 | "json encoded strings", 26 | []byte(`{"key\n":"val\n"}`), 27 | []model.Meta{ 28 | {Name: []byte("key\n"), Value: []byte("val\n")}, 29 | }, 30 | }, 31 | { 32 | // expected behaviour 33 | "duplicate keys", 34 | []byte(`{"key":"val","key":"val"}`), 35 | []model.Meta{ 36 | {Name: []byte("key"), Value: []byte("val")}, 37 | {Name: []byte("key"), Value: []byte("val")}, 38 | }, 39 | }, 40 | } 41 | 42 | func TestSingleValDecoder(t *testing.T) { 43 | t.Parallel() 44 | d := NewSingleVal() 45 | 46 | for _, tc := range singleValTests { 47 | tc := tc 48 | 49 | t.Run(tc.name, func(t *testing.T) { 50 | t.Parallel() 51 | out, err := d.UnmarshalAppend(nil, tc.bytes) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | assert.Equal(t, tc.data, out) 56 | }) 57 | 58 | t.Run("append/"+tc.name, func(t *testing.T) { 59 | t.Parallel() 60 | buf := []model.Meta{ 61 | {Name: []byte("exist key"), Value: []byte("exist val")}, 62 | } 63 | out, err := d.UnmarshalAppend(buf, tc.bytes) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | assert.Equal(t, append([]model.Meta{ 68 | {Name: []byte("exist key"), Value: []byte("exist val")}, 69 | }, tc.data...), out) 70 | }) 71 | } 72 | 73 | t.Run("error", func(t *testing.T) { 74 | t.Parallel() 75 | _, err := d.UnmarshalAppend(nil, []byte(`some garbage{///]`)) 76 | assert.Error(t, err) 77 | }) 78 | } 79 | 80 | func TestSingleValEncoder(t *testing.T) { 81 | t.Parallel() 82 | d := NewSingleVal() 83 | for _, tc := range singleValTests { 84 | tc := tc 85 | 86 | t.Run(tc.name, func(t *testing.T) { 87 | t.Parallel() 88 | b := d.MarshalAppend(nil, tc.data) 89 | assert.Equal(t, string(tc.bytes), string(b)) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /formats/internal/pooledreader/pooledreader.go: -------------------------------------------------------------------------------- 1 | package pooledreader 2 | 3 | import ( 4 | "github.com/ozontech/framer/formats/model" 5 | "github.com/ozontech/framer/utils/pool" 6 | ) 7 | 8 | type PooledReader struct { 9 | pool *pool.SlicePool[[]byte] 10 | r model.RequestReader 11 | } 12 | 13 | func New(r model.RequestReader) *PooledReader { 14 | return &PooledReader{pool.NewSlicePool[[]byte](), r} 15 | } 16 | 17 | func (r *PooledReader) ReadNext() ([]byte, error) { 18 | b, _ := r.pool.Acquire() 19 | return r.r.ReadNext(b[:0]) 20 | } 21 | 22 | func (r *PooledReader) Release(b []byte) { 23 | r.pool.Release(b) 24 | } 25 | -------------------------------------------------------------------------------- /formats/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Meta struct { 4 | Name []byte 5 | Value []byte 6 | } 7 | 8 | type Data struct { 9 | Tag []byte 10 | Method []byte // "/" {service name} "/" {method name} 11 | Metadata []Meta 12 | Message []byte 13 | } 14 | 15 | func (d *Data) Reset() { 16 | d.Tag = d.Tag[:0] 17 | d.Method = d.Method[:0] 18 | d.Metadata = d.Metadata[:0] 19 | d.Message = d.Message[:0] 20 | } 21 | 22 | type Marshaler interface { 23 | MarshalAppend([]byte, *Data) ([]byte, error) 24 | } 25 | 26 | type Unmarshaler interface { 27 | Unmarshal(d *Data, b []byte) error 28 | } 29 | 30 | type RequestReader interface { 31 | ReadNext([]byte) ([]byte, error) 32 | } 33 | 34 | type PooledRequestReader interface { 35 | ReadNext() ([]byte, error) 36 | Release([]byte) 37 | } 38 | 39 | type RequestWriter interface { 40 | WriteNext([]byte) error 41 | } 42 | 43 | type InputFormat struct { 44 | Reader PooledRequestReader 45 | Decoder Unmarshaler 46 | } 47 | 48 | type OutputFormat struct { 49 | Writer RequestWriter 50 | Encoder Marshaler 51 | } 52 | -------------------------------------------------------------------------------- /frameheader/frameheader.go: -------------------------------------------------------------------------------- 1 | package frameheader 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strconv" 7 | 8 | "golang.org/x/net/http2" 9 | ) 10 | 11 | type FrameHeader []byte 12 | 13 | func NewFrameHeader() FrameHeader { return make([]byte, 9) } 14 | 15 | func (f FrameHeader) Fill( 16 | length int, 17 | t http2.FrameType, 18 | flags http2.Flags, 19 | streamID uint32, 20 | ) { 21 | _ = f[8] 22 | f[0] = byte(length >> 16) 23 | f[1] = byte(length >> 8) 24 | f[2] = byte(length) 25 | f[3] = byte(t) 26 | f[4] = byte(flags) 27 | f[5] = byte(streamID >> 24) 28 | f[6] = byte(streamID >> 16) 29 | f[7] = byte(streamID >> 8) 30 | f[8] = byte(streamID) 31 | } 32 | 33 | func (f FrameHeader) Length() int { 34 | _ = f[2] 35 | return (int(f[0])<<16 | int(f[1])<<8 | int(f[2])) 36 | } 37 | 38 | func (f FrameHeader) SetLength(l int) { 39 | _ = f[2] 40 | f[0] = byte(l >> 16) 41 | f[1] = byte(l >> 8) 42 | f[2] = byte(l) 43 | } 44 | 45 | func (f FrameHeader) Type() http2.FrameType { return http2.FrameType(f[3]) } 46 | func (f FrameHeader) SetType(t http2.FrameType) { f[3] = byte(t) } 47 | 48 | func (f FrameHeader) Flags() http2.Flags { return http2.Flags(f[4]) } 49 | func (f FrameHeader) SetFlags(flag http2.Flags) { f[4] = byte(flag) } 50 | 51 | func (f FrameHeader) StreamID() uint32 { return binary.BigEndian.Uint32(f[5:]) } 52 | func (f FrameHeader) SetStreamID(streamID uint32) { 53 | _ = f[8] 54 | f[5] = byte(streamID >> 24) 55 | f[6] = byte(streamID >> 16) 56 | f[7] = byte(streamID >> 8) 57 | f[8] = byte(streamID) 58 | } 59 | 60 | func (f FrameHeader) String() string { 61 | streamID := f.StreamID() 62 | return f.Type().String() + 63 | "/ length=" + strconv.FormatUint(uint64(f.Length()), 10) + 64 | "/ streamID = " + strconv.FormatUint(uint64(streamID), 10) + 65 | "/ flags = " + fmt.Sprintf("%o", f.Flags()) 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ozontech/framer 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/alecthomas/kong v0.9.0 7 | github.com/alecthomas/mango-kong v0.1.0 8 | github.com/dustin/go-humanize v1.0.1 9 | github.com/jhump/protoreflect v1.12.0 10 | github.com/mailru/easyjson v0.7.7 11 | github.com/stretchr/testify v1.9.0 12 | go.uber.org/multierr v1.8.0 13 | go.uber.org/zap v1.24.0 14 | golang.org/x/net v0.27.0 15 | golang.org/x/sync v0.7.0 16 | google.golang.org/grpc v1.52.0 17 | google.golang.org/protobuf v1.34.2 18 | ) 19 | 20 | require ( 21 | github.com/benbjohnson/clock v1.1.0 // indirect 22 | github.com/bufbuild/protocompile v0.4.0 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/josharian/intern v1.0.0 // indirect 26 | github.com/muesli/mango v0.1.1-0.20220205060214-77e2058169ab // indirect 27 | github.com/muesli/roff v0.1.0 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | go.uber.org/atomic v1.7.0 // indirect 30 | golang.org/x/sys v0.22.0 // indirect 31 | golang.org/x/text v0.16.0 // indirect 32 | google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /loader/e2e_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "fmt" 8 | "math" 9 | "net" 10 | "os" 11 | "testing" 12 | 13 | "github.com/ozontech/framer/consts" 14 | "github.com/ozontech/framer/datasource" 15 | "github.com/ozontech/framer/loader/types" 16 | "github.com/stretchr/testify/assert" 17 | "go.uber.org/zap" 18 | "go.uber.org/zap/zaptest" 19 | "golang.org/x/net/http2" 20 | "golang.org/x/net/http2/hpack" 21 | "golang.org/x/sync/errgroup" 22 | "google.golang.org/protobuf/encoding/protowire" 23 | ) 24 | 25 | func TestE2E(t *testing.T) { 26 | t.Parallel() 27 | log := zaptest.NewLogger(t) 28 | a := assert.New(t) 29 | clientConn, serverConn := net.Pipe() 30 | l := newLoader( 31 | clientConn, noopReporter{}, 32 | loaderConfig{timeout: consts.DefaultTimeout}, log, 33 | ) 34 | 35 | requestsFile, err := os.Open("../test_files/requests") 36 | if err != nil { 37 | a.NoError(err) 38 | } 39 | dataSource := datasource.NewFileDataSource(datasource.NewCyclicReader(requestsFile)) 40 | 41 | const reqCount = 10_000 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | defer cancel() 44 | 45 | g, ctx := errgroup.WithContext(ctx) 46 | g.Go(func() error { return l.Run(ctx) }) 47 | g.Go(func() (err error) { 48 | defer func() { log.Info("scheduling done", zap.Error(err)) }() 49 | for i := 0; i < reqCount; i++ { 50 | req, err := dataSource.Fetch() 51 | if err != nil { 52 | return err 53 | } 54 | l.DoRequest(req) 55 | } 56 | l.WaitResponses(ctx) 57 | return nil 58 | }) 59 | 60 | framer := http2.NewFramer(serverConn, serverConn) 61 | framer.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {}) 62 | framer.SetMaxReadFrameSize(256) 63 | respChan := make(chan uint32, 128) 64 | 65 | g.Go(func() (err error) { 66 | defer func() { log.Info("sending done", zap.Error(err)) }() 67 | defer cancel() 68 | 69 | headerBuf := bytes.NewBuffer(nil) 70 | enc := hpack.NewEncoder(headerBuf) 71 | data := make([]byte, 5) 72 | 73 | for streamID := range respChan { 74 | headerBuf.Reset() 75 | a.NoError(enc.WriteField(hpack.HeaderField{Name: ":status", Value: "200"})) 76 | a.NoError(enc.WriteField(hpack.HeaderField{Name: "content-type", Value: "application/grpc"})) 77 | err := framer.WriteHeaders(http2.HeadersFrameParam{ 78 | StreamID: streamID, 79 | BlockFragment: headerBuf.Bytes(), 80 | EndHeaders: true, 81 | }) 82 | if err != nil { 83 | return fmt.Errorf("writing headers: %w", err) 84 | } 85 | 86 | err = framer.WriteData(streamID, false, data) 87 | if err != nil { 88 | return fmt.Errorf("writing data: %w", err) 89 | } 90 | 91 | headerBuf.Reset() 92 | a.NoError(enc.WriteField(hpack.HeaderField{Name: "grpc-status", Value: "0"})) 93 | err = framer.WriteHeaders(http2.HeadersFrameParam{ 94 | StreamID: streamID, 95 | BlockFragment: headerBuf.Bytes(), 96 | EndHeaders: true, 97 | EndStream: true, 98 | }) 99 | if err != nil { 100 | return fmt.Errorf("writing headers: %w", err) 101 | } 102 | } 103 | return nil 104 | }) 105 | g.Go(func() (err error) { 106 | defer func() { log.Info("recieving done", zap.Error(err)) }() 107 | defer close(respChan) 108 | 109 | expectedHeaders := []hpack.HeaderField{ 110 | {Name: ":path", Value: "/test.api.TestApi/Test"}, 111 | {Name: ":method", Value: "POST"}, 112 | {Name: ":scheme", Value: "http"}, 113 | {Name: "content-type", Value: "application/grpc"}, 114 | {Name: "te", Value: "trailers"}, 115 | {Name: "x-my-header-key1", Value: "my-header-val1"}, 116 | {Name: "x-my-header-key2", Value: "my-header-val2"}, 117 | } 118 | 119 | b := make([]byte, 5) 120 | b = protowire.AppendTag(b, 1, protowire.BytesType) 121 | b = protowire.AppendString(b, "ping") 122 | binary.BigEndian.PutUint32(b[1:5], uint32(len(b)-5)) 123 | 124 | err = framer.WriteWindowUpdate(0, math.MaxUint32&0x7fffffff) 125 | if err != nil { 126 | return fmt.Errorf("write window update frame: %w", err) 127 | } 128 | 129 | for i := 0; i < reqCount; i++ { 130 | var frame http2.Frame 131 | for i := 0; i < 2; i++ { 132 | frame, err = framer.ReadFrame() 133 | if err != nil { 134 | return fmt.Errorf("read frame: %w", err) 135 | } 136 | if frame.Header().Type == http2.FrameWindowUpdate { 137 | continue 138 | } 139 | break 140 | } 141 | 142 | headersFrame := frame.(*http2.MetaHeadersFrame) 143 | a.Equal(expectedHeaders, headersFrame.Fields) 144 | 145 | for i := 0; i < 2; i++ { 146 | frame, err = framer.ReadFrame() 147 | if err != nil { 148 | return fmt.Errorf("read frame: %w", err) 149 | } 150 | if frame.Header().Type == http2.FrameWindowUpdate { 151 | continue 152 | } 153 | break 154 | } 155 | dataFrame := frame.(*http2.DataFrame) 156 | a.Equal(b, dataFrame.Data()) 157 | respChan <- dataFrame.StreamID 158 | } 159 | 160 | return nil 161 | }) 162 | a.NoError(g.Wait()) 163 | } 164 | 165 | type noopReporter struct{} 166 | 167 | func (a noopReporter) Acquire(string, uint32) types.StreamState { 168 | return streamState{} 169 | } 170 | 171 | type streamState struct{} 172 | 173 | func (s streamState) FirstByteSent() {} 174 | func (s streamState) LastByteSent() {} 175 | func (s streamState) RequestError(error) {} 176 | func (s streamState) SetSize(int) {} 177 | func (s streamState) Reset(string) {} 178 | func (s streamState) OnHeader(string, string) {} 179 | func (s streamState) IoError(error) {} 180 | func (s streamState) RSTStream(http2.ErrCode) {} 181 | func (s streamState) GoAway(http2.ErrCode, []byte) {} 182 | func (s streamState) Timeout() {} 183 | func (s streamState) End() {} 184 | -------------------------------------------------------------------------------- /loader/flowcontrol/flowcontrol.go: -------------------------------------------------------------------------------- 1 | package flowcontrol 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type FlowControl struct { 8 | n uint32 9 | cond *sync.Cond 10 | ok bool 11 | } 12 | 13 | func NewFlowControl(n uint32) *FlowControl { 14 | fc := FlowControl{ 15 | n: n, 16 | cond: sync.NewCond(&sync.Mutex{}), 17 | ok: true, 18 | } 19 | return &fc 20 | } 21 | 22 | func (fc *FlowControl) Wait(n uint32) bool { 23 | if n == 0 { 24 | return true 25 | } 26 | cond := fc.cond 27 | 28 | cond.L.Lock() 29 | defer cond.L.Unlock() 30 | 31 | for n > fc.n && fc.ok { 32 | cond.Wait() 33 | } 34 | fc.n -= n 35 | return fc.ok 36 | } 37 | 38 | func (fc *FlowControl) Add(n uint32) { 39 | fc.cond.L.Lock() 40 | defer fc.cond.L.Unlock() 41 | 42 | fc.n += n 43 | fc.cond.Broadcast() // оповещаем все горутины, заблокированный в ожидании flowControl проверить лимиты 44 | } 45 | 46 | func (fc *FlowControl) Reset(n uint32) { 47 | // тут лок нужен чтобы избежать ситуации гонок, когда ошибку уже установили, 48 | // но Wait еще не вернул результат 49 | fc.cond.L.Lock() 50 | defer fc.cond.L.Unlock() 51 | 52 | fc.n = n 53 | fc.ok = true 54 | } 55 | 56 | func (fc *FlowControl) Disable() { 57 | fc.cond.L.Lock() 58 | defer fc.cond.L.Unlock() 59 | 60 | fc.ok = false 61 | fc.cond.Broadcast() 62 | } 63 | -------------------------------------------------------------------------------- /loader/reciever/framer.go: -------------------------------------------------------------------------------- 1 | package reciever 2 | 3 | import "github.com/ozontech/framer/frameheader" 4 | 5 | type Framer struct { 6 | currentHeader frameheader.FrameHeader 7 | header frameheader.FrameHeader 8 | payloadLeft int 9 | buf []byte 10 | } 11 | 12 | type Status int 13 | 14 | const ( 15 | StatusFrameDone Status = iota 16 | StatusFrameDoneBufEmpty 17 | StatusHeaderIncomplete 18 | StatusPayloadIncomplete 19 | ) 20 | 21 | func (p *Framer) Header() frameheader.FrameHeader { 22 | return p.header 23 | } 24 | 25 | func (p *Framer) Next() ([]byte, Status) { 26 | currentHeaderLen := len(p.currentHeader) 27 | if currentHeaderLen != 9 { 28 | bufLen := len(p.buf) 29 | needToFill := 9 - currentHeaderLen 30 | if bufLen < needToFill { 31 | p.currentHeader = append(p.currentHeader, p.buf...) 32 | return nil, StatusHeaderIncomplete 33 | } 34 | 35 | p.currentHeader = append(p.currentHeader, p.buf[:needToFill]...) 36 | p.buf = p.buf[needToFill:] 37 | p.payloadLeft = p.currentHeader.Length() 38 | } 39 | p.header = p.currentHeader 40 | 41 | bufLen := len(p.buf) 42 | if bufLen > p.payloadLeft { 43 | payload := p.buf[:p.payloadLeft] 44 | p.buf = p.buf[p.payloadLeft:] 45 | p.currentHeader = p.currentHeader[:0] 46 | return payload, StatusFrameDone 47 | } 48 | 49 | if bufLen == p.payloadLeft { 50 | p.currentHeader = p.currentHeader[:0] 51 | return p.buf, StatusFrameDoneBufEmpty 52 | } 53 | 54 | p.payloadLeft -= len(p.buf) 55 | return p.buf, StatusPayloadIncomplete 56 | } 57 | 58 | func (p *Framer) Fill(b []byte) { 59 | p.buf = b 60 | } 61 | -------------------------------------------------------------------------------- /loader/reciever/framer_test.go: -------------------------------------------------------------------------------- 1 | package reciever_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/net/http2" 10 | 11 | "github.com/ozontech/framer/loader/reciever" 12 | ) 13 | 14 | func TestFramer(t *testing.T) { 15 | t.Parallel() 16 | a := assert.New(t) 17 | f := new(reciever.Framer) 18 | 19 | buf := bytes.NewBuffer(nil) 20 | framer := http2.NewFramer(buf, nil) 21 | 22 | payload1 := make([]byte, 512) 23 | _, err := rand.Read(payload1) 24 | a.NoError(err) 25 | a.NoError(framer.WriteData(123, false, payload1)) 26 | firstFrameLen := buf.Len() 27 | 28 | payload2 := make([]byte, 512) 29 | _, err = rand.Read(payload2) 30 | a.NoError(err) 31 | a.NoError(framer.WriteData(321, true, payload2)) 32 | 33 | p := buf.Bytes() 34 | f.Fill(p[:1]) 35 | b, status := f.Next() 36 | a.Nil(b) 37 | a.Equal(reciever.StatusHeaderIncomplete, status) 38 | 39 | f.Fill(p[1:9]) 40 | b, status = f.Next() 41 | a.Empty(b) 42 | a.Equal(reciever.StatusPayloadIncomplete, status) 43 | 44 | header := f.Header() 45 | a.Equal(512, header.Length()) 46 | a.Equal(http2.FrameData, header.Type()) 47 | a.Equal(http2.Flags(0), header.Flags()) 48 | a.Equal(uint32(123), header.StreamID()) 49 | 50 | f.Fill(p[9:11]) 51 | b, status = f.Next() 52 | a.Equal(b, p[9:11]) 53 | a.Equal(reciever.StatusPayloadIncomplete, status) 54 | 55 | f.Fill(p[11 : firstFrameLen+15]) 56 | b, status = f.Next() 57 | a.Equal(b, p[11:firstFrameLen]) 58 | a.Equal(reciever.StatusFrameDone, status) 59 | 60 | b, status = f.Next() 61 | a.Equal(b, p[firstFrameLen+9:firstFrameLen+15]) 62 | a.Equal(reciever.StatusPayloadIncomplete, status) 63 | 64 | f.Fill(p[firstFrameLen+15:]) 65 | b, status = f.Next() 66 | a.Equal(b, p[firstFrameLen+15:]) 67 | a.Equal(reciever.StatusFrameDoneBufEmpty, status) 68 | } 69 | -------------------------------------------------------------------------------- /loader/reciever/mock_generate_test.go: -------------------------------------------------------------------------------- 1 | //go:generate moq -pkg reciever -out ./mock_loader_types_test.go ../types StreamStore StreamsLimiter Stream FlowControl 2 | //go:generate moq -pkg reciever -out ./mock_reciever_test.go . FrameTypeProcessor 3 | package reciever 4 | -------------------------------------------------------------------------------- /loader/reciever/mock_reciever_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by moq; DO NOT EDIT. 2 | // github.com/matryer/moq 3 | 4 | package reciever 5 | 6 | import ( 7 | "github.com/ozontech/framer/frameheader" 8 | "sync" 9 | ) 10 | 11 | // Ensure, that FrameTypeProcessorMock does implement FrameTypeProcessor. 12 | // If this is not the case, regenerate this file with moq. 13 | var _ FrameTypeProcessor = &FrameTypeProcessorMock{} 14 | 15 | // FrameTypeProcessorMock is a mock implementation of FrameTypeProcessor. 16 | // 17 | // func TestSomethingThatUsesFrameTypeProcessor(t *testing.T) { 18 | // 19 | // // make and configure a mocked FrameTypeProcessor 20 | // mockedFrameTypeProcessor := &FrameTypeProcessorMock{ 21 | // ProcessFunc: func(header frameheader.FrameHeader, payload []byte, incomplete bool) error { 22 | // panic("mock out the Process method") 23 | // }, 24 | // } 25 | // 26 | // // use mockedFrameTypeProcessor in code that requires FrameTypeProcessor 27 | // // and then make assertions. 28 | // 29 | // } 30 | type FrameTypeProcessorMock struct { 31 | // ProcessFunc mocks the Process method. 32 | ProcessFunc func(header frameheader.FrameHeader, payload []byte, incomplete bool) error 33 | 34 | // calls tracks calls to the methods. 35 | calls struct { 36 | // Process holds details about calls to the Process method. 37 | Process []struct { 38 | // Header is the header argument value. 39 | Header frameheader.FrameHeader 40 | // Payload is the payload argument value. 41 | Payload []byte 42 | // Incomplete is the incomplete argument value. 43 | Incomplete bool 44 | } 45 | } 46 | lockProcess sync.RWMutex 47 | } 48 | 49 | // Process calls ProcessFunc. 50 | func (mock *FrameTypeProcessorMock) Process(header frameheader.FrameHeader, payload []byte, incomplete bool) error { 51 | if mock.ProcessFunc == nil { 52 | panic("FrameTypeProcessorMock.ProcessFunc: method is nil but FrameTypeProcessor.Process was just called") 53 | } 54 | callInfo := struct { 55 | Header frameheader.FrameHeader 56 | Payload []byte 57 | Incomplete bool 58 | }{ 59 | Header: header, 60 | Payload: payload, 61 | Incomplete: incomplete, 62 | } 63 | mock.lockProcess.Lock() 64 | mock.calls.Process = append(mock.calls.Process, callInfo) 65 | mock.lockProcess.Unlock() 66 | return mock.ProcessFunc(header, payload, incomplete) 67 | } 68 | 69 | // ProcessCalls gets all the calls that were made to Process. 70 | // Check the length with: 71 | // 72 | // len(mockedFrameTypeProcessor.ProcessCalls()) 73 | func (mock *FrameTypeProcessorMock) ProcessCalls() []struct { 74 | Header frameheader.FrameHeader 75 | Payload []byte 76 | Incomplete bool 77 | } { 78 | var calls []struct { 79 | Header frameheader.FrameHeader 80 | Payload []byte 81 | Incomplete bool 82 | } 83 | mock.lockProcess.RLock() 84 | calls = mock.calls.Process 85 | mock.lockProcess.RUnlock() 86 | return calls 87 | } 88 | -------------------------------------------------------------------------------- /loader/reciever/processor_benchamrk_test.go: -------------------------------------------------------------------------------- 1 | package reciever 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/net/http2" 10 | ) 11 | 12 | func BenchmarkProcessor(b *testing.B) { 13 | a := assert.New(b) 14 | priorityFramesChan := make(chan []byte, 100) 15 | bytesChan := make(chan []byte) 16 | 17 | processor := Processor{ 18 | new(Framer), []FrameTypeProcessor{ 19 | http2.FramePing: newPingFrameProcessor(priorityFramesChan), 20 | }, 21 | } 22 | 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | done := make(chan struct{}) 27 | go func() { 28 | defer close(done) 29 | a.NoError(processor.Run(bytesChan)) 30 | }() 31 | go func() { 32 | for { 33 | select { 34 | case <-ctx.Done(): 35 | close(bytesChan) 36 | return 37 | case <-priorityFramesChan: 38 | } 39 | } 40 | }() 41 | 42 | bufW := bytes.NewBuffer(nil) 43 | framer := http2.NewFramer(bufW, nil) 44 | pingPayload := [8]byte{8, 7, 6, 5, 4, 3, 2, 1} 45 | 46 | a.NoError(framer.WritePing(false, pingPayload)) 47 | frameLen := bufW.Len() 48 | for i := 0; i < 10; i++ { 49 | a.NoError(framer.WritePing(false, pingPayload)) 50 | } 51 | 52 | bts := bufW.Bytes() 53 | b.ResetTimer() 54 | for i := 0; i < b.N; i++ { 55 | b := bts[:] 56 | for len(b) > 0 { 57 | cutIndex := min(len(b), frameLen+1) 58 | bytesChan <- b[:cutIndex] 59 | b = b[cutIndex:] 60 | } 61 | } 62 | 63 | cancel() 64 | <-done 65 | } 66 | -------------------------------------------------------------------------------- /loader/reciever/reciever.go: -------------------------------------------------------------------------------- 1 | package reciever 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/net/http2" 13 | "golang.org/x/sync/errgroup" 14 | 15 | "github.com/ozontech/framer/consts" 16 | "github.com/ozontech/framer/loader/types" 17 | ) 18 | 19 | type GoAwayError struct { 20 | Code http2.ErrCode 21 | LastStreamID uint32 22 | DebugData []byte 23 | } 24 | 25 | func (e GoAwayError) Error() string { 26 | return "go away (" + e.Code.String() + "): " + string(e.DebugData) 27 | } 28 | 29 | type RSTStreamError struct { 30 | Code http2.ErrCode 31 | } 32 | 33 | func (e RSTStreamError) Error() string { 34 | return "rst stream: " + e.Code.String() 35 | } 36 | 37 | type Reciever struct { 38 | conn net.Conn 39 | buf1 []byte 40 | buf2 []byte 41 | processor *Processor 42 | } 43 | 44 | func NewReciever( 45 | conn net.Conn, 46 | fcConn types.FlowControl, 47 | priorityFramesChan chan<- []byte, 48 | streams types.Streams, 49 | ) *Reciever { 50 | return &Reciever{ 51 | conn, 52 | make([]byte, consts.RecieveBufferSize), 53 | make([]byte, consts.RecieveBufferSize), 54 | NewDefaultProcessor(streams.Store, streams.Limiter, fcConn, priorityFramesChan), 55 | } 56 | } 57 | 58 | func (r *Reciever) Run(ctx context.Context) error { 59 | g, ctx := errgroup.WithContext(ctx) 60 | 61 | ch := make(chan []byte) 62 | g.Go(func() error { 63 | return r.processor.Run(ch) 64 | }) 65 | g.Go(func() error { 66 | defer close(ch) 67 | for ctx.Err() == nil { 68 | err := r.read(ctx, ch, r.buf1) 69 | if err != nil { 70 | return err 71 | } 72 | err = r.read(ctx, ch, r.buf2) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | return nil 78 | }) 79 | return g.Wait() 80 | } 81 | 82 | func (r *Reciever) read(ctx context.Context, ch chan<- []byte, b []byte) error { 83 | if ctx.Err() != nil { 84 | return ctx.Err() 85 | } 86 | 87 | n, err := r.conn.Read(b) 88 | if err != nil { 89 | return fmt.Errorf("reading error: %w", err) 90 | } 91 | b = b[:n] 92 | 93 | select { 94 | case ch <- b: 95 | return nil 96 | case <-ctx.Done(): 97 | return ctx.Err() 98 | } 99 | } 100 | 101 | //nolint:unused 102 | func (r *Reciever) readExpiremental(ctx context.Context, ch chan<- []byte, b []byte) error { 103 | for { 104 | if ctx.Err() != nil { 105 | return ctx.Err() 106 | } 107 | 108 | err := r.conn.SetReadDeadline(time.Now().Add(consts.RecieveBatchTimeout)) 109 | if err != nil { 110 | return fmt.Errorf("set read deadline: %w", err) 111 | } 112 | 113 | n, err := io.ReadFull(r.conn, b) 114 | if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) { 115 | return fmt.Errorf("reading error: %w", err) 116 | } 117 | if n != 0 { 118 | b = b[:n] 119 | break 120 | } 121 | } 122 | 123 | select { 124 | case ch <- b: 125 | return nil 126 | case <-ctx.Done(): 127 | return ctx.Err() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /loader/streams/limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/ozontech/framer/loader/types" 7 | ) 8 | 9 | func New(quota uint32) types.StreamsLimiter { 10 | if quota == 0 { 11 | return noopLimiter{} 12 | } 13 | return newLimiter(quota) 14 | } 15 | 16 | type noopLimiter struct{} 17 | 18 | func (noopLimiter) WaitAllow() {} 19 | func (noopLimiter) Release() {} 20 | 21 | type limiter struct { 22 | quota uint32 23 | cond *sync.Cond 24 | } 25 | 26 | func newLimiter(quota uint32) *limiter { 27 | return &limiter{quota, sync.NewCond(&sync.Mutex{})} 28 | } 29 | 30 | func (l *limiter) WaitAllow() { 31 | l.cond.L.Lock() 32 | defer l.cond.L.Unlock() 33 | for l.quota == 0 { 34 | l.cond.Wait() 35 | } 36 | 37 | l.quota-- 38 | } 39 | 40 | func (l *limiter) Release() { 41 | l.cond.L.Lock() 42 | defer l.cond.Signal() 43 | defer l.cond.L.Unlock() 44 | 45 | l.quota++ 46 | } 47 | -------------------------------------------------------------------------------- /loader/streams/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/ozontech/framer/consts" 7 | "github.com/ozontech/framer/loader/flowcontrol" 8 | "github.com/ozontech/framer/loader/types" 9 | ) 10 | 11 | type StreamsPool struct { 12 | reporter types.LoaderReporter 13 | 14 | cond *sync.Cond 15 | pool []*streamImpl 16 | 17 | inUse uint32 18 | initialWindowSize uint32 19 | } 20 | 21 | func NewStreamsPool(reporter types.LoaderReporter, opts ...Opt) *StreamsPool { 22 | p := &StreamsPool{ 23 | reporter: reporter, 24 | cond: sync.NewCond(&sync.Mutex{}), 25 | pool: make([]*streamImpl, 0, 1024), 26 | initialWindowSize: consts.DefaultInitialWindowSize, 27 | } 28 | for _, o := range opts { 29 | o.apply(p) 30 | } 31 | return p 32 | } 33 | 34 | func (p *StreamsPool) Acquire(streamID uint32, tag string) types.Stream { 35 | var stream *streamImpl 36 | p.cond.L.Lock() 37 | p.inUse++ 38 | l := len(p.pool) 39 | if l > 0 { 40 | last := l - 1 41 | stream = p.pool[last] 42 | p.pool = p.pool[:last] 43 | p.cond.L.Unlock() 44 | } else { 45 | p.cond.L.Unlock() 46 | stream = &streamImpl{pool: p, fc: flowcontrol.NewFlowControl(p.initialWindowSize)} 47 | } 48 | 49 | stream.streamID = streamID 50 | stream.fc.Reset(p.initialWindowSize) 51 | stream.StreamState = p.reporter.Acquire(tag, streamID) 52 | 53 | return stream 54 | } 55 | 56 | func (p *StreamsPool) release(stream *streamImpl) { 57 | p.cond.L.Lock() 58 | defer p.cond.Signal() 59 | defer p.cond.L.Unlock() 60 | 61 | p.pool = append(p.pool, stream) 62 | p.inUse-- 63 | } 64 | 65 | func (p *StreamsPool) InUse() uint32 { 66 | p.cond.L.Lock() 67 | defer p.cond.L.Unlock() 68 | return p.inUse 69 | } 70 | 71 | func (p *StreamsPool) WaitAllReleased() <-chan struct{} { 72 | ch := make(chan struct{}) 73 | 74 | go func() { 75 | p.cond.L.Lock() 76 | defer p.cond.L.Unlock() 77 | 78 | for p.inUse != 0 { 79 | p.cond.Wait() 80 | } 81 | 82 | close(ch) 83 | }() 84 | 85 | return ch 86 | } 87 | 88 | type streamImpl struct { 89 | pool *StreamsPool 90 | streamID uint32 91 | fc types.FlowControl 92 | types.StreamState 93 | } 94 | 95 | func (s *streamImpl) ID() uint32 { return s.streamID } 96 | func (s *streamImpl) FC() types.FlowControl { return s.fc } 97 | func (s *streamImpl) End() { 98 | s.StreamState.End() 99 | s.pool.release(s) 100 | } 101 | 102 | type Opt interface { 103 | apply(*StreamsPool) 104 | } 105 | 106 | type WithInitialWindowSize uint32 107 | 108 | func (s WithInitialWindowSize) apply(p *StreamsPool) { 109 | p.initialWindowSize = uint32(s) 110 | } 111 | -------------------------------------------------------------------------------- /loader/streams/pool/pool_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ozontech/framer/loader/streams/pool" 7 | "github.com/ozontech/framer/report/noop" 8 | ) 9 | 10 | func BenchmarkStreamsPool(b *testing.B) { 11 | p := pool.NewStreamsPool(noop.New()) 12 | for i := 0; i < b.N; i++ { 13 | s := p.Acquire(0, "") 14 | s.End() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /loader/streams/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/ozontech/framer/loader/types" 7 | ) 8 | 9 | type StreamsNoop struct{} 10 | 11 | func NewStreamsNoop() StreamsNoop { return StreamsNoop{} } 12 | 13 | func (m StreamsNoop) Each(func(types.Stream)) {} 14 | func (m StreamsNoop) Set(uint32, types.Stream) {} 15 | func (m StreamsNoop) Get(uint32) types.Stream { return nil } 16 | func (m StreamsNoop) GetAndDelete(uint32) types.Stream { return nil } 17 | func (m StreamsNoop) Delete(uint32) {} 18 | 19 | type StreamsMapUnlocked map[uint32]types.Stream 20 | 21 | func NewStreamsMapUnlocked(size int) StreamsMapUnlocked { 22 | return make(map[uint32]types.Stream, size) 23 | } 24 | 25 | func (m StreamsMapUnlocked) Each(fn func(types.Stream)) { 26 | for _, stream := range m { 27 | fn(stream) 28 | } 29 | } 30 | func (m StreamsMapUnlocked) Set(id uint32, stream types.Stream) { m[id] = stream } 31 | func (m StreamsMapUnlocked) Get(id uint32) types.Stream { return m[id] } 32 | 33 | func (m StreamsMapUnlocked) GetAndDelete(id uint32) types.Stream { 34 | stream := m.Get(id) 35 | if stream != nil { 36 | m.Delete(id) 37 | } 38 | return stream 39 | } 40 | func (m StreamsMapUnlocked) Delete(id uint32) { delete(m, id) } 41 | 42 | // StreamsMap имплементация хранилища стримов используя map 43 | type StreamsMap struct { 44 | m StreamsMapUnlocked 45 | mu *sync.RWMutex 46 | } 47 | 48 | func NewStreamsMap(size int) *StreamsMap { 49 | return &StreamsMap{ 50 | m: NewStreamsMapUnlocked(size), 51 | mu: &sync.RWMutex{}, 52 | } 53 | } 54 | 55 | func (s *StreamsMap) Each(fn func(types.Stream)) { 56 | s.mu.RLock() 57 | defer s.mu.RUnlock() 58 | 59 | s.m.Each(fn) 60 | } 61 | 62 | func (s *StreamsMap) Set(id uint32, stream types.Stream) { 63 | s.mu.Lock() 64 | defer s.mu.Unlock() 65 | 66 | s.m.Set(id, stream) 67 | } 68 | 69 | func (s *StreamsMap) Get(id uint32) types.Stream { 70 | s.mu.RLock() 71 | defer s.mu.RUnlock() 72 | 73 | return s.m.Get(id) 74 | } 75 | 76 | func (s *StreamsMap) GetAndDelete(id uint32) types.Stream { 77 | s.mu.Lock() 78 | defer s.mu.Unlock() 79 | 80 | return s.m.GetAndDelete(id) 81 | } 82 | 83 | func (s *StreamsMap) Delete(id uint32) { 84 | s.mu.Lock() 85 | defer s.mu.Unlock() 86 | 87 | s.m.Delete(id) 88 | } 89 | 90 | // ShardedStreamsMap имплементация хранилища стримов используя шардированный map 91 | type ShardedStreamsMap struct { 92 | shards []types.StreamStore 93 | max uint32 94 | } 95 | 96 | func NewShardedStreamsMap(size uint32, build func() types.StreamStore) *ShardedStreamsMap { 97 | shards := make([]types.StreamStore, size*2) 98 | for i := 1; i < len(shards); i += 2 { 99 | shards[i] = build() 100 | } 101 | return &ShardedStreamsMap{shards, size - 1} 102 | } 103 | 104 | func (s *ShardedStreamsMap) shard(id uint32) types.StreamStore { 105 | return s.shards[id&s.max] 106 | } 107 | 108 | func (s *ShardedStreamsMap) Each(fn func(types.Stream)) { 109 | for i := 1; i < len(s.shards); i += 2 { 110 | shard := s.shards[i] 111 | shard.Each(fn) 112 | } 113 | } 114 | 115 | func (s *ShardedStreamsMap) Set(id uint32, stream types.Stream) { 116 | s.shard(id).Set(id, stream) 117 | } 118 | 119 | func (s *ShardedStreamsMap) Get(id uint32) types.Stream { 120 | return s.shard(id).Get(id) 121 | } 122 | 123 | func (s *ShardedStreamsMap) GetAndDelete(id uint32) types.Stream { 124 | return s.shard(id).GetAndDelete(id) 125 | } 126 | 127 | func (s *ShardedStreamsMap) Delete(id uint32) { 128 | s.shard(id).Delete(id) 129 | } 130 | -------------------------------------------------------------------------------- /loader/timeout_queue.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var NewTimeoutQueue = NewTimeoutSliceQueue 9 | 10 | // очередь проверки таймаута на основе слайса. 11 | type timeoutQueueItem struct { 12 | streamID uint32 13 | deadline time.Time 14 | } 15 | 16 | type timeoutSliceQueue struct { 17 | timeout time.Duration 18 | queue []timeoutQueueItem 19 | cond *sync.Cond 20 | 21 | done chan struct{} 22 | } 23 | 24 | func NewTimeoutSliceQueue(timeout time.Duration) TimeoutQueue { 25 | return &timeoutSliceQueue{ 26 | timeout: timeout, 27 | queue: make([]timeoutQueueItem, 0, 10), 28 | cond: sync.NewCond(&sync.Mutex{}), 29 | done: make(chan struct{}), 30 | } 31 | } 32 | 33 | func (q *timeoutSliceQueue) Add(streamID uint32) { 34 | q.cond.L.Lock() 35 | q.queue = append(q.queue, timeoutQueueItem{ 36 | streamID, 37 | time.Now().Add(q.timeout), 38 | }) 39 | q.cond.L.Unlock() 40 | 41 | q.cond.Signal() 42 | } 43 | 44 | func (q *timeoutSliceQueue) Next() (uint32, bool) { 45 | q.cond.L.Lock() 46 | for len(q.queue) == 0 { 47 | select { 48 | case <-q.done: 49 | return 0, false 50 | default: 51 | q.cond.Wait() 52 | } 53 | } 54 | nextItem := q.queue[0] 55 | q.queue = q.queue[1:] 56 | q.cond.L.Unlock() 57 | 58 | select { 59 | case <-q.done: 60 | return 0, false 61 | case <-time.After(time.Until(nextItem.deadline)): 62 | return nextItem.streamID, true 63 | } 64 | } 65 | 66 | func (q *timeoutSliceQueue) Close() { 67 | close(q.done) 68 | q.cond.Signal() 69 | } 70 | -------------------------------------------------------------------------------- /loader/types/flow_control.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type FlowControl interface { 4 | Wait(n uint32) (ok bool) // Ждем разрешения на отправку пакета длиной n. Если ok == false, стрим больше не может отправлять данные 5 | Disable() // Переводит flow control в невалидное состояние 6 | Add(n uint32) // Увеличение размера окна на отправку 7 | Reset(n uint32) // Сбрасывает состояние 8 | } 9 | -------------------------------------------------------------------------------- /loader/types/req.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "io" 4 | 5 | type HPackFieldWriter interface { 6 | SetWriter(w io.Writer) 7 | WriteField(k, v string) 8 | } 9 | 10 | type Req interface { 11 | SetUp( 12 | maxFramePayloadLen int, 13 | maxHeaderListSize int, 14 | streamID uint32, 15 | fieldWriter HPackFieldWriter, 16 | ) ([]Frame, error) 17 | FullMethodName() string 18 | Tag() string 19 | Size() int 20 | Releaser 21 | } 22 | 23 | type Releaser interface { 24 | Release() 25 | } 26 | 27 | type Frame struct { 28 | Chunks [3][]byte 29 | FlowControlPrice uint32 30 | } 31 | 32 | type DataSource interface { 33 | Fetch() (Req, error) 34 | } 35 | -------------------------------------------------------------------------------- /loader/types/stream.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "golang.org/x/net/http2" 5 | ) 6 | 7 | type Streams struct { 8 | Pool StreamsPool 9 | Limiter StreamsLimiter 10 | Store StreamStore 11 | } 12 | 13 | type StreamsPool interface { 14 | Acquire(streamID uint32, tag string) Stream 15 | } 16 | 17 | type StreamsLimiter interface { 18 | WaitAllow() // дождаться разрешение лимитера на создание нового стрима 19 | Release() // сообщить о завершении стрима 20 | } 21 | 22 | type StreamStore interface { 23 | Set(uint32, Stream) // добавить стрим в хранилище 24 | Get(uint32) Stream // получить стрим из хранилища 25 | GetAndDelete(uint32) Stream // удалить и вернуть 26 | Delete(uint32) // удалить стрим из хранилища 27 | Each(func(Stream)) // итерируется по всем стримам хранилища 28 | } 29 | 30 | type LoaderReporter interface { 31 | Acquire(tag string, streamID uint32) StreamState 32 | } 33 | 34 | type Reporter interface { 35 | LoaderReporter 36 | Run() error 37 | Close() error 38 | } 39 | 40 | type StreamState interface { 41 | RequestError(error) // не смогли подготовить запрос: не смогли декодировать запрос, привысили лимиты сервера (SettingMaxHeaderListSize) etc 42 | FirstByteSent() // отправили первый байт 43 | LastByteSent() // отправили последний байт 44 | SetSize(int) // сообщаем какой размер у данного унарного стрима 45 | OnHeader(name, value string) // сообщаем хедеры по мере их получения 46 | IoError(error) // сообщаем стриму, что получили ошибку ввода/вывода 47 | RSTStream(code http2.ErrCode) // если получили RST_STREAM 48 | GoAway(code http2.ErrCode, debugData []byte) // получен фрейм goaway со stream id > текущего 49 | Timeout() // случился таймаут стрима 50 | End() // завершение стрима. отправляет результат в отчет 51 | } 52 | 53 | type Stream interface { 54 | ID() uint32 55 | FC() FlowControl 56 | StreamState 57 | } 58 | -------------------------------------------------------------------------------- /report/multi/multi.go: -------------------------------------------------------------------------------- 1 | package multi 2 | 3 | import ( 4 | "github.com/ozontech/framer/loader/types" 5 | "github.com/ozontech/framer/utils/pool" 6 | "golang.org/x/net/http2" 7 | "golang.org/x/sync/errgroup" 8 | ) 9 | 10 | type Multi struct { 11 | nested []types.Reporter 12 | pool *pool.SlicePool[multiState] 13 | } 14 | 15 | func NewMutli(nested ...types.Reporter) *Multi { 16 | return &Multi{ 17 | nested, 18 | pool.NewSlicePoolSize[multiState](128), 19 | } 20 | } 21 | 22 | func (m *Multi) Run() error { 23 | g := new(errgroup.Group) 24 | for i := range m.nested { 25 | r := m.nested[i] 26 | g.Go(r.Run) 27 | } 28 | return g.Wait() 29 | } 30 | 31 | func (m *Multi) Close() error { 32 | g := new(errgroup.Group) 33 | for i := range m.nested { 34 | r := m.nested[i] 35 | g.Go(r.Close) 36 | } 37 | return g.Wait() 38 | } 39 | 40 | func (m *Multi) Acquire(tag string, streamID uint32) types.StreamState { 41 | ms, ok := m.pool.Acquire() 42 | if !ok { 43 | ms = make(multiState, len(m.nested)) 44 | } 45 | 46 | for i, r := range m.nested { 47 | ms[i] = r.Acquire(tag, streamID) 48 | } 49 | return ms 50 | } 51 | 52 | type multiState []types.StreamState 53 | 54 | func (s multiState) FirstByteSent() { 55 | for _, s := range s { 56 | s.FirstByteSent() 57 | } 58 | } 59 | 60 | func (s multiState) LastByteSent() { 61 | for _, s := range s { 62 | s.FirstByteSent() 63 | } 64 | } 65 | 66 | func (s multiState) SetSize(n int) { 67 | for _, s := range s { 68 | s.SetSize(n) 69 | } 70 | } 71 | 72 | func (s multiState) OnHeader(name, value string) { 73 | for _, s := range s { 74 | s.OnHeader(name, value) 75 | } 76 | } 77 | 78 | func (s multiState) RSTStream(code http2.ErrCode) { 79 | for _, s := range s { 80 | s.RSTStream(code) 81 | } 82 | } 83 | 84 | func (s multiState) RequestError(err error) { 85 | for _, s := range s { 86 | s.RequestError(err) 87 | } 88 | } 89 | 90 | func (s multiState) IoError(err error) { 91 | for _, s := range s { 92 | s.IoError(err) 93 | } 94 | } 95 | 96 | func (s multiState) GoAway(errCode http2.ErrCode, debugData []byte) { 97 | for _, s := range s { 98 | s.GoAway(errCode, debugData) 99 | } 100 | } 101 | 102 | func (s multiState) Timeout() { 103 | for _, s := range s { 104 | s.Timeout() 105 | } 106 | } 107 | 108 | func (s multiState) End() { 109 | for _, s := range s { 110 | s.End() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /report/noop/noop.go: -------------------------------------------------------------------------------- 1 | package noop 2 | 3 | import ( 4 | "github.com/ozontech/framer/loader/types" 5 | "golang.org/x/net/http2" 6 | ) 7 | 8 | type Noop struct { 9 | close chan struct{} 10 | } 11 | 12 | func New() *Noop { 13 | return &Noop{make(chan struct{})} 14 | } 15 | 16 | func (m *Noop) Run() error { 17 | <-m.close 18 | return nil 19 | } 20 | 21 | func (m *Noop) Close() error { 22 | close(m.close) 23 | return nil 24 | } 25 | 26 | func (m *Noop) Acquire(string, uint32) types.StreamState { 27 | return &noopState{} 28 | } 29 | 30 | type noopState struct{} 31 | 32 | func (s *noopState) FirstByteSent() {} 33 | func (s *noopState) LastByteSent() {} 34 | func (s *noopState) RequestError(error) {} 35 | func (s *noopState) SetSize(int) {} 36 | func (s *noopState) OnHeader(string, string) {} 37 | func (s *noopState) RSTStream(http2.ErrCode) {} 38 | func (s *noopState) IoError(error) {} 39 | func (s *noopState) GoAway(http2.ErrCode, []byte) {} 40 | func (s *noopState) Timeout() {} 41 | func (s *noopState) End() {} 42 | -------------------------------------------------------------------------------- /report/phout/phout.go: -------------------------------------------------------------------------------- 1 | package phout 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/ozontech/framer/loader/types" 13 | "github.com/ozontech/framer/utils/pool" 14 | "golang.org/x/net/http2" 15 | ) 16 | 17 | var now = time.Now 18 | 19 | type Reporter struct { 20 | closeCh chan struct{} 21 | w *bufio.Writer 22 | ch chan *streamState 23 | pool *pool.SlicePool[*streamState] 24 | } 25 | 26 | func New(w io.Writer) *Reporter { 27 | return &Reporter{ 28 | make(chan struct{}), 29 | bufio.NewWriter(w), 30 | make(chan *streamState, 256), 31 | pool.NewSlicePoolSize[*streamState](256), 32 | } 33 | } 34 | 35 | func (r *Reporter) Run() error { 36 | for s := range r.ch { 37 | _, err := r.w.Write(s.result()) 38 | if err != nil { 39 | return fmt.Errorf("write: %w", err) 40 | } 41 | r.pool.Release(s) 42 | } 43 | return r.w.Flush() 44 | } 45 | 46 | func (r *Reporter) Close() error { 47 | close(r.ch) 48 | return nil 49 | } 50 | 51 | func (r *Reporter) Acquire(tag string, _ uint32) types.StreamState { 52 | ss, ok := r.pool.Acquire() 53 | if !ok { 54 | ss = &streamState{ 55 | reportLine: make([]byte, 128), 56 | reporter: r, 57 | } 58 | } 59 | ss.reset(tag) 60 | return ss 61 | } 62 | 63 | func (r *Reporter) accept(s *streamState) { 64 | r.ch <- s 65 | } 66 | 67 | type streamState struct { 68 | reportLine []byte 69 | 70 | reporter *Reporter 71 | 72 | grpcCodeHeader string 73 | http2CodeHeader string 74 | 75 | requestError error 76 | ioErr error 77 | rstStreamCode *http2.ErrCode 78 | goAwayCode *http2.ErrCode 79 | timeouted bool 80 | 81 | reqSize int 82 | startTime time.Time 83 | endTime time.Time 84 | tag string 85 | } 86 | 87 | func (s *streamState) reset(tag string) { 88 | s.tag = tag 89 | s.startTime = now() 90 | 91 | s.grpcCodeHeader = "" 92 | s.http2CodeHeader = "" 93 | s.timeouted = false 94 | 95 | s.requestError = nil 96 | s.ioErr = nil 97 | s.goAwayCode = nil 98 | s.rstStreamCode = nil 99 | s.reqSize = 0 100 | } 101 | 102 | func (s *streamState) FirstByteSent() {} 103 | func (s *streamState) LastByteSent() {} 104 | 105 | func (s *streamState) SetSize(size int) { 106 | s.reqSize = size 107 | } 108 | 109 | func (s *streamState) OnHeader(name, value string) { 110 | switch name { 111 | case "grpc-status": 112 | s.grpcCodeHeader = value 113 | case ":status": 114 | s.http2CodeHeader = value 115 | } 116 | } 117 | 118 | func (s *streamState) RequestError(err error) { 119 | s.requestError = err 120 | } 121 | 122 | func (s *streamState) IoError(err error) { 123 | s.ioErr = err 124 | } 125 | 126 | func (s *streamState) RSTStream(code http2.ErrCode) { 127 | s.rstStreamCode = &code 128 | } 129 | 130 | func (s *streamState) GoAway(code http2.ErrCode, _ []byte) { 131 | s.goAwayCode = &code 132 | } 133 | 134 | func (s *streamState) Timeout() { 135 | s.timeouted = true 136 | } 137 | 138 | const tabChar = '\t' 139 | 140 | func (s *streamState) result() []byte { 141 | s.reportLine = s.reportLine[:0] 142 | s.reportLine = strconv.AppendInt(s.reportLine, s.startTime.Unix(), 10) 143 | s.reportLine = append(s.reportLine, '.') 144 | s.reportLine = strconv.AppendInt(s.reportLine, int64(s.startTime.Nanosecond()/1e6), 10) 145 | s.reportLine = append(s.reportLine, tabChar) 146 | s.reportLine = append(s.reportLine, []byte(s.tag)...) 147 | s.reportLine = append(s.reportLine, tabChar) 148 | 149 | // keyRTTMicro = iota 150 | rtt := s.endTime.Sub(s.startTime).Microseconds() 151 | 152 | s.reportLine = strconv.AppendInt(s.reportLine, rtt, 10) 153 | s.reportLine = append(s.reportLine, tabChar) 154 | 155 | // keyConnectMicro 156 | s.reportLine = append(s.reportLine, '0', tabChar) 157 | // keySendMicro 158 | s.reportLine = append(s.reportLine, '0', tabChar) 159 | // keyLatencyMicro 160 | s.reportLine = append(s.reportLine, '0', tabChar) 161 | // keyReceiveMicro 162 | s.reportLine = append(s.reportLine, '0', tabChar) 163 | // keyIntervalEventMicro 164 | s.reportLine = append(s.reportLine, '0', tabChar) 165 | // keyRequestBytes 166 | s.reportLine = strconv.AppendInt(s.reportLine, int64(s.reqSize), 10) 167 | s.reportLine = append(s.reportLine, tabChar) 168 | // keyResponseBytes 169 | s.reportLine = append(s.reportLine, '0', tabChar) 170 | // keyErrno 171 | var errNo syscall.Errno 172 | if s.ioErr != nil { 173 | if !errors.As(s.ioErr, &errNo) { 174 | errNo = 999 175 | } 176 | s.reportLine = strconv.AppendInt(s.reportLine, int64(errNo), 10) 177 | s.reportLine = append(s.reportLine, tabChar) 178 | } else { 179 | s.reportLine = append(s.reportLine, '0', tabChar) 180 | } 181 | // keyProtoCode 182 | switch { 183 | case s.requestError != nil: 184 | s.reportLine = append(s.reportLine, "client_error"...) 185 | case s.rstStreamCode != nil: 186 | s.reportLine = append(s.reportLine, "rst_"...) 187 | s.reportLine = strconv.AppendInt(s.reportLine, int64(*s.rstStreamCode), 10) 188 | case s.goAwayCode != nil: 189 | s.reportLine = append(s.reportLine, "goaway_"...) 190 | s.reportLine = strconv.AppendInt(s.reportLine, int64(*s.goAwayCode), 10) 191 | case s.timeouted: 192 | s.reportLine = append(s.reportLine, "grpc_4"...) 193 | case s.http2CodeHeader == "": 194 | s.reportLine = append(s.reportLine, "http2_1"...) // protocol errro 195 | case s.grpcCodeHeader != "": 196 | s.reportLine = append(s.reportLine, "grpc_"...) 197 | s.reportLine = append(s.reportLine, []byte(s.grpcCodeHeader)...) 198 | default: 199 | s.reportLine = append(s.reportLine, "http2_1"...) // protocol error 200 | } 201 | s.reportLine = append(s.reportLine, '\n') 202 | return s.reportLine 203 | } 204 | 205 | func (s *streamState) End() { 206 | s.endTime = now() 207 | s.reporter.accept(s) 208 | } 209 | -------------------------------------------------------------------------------- /report/phout/phout_test.go: -------------------------------------------------------------------------------- 1 | package phout 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "syscall" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/net/http2" 13 | ) 14 | 15 | const streamID = 1 16 | 17 | func TestPhout(t *testing.T) { 18 | t.Parallel() 19 | a := assert.New(t) 20 | const timeout = 11 * time.Second 21 | 22 | b := new(bytes.Buffer) 23 | r := New(b) 24 | errChan := make(chan error) 25 | go func() { 26 | errChan <- r.Run() 27 | }() 28 | 29 | var expected string 30 | 31 | { 32 | startTime := time.Now() 33 | now = func() time.Time { return startTime } 34 | 35 | state := r.Acquire("tag1", streamID) 36 | state.SetSize(111) 37 | state.OnHeader(":status", "200") 38 | state.OnHeader("grpc-status", "0") 39 | 40 | endTime := time.Now() 41 | now = func() time.Time { return endTime } 42 | state.End() 43 | 44 | expected += fmt.Sprintf( 45 | "%d.%d tag1 %d 0 0 0 0 0 111 0 0 grpc_0\n", 46 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 47 | endTime.Sub(startTime).Microseconds(), 48 | ) 49 | } 50 | 51 | { 52 | startTime := time.Now() 53 | now = func() time.Time { return startTime } 54 | 55 | state := r.Acquire("tag2", streamID) 56 | state.SetSize(222) 57 | state.OnHeader(":status", "200") 58 | state.OnHeader("grpc-status", "0") 59 | state.IoError(fmt.Errorf("read error: %w", syscall.Errno(123))) 60 | 61 | endTime := time.Now() 62 | now = func() time.Time { return endTime } 63 | state.End() 64 | 65 | expected += fmt.Sprintf( 66 | "%d.%d tag2 %d 0 0 0 0 0 222 0 123 grpc_0\n", 67 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 68 | endTime.Sub(startTime).Microseconds(), 69 | ) 70 | } 71 | 72 | { 73 | startTime := time.Now() 74 | now = func() time.Time { return startTime } 75 | 76 | state := r.Acquire("tag2", streamID) 77 | state.SetSize(222) 78 | state.OnHeader(":status", "200") 79 | state.OnHeader("grpc-status", "0") 80 | state.IoError(fmt.Errorf("read error: %w", errors.New("unknown error"))) 81 | 82 | endTime := time.Now() 83 | now = func() time.Time { return endTime } 84 | state.End() 85 | 86 | expected += fmt.Sprintf( 87 | "%d.%d tag2 %d 0 0 0 0 0 222 0 999 grpc_0\n", 88 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 89 | endTime.Sub(startTime).Microseconds(), 90 | ) 91 | } 92 | 93 | { 94 | startTime := time.Now() 95 | now = func() time.Time { return startTime } 96 | 97 | state := r.Acquire("", streamID) 98 | state.RSTStream(http2.ErrCodeInternal) 99 | 100 | endTime := time.Now() 101 | now = func() time.Time { return endTime } 102 | state.End() 103 | 104 | expected += fmt.Sprintf( 105 | "%d.%d %d 0 0 0 0 0 0 0 0 rst_2\n", 106 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 107 | endTime.Sub(startTime).Microseconds(), 108 | ) 109 | } 110 | 111 | { 112 | startTime := time.Now() 113 | now = func() time.Time { return startTime } 114 | 115 | state := r.Acquire("", streamID) 116 | state.GoAway(http2.ErrCodeInternal, nil) 117 | 118 | endTime := time.Now() 119 | now = func() time.Time { return endTime } 120 | state.End() 121 | 122 | expected += fmt.Sprintf( 123 | "%d.%d %d 0 0 0 0 0 0 0 0 goaway_2\n", 124 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 125 | endTime.Sub(startTime).Microseconds(), 126 | ) 127 | } 128 | 129 | { 130 | startTime := time.Now() 131 | now = func() time.Time { return startTime } 132 | 133 | state := r.Acquire("", streamID) 134 | 135 | endTime := startTime.Add(timeout + 1) 136 | now = func() time.Time { return endTime } 137 | state.Timeout() 138 | state.End() 139 | 140 | expected += fmt.Sprintf( 141 | "%d.%d %d 0 0 0 0 0 0 0 0 grpc_4\n", 142 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 143 | endTime.Sub(startTime).Microseconds(), 144 | ) 145 | } 146 | 147 | { 148 | startTime := time.Now() 149 | now = func() time.Time { return startTime } 150 | 151 | state := r.Acquire("", streamID) 152 | 153 | endTime := time.Now() 154 | now = func() time.Time { return endTime } 155 | state.End() 156 | 157 | expected += fmt.Sprintf( 158 | "%d.%d %d 0 0 0 0 0 0 0 0 http2_1\n", 159 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 160 | endTime.Sub(startTime).Microseconds(), 161 | ) 162 | } 163 | 164 | { 165 | startTime := time.Now() 166 | now = func() time.Time { return startTime } 167 | 168 | state := r.Acquire("", streamID) 169 | 170 | endTime := time.Now() 171 | now = func() time.Time { return endTime } 172 | 173 | state.OnHeader("grpc-status", "0") 174 | state.End() 175 | 176 | expected += fmt.Sprintf( 177 | "%d.%d %d 0 0 0 0 0 0 0 0 http2_1\n", 178 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 179 | endTime.Sub(startTime).Microseconds(), 180 | ) 181 | } 182 | 183 | { 184 | startTime := time.Now() 185 | now = func() time.Time { return startTime } 186 | 187 | state := r.Acquire("", streamID) 188 | 189 | endTime := time.Now() 190 | now = func() time.Time { return endTime } 191 | 192 | state.OnHeader(":status", "200") 193 | state.End() 194 | 195 | expected += fmt.Sprintf( 196 | "%d.%d %d 0 0 0 0 0 0 0 0 http2_1\n", 197 | startTime.UnixMilli()/1e3, startTime.UnixMilli()%1e3, 198 | endTime.Sub(startTime).Microseconds(), 199 | ) 200 | } 201 | 202 | r.Close() 203 | a.NoError(<-errChan) 204 | a.Equal(expected, b.String()) 205 | } 206 | -------------------------------------------------------------------------------- /report/simple/simple.go: -------------------------------------------------------------------------------- 1 | package simple 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/dustin/go-humanize" 9 | "github.com/ozontech/framer/loader/types" 10 | "github.com/ozontech/framer/utils/pool" 11 | "golang.org/x/net/http2" 12 | ) 13 | 14 | type Reporter struct { 15 | pool *pool.SlicePool[*streamState] 16 | closeCh chan struct{} 17 | 18 | start time.Time 19 | ok atomic.Uint32 20 | nook atomic.Uint32 21 | req atomic.Uint32 22 | size atomic.Uint64 23 | 24 | lastOk uint32 25 | lastNook uint32 26 | lastReq uint32 27 | lastSize uint64 28 | lastTime time.Time 29 | } 30 | 31 | func New() *Reporter { 32 | now := time.Now() 33 | return &Reporter{ 34 | pool: pool.NewSlicePoolSize[*streamState](100), 35 | closeCh: make(chan struct{}), 36 | start: now, 37 | lastTime: now, 38 | } 39 | } 40 | 41 | func (a *Reporter) Run() error { 42 | t := time.NewTicker(time.Second) 43 | defer a.total() 44 | for { 45 | select { 46 | case now := <-t.C: 47 | a.report(now) 48 | case <-a.closeCh: 49 | return nil 50 | } 51 | } 52 | } 53 | 54 | func (a *Reporter) Close() error { 55 | close(a.closeCh) 56 | return nil 57 | } 58 | 59 | func (a *Reporter) Acquire(_ string, _ uint32) types.StreamState { 60 | a.req.Add(1) 61 | ss, ok := a.pool.Acquire() 62 | if !ok { 63 | ss = &streamState{reporter: a} 64 | } 65 | ss.reset() 66 | return ss 67 | } 68 | 69 | func (a *Reporter) accept(s *streamState) { 70 | if s.result() { 71 | a.nook.Add(1) 72 | } else { 73 | a.ok.Add(1) 74 | } 75 | 76 | a.pool.Release(s) 77 | } 78 | 79 | func (a *Reporter) addSize(size int) { 80 | a.size.Add(uint64(size)) 81 | } 82 | 83 | func (a *Reporter) write(ok, nook, req uint32, size uint64, d time.Duration) { 84 | total := ok + nook 85 | miliSeconds := d.Milliseconds() 86 | if miliSeconds > 0 { 87 | fmt.Printf( 88 | "total=%d ok=%d nook=%d req=%d size=%s req/s=%.2f resp/s=%.2f\n", 89 | total, ok, nook, req, 90 | humanize.Bytes(size*1000/uint64(miliSeconds)), 91 | float64(req)*1000/float64(miliSeconds), float64(total)*1000/float64(miliSeconds), 92 | ) 93 | } else { 94 | fmt.Printf("total=%d ok=%d nook=%d req=%d\n", total, ok, nook, req) 95 | } 96 | } 97 | 98 | func (a *Reporter) total() { 99 | fmt.Println("total") 100 | a.write(a.ok.Load(), a.nook.Load(), a.req.Load(), a.size.Load(), time.Since(a.start)) 101 | } 102 | 103 | func (a *Reporter) report(now time.Time) { 104 | ok, nook, req, size, period := a.ok.Load(), a.nook.Load(), a.req.Load(), a.size.Load(), now.Sub(a.lastTime) 105 | a.write(ok-a.lastOk, nook-a.lastNook, req-a.lastReq, size-a.lastSize, period) 106 | a.lastOk, a.lastNook, a.lastTime, a.lastReq, a.lastSize = ok, nook, now, req, size 107 | } 108 | 109 | type streamState struct { 110 | reporter *Reporter 111 | size int 112 | 113 | timeouted bool 114 | grpcCodeStr string 115 | grpcMessageStr string 116 | code http2.ErrCode 117 | goAway bool 118 | ioErr error 119 | requestErr error 120 | } 121 | 122 | func (s *streamState) reset() { 123 | s.timeouted = false 124 | 125 | s.size = 0 126 | s.grpcCodeStr = "" 127 | s.grpcMessageStr = "" 128 | s.code = 0 129 | s.ioErr = nil 130 | } 131 | 132 | func (s *streamState) FirstByteSent() {} 133 | func (s *streamState) LastByteSent() {} 134 | 135 | func (s *streamState) SetSize(size int) { 136 | s.reporter.addSize(size) 137 | } 138 | 139 | func (s *streamState) OnHeader(name, value string) { 140 | switch name { 141 | case "grpc-status": 142 | s.grpcCodeStr = value 143 | case "grpc-message": 144 | s.grpcMessageStr = value 145 | } 146 | } 147 | 148 | func (s *streamState) RequestError(err error) { 149 | s.requestErr = err 150 | } 151 | 152 | func (s *streamState) IoError(err error) { 153 | s.ioErr = err 154 | } 155 | 156 | func (s *streamState) RSTStream(code http2.ErrCode) { 157 | s.code = code 158 | } 159 | 160 | func (s *streamState) GoAway(http2.ErrCode, []byte) { 161 | s.goAway = true 162 | } 163 | 164 | func (s *streamState) Timeout() { 165 | s.timeouted = true 166 | } 167 | 168 | func (s *streamState) result() (ok bool) { 169 | switch { 170 | case s.timeouted: 171 | return false 172 | case s.code != http2.ErrCodeNo: 173 | return false 174 | case s.requestErr != nil: 175 | return false 176 | case s.ioErr != nil: 177 | return false 178 | case s.goAway: 179 | return false 180 | case s.grpcCodeStr == "0": 181 | return true 182 | } 183 | return false 184 | } 185 | 186 | func (s *streamState) End() { 187 | s.reporter.accept(s) 188 | } 189 | -------------------------------------------------------------------------------- /report/supersimple/supersimple.go: -------------------------------------------------------------------------------- 1 | package supersimple 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/dustin/go-humanize" 9 | "github.com/ozontech/framer/loader/types" 10 | "github.com/ozontech/framer/utils/pool" 11 | "golang.org/x/net/http2" 12 | ) 13 | 14 | type Reporter struct { 15 | pool *pool.SlicePool[*streamState] 16 | closeCh chan struct{} 17 | 18 | start time.Time 19 | ok atomic.Uint32 20 | nook atomic.Uint32 21 | req atomic.Uint32 22 | size atomic.Uint64 23 | 24 | lastOk uint32 25 | lastNook uint32 26 | lastReq uint32 27 | lastSize uint64 28 | lastTime time.Time 29 | } 30 | 31 | func New() *Reporter { 32 | now := time.Now() 33 | return &Reporter{ 34 | pool: pool.NewSlicePoolSize[*streamState](100), 35 | closeCh: make(chan struct{}), 36 | start: now, 37 | lastTime: now, 38 | } 39 | } 40 | 41 | func (a *Reporter) Run() error { 42 | t := time.NewTicker(time.Second) 43 | defer t.Stop() 44 | defer a.total() 45 | for { 46 | select { 47 | case now := <-t.C: 48 | a.report(now) 49 | case <-a.closeCh: 50 | return nil 51 | } 52 | } 53 | } 54 | 55 | func (a *Reporter) Close() error { 56 | close(a.closeCh) 57 | return nil 58 | } 59 | 60 | func (a *Reporter) Acquire(tag string, _ uint32) types.StreamState { 61 | a.req.Add(1) 62 | ss, ok := a.pool.Acquire() 63 | if !ok { 64 | ss = &streamState{reporter: a} 65 | } 66 | ss.reset(tag) 67 | return ss 68 | } 69 | 70 | func (a *Reporter) accept(s *streamState) { 71 | if s.result() { 72 | a.ok.Add(1) 73 | } else { 74 | a.nook.Add(1) 75 | } 76 | 77 | a.pool.Release(s) 78 | } 79 | 80 | func (a *Reporter) addSize(size int) { 81 | a.size.Add(uint64(size)) 82 | } 83 | 84 | // var p = message.NewPrinter(language.English) 85 | 86 | func (a *Reporter) write(ok, nook, req uint32, size uint64, d time.Duration) { 87 | total := ok + nook 88 | miliSeconds := d.Milliseconds() 89 | if miliSeconds > 0 { 90 | fmt.Printf( 91 | "total=%d ok=%d nook=%d req=%d size=%s req/s=%.2f resp/s=%.2f\n", 92 | total, ok, nook, req, 93 | humanize.Bytes(size*1000/uint64(miliSeconds)), 94 | float64(req)*1000/float64(miliSeconds), float64(total)*1000/float64(miliSeconds), 95 | ) 96 | } else { 97 | fmt.Printf("total=%d ok=%d nook=%d req=%d\n", total, ok, nook, req) 98 | } 99 | } 100 | 101 | func (a *Reporter) total() { 102 | fmt.Println("total") 103 | a.write(a.ok.Load(), a.nook.Load(), a.req.Load(), a.size.Load(), time.Since(a.start)) 104 | } 105 | 106 | func (a *Reporter) report(now time.Time) { 107 | ok, nook, req, size, period := a.ok.Load(), a.nook.Load(), a.req.Load(), a.size.Load(), now.Sub(a.lastTime) 108 | a.write(ok-a.lastOk, nook-a.lastNook, req-a.lastReq, size-a.lastSize, period) 109 | a.lastOk, a.lastNook, a.lastTime, a.lastReq, a.lastSize = ok, nook, now, req, size 110 | } 111 | 112 | type streamState struct { 113 | reporter *Reporter 114 | noOk bool 115 | } 116 | 117 | func (s *streamState) reset(_ string) { 118 | s.noOk = false 119 | } 120 | 121 | func (s *streamState) FirstByteSent() {} 122 | func (s *streamState) LastByteSent() {} 123 | 124 | func (s *streamState) SetSize(size int) { 125 | s.reporter.addSize(size) 126 | } 127 | 128 | func (s *streamState) OnHeader(name, value string) { 129 | if name == "grpc-status" && value != "0" { 130 | s.noOk = true 131 | } 132 | } 133 | 134 | func (s *streamState) RequestError(error) { s.noOk = true } 135 | func (s *streamState) IoError(error) { s.noOk = true } 136 | func (s *streamState) RSTStream(http2.ErrCode) { s.noOk = true } 137 | func (s *streamState) GoAway(http2.ErrCode, []byte) { s.noOk = true } 138 | func (s *streamState) Timeout() { s.noOk = true } 139 | 140 | func (s *streamState) result() (ok bool) { 141 | return !s.noOk 142 | } 143 | 144 | func (s *streamState) End() { 145 | s.reporter.accept(s) 146 | } 147 | -------------------------------------------------------------------------------- /scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | // Scheduler defines the interface to control the rate of request. 10 | type Scheduler interface { 11 | Next(currentReq int64) (wait time.Duration, stop bool) 12 | } 13 | 14 | type CountLimiter struct { 15 | s Scheduler 16 | limit int64 17 | } 18 | 19 | func NewCountLimiter(s Scheduler, limit int64) CountLimiter { 20 | return CountLimiter{s, limit} 21 | } 22 | 23 | func (cl CountLimiter) Next(currentReq int64) (time.Duration, bool) { 24 | if currentReq >= cl.limit { 25 | return 0, false 26 | } 27 | return cl.s.Next(currentReq) 28 | } 29 | 30 | // A Constant defines a constant rate of requests. 31 | type Constant struct { 32 | interval time.Duration 33 | } 34 | 35 | func NewConstant(freq uint64) (Constant, error) { 36 | if freq == 0 { 37 | return Constant{}, fmt.Errorf("freq must be positive") 38 | } 39 | return Constant{time.Second / time.Duration(freq)}, nil 40 | } 41 | 42 | // Next determines the length of time to sleep until the next request is sent. 43 | func (cp Constant) Next(currentReq int64) (time.Duration, bool) { 44 | return time.Duration(currentReq) * cp.interval, true 45 | } 46 | 47 | // Unlimited defines a unlimited rate of request. 48 | type Unlimited struct{} 49 | 50 | // Next determines the length of time to sleep until the next request is sent. 51 | func (cp Unlimited) Next(_ int64) (time.Duration, bool) { 52 | return 0, true 53 | } 54 | 55 | // Line defines a line rate of request. 56 | type Line struct { 57 | b float64 58 | twoA float64 59 | bSquare float64 60 | bilionDivA float64 61 | } 62 | 63 | func NewLine(from, to float64, d time.Duration) Line { 64 | a := (to - from) / float64(d/1e9) 65 | b := from 66 | return Line{ 67 | b: from, 68 | twoA: 2 * a, 69 | bSquare: b * b, 70 | bilionDivA: 1e9 / a, 71 | } 72 | } 73 | 74 | // Next determines the length of time to sleep until the next request is sent. 75 | func (cp Line) Next(currentReq int64) (time.Duration, bool) { 76 | return time.Duration((math.Sqrt(cp.twoA*float64(currentReq)+cp.bSquare) - cp.b) * cp.bilionDivA), true 77 | } 78 | -------------------------------------------------------------------------------- /test_files/requests: -------------------------------------------------------------------------------- 1 | 113 2 | /Test 3 | /test.api.TestApi/Test 4 | {"x-my-header-key1":["my-header-val1"],"x-my-header-key2":["my-header-val2"]} 5 | 6 | ping 7 | 113 8 | /Test 9 | /test.api.TestApi/Test 10 | {"x-my-header-key1":["my-header-val1"],"x-my-header-key2":["my-header-val2"]} 11 | 12 | ping 13 | -------------------------------------------------------------------------------- /utils/grpc/encode_duration.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2020 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package grpcutil 20 | 21 | import ( 22 | "strconv" 23 | "time" 24 | ) 25 | 26 | const maxTimeoutValue int64 = 100000000 - 1 27 | 28 | // div does integer division and round-up the result. Note that this is 29 | // equivalent to (d+r-1)/r but has less chance to overflow. 30 | func div(d, r time.Duration) int64 { 31 | if d%r > 0 { 32 | return int64(d/r + 1) 33 | } 34 | return int64(d / r) 35 | } 36 | 37 | // EncodeDuration encodes the duration to the format grpc-timeout header 38 | // accepts. 39 | // 40 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests 41 | func EncodeDuration(t time.Duration) string { 42 | // TODO: This is simplistic and not bandwidth efficient. Improve it. 43 | if t <= 0 { 44 | return "0n" 45 | } 46 | if d := div(t, time.Nanosecond); d <= maxTimeoutValue { 47 | return strconv.FormatInt(d, 10) + "n" 48 | } 49 | if d := div(t, time.Microsecond); d <= maxTimeoutValue { 50 | return strconv.FormatInt(d, 10) + "u" 51 | } 52 | if d := div(t, time.Millisecond); d <= maxTimeoutValue { 53 | return strconv.FormatInt(d, 10) + "m" 54 | } 55 | if d := div(t, time.Second); d <= maxTimeoutValue { 56 | return strconv.FormatInt(d, 10) + "S" 57 | } 58 | if d := div(t, time.Minute); d <= maxTimeoutValue { 59 | return strconv.FormatInt(d, 10) + "M" 60 | } 61 | // Note that maxTimeoutValue * time.Hour > MaxInt64. 62 | return strconv.FormatInt(div(t, time.Hour), 10) + "H" 63 | } 64 | -------------------------------------------------------------------------------- /utils/grpc/encode_duration_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2020 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | package grpcutil 20 | 21 | import ( 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestEncodeDuration(t *testing.T) { 27 | t.Parallel() 28 | for _, test := range []struct { 29 | in string 30 | out string 31 | }{ 32 | {"12345678ns", "12345678n"}, 33 | {"123456789ns", "123457u"}, 34 | {"12345678us", "12345678u"}, 35 | {"123456789us", "123457m"}, 36 | {"12345678ms", "12345678m"}, 37 | {"123456789ms", "123457S"}, 38 | {"12345678s", "12345678S"}, 39 | {"123456789s", "2057614M"}, 40 | {"12345678m", "12345678M"}, 41 | {"123456789m", "2057614H"}, 42 | } { 43 | d, err := time.ParseDuration(test.in) 44 | if err != nil { 45 | t.Fatalf("failed to parse duration string %s: %v", test.in, err) 46 | } 47 | out := EncodeDuration(d) 48 | if out != test.out { 49 | t.Fatalf("timeoutEncode(%s) = %s, want %s", test.in, out, test.out) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /utils/hpack_wrapper/wrapper.go: -------------------------------------------------------------------------------- 1 | package hpackwrapper 2 | 3 | import ( 4 | "io" 5 | 6 | "golang.org/x/net/http2/hpack" 7 | ) 8 | 9 | type Wrapper struct { 10 | io.Writer 11 | enc *hpack.Encoder 12 | } 13 | 14 | func NewWrapper(opts ...Opt) *Wrapper { 15 | wrapper := &Wrapper{} 16 | wrapper.enc = hpack.NewEncoder(wrapper) 17 | for _, o := range opts { 18 | o.apply(wrapper) 19 | } 20 | 21 | return wrapper 22 | } 23 | 24 | func (ww *Wrapper) SetWriter(w io.Writer) { ww.Writer = w } 25 | func (ww *Wrapper) WriteField(k, v string) { 26 | //nolint:errcheck // всегда пишем в буфер, это безопасно 27 | ww.enc.WriteField(hpack.HeaderField{ 28 | Name: k, 29 | Value: v, 30 | }) 31 | } 32 | 33 | type Opt interface { 34 | apply(*Wrapper) 35 | } 36 | 37 | type WithMaxDynamicTableSize uint32 38 | 39 | func (s WithMaxDynamicTableSize) apply(w *Wrapper) { 40 | w.enc.SetMaxDynamicTableSize(uint32(s)) 41 | } 42 | -------------------------------------------------------------------------------- /utils/lru/lru.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | type LRU struct { 9 | maxSize int 10 | items map[string]*list.Element 11 | list *list.List 12 | mu sync.Mutex 13 | } 14 | 15 | func New(maxSize int) *LRU { 16 | if maxSize < 1 { 17 | panic("assertion error: maxSize < 1") 18 | } 19 | return &LRU{ 20 | maxSize: maxSize, 21 | items: make(map[string]*list.Element, maxSize), 22 | list: list.New(), 23 | } 24 | } 25 | 26 | // GetOrAdd fetch item from lru and increase eviction order or create 27 | func (l *LRU) GetOrAdd(keyB []byte) string { 28 | l.mu.Lock() 29 | defer l.mu.Unlock() 30 | 31 | element, ok := l.items[string(keyB)] 32 | if ok { 33 | l.list.MoveToFront(element) 34 | return element.Value.(string) 35 | } 36 | 37 | if len(l.items) >= l.maxSize { 38 | element = l.list.Back() 39 | l.list.Remove(element) 40 | delete(l.items, element.Value.(string)) 41 | } 42 | 43 | keyS := string(keyB) 44 | element = l.list.PushFront(keyS) 45 | l.items[keyS] = element 46 | return keyS 47 | } 48 | -------------------------------------------------------------------------------- /utils/lru/lru_test.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLRU(t *testing.T) { 10 | t.Parallel() 11 | 12 | a := assert.New(t) 13 | l := New(3) 14 | l.GetOrAdd([]byte("one")) 15 | l.GetOrAdd([]byte("two")) 16 | l.GetOrAdd([]byte("three")) 17 | l.GetOrAdd([]byte("one")) 18 | a.Len(l.items, 3) 19 | a.Equal(l.list.Len(), 3) 20 | l.GetOrAdd([]byte("four")) 21 | a.Len(l.items, 3) 22 | a.Equal(l.list.Len(), 3) 23 | 24 | lruOrder := []string{"four", "one", "three"} 25 | a.Len(l.items, len(lruOrder)) 26 | el := l.list.Front() 27 | for _, v := range lruOrder { 28 | _, ok := l.items[v] 29 | a.True(ok) 30 | a.Equal(el.Value, v) 31 | el = el.Next() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utils/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import "sync" 4 | 5 | type SlicePool[T any] struct { 6 | mu sync.Mutex 7 | s []T 8 | } 9 | 10 | func NewSlicePool[T any]() *SlicePool[T] { 11 | return new(SlicePool[T]) 12 | } 13 | 14 | func NewSlicePoolSize[T any](size int) *SlicePool[T] { 15 | return &SlicePool[T]{s: make([]T, 0, size)} 16 | } 17 | 18 | func (p *SlicePool[T]) Acquire() (v T, ok bool) { 19 | p.mu.Lock() 20 | defer p.mu.Unlock() 21 | 22 | l := len(p.s) 23 | if l == 0 { 24 | return v, false 25 | } 26 | 27 | v = p.s[l-1] 28 | p.s = p.s[:l-1] 29 | return v, true 30 | } 31 | 32 | func (p *SlicePool[T]) Release(v T) { 33 | p.mu.Lock() 34 | defer p.mu.Unlock() 35 | 36 | p.s = append(p.s, v) 37 | } 38 | --------------------------------------------------------------------------------