diff options
208 files changed, 5169 insertions, 19948 deletions
diff --git a/.dockerignore b/.dockerignore index b817b3c8..bfa82a3d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,4 +7,4 @@ /tests /bin composer.json -vendor_php +vendor_php
\ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 448a1aa4..448a1aa4 100644..100755 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8c4b568f..8c4b568f 100644..100755 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..6143fb25 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04208eeb..22a2440e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,62 +11,16 @@ on: - '**:**' jobs: - php: - name: Build (PHP ${{ matrix.php }}, ${{ matrix.setup }} setup) - runs-on: ubuntu-20.04 - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - php: [ '7.2', '7.3', '7.4', '8.0' ] - setup: [ basic, lowest ] - steps: - - name: Set up PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 # action page: <https://github.com/shivammathur/setup-php> - with: - php-version: ${{ matrix.php }} - - - name: Check out code - uses: actions/checkout@v2 - - - name: Syntax check only (lint) - run: find ./src/ ./tests/ -name "*.php" -print0 | xargs -0 -n1 -P8 php -l - - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Init Composer Cache # Docs: <https://git.io/JfAKn#php---composer> - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ matrix.setup }}-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install lowest Composer dependencies - if: matrix.setup == 'lowest' - run: composer update --prefer-dist --no-progress --prefer-lowest --ansi - - - name: Install basic Composer dependencies - if: matrix.setup == 'basic' - run: composer update --prefer-dist --no-progress --ansi - - - name: Analyze PHP sources - run: composer analyze - - # TODO write phpunit tests - #- name: Analyze PHP sources - # run: composer test - golang: - name: Build (Go ${{ matrix.go }}, PHP ${{ matrix.php }}) - runs-on: ubuntu-20.04 - timeout-minutes: 20 + name: Build (Go ${{ matrix.go }}, PHP ${{ matrix.php }}, OS ${{matrix.os}}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: fail-fast: false matrix: - php: [ '7.2', '7.3', '7.4', '8.0' ] + php: [ '7.4', '8.0' ] go: [ '1.14', '1.15' ] + os: [ ubuntu-20.04, windows-latest, macos-latest ] steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v2 # action page: <https://github.com/actions/setup-go> @@ -77,15 +31,18 @@ jobs: uses: shivammathur/setup-php@v2 # action page: <https://github.com/shivammathur/setup-php> with: php-version: ${{ matrix.php }} + extensions: sockets - name: Check out code uses: actions/checkout@v2 - name: Get Composer Cache Directory + if: ${{ matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest' }} id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Init Composer Cache # Docs: <https://git.io/JfAKn#php---composer> + if: ${{ matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest' }} uses: actions/cache@v2 with: path: ${{ steps.composer-cache.outputs.dir }} @@ -93,7 +50,7 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install Composer dependencies - run: composer update --prefer-dist --no-progress --ansi + run: cd tests && composer update --prefer-dist --no-progress --ansi - name: Init Go modules Cache # Docs: <https://git.io/JfAKn#go---modules> uses: actions/cache@v2 @@ -105,57 +62,60 @@ jobs: - name: Install Go dependencies run: go mod download - - name: Run golang tests + - name: Run golang tests on Windows without codecov + if: ${{ matrix.os == 'windows-latest' }} + run: | + go test -v -race -cover -tags=debug ./util + go test -v -race -cover -tags=debug ./pkg/pipe + go test -v -race -cover -tags=debug ./pkg/pool + go test -v -race -cover -tags=debug ./pkg/socket + go test -v -race -cover -tags=debug ./pkg/worker + + - name: Run golang tests on Linux and MacOS + if: ${{ matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest' }} run: | mkdir ./coverage-ci - go test -race -v -covermode=atomic -coverprofile=./coverage-ci/lib.txt - go test ./util -race -v -covermode=atomic -coverprofile=./coverage-ci/util.txt - go test ./service -race -v -covermode=atomic -coverprofile=./coverage-ci/service.txt - go test ./service/env -race -v -covermode=atomic -coverprofile=./coverage-ci/env.txt - go test ./service/rpc -race -v -covermode=atomic -coverprofile=./coverage-ci/rpc.txt - go test ./service/http -race -v -covermode=atomic -coverprofile=./coverage-ci/http.txt - go test ./service/static -race -v -covermode=atomic -coverprofile=./coverage-ci/static.txt - go test ./service/limit -race -v -covermode=atomic -coverprofile=./coverage-ci/limit.txt - go test ./service/headers -race -v -covermode=atomic -coverprofile=./coverage-ci/headers.txt - go test ./service/metrics -race -v -covermode=atomic -coverprofile=./coverage-ci/metrics.txt - go test ./service/health -race -v -covermode=atomic -coverprofile=./coverage-ci/health.txt - go test ./service/gzip -race -v -covermode=atomic -coverprofile=./coverage-ci/gzip.txt - # reload tests temporarily excluded from CI due to intensive work with the file system, and unpredictable GHA - # go test ./service/reload -race -v -covermode=atomic -coverprofile=./coverage-ci/reload.txt + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/util.txt -covermode=atomic ./util + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/pipe.txt -covermode=atomic ./pkg/pipe + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/pool.txt -covermode=atomic ./pkg/pool + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/socket.txt -covermode=atomic ./pkg/socket + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/worker.txt -covermode=atomic ./pkg/worker cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt - uses: codecov/codecov-action@v1 # Docs: <https://github.com/codecov/codecov-action> + if: ${{ matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest' }} with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage-ci/summary.txt fail_ci_if_error: false - golangci-check: + golangci-lint: name: Golang-CI (lint) runs-on: ubuntu-20.04 steps: - name: Check out code - uses: actions/checkout@v1 - - - name: golangci-lint - uses: reviewdog/action-golangci-lint@v1 # action page: <https://github.com/reviewdog/action-golangci-lint> - with: - github_token: ${{ secrets.github_token }} - - image: - name: Build docker image - runs-on: ubuntu-20.04 - timeout-minutes: 10 - steps: - - name: Check out code uses: actions/checkout@v2 - - name: Build image - run: docker build -t roadrunner:local -f Dockerfile . - - - name: Scan image - uses: anchore/scan-action@v2 # action page: <https://github.com/anchore/scan-action> + - name: Run linter + uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action> with: - image: roadrunner:local - fail-build: true - severity-cutoff: low # negligible, low, medium, high or critical + version: v1.33 # without patch version + only-new-issues: false # show only new issues if it's a pull request + +# image: +# name: Build docker image +# runs-on: ubuntu-20.04 +# timeout-minutes: 10 +# steps: +# - name: Check out code +# uses: actions/checkout@v2 + +# - name: Build image +# run: docker build -t roadrunner:local -f Dockerfile . + +# - name: Scan image +# uses: anchore/scan-action@v2 # action page: <https://github.com/anchore/scan-action> +# with: +# image: roadrunner:local +# fail-build: true +# severity-cutoff: low # negligible, low, medium, high or critical diff --git a/.gitignore b/.gitignore index 8a2f7d68..78c1ee17 100644..100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ .idea composer.lock vendor @@ -5,4 +22,6 @@ vendor_php builds/ tests/vendor/ .rr-sample.yaml -psr-worker.php +unit_tests +unit_tests_copied +dir1
\ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100755 index 00000000..208db670 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,62 @@ +run: + skip-files: + - plugins/http/tests/http_test.go + - plugins/http/tests/plugin_test_old.go + - plugins/http/tests/rpc_test_old.go + - plugins/http/tests/config_test.go + - plugins/static/tests/static_plugin_test.go + - plugins/headers/tests/old.go +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + # - dupl + - errcheck + - exhaustive + # - funlen + # - gochecknoinits + # - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + # - gomnd + - goprintffuncname + - gosec + # - gosimple + - govet + - ineffassign + - interfacer + # - lll + - misspell + - nakedret + # - noctx + - nolintlint + - rowserrcheck + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + # - unparam + # - unused + - varcheck + - whitespace + + # don't enable: + # - asciicheck + # - gochecknoglobals + # - gocognit + # - godot + # - godox + # - goerr113 + # - maligned + # - nestif + # - prealloc + # - testpackage + # - wsl @@ -1,200 +1,108 @@ -# defines environment variables for all underlying php processes -env: - key: value - -# rpc bus allows php application and external clients to talk to rr services. rpc: - # enable rpc server - enable: true - - # rpc connection DSN. Supported TCP and Unix sockets. listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php tests/psr-worker-bench.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +logs: + mode: development + level: error + +http: + debug: true + address: 127.0.0.1:44933 + maxRequestSize: 1024 + middleware: [ "gzip", "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 6 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + +# ssl: +# port: 8892 +# redirect: false +# cert: fixtures/server.crt +# key: fixtures/server.key +# # rootCa: root.crt +# fcgi: +# address: tcp://0.0.0.0:7921 +# http2: +# enabled: false +# h2c: false +# maxConcurrentStreams: 128 + +redis: + # UniversalClient is an abstract client which - based on the provided options - + # can connect to either clusters, or sentinel-backed failover instances + # or simple single-instance servers. This can be useful for testing + # cluster-specific applications locally. + # if the number of addrs is 1 and master_name is empty, a single-node redis Client will be returned + + # if the number of Addrs is two or more, a ClusterClient will be returned + addrs: + - 'localhost:6379' + # if a MasterName is passed a sentinel-backed FailoverClient will be returned + master_name: '' + username: '' + password: '' + db: 0 + sentinel_password: '' + route_by_latency: false + route_randomly: false + dial_timeout: 0 # accepted values [1s, 5m, 3h] + max_retries: 1 + min_retry_backoff: 0 # accepted values [1s, 5m, 3h] + max_retry_backoff: 0 # accepted values [1s, 5m, 3h] + pool_size: 0 + min_idle_conns: 0 + max_conn_age: 0 # accepted values [1s, 5m, 3h] + read_timeout: 0 # accepted values [1s, 5m, 3h] + write_timeout: 0 # accepted values [1s, 5m, 3h] + pool_timeout: 0 # accepted values [1s, 5m, 3h] + idle_timeout: 0 # accepted values [1s, 5m, 3h] + idle_check_freq: 0 # accepted values [1s, 5m, 3h] + read_only: false metrics: # prometheus client address (path /metrics added automatically) address: localhost:2112 - - # list of metrics to collect from application collect: - # metric name app_metric: - # type [gauge, counter, histogram, summary] type: histogram - - # short description help: "Custom application metric" - - # metric groups/tags labels: [ "type" ] - - # for histogram only buckets: [ 0.1, 0.2, 0.3, 1.0 ] - # objectives defines the quantile rank estimates with their respective # absolute error [ for summary only ] objectives: - 1.4: 2.3 - 2.0: 1.4 - -# http service configuration. -http: - # http host to listen. - address: 0.0.0.0:8080 - - ssl: - # custom https port (default 443) - port: 443 - - # force redirect to https connection - redirect: true - - # ssl cert - cert: server.crt - - # ssl private key - key: server.key - - # rootCA certificate - rootCa: root.crt - - # HTTP service provides FastCGI as frontend - fcgi: - # FastCGI connection DSN. Supported TCP and Unix sockets. - address: tcp://0.0.0.0:6920 - - # HTTP service provides HTTP2 transport - http2: - # enable HTTP/2, only with TSL - enabled: true - - # enable H2C on TCP connections - h2c: true - - # max transfer channels - maxConcurrentStreams: 128 - - # max POST request size, including file uploads in MB. - maxRequestSize: 200 - - # file upload configuration. - uploads: - # list of file extensions which are forbidden for uploading. - forbid: [ ".php", ".exe", ".bat" ] - - # cidr blocks which can set ip using X-Real-Ip or X-Forwarded-For - trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] - - # http worker pool configuration. - workers: - # php worker command. - command: "php psr-worker.php pipes" - - # connection method (pipes, tcp://:9000, unix://socket.unix). default "pipes" - relay: "pipes" - - # user under which process will be started - user: "" - - # worker pool configuration. - pool: - # number of workers to be serving. - numWorkers: 4 - - # maximum jobs per worker, 0 - unlimited. - maxJobs: 0 - - # for how long worker is allowed to be bootstrapped. - allocateTimeout: 60 - - # amount of time given to worker to gracefully destruct itself. - destroyTimeout: 60 - -# Additional HTTP headers and CORS control. -headers: - # Middleware to handle CORS requests, https://www.w3.org/TR/cors/ - cors: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - allowedOrigin: "*" - - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers - allowedHeaders: "*" - - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods - allowedMethods: "GET,POST,PUT,DELETE" - - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials - allowCredentials: true - - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers - exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" - - # Max allowed age in seconds - maxAge: 600 - - # Automatically add headers to every request passed to PHP. - request: - "Example-Request-Header": "Value" - - # Automatically add headers to every response. - response: - "X-Powered-By": "RoadRunner" - -# monitors rr server(s) -limit: - # check worker state each second - interval: 1 - - # custom watch configuration for each service - services: - # monitor http workers - http: - # maximum allowed memory consumption per worker (soft) - maxMemory: 100 - - # maximum time to live for the worker (soft) - TTL: 0 - - # maximum allowed amount of time worker can spend in idle before being removed (for weak db connections, soft) - idleTTL: 0 - - # max_execution_time (brutal) - execTTL: 60 - -# static file serving. remove this section to disable static file serving. -static: - # root directory for static file (http would not serve .php and .htaccess files). - dir: "public" - - # list of extensions for forbid for serving. - forbid: [ ".php", ".htaccess" ] - - # Automatically add headers to every request. - request: - "Example-Request-Header": "Value" - - # Automatically add headers to every response. - response: - "X-Powered-By": "RoadRunner" - -# health service configuration -health: - # http host to serve health requests. - address: localhost:2113 - -# reload can reset rr servers when files change reload: - # refresh internval (default 1s) + # sync interval interval: 1s - - # file extensions to watch, defaults to [.php] - patterns: [ ".php" ] - - # list of services to watch + # global patterns to sync + patterns: [ ".go" ] + # list of included for sync services services: http: - # list of dirs, "" root - dirs: [ "" ] - - # include sub directories - recursive: true
\ No newline at end of file + # recursive search for file patterns to add + recursive: true + # ignored folders + ignore: [ "vendor" ] + # service specific file pattens to sync + patterns: [ ".php", ".go",".md", ] + # directories to sync. If recursive is set to true, + # recursive sync will be applied only to the directories in `dirs` section + dirs: [ "." ]
\ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 28063ba5..a3f4a6cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,6 @@ CHANGELOG ========= -v1.9.1 (21.12.2020) ----------- -- Add `rr --version` flag support (thanks @tarampampam) -- Update `reload` plugin, remove unnecessary goroutines and locks. -- Add objectives to the metrics summary. -- Automated releases build. v1.9.0 (02.12.2020) ------------------- diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ae0b283a..ae0b283a 100644..100755 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -24,20 +24,11 @@ uninstall: ## Uninstall locally installed RR rm -f /usr/local/bin/rr test: ## Run application tests - test -d ./vendor_php || composer update --prefer-dist --ansi - go test -v -race -cover - go test -v -race -cover ./util - go test -v -race -cover ./service - go test -v -race -cover ./service/env - go test -v -race -cover ./service/rpc - go test -v -race -cover ./service/http - go test -v -race -cover ./service/static - go test -v -race -cover ./service/limit - go test -v -race -cover ./service/headers - go test -v -race -cover ./service/metrics - go test -v -race -cover ./service/health - go test -v -race -cover ./service/gzip - go test -v -race -cover ./service/reload + go test -v -race -cover -tags=debug -covermode=atomic ./util + go test -v -race -cover -tags=debug -covermode=atomic ./pkg/pipe + go test -v -race -cover -tags=debug -covermode=atomic ./pkg/pool + go test -v -race -cover -tags=debug -covermode=atomic ./pkg/socket + go test -v -race -cover -tags=debug -covermode=atomic ./pkg/worker lint: ## Run application linters go fmt ./... @@ -1,3 +1,5 @@ +[WIP] + <p align="center"> <img src="https://user-images.githubusercontent.com/796136/50286124-6f7f3780-046f-11e9-9f45-e8fedd4f786d.png" height="75px" alt="RoadRunner"> </p> @@ -13,7 +15,7 @@ </p> RoadRunner is an open-source (MIT licensed) high-performance PHP application server, load balancer, and process manager. -It supports running as a service with the ability to extend its functionality on a per-project basis. +It supports running as a service with the ability to extend its functionality on a per-project basis. RoadRunner includes PSR-7/PSR-17 compatible HTTP and HTTP/2 server and can be used to replace classic Nginx+FPM setup with much greater performance and flexibility. @@ -28,7 +30,7 @@ Features: - PCI DSS compliant - PSR-7 HTTP server (file uploads, error handling, static files, hot reload, middlewares, event listeners) - HTTPS and HTTP/2 support (including HTTP/2 Push, H2C) -- Fully customizable server, FastCGI support +- A Fully customizable server, FastCGI support - Flexible environment configuration - No external PHP dependencies (64bit version required), drop-in (based on [Goridge](https://github.com/spiral/goridge)) - Load balancer, process manager and task pipeline @@ -38,11 +40,11 @@ Features: - Automatic worker replacement and safe PHP process destruction - Worker create/allocate/destroy timeouts - Max jobs per worker -- Worker lifecycle management (controller) +- Worker lifecycle management (controller) - maxMemory (graceful stop) - TTL (graceful stop) - idleTTL (graceful stop) - - execTTL (brute, max_execution_time) + - execTTL (brute, max_execution_time) - Payload context and body - Protocol, worker and job level error management (including PHP errors) - Very fast (~250k rpc calls per second on Ryzen 1700X using 16 threads) @@ -53,48 +55,13 @@ Features: Installation: -------- -To install: ``` -$ composer require spiral/roadrunner -$ ./vendor/bin/rr get-binary +go get -u github.com/spiral/roadrunner/v2 ``` > For getting roadrunner binary file you can use our docker image: `spiralscout/roadrunner:X.X.X` (more information about image and tags can be found [here](https://hub.docker.com/r/spiralscout/roadrunner/)) -Extensions: --------- -| Extension | Current Status -| --- | --- -spiral/jobs | [![Latest Stable Version](https://poser.pugx.org/spiral/jobs/version)](https://packagist.org/packages/spiral/jobs) [![Build Status](https://travis-ci.org/spiral/jobs.svg?branch=master)](https://travis-ci.org/spiral/jobs) [![Codecov](https://codecov.io/gh/spiral/jobs/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/jobs/) -spiral/php-grpc | [![Latest Stable Version](https://poser.pugx.org/spiral/php-grpc/version)](https://packagist.org/packages/spiral/php-grpc) [![Build Status](https://travis-ci.org/spiral/php-grpc.svg?branch=master)](https://travis-ci.org/spiral/php-grpc) [![Codecov](https://codecov.io/gh/spiral/php-grpc/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/php-grpc/) -spiral/broadcast | [![Latest Stable Version](https://poser.pugx.org/spiral/broadcast/version)](https://packagist.org/packages/spiral/broadcast) [![Build Status](https://travis-ci.org/spiral/broadcast.svg?branch=master)](https://travis-ci.org/spiral/broadcast) [![Codecov](https://codecov.io/gh/spiral/broadcast/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/broadcast/) -spiral/broadcast-ws | [![Latest Stable Version](https://poser.pugx.org/spiral/broadcast-ws/version)](https://packagist.org/packages/spiral/broadcast-ws) [![Build Status](https://travis-ci.org/spiral/broadcast-ws.svg?branch=master)](https://travis-ci.org/spiral/broadcast-ws) [![Codecov](https://codecov.io/gh/spiral/broadcast-ws/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/broadcast-ws/) - -Example: --------- - -```php -<?php -// worker.php -ini_set('display_errors', 'stderr'); -include "vendor/autoload.php"; - -$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT); -$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay)); - -while ($req = $psr7->acceptRequest()) { - try { - $resp = new \Zend\Diactoros\Response(); - $resp->getBody()->write("hello world"); - - $psr7->respond($resp); - } catch (\Throwable $e) { - $psr7->getWorker()->error((string)$e); - } -} -``` - Configuration can be located in `.rr.yaml` file ([full sample](https://github.com/spiral/roadrunner/blob/master/.rr.yaml)): ```yaml @@ -115,4 +82,4 @@ $ ./rr serve -v -d License: -------- -The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com). +The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com).
\ No newline at end of file diff --git a/bors.toml b/bors.toml index 9d409023..356b17be 100644..100755 --- a/bors.toml +++ b/bors.toml @@ -1,18 +1,16 @@ status = [ - 'Build (PHP 7.2, basic setup)', - 'Build (PHP 7.2, lowest setup)', - 'Build (PHP 7.3, basic setup)', - 'Build (PHP 7.3, lowest setup)', - 'Build (PHP 8.0, basic setup)', - 'Build (PHP 8.0, lowest setup)', - 'Build (Go 1.14, PHP 7.2)', - 'Build (Go 1.15, PHP 7.2)', - 'Build (Go 1.14, PHP 7.3)', - 'Build (Go 1.15, PHP 7.3)', - 'Build (Go 1.14, PHP 7.4)', - 'Build (Go 1.15, PHP 7.4)', - 'Build (Go 1.14, PHP 8.0)', - 'Build (Go 1.15, PHP 8.0)', + 'Build (Go 1.14, PHP 7.4, OS ubuntu-latest)', + 'Build (Go 1.14, PHP 7.4, OS windows-latest)', + 'Build (Go 1.14, PHP 7.4, OS macos-latest)', + 'Build (Go 1.15, PHP 7.4, OS ubuntu-latest)', + 'Build (Go 1.15, PHP 7.4, OS windows-latest)', + 'Build (Go 1.15, PHP 7.4, OS macos-latest)', + 'Build (Go 1.14, PHP 8.0, OS ubuntu-latest)', + 'Build (Go 1.14, PHP 8.0, OS windows-latest)', + 'Build (Go 1.14, PHP 8.0, OS macos-latest)', + 'Build (Go 1.15, PHP 8.0, OS ubuntu-latest)', + 'Build (Go 1.15, PHP 8.0, OS windows-latest)', + 'Build (Go 1.15, PHP 8.0, OS macos-latest)', 'Golang-CI (lint)', 'Build docker image', ] diff --git a/cmd/cli/reset.go b/cmd/cli/reset.go new file mode 100644 index 00000000..03b470e5 --- /dev/null +++ b/cmd/cli/reset.go @@ -0,0 +1,104 @@ +package cli + +import ( + "fmt" + "sync" + + "github.com/fatih/color" + "github.com/mattn/go-runewidth" + "github.com/spf13/cobra" + "github.com/spiral/errors" + "github.com/vbauerster/mpb/v5" + "github.com/vbauerster/mpb/v5/decor" +) + +const List string = "resetter.List" +const Reset string = "resetter.Reset" + +func init() { + root.AddCommand(&cobra.Command{ + Use: "reset", + Short: "Reset workers of all or specific RoadRunner service", + RunE: resetHandler, + }) +} + +func resetHandler(cmd *cobra.Command, args []string) error { + const op = errors.Op("reset handler") + client, err := RPCClient() + if err != nil { + return err + } + defer func() { + _ = client.Close() + }() + + var services []string + if len(args) != 0 { + services = args + } else { + err = client.Call(List, true, &services) + if err != nil { + return errors.E(op, err) + } + } + + var wg sync.WaitGroup + pr := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(6)) + wg.Add(len(services)) + + for _, service := range services { + var ( + bar *mpb.Bar + name = runewidth.FillRight(fmt.Sprintf("Resetting plugin: [%s]", color.HiYellowString(service)), 27) + result = make(chan interface{}) + ) + + bar = pr.AddSpinner( + 1, + mpb.SpinnerOnMiddle, + mpb.SpinnerStyle([]string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(onComplete(result)), + ) + + // simulating some work + go func(service string, result chan interface{}) { + defer wg.Done() + defer bar.Increment() + + var done bool + err = client.Call(Reset, service, &done) + if err != nil { + result <- errors.E(op, err) + return + } + result <- nil + }(service, result) + } + + pr.Wait() + return nil +} + +func onComplete(result chan interface{}) decor.Decorator { + var ( + msg = "" + fn = func(s decor.Statistics) string { + select { + case r := <-result: + if err, ok := r.(error); ok { + msg = color.HiRedString(err.Error()) + return msg + } + + msg = color.HiGreenString("done") + return msg + default: + return msg + } + } + ) + + return decor.Any(fn) +} diff --git a/cmd/cli/root.go b/cmd/cli/root.go new file mode 100644 index 00000000..febe410b --- /dev/null +++ b/cmd/cli/root.go @@ -0,0 +1,97 @@ +package cli + +import ( + "log" + "net/rpc" + "os" + "path/filepath" + + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" + + "github.com/spiral/roadrunner-plugins/config" + + "github.com/spf13/cobra" + "github.com/spiral/endure" +) + +var ( + WorkDir string + CfgFile string + Container *endure.Endure + cfg *config.Viper + root = &cobra.Command{ + Use: "rr", + SilenceErrors: true, + SilenceUsage: true, + } +) + +func Execute() { + if err := root.Execute(); err != nil { + // exit with error, fatal invoke os.Exit(1) + log.Fatal(err) + } +} + +func init() { + root.PersistentFlags().StringVarP(&CfgFile, "config", "c", ".rr.yaml", "config file (default is .rr.yaml)") + root.PersistentFlags().StringVarP(&WorkDir, "WorkDir", "w", "", "work directory") + + cobra.OnInitialize(func() { + if CfgFile != "" { + if absPath, err := filepath.Abs(CfgFile); err == nil { + CfgFile = absPath + + // force working absPath related to config file + if err := os.Chdir(filepath.Dir(absPath)); err != nil { + panic(err) + } + } + } + + if WorkDir != "" { + if err := os.Chdir(WorkDir); err != nil { + panic(err) + } + } + + cfg = &config.Viper{} + cfg.Path = CfgFile + cfg.Prefix = "rr" + + // register config + err := Container.Register(cfg) + if err != nil { + panic(err) + } + }) +} + +// RPCClient is using to make a requests to the ./rr reset, ./rr workers +func RPCClient() (*rpc.Client, error) { + rpcConfig := &rpcPlugin.Config{} + + err := cfg.Init() + if err != nil { + return nil, err + } + + if !cfg.Has(rpcPlugin.PluginName) { + return nil, errors.E("rpc service disabled") + } + + err = cfg.UnmarshalKey(rpcPlugin.PluginName, rpcConfig) + if err != nil { + return nil, err + } + rpcConfig.InitDefaults() + + conn, err := rpcConfig.Dialer() + if err != nil { + return nil, err + } + + return rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)), nil +} diff --git a/cmd/cli/serve.go b/cmd/cli/serve.go new file mode 100644 index 00000000..2fe54932 --- /dev/null +++ b/cmd/cli/serve.go @@ -0,0 +1,63 @@ +package cli + +import ( + "log" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + "github.com/spiral/errors" + "go.uber.org/multierr" +) + +func init() { + root.AddCommand(&cobra.Command{ + Use: "serve", + Short: "Start RoadRunner server", + RunE: handler, + }) +} + +func handler(cmd *cobra.Command, args []string) error { + const op = errors.Op("handle serve command") + /* + We need to have path to the config at the RegisterTarget stage + But after cobra.Execute, because cobra fills up cli variables on this stage + */ + + err := Container.Init() + if err != nil { + return errors.E(op, err) + } + + errCh, err := Container.Serve() + if err != nil { + return errors.E(op, err) + } + + // https://golang.org/pkg/os/signal/#Notify + // should be of buffer size at least 1 + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + for { + select { + case e := <-errCh: + err = multierr.Append(err, e.Error) + log.Printf("error occurred: %v, service: %s", e.Error.Error(), e.VertexID) + er := Container.Stop() + if er != nil { + err = multierr.Append(err, er) + return errors.E(op, err) + } + return errors.E(op, err) + case <-c: + err = Container.Stop() + if err != nil { + return errors.E(op, err) + } + return nil + } + } +} diff --git a/cmd/cli/workers.go b/cmd/cli/workers.go new file mode 100644 index 00000000..d34de40a --- /dev/null +++ b/cmd/cli/workers.go @@ -0,0 +1,118 @@ +package cli + +import ( + "fmt" + "log" + "net/rpc" + "os" + "os/signal" + "syscall" + "time" + + tm "github.com/buger/goterm" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spiral/errors" + "github.com/spiral/roadrunner-plugins/informer" + "github.com/spiral/roadrunner/v2/tools" +) + +var ( + interactive bool +) + +const InformerList string = "informer.List" + +func init() { + workersCommand := &cobra.Command{ + Use: "workers", + Short: "Show information about active roadrunner workers", + RunE: workersHandler, + } + + workersCommand.Flags().BoolVarP( + &interactive, + "interactive", + "i", + false, + "render interactive workers table", + ) + + root.AddCommand(workersCommand) +} + +func workersHandler(cmd *cobra.Command, args []string) error { + const op = errors.Op("workers handler") + // get RPC client + client, err := RPCClient() + if err != nil { + return err + } + defer func() { + err := client.Close() + if err != nil { + log.Printf("error when closing RPCClient: error %v", err) + } + }() + + var plugins []string + // assume user wants to show workers from particular plugin + if len(args) != 0 { + plugins = args + } else { + err = client.Call(InformerList, true, &plugins) + if err != nil { + return errors.E(op, err) + } + } + + if !interactive { + return showWorkers(plugins, client) + } + + // https://golang.org/pkg/os/signal/#Notify + // should be of buffer size at least 1 + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + tm.Clear() + tt := time.NewTicker(time.Second) + defer tt.Stop() + for { + select { + case <-c: + return nil + case <-tt.C: + tm.MoveCursor(1, 1) + err := showWorkers(plugins, client) + if err != nil { + return errors.E(op, err) + } + tm.Flush() + } + } +} + +func showWorkers(plugins []string, client *rpc.Client) error { + for _, plugin := range plugins { + list := &informer.WorkerList{} + err := client.Call("informer.Workers", plugin, &list) + if err != nil { + return err + } + + // it's a golang :) + ps := make([]tools.ProcessState, len(list.Workers)) + for i := 0; i < len(list.Workers); i++ { + ps[i].Created = list.Workers[i].Created + ps[i].NumJobs = list.Workers[i].NumJobs + ps[i].MemoryUsage = list.Workers[i].MemoryUsage + ps[i].Pid = list.Workers[i].Pid + ps[i].Status = list.Workers[i].Status + } + + fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) + tools.WorkerTable(os.Stdout, ps).Render() + } + return nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..8151d4fe --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner-plugins/http" + "github.com/spiral/roadrunner-plugins/informer" + "github.com/spiral/roadrunner-plugins/logger" + "github.com/spiral/roadrunner-plugins/metrics" + "github.com/spiral/roadrunner-plugins/redis" + "github.com/spiral/roadrunner-plugins/reload" + "github.com/spiral/roadrunner-plugins/resetter" + "github.com/spiral/roadrunner-plugins/rpc" + "github.com/spiral/roadrunner-plugins/server" + "github.com/spiral/roadrunner/v2/cmd/cli" +) + +func main() { + var err error + cli.Container, err = endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel), endure.RetryOnFail(false)) + if err != nil { + log.Fatal(err) + } + + err = cli.Container.RegisterAll( + // logger plugin + &logger.ZapLogger{}, + // metrics plugin + &metrics.Plugin{}, + // redis plugin (internal) + &redis.Plugin{}, + // http server plugin + &http.Plugin{}, + // reload plugin + &reload.Plugin{}, + // informer plugin (./rr workers) + &informer.Plugin{}, + // resetter plugin (./rr reset) + &resetter.Plugin{}, + // rpc plugin (workers, reset) + &rpc.Plugin{}, + // server plugin (NewWorker, NewWorkerPool) + &server.Plugin{}, + ) + if err != nil { + log.Fatal(err) + } + + cli.Execute() +} diff --git a/cmd/rr/LICENSE b/cmd/rr/LICENSE deleted file mode 100644 index efb98c87..00000000 --- a/cmd/rr/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 SpiralScout - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.
\ No newline at end of file diff --git a/cmd/rr/cmd/root.go b/cmd/rr/cmd/root.go deleted file mode 100644 index e67a4e62..00000000 --- a/cmd/rr/cmd/root.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package cmd - -import ( - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spiral/roadrunner/cmd/util" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/limit" - "log" - "net/http" - "net/http/pprof" - "os" -) - -// Services bus for all the commands. -var ( - cfgFile, workDir, logFormat string - override []string - mergeJson string - - // Verbose enables verbosity mode (container specific). - Verbose bool - - // Debug enables debug mode (service specific). - Debug bool - - // Logger - shared logger. - Logger = logrus.New() - - // Container - shared service bus. - Container = service.NewContainer(Logger) - - // CLI is application endpoint. - CLI = &cobra.Command{ - Use: "rr", - SilenceErrors: true, - SilenceUsage: true, - Version: Version, // allows to use `--version` flag - Short: util.Sprintf( - "<green>RoadRunner</reset>, PHP Application Server\nVersion: <yellow+hb>%s</reset>, %s", - Version, - BuildTime, - ), - } -) - -// Execute adds all child commands to the CLI command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the CLI. -func Execute() { - if err := CLI.Execute(); err != nil { - util.ExitWithError(err) - } -} - -func init() { - CLI.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") - CLI.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "debug mode") - CLI.PersistentFlags().StringVarP(&logFormat, "logFormat", "l", "color", "select log formatter (color, json, plain)") - CLI.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is .rr.yaml)") - CLI.PersistentFlags().StringVarP(&workDir, "workDir", "w", "", "work directory") - CLI.PersistentFlags().StringVarP(&mergeJson, "jsonConfig", "j", "", "merge json configuration") - - CLI.PersistentFlags().StringArrayVarP( - &override, - "override", - "o", - nil, - "override config value (dot.notation=value)", - ) - - cobra.OnInitialize(func() { - if Verbose { - Logger.SetLevel(logrus.DebugLevel) - } - - configureLogger(logFormat) - - cfg, err := util.LoadConfig(cfgFile, []string{"."}, ".rr", override, mergeJson) - if err != nil { - Logger.Warnf("config: %s", err) - return - } - - if workDir != "" { - if err := os.Chdir(workDir); err != nil { - util.ExitWithError(err) - } - } - - if err := Container.Init(cfg); err != nil { - util.ExitWithError(err) - } - - // global watcher config - if Verbose { - wcv, _ := Container.Get(limit.ID) - if wcv, ok := wcv.(*limit.Service); ok { - wcv.AddListener(func(event int, ctx interface{}) { - util.LogEvent(Logger, event, ctx) - }) - } - } - - // if debug --> also run pprof service - if Debug { - go runDebugServer() - } - }) -} -func runDebugServer() { - mux := http.NewServeMux() - mux.HandleFunc("/debug/pprof/", pprof.Index) - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - srv := http.Server{ - Addr: ":6061", - Handler: mux, - } - - if err := srv.ListenAndServe(); err != nil { - log.Fatal(err) - } -} - -func configureLogger(format string) { - util.Colorize = false - switch format { - case "color", "default": - util.Colorize = true - Logger.Formatter = &logrus.TextFormatter{ForceColors: true} - case "plain": - Logger.Formatter = &logrus.TextFormatter{DisableColors: true} - case "json": - Logger.Formatter = &logrus.JSONFormatter{} - } -} diff --git a/cmd/rr/cmd/serve.go b/cmd/rr/cmd/serve.go deleted file mode 100644 index cafbdd4f..00000000 --- a/cmd/rr/cmd/serve.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package cmd - -import ( - "github.com/spf13/cobra" - "os" - "os/signal" - "sync" - "syscall" -) - -func init() { - CLI.AddCommand(&cobra.Command{ - Use: "serve", - Short: "Serve RoadRunner service(s)", - RunE: serveHandler, - }) -} - -func serveHandler(cmd *cobra.Command, args []string) error { - // https://golang.org/pkg/os/signal/#Notify - // should be of buffer size at least 1 - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - - wg := &sync.WaitGroup{} - - wg.Add(1) - go func() { - defer wg.Done() - // get the signal - <-c - Container.Stop() - }() - - // blocking operation - if err := Container.Serve(); err != nil { - return err - } - - wg.Wait() - - return nil -} diff --git a/cmd/rr/cmd/stop.go b/cmd/rr/cmd/stop.go deleted file mode 100644 index 7b4794e7..00000000 --- a/cmd/rr/cmd/stop.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/spiral/roadrunner/cmd/util" -) - -func init() { - CLI.AddCommand(&cobra.Command{ - Use: "stop", - Short: "Stop RoadRunner server", - RunE: stopHandler, - }) -} - -func stopHandler(cmd *cobra.Command, args []string) error { - client, err := util.RPCClient(Container) - if err != nil { - return err - } - - util.Printf("<green>Stopping RoadRunner</reset>: ") - - var r string - if err := client.Call("system.Stop", true, &r); err != nil { - return err - } - - util.Printf("<green+hb>done</reset>\n") - return client.Close() -} diff --git a/cmd/rr/cmd/version.go b/cmd/rr/cmd/version.go deleted file mode 100644 index a550c682..00000000 --- a/cmd/rr/cmd/version.go +++ /dev/null @@ -1,9 +0,0 @@ -package cmd - -var ( - // Version - defines build version. - Version = "local" - - // BuildTime - defined build time. - BuildTime = "development" -) diff --git a/cmd/rr/http/debug.go b/cmd/rr/http/debug.go deleted file mode 100644 index ae383e8d..00000000 --- a/cmd/rr/http/debug.go +++ /dev/null @@ -1,138 +0,0 @@ -package http - -import ( - "fmt" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spiral/roadrunner" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/cmd/util" - rrhttp "github.com/spiral/roadrunner/service/http" - "net" - "net/http" - "strings" - "time" -) - -func init() { - cobra.OnInitialize(func() { - if rr.Debug { - svc, _ := rr.Container.Get(rrhttp.ID) - if svc, ok := svc.(*rrhttp.Service); ok { - svc.AddListener((&debugger{logger: rr.Logger}).listener) - } - } - }) -} - -// listener provide debug callback for system events. With colors! -type debugger struct{ logger *logrus.Logger } - -// listener listens to http events and generates nice looking output. -func (s *debugger) listener(event int, ctx interface{}) { - if util.LogEvent(s.logger, event, ctx) { - // handler by default debug package - return - } - - // http events - switch event { - case rrhttp.EventResponse: - e := ctx.(*rrhttp.ResponseEvent) - s.logger.Info(util.Sprintf( - "<cyan+h>%s</reset> %s %s <white+hb>%s</reset> %s", - e.Request.RemoteAddr, - elapsed(e.Elapsed()), - statusColor(e.Response.Status), - e.Request.Method, - e.Request.URI, - )) - - case rrhttp.EventError: - e := ctx.(*rrhttp.ErrorEvent) - - if _, ok := e.Error.(roadrunner.JobError); ok { - s.logger.Info(util.Sprintf( - "<cyan+h>%s</reset> %s %s <white+hb>%s</reset> %s", - addr(e.Request.RemoteAddr), - elapsed(e.Elapsed()), - statusColor(500), - e.Request.Method, - uri(e.Request), - )) - } else { - s.logger.Info(util.Sprintf( - "<cyan+h>%s</reset> %s %s <white+hb>%s</reset> %s <red>%s</reset>", - addr(e.Request.RemoteAddr), - elapsed(e.Elapsed()), - statusColor(500), - e.Request.Method, - uri(e.Request), - e.Error, - )) - } - } -} - -func statusColor(status int) string { - if status < 300 { - return util.Sprintf("<green>%v</reset>", status) - } - - if status < 400 { - return util.Sprintf("<cyan>%v</reset>", status) - } - - if status < 500 { - return util.Sprintf("<yellow>%v</reset>", status) - } - - return util.Sprintf("<red>%v</reset>", status) -} - -func uri(r *http.Request) string { - if r.TLS != nil { - return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) - } - - return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) -} - -// fits duration into 5 characters -func elapsed(d time.Duration) string { - var v string - switch { - case d > 100*time.Second: - v = fmt.Sprintf("%.1fs", d.Seconds()) - case d > 10*time.Second: - v = fmt.Sprintf("%.2fs", d.Seconds()) - case d > time.Second: - v = fmt.Sprintf("%.3fs", d.Seconds()) - case d > 100*time.Millisecond: - v = fmt.Sprintf("%.0fms", d.Seconds()*1000) - case d > 10*time.Millisecond: - v = fmt.Sprintf("%.1fms", d.Seconds()*1000) - default: - v = fmt.Sprintf("%.2fms", d.Seconds()*1000) - } - - if d > time.Second { - return util.Sprintf("<red>{%v}</reset>", v) - } - - if d > time.Millisecond*500 { - return util.Sprintf("<yellow>{%v}</reset>", v) - } - - return util.Sprintf("<gray+hb>{%v}</reset>", v) -} - -func addr(addr string) string { - // otherwise, return remote address as is - if !strings.ContainsRune(addr, ':') { - return addr - } - - addr, _, _ = net.SplitHostPort(addr) - return addr -} diff --git a/cmd/rr/http/metrics.go b/cmd/rr/http/metrics.go deleted file mode 100644 index 21bbbaf1..00000000 --- a/cmd/rr/http/metrics.go +++ /dev/null @@ -1,123 +0,0 @@ -package http - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/cobra" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/spiral/roadrunner/service/metrics" - "github.com/spiral/roadrunner/util" - "strconv" - "time" -) - -func init() { - cobra.OnInitialize(func() { - svc, _ := rr.Container.Get(metrics.ID) - mtr, ok := svc.(*metrics.Service) - if !ok || !mtr.Enabled() { - return - } - - ht, _ := rr.Container.Get(rrhttp.ID) - if ht, ok := ht.(*rrhttp.Service); ok { - collector := newCollector() - - // register metrics - mtr.MustRegister(collector.requestCounter) - mtr.MustRegister(collector.requestDuration) - mtr.MustRegister(collector.workersMemory) - - // collect events - ht.AddListener(collector.listener) - - // update memory usage every 10 seconds - go collector.collectMemory(ht, time.Second*10) - } - }) -} - -// listener provide debug callback for system events. With colors! -type metricCollector struct { - requestCounter *prometheus.CounterVec - requestDuration *prometheus.HistogramVec - workersMemory prometheus.Gauge -} - -func newCollector() *metricCollector { - return &metricCollector{ - requestCounter: prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "rr_http_request_total", - Help: "Total number of handled http requests after server restart.", - }, - []string{"status"}, - ), - requestDuration: prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "rr_http_request_duration_seconds", - Help: "HTTP request duration.", - }, - []string{"status"}, - ), - workersMemory: prometheus.NewGauge( - prometheus.GaugeOpts{ - Name: "rr_http_workers_memory_bytes", - Help: "Memory usage by HTTP workers.", - }, - ), - } -} - -// listener listens to http events and generates nice looking output. -func (c *metricCollector) listener(event int, ctx interface{}) { - // http events - switch event { - case rrhttp.EventResponse: - e := ctx.(*rrhttp.ResponseEvent) - - c.requestCounter.With(prometheus.Labels{ - "status": strconv.Itoa(e.Response.Status), - }).Inc() - - c.requestDuration.With(prometheus.Labels{ - "status": strconv.Itoa(e.Response.Status), - }).Observe(e.Elapsed().Seconds()) - - case rrhttp.EventError: - e := ctx.(*rrhttp.ErrorEvent) - - c.requestCounter.With(prometheus.Labels{ - "status": "500", - }).Inc() - - c.requestDuration.With(prometheus.Labels{ - "status": "500", - }).Observe(e.Elapsed().Seconds()) - } -} - -// collect memory usage by server workers -func (c *metricCollector) collectMemory(service *rrhttp.Service, tick time.Duration) { - started := false - for { - server := service.Server() - if server == nil && started { - // stopped - return - } - - started = true - - if workers, err := util.ServerState(server); err == nil { - sum := 0.0 - for _, w := range workers { - sum = sum + float64(w.MemoryUsage) - } - - c.workersMemory.Set(sum) - } - - time.Sleep(tick) - } -} diff --git a/cmd/rr/http/reset.go b/cmd/rr/http/reset.go deleted file mode 100644 index 3008848a..00000000 --- a/cmd/rr/http/reset.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package http - -import ( - "github.com/spf13/cobra" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/cmd/util" -) - -func init() { - rr.CLI.AddCommand(&cobra.Command{ - Use: "http:reset", - Short: "Reload RoadRunner worker pool for the HTTP service", - RunE: reloadHandler, - }) -} - -func reloadHandler(cmd *cobra.Command, args []string) error { - client, err := util.RPCClient(rr.Container) - if err != nil { - return err - } - defer client.Close() - - util.Printf("<green>Restarting http worker pool</reset>: ") - - var r string - if err := client.Call("http.Reset", true, &r); err != nil { - return err - } - - util.Printf("<green+hb>done</reset>\n") - return nil -} diff --git a/cmd/rr/http/workers.go b/cmd/rr/http/workers.go deleted file mode 100644 index 4444b87f..00000000 --- a/cmd/rr/http/workers.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package http - -import ( - tm "github.com/buger/goterm" - "github.com/spf13/cobra" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/cmd/util" - "github.com/spiral/roadrunner/service/http" - "net/rpc" - "os" - "os/signal" - "syscall" - "time" -) - -var ( - interactive bool - stopSignal = make(chan os.Signal, 1) -) - -func init() { - workersCommand := &cobra.Command{ - Use: "http:workers", - Short: "List workers associated with RoadRunner HTTP service", - RunE: workersHandler, - } - - workersCommand.Flags().BoolVarP( - &interactive, - "interactive", - "i", - false, - "render interactive workers table", - ) - - rr.CLI.AddCommand(workersCommand) - - signal.Notify(stopSignal, syscall.SIGTERM) - signal.Notify(stopSignal, syscall.SIGINT) -} - -func workersHandler(cmd *cobra.Command, args []string) (err error) { - defer func() { - if r, ok := recover().(error); ok { - err = r - } - }() - - client, err := util.RPCClient(rr.Container) - if err != nil { - return err - } - defer client.Close() - - if !interactive { - showWorkers(client) - return nil - } - - tm.Clear() - for { - select { - case <-stopSignal: - return nil - case <-time.NewTicker(time.Millisecond * 500).C: - tm.MoveCursor(1, 1) - showWorkers(client) - tm.Flush() - } - } -} - -func showWorkers(client *rpc.Client) { - var r http.WorkerList - if err := client.Call("http.Workers", true, &r); err != nil { - panic(err) - } - - util.WorkerTable(r.Workers).Render() -} diff --git a/cmd/rr/limit/debug.go b/cmd/rr/limit/debug.go deleted file mode 100644 index b9d919dc..00000000 --- a/cmd/rr/limit/debug.go +++ /dev/null @@ -1,71 +0,0 @@ -package limit - -import ( - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spiral/roadrunner" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/cmd/util" - "github.com/spiral/roadrunner/service/limit" -) - -func init() { - cobra.OnInitialize(func() { - if rr.Debug { - svc, _ := rr.Container.Get(limit.ID) - if svc, ok := svc.(*limit.Service); ok { - svc.AddListener((&debugger{logger: rr.Logger}).listener) - } - } - }) -} - -// listener provide debug callback for system events. With colors! -type debugger struct{ logger *logrus.Logger } - -// listener listens to http events and generates nice looking output. -func (s *debugger) listener(event int, ctx interface{}) { - if util.LogEvent(s.logger, event, ctx) { - // handler by default debug package - return - } - - // watchers - switch event { - case limit.EventTTL: - w := ctx.(roadrunner.WorkerError) - s.logger.Debug(util.Sprintf( - "<white+hb>worker.%v</reset> <yellow>%s</reset>", - *w.Worker.Pid, - w.Caused, - )) - return - - case limit.EventIdleTTL: - w := ctx.(roadrunner.WorkerError) - s.logger.Debug(util.Sprintf( - "<white+hb>worker.%v</reset> <yellow>%s</reset>", - *w.Worker.Pid, - w.Caused, - )) - return - - case limit.EventMaxMemory: - w := ctx.(roadrunner.WorkerError) - s.logger.Error(util.Sprintf( - "<white+hb>worker.%v</reset> <red>%s</reset>", - *w.Worker.Pid, - w.Caused, - )) - return - - case limit.EventExecTTL: - w := ctx.(roadrunner.WorkerError) - s.logger.Error(util.Sprintf( - "<white+hb>worker.%v</reset> <red>%s</reset>", - *w.Worker.Pid, - w.Caused, - )) - return - } -} diff --git a/cmd/rr/limit/metrics.go b/cmd/rr/limit/metrics.go deleted file mode 100644 index 947f53fe..00000000 --- a/cmd/rr/limit/metrics.go +++ /dev/null @@ -1,63 +0,0 @@ -package limit - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/cobra" - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - rrlimit "github.com/spiral/roadrunner/service/limit" - "github.com/spiral/roadrunner/service/metrics" -) - -func init() { - cobra.OnInitialize(func() { - svc, _ := rr.Container.Get(metrics.ID) - mtr, ok := svc.(*metrics.Service) - if !ok || !mtr.Enabled() { - return - } - - ht, _ := rr.Container.Get(rrlimit.ID) - if ht, ok := ht.(*rrlimit.Service); ok { - collector := newCollector() - - // register metrics - mtr.MustRegister(collector.maxMemory) - - // collect events - ht.AddListener(collector.listener) - } - }) -} - -// listener provide debug callback for system events. With colors! -type metricCollector struct { - maxMemory prometheus.Counter - maxExecutionTime prometheus.Counter -} - -func newCollector() *metricCollector { - return &metricCollector{ - maxMemory: prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "rr_limit_max_memory", - Help: "Total number of workers that was killed because they reached max memory limit.", - }, - ), - maxExecutionTime: prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "rr_limit_max_execution_time", - Help: "Total number of workers that was killed because they reached max execution time limit.", - }, - ), - } -} - -// listener listens to http events and generates nice looking output. -func (c *metricCollector) listener(event int, ctx interface{}) { - switch event { - case rrlimit.EventMaxMemory: - c.maxMemory.Inc() - case rrlimit.EventExecTTL: - c.maxExecutionTime.Inc() - } -} diff --git a/cmd/rr/main.go b/cmd/rr/main.go deleted file mode 100644 index 54a1f060..00000000 --- a/cmd/rr/main.go +++ /dev/null @@ -1,59 +0,0 @@ -// MIT License -// -// Copyright (c) 2018 SpiralScout -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package main - -import ( - rr "github.com/spiral/roadrunner/cmd/rr/cmd" - - // services (plugins) - "github.com/spiral/roadrunner/service/env" - "github.com/spiral/roadrunner/service/gzip" - "github.com/spiral/roadrunner/service/headers" - "github.com/spiral/roadrunner/service/health" - "github.com/spiral/roadrunner/service/http" - "github.com/spiral/roadrunner/service/limit" - "github.com/spiral/roadrunner/service/metrics" - "github.com/spiral/roadrunner/service/reload" - "github.com/spiral/roadrunner/service/rpc" - "github.com/spiral/roadrunner/service/static" - - // additional commands and debug handlers - _ "github.com/spiral/roadrunner/cmd/rr/http" - _ "github.com/spiral/roadrunner/cmd/rr/limit" -) - -func main() { - rr.Container.Register(env.ID, &env.Service{}) - rr.Container.Register(rpc.ID, &rpc.Service{}) - rr.Container.Register(http.ID, &http.Service{}) - rr.Container.Register(metrics.ID, &metrics.Service{}) - rr.Container.Register(headers.ID, &headers.Service{}) - rr.Container.Register(static.ID, &static.Service{}) - rr.Container.Register(limit.ID, &limit.Service{}) - rr.Container.Register(health.ID, &health.Service{}) - rr.Container.Register(gzip.ID, &gzip.Service{}) - rr.Container.Register(reload.ID, &reload.Service{}) - - // you can register additional commands using cmd.CLI - rr.Execute() -} diff --git a/cmd/util/config.go b/cmd/util/config.go deleted file mode 100644 index 08e01a89..00000000 --- a/cmd/util/config.go +++ /dev/null @@ -1,181 +0,0 @@ -package util - -import ( - "bytes" - "fmt" - "github.com/spf13/viper" - "github.com/spiral/roadrunner/service" - "os" - "path/filepath" - "strings" -) - -// ConfigWrapper provides interface bridge between v configs and service.Config. -type ConfigWrapper struct { - v *viper.Viper -} - -// Get nested config section (sub-map), returns nil if section not found. -func (w *ConfigWrapper) Get(key string) service.Config { - sub := w.v.Sub(key) - if sub == nil { - return nil - } - - return &ConfigWrapper{sub} -} - -// Unmarshal unmarshal config data into given struct. -func (w *ConfigWrapper) Unmarshal(out interface{}) error { - return w.v.Unmarshal(out) -} - -// LoadConfig config and merge it's values with set of flags. -func LoadConfig(cfgFile string, path []string, name string, flags []string, jsonConfig string) (*ConfigWrapper, error) { - cfg := viper.New() - - if cfgFile != "" { - if absPath, err := filepath.Abs(cfgFile); err == nil { - cfgFile = absPath - - // force working absPath related to config file - if err := os.Chdir(filepath.Dir(absPath)); err != nil { - return nil, err - } - } - - // Use cfg file from the flag. - cfg.SetConfigFile(cfgFile) - - if dir, err := filepath.Abs(cfgFile); err == nil { - // force working absPath related to config file - if err := os.Chdir(filepath.Dir(dir)); err != nil { - return nil, err - } - } - } else { - // automatic location - for _, p := range path { - cfg.AddConfigPath(p) - } - - cfg.SetConfigName(name) - } - - // read in environment variables that match - cfg.AutomaticEnv() - cfg.SetEnvPrefix("rr") - cfg.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - - // If a cfg file is found, read it in. - if err := cfg.ReadInConfig(); err != nil { - if len(flags) == 0 && jsonConfig == "" { - return nil, err - } - } - - // merge included configs - if include, ok := cfg.Get("include").([]interface{}); ok { - for _, file := range include { - filename, ok := file.(string) - if !ok { - continue - } - - partial := viper.New() - partial.AutomaticEnv() - partial.SetEnvPrefix("rr") - partial.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - partial.SetConfigFile(filename) - - if err := partial.ReadInConfig(); err != nil { - return nil, err - } - - // merging - if err := cfg.MergeConfigMap(partial.AllSettings()); err != nil { - return nil, err - } - } - } - - // automatically inject ENV variables using ${ENV} pattern - for _, key := range cfg.AllKeys() { - val := cfg.Get(key) - cfg.Set(key, parseEnv(val)) - } - - // merge with console flags - if len(flags) != 0 { - for _, f := range flags { - k, v, err := parseFlag(f) - if err != nil { - return nil, err - } - - cfg.Set(k, v) - } - } - - if jsonConfig != "" { - jConfig := viper.New() - jConfig.AutomaticEnv() - jConfig.SetEnvPrefix("rr") - jConfig.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - - jConfig.SetConfigType("json") - if err := jConfig.ReadConfig(bytes.NewBufferString(jsonConfig)); err != nil { - return nil, err - } - - // merging - if err := cfg.MergeConfigMap(jConfig.AllSettings()); err != nil { - return nil, err - } - } - - merged := viper.New() - - // we have to copy all the merged values into new config in order normalize it (viper bug?) - if err := merged.MergeConfigMap(cfg.AllSettings()); err != nil { - return nil, err - } - - return &ConfigWrapper{merged}, nil -} - -func parseFlag(flag string) (string, string, error) { - if !strings.Contains(flag, "=") { - return "", "", fmt.Errorf("invalid flag `%s`", flag) - } - - parts := strings.SplitN(strings.TrimLeft(flag, " \"'`"), "=", 2) - - return strings.Trim(parts[0], " \n\t"), parseValue(strings.Trim(parts[1], " \n\t")), nil -} - -func parseValue(value string) string { - escape := []rune(value)[0] - - if escape == '"' || escape == '\'' || escape == '`' { - value = strings.Trim(value, string(escape)) - value = strings.Replace(value, fmt.Sprintf("\\%s", string(escape)), string(escape), -1) - } - - return value -} - -func parseEnv(value interface{}) interface{} { - str, ok := value.(string) - if !ok || len(str) <= 3 { - return value - } - - if str[0:2] == "${" && str[len(str)-1:] == "}" { - if v, ok := os.LookupEnv(str[2 : len(str)-1]); ok { - return v - } - } - - return str -} diff --git a/cmd/util/cprint.go b/cmd/util/cprint.go deleted file mode 100644 index 3a986fd6..00000000 --- a/cmd/util/cprint.go +++ /dev/null @@ -1,47 +0,0 @@ -package util - -import ( - "fmt" - "github.com/mgutz/ansi" - "os" - "regexp" - "strings" -) - -var ( - reg *regexp.Regexp - - // Colorize enables colors support. - Colorize = true -) - -func init() { - reg, _ = regexp.Compile(`<([^>]+)>`) -} - -// Printf works identically to fmt.Print but adds `<white+hb>color formatting support for CLI</reset>`. -func Printf(format string, args ...interface{}) { - fmt.Print(Sprintf(format, args...)) -} - -// Sprintf works identically to fmt.Sprintf but adds `<white+hb>color formatting support for CLI</reset>`. -func Sprintf(format string, args ...interface{}) string { - format = reg.ReplaceAllStringFunc(format, func(s string) string { - if !Colorize { - return "" - } - - return ansi.ColorCode(strings.Trim(s, "<>/")) - }) - - return fmt.Sprintf(format, args...) -} - -// Panicf prints `<white+hb>color formatted message to STDERR</reset>`. -func Panicf(format string, args ...interface{}) error { - _, err := fmt.Fprint(os.Stderr, Sprintf(format, args...)) - if err != nil { - return err - } - return nil -} diff --git a/cmd/util/debug.go b/cmd/util/debug.go deleted file mode 100644 index 9b94510d..00000000 --- a/cmd/util/debug.go +++ /dev/null @@ -1,61 +0,0 @@ -package util - -import ( - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "strings" -) - -// LogEvent outputs rr event into given logger and return false if event was not handled. -func LogEvent(logger *logrus.Logger, event int, ctx interface{}) bool { - switch event { - case roadrunner.EventWorkerKill: - w := ctx.(*roadrunner.Worker) - logger.Warning(Sprintf( - "<white+hb>worker.%v</reset> <yellow>killed</reset>", - *w.Pid, - )) - return true - case roadrunner.EventWorkerError: - err := ctx.(roadrunner.WorkerError) - logger.Error(Sprintf( - "<white+hb>worker.%v</reset> <red>%s</reset>", - *err.Worker.Pid, - err.Caused, - )) - return true - } - - // outputs - switch event { - case roadrunner.EventStderrOutput: - for _, line := range strings.Split(string(ctx.([]byte)), "\n") { - if line == "" { - continue - } - - logger.Warning(strings.Trim(line, "\r\n")) - } - - return true - } - - // rr server events - switch event { - case roadrunner.EventServerFailure: - logger.Error(Sprintf("<red>server is dead</reset>")) - return true - } - - // pool events - switch event { - case roadrunner.EventPoolConstruct: - logger.Debug(Sprintf("<cyan>new worker pool</reset>")) - return true - case roadrunner.EventPoolError: - logger.Error(Sprintf("<red>%s</reset>", ctx)) - return true - } - - return false -} diff --git a/cmd/util/exit.go b/cmd/util/exit.go deleted file mode 100644 index 8871a483..00000000 --- a/cmd/util/exit.go +++ /dev/null @@ -1,15 +0,0 @@ -package util - -import ( - "os" -) - -// ExitWithError prints error and exits with error code`. -func ExitWithError(err error) { - errP := Panicf("<red+hb>Error:</reset> <red>%s</reset>\n", err) - if errP != nil { - // in case of error during Panicf, print this error via build-int print function - println("error occurred during fmt.Fprint: " + err.Error()) - } - os.Exit(1) -} diff --git a/cmd/util/rpc.go b/cmd/util/rpc.go deleted file mode 100644 index 8ff6720a..00000000 --- a/cmd/util/rpc.go +++ /dev/null @@ -1,18 +0,0 @@ -package util - -import ( - "errors" - "github.com/spiral/roadrunner/service" - rrpc "github.com/spiral/roadrunner/service/rpc" - "net/rpc" -) - -// RPCClient returns RPC client associated with given rr service container. -func RPCClient(container service.Container) (*rpc.Client, error) { - svc, st := container.Get(rrpc.ID) - if st < service.StatusOK { - return nil, errors.New("RPC service is not configured") - } - - return svc.(*rrpc.Service).Client() -} diff --git a/codecov.yml b/codecov.yml index 672717e3..75e92a5f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,4 +9,4 @@ coverage: default: target: auto threshold: 0% - informational: true + informational: true
\ No newline at end of file diff --git a/composer.json b/composer.json deleted file mode 100644 index f64a9263..00000000 --- a/composer.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "spiral/roadrunner", - "type": "server", - "description": "High-performance PHP application server, load-balancer and process manager written in Golang", - "license": "MIT", - "authors": [ - { - "name": "Anton Titov / Wolfy-J", - "email": "[email protected]" - }, - { - "name": "RoadRunner Community", - "homepage": "https://github.com/spiral/roadrunner/graphs/contributors" - } - ], - "require": { - "php": "^7.2 || ^8.0", - "ext-json": "*", - "ext-curl": "*", - "spiral/goridge": "^2.4.2", - "psr/http-factory": "^1.0.1", - "psr/http-message": "^1.0.1", - "symfony/console": "^2.5.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "laminas/laminas-diactoros": "^1.3.6 || ^2.0", - "composer/package-versions-deprecated": "^1.8" - }, - "config": { - "vendor-dir": "vendor_php" - }, - "require-dev": { - "phpstan/phpstan": "~0.12.34" - }, - "scripts": { - "analyze": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi" - }, - "autoload": { - "psr-4": { - "Spiral\\RoadRunner\\": "src/" - } - }, - "bin": [ - "bin/rr" - ] -} diff --git a/config.go b/config.go deleted file mode 100644 index 649ad9f4..00000000 --- a/config.go +++ /dev/null @@ -1,53 +0,0 @@ -package roadrunner - -import ( - "fmt" - "runtime" - "time" -) - -// Config defines basic behaviour of worker creation and handling process. -type Config struct { - // NumWorkers defines how many sub-processes can be run at once. This value - // might be doubled by Swapper while hot-swap. - NumWorkers int64 - - // MaxJobs defines how many executions is allowed for the worker until - // it's destruction. set 1 to create new process for each new task, 0 to let - // worker handle as many tasks as it can. - MaxJobs int64 - - // AllocateTimeout defines for how long pool will be waiting for a worker to - // be freed to handle the task. - AllocateTimeout time.Duration - - // DestroyTimeout defines for how long pool should be waiting for worker to - // properly stop, if timeout reached worker will be killed. - DestroyTimeout time.Duration -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (cfg *Config) InitDefaults() error { - cfg.AllocateTimeout = time.Minute - cfg.DestroyTimeout = time.Minute - cfg.NumWorkers = int64(runtime.NumCPU()) - - return nil -} - -// Valid returns error if config not valid. -func (cfg *Config) Valid() error { - if cfg.NumWorkers == 0 { - return fmt.Errorf("pool.NumWorkers must be set") - } - - if cfg.AllocateTimeout == 0 { - return fmt.Errorf("pool.AllocateTimeout must be set") - } - - if cfg.DestroyTimeout == 0 { - return fmt.Errorf("pool.DestroyTimeout must be set") - } - - return nil -} diff --git a/config_test.go b/config_test.go deleted file mode 100644 index e51cb2c4..00000000 --- a/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func Test_NumWorkers(t *testing.T) { - cfg := Config{ - AllocateTimeout: time.Second, - DestroyTimeout: time.Second * 10, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.NumWorkers must be set", err.Error()) -} - -func Test_NumWorkers_Default(t *testing.T) { - cfg := Config{ - AllocateTimeout: time.Second, - DestroyTimeout: time.Second * 10, - } - - assert.NoError(t, cfg.InitDefaults()) - err := cfg.Valid() - assert.Nil(t, err) -} - -func Test_AllocateTimeout(t *testing.T) { - cfg := Config{ - NumWorkers: 10, - DestroyTimeout: time.Second * 10, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.AllocateTimeout must be set", err.Error()) -} - -func Test_DestroyTimeout(t *testing.T) { - cfg := Config{ - NumWorkers: 10, - AllocateTimeout: time.Second, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.DestroyTimeout must be set", err.Error()) -} diff --git a/controller.go b/controller.go deleted file mode 100644 index 020ea4dd..00000000 --- a/controller.go +++ /dev/null @@ -1,16 +0,0 @@ -package roadrunner - -// Controller observes pool state and decides if any worker must be destroyed. -type Controller interface { - // Lock controller on given pool instance. - Attach(p Pool) Controller - - // Detach pool watching. - Detach() -} - -// Attacher defines the ability to attach rr controller. -type Attacher interface { - // Attach attaches controller to the service. - Attach(c Controller) -}
\ No newline at end of file diff --git a/controller_test.go b/controller_test.go deleted file mode 100644 index d177feda..00000000 --- a/controller_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package roadrunner - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "runtime" - "testing" - "time" -) - -type eWatcher struct { - p Pool - onAttach func(p Pool) - onDetach func(p Pool) -} - -func (w *eWatcher) Attach(p Pool) Controller { - wp := &eWatcher{p: p, onAttach: w.onAttach, onDetach: w.onDetach} - - if wp.onAttach != nil { - wp.onAttach(p) - } - - return wp -} - -func (w *eWatcher) Detach() { - if w.onDetach != nil { - w.onDetach(w.p) - } -} - -func (w *eWatcher) remove(wr *Worker, err error) { - w.p.Remove(wr, err) -} - -func Test_WatcherWatch(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_WatcherReattach(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - oldWatcher := rr.pController - - assert.NoError(t, rr.Reset()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - assert.NotEqual(t, oldWatcher, rr.pController) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_WatcherAttachDetachSequence(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - var attachedPool Pool - - rr.Attach(&eWatcher{ - onAttach: func(p Pool) { - attachedPool = p - }, - onDetach: func(p Pool) { - assert.Equal(t, attachedPool, p) - }, - }) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_RemoveWorkerOnAllocation(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php pid pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - wr := rr.Workers()[0] - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%v", *wr.Pid), res.String()) - lastPid := res.String() - - rr.pController.(*eWatcher).remove(wr, nil) - - res, err = rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.NotEqual(t, lastPid, res.String()) - - assert.NotEqual(t, StateReady, wr.state.Value()) - - _, ok := rr.pool.(*StaticPool).remove.Load(wr) - assert.False(t, ok) -} - -func Test_RemoveWorkerAfterTask(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php slow-pid pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - wr := rr.Workers()[0] - lastPid := "" - - wait := make(chan interface{}) - go func() { - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%v", *wr.Pid), res.String()) - lastPid = res.String() - - close(wait) - }() - - // wait for worker execution to be in progress - time.Sleep(time.Millisecond * 250) - rr.pController.(*eWatcher).remove(wr, nil) - - <-wait - - // must be replaced - assert.NotEqual(t, lastPid, fmt.Sprintf("%v", rr.Workers()[0])) - - // must not be registered within the pool - rr.pController.(*eWatcher).remove(wr, nil) -} diff --git a/error_buffer.go b/error_buffer.go deleted file mode 100644 index 0fc020c7..00000000 --- a/error_buffer.go +++ /dev/null @@ -1,113 +0,0 @@ -package roadrunner - -import ( - "sync" - "time" -) - -const ( - // EventStderrOutput - is triggered when worker sends data into stderr. The context - // is error message ([]byte). - EventStderrOutput = 1900 - - // WaitDuration - for how long error buffer should attempt to aggregate error messages - // before merging output together since lastError update (required to keep error update together). - WaitDuration = 100 * time.Millisecond -) - -// thread safe errBuffer -type errBuffer struct { - mu sync.Mutex - buf []byte - last int - wait *time.Timer - update chan interface{} - stop chan interface{} - lsn func(event int, ctx interface{}) -} - -func newErrBuffer() *errBuffer { - eb := &errBuffer{ - buf: make([]byte, 0), - update: make(chan interface{}), - wait: time.NewTimer(WaitDuration), - stop: make(chan interface{}), - } - - go func() { - for { - select { - case <-eb.update: - eb.wait.Reset(WaitDuration) - case <-eb.wait.C: - eb.mu.Lock() - if len(eb.buf) > eb.last { - if eb.lsn != nil { - eb.lsn(EventStderrOutput, eb.buf[eb.last:]) - eb.buf = eb.buf[0:0] - } - - eb.last = len(eb.buf) - } - eb.mu.Unlock() - case <-eb.stop: - eb.wait.Stop() - - eb.mu.Lock() - if len(eb.buf) > eb.last { - if eb.lsn != nil { - eb.lsn(EventStderrOutput, eb.buf[eb.last:]) - } - - eb.last = len(eb.buf) - } - eb.mu.Unlock() - return - } - } - }() - - return eb -} - -// Listen attaches error stream even listener. -func (eb *errBuffer) Listen(l func(event int, ctx interface{})) { - eb.mu.Lock() - eb.lsn = l - eb.mu.Unlock() -} - -// Len returns the number of buf of the unread portion of the errBuffer; -// buf.Len() == len(buf.Bytes()). -func (eb *errBuffer) Len() int { - eb.mu.Lock() - defer eb.mu.Unlock() - - // currently active message - return len(eb.buf) -} - -// Write appends the contents of pool to the errBuffer, growing the errBuffer as -// needed. The return value n is the length of pool; err is always nil. -func (eb *errBuffer) Write(p []byte) (int, error) { - eb.mu.Lock() - eb.buf = append(eb.buf, p...) - eb.mu.Unlock() - eb.update <- nil - - return len(p), nil -} - -// Strings fetches all errBuffer data into string. -func (eb *errBuffer) String() string { - eb.mu.Lock() - defer eb.mu.Unlock() - - return string(eb.buf) -} - -// Close aggregation timer. -func (eb *errBuffer) Close() error { - close(eb.stop) - return nil -} diff --git a/error_buffer_test.go b/error_buffer_test.go deleted file mode 100644 index c163ea43..00000000 --- a/error_buffer_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestErrBuffer_Write_Len(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hello")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - assert.Equal(t, 5, buf.Len()) - assert.Equal(t, "hello", buf.String()) -} - -func TestErrBuffer_Write_Event(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - tr := make(chan interface{}) - buf.Listen(func(event int, ctx interface{}) { - assert.Equal(t, EventStderrOutput, event) - assert.Equal(t, []byte("hello\n"), ctx) - close(tr) - }) - - _, err := buf.Write([]byte("hello\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - <-tr - - // messages are read - assert.Equal(t, 0, buf.Len()) -} - -func TestErrBuffer_Write_Event_Separated(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - tr := make(chan interface{}) - buf.Listen(func(event int, ctx interface{}) { - assert.Equal(t, EventStderrOutput, event) - assert.Equal(t, []byte("hello\nending"), ctx) - close(tr) - }) - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("lo\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("ending")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - <-tr - assert.Equal(t, 0, buf.Len()) - assert.Equal(t, "", buf.String()) -} - -func TestErrBuffer_Write_Event_Separated_NoListener(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("lo\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("ending")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - assert.Equal(t, 12, buf.Len()) - assert.Equal(t, "hello\nending", buf.String()) -} - -func TestErrBuffer_Write_Remaining(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - assert.Equal(t, 3, buf.Len()) - assert.Equal(t, "hel", buf.String()) -} diff --git a/errors.go b/errors.go deleted file mode 100644 index db995721..00000000 --- a/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package roadrunner - -// JobError is job level error (no worker halt), wraps at top -// of error context -type JobError []byte - -// Error converts error context to string -func (je JobError) Error() string { - return string(je) -} - -// WorkerError is worker related error -type WorkerError struct { - // Worker - Worker *Worker - - // Caused error - Caused error -} - -// Error converts error context to string -func (e WorkerError) Error() string { - return e.Caused.Error() -} diff --git a/errors_test.go b/errors_test.go deleted file mode 100644 index 6bb650af..00000000 --- a/errors_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package roadrunner - -import ( - "errors" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_JobError_Error(t *testing.T) { - e := JobError([]byte("error")) - assert.Equal(t, "error", e.Error()) -} - -func Test_WorkerError_Error(t *testing.T) { - e := WorkerError{Worker: nil, Caused: errors.New("error")} - assert.Equal(t, "error", e.Error()) -} diff --git a/factory.go b/factory.go deleted file mode 100644 index 3c304824..00000000 --- a/factory.go +++ /dev/null @@ -1,13 +0,0 @@ -package roadrunner - -import "os/exec" - -// Factory is responsible of wrapping given command into tasks worker. -type Factory interface { - // SpawnWorker creates new worker process based on given command. - // Process must not be started. - SpawnWorker(cmd *exec.Cmd) (w *Worker, err error) - - // Close the factory and underlying connections. - Close() error -} @@ -1,28 +1,37 @@ -module github.com/spiral/roadrunner +module github.com/spiral/roadrunner/v2 go 1.15 require ( - github.com/NYTimes/gziphandler v1.1.1 - github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect - github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 - github.com/cenkalti/backoff/v4 v4.0.0 - github.com/dustin/go-humanize v1.0.0 - github.com/go-ole/go-ole v1.2.4 // indirect + github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 + github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 + github.com/fatih/color v1.10.0 github.com/json-iterator/go v1.1.10 - github.com/mattn/go-colorable v0.1.7 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d + github.com/mattn/go-runewidth v0.0.9 github.com/olekukonko/tablewriter v0.0.4 - github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.7.1 - github.com/shirou/gopsutil v2.20.7+incompatible - github.com/sirupsen/logrus v1.6.0 - github.com/spf13/cobra v1.0.0 - github.com/spf13/viper v1.7.1 - github.com/spiral/goridge/v2 v2.4.6 + github.com/shirou/gopsutil v3.20.11+incompatible + github.com/spf13/cobra v1.1.1 + github.com/spiral/endure v1.0.0-beta20 + github.com/spiral/errors v1.0.6 + github.com/spiral/goridge/v3 v3.0.0-beta8 + github.com/spiral/roadrunner-plugins/config v1.0.0 + github.com/spiral/roadrunner-plugins/http v1.0.2 + github.com/spiral/roadrunner-plugins/informer v1.0.4 + github.com/spiral/roadrunner-plugins/logger v1.0.1 + github.com/spiral/roadrunner-plugins/metrics v1.0.0 + github.com/spiral/roadrunner-plugins/redis v1.0.0 + github.com/spiral/roadrunner-plugins/reload v1.0.1 + github.com/spiral/roadrunner-plugins/resetter v1.0.0 + github.com/spiral/roadrunner-plugins/rpc v1.0.1 + github.com/spiral/roadrunner-plugins/server v1.0.4 github.com/stretchr/testify v1.6.1 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a - github.com/yookoala/gofast v0.4.0 - golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 - golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 + github.com/vbauerster/mpb/v5 v5.4.0 + go.uber.org/multierr v1.6.0 + golang.org/x/sync v0.0.0-20201207232520-09787c993a3a ) + +//replace github.com/spiral/roadrunner-plugins/http v1.0.2 => ../roadrunner-plugins/http +//replace github.com/spiral/roadrunner-plugins/reload v1.0.0 => ../roadrunner-plugins/reload +//replace github.com/spiral/roadrunner-plugins/rpc v1.0.0 => ../roadrunner-plugins/rpc +//replace github.com/spiral/roadrunner-plugins/server v1.0.3 => ../roadrunner-plugins/server @@ -14,19 +14,37 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg= +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -34,53 +52,95 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 h1:uxxtrnACqI9zK4ENDMf0WpXfUsHP5V8liuq5QdgDISU= -github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= -github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= -github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= +github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= +github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.38.1/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc= +github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofiber/fiber/v2 v2.3.0 h1:82ufvLne0cxzdkDOeLkUmteA+z1uve9JQ/ZFsMOnkzc= +github.com/gofiber/fiber/v2 v2.3.0/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -89,42 +149,58 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -134,10 +210,17 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -145,11 +228,13 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -157,22 +242,26 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -190,56 +279,106 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= +github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU= +github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w= -github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto= +github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -248,52 +387,134 @@ github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:X github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spiral/goridge/v2 v2.4.6 h1:9u/mrxCtOSy0lnumrpPCSOlGBX/Vprid/hFsnzWrd6k= -github.com/spiral/goridge/v2 v2.4.6/go.mod h1:mYjL+Ny7nVfLqjRwIYV2pUSQ61eazvVclHII6FfZfYc= +github.com/spiral/endure v1.0.0-beta20 h1:QD3EJ6CRLgeo/6trfnlUcQhH3vrK8Hvf9ceDpde+yss= +github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLqJUgcV+nqxn+o= +github.com/spiral/errors v1.0.4/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= +github.com/spiral/errors v1.0.5 h1:TwlR9cZtTgnZrSngcEUpyiMO9yJ45gdQ+XcrCRoCCAM= +github.com/spiral/errors v1.0.5/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= +github.com/spiral/errors v1.0.6 h1:berk5ShEILSw6DplUVv9Ea1wGdk2WlVKQpuvDngll0U= +github.com/spiral/errors v1.0.6/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= +github.com/spiral/goridge/v3 v3.0.0-beta8 h1:x8uXCdhY49U1LEvmehnTaD2El6J9ZHAefRdh/QIZ6A4= +github.com/spiral/goridge/v3 v3.0.0-beta8/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE= +github.com/spiral/roadrunner-plugins/checker v1.0.1 h1:UVMX9zUF0R3ScRFUQg89e5+CLn3I2e5uAKsD4A52iDU= +github.com/spiral/roadrunner-plugins/checker v1.0.1/go.mod h1:/gnVybeKye5uq/8hgjI7YDoK/ntPd0ej6tWkxlYdiz8= +github.com/spiral/roadrunner-plugins/config v1.0.0 h1:RPYB8Ha/UeuBGRwtcqNb0uU8R5qe4XpUXVmv/lGSUdQ= +github.com/spiral/roadrunner-plugins/config v1.0.0/go.mod h1:XcVJLFDUlYPvZ3kVzssmP4fJbEzUvVJf534+eZaotAo= +github.com/spiral/roadrunner-plugins/http v1.0.0 h1:8WGAuZOrkYZQWo6n13ip+ZtzhKugZZ+b5W+ivVr7FjI= +github.com/spiral/roadrunner-plugins/http v1.0.0/go.mod h1:37ReUuAKJDtXH3GjMjRH5q3plBXq5r5lUfltRTVZzDE= +github.com/spiral/roadrunner-plugins/http v1.0.2 h1:BRmdbx0DYlgYTkvmd1IDZJewC1y30GqBCKRjbTn3OLc= +github.com/spiral/roadrunner-plugins/http v1.0.2/go.mod h1:JRgVSgJRh6wDmVs1rFJHQ9PM0xtCpzX9vBtVDItXZ/E= +github.com/spiral/roadrunner-plugins/informer v1.0.3 h1:6DXs8IRDjBvAr2mGKXVlhx1KLLfVqpXgvP2va79kR1s= +github.com/spiral/roadrunner-plugins/informer v1.0.3/go.mod h1:OPEJNADBbNQyx0/KuXQbY3Mqemo30vZh6duf6YpRf7M= +github.com/spiral/roadrunner-plugins/informer v1.0.4 h1:J2DXrQkRRyYDGw/6TIo7qF74Zn3AOHQe+9g0Hj5BwR4= +github.com/spiral/roadrunner-plugins/informer v1.0.4/go.mod h1:4qvFbVw0ESwE5RJQRiP939QN3SJ8l6aDnrQhL09f5lQ= +github.com/spiral/roadrunner-plugins/logger v1.0.1 h1:cc/pRE+8bUQgLiF05G7f948UpWtrZmJk/axTh1ywrks= +github.com/spiral/roadrunner-plugins/logger v1.0.1/go.mod h1:/oX1P56+/joLaAPwZaiRqPVVzM36701kdYqOHu+U3EM= +github.com/spiral/roadrunner-plugins/metrics v1.0.0 h1:PEK+AlcMv6yrH/vag7yQm/5sV7gMoWsNn9KWOsDPw/0= +github.com/spiral/roadrunner-plugins/metrics v1.0.0/go.mod h1:zLd9TdARJSwo6tQjTb0auD7pxN2ODXnsy4evInCMp9o= +github.com/spiral/roadrunner-plugins/redis v1.0.0 h1:/J4JpbNLd0G23BD9VaHsXKR8OHyAnrRQf7rRZMFx57U= +github.com/spiral/roadrunner-plugins/redis v1.0.0/go.mod h1:VzXRQcNaNONaUxyCW9t9eC+p8UxkXx2kwGeVw3s7zR0= +github.com/spiral/roadrunner-plugins/reload v1.0.0 h1:cBevNueyJjG4ykTjd9C4WSV8xNJq3UuXh3ympp3GU0I= +github.com/spiral/roadrunner-plugins/reload v1.0.0/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= +github.com/spiral/roadrunner-plugins/reload v1.0.1 h1:K7uvITE2B9bZyYWXk2b82erCeWjFBdjcCdfcXDVBjlU= +github.com/spiral/roadrunner-plugins/reload v1.0.1/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= +github.com/spiral/roadrunner-plugins/resetter v1.0.0 h1:Nx4mIzeoH/IcUfY4LM9xhY0yAXXYkAIAqWCRdMcsr68= +github.com/spiral/roadrunner-plugins/resetter v1.0.0/go.mod h1:DLFifJk1n3PWViXkT5+qAmzeRcPTowDRSbRqotf+WlE= +github.com/spiral/roadrunner-plugins/rpc v1.0.0 h1:cC17yCNqQUNtedKefdeT2P6z7q52L0QakxS4qwB7n+g= +github.com/spiral/roadrunner-plugins/rpc v1.0.0/go.mod h1:p+ClRf1ibW+xvekf+nGQtvipyrtPJP6WZ/J/DSp+Qck= +github.com/spiral/roadrunner-plugins/rpc v1.0.1 h1:rbMJNM+4opzoB7ZXi9dbntfix9+cTuQdUMcDIWsaC7k= +github.com/spiral/roadrunner-plugins/rpc v1.0.1/go.mod h1:iz1a3poEKCppX1fYfneMWOxWBfToYSi2TE1rYD0yBls= +github.com/spiral/roadrunner-plugins/server v1.0.1 h1:yzCw5rgz+KHpJb56QIID3gWH/IBbNtHZD2zvkck1bYc= +github.com/spiral/roadrunner-plugins/server v1.0.1/go.mod h1:qedfnQFlK1+Jwv5M8mRXJCOWTvF/qfFyULK/0UMBeOk= +github.com/spiral/roadrunner-plugins/server v1.0.3 h1:Bhr5tK0eW+52/qeew6QMowowOpeUKJv/qqECCmW1X+Y= +github.com/spiral/roadrunner-plugins/server v1.0.3/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= +github.com/spiral/roadrunner-plugins/server v1.0.4 h1:i6QWbVM/gLnM3Tb/DDFSFpRnxccJd3mCpENW2rBuTeE= +github.com/spiral/roadrunner-plugins/server v1.0.4/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= +github.com/spiral/roadrunner/v2 v2.0.0-alpha26/go.mod h1:r7ojuHm9qCVbg4fKcqr4Aqk7VXqZ9YPefr1LOv7HNys= +github.com/spiral/roadrunner/v2 v2.0.0-alpha28/go.mod h1:Uqtk/47S+qweVjSl4R9XVImEVZc3nMWku4yg3A+f18Q= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM= +github.com/valyala/fasthttp v1.18.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vbauerster/mpb/v5 v5.4.0 h1:n8JPunifvQvh6P1D1HAl2Ur9YcmKT1tpoUuiea5mlmg= +github.com/vbauerster/mpb/v5 v5.4.0/go.mod h1:fi4wVo7BVQ22QcvFObm+VwliQXlV1eBT8JDaKXR4JGI= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yookoala/gofast v0.4.0 h1:dLBjghcsbbZNOEHN8N1X/gh9S6srmJed4WQfG7DlKwo= github.com/yookoala/gofast v0.4.0/go.mod h1:rfbkoKaQG1bnuTUZcmV3vAlnfpF4FTq8WbQJf2vcpg8= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw= +go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -307,30 +528,38 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= +golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -338,15 +567,20 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -354,65 +588,102 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ= +golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo= +golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180726210403-bfb5194568d3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -426,22 +697,35 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/interfaces/events/handler.go b/interfaces/events/handler.go new file mode 100644 index 00000000..01f64d73 --- /dev/null +++ b/interfaces/events/handler.go @@ -0,0 +1,10 @@ +package events + +type Handler interface { + NumListeners() int + AddListener(listener EventListener) + Push(e interface{}) +} + +// Event listener listens for the events produced by worker, worker pool or other service. +type EventListener func(event interface{}) diff --git a/interfaces/events/pool_events.go b/interfaces/events/pool_events.go new file mode 100644 index 00000000..cc32f6b2 --- /dev/null +++ b/interfaces/events/pool_events.go @@ -0,0 +1,65 @@ +package events + +const ( + // EventWorkerConstruct thrown when new worker is spawned. + EventWorkerConstruct P = iota + 7800 + + // EventWorkerDestruct thrown after worker destruction. + EventWorkerDestruct + + // EventPoolError caused on pool wide errors. + EventPoolError + + // EventSupervisorError triggered when supervisor can not complete work. + EventSupervisorError + + // EventNoFreeWorkers triggered when there are no free workers in the stack and timeout for worker allocate elapsed + EventNoFreeWorkers + + // todo: EventMaxMemory caused when worker consumes more memory than allowed. + EventMaxMemory + + // todo: EventTTL thrown when worker is removed due TTL being reached. Context is rr.WorkerError + EventTTL + + // todo: EventIdleTTL triggered when worker spends too much time at rest. + EventIdleTTL + + // todo: EventExecTTL triggered when worker spends too much time doing the task (max_execution_time). + EventExecTTL +) + +type P int64 + +func (ev P) String() string { + switch ev { + case EventWorkerConstruct: + return "EventWorkerConstruct" + case EventWorkerDestruct: + return "EventWorkerDestruct" + case EventPoolError: + return "EventPoolError" + case EventSupervisorError: + return "EventSupervisorError" + case EventNoFreeWorkers: + return "EventNoFreeWorkers" + case EventMaxMemory: + return "EventMaxMemory" + case EventTTL: + return "EventTTL" + case EventIdleTTL: + return "EventIdleTTL" + case EventExecTTL: + return "EventExecTTL" + } + return "Unknown event type" +} + +// PoolEvent triggered by pool on different events. Pool as also trigger WorkerEvent in case of log. +type PoolEvent struct { + // Event type, see below. + Event P + + // Payload depends on event type, typically it's worker or error. + Payload interface{} +} diff --git a/interfaces/events/worker_events.go b/interfaces/events/worker_events.go new file mode 100644 index 00000000..497f0a06 --- /dev/null +++ b/interfaces/events/worker_events.go @@ -0,0 +1,34 @@ +package events + +// EventWorkerKill thrown after WorkerProcess is being forcefully killed. +const ( + // EventWorkerError triggered after WorkerProcess. Except payload to be error. + EventWorkerError E = iota + 200 + + // EventWorkerLog triggered on every write to WorkerProcess StdErr pipe (batched). Except payload to be []byte string. + EventWorkerLog +) + +type E int64 + +func (ev E) String() string { + switch ev { + case EventWorkerError: + return "EventWorkerError" + case EventWorkerLog: + return "EventWorkerLog" + } + return "Unknown event type" +} + +// WorkerEvent wraps worker events. +type WorkerEvent struct { + // Event id, see below. + Event E + + // Worker triggered the event. + Worker interface{} + + // Event specific payload. + Payload interface{} +} diff --git a/interfaces/pool/pool.go b/interfaces/pool/pool.go new file mode 100644 index 00000000..97cc945c --- /dev/null +++ b/interfaces/pool/pool.go @@ -0,0 +1,100 @@ +package pool + +import ( + "context" + "runtime" + "time" + + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/pkg/payload" +) + +// Pool managed set of inner worker processes. +type Pool interface { + // GetConfig returns pool configuration. + GetConfig() interface{} + + // Exec executes task with payload + Exec(rqs payload.Payload) (payload.Payload, error) + + // ExecWithContext executes task with context which is used with timeout + ExecWithContext(ctx context.Context, rqs payload.Payload) (payload.Payload, error) + + // Workers returns worker list associated with the pool. + Workers() (workers []worker.BaseProcess) + + // Remove worker from the pool. + RemoveWorker(worker worker.BaseProcess) error + + // Destroy all underlying stack (but let them to complete the task). + Destroy(ctx context.Context) +} + +// Configures the pool behaviour. +type Config struct { + // Debug flag creates new fresh worker before every request. + Debug bool + + // NumWorkers defines how many sub-processes can be run at once. This value + // might be doubled by Swapper while hot-swap. Defaults to number of CPU cores. + NumWorkers int64 + + // MaxJobs defines how many executions is allowed for the worker until + // it's destruction. set 1 to create new process for each new task, 0 to let + // worker handle as many tasks as it can. + MaxJobs int64 + + // AllocateTimeout defines for how long pool will be waiting for a worker to + // be freed to handle the task. Defaults to 60s. + AllocateTimeout time.Duration + + // DestroyTimeout defines for how long pool should be waiting for worker to + // properly destroy, if timeout reached worker will be killed. Defaults to 60s. + DestroyTimeout time.Duration + + // Supervision config to limit worker and pool memory usage. + Supervisor *SupervisorConfig +} + +// InitDefaults enables default config values. +func (cfg *Config) InitDefaults() { + if cfg.NumWorkers == 0 { + cfg.NumWorkers = int64(runtime.NumCPU()) + } + + if cfg.AllocateTimeout == 0 { + cfg.AllocateTimeout = time.Minute + } + + if cfg.DestroyTimeout == 0 { + cfg.DestroyTimeout = time.Minute + } + if cfg.Supervisor == nil { + return + } + cfg.Supervisor.InitDefaults() +} + +type SupervisorConfig struct { + // WatchTick defines how often to check the state of worker. + WatchTick uint64 + + // TTL defines maximum time worker is allowed to live. + TTL uint64 + + // IdleTTL defines maximum duration worker can spend in idle mode. Disabled when 0. + IdleTTL uint64 + + // ExecTTL defines maximum lifetime per job. + ExecTTL uint64 + + // MaxWorkerMemory limits memory per worker. + MaxWorkerMemory uint64 +} + +// InitDefaults enables default config values. +func (cfg *SupervisorConfig) InitDefaults() { + if cfg.WatchTick == 0 { + cfg.WatchTick = 1 + } +} diff --git a/interfaces/worker/factory.go b/interfaces/worker/factory.go new file mode 100644 index 00000000..8412428d --- /dev/null +++ b/interfaces/worker/factory.go @@ -0,0 +1,20 @@ +package worker + +import ( + "context" + "os/exec" + + "github.com/spiral/roadrunner/v2/interfaces/events" +) + +// Factory is responsible of wrapping given command into tasks WorkerProcess. +type Factory interface { + // SpawnWorkerWithContext creates new WorkerProcess process based on given command with context. + // Process must not be started. + SpawnWorkerWithTimeout(context.Context, *exec.Cmd, ...events.EventListener) (BaseProcess, error) + // SpawnWorker creates new WorkerProcess process based on given command. + // Process must not be started. + SpawnWorker(*exec.Cmd, ...events.EventListener) (BaseProcess, error) + // Close the factory and underlying connections. + Close() error +} diff --git a/interfaces/worker/watcher.go b/interfaces/worker/watcher.go new file mode 100644 index 00000000..ce2c1c5a --- /dev/null +++ b/interfaces/worker/watcher.go @@ -0,0 +1,26 @@ +package worker + +import "context" + +type Watcher interface { + // AddToWatch used to add stack to wait its state + AddToWatch(workers []BaseProcess) error + + // GetFreeWorker provide first free worker + GetFreeWorker(ctx context.Context) (BaseProcess, error) + + // PutWorker enqueues worker back + PushWorker(w BaseProcess) + + // AllocateNew used to allocate new worker and put in into the WorkerWatcher + AllocateNew() error + + // Destroy destroys the underlying stack + Destroy(ctx context.Context) + + // WorkersList return all stack w/o removing it from internal storage + WorkersList() []BaseProcess + + // RemoveWorker remove worker from the stack + RemoveWorker(wb BaseProcess) error +} diff --git a/interfaces/worker/worker.go b/interfaces/worker/worker.go new file mode 100644 index 00000000..0ac82158 --- /dev/null +++ b/interfaces/worker/worker.go @@ -0,0 +1,59 @@ +package worker + +import ( + "context" + "fmt" + "time" + + "github.com/spiral/goridge/v3/interfaces/relay" + "github.com/spiral/roadrunner/v2/internal" + "github.com/spiral/roadrunner/v2/pkg/payload" +) + +// Allocator is responsible for worker allocation in the pool +type Allocator func() (BaseProcess, error) + +type BaseProcess interface { + fmt.Stringer + + // Pid returns worker pid. + Pid() int64 + + // Created returns time worker was created at. + Created() time.Time + + // State return receive-only WorkerProcess state object, state can be used to safely access + // WorkerProcess status, time when status changed and number of WorkerProcess executions. + State() internal.State + + // Start used to run Cmd and immediately return + Start() error + + // Wait must be called once for each WorkerProcess, call will be released once WorkerProcess is + // complete and will return process error (if any), if stderr is presented it's value + // will be wrapped as WorkerError. Method will return error code if php process fails + // to find or Start the script. + Wait() error + + // Stop sends soft termination command to the WorkerProcess and waits for process completion. + Stop() error + + // Kill kills underlying process, make sure to call Wait() func to gather + // error log from the stderr. Does not waits for process completion! + Kill() error + + // Relay returns attached to worker goridge relay + Relay() relay.Relay + + // AttachRelay used to attach goridge relay to the worker process + AttachRelay(rl relay.Relay) +} + +type SyncWorker interface { + // BaseProcess provides basic functionality for the SyncWorker + BaseProcess + // Exec used to execute payload on the SyncWorker, there is no TIMEOUTS + Exec(rqs payload.Payload) (payload.Payload, error) + // ExecWithContext used to handle Exec with TTL + ExecWithTimeout(ctx context.Context, p payload.Payload) (payload.Payload, error) +} diff --git a/internal/protocol.go b/internal/protocol.go new file mode 100755 index 00000000..a099ce4d --- /dev/null +++ b/internal/protocol.go @@ -0,0 +1,94 @@ +package internal + +import ( + "os" + + j "github.com/json-iterator/go" + "github.com/spiral/errors" + "github.com/spiral/goridge/v3/interfaces/relay" + "github.com/spiral/goridge/v3/pkg/frame" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +type StopCommand struct { + Stop bool `json:"stop"` +} + +type pidCommand struct { + Pid int `json:"pid"` +} + +func SendControl(rl relay.Relay, payload interface{}) error { + const op = errors.Op("send control frame") + fr := frame.NewFrame() + fr.WriteVersion(frame.VERSION_1) + fr.WriteFlags(frame.CONTROL) + + if data, ok := payload.([]byte); ok { + // check if payload no more that 4Gb + if uint32(len(data)) > ^uint32(0) { + return errors.E(op, errors.Str("payload is more that 4gb")) + } + + fr.WritePayloadLen(uint32(len(data))) + fr.WritePayload(data) + fr.WriteCRC() + + err := rl.Send(fr) + if err != nil { + return errors.E(op, err) + } + return nil + } + + data, err := json.Marshal(payload) + if err != nil { + return errors.E(op, errors.Errorf("invalid payload: %s", err)) + } + + fr.WritePayloadLen(uint32(len(data))) + fr.WritePayload(data) + fr.WriteCRC() + + err = rl.Send(fr) + if err != nil { + return errors.E(op, err) + } + + return nil +} + +func FetchPID(rl relay.Relay) (int64, error) { + const op = errors.Op("fetchPID") + err := SendControl(rl, pidCommand{Pid: os.Getpid()}) + if err != nil { + return 0, errors.E(op, err) + } + + frameR := frame.NewFrame() + err = rl.Receive(frameR) + if !frameR.VerifyCRC() { + return 0, errors.E(op, errors.Str("CRC mismatch")) + } + if err != nil { + return 0, errors.E(op, err) + } + if frameR == nil { + return 0, errors.E(op, errors.Str("nil frame received")) + } + + flags := frameR.ReadFlags() + + if flags&(byte(frame.CONTROL)) == 0 { + return 0, errors.E(op, errors.Str("unexpected response, header is missing, no CONTROL flag")) + } + + link := &pidCommand{} + err = json.Unmarshal(frameR.Payload(), link) + if err != nil { + return 0, errors.E(op, err) + } + + return int64(link.Pid), nil +} diff --git a/internal/state.go b/internal/state.go new file mode 100755 index 00000000..8f7d939b --- /dev/null +++ b/internal/state.go @@ -0,0 +1,122 @@ +package internal + +import ( + "fmt" + "sync/atomic" +) + +// State represents WorkerProcess status and updated time. +type State interface { + fmt.Stringer + // Value returns WorkerState value + Value() int64 + // Set sets the WorkerState + Set(value int64) + // NumJobs shows how many times WorkerProcess was invoked + NumExecs() int64 + // IsActive returns true if WorkerProcess not Inactive or Stopped + IsActive() bool + // RegisterExec using to registering php executions + RegisterExec() + // SetLastUsed sets worker last used time + SetLastUsed(lu uint64) + // LastUsed return worker last used time + LastUsed() uint64 +} + +const ( + // StateInactive - no associated process + StateInactive int64 = iota + + // StateReady - ready for job. + StateReady + + // StateWorking - working on given payload. + StateWorking + + // StateInvalid - indicates that WorkerProcess is being disabled and will be removed. + StateInvalid + + // StateStopping - process is being softly stopped. + StateStopping + + StateKilling + + // State of worker, when no need to allocate new one + StateDestroyed + + // StateStopped - process has been terminated. + StateStopped + + // StateErrored - error WorkerState (can't be used). + StateErrored + + StateRemove +) + +type WorkerState struct { + value int64 + numExecs int64 + // to be lightweight, use UnixNano + lastUsed uint64 +} + +// Thread safe +func NewWorkerState(value int64) *WorkerState { + return &WorkerState{value: value} +} + +// String returns current WorkerState as string. +func (s *WorkerState) String() string { + switch s.Value() { + case StateInactive: + return "inactive" + case StateReady: + return "ready" + case StateWorking: + return "working" + case StateInvalid: + return "invalid" + case StateStopped: + return "stopped" + case StateErrored: + return "errored" + } + + return "undefined" +} + +// NumExecs returns number of registered WorkerProcess execs. +func (s *WorkerState) NumExecs() int64 { + return atomic.LoadInt64(&s.numExecs) +} + +// Value WorkerState returns WorkerState value +func (s *WorkerState) Value() int64 { + return atomic.LoadInt64(&s.value) +} + +// IsActive returns true if WorkerProcess not Inactive or Stopped +func (s *WorkerState) IsActive() bool { + val := s.Value() + return val == StateWorking || val == StateReady +} + +// change WorkerState value (status) +func (s *WorkerState) Set(value int64) { + atomic.StoreInt64(&s.value, value) +} + +// register new execution atomically +func (s *WorkerState) RegisterExec() { + atomic.AddInt64(&s.numExecs, 1) +} + +// Update last used time +func (s *WorkerState) SetLastUsed(lu uint64) { + atomic.StoreUint64(&s.lastUsed, lu) +} + +func (s *WorkerState) LastUsed() uint64 { + return atomic.LoadUint64(&s.lastUsed) +} diff --git a/internal/state_test.go b/internal/state_test.go new file mode 100755 index 00000000..bdb05825 --- /dev/null +++ b/internal/state_test.go @@ -0,0 +1,27 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewState(t *testing.T) { + st := NewWorkerState(StateErrored) + + assert.Equal(t, "errored", st.String()) + + assert.Equal(t, "inactive", NewWorkerState(StateInactive).String()) + assert.Equal(t, "ready", NewWorkerState(StateReady).String()) + assert.Equal(t, "working", NewWorkerState(StateWorking).String()) + assert.Equal(t, "stopped", NewWorkerState(StateStopped).String()) + assert.Equal(t, "undefined", NewWorkerState(1000).String()) +} + +func Test_IsActive(t *testing.T) { + assert.False(t, NewWorkerState(StateInactive).IsActive()) + assert.True(t, NewWorkerState(StateReady).IsActive()) + assert.True(t, NewWorkerState(StateWorking).IsActive()) + assert.False(t, NewWorkerState(StateStopped).IsActive()) + assert.False(t, NewWorkerState(StateErrored).IsActive()) +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index a6ef3b8a..00000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - level: 'max' - paths: - - src diff --git a/pipe_factory.go b/pipe_factory.go deleted file mode 100644 index e9e750c4..00000000 --- a/pipe_factory.go +++ /dev/null @@ -1,79 +0,0 @@ -package roadrunner - -import ( - "fmt" - "io" - "os/exec" - - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" -) - -// PipeFactory connects to workers using standard -// streams (STDIN, STDOUT pipes). -type PipeFactory struct { -} - -// NewPipeFactory returns new factory instance and starts -// listening -func NewPipeFactory() *PipeFactory { - return &PipeFactory{} -} - -// SpawnWorker creates new worker and connects it to goridge relay, -// method Wait() must be handled on level above. -func (f *PipeFactory) SpawnWorker(cmd *exec.Cmd) (w *Worker, err error) { - if w, err = newWorker(cmd); err != nil { - return nil, err - } - - var ( - in io.ReadCloser - out io.WriteCloser - ) - - if in, err = cmd.StdoutPipe(); err != nil { - return nil, err - } - - if out, err = cmd.StdinPipe(); err != nil { - return nil, err - } - - w.rl = goridge.NewPipeRelay(in, out) - - if err := w.start(); err != nil { - return nil, errors.Wrap(err, "process error") - } - - if pid, err := fetchPID(w.rl); pid != *w.Pid { - go func(w *Worker) { - err := w.Kill() - if err != nil { - // there is no logger here, how to handle error in goroutines ? - fmt.Printf("error killing the worker with PID number %d, Created: %s", w.Pid, w.Created) - } - }(w) - - if wErr := w.Wait(); wErr != nil { - if _, ok := wErr.(*exec.ExitError); ok { - // error might be nil here - if err != nil { - err = errors.Wrap(wErr, err.Error()) - } - } else { - err = wErr - } - } - - return nil, errors.Wrap(err, "unable to connect to worker") - } - - w.state.set(StateReady) - return w, nil -} - -// Close the factory. -func (f *PipeFactory) Close() error { - return nil -} diff --git a/pipe_factory_test.go b/pipe_factory_test.go deleted file mode 100644 index 14cf1272..00000000 --- a/pipe_factory_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "os/exec" - "testing" - "time" -) - -func Test_Pipe_Start(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, err := NewPipeFactory().SpawnWorker(cmd) - assert.NoError(t, err) - assert.NotNil(t, w) - - go func() { - assert.NoError(t, w.Wait()) - }() - - assert.NoError(t, w.Stop()) -} - -func Test_Pipe_StartError(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - err := cmd.Start() - if err != nil { - t.Errorf("error running the command: error %v", err) - } - - w, err := NewPipeFactory().SpawnWorker(cmd) - assert.Error(t, err) - assert.Nil(t, w) -} - -func Test_Pipe_PipeError(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - _, err := cmd.StdinPipe() - if err != nil { - t.Errorf("error creating the STDIN pipe: error %v", err) - } - - w, err := NewPipeFactory().SpawnWorker(cmd) - assert.Error(t, err) - assert.Nil(t, w) -} - -func Test_Pipe_PipeError2(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - _, err := cmd.StdinPipe() - if err != nil { - t.Errorf("error creating the STDIN pipe: error %v", err) - } - - w, err := NewPipeFactory().SpawnWorker(cmd) - assert.Error(t, err) - assert.Nil(t, w) -} - -func Test_Pipe_Failboot(t *testing.T) { - cmd := exec.Command("php", "tests/failboot.php") - w, err := NewPipeFactory().SpawnWorker(cmd) - - assert.Nil(t, w) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failboot") -} - -func Test_Pipe_Invalid(t *testing.T) { - cmd := exec.Command("php", "tests/invalid.php") - - w, err := NewPipeFactory().SpawnWorker(cmd) - assert.Error(t, err) - assert.Nil(t, w) -} - -func Test_Pipe_Echo(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_Pipe_Broken(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "broken", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - err := w.Wait() - - assert.Error(t, err) - assert.Contains(t, err.Error(), "undefined_function()") - }() - defer func() { - time.Sleep(time.Second) - err := w.Stop() - assert.NoError(t, err) - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) -} - -func Benchmark_Pipe_SpawnWorker_Stop(b *testing.B) { - f := NewPipeFactory() - for n := 0; n < b.N; n++ { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - w, _ := f.SpawnWorker(cmd) - go func() { - if w.Wait() != nil { - b.Fail() - } - }() - - err := w.Stop() - if err != nil { - b.Errorf("error stopping the worker: error %v", err) - } - } -} - -func Benchmark_Pipe_Worker_ExecEcho(b *testing.B) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - err := w.Wait() - if err != nil { - b.Errorf("error waiting the worker: error %v", err) - } - }() - defer func() { - err := w.Stop() - if err != nil { - b.Errorf("error stopping the worker: error %v", err) - } - }() - - for n := 0; n < b.N; n++ { - if _, err := w.Exec(&Payload{Body: []byte("hello")}); err != nil { - b.Fail() - } - } -} diff --git a/pkg/events/events.go b/pkg/events/events.go new file mode 100755 index 00000000..92dc103a --- /dev/null +++ b/pkg/events/events.go @@ -0,0 +1,38 @@ +package events + +import ( + "sync" + + "github.com/spiral/roadrunner/v2/interfaces/events" +) + +// EventHandler helps to broadcast events to multiple listeners. +type EventHandler struct { + listeners []events.EventListener + sync.RWMutex +} + +func NewEventsHandler() events.Handler { + return &EventHandler{listeners: make([]events.EventListener, 0, 2)} +} + +// NumListeners returns number of event listeners. +func (eb *EventHandler) NumListeners() int { + return len(eb.listeners) +} + +// AddListener registers new event listener. +func (eb *EventHandler) AddListener(listener events.EventListener) { + eb.Lock() + defer eb.Unlock() + eb.listeners = append(eb.listeners, listener) +} + +// Push broadcast events across all event listeners. +func (eb *EventHandler) Push(e interface{}) { + eb.Lock() + defer eb.Unlock() + for k := range eb.listeners { + eb.listeners[k](e) + } +} diff --git a/payload.go b/pkg/payload/payload.go index 154cec95..fac36852 100644..100755 --- a/payload.go +++ b/pkg/payload/payload.go @@ -1,12 +1,12 @@ -package roadrunner +package payload -// Payload carries binary header and body to workers and +// Payload carries binary header and body to stack and // back to the server. type Payload struct { // Context represent payload context, might be omitted. Context []byte - // body contains binary payload to be processed by worker. + // body contains binary payload to be processed by WorkerProcess. Body []byte } diff --git a/pkg/pipe/pipe_factory.go b/pkg/pipe/pipe_factory.go new file mode 100755 index 00000000..ecb3fa71 --- /dev/null +++ b/pkg/pipe/pipe_factory.go @@ -0,0 +1,163 @@ +package pipe + +import ( + "context" + "os/exec" + + "github.com/spiral/errors" + "github.com/spiral/goridge/v3/pkg/pipe" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + workerImpl "github.com/spiral/roadrunner/v2/pkg/worker" + "go.uber.org/multierr" +) + +// Factory connects to stack using standard +// streams (STDIN, STDOUT pipes). +type Factory struct{} + +// NewPipeFactory returns new factory instance and starts +// listening +func NewPipeFactory() worker.Factory { + return &Factory{} +} + +type SpawnResult struct { + w worker.BaseProcess + err error +} + +// SpawnWorker creates new Process and connects it to goridge relay, +// method Wait() must be handled on level above. +func (f *Factory) SpawnWorkerWithTimeout(ctx context.Context, cmd *exec.Cmd, listeners ...events.EventListener) (worker.BaseProcess, error) { + c := make(chan SpawnResult) + const op = errors.Op("spawn worker with context") + go func() { + w, err := workerImpl.InitBaseWorker(cmd, workerImpl.AddListeners(listeners...)) + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // TODO why out is in? + in, err := cmd.StdoutPipe() + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // TODO why in is out? + out, err := cmd.StdinPipe() + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // Init new PIPE relay + relay := pipe.NewPipeRelay(in, out) + w.AttachRelay(relay) + + // Start the worker + err = w.Start() + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // errors bundle + pid, err := internal.FetchPID(relay) + if pid != w.Pid() || err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // everything ok, set ready state + w.State().Set(internal.StateReady) + + // return worker + c <- SpawnResult{ + w: w, + err: nil, + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-c: + if res.err != nil { + return nil, res.err + } + return res.w, nil + } +} + +func (f *Factory) SpawnWorker(cmd *exec.Cmd, listeners ...events.EventListener) (worker.BaseProcess, error) { + const op = errors.Op("spawn worker") + w, err := workerImpl.InitBaseWorker(cmd, workerImpl.AddListeners(listeners...)) + if err != nil { + return nil, errors.E(op, err) + } + + // TODO why out is in? + in, err := cmd.StdoutPipe() + if err != nil { + return nil, errors.E(op, err) + } + + // TODO why in is out? + out, err := cmd.StdinPipe() + if err != nil { + return nil, errors.E(op, err) + } + + // Init new PIPE relay + relay := pipe.NewPipeRelay(in, out) + w.AttachRelay(relay) + + // Start the worker + err = w.Start() + if err != nil { + return nil, errors.E(op, err) + } + + // errors bundle + if pid, err := internal.FetchPID(relay); pid != w.Pid() { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + return nil, errors.E(op, err) + } + + // everything ok, set ready state + w.State().Set(internal.StateReady) + return w, nil +} + +// Close the factory. +func (f *Factory) Close() error { + return nil +} diff --git a/pkg/pipe/pipe_factory_test.go b/pkg/pipe/pipe_factory_test.go new file mode 100755 index 00000000..dca09375 --- /dev/null +++ b/pkg/pipe/pipe_factory_test.go @@ -0,0 +1,511 @@ +package pipe + +import ( + "context" + "os/exec" + "strings" + "sync" + "testing" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/internal" + "github.com/spiral/roadrunner/v2/pkg/payload" + workerImpl "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/stretchr/testify/assert" +) + +func Test_GetState(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + go func() { + assert.NoError(t, w.Wait()) + assert.Equal(t, internal.StateStopped, w.State().Value()) + }() + + assert.NoError(t, err) + assert.NotNil(t, w) + + assert.Equal(t, internal.StateReady, w.State().Value()) + err = w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } +} + +func Test_Kill(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + assert.Error(t, w.Wait()) + // TODO changed from stopped, discuss + assert.Equal(t, internal.StateErrored, w.State().Value()) + }() + + assert.NoError(t, err) + assert.NotNil(t, w) + + assert.Equal(t, internal.StateReady, w.State().Value()) + err = w.Kill() + if err != nil { + t.Errorf("error killing the Process: error %v", err) + } + wg.Wait() +} + +func Test_Pipe_Start(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + assert.NoError(t, err) + assert.NotNil(t, w) + + go func() { + assert.NoError(t, w.Wait()) + }() + + assert.NoError(t, w.Stop()) +} + +func Test_Pipe_StartError(t *testing.T) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + err := cmd.Start() + if err != nil { + t.Errorf("error running the command: error %v", err) + } + + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + assert.Error(t, err) + assert.Nil(t, w) +} + +func Test_Pipe_PipeError(t *testing.T) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + _, err := cmd.StdinPipe() + if err != nil { + t.Errorf("error creating the STDIN pipe: error %v", err) + } + + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + assert.Error(t, err) + assert.Nil(t, w) +} + +func Test_Pipe_PipeError2(t *testing.T) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + _, err := cmd.StdinPipe() + if err != nil { + t.Errorf("error creating the STDIN pipe: error %v", err) + } + + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + assert.Error(t, err) + assert.Nil(t, w) +} + +func Test_Pipe_Failboot(t *testing.T) { + cmd := exec.Command("php", "../../tests/failboot.php") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + + assert.Nil(t, w) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failboot") +} + +func Test_Pipe_Invalid(t *testing.T) { + cmd := exec.Command("php", "../../tests/invalid.php") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + assert.Error(t, err) + assert.Nil(t, w) +} + +func Test_Pipe_Echo(t *testing.T) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } + defer func() { + err = w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + sw, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_Pipe_Broken(t *testing.T) { + cmd := exec.Command("php", "../../tests/client.php", "broken", "pipes") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } + defer func() { + time.Sleep(time.Second) + err = w.Stop() + assert.Error(t, err) + }() + + sw, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) +} + +func Benchmark_Pipe_SpawnWorker_Stop(b *testing.B) { + f := NewPipeFactory() + for n := 0; n < b.N; n++ { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + w, _ := f.SpawnWorkerWithTimeout(context.Background(), cmd) + go func() { + if w.Wait() != nil { + b.Fail() + } + }() + + err := w.Stop() + if err != nil { + b.Errorf("error stopping the worker: error %v", err) + } + } +} + +func Benchmark_Pipe_Worker_ExecEcho(b *testing.B) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(context.Background(), cmd) + sw, err := workerImpl.From(w) + if err != nil { + b.Fatal(err) + } + b.ReportAllocs() + b.ResetTimer() + go func() { + err := w.Wait() + if err != nil { + b.Errorf("error waiting the worker: error %v", err) + } + }() + defer func() { + err := w.Stop() + if err != nil { + b.Errorf("error stopping the worker: error %v", err) + } + }() + + for n := 0; n < b.N; n++ { + if _, err := sw.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + } + } +} + +func Benchmark_Pipe_Worker_ExecEcho3(b *testing.B) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = w.Stop() + if err != nil { + b.Errorf("error stopping the Process: error %v", err) + } + }() + + sw, err := workerImpl.From(w) + if err != nil { + b.Fatal(err) + } + + for n := 0; n < b.N; n++ { + if _, err := sw.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + } + } +} + +func Benchmark_Pipe_Worker_ExecEchoWithoutContext(b *testing.B) { + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = w.Stop() + if err != nil { + b.Errorf("error stopping the Process: error %v", err) + } + }() + + sw, err := workerImpl.From(w) + if err != nil { + b.Fatal(err) + } + + for n := 0; n < b.N; n++ { + if _, err := sw.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + } + } +} + +func Test_Echo(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + go func() { + assert.NoError(t, syncWorker.Wait()) + }() + defer func() { + err := syncWorker.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + res, err := syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_BadPayload(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + go func() { + assert.NoError(t, syncWorker.Wait()) + }() + defer func() { + err := syncWorker.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + res, err := syncWorker.Exec(payload.Payload{}) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + assert.Contains(t, err.Error(), "payload can not be empty") +} + +func Test_String(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + go func() { + assert.NoError(t, w.Wait()) + }() + defer func() { + err := w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + assert.Contains(t, w.String(), "php ../../tests/client.php echo pipes") + assert.Contains(t, w.String(), "ready") + assert.Contains(t, w.String(), "numExecs: 0") +} + +func Test_Echo_Slow(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/slow-client.php", "echo", "pipes", "10", "10") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + go func() { + assert.NoError(t, w.Wait()) + }() + defer func() { + err := w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_Broken(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "broken", "pipes") + data := "" + mu := &sync.Mutex{} + listener := func(event interface{}) { + if wev, ok := event.(events.WorkerEvent); ok { + mu.Lock() + data = string(wev.Payload.([]byte)) + mu.Unlock() + } + } + + w, err := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd, listener) + if err != nil { + t.Fatal(err) + } + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + assert.NotNil(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + time.Sleep(time.Second * 3) + mu.Lock() + if strings.ContainsAny(data, "undefined_function()") == false { + t.Fail() + } + mu.Unlock() + assert.Error(t, w.Stop()) +} + +func Test_Error(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "error", "pipes") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + go func() { + assert.NoError(t, w.Wait()) + }() + + defer func() { + err := w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + assert.NotNil(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + if errors.Is(errors.ErrSoftJob, err) == false { + t.Fatal("error should be of type errors.ErrSoftJob") + } + assert.Contains(t, err.Error(), "hello") +} + +func Test_NumExecs(t *testing.T) { + ctx := context.Background() + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") + + w, _ := NewPipeFactory().SpawnWorkerWithTimeout(ctx, cmd) + go func() { + assert.NoError(t, w.Wait()) + }() + defer func() { + err := w.Stop() + if err != nil { + t.Errorf("error stopping the Process: error %v", err) + } + }() + + syncWorker, err := workerImpl.From(w) + if err != nil { + t.Fatal(err) + } + + _, err = syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + if err != nil { + t.Errorf("fail to execute payload: error %v", err) + } + assert.Equal(t, int64(1), w.State().NumExecs()) + + _, err = syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + if err != nil { + t.Errorf("fail to execute payload: error %v", err) + } + assert.Equal(t, int64(2), w.State().NumExecs()) + + _, err = syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + if err != nil { + t.Errorf("fail to execute payload: error %v", err) + } + assert.Equal(t, int64(3), w.State().NumExecs()) +} diff --git a/pkg/pool/config.go b/pkg/pool/config.go new file mode 100644 index 00000000..3dcc3584 --- /dev/null +++ b/pkg/pool/config.go @@ -0,0 +1,75 @@ +package pool + +import ( + "runtime" + "time" +) + +// Configures the pool behaviour. +type Config struct { + // Debug flag creates new fresh worker before every request. + Debug bool + + // NumWorkers defines how many sub-processes can be run at once. This value + // might be doubled by Swapper while hot-swap. Defaults to number of CPU cores. + NumWorkers int64 + + // MaxJobs defines how many executions is allowed for the worker until + // it's destruction. set 1 to create new process for each new task, 0 to let + // worker handle as many tasks as it can. + MaxJobs int64 + + // AllocateTimeout defines for how long pool will be waiting for a worker to + // be freed to handle the task. Defaults to 60s. + AllocateTimeout time.Duration + + // DestroyTimeout defines for how long pool should be waiting for worker to + // properly destroy, if timeout reached worker will be killed. Defaults to 60s. + DestroyTimeout time.Duration + + // Supervision config to limit worker and pool memory usage. + Supervisor *SupervisorConfig +} + +// InitDefaults enables default config values. +func (cfg *Config) InitDefaults() { + if cfg.NumWorkers == 0 { + cfg.NumWorkers = int64(runtime.NumCPU()) + } + + if cfg.AllocateTimeout == 0 { + cfg.AllocateTimeout = time.Minute + } + + if cfg.DestroyTimeout == 0 { + cfg.DestroyTimeout = time.Minute + } + if cfg.Supervisor == nil { + return + } + cfg.Supervisor.InitDefaults() +} + +type SupervisorConfig struct { + // WatchTick defines how often to check the state of worker. + WatchTick uint64 + + // TTL defines maximum time worker is allowed to live. + TTL uint64 + + // IdleTTL defines maximum duration worker can spend in idle mode. Disabled when 0. + IdleTTL uint64 + + // ExecTTL defines maximum lifetime per job. + ExecTTL uint64 + + // MaxWorkerMemory limits memory per worker. + MaxWorkerMemory uint64 +} + +// InitDefaults enables default config values. +func (cfg *SupervisorConfig) InitDefaults() { + if cfg.WatchTick == 0 { + cfg.WatchTick = 1 + } +} diff --git a/pkg/pool/static_pool.go b/pkg/pool/static_pool.go new file mode 100755 index 00000000..23bb2d5f --- /dev/null +++ b/pkg/pool/static_pool.go @@ -0,0 +1,324 @@ +package pool + +import ( + "context" + "os/exec" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + eventsPkg "github.com/spiral/roadrunner/v2/pkg/events" + "github.com/spiral/roadrunner/v2/pkg/payload" + syncWorker "github.com/spiral/roadrunner/v2/pkg/worker" + workerWatcher "github.com/spiral/roadrunner/v2/pkg/worker_watcher" +) + +// StopRequest can be sent by worker to indicate that restart is required. +const StopRequest = "{\"stop\":true}" + +// ErrorEncoder encode error or make a decision based on the error type +type ErrorEncoder func(err error, w worker.BaseProcess) (payload.Payload, error) + +type Options func(p *StaticPool) + +type Command func() *exec.Cmd + +// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of stack. +type StaticPool struct { + cfg Config + + // worker command creator + cmd Command + + // creates and connects to stack + factory worker.Factory + + // distributes the events + events events.Handler + + // saved list of event listeners + listeners []events.EventListener + + // manages worker states and TTLs + ww worker.Watcher + + // allocate new worker + allocator worker.Allocator + + // errEncoder is the default Exec error encoder + errEncoder ErrorEncoder +} + +// Initialize creates new worker pool and task multiplexer. StaticPool will initiate with one worker. +func Initialize(ctx context.Context, cmd Command, factory worker.Factory, cfg Config, options ...Options) (pool.Pool, error) { + const op = errors.Op("Initialize") + if factory == nil { + return nil, errors.E(op, errors.Str("no factory initialized")) + } + cfg.InitDefaults() + + if cfg.Debug { + cfg.NumWorkers = 0 + cfg.MaxJobs = 1 + } + + p := &StaticPool{ + cfg: cfg, + cmd: cmd, + factory: factory, + events: eventsPkg.NewEventsHandler(), + } + + // add pool options + for i := 0; i < len(options); i++ { + options[i](p) + } + + p.allocator = p.newPoolAllocator(ctx, p.cfg.AllocateTimeout, factory, cmd) + p.ww = workerWatcher.NewWorkerWatcher(p.allocator, p.cfg.NumWorkers, p.events) + + workers, err := p.allocateWorkers(p.cfg.NumWorkers) + if err != nil { + return nil, errors.E(op, err) + } + + // put stack in the pool + err = p.ww.AddToWatch(workers) + if err != nil { + return nil, errors.E(op, err) + } + + p.errEncoder = defaultErrEncoder(p) + + // if supervised config not nil, guess, that pool wanted to be supervised + if cfg.Supervisor != nil { + sp := supervisorWrapper(p, p.events, p.cfg.Supervisor) + // start watcher timer + sp.Start() + return sp, nil + } + + return p, nil +} + +func AddListeners(listeners ...events.EventListener) Options { + return func(p *StaticPool) { + p.listeners = listeners + for i := 0; i < len(listeners); i++ { + p.addListener(listeners[i]) + } + } +} + +// AddListener connects event listener to the pool. +func (sp *StaticPool) addListener(listener events.EventListener) { + sp.events.AddListener(listener) +} + +// Config returns associated pool configuration. Immutable. +func (sp *StaticPool) GetConfig() interface{} { + return sp.cfg +} + +// Workers returns worker list associated with the pool. +func (sp *StaticPool) Workers() (workers []worker.BaseProcess) { + return sp.ww.WorkersList() +} + +func (sp *StaticPool) RemoveWorker(wb worker.BaseProcess) error { + return sp.ww.RemoveWorker(wb) +} + +func (sp *StaticPool) Exec(p payload.Payload) (payload.Payload, error) { + const op = errors.Op("exec") + if sp.cfg.Debug { + return sp.execDebug(p) + } + ctxGetFree, cancel := context.WithTimeout(context.Background(), sp.cfg.AllocateTimeout) + defer cancel() + w, err := sp.getWorker(ctxGetFree, op) + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + + rsp, err := w.Exec(p) + if err != nil { + return sp.errEncoder(err, w) + } + + // worker want's to be terminated + // TODO careful with string(rsp.Context) + if len(rsp.Body) == 0 && string(rsp.Context) == StopRequest { + w.State().Set(internal.StateInvalid) + err = w.Stop() + if err != nil { + sp.events.Push(events.WorkerEvent{Event: events.EventWorkerError, Worker: w, Payload: errors.E(op, err)}) + } + + return sp.Exec(p) + } + + if sp.cfg.MaxJobs != 0 && w.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + } else { + sp.ww.PushWorker(w) + } + + return rsp, nil +} + +func (sp *StaticPool) ExecWithContext(ctx context.Context, rqs payload.Payload) (payload.Payload, error) { + const op = errors.Op("exec with context") + ctxGetFree, cancel := context.WithTimeout(ctx, sp.cfg.AllocateTimeout) + defer cancel() + w, err := sp.getWorker(ctxGetFree, op) + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + + rsp, err := w.ExecWithTimeout(ctx, rqs) + if err != nil { + return sp.errEncoder(err, w) + } + + // worker want's to be terminated + if rsp.Body == nil && rsp.Context != nil && string(rsp.Context) == StopRequest { + w.State().Set(internal.StateInvalid) + err = w.Stop() + if err != nil { + sp.events.Push(events.WorkerEvent{Event: events.EventWorkerError, Worker: w, Payload: errors.E(op, err)}) + } + + return sp.ExecWithContext(ctx, rqs) + } + + if sp.cfg.MaxJobs != 0 && w.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + } else { + sp.ww.PushWorker(w) + } + + return rsp, nil +} + +func (sp *StaticPool) getWorker(ctxGetFree context.Context, op errors.Op) (worker.SyncWorker, error) { + // GetFreeWorker function consumes context with timeout + w, err := sp.ww.GetFreeWorker(ctxGetFree) + if err != nil { + // if the error is of kind NoFreeWorkers, it means, that we can't get worker from the stack during the allocate timeout + if errors.Is(errors.NoFreeWorkers, err) { + sp.events.Push(events.PoolEvent{Event: events.EventNoFreeWorkers, Payload: errors.E(op, err)}) + return nil, errors.E(op, err) + } + // else if err not nil - return error + return nil, errors.E(op, err) + } + return w.(worker.SyncWorker), nil +} + +// Destroy all underlying stack (but let them to complete the task). +func (sp *StaticPool) Destroy(ctx context.Context) { + sp.ww.Destroy(ctx) +} + +func defaultErrEncoder(sp *StaticPool) ErrorEncoder { + return func(err error, w worker.BaseProcess) (payload.Payload, error) { + const op = errors.Op("error encoder") + // soft job errors are allowed + if errors.Is(errors.ErrSoftJob, err) { + if sp.cfg.MaxJobs != 0 && w.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + sp.events.Push(events.PoolEvent{Event: events.EventWorkerConstruct, Payload: errors.E(op, err)}) + } + + w.State().Set(internal.StateInvalid) + err = w.Stop() + if err != nil { + sp.events.Push(events.WorkerEvent{Event: events.EventWorkerError, Worker: w, Payload: errors.E(op, err)}) + } + } else { + sp.ww.PushWorker(w) + } + + return payload.Payload{}, errors.E(op, err) + } + + w.State().Set(internal.StateInvalid) + sp.events.Push(events.PoolEvent{Event: events.EventWorkerDestruct, Payload: w}) + errS := w.Stop() + + if errS != nil { + return payload.Payload{}, errors.E(op, errors.Errorf("%v, %v", err, errS)) + } + + return payload.Payload{}, errors.E(op, err) + } +} + +func (sp *StaticPool) newPoolAllocator(ctx context.Context, timeout time.Duration, factory worker.Factory, cmd func() *exec.Cmd) worker.Allocator { + return func() (worker.BaseProcess, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + w, err := factory.SpawnWorkerWithTimeout(ctx, cmd(), sp.listeners...) + if err != nil { + return nil, err + } + + sw, err := syncWorker.From(w) + if err != nil { + return nil, err + } + + sp.events.Push(events.PoolEvent{ + Event: events.EventWorkerConstruct, + Payload: sw, + }) + return sw, nil + } +} + +func (sp *StaticPool) execDebug(p payload.Payload) (payload.Payload, error) { + sw, err := sp.allocator() + if err != nil { + return payload.Payload{}, err + } + + r, err := sw.(worker.SyncWorker).Exec(p) + + if stopErr := sw.Stop(); stopErr != nil { + sp.events.Push(events.WorkerEvent{Event: events.EventWorkerError, Worker: sw, Payload: err}) + } + + return r, err +} + +// allocate required number of stack +func (sp *StaticPool) allocateWorkers(numWorkers int64) ([]worker.BaseProcess, error) { + const op = errors.Op("allocate workers") + var workers []worker.BaseProcess + + // constant number of stack simplify logic + for i := int64(0); i < numWorkers; i++ { + w, err := sp.allocator() + if err != nil { + return nil, errors.E(op, errors.WorkerAllocate, err) + } + + sw, err := syncWorker.From(w) + if err != nil { + return nil, errors.E(op, err) + } + workers = append(workers, sw) + } + return workers, nil +} diff --git a/pkg/pool/static_pool_test.go b/pkg/pool/static_pool_test.go new file mode 100755 index 00000000..acdd6ab7 --- /dev/null +++ b/pkg/pool/static_pool_test.go @@ -0,0 +1,570 @@ +package pool + +import ( + "context" + "log" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/internal" + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/pkg/pipe" + "github.com/stretchr/testify/assert" +) + +var cfg = Config{ + NumWorkers: int64(runtime.NumCPU()), + AllocateTimeout: time.Second * 5, + DestroyTimeout: time.Second * 5, +} + +func Test_NewPool(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + assert.NoError(t, err) + + defer p.Destroy(ctx) + + assert.NotNil(t, p) +} + +func Test_StaticPool_Invalid(t *testing.T) { + p, err := Initialize( + context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../tests/invalid.php") }, + pipe.NewPipeFactory(), + cfg, + ) + + assert.Nil(t, p) + assert.Error(t, err) +} + +func Test_ConfigNoErrorInitDefaults(t *testing.T) { + p, err := Initialize( + context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + Config{ + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + + assert.NotNil(t, p) + assert.NoError(t, err) +} + +func Test_StaticPool_Echo(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + assert.NoError(t, err) + + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_StaticPool_Echo_NilContext(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + assert.NoError(t, err) + + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + res, err := p.Exec(payload.Payload{Body: []byte("hello"), Context: nil}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_StaticPool_Echo_Context(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "head", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + assert.NoError(t, err) + + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + res, err := p.Exec(payload.Payload{Body: []byte("hello"), Context: []byte("world")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Empty(t, res.Body) + assert.NotNil(t, res.Context) + + assert.Equal(t, "world", string(res.Context)) +} + +func Test_StaticPool_JobError(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "error", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + if errors.Is(errors.ErrSoftJob, err) == false { + t.Fatal("error should be of type errors.Exec") + } + + assert.Contains(t, err.Error(), "hello") +} + +func Test_StaticPool_Broken_Replace(t *testing.T) { + ctx := context.Background() + block := make(chan struct{}, 1) + + listener := func(event interface{}) { + if wev, ok := event.(events.WorkerEvent); ok { + if wev.Event == events.EventWorkerLog { + e := string(wev.Payload.([]byte)) + if strings.ContainsAny(e, "undefined_function()") { + block <- struct{}{} + return + } + } + } + } + + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "broken", "pipes") }, + pipe.NewPipeFactory(), + cfg, + AddListeners(listener), + ) + assert.NoError(t, err) + assert.NotNil(t, p) + + time.Sleep(time.Second) + res, err := p.ExecWithContext(ctx, payload.Payload{Body: []byte("hello")}) + assert.Error(t, err) + assert.Nil(t, res.Context) + assert.Nil(t, res.Body) + + <-block + + p.Destroy(ctx) +} + +func Test_StaticPool_Broken_FromOutside(t *testing.T) { + ctx := context.Background() + // Consume pool events + ev := make(chan struct{}, 1) + listener := func(event interface{}) { + if pe, ok := event.(events.PoolEvent); ok { + if pe.Event == events.EventWorkerConstruct { + ev <- struct{}{} + } + } + } + + var cfg = Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 5, + DestroyTimeout: time.Second * 5, + } + + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + cfg, + AddListeners(listener), + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.Equal(t, "hello", res.String()) + assert.Equal(t, 1, len(p.Workers())) + + // first creation + <-ev + // killing random worker and expecting pool to replace it + err = p.Workers()[0].Kill() + if err != nil { + t.Errorf("error killing the process: error %v", err) + } + + // re-creation + <-ev + + list := p.Workers() + for _, w := range list { + assert.Equal(t, internal.StateReady, w.State().Value()) + } +} + +func Test_StaticPool_AllocateTimeout(t *testing.T) { + p, err := Initialize( + context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "delay", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + AllocateTimeout: time.Nanosecond * 1, + DestroyTimeout: time.Second * 2, + }, + ) + assert.Error(t, err) + if !errors.Is(errors.WorkerAllocate, err) { + t.Fatal("error should be of type WorkerAllocate") + } + assert.Nil(t, p) +} + +func Test_StaticPool_Replace_Worker(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "pid", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + MaxJobs: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + var lastPID string + lastPID = strconv.Itoa(int(p.Workers()[0].Pid())) + + res, _ := p.Exec(payload.Payload{Body: []byte("hello")}) + assert.Equal(t, lastPID, string(res.Body)) + + for i := 0; i < 10; i++ { + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.NotEqual(t, lastPID, string(res.Body)) + lastPID = string(res.Body) + } +} + +func Test_StaticPool_Debug_Worker(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "pid", "pipes") }, + pipe.NewPipeFactory(), + Config{ + Debug: true, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + assert.Len(t, p.Workers(), 0) + + var lastPID string + res, _ := p.Exec(payload.Payload{Body: []byte("hello")}) + assert.NotEqual(t, lastPID, string(res.Body)) + + assert.Len(t, p.Workers(), 0) + + for i := 0; i < 10; i++ { + assert.Len(t, p.Workers(), 0) + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.NotEqual(t, lastPID, string(res.Body)) + lastPID = string(res.Body) + } +} + +// identical to replace but controlled on worker side +func Test_StaticPool_Stop_Worker(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "stop", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + var lastPID string + lastPID = strconv.Itoa(int(p.Workers()[0].Pid())) + + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, lastPID, string(res.Body)) + + for i := 0; i < 10; i++ { + res, err := p.Exec(payload.Payload{Body: []byte("hello")}) + + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Empty(t, res.Context) + + assert.NotEqual(t, lastPID, string(res.Body)) + lastPID = string(res.Body) + } +} + +// identical to replace but controlled on worker side +func Test_Static_Pool_Destroy_And_Close(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "delay", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + + assert.NotNil(t, p) + assert.NoError(t, err) + + p.Destroy(ctx) + _, err = p.Exec(payload.Payload{Body: []byte("100")}) + assert.Error(t, err) +} + +// identical to replace but controlled on worker side +func Test_Static_Pool_Destroy_And_Close_While_Wait(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "delay", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + + assert.NotNil(t, p) + assert.NoError(t, err) + + go func() { + _, err := p.Exec(payload.Payload{Body: []byte("100")}) + if err != nil { + t.Errorf("error executing payload: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + p.Destroy(ctx) + _, err = p.Exec(payload.Payload{Body: []byte("100")}) + assert.Error(t, err) +} + +// identical to replace but controlled on worker side +func Test_Static_Pool_Handle_Dead(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../tests/slow-destroy.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 5, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + + assert.NotNil(t, p) + + for _, w := range p.Workers() { + w.State().Set(internal.StateErrored) + } + + _, err = p.Exec(payload.Payload{Body: []byte("hello")}) + assert.Error(t, err) +} + +// identical to replace but controlled on worker side +func Test_Static_Pool_Slow_Destroy(t *testing.T) { + p, err := Initialize( + context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../tests/slow-destroy.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 5, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + + p.Destroy(context.Background()) +} + +func Benchmark_Pool_Echo(b *testing.B) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + cfg, + ) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + if _, err := p.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + } + } +} + +// +func Benchmark_Pool_Echo_Batched(b *testing.B) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: int64(runtime.NumCPU()), + AllocateTimeout: time.Second * 100, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(b, err) + defer p.Destroy(ctx) + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + if _, err := p.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + log.Println(err) + } + }() + } + + wg.Wait() +} + +// +func Benchmark_Pool_Echo_Replaced(b *testing.B) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + Config{ + NumWorkers: 1, + MaxJobs: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(b, err) + defer p.Destroy(ctx) + b.ResetTimer() + b.ReportAllocs() + + for n := 0; n < b.N; n++ { + if _, err := p.Exec(payload.Payload{Body: []byte("hello")}); err != nil { + b.Fail() + log.Println(err) + } + } +} diff --git a/pkg/pool/supervisor_pool.go b/pkg/pool/supervisor_pool.go new file mode 100755 index 00000000..378be7dd --- /dev/null +++ b/pkg/pool/supervisor_pool.go @@ -0,0 +1,206 @@ +package pool + +import ( + "context" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/tools" +) + +const MB = 1024 * 1024 + +type Supervised interface { + pool.Pool + // Start used to start watching process for all pool workers + Start() +} + +type supervised struct { + cfg *SupervisorConfig + events events.Handler + pool pool.Pool + stopCh chan struct{} + mu *sync.RWMutex +} + +func supervisorWrapper(pool pool.Pool, events events.Handler, cfg *SupervisorConfig) Supervised { + sp := &supervised{ + cfg: cfg, + events: events, + pool: pool, + mu: &sync.RWMutex{}, + stopCh: make(chan struct{}), + } + + return sp +} + +type ttlExec struct { + err error + p payload.Payload +} + +func (sp *supervised) ExecWithContext(ctx context.Context, rqs payload.Payload) (payload.Payload, error) { + const op = errors.Op("exec_supervised") + if sp.cfg.ExecTTL == 0 { + return sp.pool.Exec(rqs) + } + + c := make(chan ttlExec, 1) + ctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(sp.cfg.ExecTTL)) + defer cancel() + go func() { + res, err := sp.pool.ExecWithContext(ctx, rqs) + if err != nil { + c <- ttlExec{ + err: errors.E(op, err), + p: payload.Payload{}, + } + } + + c <- ttlExec{ + err: nil, + p: res, + } + }() + + for { + select { + case <-ctx.Done(): + return payload.Payload{}, errors.E(op, errors.TimeOut, ctx.Err()) + case res := <-c: + if res.err != nil { + return payload.Payload{}, res.err + } + + return res.p, nil + } + } +} + +func (sp *supervised) Exec(p payload.Payload) (payload.Payload, error) { + const op = errors.Op("supervised exec") + rsp, err := sp.pool.Exec(p) + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + return rsp, nil +} + +func (sp *supervised) GetConfig() interface{} { + return sp.pool.GetConfig() +} + +func (sp *supervised) Workers() (workers []worker.BaseProcess) { + sp.mu.Lock() + defer sp.mu.Unlock() + return sp.pool.Workers() +} + +func (sp *supervised) RemoveWorker(worker worker.BaseProcess) error { + return sp.pool.RemoveWorker(worker) +} + +func (sp *supervised) Destroy(ctx context.Context) { + sp.pool.Destroy(ctx) +} + +func (sp *supervised) Start() { + go func() { + watchTout := time.NewTicker(time.Second * time.Duration(sp.cfg.WatchTick)) + for { + select { + case <-sp.stopCh: + watchTout.Stop() + return + // stop here + case <-watchTout.C: + sp.mu.Lock() + sp.control() + sp.mu.Unlock() + } + } + }() +} + +func (sp *supervised) Stop() { + sp.stopCh <- struct{}{} +} + +func (sp *supervised) control() { + now := time.Now() + const op = errors.Op("supervised pool control tick") + + // THIS IS A COPY OF WORKERS + workers := sp.pool.Workers() + + for i := 0; i < len(workers); i++ { + if workers[i].State().Value() == internal.StateInvalid { + continue + } + + s, err := tools.WorkerProcessState(workers[i]) + if err != nil { + // worker not longer valid for supervision + continue + } + + if sp.cfg.TTL != 0 && now.Sub(workers[i].Created()).Seconds() >= float64(sp.cfg.TTL) { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(events.PoolEvent{Event: events.EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(events.PoolEvent{Event: events.EventTTL, Payload: workers[i]}) + continue + } + + if sp.cfg.MaxWorkerMemory != 0 && s.MemoryUsage >= sp.cfg.MaxWorkerMemory*MB { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(events.PoolEvent{Event: events.EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(events.PoolEvent{Event: events.EventMaxMemory, Payload: workers[i]}) + continue + } + + // firs we check maxWorker idle + if sp.cfg.IdleTTL != 0 { + // then check for the worker state + if workers[i].State().Value() != internal.StateReady { + continue + } + + /* + Calculate idle time + If worker in the StateReady, we read it LastUsed timestamp as UnixNano uint64 + 2. For example maxWorkerIdle is equal to 5sec, then, if (time.Now - LastUsed) > maxWorkerIdle + we are guessing that worker overlap idle time and has to be killed + */ + + // get last used unix nano + lu := workers[i].State().LastUsed() + + // convert last used to unixNano and sub time.now + res := int64(lu) - now.UnixNano() + + // maxWorkerIdle more than diff between now and last used + if sp.cfg.IdleTTL-uint64(res) <= 0 { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(events.PoolEvent{Event: events.EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(events.PoolEvent{Event: events.EventIdleTTL, Payload: workers[i]}) + } + } + } +} diff --git a/pkg/pool/supervisor_test.go b/pkg/pool/supervisor_test.go new file mode 100644 index 00000000..cb67ebe1 --- /dev/null +++ b/pkg/pool/supervisor_test.go @@ -0,0 +1,155 @@ +package pool + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/pkg/pipe" + "github.com/spiral/roadrunner/v2/tools" + "github.com/stretchr/testify/assert" +) + +var cfgSupervised = Config{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 100, + MaxWorkerMemory: 100, + }, +} + +func TestSupervisedPool_Exec(t *testing.T) { + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/memleak.php", "pipes") }, + pipe.NewPipeFactory(), + cfgSupervised, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + stopCh := make(chan struct{}) + defer p.Destroy(context.Background()) + + go func() { + for { + select { + case <-stopCh: + return + default: + workers := p.Workers() + if len(workers) > 0 { + s, err := tools.WorkerProcessState(workers[0]) + assert.NoError(t, err) + assert.NotNil(t, s) + // since this is soft limit, double max memory limit watch + if (s.MemoryUsage / MB) > cfgSupervised.Supervisor.MaxWorkerMemory*2 { + assert.Fail(t, "max memory reached") + } + } + } + } + }() + + for i := 0; i < 100; i++ { + time.Sleep(time.Millisecond * 50) + _, err = p.Exec(payload.Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + assert.NoError(t, err) + } + + stopCh <- struct{}{} +} + +func TestSupervisedPool_ExecTTL_TimedOut(t *testing.T) { + var cfgExecTTL = Config{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 1, + MaxWorkerMemory: 100, + }, + } + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/sleep.php", "pipes") }, + pipe.NewPipeFactory(), + cfgExecTTL, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + defer p.Destroy(context.Background()) + + pid := p.Workers()[0].Pid() + + resp, err := p.ExecWithContext(context.Background(), payload.Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + + assert.Error(t, err) + assert.Empty(t, resp.Body) + assert.Empty(t, resp.Context) + + time.Sleep(time.Second * 1) + // should be new worker with new pid + assert.NotEqual(t, pid, p.Workers()[0].Pid()) +} + +func TestSupervisedPool_ExecTTL_OK(t *testing.T) { + var cfgExecTTL = Config{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 4, + MaxWorkerMemory: 100, + }, + } + ctx := context.Background() + p, err := Initialize( + ctx, + func() *exec.Cmd { return exec.Command("php", "../../tests/sleep.php", "pipes") }, + pipe.NewPipeFactory(), + cfgExecTTL, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + defer p.Destroy(context.Background()) + + pid := p.Workers()[0].Pid() + + time.Sleep(time.Millisecond * 100) + resp, err := p.Exec(payload.Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + + assert.NoError(t, err) + assert.Empty(t, resp.Body) + assert.Empty(t, resp.Context) + + time.Sleep(time.Second * 1) + // should be the same pid + assert.Equal(t, pid, p.Workers()[0].Pid()) +} diff --git a/pkg/socket/socket_factory.go b/pkg/socket/socket_factory.go new file mode 100755 index 00000000..38b3e7c9 --- /dev/null +++ b/pkg/socket/socket_factory.go @@ -0,0 +1,229 @@ +package socket + +import ( + "context" + "net" + "os/exec" + "sync" + "time" + + "github.com/shirou/gopsutil/process" + "github.com/spiral/errors" + "github.com/spiral/goridge/v3/interfaces/relay" + "github.com/spiral/goridge/v3/pkg/socket" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + workerImpl "github.com/spiral/roadrunner/v2/pkg/worker" + + "go.uber.org/multierr" + "golang.org/x/sync/errgroup" +) + +// Factory connects to external stack using socket server. +type Factory struct { + // listens for incoming connections from underlying processes + ls net.Listener + + // relay connection timeout + tout time.Duration + + // sockets which are waiting for process association + relays sync.Map + + ErrCh chan error +} + +// todo: review + +// NewSocketServer returns Factory attached to a given socket listener. +// tout specifies for how long factory should serve for incoming relay connection +func NewSocketServer(ls net.Listener, tout time.Duration) worker.Factory { + f := &Factory{ + ls: ls, + tout: tout, + relays: sync.Map{}, + ErrCh: make(chan error, 10), + } + + // Be careful + // https://github.com/go101/go101/wiki/About-memory-ordering-guarantees-made-by-atomic-operations-in-Go + // https://github.com/golang/go/issues/5045 + go func() { + f.ErrCh <- f.listen() + }() + + return f +} + +// blocking operation, returns an error +func (f *Factory) listen() error { + errGr := &errgroup.Group{} + errGr.Go(func() error { + for { + conn, err := f.ls.Accept() + if err != nil { + return err + } + + rl := socket.NewSocketRelay(conn) + pid, err := internal.FetchPID(rl) + if err != nil { + return err + } + + f.attachRelayToPid(pid, rl) + } + }) + + return errGr.Wait() +} + +type socketSpawn struct { + w worker.BaseProcess + err error +} + +// SpawnWorker creates Process and connects it to appropriate relay or returns error +func (f *Factory) SpawnWorkerWithTimeout(ctx context.Context, cmd *exec.Cmd, listeners ...events.EventListener) (worker.BaseProcess, error) { + const op = errors.Op("spawn_worker_with_context") + c := make(chan socketSpawn) + go func() { + ctx, cancel := context.WithTimeout(ctx, f.tout) + defer cancel() + w, err := workerImpl.InitBaseWorker(cmd, workerImpl.AddListeners(listeners...)) + if err != nil { + c <- socketSpawn{ + w: nil, + err: err, + } + return + } + + err = w.Start() + if err != nil { + c <- socketSpawn{ + w: nil, + err: errors.E(op, err), + } + return + } + + rl, err := f.findRelayWithContext(ctx, w) + if err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + + c <- socketSpawn{ + w: nil, + err: errors.E(op, err), + } + return + } + + w.AttachRelay(rl) + w.State().Set(internal.StateReady) + + c <- socketSpawn{ + w: w, + err: nil, + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-c: + if res.err != nil { + return nil, res.err + } + + return res.w, nil + } +} + +func (f *Factory) SpawnWorker(cmd *exec.Cmd, listeners ...events.EventListener) (worker.BaseProcess, error) { + const op = errors.Op("spawn_worker") + w, err := workerImpl.InitBaseWorker(cmd, workerImpl.AddListeners(listeners...)) + if err != nil { + return nil, err + } + + err = w.Start() + if err != nil { + return nil, errors.E(op, err) + } + + rl, err := f.findRelay(w) + if err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + return nil, err + } + + w.AttachRelay(rl) + w.State().Set(internal.StateReady) + + return w, nil +} + +// Close socket factory and underlying socket connection. +func (f *Factory) Close() error { + return f.ls.Close() +} + +// waits for Process to connect over socket and returns associated relay of timeout +func (f *Factory) findRelayWithContext(ctx context.Context, w worker.BaseProcess) (*socket.Relay, error) { + ticker := time.NewTicker(time.Millisecond * 100) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + _, err := process.NewProcess(int32(w.Pid())) + if err != nil { + return nil, err + } + default: + tmp, ok := f.relays.Load(w.Pid()) + if !ok { + continue + } + return tmp.(*socket.Relay), nil + } + } +} + +func (f *Factory) findRelay(w worker.BaseProcess) (*socket.Relay, error) { + const op = errors.Op("find_relay") + // poll every 1ms for the relay + pollDone := time.NewTimer(f.tout) + for { + select { + case <-pollDone.C: + return nil, errors.E(op, errors.Str("relay timeout")) + default: + tmp, ok := f.relays.Load(w.Pid()) + if !ok { + continue + } + return tmp.(*socket.Relay), nil + } + } +} + +// chan to store relay associated with specific pid +func (f *Factory) attachRelayToPid(pid int64, relay relay.Relay) { + f.relays.Store(pid, relay) +} + +// deletes relay chan associated with specific pid +func (f *Factory) removeRelayFromPid(pid int64) { + f.relays.Delete(pid) +} diff --git a/socket_factory_test.go b/pkg/socket/socket_factory_test.go index abb40f16..983f3e8e 100644..100755 --- a/socket_factory_test.go +++ b/pkg/socket/socket_factory_test.go @@ -1,14 +1,20 @@ -package roadrunner +package socket import ( - "github.com/stretchr/testify/assert" + "context" "net" "os/exec" + "sync" "testing" "time" + + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/stretchr/testify/assert" ) func Test_Tcp_Start(t *testing.T) { + ctx := context.Background() time.Sleep(time.Millisecond * 10) // to ensure free socket ls, err := net.Listen("tcp", "localhost:9007") @@ -23,9 +29,9 @@ func Test_Tcp_Start(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "echo", "tcp") - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) assert.NoError(t, err) assert.NotNil(t, w) @@ -35,22 +41,22 @@ func Test_Tcp_Start(t *testing.T) { err = w.Stop() if err != nil { - t.Errorf("error stopping the worker: error %v", err) + t.Errorf("error stopping the Process: error %v", err) } } func Test_Tcp_StartCloseFactory(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { } else { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "echo", "tcp") - f := NewSocketFactory(ls, time.Minute) + f := NewSocketServer(ls, time.Minute) defer func() { err := ls.Close() if err != nil { @@ -58,23 +64,19 @@ func Test_Tcp_StartCloseFactory(t *testing.T) { } }() - w, err := f.SpawnWorker(cmd) + w, err := f.SpawnWorkerWithTimeout(ctx, cmd) assert.NoError(t, err) assert.NotNil(t, w) - go func() { - assert.NoError(t, w.Wait()) - }() - err = w.Stop() if err != nil { - t.Errorf("error stopping the worker: error %v", err) + t.Errorf("error stopping the Process: error %v", err) } } func Test_Tcp_StartError(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -87,19 +89,20 @@ func Test_Tcp_StartError(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + cmd := exec.Command("php", "../../tests/client.php", "echo", "pipes") err = cmd.Start() if err != nil { t.Errorf("error executing the command: error %v", err) } - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) assert.Error(t, err) assert.Nil(t, w) } func Test_Tcp_Failboot(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { @@ -113,9 +116,9 @@ func Test_Tcp_Failboot(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/failboot.php") + cmd := exec.Command("php", "../../tests/failboot.php") - w, err2 := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err2 := NewSocketServer(ls, time.Second*5).SpawnWorkerWithTimeout(ctx, cmd) assert.Nil(t, w) assert.Error(t, err2) assert.Contains(t, err2.Error(), "failboot") @@ -123,7 +126,7 @@ func Test_Tcp_Failboot(t *testing.T) { func Test_Tcp_Timeout(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -136,17 +139,17 @@ func Test_Tcp_Timeout(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/slow-client.php", "echo", "tcp", "200", "0") + cmd := exec.Command("php", "../../tests/slow-client.php", "echo", "tcp", "200", "0") - w, err := NewSocketFactory(ls, time.Millisecond*100).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Millisecond*1).SpawnWorkerWithTimeout(ctx, cmd) assert.Nil(t, w) assert.Error(t, err) - assert.Contains(t, err.Error(), "relay timeout") + assert.Contains(t, err.Error(), "context deadline exceeded") } func Test_Tcp_Invalid(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -159,16 +162,16 @@ func Test_Tcp_Invalid(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/invalid.php") + cmd := exec.Command("php", "../../tests/invalid.php") - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Second*1).SpawnWorkerWithTimeout(ctx, cmd) assert.Error(t, err) assert.Nil(t, w) } func Test_Tcp_Broken(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -181,12 +184,17 @@ func Test_Tcp_Broken(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "broken", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "broken", "tcp") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } + wg := sync.WaitGroup{} + wg.Add(1) go func() { + defer wg.Done() err := w.Wait() - assert.Error(t, err) assert.Contains(t, err.Error(), "undefined_function()") }() @@ -194,18 +202,25 @@ func Test_Tcp_Broken(t *testing.T) { defer func() { time.Sleep(time.Second) err2 := w.Stop() - assert.NoError(t, err2) + // write tcp 127.0.0.1:9007->127.0.0.1:34204: use of closed network connection + assert.Error(t, err2) }() - res, err := w.Exec(&Payload{Body: []byte("hello")}) + sw, err := worker.From(w) + if err != nil { + t.Fatal(err) + } + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) assert.Error(t, err) - assert.Nil(t, res) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + wg.Wait() } func Test_Tcp_Echo(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -218,30 +233,36 @@ func Test_Tcp_Echo(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "echo", "tcp") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, _ := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) go func() { assert.NoError(t, w.Wait()) }() defer func() { err = w.Stop() if err != nil { - t.Errorf("error stopping the worker: error %v", err) + t.Errorf("error stopping the Process: error %v", err) } }() - res, err := w.Exec(&Payload{Body: []byte("hello")}) + sw, err := worker.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) + assert.Empty(t, res.Context) assert.Equal(t, "hello", res.String()) } func Test_Unix_Start(t *testing.T) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -254,9 +275,9 @@ func Test_Unix_Start(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "unix") + cmd := exec.Command("php", "../../tests/client.php", "echo", "unix") - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) assert.NoError(t, err) assert.NotNil(t, w) @@ -266,12 +287,13 @@ func Test_Unix_Start(t *testing.T) { err = w.Stop() if err != nil { - t.Errorf("error stopping the worker: error %v", err) + t.Errorf("error stopping the Process: error %v", err) } } func Test_Unix_Failboot(t *testing.T) { ls, err := net.Listen("unix", "sock.unix") + ctx := context.Background() if err == nil { defer func() { err := ls.Close() @@ -283,9 +305,9 @@ func Test_Unix_Failboot(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/failboot.php") + cmd := exec.Command("php", "../../tests/failboot.php") - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Second*5).SpawnWorkerWithTimeout(ctx, cmd) assert.Nil(t, w) assert.Error(t, err) assert.Contains(t, err.Error(), "failboot") @@ -293,6 +315,7 @@ func Test_Unix_Failboot(t *testing.T) { func Test_Unix_Timeout(t *testing.T) { ls, err := net.Listen("unix", "sock.unix") + ctx := context.Background() if err == nil { defer func() { err := ls.Close() @@ -304,15 +327,16 @@ func Test_Unix_Timeout(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/slow-client.php", "echo", "unix", "200", "0") + cmd := exec.Command("php", "../../tests/slow-client.php", "echo", "unix", "200", "0") - w, err := NewSocketFactory(ls, time.Millisecond*100).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Millisecond*100).SpawnWorkerWithTimeout(ctx, cmd) assert.Nil(t, w) assert.Error(t, err) - assert.Contains(t, err.Error(), "relay timeout") + assert.Contains(t, err.Error(), "context deadline exceeded") } func Test_Unix_Invalid(t *testing.T) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -325,14 +349,15 @@ func Test_Unix_Invalid(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/invalid.php") + cmd := exec.Command("php", "../../tests/invalid.php") - w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Second*10).SpawnWorkerWithTimeout(ctx, cmd) assert.Error(t, err) assert.Nil(t, w) } func Test_Unix_Broken(t *testing.T) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -345,10 +370,16 @@ func Test_Unix_Broken(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "broken", "unix") + cmd := exec.Command("php", "../../tests/client.php", "broken", "unix") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } + wg := &sync.WaitGroup{} + wg.Add(1) go func() { + defer wg.Done() err := w.Wait() assert.Error(t, err) assert.Contains(t, err.Error(), "undefined_function()") @@ -357,16 +388,24 @@ func Test_Unix_Broken(t *testing.T) { defer func() { time.Sleep(time.Second) err = w.Stop() - assert.NoError(t, err) + assert.Error(t, err) }() - res, err := w.Exec(&Payload{Body: []byte("hello")}) + sw, err := worker.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) assert.Error(t, err) - assert.Nil(t, res) + assert.Nil(t, res.Context) + assert.Nil(t, res.Body) + wg.Wait() } func Test_Unix_Echo(t *testing.T) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -379,34 +418,43 @@ func Test_Unix_Echo(t *testing.T) { t.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "unix") + cmd := exec.Command("php", "../../tests/client.php", "echo", "unix") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + t.Fatal(err) + } go func() { assert.NoError(t, w.Wait()) }() defer func() { err = w.Stop() if err != nil { - t.Errorf("error stopping the worker: error %v", err) + t.Errorf("error stopping the Process: error %v", err) } }() - res, err := w.Exec(&Payload{Body: []byte("hello")}) + sw, err := worker.From(w) + if err != nil { + t.Fatal(err) + } + + res, err := sw.Exec(payload.Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) + assert.Empty(t, res.Context) assert.Equal(t, "hello", res.String()) } func Benchmark_Tcp_SpawnWorker_Stop(b *testing.B) { + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if err == nil { defer func() { - err := ls.Close() + err = ls.Close() if err != nil { b.Errorf("error closing the listener: error %v", err) } @@ -415,29 +463,31 @@ func Benchmark_Tcp_SpawnWorker_Stop(b *testing.B) { b.Skip("socket is busy") } - f := NewSocketFactory(ls, time.Minute) + f := NewSocketServer(ls, time.Minute) for n := 0; n < b.N; n++ { - cmd := exec.Command("php", "tests/client.php", "echo", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "echo", "tcp") - w, _ := f.SpawnWorker(cmd) + w, err := f.SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } go func() { - if w.Wait() != nil { - b.Fail() - } + assert.NoError(b, w.Wait()) }() err = w.Stop() if err != nil { - b.Errorf("error stopping the worker: error %v", err) + b.Errorf("error stopping the Process: error %v", err) } } } func Benchmark_Tcp_Worker_ExecEcho(b *testing.B) { + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if err == nil { defer func() { - err := ls.Close() + err = ls.Close() if err != nil { b.Errorf("error closing the listener: error %v", err) } @@ -446,30 +496,33 @@ func Benchmark_Tcp_Worker_ExecEcho(b *testing.B) { b.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "tcp") + cmd := exec.Command("php", "../../tests/client.php", "echo", "tcp") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) - go func() { - err := w.Wait() - if err != nil { - b.Errorf("error waiting: %v", err) - } - }() + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } defer func() { err = w.Stop() if err != nil { - b.Errorf("error stopping the worker: error %v", err) + b.Errorf("error stopping the Process: error %v", err) } }() + sw, err := worker.From(w) + if err != nil { + b.Fatal(err) + } + for n := 0; n < b.N; n++ { - if _, err := w.Exec(&Payload{Body: []byte("hello")}); err != nil { + if _, err := sw.Exec(payload.Payload{Body: []byte("hello")}); err != nil { b.Fail() } } } func Benchmark_Unix_SpawnWorker_Stop(b *testing.B) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -482,25 +535,23 @@ func Benchmark_Unix_SpawnWorker_Stop(b *testing.B) { b.Skip("socket is busy") } - f := NewSocketFactory(ls, time.Minute) + f := NewSocketServer(ls, time.Minute) for n := 0; n < b.N; n++ { - cmd := exec.Command("php", "tests/client.php", "echo", "unix") - - w, _ := f.SpawnWorker(cmd) - go func() { - if w.Wait() != nil { - b.Fail() - } - }() + cmd := exec.Command("php", "../../tests/client.php", "echo", "unix") + w, err := f.SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } err = w.Stop() if err != nil { - b.Errorf("error stopping the worker: error %v", err) + b.Errorf("error stopping the Process: error %v", err) } } } func Benchmark_Unix_Worker_ExecEcho(b *testing.B) { + ctx := context.Background() ls, err := net.Listen("unix", "sock.unix") if err == nil { defer func() { @@ -513,24 +564,26 @@ func Benchmark_Unix_Worker_ExecEcho(b *testing.B) { b.Skip("socket is busy") } - cmd := exec.Command("php", "tests/client.php", "echo", "unix") + cmd := exec.Command("php", "../../tests/client.php", "echo", "unix") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) - go func() { - err := w.Wait() - if err != nil { - b.Errorf("error waiting: %v", err) - } - }() + w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithTimeout(ctx, cmd) + if err != nil { + b.Fatal(err) + } defer func() { err = w.Stop() if err != nil { - b.Errorf("error stopping the worker: error %v", err) + b.Errorf("error stopping the Process: error %v", err) } }() + sw, err := worker.From(w) + if err != nil { + b.Fatal(err) + } + for n := 0; n < b.N; n++ { - if _, err := w.Exec(&Payload{Body: []byte("hello")}); err != nil { + if _, err := sw.Exec(payload.Payload{Body: []byte("hello")}); err != nil { b.Fail() } } diff --git a/pkg/worker/sync_worker.go b/pkg/worker/sync_worker.go new file mode 100755 index 00000000..13212cc6 --- /dev/null +++ b/pkg/worker/sync_worker.go @@ -0,0 +1,224 @@ +package worker + +import ( + "bytes" + "context" + "time" + + "github.com/spiral/errors" + "github.com/spiral/goridge/v3/interfaces/relay" + "github.com/spiral/goridge/v3/pkg/frame" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + "github.com/spiral/roadrunner/v2/pkg/payload" + "go.uber.org/multierr" +) + +type syncWorker struct { + w worker.BaseProcess +} + +// From creates SyncWorker from BaseProcess +func From(w worker.BaseProcess) (worker.SyncWorker, error) { + return &syncWorker{ + w: w, + }, nil +} + +// Exec payload without TTL timeout. +func (tw *syncWorker) Exec(p payload.Payload) (payload.Payload, error) { + const op = errors.Op("sync worker Exec") + if len(p.Body) == 0 && len(p.Context) == 0 { + return payload.Payload{}, errors.E(op, errors.Str("payload can not be empty")) + } + + if tw.w.State().Value() != internal.StateReady { + return payload.Payload{}, errors.E(op, errors.Errorf("Process is not ready (%s)", tw.w.State().String())) + } + + // set last used time + tw.w.State().SetLastUsed(uint64(time.Now().UnixNano())) + tw.w.State().Set(internal.StateWorking) + + rsp, err := tw.execPayload(p) + if err != nil { + // just to be more verbose + if errors.Is(errors.ErrSoftJob, err) == false { + tw.w.State().Set(internal.StateErrored) + tw.w.State().RegisterExec() + } + return payload.Payload{}, err + } + + tw.w.State().Set(internal.StateReady) + tw.w.State().RegisterExec() + + return rsp, nil +} + +type wexec struct { + payload payload.Payload + err error +} + +// Exec payload without TTL timeout. +func (tw *syncWorker) ExecWithTimeout(ctx context.Context, p payload.Payload) (payload.Payload, error) { + const op = errors.Op("ExecWithTimeout") + c := make(chan wexec, 1) + + go func() { + if len(p.Body) == 0 && len(p.Context) == 0 { + c <- wexec{ + payload: payload.Payload{}, + err: errors.E(op, errors.Str("payload can not be empty")), + } + return + } + + if tw.w.State().Value() != internal.StateReady { + c <- wexec{ + payload: payload.Payload{}, + err: errors.E(op, errors.Errorf("Process is not ready (%s)", tw.w.State().String())), + } + return + } + + // set last used time + tw.w.State().SetLastUsed(uint64(time.Now().UnixNano())) + tw.w.State().Set(internal.StateWorking) + + rsp, err := tw.execPayload(p) + if err != nil { + // just to be more verbose + if errors.Is(errors.ErrSoftJob, err) == false { + tw.w.State().Set(internal.StateErrored) + tw.w.State().RegisterExec() + } + c <- wexec{ + payload: payload.Payload{}, + err: errors.E(op, err), + } + return + } + + tw.w.State().Set(internal.StateReady) + tw.w.State().RegisterExec() + + c <- wexec{ + payload: rsp, + err: nil, + } + }() + + select { + case <-ctx.Done(): + err := multierr.Combine(tw.Kill()) + if err != nil { + return payload.Payload{}, multierr.Append(err, ctx.Err()) + } + return payload.Payload{}, ctx.Err() + case res := <-c: + if res.err != nil { + return payload.Payload{}, res.err + } + return res.payload, nil + } +} + +func (tw *syncWorker) execPayload(p payload.Payload) (payload.Payload, error) { + const op = errors.Op("exec pl") + + fr := frame.NewFrame() + fr.WriteVersion(frame.VERSION_1) + // can be 0 here + + buf := new(bytes.Buffer) + buf.Write(p.Context) + buf.Write(p.Body) + + // Context offset + fr.WriteOptions(uint32(len(p.Context))) + fr.WritePayloadLen(uint32(buf.Len())) + fr.WritePayload(buf.Bytes()) + + fr.WriteCRC() + + // empty and free the buffer + buf.Truncate(0) + + err := tw.Relay().Send(fr) + if err != nil { + return payload.Payload{}, err + } + + frameR := frame.NewFrame() + + err = tw.w.Relay().Receive(frameR) + if err != nil { + return payload.Payload{}, errors.E(op, err) + } + if frameR == nil { + return payload.Payload{}, errors.E(op, errors.Str("nil fr received")) + } + + if !frameR.VerifyCRC() { + return payload.Payload{}, errors.E(op, errors.Str("failed to verify CRC")) + } + + flags := frameR.ReadFlags() + + if flags&byte(frame.ERROR) != byte(0) { + return payload.Payload{}, errors.E(op, errors.ErrSoftJob, errors.Str(string(frameR.Payload()))) + } + + options := frameR.ReadOptions() + if len(options) != 1 { + return payload.Payload{}, errors.E(op, errors.Str("options length should be equal 1 (body offset)")) + } + + pl := payload.Payload{} + pl.Context = frameR.Payload()[:options[0]] + pl.Body = frameR.Payload()[options[0]:] + + return pl, nil +} + +func (tw *syncWorker) String() string { + return tw.w.String() +} + +func (tw *syncWorker) Pid() int64 { + return tw.w.Pid() +} + +func (tw *syncWorker) Created() time.Time { + return tw.w.Created() +} + +func (tw *syncWorker) State() internal.State { + return tw.w.State() +} + +func (tw *syncWorker) Start() error { + return tw.w.Start() +} + +func (tw *syncWorker) Wait() error { + return tw.w.Wait() +} + +func (tw *syncWorker) Stop() error { + return tw.w.Stop() +} + +func (tw *syncWorker) Kill() error { + return tw.w.Kill() +} + +func (tw *syncWorker) Relay() relay.Relay { + return tw.w.Relay() +} + +func (tw *syncWorker) AttachRelay(rl relay.Relay) { + tw.w.AttachRelay(rl) +} diff --git a/pkg/worker/sync_worker_test.go b/pkg/worker/sync_worker_test.go new file mode 100755 index 00000000..40988b06 --- /dev/null +++ b/pkg/worker/sync_worker_test.go @@ -0,0 +1,37 @@ +package worker + +import ( + "os/exec" + "testing" + + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/stretchr/testify/assert" +) + +func Test_NotStarted_String(t *testing.T) { + cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + + w, _ := InitBaseWorker(cmd) + assert.Contains(t, w.String(), "php tests/client.php echo pipes") + assert.Contains(t, w.String(), "inactive") + assert.Contains(t, w.String(), "numExecs: 0") +} + +func Test_NotStarted_Exec(t *testing.T) { + cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + + w, _ := InitBaseWorker(cmd) + + syncWorker, err := From(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(payload.Payload{Body: []byte("hello")}) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + assert.Contains(t, err.Error(), "Process is not ready (inactive)") +} diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go new file mode 100755 index 00000000..db182a3e --- /dev/null +++ b/pkg/worker/worker.go @@ -0,0 +1,315 @@ +package worker + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/goridge/v3/interfaces/relay" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" + eventsPkg "github.com/spiral/roadrunner/v2/pkg/events" + "go.uber.org/multierr" +) + +const ( + // WaitDuration - for how long error buffer should attempt to aggregate error messages + // before merging output together since lastError update (required to keep error update together). + WaitDuration = 25 * time.Millisecond + + // ReadBufSize used to make a slice with specified length to read from stderr + ReadBufSize = 10240 // Kb +) + +type Options func(p *Process) + +// Process - supervised process with api over goridge.Relay. +type Process struct { + // created indicates at what time Process has been created. + created time.Time + + // updates parent supervisor or pool about Process events + events events.Handler + + // state holds information about current Process state, + // number of Process executions, buf status change time. + // publicly this object is receive-only and protected using Mutex + // and atomic counter. + state *internal.WorkerState + + // underlying command with associated process, command must be + // provided to Process from outside in non-started form. CmdSource + // stdErr direction will be handled by Process to aggregate error message. + cmd *exec.Cmd + + // pid of the process, points to pid of underlying process and + // can be nil while process is not started. + pid int + + // stderr aggregates stderr output from underlying process. Value can be + // receive only once command is completed and all pipes are closed. + stderr *bytes.Buffer + + // channel is being closed once command is complete. + // waitDone chan interface{} + + // contains information about resulted process state. + endState *os.ProcessState + + // ensures than only one execution can be run at once. + mu sync.RWMutex + + // communication bus with underlying process. + relay relay.Relay + // rd in a second part of pipe to read from stderr + rd io.Reader + // stop signal terminates io.Pipe from reading from stderr + stop chan struct{} + + syncPool sync.Pool +} + +// InitBaseWorker creates new Process over given exec.cmd. +func InitBaseWorker(cmd *exec.Cmd, options ...Options) (worker.BaseProcess, error) { + if cmd.Process != nil { + return nil, fmt.Errorf("can't attach to running process") + } + w := &Process{ + created: time.Now(), + events: eventsPkg.NewEventsHandler(), + cmd: cmd, + state: internal.NewWorkerState(internal.StateInactive), + stderr: new(bytes.Buffer), + stop: make(chan struct{}, 1), + // sync pool for STDERR + // All receivers are pointers + syncPool: sync.Pool{ + New: func() interface{} { + buf := make([]byte, ReadBufSize) + return &buf + }, + }, + } + + w.rd, w.cmd.Stderr = io.Pipe() + + // small buffer optimization + // at this point we know, that stderr will contain huge messages + w.stderr.Grow(ReadBufSize) + + // add options + for i := 0; i < len(options); i++ { + options[i](w) + } + + go func() { + w.watch() + }() + + return w, nil +} + +func AddListeners(listeners ...events.EventListener) Options { + return func(p *Process) { + for i := 0; i < len(listeners); i++ { + p.addListener(listeners[i]) + } + } +} + +// Pid returns worker pid. +func (w *Process) Pid() int64 { + return int64(w.pid) +} + +// Created returns time worker was created at. +func (w *Process) Created() time.Time { + return w.created +} + +// AddListener registers new worker event listener. +func (w *Process) addListener(listener events.EventListener) { + w.events.AddListener(listener) +} + +// State return receive-only Process state object, state can be used to safely access +// Process status, time when status changed and number of Process executions. +func (w *Process) State() internal.State { + return w.state +} + +// State return receive-only Process state object, state can be used to safely access +// Process status, time when status changed and number of Process executions. +func (w *Process) AttachRelay(rl relay.Relay) { + w.relay = rl +} + +// State return receive-only Process state object, state can be used to safely access +// Process status, time when status changed and number of Process executions. +func (w *Process) Relay() relay.Relay { + return w.relay +} + +// String returns Process description. fmt.Stringer interface +func (w *Process) String() string { + st := w.state.String() + // we can safely compare pid to 0 + if w.pid != 0 { + st = st + ", pid:" + strconv.Itoa(w.pid) + } + + return fmt.Sprintf( + "(`%s` [%s], numExecs: %v)", + strings.Join(w.cmd.Args, " "), + st, + w.state.NumExecs(), + ) +} + +func (w *Process) Start() error { + err := w.cmd.Start() + if err != nil { + return err + } + w.pid = w.cmd.Process.Pid + return nil +} + +// Wait must be called once for each Process, call will be released once Process is +// complete and will return process error (if any), if stderr is presented it's value +// will be wrapped as WorkerError. Method will return error code if php process fails +// to find or Start the script. +func (w *Process) Wait() error { + const op = errors.Op("worker process wait") + err := multierr.Combine(w.cmd.Wait()) + + if w.State().Value() == internal.StateDestroyed { + return errors.E(op, err) + } + + // at this point according to the documentation (see cmd.Wait comment) + // if worker finishes with an error, message will be written to the stderr first + // and then w.cmd.Wait return an error + w.endState = w.cmd.ProcessState + if err != nil { + w.state.Set(internal.StateErrored) + + w.mu.RLock() + // if process return code > 0, here will be an error from stderr (if presents) + if w.stderr.Len() > 0 { + err = multierr.Append(err, errors.E(op, errors.Str(w.stderr.String()))) + // stop the stderr buffer + w.stop <- struct{}{} + } + w.mu.RUnlock() + + return multierr.Append(err, w.closeRelay()) + } + + err = multierr.Append(err, w.closeRelay()) + if err != nil { + w.state.Set(internal.StateErrored) + return err + } + + if w.endState.Success() { + w.state.Set(internal.StateStopped) + } + + return nil +} + +func (w *Process) closeRelay() error { + if w.relay != nil { + err := w.relay.Close() + if err != nil { + return err + } + } + return nil +} + +// Stop sends soft termination command to the Process and waits for process completion. +func (w *Process) Stop() error { + var err error + w.state.Set(internal.StateStopping) + err = multierr.Append(err, internal.SendControl(w.relay, &internal.StopCommand{Stop: true})) + if err != nil { + w.state.Set(internal.StateKilling) + return multierr.Append(err, w.cmd.Process.Kill()) + } + w.state.Set(internal.StateStopped) + return nil +} + +// Kill kills underlying process, make sure to call Wait() func to gather +// error log from the stderr. Does not waits for process completion! +func (w *Process) Kill() error { + if w.State().Value() == internal.StateDestroyed { + err := w.cmd.Process.Signal(os.Kill) + if err != nil { + return err + } + return nil + } + + w.state.Set(internal.StateKilling) + err := w.cmd.Process.Signal(os.Kill) + if err != nil { + return err + } + w.state.Set(internal.StateStopped) + return nil +} + +// put the pointer, to not allocate new slice +// but erase it len and then return back +func (w *Process) put(data *[]byte) { + w.syncPool.Put(data) +} + +// get pointer to the byte slice +func (w *Process) get() *[]byte { + return w.syncPool.Get().(*[]byte) +} + +// Write appends the contents of pool to the errBuffer, growing the errBuffer as +// needed. The return value n is the length of pool; errBuffer is always nil. +func (w *Process) watch() { + go func() { + for { + select { + case <-w.stop: + buf := w.get() + // read the last data + n, _ := w.rd.Read(*buf) + w.events.Push(events.WorkerEvent{Event: events.EventWorkerLog, Worker: w, Payload: (*buf)[:n]}) + w.mu.Lock() + // write new message + // we are sending only n read bytes, without sending previously written message as bytes slice from syncPool + w.stderr.Write((*buf)[:n]) + w.mu.Unlock() + w.put(buf) + return + default: + // read the max 10kb of stderr per one read + buf := w.get() + n, _ := w.rd.Read(*buf) + w.events.Push(events.WorkerEvent{Event: events.EventWorkerLog, Worker: w, Payload: (*buf)[:n]}) + w.mu.Lock() + // write new message + w.stderr.Write((*buf)[:n]) + w.mu.Unlock() + w.put(buf) + } + } + }() +} diff --git a/pkg/worker/worker_test.go b/pkg/worker/worker_test.go new file mode 100755 index 00000000..805f66b5 --- /dev/null +++ b/pkg/worker/worker_test.go @@ -0,0 +1,19 @@ +package worker + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_OnStarted(t *testing.T) { + cmd := exec.Command("php", "tests/client.php", "broken", "pipes") + assert.Nil(t, cmd.Start()) + + w, err := InitBaseWorker(cmd) + assert.Nil(t, w) + assert.NotNil(t, err) + + assert.Equal(t, "can't attach to running process", err.Error()) +} diff --git a/pkg/worker_watcher/worker_watcher.go b/pkg/worker_watcher/worker_watcher.go new file mode 100755 index 00000000..39d334ba --- /dev/null +++ b/pkg/worker_watcher/worker_watcher.go @@ -0,0 +1,302 @@ +package worker_watcher //nolint:golint,stylecheck + +import ( + "context" + "runtime" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/internal" +) + +type Stack struct { + workers []worker.BaseProcess + mutex sync.RWMutex + destroy bool + actualNumOfWorkers int64 +} + +func NewWorkersStack() *Stack { + w := runtime.NumCPU() + return &Stack{ + workers: make([]worker.BaseProcess, 0, w), + actualNumOfWorkers: 0, + } +} + +func (stack *Stack) Reset() { + stack.mutex.Lock() + defer stack.mutex.Unlock() + stack.actualNumOfWorkers = 0 + stack.workers = nil +} + +// Push worker back to the stack +// If stack in destroy state, Push will provide 100ms window to unlock the mutex +func (stack *Stack) Push(w worker.BaseProcess) { + stack.mutex.Lock() + defer stack.mutex.Unlock() + stack.actualNumOfWorkers++ + stack.workers = append(stack.workers, w) +} + +func (stack *Stack) IsEmpty() bool { + stack.mutex.Lock() + defer stack.mutex.Unlock() + return len(stack.workers) == 0 +} + +func (stack *Stack) Pop() (worker.BaseProcess, bool) { + stack.mutex.Lock() + defer stack.mutex.Unlock() + + // do not release new stack + if stack.destroy { + return nil, true + } + + if len(stack.workers) == 0 { + return nil, false + } + + // move worker + w := stack.workers[len(stack.workers)-1] + stack.workers = stack.workers[:len(stack.workers)-1] + stack.actualNumOfWorkers-- + return w, false +} + +func (stack *Stack) FindAndRemoveByPid(pid int64) bool { + stack.mutex.Lock() + defer stack.mutex.Unlock() + for i := 0; i < len(stack.workers); i++ { + // worker in the stack, reallocating + if stack.workers[i].Pid() == pid { + stack.workers = append(stack.workers[:i], stack.workers[i+1:]...) + stack.actualNumOfWorkers-- + // worker found and removed + return true + } + } + // no worker with such ID + return false +} + +// Workers return copy of the workers in the stack +func (stack *Stack) Workers() []worker.BaseProcess { + stack.mutex.Lock() + defer stack.mutex.Unlock() + workersCopy := make([]worker.BaseProcess, 0, 1) + // copy + for _, v := range stack.workers { + workersCopy = append(workersCopy, v) + } + + return workersCopy +} + +func (stack *Stack) isDestroying() bool { + stack.mutex.Lock() + defer stack.mutex.Unlock() + return stack.destroy +} + +// we also have to give a chance to pool to Push worker (return it) +func (stack *Stack) Destroy(ctx context.Context) { + stack.mutex.Lock() + stack.destroy = true + stack.mutex.Unlock() + + tt := time.NewTicker(time.Millisecond * 100) + for { + select { + case <-tt.C: + stack.mutex.Lock() + // that might be one of the workers is working + if len(stack.workers) != int(stack.actualNumOfWorkers) { + stack.mutex.Unlock() + continue + } + stack.mutex.Unlock() + // unnecessary mutex, but + // just to make sure. All stack at this moment are in the stack + // Pop operation is blocked, push can't be done, since it's not possible to pop + stack.mutex.Lock() + for i := 0; i < len(stack.workers); i++ { + // set state for the stack in the stack (unused at the moment) + stack.workers[i].State().Set(internal.StateDestroyed) + // kill the worker + _ = stack.workers[i].Kill() + } + stack.mutex.Unlock() + tt.Stop() + // clear + stack.Reset() + return + } + } +} + +// workerCreateFunc can be nil, but in that case, dead stack will not be replaced +func NewWorkerWatcher(allocator worker.Allocator, numWorkers int64, events events.Handler) worker.Watcher { + ww := &workerWatcher{ + stack: NewWorkersStack(), + allocator: allocator, + initialNumWorkers: numWorkers, + actualNumWorkers: numWorkers, + events: events, + } + + return ww +} + +type workerWatcher struct { + mutex sync.RWMutex + stack *Stack + allocator worker.Allocator + initialNumWorkers int64 + actualNumWorkers int64 + events events.Handler +} + +func (ww *workerWatcher) AddToWatch(workers []worker.BaseProcess) error { + for i := 0; i < len(workers); i++ { + ww.stack.Push(workers[i]) + + go func(swc worker.BaseProcess) { + ww.wait(swc) + }(workers[i]) + } + return nil +} + +func (ww *workerWatcher) GetFreeWorker(ctx context.Context) (worker.BaseProcess, error) { + const op = errors.Op("GetFreeWorker") + // thread safe operation + w, stop := ww.stack.Pop() + if stop { + return nil, errors.E(op, errors.ErrWatcherStopped) + } + + // handle worker remove state + // in this state worker is destroyed by supervisor + if w != nil && w.State().Value() == internal.StateRemove { + err := ww.RemoveWorker(w) + if err != nil { + return nil, err + } + // try to get next + return ww.GetFreeWorker(ctx) + } + // no free stack + if w == nil { + for { + select { + default: + w, stop = ww.stack.Pop() + if stop { + return nil, errors.E(op, errors.ErrWatcherStopped) + } + if w == nil { + continue + } + return w, nil + case <-ctx.Done(): + return nil, errors.E(op, errors.NoFreeWorkers, errors.Str("no free workers in the stack, timeout exceed")) + } + } + } + + return w, nil +} + +func (ww *workerWatcher) AllocateNew() error { + ww.stack.mutex.Lock() + const op = errors.Op("allocate new worker") + sw, err := ww.allocator() + if err != nil { + return errors.E(op, errors.WorkerAllocate, err) + } + + ww.addToWatch(sw) + ww.stack.mutex.Unlock() + ww.PushWorker(sw) + + return nil +} + +func (ww *workerWatcher) RemoveWorker(wb worker.BaseProcess) error { + ww.mutex.Lock() + defer ww.mutex.Unlock() + + const op = errors.Op("remove worker") + pid := wb.Pid() + + if ww.stack.FindAndRemoveByPid(pid) { + wb.State().Set(internal.StateInvalid) + err := wb.Kill() + if err != nil { + return errors.E(op, err) + } + return nil + } + + wb.State().Set(internal.StateRemove) + return nil +} + +// O(1) operation +func (ww *workerWatcher) PushWorker(w worker.BaseProcess) { + ww.mutex.Lock() + defer ww.mutex.Unlock() + ww.stack.Push(w) +} + +// Destroy all underlying stack (but let them to complete the task) +func (ww *workerWatcher) Destroy(ctx context.Context) { + // destroy stack, we don't use ww mutex here, since we should be able to push worker + ww.stack.Destroy(ctx) +} + +// Warning, this is O(n) operation, and it will return copy of the actual workers +func (ww *workerWatcher) WorkersList() []worker.BaseProcess { + return ww.stack.Workers() +} + +func (ww *workerWatcher) wait(w worker.BaseProcess) { + const op = errors.Op("process wait") + err := w.Wait() + if err != nil { + ww.events.Push(events.WorkerEvent{ + Event: events.EventWorkerError, + Worker: w, + Payload: errors.E(op, err), + }) + } + + if w.State().Value() == internal.StateDestroyed { + // worker was manually destroyed, no need to replace + ww.events.Push(events.PoolEvent{Event: events.EventWorkerDestruct, Payload: w}) + return + } + + _ = ww.stack.FindAndRemoveByPid(w.Pid()) + err = ww.AllocateNew() + if err != nil { + ww.events.Push(events.PoolEvent{ + Event: events.EventPoolError, + Payload: errors.E(op, err), + }) + } +} + +func (ww *workerWatcher) addToWatch(wb worker.BaseProcess) { + ww.mutex.Lock() + defer ww.mutex.Unlock() + go func() { + ww.wait(wb) + }() +} diff --git a/pool.go b/pool.go deleted file mode 100644 index d863e96f..00000000 --- a/pool.go +++ /dev/null @@ -1,39 +0,0 @@ -package roadrunner - -const ( - // EventWorkerConstruct thrown when new worker is spawned. - EventWorkerConstruct = iota + 100 - - // EventWorkerDestruct thrown after worker destruction. - EventWorkerDestruct - - // EventWorkerKill thrown after worker is being forcefully killed. - EventWorkerKill - - // EventWorkerError thrown any worker related even happen (passed with WorkerError) - EventWorkerError - - // EventWorkerDead thrown when worker stops worker for any reason. - EventWorkerDead - - // EventPoolError caused on pool wide errors - EventPoolError -) - -// Pool managed set of inner worker processes. -type Pool interface { - // Listen all caused events to attached controller. - Listen(l func(event int, ctx interface{})) - - // Exec one task with given payload and context, returns result or error. - Exec(rqs *Payload) (rsp *Payload, err error) - - // Workers returns worker list associated with the pool. - Workers() (workers []*Worker) - - // Remove forces pool to remove specific worker. Return true is this is first remove request on given worker. - Remove(w *Worker, err error) bool - - // Destroy all underlying workers (but let them to complete the task). - Destroy() -} diff --git a/protocol.go b/protocol.go deleted file mode 100644 index b00eb2a4..00000000 --- a/protocol.go +++ /dev/null @@ -1,51 +0,0 @@ -package roadrunner - -import ( - "fmt" - json "github.com/json-iterator/go" - "github.com/spiral/goridge/v2" - "os" -) - -type stopCommand struct { - Stop bool `json:"stop"` -} - -type pidCommand struct { - Pid int `json:"pid"` -} - -func sendControl(rl goridge.Relay, v interface{}) error { - if data, ok := v.([]byte); ok { - return rl.Send(data, goridge.PayloadControl|goridge.PayloadRaw) - } - - j := json.ConfigCompatibleWithStandardLibrary - data, err := j.Marshal(v) - if err != nil { - return fmt.Errorf("invalid payload: %s", err) - } - - return rl.Send(data, goridge.PayloadControl) -} - -func fetchPID(rl goridge.Relay) (pid int, err error) { - if err := sendControl(rl, pidCommand{Pid: os.Getpid()}); err != nil { - return 0, err - } - - body, p, err := rl.Receive() - if err != nil { - return 0, err - } - if !p.HasFlag(goridge.PayloadControl) { - return 0, fmt.Errorf("unexpected response, header is missing") - } - - link := &pidCommand{} - if err := json.Unmarshal(body, link); err != nil { - return 0, err - } - - return link.Pid, nil -} diff --git a/protocol_test.go b/protocol_test.go deleted file mode 100644 index 55c603a5..00000000 --- a/protocol_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package roadrunner - -import ( - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" - "github.com/stretchr/testify/assert" - "testing" -) - -type relayMock struct { - error bool - payload string -} - -func (r *relayMock) Send(data []byte, flags byte) (err error) { - if r.error { - return errors.New("send error") - } - - return nil -} - -func (r *relayMock) Receive() (data []byte, p goridge.Prefix, err error) { - return []byte(r.payload), goridge.NewPrefix().WithFlag(goridge.PayloadControl), nil -} - -func (r *relayMock) Close() error { - return nil -} - -func Test_Protocol_Errors(t *testing.T) { - err := sendControl(&relayMock{}, make(chan int)) - assert.Error(t, err) -} - -func Test_Protocol_FetchPID(t *testing.T) { - pid, err := fetchPID(&relayMock{error: false, payload: "{\"pid\":100}"}) - assert.NoError(t, err) - assert.Equal(t, 100, pid) - - _, err = fetchPID(&relayMock{error: true, payload: "{\"pid\":100}"}) - assert.Error(t, err) - - _, err = fetchPID(&relayMock{error: false, payload: "{\"pid:100"}) - assert.Error(t, err) -} diff --git a/server.go b/server.go deleted file mode 100644 index 406bc0a0..00000000 --- a/server.go +++ /dev/null @@ -1,255 +0,0 @@ -package roadrunner - -import ( - "fmt" - "github.com/pkg/errors" - "sync" -) - -const ( - // EventServerStart triggered when server creates new pool. - EventServerStart = iota + 200 - - // EventServerStop triggered when server creates new pool. - EventServerStop - - // EventServerFailure triggered when server is unable to replace dead pool. - EventServerFailure - - // EventPoolConstruct triggered when server creates new pool. - EventPoolConstruct - - // EventPoolDestruct triggered when server destroys existed pool. - EventPoolDestruct -) - -// Controllable defines the ability to attach rr controller. -type Controllable interface { - // Server represents RR server - Server() *Server -} - -// Server manages pool creation and swapping. -type Server struct { - // configures server, pool, cmd creation and factory. - cfg *ServerConfig - - // protects pool while the re-configuration - mu sync.Mutex - - // indicates that server was started - started bool - - // creates and connects to workers - factory Factory - - // associated pool controller - controller Controller - - // currently active pool instance - mup sync.Mutex - pool Pool - pController Controller - - // observes pool events (can be attached to multiple pools at the same time) - mul sync.Mutex - lsn func(event int, ctx interface{}) -} - -// NewServer creates new router. Make sure to call configure before the usage. -func NewServer(cfg *ServerConfig) *Server { - return &Server{cfg: cfg} -} - -// Listen attaches server event controller. -func (s *Server) Listen(l func(event int, ctx interface{})) { - s.mul.Lock() - defer s.mul.Unlock() - - s.lsn = l -} - -// Attach attaches worker controller. -func (s *Server) Attach(c Controller) { - s.mu.Lock() - defer s.mu.Unlock() - - s.controller = c - - s.mul.Lock() - if s.pController != nil && s.pool != nil { - s.pController.Detach() - s.pController = s.controller.Attach(s.pool) - } - s.mul.Unlock() -} - -// Start underlying worker pool, configure factory and command provider. -func (s *Server) Start() (err error) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.factory, err = s.cfg.makeFactory(); err != nil { - return err - } - - if s.pool, err = NewPool(s.cfg.makeCommand(), s.factory, *s.cfg.Pool); err != nil { - return err - } - - if s.controller != nil { - s.pController = s.controller.Attach(s.pool) - } - - s.pool.Listen(s.poolListener) - s.started = true - s.throw(EventServerStart, s) - - return nil -} - -// Stop underlying worker pool and close the factory. -func (s *Server) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.started { - return - } - - s.throw(EventPoolDestruct, s.pool) - - if s.pController != nil { - s.pController.Detach() - s.pController = nil - } - - s.pool.Destroy() - s.factory.Close() - - s.factory = nil - s.pool = nil - s.started = false - s.throw(EventServerStop, s) -} - -// Exec one task with given payload and context, returns result or error. -func (s *Server) Exec(rqs *Payload) (rsp *Payload, err error) { - pool := s.Pool() - if pool == nil { - return nil, fmt.Errorf("no associared pool") - } - - return pool.Exec(rqs) -} - -// Reconfigure re-configures underlying pool and destroys it's previous version if any. Reconfigure will ignore factory -// and relay settings. -func (s *Server) Reconfigure(cfg *ServerConfig) error { - s.mup.Lock() - defer s.mup.Unlock() - - s.mu.Lock() - if !s.started { - s.cfg = cfg - s.mu.Unlock() - return nil - } - s.mu.Unlock() - - if s.cfg.Differs(cfg) { - return errors.New("unable to reconfigure server (cmd and pool changes are allowed)") - } - - s.mu.Lock() - previous := s.pool - pWatcher := s.pController - s.mu.Unlock() - - pool, err := NewPool(cfg.makeCommand(), s.factory, *cfg.Pool) - if err != nil { - return err - } - - pool.Listen(s.poolListener) - - s.mu.Lock() - s.cfg.Pool, s.pool = cfg.Pool, pool - - if s.controller != nil { - s.pController = s.controller.Attach(pool) - } - - s.mu.Unlock() - - s.throw(EventPoolConstruct, pool) - - if previous != nil { - go func(previous Pool, pWatcher Controller) { - s.throw(EventPoolDestruct, previous) - if pWatcher != nil { - pWatcher.Detach() - } - - previous.Destroy() - }(previous, pWatcher) - } - - return nil -} - -// Reset resets the state of underlying pool and rebuilds all of it's workers. -func (s *Server) Reset() error { - s.mu.Lock() - cfg := s.cfg - s.mu.Unlock() - - return s.Reconfigure(cfg) -} - -// Workers returns worker list associated with the server pool. -func (s *Server) Workers() (workers []*Worker) { - p := s.Pool() - if p == nil { - return nil - } - - return p.Workers() -} - -// Pool returns active pool or error. -func (s *Server) Pool() Pool { - s.mu.Lock() - defer s.mu.Unlock() - - return s.pool -} - -// Listen pool events. -func (s *Server) poolListener(event int, ctx interface{}) { - if event == EventPoolError { - // pool failure, rebuilding - if err := s.Reset(); err != nil { - s.mu.Lock() - s.started = false - s.pool = nil - s.factory = nil - s.mu.Unlock() - - // everything is dead, this is recoverable but heavy state - s.throw(EventServerFailure, err) - } - } - - // bypassing to user specified lsn - s.throw(event, ctx) -} - -// throw invokes event handler if any. -func (s *Server) throw(event int, ctx interface{}) { - s.mul.Lock() - if s.lsn != nil { - s.lsn(event, ctx) - } - s.mul.Unlock() -} diff --git a/server_config.go b/server_config.go deleted file mode 100644 index 32ff0ebc..00000000 --- a/server_config.go +++ /dev/null @@ -1,168 +0,0 @@ -package roadrunner - -import ( - "errors" - "fmt" - "github.com/spiral/roadrunner/osutil" - "net" - "os" - "os/exec" - "strings" - "sync" - "syscall" - "time" -) - -// CommandProducer can produce commands. -type CommandProducer func(cfg *ServerConfig) func() *exec.Cmd - -// ServerConfig config combines factory, pool and cmd configurations. -type ServerConfig struct { - // Command includes command strings with all the parameters, example: "php worker.php pipes". - Command string - - // User under which process will be started - User string - - // CommandProducer overwrites - CommandProducer CommandProducer - - // Relay defines connection method and factory to be used to connect to workers: - // "pipes", "tcp://:6001", "unix://rr.sock" - // This config section must not change on re-configuration. - Relay string - - // RelayTimeout defines for how long socket factory will be waiting for worker connection. This config section - // must not change on re-configuration. - RelayTimeout time.Duration - - // Pool defines worker pool configuration, number of workers, timeouts and etc. This config section might change - // while server is running. - Pool *Config - - // values defines set of values to be passed to the command context. - mu sync.Mutex - env map[string]string -} - -// InitDefaults sets missing values to their default values. -func (cfg *ServerConfig) InitDefaults() error { - cfg.Relay = "pipes" - cfg.RelayTimeout = time.Minute - - if cfg.Pool == nil { - cfg.Pool = &Config{} - } - - return cfg.Pool.InitDefaults() -} - -// UpscaleDurations converts duration values from nanoseconds to seconds. -func (cfg *ServerConfig) UpscaleDurations() { - if cfg.RelayTimeout < time.Microsecond { - cfg.RelayTimeout = time.Second * time.Duration(cfg.RelayTimeout.Nanoseconds()) - } - - if cfg.Pool.AllocateTimeout < time.Microsecond { - cfg.Pool.AllocateTimeout = time.Second * time.Duration(cfg.Pool.AllocateTimeout.Nanoseconds()) - } - - if cfg.Pool.DestroyTimeout < time.Microsecond { - cfg.Pool.DestroyTimeout = time.Second * time.Duration(cfg.Pool.DestroyTimeout.Nanoseconds()) - } -} - -// Differs returns true if configuration has changed but ignores pool or cmd changes. -func (cfg *ServerConfig) Differs(new *ServerConfig) bool { - return cfg.Relay != new.Relay || cfg.RelayTimeout != new.RelayTimeout -} - -// SetEnv sets new environment variable. Value is automatically uppercase-d. -func (cfg *ServerConfig) SetEnv(k, v string) { - cfg.mu.Lock() - defer cfg.mu.Unlock() - - if cfg.env == nil { - cfg.env = make(map[string]string) - } - - cfg.env[k] = v -} - -// GetEnv must return list of env variables. -func (cfg *ServerConfig) GetEnv() (env []string) { - env = append(os.Environ(), fmt.Sprintf("RR_RELAY=%s", cfg.Relay)) - for k, v := range cfg.env { - env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(k), v)) - } - - return -} - -//=================================== PRIVATE METHODS ====================================================== - -func (cfg *ServerConfig) makeCommand() func() *exec.Cmd { - cfg.mu.Lock() - defer cfg.mu.Unlock() - - if cfg.CommandProducer != nil { - return cfg.CommandProducer(cfg) - } - - var cmdArgs []string - cmdArgs = append(cmdArgs, strings.Split(cfg.Command, " ")...) - - return func() *exec.Cmd { - cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) - osutil.IsolateProcess(cmd) - - // if user is not empty, and OS is linux or macos - // execute php worker from that particular user - if cfg.User != "" { - err := osutil.ExecuteFromUser(cmd, cfg.User) - if err != nil { - return nil - } - } - - cmd.Env = cfg.GetEnv() - - return cmd - } -} - -// makeFactory creates and connects new factory instance based on given parameters. -func (cfg *ServerConfig) makeFactory() (Factory, error) { - if cfg.Relay == "pipes" || cfg.Relay == "pipe" { - return NewPipeFactory(), nil - } - - dsn := strings.Split(cfg.Relay, "://") - if len(dsn) != 2 { - return nil, errors.New("invalid relay DSN (pipes, tcp://:6001, unix://rr.sock)") - } - - if dsn[0] == "unix" && fileExists(dsn[1]) { - err := syscall.Unlink(dsn[1]) - if err != nil { - return nil, err - } - } - - ln, err := net.Listen(dsn[0], dsn[1]) - if err != nil { - return nil, err - } - - return NewSocketFactory(ln, cfg.RelayTimeout), nil -} - -// fileExists checks if a file exists and is not a directory before we -// try using it to prevent further errors. -func fileExists(filename string) bool { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} diff --git a/server_config_test.go b/server_config_test.go deleted file mode 100644 index c88f9082..00000000 --- a/server_config_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func Test_ServerConfig_PipeFactory(t *testing.T) { - cfg := &ServerConfig{Relay: "pipes"} - f, err := cfg.makeFactory() - - assert.NoError(t, err) - assert.IsType(t, &PipeFactory{}, f) - - cfg = &ServerConfig{Relay: "pipe"} - f, err = cfg.makeFactory() - assert.NoError(t, err) - assert.NotNil(t, f) - defer func() { - err := f.Close() - if err != nil { - t.Errorf("error closing factory or underlying connections: error %v", err) - } - }() - - assert.NoError(t, err) - assert.IsType(t, &PipeFactory{}, f) -} - -func Test_ServerConfig_SocketFactory(t *testing.T) { - cfg := &ServerConfig{Relay: "tcp://:9111"} - f1, err := cfg.makeFactory() - assert.NoError(t, err) - assert.NotNil(t, f1) - defer func() { - err := f1.Close() - - if err != nil { - t.Errorf("error closing factory or underlying connections: error %v", err) - } - }() - - assert.NoError(t, err) - assert.IsType(t, &SocketFactory{}, f1) - assert.Equal(t, "tcp", f1.(*SocketFactory).ls.Addr().Network()) - assert.Equal(t, "[::]:9111", f1.(*SocketFactory).ls.Addr().String()) - - cfg = &ServerConfig{Relay: "tcp://localhost:9112"} - f, err := cfg.makeFactory() - assert.NoError(t, err) - assert.NotNil(t, f) - defer func() { - err := f.Close() - if err != nil { - t.Errorf("error closing factory or underlying connections: error %v", err) - } - }() - - assert.NoError(t, err) - assert.IsType(t, &SocketFactory{}, f) - assert.Equal(t, "tcp", f.(*SocketFactory).ls.Addr().Network()) - assert.Equal(t, "127.0.0.1:9112", f.(*SocketFactory).ls.Addr().String()) -} - -func Test_ServerConfig_UnixSocketFactory(t *testing.T) { - cfg := &ServerConfig{Relay: "unix://unix.sock"} - f, err := cfg.makeFactory() - if err != nil { - t.Error(err) - } - - defer func() { - err := f.Close() - if err != nil { - t.Errorf("error closing factory or underlying connections: error %v", err) - } - }() - - assert.NoError(t, err) - assert.IsType(t, &SocketFactory{}, f) - assert.Equal(t, "unix", f.(*SocketFactory).ls.Addr().Network()) - assert.Equal(t, "unix.sock", f.(*SocketFactory).ls.Addr().String()) -} - -func Test_ServerConfig_ErrorFactory(t *testing.T) { - cfg := &ServerConfig{Relay: "uni:unix.sock"} - f, err := cfg.makeFactory() - assert.Nil(t, f) - assert.Error(t, err) - assert.Equal(t, "invalid relay DSN (pipes, tcp://:6001, unix://rr.sock)", err.Error()) -} - -func Test_ServerConfig_ErrorMethod(t *testing.T) { - cfg := &ServerConfig{Relay: "xinu://unix.sock"} - - f, err := cfg.makeFactory() - assert.Nil(t, f) - assert.Error(t, err) -} - -func Test_ServerConfig_Cmd(t *testing.T) { - cfg := &ServerConfig{ - Command: "php tests/client.php pipes", - } - - cmd := cfg.makeCommand() - assert.NotNil(t, cmd) -} - -func Test_ServerConfig_SetEnv(t *testing.T) { - cfg := &ServerConfig{ - Command: "php tests/client.php pipes", - Relay: "pipes", - } - - cfg.SetEnv("key", "value") - - cmd := cfg.makeCommand() - assert.NotNil(t, cmd) - - c := cmd() - - assert.Contains(t, c.Env, "KEY=value") - assert.Contains(t, c.Env, "RR_RELAY=pipes") -} - -func Test_ServerConfig_SetEnv_Relay(t *testing.T) { - cfg := &ServerConfig{ - Command: "php tests/client.php pipes", - Relay: "unix://rr.sock", - } - - cfg.SetEnv("key", "value") - - cmd := cfg.makeCommand() - assert.NotNil(t, cmd) - - c := cmd() - - assert.Contains(t, c.Env, "KEY=value") - assert.Contains(t, c.Env, "RR_RELAY=unix://rr.sock") -} - -func Test_ServerConfigDefaults(t *testing.T) { - cfg := &ServerConfig{ - Command: "php tests/client.php pipes", - } - - err := cfg.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - - assert.Equal(t, "pipes", cfg.Relay) - assert.Equal(t, time.Minute, cfg.Pool.AllocateTimeout) - assert.Equal(t, time.Minute, cfg.Pool.DestroyTimeout) -} - -func Test_Config_Upscale(t *testing.T) { - cfg := &ServerConfig{ - Command: "php tests/client.php pipes", - RelayTimeout: 1, - Pool: &Config{ - AllocateTimeout: 1, - DestroyTimeout: 1, - }, - } - - cfg.UpscaleDurations() - assert.Equal(t, time.Second, cfg.RelayTimeout) - assert.Equal(t, time.Second, cfg.Pool.AllocateTimeout) - assert.Equal(t, time.Second, cfg.Pool.DestroyTimeout) -} diff --git a/server_test.go b/server_test.go deleted file mode 100644 index 9ab480b1..00000000 --- a/server_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "os/exec" - "runtime" - "testing" - "time" -) - -func TestServer_PipesEcho(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func TestServer_NoPool(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) -} - -func TestServer_SocketEcho(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo tcp", - Relay: "tcp://:9007", - RelayTimeout: 10 * time.Second, - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func TestServer_Configure_BeforeStart(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - err := rr.Reconfigure(&ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 2, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - assert.NoError(t, err) - - assert.NoError(t, rr.Start()) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) - assert.Len(t, rr.Workers(), 2) -} - -func TestServer_Stop_NotStarted(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - - rr.Stop() - assert.Nil(t, rr.Workers()) -} - -func TestServer_Reconfigure(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - assert.Len(t, rr.Workers(), 1) - - err := rr.Reconfigure(&ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 2, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - assert.NoError(t, err) - - assert.Len(t, rr.Workers(), 2) -} - -func TestServer_Reset(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - assert.Len(t, rr.Workers(), 1) - - pid := *rr.Workers()[0].Pid - assert.NoError(t, rr.Reset()) - assert.Len(t, rr.Workers(), 1) - assert.NotEqual(t, pid, rr.Workers()[0].Pid) -} - -func TestServer_ReplacePool(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - - constructed := make(chan interface{}) - rr.Listen(func(e int, ctx interface{}) { - if e == EventPoolConstruct { - close(constructed) - } - }) - - err := rr.Reset() - if err != nil { - t.Errorf("error resetting the pool: error %v", err) - } - <-constructed - - for _, w := range rr.Workers() { - assert.Equal(t, StateReady, w.state.Value()) - } -} - -func TestServer_ServerFailure(t *testing.T) { - rr := NewServer(&ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - - failure := make(chan interface{}) - rr.Listen(func(e int, ctx interface{}) { - if e == EventServerFailure { - failure <- nil - } - }) - - // emulating potential server failure - rr.cfg.Command = "php tests/client.php echo broken-connection" - rr.pool.(*StaticPool).cmd = func() *exec.Cmd { - return exec.Command("php", "tests/client.php", "echo", "broken-connection") - } - // killing random worker and expecting pool to replace it - err := rr.Workers()[0].cmd.Process.Kill() - if err != nil { - t.Errorf("error killing the process: error %v", err) - } - - <-failure - assert.True(t, true) -} diff --git a/service/container.go b/service/container.go deleted file mode 100644 index 77a6dfc0..00000000 --- a/service/container.go +++ /dev/null @@ -1,371 +0,0 @@ -package service - -import ( - "fmt" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "reflect" - "sync" -) - -var errNoConfig = fmt.Errorf("no config has been provided") -var errTempFix223 = fmt.Errorf("temporary error for fix #223") // meant no error here, just shutdown the server - -// InitMethod contains name of the method to be automatically invoked while service initialization. Must return -// (bool, error). Container can be requested as well. Config can be requested in a form -// of service.Config or pointer to service specific config struct (automatically unmarshalled), config argument must -// implement service.HydrateConfig. -const InitMethod = "Init" - -// Service can serve. Services can provide Init method which must return (bool, error) signature and might accept -// other services and/or configs as dependency. -type Service interface { - // Serve serves. - Serve() error - - // Detach stops the service. - Stop() -} - -// Container controls all internal RR services and provides plugin based system. -type Container interface { - // Register add new service to the container under given name. - Register(name string, service interface{}) - - // Reconfigure configures all underlying services with given configuration. - Init(cfg Config) error - - // Check if svc has been registered. - Has(service string) bool - - // get returns svc instance by it's name or nil if svc not found. Method returns current service status - // as second value. - Get(service string) (svc interface{}, status int) - - // Serve all configured services. Non blocking. - Serve() error - - // Close all active services. - Stop() - - // List service names. - List() []string -} - -// Config provides ability to slice configuration sections and unmarshal configuration data into -// given structure. -type Config interface { - // get nested config section (sub-map), returns nil if section not found. - Get(service string) Config - - // Unmarshal unmarshal config data into given struct. - Unmarshal(out interface{}) error -} - -// HydrateConfig provides ability to automatically hydrate config with values using -// service.Config as the source. -type HydrateConfig interface { - // Hydrate must populate config values using given config source. - // Must return error if config is not valid. - Hydrate(cfg Config) error -} - -// DefaultsConfig declares ability to be initated without config data provided. -type DefaultsConfig interface { - // InitDefaults allows to init blank config with pre-defined set of default values. - InitDefaults() error -} - -type container struct { - log logrus.FieldLogger - mu sync.Mutex - services []*entry - errc chan struct { - name string - err error - } -} - -// NewContainer creates new service container. -func NewContainer(log logrus.FieldLogger) Container { - return &container{ - log: log, - services: make([]*entry, 0), - errc: make(chan struct { - name string - err error - }, 1), - } -} - -// Register add new service to the container under given name. -func (c *container) Register(name string, service interface{}) { - c.mu.Lock() - defer c.mu.Unlock() - - c.services = append(c.services, &entry{ - name: name, - svc: service, - status: StatusInactive, - }) -} - -// Check hasStatus svc has been registered. -func (c *container) Has(target string) bool { - c.mu.Lock() - defer c.mu.Unlock() - - for _, e := range c.services { - if e.name == target { - return true - } - } - - return false -} - -// get returns svc instance by it's name or nil if svc not found. -func (c *container) Get(target string) (svc interface{}, status int) { - c.mu.Lock() - defer c.mu.Unlock() - - for _, e := range c.services { - if e.name == target { - return e.svc, e.getStatus() - } - } - - return nil, StatusUndefined -} - -// Init configures all underlying services with given configuration. -func (c *container) Init(cfg Config) error { - for _, e := range c.services { - if e.getStatus() >= StatusOK { - return fmt.Errorf("service [%s] has already been configured", e.name) - } - - // inject service dependencies - if ok, err := c.initService(e.svc, cfg.Get(e.name)); err != nil { - // soft error (skipping) - if err == errNoConfig { - c.log.Debugf("[%s]: disabled", e.name) - continue - } - - return errors.Wrap(err, fmt.Sprintf("[%s]", e.name)) - } else if ok { - e.setStatus(StatusOK) - } else { - c.log.Debugf("[%s]: disabled", e.name) - } - } - - return nil -} - -// Serve all configured services. Non blocking. -func (c *container) Serve() error { - var running = 0 - for _, e := range c.services { - if e.hasStatus(StatusOK) && e.canServe() { - running++ - c.log.Debugf("[%s]: started", e.name) - go func(e *entry) { - e.setStatus(StatusServing) - defer e.setStatus(StatusStopped) - if err := e.svc.(Service).Serve(); err != nil { - c.errc <- struct { - name string - err error - }{name: e.name, err: errors.Wrap(err, fmt.Sprintf("[%s]", e.name))} - } else { - c.errc <- struct { - name string - err error - }{name: e.name, err: errTempFix223} - } - }(e) - } - } - - // simple handler to handle empty configs - if running == 0 { - return nil - } - - for fail := range c.errc { - if fail.err == errTempFix223 { - // if we call stop, then stop all plugins - break - } else { - c.log.Errorf("[%s]: %s", fail.name, fail.err) - c.Stop() - return fail.err - } - } - - return nil -} - -// Detach sends stop command to all running services. -func (c *container) Stop() { - for _, e := range c.services { - if e.hasStatus(StatusServing) { - e.setStatus(StatusStopping) - e.svc.(Service).Stop() - e.setStatus(StatusStopped) - - c.log.Debugf("[%s]: stopped", e.name) - } - } -} - -// List all service names. -func (c *container) List() []string { - names := make([]string, 0, len(c.services)) - for _, e := range c.services { - names = append(names, e.name) - } - - return names -} - -// calls Init method with automatically resolved arguments. -func (c *container) initService(s interface{}, segment Config) (bool, error) { - r := reflect.TypeOf(s) - - m, ok := r.MethodByName(InitMethod) - if !ok { - // no Init method is presented, assuming service does not need initialization. - return true, nil - } - - if err := c.verifySignature(m); err != nil { - return false, err - } - - // hydrating - values, err := c.resolveValues(s, m, segment) - if err != nil { - return false, err - } - - // initiating service - out := m.Func.Call(values) - - if out[1].IsNil() { - return out[0].Bool(), nil - } - - return out[0].Bool(), out[1].Interface().(error) -} - -// resolveValues returns slice of call arguments for service Init method. -func (c *container) resolveValues(s interface{}, m reflect.Method, cfg Config) (values []reflect.Value, err error) { - for i := 0; i < m.Type.NumIn(); i++ { - v := m.Type.In(i) - - switch { - case v.ConvertibleTo(reflect.ValueOf(s).Type()): // service itself - values = append(values, reflect.ValueOf(s)) - - case v.Implements(reflect.TypeOf((*Container)(nil)).Elem()): // container - values = append(values, reflect.ValueOf(c)) - - case v.Implements(reflect.TypeOf((*logrus.StdLogger)(nil)).Elem()), - v.Implements(reflect.TypeOf((*logrus.FieldLogger)(nil)).Elem()), - v.ConvertibleTo(reflect.ValueOf(c.log).Type()): // logger - values = append(values, reflect.ValueOf(c.log)) - - case v.Implements(reflect.TypeOf((*HydrateConfig)(nil)).Elem()): // injectable config - sc := reflect.New(v.Elem()) - - if dsc, ok := sc.Interface().(DefaultsConfig); ok { - err := dsc.InitDefaults() - if err != nil { - return nil, err - } - if cfg == nil { - values = append(values, sc) - continue - } - - } else if cfg == nil { - return nil, errNoConfig - } - - if err := sc.Interface().(HydrateConfig).Hydrate(cfg); err != nil { - return nil, err - } - - values = append(values, sc) - - case v.Implements(reflect.TypeOf((*Config)(nil)).Elem()): // generic config section - if cfg == nil { - return nil, errNoConfig - } - - values = append(values, reflect.ValueOf(cfg)) - - default: // dependency on other service (resolution to nil if service can't be found) - value, err := c.resolveValue(v) - if err != nil { - return nil, err - } - - values = append(values, value) - } - } - - return -} - -// verifySignature checks if Init method has valid signature -func (c *container) verifySignature(m reflect.Method) error { - if m.Type.NumOut() != 2 { - return fmt.Errorf("method Init must have exact 2 return values") - } - - if m.Type.Out(0).Kind() != reflect.Bool { - return fmt.Errorf("first return value of Init method must be bool type") - } - - if !m.Type.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { - return fmt.Errorf("second return value of Init method value must be error type") - } - - return nil -} - -func (c *container) resolveValue(v reflect.Type) (reflect.Value, error) { - value := reflect.Value{} - for _, e := range c.services { - if !e.hasStatus(StatusOK) { - continue - } - - if v.Kind() == reflect.Interface && reflect.TypeOf(e.svc).Implements(v) { - if value.IsValid() { - return value, fmt.Errorf("disambiguous dependency `%s`", v) - } - - value = reflect.ValueOf(e.svc) - } - - if v.ConvertibleTo(reflect.ValueOf(e.svc).Type()) { - if value.IsValid() { - return value, fmt.Errorf("disambiguous dependency `%s`", v) - } - - value = reflect.ValueOf(e.svc) - } - } - - if !value.IsValid() { - // placeholder (make sure to check inside the method) - value = reflect.New(v).Elem() - } - - return value, nil -} diff --git a/service/container_test.go b/service/container_test.go deleted file mode 100644 index b3ec7054..00000000 --- a/service/container_test.go +++ /dev/null @@ -1,533 +0,0 @@ -package service - -import ( - "errors" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/stretchr/testify/assert" - "sync" - "testing" - "time" -) - -type testService struct { - mu sync.Mutex - waitForServe chan interface{} - delay time.Duration - ok bool - cfg Config - c Container - cfgE, serveE error - done chan interface{} -} - -func (t *testService) Init(cfg Config, c Container) (enabled bool, err error) { - t.cfg = cfg - t.c = c - t.done = make(chan interface{}) - return t.ok, t.cfgE -} - -func (t *testService) Serve() error { - time.Sleep(t.delay) - - if t.serveE != nil { - return t.serveE - } - - if c := t.waitChan(); c != nil { - close(c) - t.setChan(nil) - } - - <-t.done - return nil -} - -func (t *testService) Stop() { - close(t.done) -} - -func (t *testService) waitChan() chan interface{} { - t.mu.Lock() - defer t.mu.Unlock() - - return t.waitForServe -} - -func (t *testService) setChan(c chan interface{}) { - t.mu.Lock() - defer t.mu.Unlock() - - t.waitForServe = c -} - -type testCfg struct{ cfg string } - -func (cfg *testCfg) Get(name string) Config { - vars := make(map[string]interface{}) - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.cfg), &vars) - if err != nil { - panic("error unmarshalling the cfg.cfg value") - } - - v, ok := vars[name] - if !ok { - return nil - } - - d, _ := j.Marshal(v) - return &testCfg{cfg: string(d)} -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -// Config defines RPC service config. -type dConfig struct { - // Indicates if RPC connection is enabled. - Value string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *dConfig) Hydrate(cfg Config) error { - return cfg.Unmarshal(c) -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *dConfig) InitDefaults() error { - c.Value = "default" - - return nil -} - -type dService struct { - Cfg *dConfig -} - -func (s *dService) Init(cfg *dConfig) (bool, error) { - s.Cfg = cfg - return true, nil -} - -func TestContainer_Register(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) -} - -func TestContainer_Has(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) - - assert.True(t, c.Has("test")) - assert.False(t, c.Has("another")) -} - -func TestContainer_List(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) - assert.Equal(t, 1, len(c.List())) - - assert.True(t, c.Has("test")) - assert.False(t, c.Has("another")) -} - -func TestContainer_Get(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - assert.Equal(t, 0, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) - - s, st = c.Get("another") - assert.Nil(t, s) - assert.Equal(t, StatusUndefined, st) -} - -func TestContainer_Stop_NotStarted(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - assert.Equal(t, 0, len(hook.Entries)) - - c.Stop() -} - -func TestContainer_Configure(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusOK, st) -} - -func TestContainer_Init_Default(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &dService{} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{}`})) - - s, st := c.Get("test") - assert.IsType(t, &dService{}, s) - assert.Equal(t, StatusOK, st) - - assert.Equal(t, "default", svc.Cfg.Value) -} - -func TestContainer_Init_Default_Overwrite(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &dService{} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":{"value": "something"}}`})) - - s, st := c.Get("test") - assert.IsType(t, &dService{}, s) - assert.Equal(t, StatusOK, st) - - assert.Equal(t, "something", svc.Cfg.Value) -} - -func TestContainer_ConfigureNull(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"another":"something"}`})) - assert.Equal(t, 1, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureDisabled(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: false} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - assert.Equal(t, 1, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureError(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: false, - cfgE: errors.New("configure error"), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - err := c.Init(&testCfg{`{"test":"something"}`}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "configure error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureTwice(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - assert.Error(t, c.Init(&testCfg{`{"test":"something"}`})) -} - -// bug #276 test -func TestContainer_ServeEmptyContainer(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - go assert.NoError(t, c.Serve()) - - time.Sleep(time.Millisecond * 500) - - c.Stop() -} - -func TestContainer_Serve(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - waitForServe: make(chan interface{}), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - go func() { - assert.NoError(t, c.Serve()) - }() - - <-svc.waitChan() - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusServing, st) - - c.Stop() - - s, st = c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -func TestContainer_ServeError(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - waitForServe: make(chan interface{}), - serveE: errors.New("serve error"), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - err := c.Serve() - assert.Error(t, err) - assert.Contains(t, err.Error(), "serve error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -func TestContainer_ServeErrorMultiple(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - delay: time.Millisecond * 10, - waitForServe: make(chan interface{}), - serveE: errors.New("serve error"), - } - - svc2 := &testService{ - ok: true, - waitForServe: make(chan interface{}), - } - - c := NewContainer(logger) - c.Register("test2", svc2) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) - - err := c.Serve() - assert.Error(t, err) - assert.Contains(t, err.Error(), "serve error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) - - s, st = c.Get("test2") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -type testInitA struct{} - -func (t *testInitA) Init() error { - return nil -} - -type testInitB struct{} - -func (t *testInitB) Init() (int, error) { - return 0, nil -} - -func TestContainer_InitErrorA(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitA{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -func TestContainer_InitErrorB(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitB{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -type testInitC struct{} - -func (r *testInitC) Test() bool { - return true -} - -func TestContainer_NoInit(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -type testInitD struct { - c *testInitC //nolint:golint,unused,structcheck -} - -type DCfg struct { - V string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *DCfg) Hydrate(cfg Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - if c.V == "fail" { - return errors.New("failed config") - } - - return nil -} - -func (t *testInitD) Init(r *testInitC, c Container, cfg *DCfg) (bool, error) { - if r == nil { - return false, errors.New("unable to find testInitC") - } - - if c == nil { - return false, errors.New("unable to find Container") - } - - if cfg.V != "ok" { - return false, errors.New("invalid config") - } - - return false, nil -} - -func TestContainer_InitDependency(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - c.Register("test2", &testInitD{}) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":{"v":"ok"}}`})) -} - -func TestContainer_InitDependencyFail(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - c.Register("test2", &testInitD{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":{"v":"fail"}}`})) -} - -func TestContainer_InitDependencyEmpty(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test2", &testInitD{}) - - assert.Contains(t, c.Init(&testCfg{`{"test2":{"v":"ok"}}`}).Error(), "testInitC") -} diff --git a/service/entry.go b/service/entry.go deleted file mode 100644 index 497742d1..00000000 --- a/service/entry.go +++ /dev/null @@ -1,59 +0,0 @@ -package service - -import ( - "sync" -) - -const ( - // StatusUndefined when service bus can not find the service. - StatusUndefined = iota - - // StatusInactive when service has been registered in container. - StatusInactive - - // StatusOK when service has been properly configured. - StatusOK - - // StatusServing when service is currently done. - StatusServing - - // StatusStopping when service is currently stopping. - StatusStopping - - // StatusStopped when service being stopped. - StatusStopped -) - -// entry creates association between service instance and given name. -type entry struct { - name string - svc interface{} - mu sync.Mutex - status int -} - -// status returns service status -func (e *entry) getStatus() int { - e.mu.Lock() - defer e.mu.Unlock() - - return e.status -} - -// setStarted indicates that service hasStatus status. -func (e *entry) setStatus(status int) { - e.mu.Lock() - defer e.mu.Unlock() - e.status = status -} - -// hasStatus checks if entry in specific status -func (e *entry) hasStatus(status int) bool { - return e.getStatus() == status -} - -// canServe returns true is service can serve. -func (e *entry) canServe() bool { - _, ok := e.svc.(Service) - return ok -} diff --git a/service/entry_test.go b/service/entry_test.go deleted file mode 100644 index b5c71a10..00000000 --- a/service/entry_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package service - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestEntry_CanServeFalse(t *testing.T) { - e := &entry{svc: nil} - assert.False(t, e.canServe()) -} - -func TestEntry_CanServeTrue(t *testing.T) { - e := &entry{svc: &testService{}} - assert.True(t, e.canServe()) -} diff --git a/service/env/config.go b/service/env/config.go deleted file mode 100644 index a7da695e..00000000 --- a/service/env/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package env - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config defines set of env values for RR workers. -type Config struct { - // values to set as worker _ENV. - Values map[string]string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(&c.Values) -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *Config) InitDefaults() error { - c.Values = make(map[string]string) - return nil -} diff --git a/service/env/config_test.go b/service/env/config_test.go deleted file mode 100644 index a526990d..00000000 --- a/service/env/config_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package env - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"key":"value"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 1) -} - -func Test_Config_Hydrate_Empty(t *testing.T) { - cfg := &mockCfg{`{}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 0) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("Test_Config_Defaults failed: error %v", err) - } - assert.Len(t, c.Values, 0) -} diff --git a/service/env/environment.go b/service/env/environment.go deleted file mode 100644 index ab8febf7..00000000 --- a/service/env/environment.go +++ /dev/null @@ -1,23 +0,0 @@ -package env - -// Environment aggregates list of environment variables. This interface can be used in custom implementation to drive -// values from external sources. -type Environment interface { - Setter - Getter - - // Copy all environment values. - Copy(setter Setter) error -} - -// Setter provides ability to set environment value. -type Setter interface { - // SetEnv sets or creates environment value. - SetEnv(key, value string) -} - -// Getter provides ability to set environment value. -type Getter interface { - // GetEnv must return list of env variables. - GetEnv() (map[string]string, error) -} diff --git a/service/env/service.go b/service/env/service.go deleted file mode 100644 index 83175b36..00000000 --- a/service/env/service.go +++ /dev/null @@ -1,55 +0,0 @@ -package env - -// ID contains default service name. -const ID = "env" - -// Service provides ability to map _ENV values from config file. -type Service struct { - // values is default set of values. - values map[string]string -} - -// NewService creates new env service instance for given rr version. -func NewService(defaults map[string]string) *Service { - s := &Service{values: defaults} - return s -} - -// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config) (bool, error) { - if s.values == nil { - s.values = make(map[string]string) - s.values["RR"] = "true" - } - - for k, v := range cfg.Values { - s.values[k] = v - } - - return true, nil -} - -// GetEnv must return list of env variables. -func (s *Service) GetEnv() (map[string]string, error) { - return s.values, nil -} - -// SetEnv sets or creates environment value. -func (s *Service) SetEnv(key, value string) { - s.values[key] = value -} - -// Copy all environment values. -func (s *Service) Copy(setter Setter) error { - values, err := s.GetEnv() - if err != nil { - return err - } - - for k, v := range values { - setter.SetEnv(k, v) - } - - return nil -} diff --git a/service/env/service_test.go b/service/env/service_test.go deleted file mode 100644 index 19cc03c7..00000000 --- a/service/env/service_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package env - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_NewService(t *testing.T) { - s := NewService(map[string]string{"version": "test"}) - assert.Len(t, s.values, 1) -} - -func Test_Init(t *testing.T) { - var err error - s := &Service{} - _, err = s.Init(&Config{}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 1) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Equal(t, "true", values["RR"]) -} - -func Test_Extend(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 2) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value", values["key"]) -} - -func Test_Set(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - s.SetEnv("key", "value-new") - s.SetEnv("other", "new") - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} - -func Test_Copy(t *testing.T) { - s1 := NewService(map[string]string{"RR": "version"}) - s2 := NewService(map[string]string{}) - - s1.SetEnv("key", "value-new") - s1.SetEnv("other", "new") - - assert.NoError(t, s1.Copy(s2)) - - values, err := s2.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} diff --git a/service/gzip/config.go b/service/gzip/config.go deleted file mode 100644 index 00ac559d..00000000 --- a/service/gzip/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package gzip - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config describes file location and controls access to them. -type Config struct { - Enable bool -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Enable = true - - return nil -} diff --git a/service/gzip/config_test.go b/service/gzip/config_test.go deleted file mode 100644 index c2168166..00000000 --- a/service/gzip/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"enable": true}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &mockCfg{`{"enable": "invalid"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"enable": 1}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - assert.Equal(t, true, c.Enable) -} diff --git a/service/gzip/service.go b/service/gzip/service.go deleted file mode 100644 index 231ba4d9..00000000 --- a/service/gzip/service.go +++ /dev/null @@ -1,36 +0,0 @@ -package gzip - -import ( - "errors" - "github.com/NYTimes/gziphandler" - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" -) - -// ID contains default service name. -const ID = "gzip" -var httpNotInitialized = errors.New("http service should be defined properly in config to use gzip") - -type Service struct { - cfg *Config -} - -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - s.cfg = cfg - if !s.cfg.Enable { - return false, nil - } - if r == nil { - return false, httpNotInitialized - } - - r.AddMiddleware(s.middleware) - - return true, nil -} - -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - gziphandler.GzipHandler(f).ServeHTTP(w, r) - } -} diff --git a/service/gzip/service_test.go b/service/gzip/service_test.go deleted file mode 100644 index 778bdacd..00000000 --- a/service/gzip/service_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "testing" -) - -type testCfg struct { - gzip string - httpCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.gzip} - } - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Disabled(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{cfg: &Config{Enable: true}}) - - assert.NoError(t, c.Init(&testCfg{ - httpCfg: `{ - "address": ":6029", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - } - }`, - gzip: `{"enable":false}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -// TEST bug #275 -func Test_Bug275(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - httpCfg: "", - gzip: `{"enable":true}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} diff --git a/service/headers/config.go b/service/headers/config.go deleted file mode 100644 index f9af1df2..00000000 --- a/service/headers/config.go +++ /dev/null @@ -1,41 +0,0 @@ -package headers - -import "github.com/spiral/roadrunner/service" - -// Config declares headers service configuration. -type Config struct { - // CORS settings. - CORS *CORSConfig - - // Request headers to add to every payload send to PHP. - Request map[string]string - - // Response headers to add to every payload generated by PHP. - Response map[string]string -} - -// CORSConfig headers configuration. -type CORSConfig struct { - // AllowedOrigin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - AllowedOrigin string - - // AllowedHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers - AllowedHeaders string - - // AllowedMethods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods - AllowedMethods string - - // AllowCredentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials - AllowCredentials *bool - - // ExposeHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers - ExposedHeaders string - - // MaxAge of CORS headers in seconds/ - MaxAge int -} - -// Hydrate service config. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} diff --git a/service/headers/config_test.go b/service/headers/config_test.go deleted file mode 100644 index 6ea02f67..00000000 --- a/service/headers/config_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package headers - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"request": {"From": "Something"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} diff --git a/service/headers/service.go b/service/headers/service.go deleted file mode 100644 index 429219d7..00000000 --- a/service/headers/service.go +++ /dev/null @@ -1,113 +0,0 @@ -package headers - -import ( - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" - "strconv" -) - -// ID contains default service name. -const ID = "headers" - -// Service serves headers files. Potentially convert into middleware? -type Service struct { - // server configuration (location, forbidden files and etc) - cfg *Config -} - -// Init must return configure service and return true if service hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - if r == nil { - return false, nil - } - - s.cfg = cfg - r.AddMiddleware(s.middleware) - - return true, nil -} - -// middleware must return true if request/response pair is handled within the middleware. -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - // Define the http.HandlerFunc - return func(w http.ResponseWriter, r *http.Request) { - - if s.cfg.Request != nil { - for k, v := range s.cfg.Request { - r.Header.Add(k, v) - } - } - - if s.cfg.Response != nil { - for k, v := range s.cfg.Response { - w.Header().Set(k, v) - } - } - - if s.cfg.CORS != nil { - if r.Method == http.MethodOptions { - s.preflightRequest(w, r) - return - } - - s.corsHeaders(w, r) - } - - f(w, r) - } -} - -// configure OPTIONS response -func (s *Service) preflightRequest(w http.ResponseWriter, r *http.Request) { - headers := w.Header() - - headers.Add("Vary", "Origin") - headers.Add("Vary", "Access-Control-Request-Method") - headers.Add("Vary", "Access-Control-Request-Headers") - - if s.cfg.CORS.AllowedOrigin != "" { - headers.Set("Access-Control-Allow-Origin", s.cfg.CORS.AllowedOrigin) - } - - if s.cfg.CORS.AllowedHeaders != "" { - headers.Set("Access-Control-Allow-Headers", s.cfg.CORS.AllowedHeaders) - } - - if s.cfg.CORS.AllowedMethods != "" { - headers.Set("Access-Control-Allow-Methods", s.cfg.CORS.AllowedMethods) - } - - if s.cfg.CORS.AllowCredentials != nil { - headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.CORS.AllowCredentials)) - } - - if s.cfg.CORS.MaxAge > 0 { - headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.CORS.MaxAge)) - } - - w.WriteHeader(http.StatusOK) -} - -// configure CORS headers -func (s *Service) corsHeaders(w http.ResponseWriter, r *http.Request) { - headers := w.Header() - - headers.Add("Vary", "Origin") - - if s.cfg.CORS.AllowedOrigin != "" { - headers.Set("Access-Control-Allow-Origin", s.cfg.CORS.AllowedOrigin) - } - - if s.cfg.CORS.AllowedHeaders != "" { - headers.Set("Access-Control-Allow-Headers", s.cfg.CORS.AllowedHeaders) - } - - if s.cfg.CORS.ExposedHeaders != "" { - headers.Set("Access-Control-Expose-Headers", s.cfg.CORS.ExposedHeaders) - } - - if s.cfg.CORS.AllowCredentials != nil { - headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.CORS.AllowCredentials)) - } -} diff --git a/service/headers/service_test.go b/service/headers/service_test.go deleted file mode 100644 index a67def02..00000000 --- a/service/headers/service_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package headers - -import ( - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - headers string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.headers} - } - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - return json.Unmarshal([]byte(cfg.target), out) -} - -func Test_RequestHeaders(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{"request":{"input": "custom-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":6078", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php header pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6078?hello=value", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "CUSTOM-HEADER", string(b)) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_ResponseHeaders(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{"response":{"output": "output-header"},"request":{"input": "custom-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":6079", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php header pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6079?hello=value", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "output-header", r.Header.Get("output")) - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "CUSTOM-HEADER", string(b)) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func TestCORS_OPTIONS(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{ -"cors":{ - "allowedOrigin": "*", - "allowedHeaders": "*", - "allowedMethods": "GET,POST,PUT,DELETE", - "allowCredentials": true, - "exposedHeaders": "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", - "maxAge": 600 -} -}`, - httpCfg: `{ - "enable": true, - "address": ":16379", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php headers pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("OPTIONS", "http://localhost:16379", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) - assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) - assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age")) - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - - _, err = ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func TestCORS_Pass(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{ -"cors":{ - "allowedOrigin": "*", - "allowedHeaders": "*", - "allowedMethods": "GET,POST,PUT,DELETE", - "allowCredentials": true, - "exposedHeaders": "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", - "maxAge": 600 -} -}`, - httpCfg: `{ - "enable": true, - "address": ":6672", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php headers pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6672", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - - _, err = ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} diff --git a/service/health/config.go b/service/health/config.go deleted file mode 100644 index 60a52d6e..00000000 --- a/service/health/config.go +++ /dev/null @@ -1,32 +0,0 @@ -package health - -import ( - "errors" - "strings" - - "github.com/spiral/roadrunner/service" -) - -// Config configures the health service -type Config struct { - // Address to listen on - Address string -} - -// Hydrate the config -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - return c.Valid() -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - // Validate the address - if c.Address != "" && !strings.Contains(c.Address, ":") { - return errors.New("malformed http server address") - } - - return nil -} diff --git a/service/health/config_test.go b/service/health/config_test.go deleted file mode 100644 index ba7d7c12..00000000 --- a/service/health/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package health - -import ( - json "github.com/json-iterator/go" - "testing" - - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost:8080"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Equal(t, "localhost:8080", c.Address) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Valid1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Valid2(t *testing.T) { - cfg := &mockCfg{`{"address": ":1111"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} diff --git a/service/health/service.go b/service/health/service.go deleted file mode 100644 index ce127340..00000000 --- a/service/health/service.go +++ /dev/null @@ -1,116 +0,0 @@ -package health - -import ( - "context" - "fmt" - "github.com/sirupsen/logrus" - "net/http" - "sync" - "time" - - rrhttp "github.com/spiral/roadrunner/service/http" -) - -const ( - // ID declares public service name. - ID = "health" - // maxHeaderSize declares max header size for prometheus server - maxHeaderSize = 1024 * 1024 * 100 // 104MB -) - -// Service to serve an endpoint for checking the health of the worker pool -type Service struct { - cfg *Config - log *logrus.Logger - mu sync.Mutex - http *http.Server - httpService *rrhttp.Service -} - -// Init health service -func (s *Service) Init(cfg *Config, r *rrhttp.Service, log *logrus.Logger) (bool, error) { - // Ensure the httpService is set - if r == nil { - return false, nil - } - - s.cfg = cfg - s.log = log - s.httpService = r - return true, nil -} - -// Serve the health endpoint -func (s *Service) Serve() error { - // Configure and start the http server - s.mu.Lock() - s.http = &http.Server{ - Addr: s.cfg.Address, - Handler: s, - IdleTimeout: time.Hour * 24, - ReadTimeout: time.Minute * 60, - MaxHeaderBytes: maxHeaderSize, - ReadHeaderTimeout: time.Minute * 60, - WriteTimeout: time.Minute * 60, - } - s.mu.Unlock() - - err := s.http.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - return err - } - - return nil -} - -// Stop the health endpoint -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.http != nil { - // gracefully stop the server - go func() { - err := s.http.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the metrics server: error %v", err)) - } - }() - } -} - -// ServeHTTP returns the health of the pool of workers -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - status := http.StatusOK - if !s.isHealthy() { - status = http.StatusInternalServerError - } - w.WriteHeader(status) -} - -// isHealthy checks the server, pool and ensures at least one worker is active -func (s *Service) isHealthy() bool { - httpService := s.httpService - if httpService == nil { - return false - } - - server := httpService.Server() - if server == nil { - return false - } - - pool := server.Pool() - if pool == nil { - return false - } - - // Ensure at least one worker is active - for _, w := range pool.Workers() { - if w.State().IsActive() { - return true - } - } - - return false -} diff --git a/service/health/service_test.go b/service/health/service_test.go deleted file mode 100644 index fc743a62..00000000 --- a/service/health/service_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package health - -import ( - json "github.com/json-iterator/go" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" -) - -type testCfg struct { - healthCfg string - httpCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - return &testCfg{target: cfg.healthCfg} - } - - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - return err -} - -func TestService_Serve(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2116" - }`, - httpCfg: `{ - "address": "localhost:2115", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - _, res, err := get("http://localhost:2116/") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) -} - -func TestService_Serve_DeadWorker(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2117" - }`, - httpCfg: `{ - "address": "localhost:2118", - "workers":{ - "command": "php ../../tests/http/slow-client.php echo pipes 1000", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("server error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Kill the worker - httpSvc := hS.(*rrhttp.Service) - err := httpSvc.Server().Workers()[0].Kill() - if err != nil { - t.Errorf("error killing the worker: error %v", err) - } - - // Check health check - _, res, err := get("http://localhost:2117/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -func TestService_Serve_DeadWorkerStillHealthy(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2119" - }`, - httpCfg: `{ - "address": "localhost:2120", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 2} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Second * 1) - defer c.Stop() - - // Kill one of the workers - httpSvc := hS.(*rrhttp.Service) - err := httpSvc.Server().Workers()[0].Kill() - if err != nil { - t.Errorf("error killing the worker: error %v", err) - } - - // Check health check - _, res, err := get("http://localhost:2119/") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) -} - -func TestService_Serve_NoHTTPService(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2121" - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, status) -} - -func TestService_Serve_NoServer(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - healthSvc := &Service{} - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, healthSvc) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2122" - }`, - httpCfg: `{ - "address": "localhost:2123", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Set the httpService to nil - healthSvc.httpService = nil - - _, res, err := get("http://localhost:2122/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -func TestService_Serve_NoPool(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - httpSvc := &rrhttp.Service{} - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, httpSvc) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2124" - }`, - httpCfg: `{ - "address": "localhost:2125", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Stop the pool - httpSvc.Server().Stop() - - _, res, err := get("http://localhost:2124/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} diff --git a/service/http/attributes/attributes.go b/service/http/attributes/attributes.go deleted file mode 100644 index 77d6ea69..00000000 --- a/service/http/attributes/attributes.go +++ /dev/null @@ -1,76 +0,0 @@ -package attributes - -import ( - "context" - "errors" - "net/http" -) - -type attrKey int - -const contextKey attrKey = iota - -type attrs map[string]interface{} - -func (v attrs) get(key string) interface{} { - if v == nil { - return "" - } - - return v[key] -} - -func (v attrs) set(key string, value interface{}) { - v[key] = value -} - -func (v attrs) del(key string) { - delete(v, key) -} - -// Init returns request with new context and attribute bag. -func Init(r *http.Request) *http.Request { - return r.WithContext(context.WithValue(r.Context(), contextKey, attrs{})) -} - -// All returns all context attributes. -func All(r *http.Request) map[string]interface{} { - v := r.Context().Value(contextKey) - if v == nil { - return attrs{} - } - - return v.(attrs) -} - -// Get gets the value from request context. It replaces any existing -// values. -func Get(r *http.Request, key string) interface{} { - v := r.Context().Value(contextKey) - if v == nil { - return nil - } - - return v.(attrs).get(key) -} - -// Set sets the key to value. It replaces any existing -// values. Context specific. -func Set(r *http.Request, key string, value interface{}) error { - v := r.Context().Value(contextKey) - if v == nil { - return errors.New("unable to find `psr:attributes` context key") - } - - v.(attrs).set(key, value) - return nil -} - -// Delete deletes values associated with attribute key. -func (v attrs) Delete(key string) { - if v == nil { - return - } - - v.del(key) -} diff --git a/service/http/attributes/attributes_test.go b/service/http/attributes/attributes_test.go deleted file mode 100644 index 2360fd12..00000000 --- a/service/http/attributes/attributes_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package attributes - -import ( - "github.com/stretchr/testify/assert" - "net/http" - "testing" -) - -func TestAllAttributes(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - - assert.Equal(t, All(r), map[string]interface{}{ - "key": "value", - }) -} - -func TestAllAttributesNone(t *testing.T) { - r := &http.Request{} - r = Init(r) - - assert.Equal(t, All(r), map[string]interface{}{}) -} - -func TestAllAttributesNone2(t *testing.T) { - r := &http.Request{} - - assert.Equal(t, All(r), map[string]interface{}{}) -} - -func TestGetAttribute(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), "value") -} - -func TestGetAttributeNone(t *testing.T) { - r := &http.Request{} - r = Init(r) - - assert.Equal(t, Get(r, "key"), nil) -} - -func TestGetAttributeNone2(t *testing.T) { - r := &http.Request{} - - assert.Equal(t, Get(r, "key"), nil) -} - -func TestSetAttribute(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), "value") -} - -func TestSetAttributeNone(t *testing.T) { - r := &http.Request{} - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), nil) -} diff --git a/service/http/config.go b/service/http/config.go deleted file mode 100644 index 00f61652..00000000 --- a/service/http/config.go +++ /dev/null @@ -1,263 +0,0 @@ -package http - -import ( - "errors" - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "net" - "os" - "strings" -) - -// Config configures RoadRunner HTTP server. -type Config struct { - // Port and port to handle as http server. - Address string - - // SSL defines https server options. - SSL SSLConfig - - // FCGI configuration. You can use FastCGI without HTTP server. - FCGI *FCGIConfig - - // HTTP2 configuration - HTTP2 *HTTP2Config - - // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. - MaxRequestSize int64 - - // TrustedSubnets declare IP subnets which are allowed to set ip using X-Real-Ip and X-Forwarded-For - TrustedSubnets []string - cidrs []*net.IPNet - - // Uploads configures uploads configuration. - Uploads *UploadsConfig - - // Workers configures rr server and worker pool. - Workers *roadrunner.ServerConfig -} - -// FCGIConfig for FastCGI server. -type FCGIConfig struct { - // Address and port to handle as http server. - Address string -} - -// HTTP2Config HTTP/2 server customizations. -type HTTP2Config struct { - // Enable or disable HTTP/2 extension, default enable. - Enabled bool - - // H2C enables HTTP/2 over TCP - H2C bool - - // MaxConcurrentStreams defaults to 128. - MaxConcurrentStreams uint32 -} - -// InitDefaults sets default values for HTTP/2 configuration. -func (cfg *HTTP2Config) InitDefaults() error { - cfg.Enabled = true - cfg.MaxConcurrentStreams = 128 - - return nil -} - -// SSLConfig defines https server configuration. -type SSLConfig struct { - // Port to listen as HTTPS server, defaults to 443. - Port int - - // Redirect when enabled forces all http connections to switch to https. - Redirect bool - - // Key defined private server key. - Key string - - // Cert is https certificate. - Cert string - - // Root CA file - RootCA string -} - -// EnableHTTP is true when http server must run. -func (c *Config) EnableHTTP() bool { - return c.Address != "" -} - -// EnableTLS returns true if rr must listen TLS connections. -func (c *Config) EnableTLS() bool { - return c.SSL.Key != "" || c.SSL.Cert != "" || c.SSL.RootCA != "" -} - -// EnableHTTP2 when HTTP/2 extension must be enabled (only with TSL). -func (c *Config) EnableHTTP2() bool { - return c.HTTP2.Enabled -} - -// EnableH2C when HTTP/2 extension must be enabled on TCP. -func (c *Config) EnableH2C() bool { - return c.HTTP2.H2C -} - -// EnableFCGI is true when FastCGI server must be enabled. -func (c *Config) EnableFCGI() bool { - return c.FCGI.Address != "" -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if c.Workers == nil { - c.Workers = &roadrunner.ServerConfig{} - } - - if c.HTTP2 == nil { - c.HTTP2 = &HTTP2Config{} - } - - if c.FCGI == nil { - c.FCGI = &FCGIConfig{} - } - - if c.Uploads == nil { - c.Uploads = &UploadsConfig{} - } - - if c.SSL.Port == 0 { - c.SSL.Port = 443 - } - - err := c.HTTP2.InitDefaults() - if err != nil { - return err - } - err = c.Uploads.InitDefaults() - if err != nil { - return err - } - err = c.Workers.InitDefaults() - if err != nil { - return err - } - - if err := cfg.Unmarshal(c); err != nil { - return err - } - - c.Workers.UpscaleDurations() - - if c.TrustedSubnets == nil { - // @see https://en.wikipedia.org/wiki/Reserved_IP_addresses - c.TrustedSubnets = []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - } - } - - if err := c.parseCIDRs(); err != nil { - return err - } - - return c.Valid() -} - -func (c *Config) parseCIDRs() error { - for _, cidr := range c.TrustedSubnets { - _, cr, err := net.ParseCIDR(cidr) - if err != nil { - return err - } - - c.cidrs = append(c.cidrs, cr) - } - - return nil -} - -// IsTrusted if api can be trusted to use X-Real-Ip, X-Forwarded-For -func (c *Config) IsTrusted(ip string) bool { - if c.cidrs == nil { - return false - } - - i := net.ParseIP(ip) - if i == nil { - return false - } - - for _, cird := range c.cidrs { - if cird.Contains(i) { - return true - } - } - - return false -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - if c.Uploads == nil { - return errors.New("malformed uploads config") - } - - if c.HTTP2 == nil { - return errors.New("malformed http2 config") - } - - if c.Workers == nil { - return errors.New("malformed workers config") - } - - if c.Workers.Pool == nil { - return errors.New("malformed workers config (pool config is missing)") - } - - if err := c.Workers.Pool.Valid(); err != nil { - return err - } - - if !c.EnableHTTP() && !c.EnableTLS() && !c.EnableFCGI() { - return errors.New("unable to run http service, no method has been specified (http, https, http/2 or FastCGI)") - } - - if c.Address != "" && !strings.Contains(c.Address, ":") { - return errors.New("malformed http server address") - } - - if c.EnableTLS() { - if _, err := os.Stat(c.SSL.Key); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("key file '%s' does not exists", c.SSL.Key) - } - - return err - } - - if _, err := os.Stat(c.SSL.Cert); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("cert file '%s' does not exists", c.SSL.Cert) - } - - return err - } - - // RootCA is optional, but if provided - check it - if c.SSL.RootCA != "" { - if _, err := os.Stat(c.SSL.RootCA); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("root ca path provided, but path '%s' does not exists", c.SSL.RootCA) - } - return err - } - } - } - - return nil -} diff --git a/service/http/config_test.go b/service/http/config_test.go deleted file mode 100644 index d95e0995..00000000 --- a/service/http/config_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package http - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "os" - "testing" - "time" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost:8080"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Valid(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.NoError(t, cfg.Valid()) -} - -func Test_Trusted_Subnets(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - TrustedSubnets: []string{"200.1.0.0/16"}, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.NoError(t, cfg.parseCIDRs()) - - assert.True(t, cfg.IsTrusted("200.1.0.10")) - assert.False(t, cfg.IsTrusted("127.0.0.0.1")) -} - -func Test_Trusted_Subnets_Err(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - TrustedSubnets: []string{"200.1.0.0"}, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.parseCIDRs()) -} - -func Test_Config_Valid_SSL(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Cert: "fixtures/server.crt", - Key: "fixtures/server.key", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Hydrate(&testCfg{httpCfg: "{}"})) - - assert.NoError(t, cfg.Valid()) - assert.True(t, cfg.EnableTLS()) - assert.Equal(t, 443, cfg.SSL.Port) -} - -func Test_Config_SSL_No_key(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Cert: "fixtures/server.crt", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_SSL_No_Cert(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Key: "fixtures/server.key", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoUploads(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoHTTP2(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 0, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoWorkers(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoPool(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 0, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_DeadPool(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_InvalidAddress(t *testing.T) { - cfg := &Config{ - Address: "unexpected_address", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} diff --git a/service/http/constants.go b/service/http/constants.go deleted file mode 100644 index a25f52a4..00000000 --- a/service/http/constants.go +++ /dev/null @@ -1,6 +0,0 @@ -package http - -import "net/http" - -var http2pushHeaderKey = http.CanonicalHeaderKey("http2-push") -var trailerHeaderKey = http.CanonicalHeaderKey("trailer") diff --git a/service/http/errors.go b/service/http/errors.go deleted file mode 100644 index fb8762ef..00000000 --- a/service/http/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build !windows - -package http - -import ( - "errors" - "net" - "os" - "syscall" -) - -// Broken pipe -var errEPIPE = errors.New("EPIPE(32) -> connection reset by peer") - -// handleWriteError just check if error was caused by aborted connection on linux -func handleWriteError(err error) error { - if netErr, ok2 := err.(*net.OpError); ok2 { - if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { - if syscallErr.Err == syscall.EPIPE { - return errEPIPE - } - } - } - return err -} diff --git a/service/http/errors_windows.go b/service/http/errors_windows.go deleted file mode 100644 index 3d0ba04c..00000000 --- a/service/http/errors_windows.go +++ /dev/null @@ -1,27 +0,0 @@ -// +build windows - -package http - -import ( - "errors" - "net" - "os" - "syscall" -) - -//Software caused connection abort. -//An established connection was aborted by the software in your host computer, -//possibly due to a data transmission time-out or protocol error. -var errEPIPE = errors.New("WSAECONNABORTED (10053) -> an established connection was aborted by peer") - -// handleWriteError just check if error was caused by aborted connection on windows -func handleWriteError(err error) error { - if netErr, ok2 := err.(*net.OpError); ok2 { - if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { - if syscallErr.Err == syscall.WSAECONNABORTED { - return errEPIPE - } - } - } - return err -} diff --git a/service/http/fcgi_test.go b/service/http/fcgi_test.go deleted file mode 100644 index e68b2e7f..00000000 --- a/service/http/fcgi_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package http - -import ( - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "github.com/yookoala/gofast" - "io/ioutil" - "net/http/httptest" - "testing" - "time" -) - -func Test_FCGI_Service_Echo(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "fcgi": { - "address": "tcp://0.0.0.0:6082" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { assert.NoError(t, c.Serve()) }() - time.Sleep(time.Second * 1) - - fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6082") - - fcgiHandler := gofast.NewHandler( - gofast.BasicParamsMap(gofast.BasicSession), - gofast.SimpleClientFactory(fcgiConnFactory, 0), - ) - - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://site.local/?hello=world", nil) - fcgiHandler.ServeHTTP(w, req) - - body, err := ioutil.ReadAll(w.Result().Body) - - assert.NoError(t, err) - assert.Equal(t, 201, w.Result().StatusCode) - assert.Equal(t, "WORLD", string(body)) - c.Stop() -} - -func Test_FCGI_Service_Request_Uri(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "fcgi": { - "address": "tcp://0.0.0.0:6083" - }, - "workers":{ - "command": "php ../../tests/http/client.php request-uri pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { assert.NoError(t, c.Serve()) }() - time.Sleep(time.Second * 1) - - fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6083") - - fcgiHandler := gofast.NewHandler( - gofast.BasicParamsMap(gofast.BasicSession), - gofast.SimpleClientFactory(fcgiConnFactory, 0), - ) - - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://site.local/hello-world", nil) - fcgiHandler.ServeHTTP(w, req) - - body, err := ioutil.ReadAll(w.Result().Body) - - assert.NoError(t, err) - assert.Equal(t, 200, w.Result().StatusCode) - assert.Equal(t, "http://site.local/hello-world", string(body)) - c.Stop() -} diff --git a/service/http/fixtures/server.crt b/service/http/fixtures/server.crt deleted file mode 100644 index 24d67fd7..00000000 --- a/service/http/fixtures/server.crt +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICTTCCAdOgAwIBAgIJAOKyUd+llTRKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYT -AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv -MRMwEQYDVQQKDApSb2FkUnVubmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgw -OTMwMTMzNDUzWhcNMjgwOTI3MTMzNDUzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE -CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwK -Um9hZFJ1bm5lcjESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EE -ACIDYgAEVnbShsM+l5RR3wfWWmGhzuFGwNzKCk7i9xyobDIyBUxG/UUSfj7KKlUX -puDnDEtF5xXcepl744CyIAYFLOXHb5WqI4jCOzG0o9f/00QQ4bQudJOdbqV910QF -C2vb7Fxro1MwUTAdBgNVHQ4EFgQU9xUexnbB6ORKayA7Pfjzs33otsAwHwYDVR0j -BBgwFoAU9xUexnbB6ORKayA7Pfjzs33otsAwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAgNoADBlAjEAue3HhR/MUhxoa9tSDBtOJT3FYbDQswrsdqBTz97CGKst -e7XeZ3HMEvEXy0hGGEMhAjAqcD/4k9vViVppgWFtkk6+NFbm+Kw/QeeAiH5FgFSj -8xQcb+b7nPwNLp3JOkXkVd4= ------END CERTIFICATE----- diff --git a/service/http/fixtures/server.key b/service/http/fixtures/server.key deleted file mode 100644 index 7501dd46..00000000 --- a/service/http/fixtures/server.key +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN EC PARAMETERS----- -BgUrgQQAIg== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MIGkAgEBBDCQP8utxNbHR6xZOLAJgUhn88r6IrPqmN0MsgGJM/jePB+T9UhkmIU8 -PMm2HeScbcugBwYFK4EEACKhZANiAARWdtKGwz6XlFHfB9ZaYaHO4UbA3MoKTuL3 -HKhsMjIFTEb9RRJ+PsoqVRem4OcMS0XnFdx6mXvjgLIgBgUs5cdvlaojiMI7MbSj -1//TRBDhtC50k51upX3XRAULa9vsXGs= ------END EC PRIVATE KEY----- diff --git a/service/http/h2c_test.go b/service/http/h2c_test.go deleted file mode 100644 index f17538bc..00000000 --- a/service/http/h2c_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package http - -import ( - "net/http" - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" -) - -func Test_Service_H2C(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "address": ":6029", - "http2": {"h2c":true}, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error serving: %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("PRI", "http://localhost:6029?hello=world", nil) - if err != nil { - return err - } - - req.Header.Add("Upgrade", "h2c") - req.Header.Add("Connection", "HTTP2-Settings") - req.Header.Add("HTTP2-Settings", "") - - r, err2 := http.DefaultClient.Do(req) - if err2 != nil { - return err2 - } - - assert.Equal(t, "101 Switching Protocols", r.Status) - - err3 := r.Body.Close() - if err3 != nil { - return err3 - } - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} diff --git a/service/http/handler.go b/service/http/handler.go deleted file mode 100644 index eca05483..00000000 --- a/service/http/handler.go +++ /dev/null @@ -1,208 +0,0 @@ -package http - -import ( - "fmt" - "net" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" -) - -const ( - // EventResponse thrown after the request been processed. See ErrorEvent as payload. - EventResponse = iota + 500 - - // EventError thrown on any non job error provided by road runner server. - EventError -) - -// ErrorEvent represents singular http error event. -type ErrorEvent struct { - // Request contains client request, must not be stored. - Request *http.Request - - // Error - associated error, if any. - Error error - - // event timings - start time.Time - elapsed time.Duration -} - -// Elapsed returns duration of the invocation. -func (e *ErrorEvent) Elapsed() time.Duration { - return e.elapsed -} - -// ResponseEvent represents singular http response event. -type ResponseEvent struct { - // Request contains client request, must not be stored. - Request *Request - - // Response contains service response. - Response *Response - - // event timings - start time.Time - elapsed time.Duration -} - -// Elapsed returns duration of the invocation. -func (e *ResponseEvent) Elapsed() time.Duration { - return e.elapsed -} - -// Handler serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, -// parsed files and query, payload will include parsed form dataTree (if any). -type Handler struct { - cfg *Config - log *logrus.Logger - rr *roadrunner.Server - mul sync.Mutex - lsn func(event int, ctx interface{}) -} - -// Listen attaches handler event controller. -func (h *Handler) Listen(l func(event int, ctx interface{})) { - h.mul.Lock() - defer h.mul.Unlock() - - h.lsn = l -} - -// mdwr serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled. -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - // validating request size - if h.cfg.MaxRequestSize != 0 { - if length := r.Header.Get("content-length"); length != "" { - if size, err := strconv.ParseInt(length, 10, 64); err != nil { - h.handleError(w, r, err, start) - return - } else if size > h.cfg.MaxRequestSize*1024*1024 { - h.handleError(w, r, errors.New("request body max size is exceeded"), start) - return - } - } - } - - req, err := NewRequest(r, h.cfg.Uploads) - if err != nil { - h.handleError(w, r, err, start) - return - } - - // proxy IP resolution - h.resolveIP(req) - - req.Open(h.log) - defer req.Close(h.log) - - p, err := req.Payload() - if err != nil { - h.handleError(w, r, err, start) - return - } - - rsp, err := h.rr.Exec(p) - if err != nil { - h.handleError(w, r, err, start) - return - } - - resp, err := NewResponse(rsp) - if err != nil { - h.handleError(w, r, err, start) - return - } - - h.handleResponse(req, resp, start) - err = resp.Write(w) - if err != nil { - h.handleError(w, r, err, start) - } -} - -// handleError sends error. -func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error, start time.Time) { - // if pipe is broken, there is no sense to write the header - // in this case we just report about error - if err == errEPIPE { - h.throw(EventError, &ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) - return - } - // ResponseWriter is ok, write the error code - w.WriteHeader(500) - _, err2 := w.Write([]byte(err.Error())) - // error during the writing to the ResponseWriter - if err2 != nil { - // concat original error with ResponseWriter error - h.throw(EventError, &ErrorEvent{Request: r, Error: errors.New(fmt.Sprintf("error: %v, during handle this error, ResponseWriter error occurred: %v", err, err2)), start: start, elapsed: time.Since(start)}) - return - } - h.throw(EventError, &ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) -} - -// handleResponse triggers response event. -func (h *Handler) handleResponse(req *Request, resp *Response, start time.Time) { - h.throw(EventResponse, &ResponseEvent{Request: req, Response: resp, start: start, elapsed: time.Since(start)}) -} - -// throw invokes event handler if any. -func (h *Handler) throw(event int, ctx interface{}) { - h.mul.Lock() - defer h.mul.Unlock() - - if h.lsn != nil { - h.lsn(event, ctx) - } -} - -// get real ip passing multiple proxy -func (h *Handler) resolveIP(r *Request) { - if !h.cfg.IsTrusted(r.RemoteAddr) { - return - } - - if r.Header.Get("X-Forwarded-For") != "" { - ips := strings.Split(r.Header.Get("X-Forwarded-For"), ",") - ipCount := len(ips) - - for i := ipCount - 1; i >= 0; i-- { - addr := strings.TrimSpace(ips[i]) - if net.ParseIP(addr) != nil { - r.RemoteAddr = addr - return - } - } - - return - } - - // The logic here is the following: - // In general case, we only expect X-Real-Ip header. If it exist, we get the IP addres from header and set request Remote address - // But, if there is no X-Real-Ip header, we also trying to check CloudFlare headers - // True-Client-IP is a general CF header in which copied information from X-Real-Ip in CF. - // CF-Connecting-IP is an Enterprise feature and we check it last in order. - // This operations are near O(1) because Headers struct are the map type -> type MIMEHeader map[string][]string - if r.Header.Get("X-Real-Ip") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("X-Real-Ip")) - return - } - - if r.Header.Get("True-Client-IP") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("True-Client-IP")) - return - } - - if r.Header.Get("CF-Connecting-IP") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("CF-Connecting-IP")) - } -} diff --git a/service/http/handler_test.go b/service/http/handler_test.go deleted file mode 100644 index 951bcbfd..00000000 --- a/service/http/handler_test.go +++ /dev/null @@ -1,1967 +0,0 @@ -package http - -import ( - "bytes" - "context" - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" - "io/ioutil" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/url" - "os" - "runtime" - "strings" - "testing" - "time" -) - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -// get request and return body -func getHeader(url string, h map[string]string) (string, *http.Response, error) { - req, err := http.NewRequest("GET", url, bytes.NewBuffer(nil)) - if err != nil { - return "", nil, err - } - - for k, v := range h { - req.Header.Set(k, v) - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -func TestHandler_Echo(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func Test_HandlerErrors(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - wr := httptest.NewRecorder() - rq := httptest.NewRequest("POST", "/", bytes.NewBuffer([]byte("data"))) - - h.ServeHTTP(wr, rq) - assert.Equal(t, 500, wr.Code) -} - -func Test_Handler_JSON_error(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - wr := httptest.NewRecorder() - rq := httptest.NewRequest("POST", "/", bytes.NewBuffer([]byte("{sd"))) - rq.Header.Add("Content-Type", "application/json") - rq.Header.Add("Content-Size", "3") - - h.ServeHTTP(wr, rq) - assert.Equal(t, 500, wr.Code) -} - -func TestHandler_Headers(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php header pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8078", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:8078?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("input", "sample") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "world", r.Header.Get("Header")) - assert.Equal(t, "SAMPLE", string(b)) -} - -func TestHandler_Empty_User_Agent(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php user-agent pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8088", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("user-agent", "") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "", string(b)) -} - -func TestHandler_User_Agent(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php user-agent pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8088", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("User-Agent", "go-agent") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "go-agent", string(b)) -} - -func TestHandler_Cookies(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php cookie pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8079", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8079", nil) - assert.NoError(t, err) - - req.AddCookie(&http.Cookie{Name: "input", Value: "input-value"}) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "INPUT-VALUE", string(b)) - - for _, c := range r.Cookies() { - assert.Equal(t, "output", c.Name) - assert.Equal(t, "cookie-output", c.Value) - } -} - -func TestHandler_JsonPayload_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8090", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest( - "POST", - "http://localhost"+hs.Addr, - bytes.NewBufferString(`{"key":"value"}`), - ) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_JsonPayload_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8081", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_JsonPayload_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8082", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_FormData_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_POST_Overwrite(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":18084", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8085", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8019", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8020", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Error(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_Error2(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error2 pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_Error3(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php pid pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - b2 := &bytes.Buffer{} - for i := 0; i < 1024*1024; i++ { - b2.Write([]byte(" ")) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, b2) - assert.NoError(t, err) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_ResponseDuration(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - gotresp := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventResponse { - c := ctx.(*ResponseEvent) - - if c.Elapsed() > 0 { - close(gotresp) - } - } - }) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-gotresp - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func TestHandler_ResponseDurationDelayed(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echoDelay pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - gotresp := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventResponse { - c := ctx.(*ResponseEvent) - - if c.Elapsed() > time.Second { - close(gotresp) - } - } - }) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-gotresp - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func TestHandler_ErrorDuration(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - goterr := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventError { - c := ctx.(*ErrorEvent) - - if c.Elapsed() > 0 { - close(goterr) - } - } - }) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-goterr - - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_IP(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := get("http://127.0.0.1:8177/") - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "127.0.0.1", body) -} - -func TestHandler_XRealIP(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Real-Ip": "200.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "200.0.0.1", body) -} - -func TestHandler_XForwardedFor(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "100.0.0.0/16", - "200.0.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "101.0.0.1", body) - - body, r, err = getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, 101.0.0.1, invalid", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "101.0.0.1", body) -} - -func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "127.0.0.1", body) -} - -func BenchmarkHandler_Listen_Echo(b *testing.B) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.rr.Start() - if err != nil { - b.Errorf("error starting the worker pool: error %v", err) - } - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - b.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - b.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - bb := "WORLD" - for n := 0; n < b.N; n++ { - r, err := http.Get("http://localhost:8177/?hello=world") - if err != nil { - b.Fail() - } - // Response might be nil here - if r != nil { - br, err := ioutil.ReadAll(r.Body) - if err != nil { - b.Errorf("error reading Body: error %v", err) - } - if string(br) != bb { - b.Fail() - } - err = r.Body.Close() - if err != nil { - b.Errorf("error closing the Body: error %v", err) - } - } else { - b.Errorf("got nil response") - } - } -} diff --git a/service/http/parse.go b/service/http/parse.go deleted file mode 100644 index 9b58d328..00000000 --- a/service/http/parse.go +++ /dev/null @@ -1,147 +0,0 @@ -package http - -import ( - "net/http" -) - -// MaxLevel defines maximum tree depth for incoming request data and files. -const MaxLevel = 127 - -type dataTree map[string]interface{} -type fileTree map[string]interface{} - -// parseData parses incoming request body into data tree. -func parseData(r *http.Request) dataTree { - data := make(dataTree) - if r.PostForm != nil { - for k, v := range r.PostForm { - data.push(k, v) - } - } - - if r.MultipartForm != nil { - for k, v := range r.MultipartForm.Value { - data.push(k, v) - } - } - - return data -} - -// pushes value into data tree. -func (d dataTree) push(k string, v []string) { - keys := fetchIndexes(k) - if len(keys) <= MaxLevel { - d.mount(keys, v) - } -} - -// mount mounts data tree recursively. -func (d dataTree) mount(i []string, v []string) { - if len(i) == 1 { - // single value context (last element) - d[i[0]] = v[len(v)-1] - return - } - - if len(i) == 2 && i[1] == "" { - // non associated array of elements - d[i[0]] = v - return - } - - if p, ok := d[i[0]]; ok { - p.(dataTree).mount(i[1:], v) - return - } - - d[i[0]] = make(dataTree) - d[i[0]].(dataTree).mount(i[1:], v) -} - -// parse incoming dataTree request into JSON (including contentMultipart form dataTree) -func parseUploads(r *http.Request, cfg *UploadsConfig) *Uploads { - u := &Uploads{ - cfg: cfg, - tree: make(fileTree), - list: make([]*FileUpload, 0), - } - - for k, v := range r.MultipartForm.File { - files := make([]*FileUpload, 0, len(v)) - for _, f := range v { - files = append(files, NewUpload(f)) - } - - u.list = append(u.list, files...) - u.tree.push(k, files) - } - - return u -} - -// pushes new file upload into it's proper place. -func (d fileTree) push(k string, v []*FileUpload) { - keys := fetchIndexes(k) - if len(keys) <= MaxLevel { - d.mount(keys, v) - } -} - -// mount mounts data tree recursively. -func (d fileTree) mount(i []string, v []*FileUpload) { - if len(i) == 1 { - // single value context - d[i[0]] = v[0] - return - } - - if len(i) == 2 && i[1] == "" { - // non associated array of elements - d[i[0]] = v - return - } - - if p, ok := d[i[0]]; ok { - p.(fileTree).mount(i[1:], v) - return - } - - d[i[0]] = make(fileTree) - d[i[0]].(fileTree).mount(i[1:], v) -} - -// fetchIndexes parses input name and splits it into separate indexes list. -func fetchIndexes(s string) []string { - var ( - pos int - ch string - keys = make([]string, 1) - ) - - for _, c := range s { - ch = string(c) - switch ch { - case " ": - // ignore all spaces - continue - case "[": - pos = 1 - continue - case "]": - if pos == 1 { - keys = append(keys, "") - } - pos = 2 - default: - if pos == 1 || pos == 2 { - keys = append(keys, "") - } - - keys[len(keys)-1] += ch - pos = 0 - } - } - - return keys -} diff --git a/service/http/parse_test.go b/service/http/parse_test.go deleted file mode 100644 index f95a3f9d..00000000 --- a/service/http/parse_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package http - -import "testing" - -var samples = []struct { - in string - out []string -}{ - {"key", []string{"key"}}, - {"key[subkey]", []string{"key", "subkey"}}, - {"key[subkey]value", []string{"key", "subkey", "value"}}, - {"key[subkey][value]", []string{"key", "subkey", "value"}}, - {"key[subkey][value][]", []string{"key", "subkey", "value", ""}}, - {"key[subkey] [value][]", []string{"key", "subkey", "value", ""}}, - {"key [ subkey ] [ value ] [ ]", []string{"key", "subkey", "value", ""}}, -} - -func Test_FetchIndexes(t *testing.T) { - for _, tt := range samples { - t.Run(tt.in, func(t *testing.T) { - r := fetchIndexes(tt.in) - if !same(r, tt.out) { - t.Errorf("got %q, want %q", r, tt.out) - } - }) - } -} - -func BenchmarkConfig_FetchIndexes(b *testing.B) { - for _, tt := range samples { - for n := 0; n < b.N; n++ { - r := fetchIndexes(tt.in) - if !same(r, tt.out) { - b.Fail() - } - } - } -} - -func same(in, out []string) bool { - if len(in) != len(out) { - return false - } - - for i, v := range in { - if v != out[i] { - return false - } - } - - return true -} diff --git a/service/http/request.go b/service/http/request.go deleted file mode 100644 index 8da5440f..00000000 --- a/service/http/request.go +++ /dev/null @@ -1,183 +0,0 @@ -package http - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "strings" - - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service/http/attributes" -) - -const ( - defaultMaxMemory = 32 << 20 // 32 MB - contentNone = iota + 900 - contentStream - contentMultipart - contentFormData -) - -// Request maps net/http requests to PSR7 compatible structure and managed state of temporary uploaded files. -type Request struct { - // RemoteAddr contains ip address of client, make sure to check X-Real-Ip and X-Forwarded-For for real client address. - RemoteAddr string `json:"remoteAddr"` - - // Protocol includes HTTP protocol version. - Protocol string `json:"protocol"` - - // Method contains name of HTTP method used for the request. - Method string `json:"method"` - - // URI contains full request URI with scheme and query. - URI string `json:"uri"` - - // Header contains list of request headers. - Header http.Header `json:"headers"` - - // Cookies contains list of request cookies. - Cookies map[string]string `json:"cookies"` - - // RawQuery contains non parsed query string (to be parsed on php end). - RawQuery string `json:"rawQuery"` - - // Parsed indicates that request body has been parsed on RR end. - Parsed bool `json:"parsed"` - - // Uploads contains list of uploaded files, their names, sized and associations with temporary files. - Uploads *Uploads `json:"uploads"` - - // Attributes can be set by chained mdwr to safely pass value from Golang to PHP. See: GetAttribute, SetAttribute functions. - Attributes map[string]interface{} `json:"attributes"` - - // request body can be parsedData or []byte - body interface{} -} - -func fetchIP(pair string) string { - if !strings.ContainsRune(pair, ':') { - return pair - } - - addr, _, _ := net.SplitHostPort(pair) - return addr -} - -// NewRequest creates new PSR7 compatible request using net/http request. -func NewRequest(r *http.Request, cfg *UploadsConfig) (req *Request, err error) { - req = &Request{ - RemoteAddr: fetchIP(r.RemoteAddr), - Protocol: r.Proto, - Method: r.Method, - URI: uri(r), - Header: r.Header, - Cookies: make(map[string]string), - RawQuery: r.URL.RawQuery, - Attributes: attributes.All(r), - } - - for _, c := range r.Cookies() { - if v, err := url.QueryUnescape(c.Value); err == nil { - req.Cookies[c.Name] = v - } - } - - switch req.contentType() { - case contentNone: - return req, nil - - case contentStream: - req.body, err = ioutil.ReadAll(r.Body) - return req, err - - case contentMultipart: - if err = r.ParseMultipartForm(defaultMaxMemory); err != nil { - return nil, err - } - - req.Uploads = parseUploads(r, cfg) - fallthrough - case contentFormData: - if err = r.ParseForm(); err != nil { - return nil, err - } - - req.body = parseData(r) - } - - req.Parsed = true - return req, nil -} - -// Open moves all uploaded files to temporary directory so it can be given to php later. -func (r *Request) Open(log *logrus.Logger) { - if r.Uploads == nil { - return - } - - r.Uploads.Open(log) -} - -// Close clears all temp file uploads -func (r *Request) Close(log *logrus.Logger) { - if r.Uploads == nil { - return - } - - r.Uploads.Clear(log) -} - -// Payload request marshaled RoadRunner payload based on PSR7 data. values encode method is JSON. Make sure to open -// files prior to calling this method. -func (r *Request) Payload() (p *roadrunner.Payload, err error) { - p = &roadrunner.Payload{} - - j := json.ConfigCompatibleWithStandardLibrary - if p.Context, err = j.Marshal(r); err != nil { - return nil, err - } - - if r.Parsed { - if p.Body, err = j.Marshal(r.body); err != nil { - return nil, err - } - } else if r.body != nil { - p.Body = r.body.([]byte) - } - - return p, nil -} - -// contentType returns the payload content type. -func (r *Request) contentType() int { - if r.Method == "HEAD" || r.Method == "OPTIONS" { - return contentNone - } - - ct := r.Header.Get("content-type") - if strings.Contains(ct, "application/x-www-form-urlencoded") { - return contentFormData - } - - if strings.Contains(ct, "multipart/form-data") { - return contentMultipart - } - - return contentStream -} - -// uri fetches full uri from request in a form of string (including https scheme if TLS connection is enabled). -func uri(r *http.Request) string { - if r.URL.Host != "" { - return r.URL.String() - } - if r.TLS != nil { - return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) - } - - return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) -} diff --git a/service/http/response.go b/service/http/response.go deleted file mode 100644 index f34754be..00000000 --- a/service/http/response.go +++ /dev/null @@ -1,107 +0,0 @@ -package http - -import ( - "io" - "net/http" - "strings" - - json "github.com/json-iterator/go" - - "github.com/spiral/roadrunner" -) - - -// Response handles PSR7 response logic. -type Response struct { - // Status contains response status. - Status int `json:"status"` - - // Header contains list of response headers. - Headers map[string][]string `json:"headers"` - - // associated body payload. - body interface{} -} - -// NewResponse creates new response based on given rr payload. -func NewResponse(p *roadrunner.Payload) (*Response, error) { - r := &Response{body: p.Body} - j := json.ConfigCompatibleWithStandardLibrary - if err := j.Unmarshal(p.Context, r); err != nil { - return nil, err - } - - return r, nil -} - -// Write writes response headers, status and body into ResponseWriter. -func (r *Response) Write(w http.ResponseWriter) error { - // INFO map is the reference type in golang - p := handlePushHeaders(r.Headers) - if pusher, ok := w.(http.Pusher); ok { - for _, v := range p { - err := pusher.Push(v, nil) - if err != nil { - return err - } - } - } - - handleTrailers(r.Headers) - for n, h := range r.Headers { - for _, v := range h { - w.Header().Add(n, v) - } - } - - w.WriteHeader(r.Status) - - if data, ok := r.body.([]byte); ok { - _, err := w.Write(data) - if err != nil { - return handleWriteError(err) - } - } - - if rc, ok := r.body.(io.Reader); ok { - if _, err := io.Copy(w, rc); err != nil { - return err - } - } - - return nil -} - -func handlePushHeaders(h map[string][]string) []string { - var p []string - pushHeader, ok := h[http2pushHeaderKey] - if !ok { - return p - } - - p = append(p, pushHeader...) - - delete(h, http2pushHeaderKey) - - return p -} - -func handleTrailers(h map[string][]string) { - trailers, ok := h[trailerHeaderKey] - if !ok { - return - } - - for _, tr := range trailers { - for _, n := range strings.Split(tr, ",") { - n = strings.Trim(n, "\t ") - if v, ok := h[n]; ok { - h["Trailer:"+n] = v - - delete(h, n) - } - } - } - - delete(h, trailerHeaderKey) -} diff --git a/service/http/response_test.go b/service/http/response_test.go deleted file mode 100644 index 1f394276..00000000 --- a/service/http/response_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package http - -import ( - "bytes" - "errors" - "net/http" - "testing" - - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" -) - -type testWriter struct { - h http.Header - buf bytes.Buffer - wroteHeader bool - code int - err error - pushErr error - pushes []string -} - -func (tw *testWriter) Header() http.Header { return tw.h } - -func (tw *testWriter) Write(p []byte) (int, error) { - if !tw.wroteHeader { - tw.WriteHeader(http.StatusOK) - } - - n, e := tw.buf.Write(p) - if e == nil { - e = tw.err - } - - return n, e -} - -func (tw *testWriter) WriteHeader(code int) { tw.wroteHeader = true; tw.code = code } - -func (tw *testWriter) Push(target string, opts *http.PushOptions) error { - tw.pushes = append(tw.pushes, target) - - return tw.pushErr -} - -func TestNewResponse_Error(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{Context: []byte(`invalid payload`)}) - assert.Error(t, err) - assert.Nil(t, r) -} - -func TestNewResponse_Write(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - Body: []byte(`sample body`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, 301, w.code) - assert.Equal(t, "value", w.h.Get("key")) - assert.Equal(t, "sample body", w.buf.String()) -} - -func TestNewResponse_Stream(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - }) - - // r is pointer, so, it might be nil - if r == nil { - t.Fatal("response is nil") - } - - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, 301, w.code) - assert.Equal(t, "value", w.h.Get("key")) - assert.Equal(t, "hello world", w.buf.String()) -} - -func TestNewResponse_StreamError(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - }) - - // r is pointer, so, it might be nil - if r == nil { - t.Fatal("response is nil") - } - - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string)), err: errors.New("error")} - assert.Error(t, r.Write(w)) -} - -func TestWrite_HandlesPush(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Nil(t, w.h["Http2-Push"]) - assert.Equal(t, []string{"/test.js"}, w.pushes) -} - -func TestWrite_HandlesTrailers(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Nil(t, w.h[trailerHeaderKey]) - assert.Nil(t, w.h["foo"]) //nolint:golint,staticcheck - assert.Nil(t, w.h["baz"]) //nolint:golint,staticcheck - - assert.Equal(t, "test", w.h.Get("Trailer:foo")) - assert.Equal(t, "demo", w.h.Get("Trailer:bar")) -} - -func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte( - `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, "a", w.h.Get("Trailer:foo")) - assert.Equal(t, "b", w.h.Get("Trailer:bar")) - assert.Equal(t, "c", w.h.Get("Trailer:baz")) -} diff --git a/service/http/rpc.go b/service/http/rpc.go deleted file mode 100644 index 7b38dece..00000000 --- a/service/http/rpc.go +++ /dev/null @@ -1,34 +0,0 @@ -package http - -import ( - "github.com/pkg/errors" - "github.com/spiral/roadrunner/util" -) - -type rpcServer struct{ svc *Service } - -// WorkerList contains list of workers. -type WorkerList struct { - // Workers is list of workers. - Workers []*util.State `json:"workers"` -} - -// Reset resets underlying RR worker pool and restarts all of it's workers. -func (rpc *rpcServer) Reset(reset bool, r *string) error { - if rpc.svc == nil || rpc.svc.handler == nil { - return errors.New("http server is not running") - } - - *r = "OK" - return rpc.svc.Server().Reset() -} - -// Workers returns list of active workers and their stats. -func (rpc *rpcServer) Workers(list bool, r *WorkerList) (err error) { - if rpc.svc == nil || rpc.svc.handler == nil { - return errors.New("http server is not running") - } - - r.Workers, err = util.ServerState(rpc.svc.Server()) - return err -} diff --git a/service/http/rpc_test.go b/service/http/rpc_test.go deleted file mode 100644 index e57a8699..00000000 --- a/service/http/rpc_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package http - -import ( - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "os" - "strconv" - "testing" - "time" -) - -func Test_RPC(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:5004"}`, - httpCfg: `{ - "enable": true, - "address": ":16031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Second) - - res, _, err := get("http://localhost:16031") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res) - - cl, err := rs.Client() - assert.NoError(t, err) - - r := "" - assert.NoError(t, cl.Call("http.Reset", true, &r)) - assert.Equal(t, "OK", r) - - res2, _, err := get("http://localhost:16031") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res2) - assert.NotEqual(t, res, res2) - c.Stop() -} - -func Test_RPC_Unix(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - sock := `unix://` + os.TempDir() + `/rpc.unix` - j := json.ConfigCompatibleWithStandardLibrary - data, _ := j.Marshal(sock) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":` + string(data) + `}`, - httpCfg: `{ - "enable": true, - "address": ":6032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - res, _, err := get("http://localhost:6032") - if err != nil { - c.Stop() - t.Fatal(err) - } - if ss.rr.Workers() != nil && len(ss.rr.Workers()) > 0 { - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res) - } else { - c.Stop() - t.Fatal("no workers initialized") - } - - cl, err := rs.Client() - if err != nil { - c.Stop() - t.Fatal(err) - } - - r := "" - assert.NoError(t, cl.Call("http.Reset", true, &r)) - assert.Equal(t, "OK", r) - - res2, _, err := get("http://localhost:6032") - if err != nil { - c.Stop() - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res2) - assert.NotEqual(t, res, res2) - c.Stop() -} - -func Test_Workers(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:5005"}`, - httpCfg: `{ - "enable": true, - "address": ":6033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - cl, err := rs.Client() - assert.NoError(t, err) - - r := &WorkerList{} - assert.NoError(t, cl.Call("http.Workers", true, &r)) - assert.Len(t, r.Workers, 1) - - assert.Equal(t, *ss.rr.Workers()[0].Pid, r.Workers[0].Pid) - c.Stop() -} - -func Test_Errors(t *testing.T) { - r := &rpcServer{nil} - - assert.Error(t, r.Reset(true, nil)) - assert.Error(t, r.Workers(true, nil)) -} diff --git a/service/http/service.go b/service/http/service.go deleted file mode 100644 index 25a10064..00000000 --- a/service/http/service.go +++ /dev/null @@ -1,427 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/http/fcgi" - "net/url" - "strings" - "sync" - - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service/env" - "github.com/spiral/roadrunner/service/http/attributes" - "github.com/spiral/roadrunner/service/rpc" - "github.com/spiral/roadrunner/util" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - "golang.org/x/sys/cpu" -) - -const ( - // ID contains default service name. - ID = "http" - - // EventInitSSL thrown at moment of https initialization. SSL server passed as context. - EventInitSSL = 750 -) - -var couldNotAppendPemError = errors.New("could not append Certs from PEM") - -// http middleware type. -type middleware func(f http.HandlerFunc) http.HandlerFunc - -// Service manages rr, http servers. -type Service struct { - sync.Mutex - sync.WaitGroup - - cfg *Config - log *logrus.Logger - cprod roadrunner.CommandProducer - env env.Environment - lsns []func(event int, ctx interface{}) - mdwr []middleware - - rr *roadrunner.Server - controller roadrunner.Controller - handler *Handler - - http *http.Server - https *http.Server - fcgi *http.Server -} - -// Attach attaches controller. Currently only one controller is supported. -func (s *Service) Attach(w roadrunner.Controller) { - s.controller = w -} - -// ProduceCommands changes the default command generator method -func (s *Service) ProduceCommands(producer roadrunner.CommandProducer) { - s.cprod = producer -} - -// AddMiddleware adds new net/http mdwr. -func (s *Service) AddMiddleware(m middleware) { - s.mdwr = append(s.mdwr, m) -} - -// AddListener attaches server event controller. -func (s *Service) AddListener(l func(event int, ctx interface{})) { - s.lsns = append(s.lsns, l) -} - -// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rpc.Service, e env.Environment, log *logrus.Logger) (bool, error) { - s.cfg = cfg - s.log = log - s.env = e - - if r != nil { - if err := r.Register(ID, &rpcServer{s}); err != nil { - return false, err - } - } - - if !cfg.EnableHTTP() && !cfg.EnableTLS() && !cfg.EnableFCGI() { - return false, nil - } - - return true, nil -} - -// Serve serves the svc. -func (s *Service) Serve() error { - s.Lock() - - if s.env != nil { - if err := s.env.Copy(s.cfg.Workers); err != nil { - return nil - } - } - - s.cfg.Workers.CommandProducer = s.cprod - s.cfg.Workers.SetEnv("RR_HTTP", "true") - - s.rr = roadrunner.NewServer(s.cfg.Workers) - s.rr.Listen(s.throw) - - if s.controller != nil { - s.rr.Attach(s.controller) - } - - s.handler = &Handler{cfg: s.cfg, rr: s.rr} - s.handler.Listen(s.throw) - - if s.cfg.EnableHTTP() { - if s.cfg.EnableH2C() { - s.http = &http.Server{Addr: s.cfg.Address, Handler: h2c.NewHandler(s, &http2.Server{})} - } else { - s.http = &http.Server{Addr: s.cfg.Address, Handler: s} - } - } - - if s.cfg.EnableTLS() { - s.https = s.initSSL() - if s.cfg.SSL.RootCA != "" { - err := s.appendRootCa() - if err != nil { - return err - } - } - - if s.cfg.EnableHTTP2() { - if err := s.initHTTP2(); err != nil { - return err - } - } - } - - if s.cfg.EnableFCGI() { - s.fcgi = &http.Server{Handler: s} - } - - s.Unlock() - - if err := s.rr.Start(); err != nil { - return err - } - defer s.rr.Stop() - - err := make(chan error, 3) - - if s.http != nil { - go func() { - httpErr := s.http.ListenAndServe() - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - } else { - err <- nil - } - }() - } - - if s.https != nil { - go func() { - httpErr := s.https.ListenAndServeTLS( - s.cfg.SSL.Cert, - s.cfg.SSL.Key, - ) - - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - return - } - err <- nil - }() - } - - if s.fcgi != nil { - go func() { - httpErr := s.serveFCGI() - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - return - } - err <- nil - }() - } - return <-err -} - -// Stop stops the http. -func (s *Service) Stop() { - s.Lock() - defer s.Unlock() - - if s.fcgi != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.fcgi.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - // Stop() error - // push error from goroutines to the channel and block unil error or success shutdown or timeout - s.log.Error(fmt.Errorf("error shutting down the fcgi server, error: %v", err)) - return - } - }() - } - - if s.https != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.https.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the https server, error: %v", err)) - return - } - }() - } - - if s.http != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.http.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the http server, error: %v", err)) - return - } - }() - } - - s.Wait() -} - -// Server returns associated rr server (if any). -func (s *Service) Server() *roadrunner.Server { - s.Lock() - defer s.Unlock() - - return s.rr -} - -// ServeHTTP handles connection using set of middleware and rr PSR-7 server. -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if s.https != nil && r.TLS == nil && s.cfg.SSL.Redirect { - target := &url.URL{ - Scheme: "https", - Host: s.tlsAddr(r.Host, false), - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - - http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) - return - } - - if s.https != nil && r.TLS != nil { - w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") - } - - r = attributes.Init(r) - - // chaining middleware - f := s.handler.ServeHTTP - for _, m := range s.mdwr { - f = m(f) - } - f(w, r) -} - -// append RootCA to the https server TLS config -func (s *Service) appendRootCa() error { - rootCAs, err := x509.SystemCertPool() - if err != nil { - s.throw(EventInitSSL, nil) - return nil - } - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } - - CA, err := ioutil.ReadFile(s.cfg.SSL.RootCA) - if err != nil { - s.throw(EventInitSSL, nil) - return err - } - - // should append our CA cert - ok := rootCAs.AppendCertsFromPEM(CA) - if !ok { - return couldNotAppendPemError - } - config := &tls.Config{ - InsecureSkipVerify: false, - RootCAs: rootCAs, - } - s.http.TLSConfig = config - - return nil -} - -// Init https server -func (s *Service) initSSL() *http.Server { - var topCipherSuites []uint16 - var defaultCipherSuitesTLS13 []uint16 - - hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ - hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL - // Keep in sync with crypto/aes/cipher_s390x.go. - hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) - - hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X - - if hasGCMAsm { - // If AES-GCM hardware is provided then prioritise AES-GCM - // cipher suites. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } else { - // Without AES-GCM hardware, we put the ChaCha20-Poly1305 - // cipher suites first. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } - - DefaultCipherSuites := make([]uint16, 0, 22) - DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) - DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) - - server := &http.Server{ - Addr: s.tlsAddr(s.cfg.Address, true), - Handler: s, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - s.throw(EventInitSSL, server) - - return server -} - -// init http/2 server -func (s *Service) initHTTP2() error { - return http2.ConfigureServer(s.https, &http2.Server{ - MaxConcurrentStreams: s.cfg.HTTP2.MaxConcurrentStreams, - }) -} - -// serveFCGI starts FastCGI server. -func (s *Service) serveFCGI() error { - l, err := util.CreateListener(s.cfg.FCGI.Address) - if err != nil { - return err - } - - err = fcgi.Serve(l, s.fcgi.Handler) - if err != nil { - return err - } - - return nil -} - -// throw handles service, server and pool events. -func (s *Service) throw(event int, ctx interface{}) { - for _, l := range s.lsns { - l(event, ctx) - } - - if event == roadrunner.EventServerFailure { - // underlying rr server is dead - s.Stop() - } -} - -// tlsAddr replaces listen or host port with port configured by SSL config. -func (s *Service) tlsAddr(host string, forcePort bool) string { - // remove current forcePort first - host = strings.Split(host, ":")[0] - - if forcePort || s.cfg.SSL.Port != 443 { - host = fmt.Sprintf("%s:%v", host, s.cfg.SSL.Port) - } - - return host -} diff --git a/service/http/service_test.go b/service/http/service_test.go deleted file mode 100644 index f7ee33cc..00000000 --- a/service/http/service_test.go +++ /dev/null @@ -1,759 +0,0 @@ -package http - -import ( - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "os" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - rpcCfg string - envCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - if cfg.httpCfg == "" { - return nil - } - - return &testCfg{target: cfg.httpCfg} - } - - if name == rpc.ID { - return &testCfg{target: cfg.rpcCfg} - } - - if name == env.ID { - return &testCfg{target: cfg.envCfg} - } - - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Service_NoConfig(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{"Enable":true}`}) - assert.Error(t, err) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Service_Configure_Disable(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Service_Configure_Enable(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":8070", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Echo(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6536", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:6536?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Env(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(env.ID, env.NewService(map[string]string{"rr": "test"})) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":10031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php env pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`, envCfg: `{"env_key":"ENV_VALUE"}`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:10031", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "ENV_VALUE", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ErrorEcho(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6030", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echoerr pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - goterr := make(chan interface{}) - s.(*Service).AddListener(func(event int, ctx interface{}) { - if event == roadrunner.EventStderrOutput { - if string(ctx.([]byte)) == "WORLD\n" { - goterr <- nil - } - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6030?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - <-goterr - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Middleware(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - s.(*Service).AddMiddleware(func(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/halt" { - w.WriteHeader(500) - _, err := w.Write([]byte("halted")) - if err != nil { - t.Errorf("error writing the data to the http reply: error %v", err) - } - } else { - f(w, r) - } - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6032?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - req, err = http.NewRequest("GET", "http://localhost:6032/halt", nil) - if err != nil { - c.Stop() - return err - } - - r, err = http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - b, err = ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 500, r.StatusCode) - assert.Equal(t, "halted", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - c.Stop() - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Listener(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - stop := make(chan interface{}) - s.(*Service).AddListener(func(event int, ctx interface{}) { - if event == roadrunner.EventServerStart { - stop <- nil - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - c.Stop() - assert.True(t, true) - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6034", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "---", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - // assert error - err = c.Serve() - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error2(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6035", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - // assert error - err = c.Serve() - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error3(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6036", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers" - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - // assert error - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Error4(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": "----", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - // assert error - if err != nil { - return nil - } - - return err - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func tmpDir() string { - p := os.TempDir() - j := json.ConfigCompatibleWithStandardLibrary - r, _ := j.Marshal(p) - - return string(r) -} diff --git a/service/http/ssl_test.go b/service/http/ssl_test.go deleted file mode 100644 index cf147be9..00000000 --- a/service/http/ssl_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package http - -import ( - "crypto/tls" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -var sslClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, -} - -func Test_SSL_Service_Echo(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6029", - "ssl": { - "port": 6900, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "https://localhost:6900?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - - c.Stop() -} - -func Test_SSL_Service_NoRedirect(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6030", - "ssl": { - "port": 6901, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6030?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - assert.Nil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} - -func Test_SSL_Service_Redirect(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6831", - "ssl": { - "port": 6902, - "redirect": true, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6831?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - assert.NotNil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} - -func Test_SSL_Service_Push(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6032", - "ssl": { - "port": 6903, - "redirect": true, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php push pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "https://localhost:6903?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - assert.NotNil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.Equal(t, "", r.Header.Get("Http2-Push")) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} diff --git a/service/http/uploads.go b/service/http/uploads.go deleted file mode 100644 index 39a9eaf2..00000000 --- a/service/http/uploads.go +++ /dev/null @@ -1,160 +0,0 @@ -package http - -import ( - "fmt" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "io" - "io/ioutil" - "mime/multipart" - "os" - "sync" -) - -const ( - // UploadErrorOK - no error, the file uploaded with success. - UploadErrorOK = 0 - - // UploadErrorNoFile - no file was uploaded. - UploadErrorNoFile = 4 - - // UploadErrorNoTmpDir - missing a temporary folder. - UploadErrorNoTmpDir = 5 - - // UploadErrorCantWrite - failed to write file to disk. - UploadErrorCantWrite = 6 - - // UploadErrorExtension - forbidden file extension. - UploadErrorExtension = 7 -) - -// Uploads tree manages uploaded files tree and temporary files. -type Uploads struct { - // associated temp directory and forbidden extensions. - cfg *UploadsConfig - - // pre processed data tree for Uploads. - tree fileTree - - // flat list of all file Uploads. - list []*FileUpload -} - -// MarshalJSON marshal tree tree into JSON. -func (u *Uploads) MarshalJSON() ([]byte, error) { - j := json.ConfigCompatibleWithStandardLibrary - return j.Marshal(u.tree) -} - -// Open moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors -// will be handled individually. -func (u *Uploads) Open(log *logrus.Logger) { - var wg sync.WaitGroup - for _, f := range u.list { - wg.Add(1) - go func(f *FileUpload) { - defer wg.Done() - err := f.Open(u.cfg) - if err != nil && log != nil { - log.Error(fmt.Errorf("error opening the file: error %v", err)) - } - }(f) - } - - wg.Wait() -} - -// Clear deletes all temporary files. -func (u *Uploads) Clear(log *logrus.Logger) { - for _, f := range u.list { - if f.TempFilename != "" && exists(f.TempFilename) { - err := os.Remove(f.TempFilename) - if err != nil && log != nil { - log.Error(fmt.Errorf("error removing the file: error %v", err)) - } - } - } -} - -// FileUpload represents singular file NewUpload. -type FileUpload struct { - // ID contains filename specified by the client. - Name string `json:"name"` - - // Mime contains mime-type provided by the client. - Mime string `json:"mime"` - - // Size of the uploaded file. - Size int64 `json:"size"` - - // Error indicates file upload error (if any). See http://php.net/manual/en/features.file-upload.errors.php - Error int `json:"error"` - - // TempFilename points to temporary file location. - TempFilename string `json:"tmpName"` - - // associated file header - header *multipart.FileHeader -} - -// NewUpload wraps net/http upload into PRS-7 compatible structure. -func NewUpload(f *multipart.FileHeader) *FileUpload { - return &FileUpload{ - Name: f.Filename, - Mime: f.Header.Get("Content-Type"), - Error: UploadErrorOK, - header: f, - } -} - -// Open moves file content into temporary file available for PHP. -// NOTE: -// There is 2 deferred functions, and in case of getting 2 errors from both functions -// error from close of temp file would be overwritten by error from the main file -// STACK -// DEFER FILE CLOSE (2) -// DEFER TMP CLOSE (1) -func (f *FileUpload) Open(cfg *UploadsConfig) (err error) { - if cfg.Forbids(f.Name) { - f.Error = UploadErrorExtension - return nil - } - - file, err := f.header.Open() - if err != nil { - f.Error = UploadErrorNoFile - return err - } - - defer func() { - // close the main file - err = file.Close() - }() - - tmp, err := ioutil.TempFile(cfg.TmpDir(), "upload") - if err != nil { - // most likely cause of this issue is missing tmp dir - f.Error = UploadErrorNoTmpDir - return err - } - - f.TempFilename = tmp.Name() - defer func() { - // close the temp file - err = tmp.Close() - }() - - if f.Size, err = io.Copy(tmp, file); err != nil { - f.Error = UploadErrorCantWrite - } - - return err -} - -// exists if file exists. -func exists(path string) bool { - if _, err := os.Stat(path); os.IsNotExist(err) { - return false - } - return true -} diff --git a/service/http/uploads_config.go b/service/http/uploads_config.go deleted file mode 100644 index 3f655064..00000000 --- a/service/http/uploads_config.go +++ /dev/null @@ -1,45 +0,0 @@ -package http - -import ( - "os" - "path" - "strings" -) - -// UploadsConfig describes file location and controls access to them. -type UploadsConfig struct { - // Dir contains name of directory to control access to. - Dir string - - // Forbid specifies list of file extensions which are forbidden for access. - // Example: .php, .exe, .bat, .htaccess and etc. - Forbid []string -} - -// InitDefaults sets missing values to their default values. -func (cfg *UploadsConfig) InitDefaults() error { - cfg.Forbid = []string{".php", ".exe", ".bat"} - return nil -} - -// TmpDir returns temporary directory. -func (cfg *UploadsConfig) TmpDir() string { - if cfg.Dir != "" { - return cfg.Dir - } - - return os.TempDir() -} - -// Forbids must return true if file extension is not allowed for the upload. -func (cfg *UploadsConfig) Forbids(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range cfg.Forbid { - if ext == v { - return true - } - } - - return false -} diff --git a/service/http/uploads_config_test.go b/service/http/uploads_config_test.go deleted file mode 100644 index 2b6ceebc..00000000 --- a/service/http/uploads_config_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package http - -import ( - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestFsConfig_Forbids(t *testing.T) { - cfg := UploadsConfig{Forbid: []string{".php"}} - - assert.True(t, cfg.Forbids("index.php")) - assert.True(t, cfg.Forbids("index.PHP")) - assert.True(t, cfg.Forbids("phpadmin/index.bak.php")) - assert.False(t, cfg.Forbids("index.html")) -} - -func TestFsConfig_TmpFallback(t *testing.T) { - cfg := UploadsConfig{Dir: "test"} - assert.Equal(t, "test", cfg.TmpDir()) - - cfg = UploadsConfig{Dir: ""} - assert.Equal(t, os.TempDir(), cfg.TmpDir()) -} diff --git a/service/http/uploads_test.go b/service/http/uploads_test.go deleted file mode 100644 index 08177c72..00000000 --- a/service/http/uploads_test.go +++ /dev/null @@ -1,434 +0,0 @@ -package http - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/hex" - "fmt" - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "os" - "testing" - "time" -) - -func TestHandler_Upload_File(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 0, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func TestHandler_Upload_NestedFile(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload[x][y][z][]", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 0, "application/octet-stream") - - assert.Equal(t, `{"upload":{"x":{"y":{"z":[`+fs+`]}}}}`, string(b)) -} - -func TestHandler_Upload_File_NoTmpDir(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: "-----", - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 5, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func TestHandler_Upload_File_Forbids(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 7, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func Test_FileExists(t *testing.T) { - assert.True(t, exists("uploads_test.go")) - assert.False(t, exists("uploads_test.")) -} - -func mustOpen(f string) *os.File { - r, err := os.Open(f) - if err != nil { - panic(err) - } - return r -} - -type fInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - Mime string `json:"mime"` - Error int `json:"error"` - MD5 string `json:"md5,omitempty"` -} - -func fileString(f string, errNo int, mime string) string { - s, err := os.Stat(f) - if err != nil { - fmt.Println(fmt.Errorf("error stat the file, error: %v", err)) - } - - ff, err := os.Open(f) - if err != nil { - fmt.Println(fmt.Errorf("error opening the file, error: %v", err)) - } - - defer func() { - er := ff.Close() - if er != nil { - fmt.Println(fmt.Errorf("error closing the file, error: %v", er)) - } - }() - - h := md5.New() - _, err = io.Copy(h, ff) - if err != nil { - fmt.Println(fmt.Errorf("error copying the file, error: %v", err)) - } - - v := &fInfo{ - Name: s.Name(), - Size: s.Size(), - Error: errNo, - Mime: mime, - MD5: hex.EncodeToString(h.Sum(nil)), - } - - if errNo != 0 { - v.MD5 = "" - v.Size = 0 - } - - j := json.ConfigCompatibleWithStandardLibrary - r, err := j.Marshal(v) - if err != nil { - fmt.Println(fmt.Errorf("error marshalling fInfo, error: %v", err)) - } - return string(r) - -} diff --git a/service/limit/config.go b/service/limit/config.go deleted file mode 100644 index 203db11b..00000000 --- a/service/limit/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "time" -) - -// Config of Limit service. -type Config struct { - // Interval defines the update duration for underlying controllers, default 1s. - Interval time.Duration - - // Services declares list of services to be watched. - Services map[string]*controllerConfig -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - // Always use second based definition for time durations - if c.Interval < time.Microsecond { - c.Interval = time.Second * time.Duration(c.Interval.Nanoseconds()) - } - - return nil -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Interval = time.Second - - return nil -} - -// Controllers returns list of defined Services -func (c *Config) Controllers(l listener) (controllers map[string]roadrunner.Controller) { - controllers = make(map[string]roadrunner.Controller) - - for name, cfg := range c.Services { - controllers[name] = &controller{lsn: l, tick: c.Interval, cfg: cfg} - } - - return controllers -} diff --git a/service/limit/config_test.go b/service/limit/config_test.go deleted file mode 100644 index c79836b8..00000000 --- a/service/limit/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package limit - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"enable: true}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Controller_Default(t *testing.T) { - cfg := &mockCfg{` -{ - "services":{ - "http": { - "TTL": 1 - } - } -} -`} - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("failed to InitDefaults: error %v", err) - } - - assert.NoError(t, c.Hydrate(cfg)) - assert.Equal(t, time.Second, c.Interval) - - list := c.Controllers(func(event int, ctx interface{}) { - }) - - sc := list["http"] - - assert.Equal(t, time.Second, sc.(*controller).tick) -} diff --git a/service/limit/controller.go b/service/limit/controller.go deleted file mode 100644 index 24a158f7..00000000 --- a/service/limit/controller.go +++ /dev/null @@ -1,166 +0,0 @@ -package limit - -import ( - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/util" - "time" -) - -const ( - // EventMaxMemory caused when worker consumes more memory than allowed. - EventMaxMemory = iota + 8000 - - // EventTTL thrown when worker is removed due TTL being reached. Context is rr.WorkerError - EventTTL - - // EventIdleTTL triggered when worker spends too much time at rest. - EventIdleTTL - - // EventExecTTL triggered when worker spends too much time doing the task (max_execution_time). - EventExecTTL -) - -// handles controller events -type listener func(event int, ctx interface{}) - -// defines the controller behaviour -type controllerConfig struct { - // MaxMemory defines maximum amount of memory allowed for worker. In megabytes. - MaxMemory uint64 - - // TTL defines maximum time worker is allowed to live. - TTL int64 - - // IdleTTL defines maximum duration worker can spend in idle mode. - IdleTTL int64 - - // ExecTTL defines maximum lifetime per job. - ExecTTL int64 -} - -type controller struct { - lsn listener - tick time.Duration - cfg *controllerConfig - - // list of workers which are currently working - sw *stateFilter - - stop chan interface{} -} - -// control the pool state -func (c *controller) control(p roadrunner.Pool) { - c.loadWorkers(p) - - now := time.Now() - - if c.cfg.ExecTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateWorking, - now.Add(-time.Second*time.Duration(c.cfg.ExecTTL)), - ) { - eID := w.State().NumExecs() - err := fmt.Errorf("max exec time reached (%vs)", c.cfg.ExecTTL) - - // make sure worker still on initial request - if p.Remove(w, err) && w.State().NumExecs() == eID { - go func() { - err := w.Kill() - if err != nil { - fmt.Printf("error killing worker with PID number: %d, created: %s", w.Pid, w.Created) - } - }() - c.report(EventExecTTL, w, err) - } - } - } - - // locale workers which are in idle mode for too long - if c.cfg.IdleTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateReady, - now.Add(-time.Second*time.Duration(c.cfg.IdleTTL)), - ) { - err := fmt.Errorf("max idle time reached (%vs)", c.cfg.IdleTTL) - if p.Remove(w, err) { - c.report(EventIdleTTL, w, err) - } - } - } -} - -func (c *controller) loadWorkers(p roadrunner.Pool) { - now := time.Now() - - for _, w := range p.Workers() { - if w.State().Value() == roadrunner.StateInvalid { - // skip duplicate assessment - continue - } - - s, err := util.WorkerState(w) - if err != nil { - continue - } - - if c.cfg.TTL != 0 && now.Sub(w.Created).Seconds() >= float64(c.cfg.TTL) { - err := fmt.Errorf("max TTL reached (%vs)", c.cfg.TTL) - if p.Remove(w, err) { - c.report(EventTTL, w, err) - } - continue - } - - if c.cfg.MaxMemory != 0 && s.MemoryUsage >= c.cfg.MaxMemory*1024*1024 { - err := fmt.Errorf("max allowed memory reached (%vMB)", c.cfg.MaxMemory) - if p.Remove(w, err) { - c.report(EventMaxMemory, w, err) - } - continue - } - - // control the worker state changes - c.sw.push(w) - } - - c.sw.sync(now) -} - -// throw controller event -func (c *controller) report(event int, worker *roadrunner.Worker, caused error) { - if c.lsn != nil { - c.lsn(event, roadrunner.WorkerError{Worker: worker, Caused: caused}) - } -} - -// Attach controller to the pool -func (c *controller) Attach(pool roadrunner.Pool) roadrunner.Controller { - wp := &controller{ - tick: c.tick, - lsn: c.lsn, - cfg: c.cfg, - sw: newStateFilter(), - stop: make(chan interface{}), - } - - go func(wp *controller, pool roadrunner.Pool) { - ticker := time.NewTicker(wp.tick) - for { - select { - case <-ticker.C: - wp.control(pool) - case <-wp.stop: - return - } - } - }(wp, pool) - - return wp -} - -// Detach controller from the pool. -func (c *controller) Detach() { - close(c.stop) -} diff --git a/service/limit/service.go b/service/limit/service.go deleted file mode 100644 index c0b4139c..00000000 --- a/service/limit/service.go +++ /dev/null @@ -1,39 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" -) - -// ID defines controller service name. -const ID = "limit" - -// Service to control the state of rr service inside other services. -type Service struct { - lsns []func(event int, ctx interface{}) -} - -// Init controller service -func (s *Service) Init(cfg *Config, c service.Container) (bool, error) { - // mount Services to designated services - for id, watcher := range cfg.Controllers(s.throw) { - svc, _ := c.Get(id) - if ctrl, ok := svc.(roadrunner.Attacher); ok { - ctrl.Attach(watcher) - } - } - - return true, nil -} - -// AddListener attaches server event controller. -func (s *Service) AddListener(l func(event int, ctx interface{})) { - s.lsns = append(s.lsns, l) -} - -// throw handles service, server and pool events. -func (s *Service) throw(event int, ctx interface{}) { - for _, l := range s.lsns { - l(event, ctx) - } -} diff --git a/service/limit/service_test.go b/service/limit/service_test.go deleted file mode 100644 index fae73166..00000000 --- a/service/limit/service_test.go +++ /dev/null @@ -1,498 +0,0 @@ -package limit - -import ( - "fmt" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" -) - -type testCfg struct { - httpCfg string - limitCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - if cfg.httpCfg == "" { - return nil - } - - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.limitCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - - if cl, ok := out.(*Config); ok { - // to speed up tests - cl.Interval = time.Millisecond - } - - return err -} - -func Test_Service_PidEcho(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":27029", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 800) - req, err := http.NewRequest("GET", "http://localhost:27029", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - assert.Equal(t, getPID(s), string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - c.Stop() - return nil - - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7030", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7030", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7030?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusIdleTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7031", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "idleTtl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventIdleTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7031", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7031?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - return nil - }, bkoff) - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxExecTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7032", - "workers":{ - "command": "php ../../tests/http/client.php stuck pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "execTTL": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventExecTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:7032", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - assert.Equal(t, 500, r.StatusCode) - - <-captured - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxMemoryUsage(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":10033", - "workers":{ - "command": "php ../../tests/http/client.php memleak pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "maxMemory": 10 - } - } - }`, - }) - if err != nil { - return err - } - - time.Sleep(time.Second * 3) - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - once := false - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventMaxMemory && !once { - close(captured) - once = true - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:10033", nil) - if err != nil { - return err - } - - for { - select { - case <-captured: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - assert.NotEqual(t, lastPID, getPID(s)) - c.Stop() - return nil - default: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - c.Stop() - return nil - } - } - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} -func getPID(s interface{}) string { - if len(s.(*rrhttp.Service).Server().Workers()) > 0 { - w := s.(*rrhttp.Service).Server().Workers()[0] - return fmt.Sprintf("%v", *w.Pid) - } else { - panic("no workers") - } -} diff --git a/service/limit/state_filter.go b/service/limit/state_filter.go deleted file mode 100644 index cd2eca94..00000000 --- a/service/limit/state_filter.go +++ /dev/null @@ -1,58 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "time" -) - -type stateFilter struct { - prev map[*roadrunner.Worker]state - next map[*roadrunner.Worker]state -} - -type state struct { - state int64 - numExecs int64 - since time.Time -} - -func newStateFilter() *stateFilter { - return &stateFilter{ - prev: make(map[*roadrunner.Worker]state), - next: make(map[*roadrunner.Worker]state), - } -} - -// add new worker to be watched -func (sw *stateFilter) push(w *roadrunner.Worker) { - sw.next[w] = state{state: w.State().Value(), numExecs: w.State().NumExecs()} -} - -// update worker states. -func (sw *stateFilter) sync(t time.Time) { - for w := range sw.prev { - if _, ok := sw.next[w]; !ok { - delete(sw.prev, w) - } - } - - for w, s := range sw.next { - ps, ok := sw.prev[w] - if !ok || ps.state != s.state || ps.numExecs != s.numExecs { - sw.prev[w] = state{state: s.state, numExecs: s.numExecs, since: t} - } - - delete(sw.next, w) - } -} - -// find all workers which spend given amount of time in a specific state. -func (sw *stateFilter) find(state int64, since time.Time) (workers []*roadrunner.Worker) { - for w, s := range sw.prev { - if s.state == state && s.since.Before(since) { - workers = append(workers, w) - } - } - - return -} diff --git a/service/metrics/config.go b/service/metrics/config.go deleted file mode 100644 index 023eff27..00000000 --- a/service/metrics/config.go +++ /dev/null @@ -1,140 +0,0 @@ -package metrics - -import ( - "fmt" - - "github.com/prometheus/client_golang/prometheus" - "github.com/spiral/roadrunner/service" -) - -// Config configures metrics service. -type Config struct { - // Address to listen - Address string - - // Collect define application specific metrics. - Collect map[string]Collector -} - -type NamedCollector struct { - // Name of the collector - Name string `json:"name"` - - // Collector structure - Collector `json:"collector"` -} - -// CollectorType represents prometheus collector types -type CollectorType string - -const ( - // Histogram type - Histogram CollectorType = "histogram" - - // Gauge type - Gauge CollectorType = "gauge" - - // Counter type - Counter CollectorType = "counter" - - // Summary type - Summary CollectorType = "summary" -) - -// Collector describes single application specific metric. -type Collector struct { - // Namespace of the metric. - Namespace string `json:"namespace"` - // Subsystem of the metric. - Subsystem string `json:"subsystem"` - // Collector type (histogram, gauge, counter, summary). - Type CollectorType `json:"type"` - // Help of collector. - Help string `json:"help"` - // Labels for vectorized metrics. - Labels []string `json:"labels"` - // Buckets for histogram metric. - Buckets []float64 `json:"buckets"` - // Objectives for the summary opts - Objectives map[float64]float64 `json:"objectives"` -} - -// Hydrate configuration. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} - -// register application specific metrics. -func (c *Config) getCollectors() (map[string]prometheus.Collector, error) { - if c.Collect == nil { - return nil, nil - } - - collectors := make(map[string]prometheus.Collector) - - for name, m := range c.Collect { - var collector prometheus.Collector - switch m.Type { - case Histogram: - opts := prometheus.HistogramOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - Buckets: m.Buckets, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewHistogramVec(opts, m.Labels) - } else { - collector = prometheus.NewHistogram(opts) - } - case Gauge: - opts := prometheus.GaugeOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewGaugeVec(opts, m.Labels) - } else { - collector = prometheus.NewGauge(opts) - } - case Counter: - opts := prometheus.CounterOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewCounterVec(opts, m.Labels) - } else { - collector = prometheus.NewCounter(opts) - } - case Summary: - opts := prometheus.SummaryOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - Objectives: m.Objectives, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewSummaryVec(opts, m.Labels) - } else { - collector = prometheus.NewSummary(opts) - } - default: - return nil, fmt.Errorf("invalid metric type `%s` for `%s`", m.Type, name) - } - - collectors[name] = collector - } - - return collectors, nil -} diff --git a/service/metrics/config_test.go b/service/metrics/config_test.go deleted file mode 100644 index 94f97da5..00000000 --- a/service/metrics/config_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package metrics - -import ( - "testing" - - json "github.com/json-iterator/go" - "github.com/prometheus/client_golang/prometheus" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"request": {"From": "Something"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Metrics(t *testing.T) { - cfg := &mockCfg{`{ -"collect":{ - "metric1":{"type": "gauge"}, - "metric2":{ "type": "counter"}, - "metric3":{"type": "summary"}, - "metric4":{"type": "histogram"} -} -}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - - m, err := c.getCollectors() - assert.NoError(t, err) - - assert.IsType(t, prometheus.NewGauge(prometheus.GaugeOpts{}), m["metric1"]) - assert.IsType(t, prometheus.NewCounter(prometheus.CounterOpts{}), m["metric2"]) - assert.IsType(t, prometheus.NewSummary(prometheus.SummaryOpts{}), m["metric3"]) - assert.IsType(t, prometheus.NewHistogram(prometheus.HistogramOpts{}), m["metric4"]) -} - -func Test_Config_MetricsVector(t *testing.T) { - cfg := &mockCfg{`{ -"collect":{ - "metric1":{"type": "gauge","labels":["label"]}, - "metric2":{ "type": "counter","labels":["label"]}, - "metric3":{"type": "summary","labels":["label"]}, - "metric4":{"type": "histogram","labels":["label"]} -} -}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - - m, err := c.getCollectors() - assert.NoError(t, err) - - assert.IsType(t, prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{}), m["metric1"]) - assert.IsType(t, prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{}), m["metric2"]) - assert.IsType(t, prometheus.NewSummaryVec(prometheus.SummaryOpts{}, []string{}), m["metric3"]) - assert.IsType(t, prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{}), m["metric4"]) -} diff --git a/service/metrics/rpc.go b/service/metrics/rpc.go deleted file mode 100644 index 0544d109..00000000 --- a/service/metrics/rpc.go +++ /dev/null @@ -1,263 +0,0 @@ -package metrics - -import ( - "fmt" - - "github.com/prometheus/client_golang/prometheus" -) - -type rpcServer struct { - svc *Service -} - -// Metric represent single metric produced by the application. -type Metric struct { - // Collector name. - Name string - - // Collector value. - Value float64 - - // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values. - Labels []string -} - -// Add new metric to the designated collector. -func (rpc *rpcServer) Add(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Add(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Add(m.Value) - - case prometheus.Counter: - c.Add(m.Value) - - case *prometheus.CounterVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Add(m.Value) - - default: - return fmt.Errorf("collector `%s` does not support method `Add`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} - -// Sub subtract the value from the specific metric (gauge only). -func (rpc *rpcServer) Sub(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Sub(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Sub(m.Value) - default: - return fmt.Errorf("collector `%s` does not support method `Sub`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} - -// Observe the value (histogram and summary only). -func (rpc *rpcServer) Observe(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case *prometheus.SummaryVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Observe(m.Value) - - case prometheus.Histogram: - c.Observe(m.Value) - - case *prometheus.HistogramVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Observe(m.Value) - default: - return fmt.Errorf("collector `%s` does not support method `Observe`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} - -// Declare is used to register new collector in prometheus -// THE TYPES ARE: -// NamedCollector -> Collector with the name -// bool -> RPC reply value -// RETURNS: -// error -func (rpc *rpcServer) Declare(c *NamedCollector, ok *bool) (err error) { - // MustRegister could panic, so, to return error and not shutdown whole app - // we recover and return error - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - if rpc.svc.Collector(c.Name) != nil { - *ok = false - // alternative is to return error - // fmt.Errorf("tried to register existing collector with the name `%s`", c.Name) - return nil - } - - var collector prometheus.Collector - switch c.Type { - case Histogram: - opts := prometheus.HistogramOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - Buckets: c.Buckets, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewHistogramVec(opts, c.Labels) - } else { - collector = prometheus.NewHistogram(opts) - } - case Gauge: - opts := prometheus.GaugeOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewGaugeVec(opts, c.Labels) - } else { - collector = prometheus.NewGauge(opts) - } - case Counter: - opts := prometheus.CounterOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewCounterVec(opts, c.Labels) - } else { - collector = prometheus.NewCounter(opts) - } - case Summary: - opts := prometheus.SummaryOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - Objectives: c.Objectives, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewSummaryVec(opts, c.Labels) - } else { - collector = prometheus.NewSummary(opts) - } - - default: - return fmt.Errorf("unknown collector type `%s`", c.Type) - - } - - // add collector to sync.Map - rpc.svc.collectors.Store(c.Name, collector) - // that method might panic, we handle it by recover - rpc.svc.MustRegister(collector) - - *ok = true - return nil -} - -// Set the metric value (only for gaude). -func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Set(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Set(m.Value) - - default: - return fmt.Errorf("collector `%s` does not support method `Set`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} diff --git a/service/metrics/rpc_test.go b/service/metrics/rpc_test.go deleted file mode 100644 index 2fc4bc32..00000000 --- a/service/metrics/rpc_test.go +++ /dev/null @@ -1,861 +0,0 @@ -package metrics - -import ( - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - rpc2 "net/rpc" - "strconv" - "testing" - "time" -) - -var port = 5004 - -func setup(t *testing.T, metric string, portNum string) (*rpc2.Client, service.Container) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:` + strconv.Itoa(port) + `"}`, - metricsCfg: `{ - "address": "localhost:` + portNum + `", - "collect":{ - ` + metric + ` - } - }`})) - - // rotate ports for travis - port++ - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - assert.True(t, s.(*Service).Enabled()) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 200) - - client, err := rs.Client() - assert.NoError(t, err) - if err != nil { - panic(err) - } - - return client, c -} - -func Test_Set_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge" - }`, - "2112", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2112/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 100`) -} - -func Test_Set_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2113", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2113/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 100`) -} - -func Test_Set_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2114", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Set_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2115", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Set_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2116", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -func Test_Set_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2117", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -// sub - -func Test_Sub_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge" - }`, - "2118", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 10.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2118/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 90`) -} - -func Test_Sub_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2119", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 10.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2119/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 90`) -} - -func Test_Register_RPC_Histogram(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2319", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_histogram", - Collector: Collector{ - Namespace: "test_histogram", - Subsystem: "test_histogram", - Type: Histogram, - Help: "test_histogram", - Labels: nil, - Buckets: []float64{0.1, 0.2, 0.5}, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // histogram does not support Add, should be an error - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "custom_histogram", - }, &ok2)) - // ok should became false - assert.False(t, ok2) - - out, _, err := get("http://localhost:2319/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `TYPE test_histogram_test_histogram_custom_histogram histogram`) - - // check buckets - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.1"} 0`) - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.2"} 0`) - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.5"} 0`) -} - -func Test_Register_RPC_Gauge(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2324", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_gauge", - Collector: Collector{ - Namespace: "test_gauge", - Subsystem: "test_gauge", - Type: Gauge, - Help: "test_gauge", - Labels: []string{"type", "section"}, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_gauge - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "custom_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok2)) - // ok should became true - assert.True(t, ok2) - - // Subtract from custom runtime metric - var ok3 bool - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "custom_gauge", - Value: 10.0, - Labels: []string{"core", "first"}, - }, &ok3)) - assert.True(t, ok3) - - out, _, err := get("http://localhost:2324/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_gauge_test_gauge_custom_gauge{section="first",type="core"} 90`) -} - -func Test_Register_RPC_Counter(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2328", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_counter", - Collector: Collector{ - Namespace: "test_counter", - Subsystem: "test_counter", - Type: Counter, - Help: "test_counter", - Labels: []string{"type", "section"}, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_counter - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "custom_counter", - Value: 100.0, - Labels: []string{"type2", "section2"}, - }, &ok2)) - // ok should became true - assert.True(t, ok2) - - out, _, err := get("http://localhost:2328/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_counter_test_counter_custom_counter{section="section2",type="type2"} 100`) -} - -func Test_Register_RPC_Summary(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "6666", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_summary", - Collector: Collector{ - Namespace: "test_summary", - Subsystem: "test_summary", - Type: Summary, - Help: "test_summary", - Labels: nil, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_summary is not supported - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "custom_summary", - Value: 100.0, - Labels: []string{"type22", "section22"}, - }, &ok2)) - // ok should became false - assert.False(t, ok2) - - out, _, err := get("http://localhost:6666/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_summary_test_summary_custom_summary_sum 0`) - assert.Contains(t, out, `test_summary_test_summary_custom_summary_count 0`) -} - -func Test_Sub_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2120", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Sub_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2121", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Sub_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2122", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -func Test_Sub_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2123", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -// -- observe - -func Test_Observe_RPC(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram" - }`, - "2124", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2124/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2125", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2125/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2126", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2127", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2128", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -// -- observe summary - -func Test_Observe2_RPC(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary" - }`, - "2129", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2129/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe2_RPC_Invalid(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary" - }`, - "2130", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_Invalid_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "gauge" - }`, - "2131", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -func Test_Observe2_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2132", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2132/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe2_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2133", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2134", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2135", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -// add -func Test_Add_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter" - }`, - "2136", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2136/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 100`) -} - -func Test_Add_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2137", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2137/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 100`) -} - -func Test_Add_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2138", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2139", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2140", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2141", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} diff --git a/service/metrics/service.go b/service/metrics/service.go deleted file mode 100644 index 4656ae04..00000000 --- a/service/metrics/service.go +++ /dev/null @@ -1,191 +0,0 @@ -package metrics - -// todo: declare metric at runtime - -import ( - "context" - "crypto/tls" - "fmt" - "net/http" - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner/service/rpc" - "golang.org/x/sys/cpu" -) - -const ( - // ID declares public service name. - ID = "metrics" - // maxHeaderSize declares max header size for prometheus server - maxHeaderSize = 1024 * 1024 * 100 // 104MB -) - -// Service to manage application metrics using Prometheus. -type Service struct { - cfg *Config - log *logrus.Logger - mu sync.Mutex - http *http.Server - collectors sync.Map - registry *prometheus.Registry -} - -// Init service. -func (s *Service) Init(cfg *Config, r *rpc.Service, log *logrus.Logger) (bool, error) { - s.cfg = cfg - s.log = log - s.registry = prometheus.NewRegistry() - - s.registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) - s.registry.MustRegister(prometheus.NewGoCollector()) - - if r != nil { - if err := r.Register(ID, &rpcServer{s}); err != nil { - return false, err - } - } - - return true, nil -} - -// Enabled indicates that server is able to collect metrics. -func (s *Service) Enabled() bool { - return s.cfg != nil -} - -// Register new prometheus collector. -func (s *Service) Register(c prometheus.Collector) error { - return s.registry.Register(c) -} - -// MustRegister registers new collector or fails with panic. -func (s *Service) MustRegister(c prometheus.Collector) { - s.registry.MustRegister(c) -} - -// Serve prometheus metrics service. -func (s *Service) Serve() error { - // register application specific metrics - collectors, err := s.cfg.getCollectors() - if err != nil { - return err - } - - for name, collector := range collectors { - if err := s.registry.Register(collector); err != nil { - return err - } - - s.collectors.Store(name, collector) - } - - s.mu.Lock() - - var topCipherSuites []uint16 - var defaultCipherSuitesTLS13 []uint16 - - hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ - hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL - // Keep in sync with crypto/aes/cipher_s390x.go. - hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) - - hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X - - if hasGCMAsm { - // If AES-GCM hardware is provided then prioritise AES-GCM - // cipher suites. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } else { - // Without AES-GCM hardware, we put the ChaCha20-Poly1305 - // cipher suites first. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } - - DefaultCipherSuites := make([]uint16, 0, 22) - DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) - DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) - - s.http = &http.Server{ - Addr: s.cfg.Address, - Handler: promhttp.HandlerFor(s.registry, promhttp.HandlerOpts{}), - IdleTimeout: time.Hour * 24, - ReadTimeout: time.Minute * 60, - MaxHeaderBytes: maxHeaderSize, - ReadHeaderTimeout: time.Minute * 60, - WriteTimeout: time.Minute * 60, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - s.mu.Unlock() - - err = s.http.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - return err - } - - return nil -} - -// Stop prometheus metrics service. -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.http != nil { - // gracefully stop server - go func() { - err := s.http.Shutdown(context.Background()) - if err != nil { - // Function should be Stop() error - s.log.Error(fmt.Errorf("error shutting down the metrics server: error %v", err)) - } - }() - } -} - -// Collector returns application specific collector by name or nil if collector not found. -func (s *Service) Collector(name string) prometheus.Collector { - collector, ok := s.collectors.Load(name) - if !ok { - return nil - } - - return collector.(prometheus.Collector) -} diff --git a/service/metrics/service_test.go b/service/metrics/service_test.go deleted file mode 100644 index cdb81147..00000000 --- a/service/metrics/service_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package metrics - -import ( - json "github.com/json-iterator/go" - "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -type testCfg struct { - rpcCfg string - metricsCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - return &testCfg{target: cfg.metricsCfg} - } - - if name == rpc.ID { - return &testCfg{target: cfg.rpcCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - return err -} - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -func TestService_Serve(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2116" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - out, _, err := get("http://localhost:2116/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "go_gc_duration_seconds") -} - -func Test_ServiceCustomMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2115" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - collector := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "my_gauge", - Help: "My gauge value", - }) - - assert.NoError(t, s.(*Service).Register(collector)) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - collector.Set(100) - - out, _, err := get("http://localhost:2115/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "my_gauge 100") -} - -func Test_ServiceCustomMetricMust(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2114" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - collector := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "my_gauge_2", - Help: "My gauge value", - }) - - s.(*Service).MustRegister(collector) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - collector.Set(100) - - out, _, err := get("http://localhost:2114/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "my_gauge_2 100") -} - -func Test_ConfiguredMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2113", - "collect":{ - "user_gauge":{ - "type": "gauge" - } - } - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - assert.True(t, s.(*Service).Enabled()) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - s.(*Service).Collector("user_gauge").(prometheus.Gauge).Set(100) - - assert.Nil(t, s.(*Service).Collector("invalid")) - - out, _, err := get("http://localhost:2113/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "user_gauge 100") -} - -func Test_ConfiguredDuplicateMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2112", - "collect":{ - "go_gc_duration_seconds":{ - "type": "gauge" - } - } - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - assert.True(t, s.(*Service).Enabled()) - - assert.Error(t, c.Serve()) -} - -func Test_ConfiguredInvalidMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2112", - "collect":{ - "user_gauge":{ - "type": "invalid" - } - } - - }`})) - - assert.Error(t, c.Serve()) -} diff --git a/service/reload/config.go b/service/reload/config.go deleted file mode 100644 index efc71972..00000000 --- a/service/reload/config.go +++ /dev/null @@ -1,72 +0,0 @@ -package reload - -import ( - "errors" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "time" -) - -// Config is a Reload configuration point. -type Config struct { - // Interval is a global refresh interval - Interval time.Duration - - // Patterns is a global file patterns to watch. It will be applied to every directory in project - Patterns []string - - // Services is set of services which would be reloaded in case of FS changes - Services map[string]ServiceConfig -} - -type ServiceConfig struct { - // Enabled indicates that service must be watched, doest not required when any other option specified - Enabled bool - - // Recursive is options to use nested files from root folder - Recursive bool - - // Patterns is per-service specific files to watch - Patterns []string - - // Dirs is per-service specific dirs which will be combined with Patterns - Dirs []string - - // Ignore is set of files which would not be watched - Ignore []string - - // service is a link to service to restart - service *roadrunner.Controllable -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return nil -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Interval = time.Second - c.Patterns = []string{".php"} - - return nil -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - if c.Interval < time.Second { - return errors.New("too short interval") - } - - if c.Services == nil { - return errors.New("should add at least 1 service") - } else if len(c.Services) == 0 { - return errors.New("service initialized, however, no config added") - } - - return nil -} diff --git a/service/reload/config_test.go b/service/reload/config_test.go deleted file mode 100644 index 600975d3..00000000 --- a/service/reload/config_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package reload - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func Test_Config_Valid(t *testing.T) { - services := make(map[string]ServiceConfig) - services["test"] = ServiceConfig{ - Recursive: false, - Patterns: nil, - Dirs: nil, - Ignore: nil, - service: nil, - } - - cfg := &Config{ - Interval: time.Second, - Patterns: nil, - Services: services, - } - assert.NoError(t, cfg.Valid()) -} - -func Test_Fake_ServiceConfig(t *testing.T) { - services := make(map[string]ServiceConfig) - cfg := &Config{ - Interval: time.Microsecond, - Patterns: nil, - Services: services, - } - assert.Error(t, cfg.Valid()) -} - -func Test_Interval(t *testing.T) { - services := make(map[string]ServiceConfig) - services["test"] = ServiceConfig{ - Enabled: false, - Recursive: false, - Patterns: nil, - Dirs: nil, - Ignore: nil, - service: nil, - } - - cfg := &Config{ - Interval: time.Millisecond, // should crash here - Patterns: nil, - Services: services, - } - assert.Error(t, cfg.Valid()) -} - -func Test_NoServiceConfig(t *testing.T) { - cfg := &Config{ - Interval: time.Second, - Patterns: nil, - Services: nil, - } - assert.Error(t, cfg.Valid()) -} diff --git a/service/reload/service.go b/service/reload/service.go deleted file mode 100644 index c065d95d..00000000 --- a/service/reload/service.go +++ /dev/null @@ -1,163 +0,0 @@ -package reload - -import ( - "errors" - "os" - "strings" - "time" - - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" -) - -// ID contains default service name. -const ID = "reload" - -type Service struct { - cfg *Config - log *logrus.Logger - watcher *Watcher - stopc chan struct{} -} - -// Init controller service -func (s *Service) Init(cfg *Config, log *logrus.Logger, c service.Container) (bool, error) { - if cfg == nil || len(cfg.Services) == 0 { - return false, nil - } - - s.cfg = cfg - s.log = log - s.stopc = make(chan struct{}) - - var configs []WatcherConfig - - // mount Services to designated services - for serviceName := range cfg.Services { - svc, _ := c.Get(serviceName) - if ctrl, ok := svc.(roadrunner.Controllable); ok { - tmp := cfg.Services[serviceName] - tmp.service = &ctrl - cfg.Services[serviceName] = tmp - } - } - - for serviceName, config := range s.cfg.Services { - if cfg.Services[serviceName].service == nil { - continue - } - ignored, err := ConvertIgnored(config.Ignore) - if err != nil { - return false, err - } - configs = append(configs, WatcherConfig{ - ServiceName: serviceName, - Recursive: config.Recursive, - Directories: config.Dirs, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: ignored, - FilePatterns: append(config.Patterns, cfg.Patterns...), - }) - } - - var err error - s.watcher, err = NewWatcher(configs) - if err != nil { - return false, err - } - - return true, nil -} - -func (s *Service) Serve() error { - if s.cfg.Interval < time.Second { - return errors.New("reload interval is too fast") - } - - // make a map with unique services - // so, if we would have a 100 events from http service - // in map we would see only 1 key and it's config - treshholdc := make(chan struct { - serviceConfig ServiceConfig - service string - }, 100) - - // use the same interval - timer := time.NewTimer(s.cfg.Interval) - - // drain channel in case of leaved messages - defer func() { - go func() { - for range treshholdc { - - } - }() - }() - - go func() { - for e := range s.watcher.Event { - treshholdc <- struct { - serviceConfig ServiceConfig - service string - }{serviceConfig: s.cfg.Services[e.service], service: e.service} - } - }() - - // map with configs by services - updated := make(map[string]ServiceConfig, 100) - - go func() { - for { - select { - case config := <-treshholdc: - // replace previous value in map by more recent without adding new one - updated[config.service] = config.serviceConfig - // stop timer - timer.Stop() - // restart - // logic is following: - // if we getting a lot of events, we should't restart particular service on each of it (user doing bug move or very fast typing) - // instead, we are resetting the timer and wait for Interval time - // If there is no more events, we restart service only once - timer.Reset(s.cfg.Interval) - case <-timer.C: - if len(updated) > 0 { - for k, v := range updated { - sv := *v.service - err := sv.Server().Reset() - if err != nil { - s.log.Error(err) - } - s.log.Debugf("[%s] found %v file(s) changes, reloading", k, len(updated)) - } - // zero map - updated = make(map[string]ServiceConfig, 100) - } - case <-s.stopc: - timer.Stop() - return - } - } - }() - - err := s.watcher.StartPolling(s.cfg.Interval) - if err != nil { - return err - } - - return nil -} - -func (s *Service) Stop() { - s.watcher.Stop() - s.stopc <- struct{}{} -} diff --git a/service/reload/watcher.go b/service/reload/watcher.go deleted file mode 100644 index 1397dfa5..00000000 --- a/service/reload/watcher.go +++ /dev/null @@ -1,372 +0,0 @@ -package reload - -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "sync" - "time" -) - -var ErrorSkip = errors.New("file is skipped") -var NoWalkerConfig = errors.New("should add at least one walker config, when reload is set to true") - -// SimpleHook is used to filter by simple criteria, CONTAINS -type SimpleHook func(filename string, pattern []string) error - -// An Event describes an event that is received when files or directory -// changes occur. It includes the os.FileInfo of the changed file or -// directory and the type of event that's occurred and the full path of the file. -type Event struct { - Path string - Info os.FileInfo - - service string // type of service, http, grpc, etc... -} - -type WatcherConfig struct { - // service name - ServiceName string - - // Recursive or just add by singe directory - Recursive bool - - // Directories used per-service - Directories []string - - // simple hook, just CONTAINS - FilterHooks func(filename string, pattern []string) error - - // path to file with Files - Files map[string]os.FileInfo - - // Ignored Directories, used map for O(1) amortized get - Ignored map[string]struct{} - - // FilePatterns to ignore - FilePatterns []string -} - -type Watcher struct { - // main event channel - Event chan Event - close chan struct{} - - // ============================= - mu *sync.Mutex - - // indicates is walker started or not - started bool - - // config for each service - // need pointer here to assign files - watcherConfigs map[string]WatcherConfig -} - -// Options is used to set Watcher Options -type Options func(*Watcher) - -// NewWatcher returns new instance of File Watcher -func NewWatcher(configs []WatcherConfig, options ...Options) (*Watcher, error) { - w := &Watcher{ - Event: make(chan Event), - mu: &sync.Mutex{}, - - close: make(chan struct{}, 1), - - //workingDir: workDir, - watcherConfigs: make(map[string]WatcherConfig), - } - - // add watcherConfigs by service names - for _, v := range configs { - w.watcherConfigs[v.ServiceName] = v - } - - // apply options - for _, option := range options { - option(w) - } - err := w.initFs() - if err != nil { - return nil, err - } - - return w, nil -} - -// initFs makes initial map with files -func (w *Watcher) initFs() error { - for srvName, config := range w.watcherConfigs { - fileList, err := w.retrieveFileList(srvName, config) - if err != nil { - return err - } - // workaround. in golang you can't assign to map in struct field - tmp := w.watcherConfigs[srvName] - tmp.Files = fileList - w.watcherConfigs[srvName] = tmp - } - return nil -} - -// ConvertIgnored is used to convert slice to map with ignored files -func ConvertIgnored(ignored []string) (map[string]struct{}, error) { - if len(ignored) == 0 { - return nil, nil - } - - ign := make(map[string]struct{}, len(ignored)) - for i := 0; i < len(ignored); i++ { - abs, err := filepath.Abs(ignored[i]) - if err != nil { - return nil, err - } - ign[abs] = struct{}{} - } - - return ign, nil -} - -// GetAllFiles returns all files initialized for particular company -func (w *Watcher) GetAllFiles(serviceName string) []os.FileInfo { - var ret []os.FileInfo - - for _, v := range w.watcherConfigs[serviceName].Files { - ret = append(ret, v) - } - - return ret -} - -// https://en.wikipedia.org/wiki/Inotify -// SetMaxFileEvents sets max file notify events for Watcher -// In case of file watch errors, this value can be increased system-wide -// For linux: set --> fs.inotify.max_user_watches = 600000 (under /etc/<choose_name_here>.conf) -// Add apply: sudo sysctl -p --system -// func SetMaxFileEvents(events int) Options { -// return func(watcher *Watcher) { -// watcher.maxFileWatchEvents = events -// } -// -// } - -// pass map from outside -func (w *Watcher) retrieveFilesSingle(serviceName, path string) (map[string]os.FileInfo, error) { - stat, err := os.Stat(path) - if err != nil { - return nil, err - } - - filesList := make(map[string]os.FileInfo, 10) - filesList[path] = stat - - // if it's not a dir, return - if !stat.IsDir() { - return filesList, nil - } - - fileInfoList, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - - // recursive calls are slow in compare to goto - // so, we will add files with goto pattern -outer: - for i := 0; i < len(fileInfoList); i++ { - // if file in ignored --> continue - if _, ignored := w.watcherConfigs[serviceName].Ignored[path]; ignored { - continue - } - - // if filename does not contain pattern --> ignore that file - if w.watcherConfigs[serviceName].FilePatterns != nil && w.watcherConfigs[serviceName].FilterHooks != nil { - err = w.watcherConfigs[serviceName].FilterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].FilePatterns) - if err == ErrorSkip { - continue outer - } - } - - filesList[fileInfoList[i].Name()] = fileInfoList[i] - } - - return filesList, nil -} - -func (w *Watcher) StartPolling(duration time.Duration) error { - w.mu.Lock() - if w.started { - w.mu.Unlock() - return errors.New("already started") - } - - w.started = true - w.mu.Unlock() - - return w.waitEvent(duration) -} - -// this is blocking operation -func (w *Watcher) waitEvent(d time.Duration) error { - ticker := time.NewTicker(d) - for { - select { - case <-w.close: - ticker.Stop() - // just exit - // no matter for the pollEvents - return nil - case <-ticker.C: - // this is not very effective way - // because we have to wait on Lock - // better is to listen files in parallel, but, since that would be used in debug... TODO - for serviceName := range w.watcherConfigs { - // TODO sync approach - fileList, _ := w.retrieveFileList(serviceName, w.watcherConfigs[serviceName]) - w.pollEvents(w.watcherConfigs[serviceName].ServiceName, fileList) - } - } - } -} - -// retrieveFileList get file list for service -func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) { - fileList := make(map[string]os.FileInfo) - if config.Recursive { - // walk through directories recursively - for i := 0; i < len(config.Directories); i++ { - // full path is workdir/relative_path - fullPath, err := filepath.Abs(config.Directories[i]) - if err != nil { - return nil, err - } - list, err := w.retrieveFilesRecursive(serviceName, fullPath) - if err != nil { - return nil, err - } - - for k := range list { - fileList[k] = list[k] - } - } - return fileList, nil - } - - for i := 0; i < len(config.Directories); i++ { - // full path is workdir/relative_path - fullPath, err := filepath.Abs(config.Directories[i]) - if err != nil { - return nil, err - } - - // list is pathToFiles with files - list, err := w.retrieveFilesSingle(serviceName, fullPath) - if err != nil { - return nil, err - } - - for pathToFile, file := range list { - fileList[pathToFile] = file - } - } - - return fileList, nil -} - -func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) { - fileList := make(map[string]os.FileInfo) - - return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // If path is ignored and it's a directory, skip the directory. If it's - // ignored and it's a single file, skip the file. - _, ignored := w.watcherConfigs[serviceName].Ignored[path] - if ignored { - if info.IsDir() { - // if it's dir, ignore whole - return filepath.SkipDir - } - return nil - } - - // if filename does not contain pattern --> ignore that file - err = w.watcherConfigs[serviceName].FilterHooks(info.Name(), w.watcherConfigs[serviceName].FilePatterns) - if err == ErrorSkip { - return nil - } - - // Add the path and it's info to the file list. - fileList[path] = info - return nil - }) -} - -func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) { - w.mu.Lock() - defer w.mu.Unlock() - - // Store create and remove events for use to check for rename events. - creates := make(map[string]os.FileInfo) - removes := make(map[string]os.FileInfo) - - // Check for removed files. - for pth := range w.watcherConfigs[serviceName].Files { - if _, found := files[pth]; !found { - removes[pth] = w.watcherConfigs[serviceName].Files[pth] - } - } - - // Check for created files, writes and chmods. - for pth := range files { - if files[pth].IsDir() { - continue - } - oldInfo, found := w.watcherConfigs[serviceName].Files[pth] - if !found { - // A file was created. - creates[pth] = files[pth] - continue - } - - if oldInfo.ModTime() != files[pth].ModTime() || oldInfo.Mode() != files[pth].Mode() { - w.watcherConfigs[serviceName].Files[pth] = files[pth] - w.Event <- Event{ - Path: pth, - Info: files[pth], - service: serviceName, - } - } - } - - // Send all the remaining create and remove events. - for pth := range creates { - // add file to the plugin watch files - w.watcherConfigs[serviceName].Files[pth] = creates[pth] - - w.Event <- Event{ - Path: pth, - Info: creates[pth], - service: serviceName, - } - } - - for pth := range removes { - // delete path from the config - delete(w.watcherConfigs[serviceName].Files, pth) - - w.Event <- Event{ - Path: pth, - Info: removes[pth], - service: serviceName, - } - } -} - -func (w *Watcher) Stop() { - w.close <- struct{}{} -} diff --git a/service/reload/watcher_test.go b/service/reload/watcher_test.go deleted file mode 100644 index 2d8ad480..00000000 --- a/service/reload/watcher_test.go +++ /dev/null @@ -1,578 +0,0 @@ -package reload - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var testServiceName = "test" - -// scenario -// Create walker instance, init with default config, check that Watcher found all files from config -func Test_Correct_Watcher_Init(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - if err != nil { - t.Fatal(err) - } - }() - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: nil, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: nil, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - if len(w.GetAllFiles(testServiceName)) != 2 { - t.Fatal("incorrect directories len") - } -} - -// scenario -// create 3 files, create walker instance -// Start poll events -// change file and see, if event had come to handler -func Test_Get_FileEvent(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func(name string) { - err = freeResources(name) - assert.NoError(t, err) - }(tempDir) - assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.txt"), - []byte{}, 0755) - assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{}, 0755) - assert.NoError(t, err) - - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: nil, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - assert.NoError(t, err) - - // should be 3 files and directory - if len(w.GetAllFiles(testServiceName)) != 4 { - t.Fatal("incorrect directories len") - } - - go func() { - stop := make(chan struct{}, 1) - go func() { - time.Sleep(time.Second * 2) - err := ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), - []byte{1, 1, 1}, 0755) - assert.NoError(t, err) - time.Sleep(time.Second) - stop <- struct{}{} - }() - - go func() { - for { - select { - case e := <-w.Event: - if e.Path != "file2.txt" { - assert.Fail(t, "didn't handle event when write file2") - } - w.Stop() - case <-stop: - return - } - } - - }() - }() - - err = w.StartPolling(time.Second) - assert.NoError(t, err) -} - -// scenario -// create 3 files with different extensions, create walker instance -// Start poll events -// change file with txt extension, and see, if event had not come to handler because it was filtered -func Test_FileExtensionFilter(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{}, 0755) - assert.NoError(t, err) - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: false, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - assert.NoError(t, err) - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (one filtered) and directory - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - stop := make(chan struct{}, 1) - - go func() { - time.Sleep(time.Second) - err := ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{1, 1, 1}, 0755) - assert.NoError(t, err) - stop <- struct{}{} - }() - - go func() { - time.Sleep(time.Second) - select { - case <-w.Event: - assert.Fail(t, "handled event from filtered file") - case <-stop: - return - } - }() - time.Sleep(time.Second) - w.Stop() - }() - - err = w.StartPolling(time.Second) - assert.NoError(t, err) - err = freeResources(tempDir) - assert.NoError(t, err) -} - -// nested -// scenario -// create dir and nested dir -// make files with aaa, bbb and txt extensions, filter txt -// change not filtered file, handle event -func Test_Recursive_Support(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - assert.NoError(t, err) - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - assert.NoError(t, err) - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 3 files (2 from root dir, and 1 from nested), filtered txt - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - stop := make(chan struct{}, 1) - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - // change file in nested directory - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{1, 1, 1}, 0755) - assert.NoError(t, err) - - go func() { - time.Sleep(time.Second) - for { - select { - case e := <-w.Event: - if e.Info.Name() != "file4.aaa" { - assert.Fail(t, "wrong handled event from watcher in nested dir") - } - case <-stop: - w.Stop() - return - } - } - }() - - time.Sleep(time.Second) - stop <- struct{}{} - }() - - err = w.StartPolling(time.Second) - assert.NoError(t, err) -} - -func Test_Wrong_Dir(t *testing.T) { - // no such file or directory - wrongDir := "askdjfhaksdlfksdf" - - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{wrongDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: nil, - FilePatterns: []string{"aaa", "bbb"}, - } - - _, err := NewWatcher([]WatcherConfig{wc}) - assert.Error(t, err) -} - -func Test_Filter_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func(name string) { - err = freeResources(name) - assert.NoError(t, err) - }(tempDir) - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - ignored, err := ConvertIgnored([]string{nestedDir}) - if err != nil { - t.Fatal(err) - } - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: ignored, - FilePatterns: []string{"aaa", "bbb", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - stop := make(chan struct{}, 1) - go func() { - time.Sleep(time.Second) - err := ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{1, 1, 1}, 0755) - assert.NoError(t, err) - }() - - go func() { - select { - case e := <-w.Event: - fmt.Println("file: " + e.Info.Name()) - assert.Fail(t, "handled event from watcher in nested dir") - case <-stop: - w.Stop() - return - } - }() - - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - stop <- struct{}{} - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// copy files from nested dir to not ignored -// should fire an event -func Test_Copy_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - - defer func() { - err = freeResources(tempDir) - assert.NoError(t, err) - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - assert.NoError(t, err) - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - assert.NoError(t, err) - - ignored, err := ConvertIgnored([]string{nestedDir}) - assert.NoError(t, err) - - wc := WatcherConfig{ - ServiceName: testServiceName, - Recursive: true, - Directories: []string{tempDir}, - FilterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - Files: make(map[string]os.FileInfo), - Ignored: ignored, - FilePatterns: []string{"aaa", "bbb", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - assert.NoError(t, err) - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - go func() { - time.Sleep(time.Second * 2) - err := copyDir(nestedDir, filepath.Join(tempDir, "copyTo")) - assert.NoError(t, err) - }() - - go func() { - for range w.Event { - // here should be event, otherwise we won't stop - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - assert.NoError(t, err) -} - -func copyFile(src, dst string) (err error) { - in, err := os.Open(src) - if err != nil { - return - } - defer func() { - _ = in.Close() - }() - - out, err := os.Create(dst) - if err != nil { - return - } - defer func() { - if e := out.Close(); e != nil { - err = e - } - }() - - _, err = io.Copy(out, in) - if err != nil { - return - } - - err = out.Sync() - if err != nil { - return - } - - si, err := os.Stat(src) - if err != nil { - return - } - err = os.Chmod(dst, si.Mode()) - if err != nil { - return - } - - return -} - -func copyDir(src string, dst string) (err error) { - src = filepath.Clean(src) - dst = filepath.Clean(dst) - - si, err := os.Stat(src) - if err != nil { - return err - } - if !si.IsDir() { - return fmt.Errorf("source is not a directory") - } - - _, err = os.Stat(dst) - if err != nil && !os.IsNotExist(err) { - return - } - if err == nil { - return fmt.Errorf("destination already exists") - } - - err = os.MkdirAll(dst, si.Mode()) - if err != nil { - return - } - - entries, err := ioutil.ReadDir(src) - if err != nil { - return - } - - for _, entry := range entries { - srcPath := filepath.Join(src, entry.Name()) - dstPath := filepath.Join(dst, entry.Name()) - - if entry.IsDir() { - err = copyDir(srcPath, dstPath) - if err != nil { - return - } - } else { - // Skip symlinks. - if entry.Mode()&os.ModeSymlink != 0 { - continue - } - - err = copyFile(srcPath, dstPath) - if err != nil { - return - } - } - } - - return -} - -func freeResources(path string) error { - return os.RemoveAll(path) -} diff --git a/service/rpc/config.go b/service/rpc/config.go deleted file mode 100644 index a4cf0f91..00000000 --- a/service/rpc/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package rpc - -import ( - "errors" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/util" - "net" - "strings" -) - -// Config defines RPC service config. -type Config struct { - // Indicates if RPC connection is enabled. - Enable bool - - // Listen string - Listen string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return c.Valid() -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *Config) InitDefaults() error { - c.Enable = true - c.Listen = "tcp://127.0.0.1:6001" - - return nil -} - -// Valid returns nil if config is valid. -func (c *Config) Valid() error { - if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 { - return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") - } - - return nil -} - -// Listener creates new rpc socket Listener. -func (c *Config) Listener() (net.Listener, error) { - return util.CreateListener(c.Listen) -} - -// Dialer creates rpc socket Dialer. -func (c *Config) Dialer() (net.Conn, error) { - dsn := strings.Split(c.Listen, "://") - if len(dsn) != 2 { - return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") - } - - return net.Dial(dsn[0], dsn[1]) -} diff --git a/service/rpc/config_test.go b/service/rpc/config_test.go deleted file mode 100644 index 1ecd71b3..00000000 --- a/service/rpc/config_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package rpc - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type testCfg struct{ cfg string } - -func (cfg *testCfg) Get(name string) service.Config { return nil } -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "tcp://:18001"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "invalid"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "invalid"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func TestConfig_Listener(t *testing.T) { - cfg := &Config{Listen: "tcp://:18001"} - - ln, err := cfg.Listener() - assert.NoError(t, err) - assert.NotNil(t, ln) - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - assert.Equal(t, "tcp", ln.Addr().Network()) - assert.Equal(t, "0.0.0.0:18001", ln.Addr().String()) -} - -func TestConfig_ListenerUnix(t *testing.T) { - cfg := &Config{Listen: "unix://file.sock"} - - ln, err := cfg.Listener() - assert.NoError(t, err) - assert.NotNil(t, ln) - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - assert.Equal(t, "unix", ln.Addr().Network()) - assert.Equal(t, "file.sock", ln.Addr().String()) -} - -func Test_Config_Error(t *testing.T) { - cfg := &Config{Listen: "uni:unix.sock"} - ln, err := cfg.Listener() - assert.Nil(t, ln) - assert.Error(t, err) - assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) -} - -func Test_Config_ErrorMethod(t *testing.T) { - cfg := &Config{Listen: "xinu://unix.sock"} - - ln, err := cfg.Listener() - assert.Nil(t, ln) - assert.Error(t, err) -} - -func TestConfig_Dialer(t *testing.T) { - cfg := &Config{Listen: "tcp://:18001"} - - ln, _ := cfg.Listener() - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - conn, err := cfg.Dialer() - assert.NoError(t, err) - assert.NotNil(t, conn) - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("error closing the connection: error %v", err) - } - }() - - assert.Equal(t, "tcp", conn.RemoteAddr().Network()) - assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String()) -} - -func TestConfig_DialerUnix(t *testing.T) { - cfg := &Config{Listen: "unix://file.sock"} - - ln, _ := cfg.Listener() - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - conn, err := cfg.Dialer() - assert.NoError(t, err) - assert.NotNil(t, conn) - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("error closing the connection: error %v", err) - } - }() - - assert.Equal(t, "unix", conn.RemoteAddr().Network()) - assert.Equal(t, "file.sock", conn.RemoteAddr().String()) -} - -func Test_Config_DialerError(t *testing.T) { - cfg := &Config{Listen: "uni:unix.sock"} - ln, err := cfg.Dialer() - assert.Nil(t, ln) - assert.Error(t, err) - assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) -} - -func Test_Config_DialerErrorMethod(t *testing.T) { - cfg := &Config{Listen: "xinu://unix.sock"} - - ln, err := cfg.Dialer() - assert.Nil(t, ln) - assert.Error(t, err) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - assert.Equal(t, true, c.Enable) - assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) -} diff --git a/service/rpc/service.go b/service/rpc/service.go deleted file mode 100644 index 7a649f1b..00000000 --- a/service/rpc/service.go +++ /dev/null @@ -1,124 +0,0 @@ -package rpc - -import ( - "errors" - "github.com/spiral/goridge/v2" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "net/rpc" - "sync" -) - -// ID contains default service name. -const ID = "rpc" - -// Service is RPC service. -type Service struct { - cfg *Config - stop chan interface{} - rpc *rpc.Server - mu sync.Mutex - serving bool -} - -// Init rpc service. Must return true if service is enabled. -func (s *Service) Init(cfg *Config, c service.Container, env env.Environment) (bool, error) { - if !cfg.Enable { - return false, nil - } - - s.cfg = cfg - s.rpc = rpc.NewServer() - - if env != nil { - env.SetEnv("RR_RPC", cfg.Listen) - } - - if err := s.Register("system", &systemService{c}); err != nil { - return false, err - } - - return true, nil -} - -// Serve serves the service. -func (s *Service) Serve() error { - if s.rpc == nil { - return errors.New("RPC service is not configured") - } - - s.mu.Lock() - s.serving = true - s.stop = make(chan interface{}) - s.mu.Unlock() - - ln, err := s.cfg.Listener() - if err != nil { - return err - } - defer ln.Close() - - go func() { - for { - select { - case <-s.stop: - return - default: - conn, err := ln.Accept() - if err != nil { - continue - } - - go s.rpc.ServeCodec(goridge.NewCodec(conn)) - } - } - }() - - <-s.stop - - s.mu.Lock() - s.serving = false - s.mu.Unlock() - - return nil -} - -// Stop stops the service. -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.serving { - close(s.stop) - } -} - -// Register publishes in the server the set of methods of the -// receiver value that satisfy the following conditions: -// - exported method of exported type -// - two arguments, both of exported type -// - the second argument is a pointer -// - one return value, of type error -// It returns an error if the receiver is not an exported type or has -// no suitable methods. It also logs the error using package log. -func (s *Service) Register(name string, svc interface{}) error { - if s.rpc == nil { - return errors.New("RPC service is not configured") - } - - return s.rpc.RegisterName(name, svc) -} - -// Client creates new RPC client. -func (s *Service) Client() (*rpc.Client, error) { - if s.cfg == nil { - return nil, errors.New("RPC service is not configured") - } - - conn, err := s.cfg.Dialer() - if err != nil { - return nil, err - } - - return rpc.NewClientWithCodec(goridge.NewClientCodec(conn)), nil -} diff --git a/service/rpc/service_test.go b/service/rpc/service_test.go deleted file mode 100644 index 51c1b337..00000000 --- a/service/rpc/service_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package rpc - -import ( - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -type testService struct{} - -func (ts *testService) Echo(msg string, r *string) error { *r = msg; return nil } - -func Test_Disabled(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: false}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.False(t, ok) -} - -func Test_RegisterNotConfigured(t *testing.T) { - s := &Service{} - assert.Error(t, s.Register("test", &testService{})) - - client, err := s.Client() - assert.Nil(t, client) - assert.Error(t, err) - assert.Error(t, s.Serve()) -} - -func Test_Enabled(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9008"}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.True(t, ok) -} - -func Test_StopNonServing(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9008"}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.True(t, ok) - s.Stop() -} - -func Test_Serve_Errors(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "malformed"}, service.NewContainer(nil), nil) - assert.NoError(t, err) - assert.True(t, ok) - - assert.Error(t, s.Serve()) - - client, err := s.Client() - assert.Nil(t, client) - assert.Error(t, err) -} - -func Test_Serve_Client(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9018"}, service.NewContainer(nil), nil) - assert.NoError(t, err) - assert.True(t, ok) - - defer s.Stop() - - assert.NoError(t, s.Register("test", &testService{})) - - go func() { assert.NoError(t, s.Serve()) }() - time.Sleep(time.Second) - - client, err := s.Client() - assert.NotNil(t, client) - assert.NoError(t, err) - - var resp string - assert.NoError(t, client.Call("test.Echo", "hello world", &resp)) - assert.Equal(t, "hello world", resp) - assert.NoError(t, client.Close()) -} - -func TestSetEnv(t *testing.T) { - s := &Service{} - e := env.NewService(map[string]string{}) - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9018"}, service.NewContainer(nil), e) - - assert.NoError(t, err) - assert.True(t, ok) - - v, _ := e.GetEnv() - assert.Equal(t, "tcp://localhost:9018", v["RR_RPC"]) -} diff --git a/service/rpc/system.go b/service/rpc/system.go deleted file mode 100644 index ffba3782..00000000 --- a/service/rpc/system.go +++ /dev/null @@ -1,18 +0,0 @@ -package rpc - -import "github.com/spiral/roadrunner/service" - -// systemService service controls rr server. -type systemService struct { - c service.Container -} - -// Detach the underlying c. -func (s *systemService) Stop(stop bool, r *string) error { - if stop { - s.c.Stop() - } - *r = "OK" - - return nil -} diff --git a/service/static/config.go b/service/static/config.go deleted file mode 100644 index 3ca20a83..00000000 --- a/service/static/config.go +++ /dev/null @@ -1,82 +0,0 @@ -package static - -import ( - "fmt" - "github.com/spiral/roadrunner/service" - "os" - "path" - "strings" -) - -// Config describes file location and controls access to them. -type Config struct { - // Dir contains name of directory to control access to. - Dir string - - // Forbid specifies list of file extensions which are forbidden for access. - // Example: .php, .exe, .bat, .htaccess and etc. - Forbid []string - - // Always specifies list of extensions which must always be served by static - // service, even if file not found. - Always []string - - // Request headers to add to every static. - Request map[string]string - - // Response headers to add to every static. - Response map[string]string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return c.Valid() -} - -// Valid returns nil if config is valid. -func (c *Config) Valid() error { - st, err := os.Stat(c.Dir) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("root directory '%s' does not exists", c.Dir) - } - - return err - } - - if !st.IsDir() { - return fmt.Errorf("invalid root directory '%s'", c.Dir) - } - - return nil -} - -// AlwaysForbid must return true if file extension is not allowed for the upload. -func (c *Config) AlwaysForbid(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range c.Forbid { - if ext == v { - return true - } - } - - return false -} - -// AlwaysServe must indicate that file is expected to be served by static service. -func (c *Config) AlwaysServe(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range c.Always { - if ext == v { - return true - } - } - - return false -} diff --git a/service/static/config_test.go b/service/static/config_test.go deleted file mode 100644 index 8bf0d372..00000000 --- a/service/static/config_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package static - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"dir": "./", "request":{"foo": "bar"}, "response":{"xxx": "yyy"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &mockCfg{`{"enable": true,"dir": "/dir/"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func TestConfig_Forbids(t *testing.T) { - cfg := Config{Forbid: []string{".php"}} - - assert.True(t, cfg.AlwaysForbid("index.php")) - assert.True(t, cfg.AlwaysForbid("index.PHP")) - assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php")) - assert.False(t, cfg.AlwaysForbid("index.html")) -} - -func TestConfig_Valid(t *testing.T) { - assert.NoError(t, (&Config{Dir: "./"}).Valid()) - assert.Error(t, (&Config{Dir: "./config.go"}).Valid()) - assert.Error(t, (&Config{Dir: "./dir/"}).Valid()) -} diff --git a/service/static/service.go b/service/static/service.go deleted file mode 100644 index 95b99860..00000000 --- a/service/static/service.go +++ /dev/null @@ -1,87 +0,0 @@ -package static - -import ( - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" - "path" -) - -// ID contains default service name. -const ID = "static" - -// Service serves static files. Potentially convert into middleware? -type Service struct { - // server configuration (location, forbidden files and etc) - cfg *Config - - // root is initiated http directory - root http.Dir -} - -// Init must return configure service and return true if service hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - if r == nil { - return false, nil - } - - s.cfg = cfg - s.root = http.Dir(s.cfg.Dir) - r.AddMiddleware(s.middleware) - - return true, nil -} - -// middleware must return true if request/response pair is handled within the middleware. -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - // Define the http.HandlerFunc - return func(w http.ResponseWriter, r *http.Request) { - if s.cfg.Request != nil { - for k, v := range s.cfg.Request { - r.Header.Add(k, v) - } - } - - if s.cfg.Response != nil { - for k, v := range s.cfg.Response { - w.Header().Set(k, v) - } - } - - if !s.handleStatic(w, r) { - f(w, r) - } - } -} - -func (s *Service) handleStatic(w http.ResponseWriter, r *http.Request) bool { - fPath := path.Clean(r.URL.Path) - - if s.cfg.AlwaysForbid(fPath) { - return false - } - - f, err := s.root.Open(fPath) - if err != nil { - if s.cfg.AlwaysServe(fPath) { - w.WriteHeader(404) - return true - } - - return false - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - return false - } - - // do not serve directories - if d.IsDir() { - return false - } - - http.ServeContent(w, r, d.Name(), d.ModTime(), f) - return true -} diff --git a/service/static/service_test.go b/service/static/service_test.go deleted file mode 100644 index 842662c9..00000000 --- a/service/static/service_test.go +++ /dev/null @@ -1,531 +0,0 @@ -package static - -import ( - "bytes" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "io" - "io/ioutil" - "net/http" - "os" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - static string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.static} - } - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Files(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - httpCfg: `{ - "enable": true, - "address": ":8029", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Second) - - - b, _, _ := get("http://localhost:8029/sample.txt") - assert.Equal(t, "sample", b) - c.Stop() -} - -func Test_Disabled(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Files_Disable(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":false, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8030", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Second) - - b, _, err := get("http://localhost:8030/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Error(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"dir/invalid", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) -} - -func Test_Files_Error2(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"dir/invalid", "forbid":[".php"]`, - httpCfg: `{ - "enable": true, - "address": ":8032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) -} - -func Test_Files_Forbid(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - b, _, err := get("http://localhost:8033/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Always(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"], "always":[".ico"]}`, - httpCfg: `{ - "enable": true, - "address": ":8034", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - _, r, err := get("http://localhost:8034/favicon.ico") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, 404, r.StatusCode) - c.Stop() -} - -func Test_Files_NotFound(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8035", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8035/client.XXX?hello=world") - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Dir(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8036", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8036/http?hello=world") - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_NotForbid(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - httpCfg: `{ - "enable": true, - "address": ":8037", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8037/client.php") - assert.Equal(t, all("../../tests/client.php"), b) - assert.Equal(t, all("../../tests/client.php"), b) - c.Stop() -} - -func TestStatic_Headers(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[], "request":{"input": "custom-header"}, "response":{"output": "output-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":8037", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:8037/client.php", nil) - if err != nil { - t.Fatal(err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - - if resp.Header.Get("Output") != "output-header" { - t.Fatal("can't find output header in response") - } - - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, all("../../tests/client.php"), string(b)) - assert.Equal(t, all("../../tests/client.php"), string(b)) - c.Stop() -} - -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - - return string(b), r, err -} - -func tmpDir() string { - p := os.TempDir() - j := json.ConfigCompatibleWithStandardLibrary - r, _ := j.Marshal(p) - - return string(r) -} - -func all(fn string) string { - f, _ := os.Open(fn) - - b := &bytes.Buffer{} - _, err := io.Copy(b, f) - if err != nil { - return "" - } - - err = f.Close() - if err != nil { - return "" - } - - return b.String() -} diff --git a/socket_factory.go b/socket_factory.go deleted file mode 100644 index 42196588..00000000 --- a/socket_factory.go +++ /dev/null @@ -1,139 +0,0 @@ -package roadrunner - -import ( - "fmt" - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" - "net" - "os/exec" - "sync" - "time" -) - -// SocketFactory connects to external workers using socket server. -type SocketFactory struct { - // listens for incoming connections from underlying processes - ls net.Listener - - // relay connection timeout - tout time.Duration - - // protects socket mapping - mu sync.Mutex - - // sockets which are waiting for process association - relays map[int]chan *goridge.SocketRelay -} - -// NewSocketFactory returns SocketFactory attached to a given socket lsn. -// tout specifies for how long factory should serve for incoming relay connection -func NewSocketFactory(ls net.Listener, tout time.Duration) *SocketFactory { - f := &SocketFactory{ - ls: ls, - tout: tout, - relays: make(map[int]chan *goridge.SocketRelay), - } - - go f.listen() - - return f -} - -// SpawnWorker creates worker and connects it to appropriate relay or returns error -func (f *SocketFactory) SpawnWorker(cmd *exec.Cmd) (w *Worker, err error) { - if w, err = newWorker(cmd); err != nil { - return nil, err - } - - if err := w.start(); err != nil { - return nil, errors.Wrap(err, "process error") - } - - rl, err := f.findRelay(w, f.tout) - if err != nil { - go func(w *Worker) { - err := w.Kill() - if err != nil { - fmt.Println(fmt.Errorf("error killing the worker %v", err)) - } - }(w) - - if wErr := w.Wait(); wErr != nil { - if _, ok := wErr.(*exec.ExitError); ok { - err = errors.Wrap(wErr, err.Error()) - } else { - err = wErr - } - } - - return nil, errors.Wrap(err, "unable to connect to worker") - } - - w.rl = rl - w.state.set(StateReady) - - return w, nil -} - -// Close socket factory and underlying socket connection. -func (f *SocketFactory) Close() error { - return f.ls.Close() -} - -// listens for incoming socket connections -func (f *SocketFactory) listen() { - for { - conn, err := f.ls.Accept() - if err != nil { - return - } - - rl := goridge.NewSocketRelay(conn) - if pid, err := fetchPID(rl); err == nil { - f.relayChan(pid) <- rl - } - } -} - -// waits for worker to connect over socket and returns associated relay of timeout -func (f *SocketFactory) findRelay(w *Worker, tout time.Duration) (*goridge.SocketRelay, error) { - timer := time.NewTimer(tout) - for { - select { - case rl := <-f.relayChan(*w.Pid): - timer.Stop() - f.cleanChan(*w.Pid) - return rl, nil - - case <-timer.C: - return nil, fmt.Errorf("relay timeout") - - case <-w.waitDone: - timer.Stop() - f.cleanChan(*w.Pid) - return nil, fmt.Errorf("worker is gone") - } - } -} - -// chan to store relay associated with specific Pid -func (f *SocketFactory) relayChan(pid int) chan *goridge.SocketRelay { - f.mu.Lock() - defer f.mu.Unlock() - - rl, ok := f.relays[pid] - if !ok { - f.relays[pid] = make(chan *goridge.SocketRelay) - return f.relays[pid] - } - - return rl -} - -// deletes relay chan associated with specific Pid -func (f *SocketFactory) cleanChan(pid int) { - f.mu.Lock() - defer f.mu.Unlock() - - delete(f.relays, pid) -} diff --git a/src/Diactoros/ServerRequestFactory.php b/src/Diactoros/ServerRequestFactory.php deleted file mode 100644 index 6a42f207..00000000 --- a/src/Diactoros/ServerRequestFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Diactoros; - -use Psr\Http\Message\ServerRequestFactoryInterface; -use Psr\Http\Message\ServerRequestInterface; -use Laminas\Diactoros\ServerRequest; - -final class ServerRequestFactory implements ServerRequestFactoryInterface -{ - /** - * @inheritdoc - * - * @param array<mixed> $serverParams Array of SAPI parameters with which to seed the generated request instance. - */ - public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface - { - $uploadedFiles = []; - return new ServerRequest($serverParams, $uploadedFiles, $uri, $method); - } -} diff --git a/src/Diactoros/StreamFactory.php b/src/Diactoros/StreamFactory.php deleted file mode 100644 index 68a77e92..00000000 --- a/src/Diactoros/StreamFactory.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Diactoros; - -use RuntimeException; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\StreamInterface; -use Laminas\Diactoros\Stream; - -final class StreamFactory implements StreamFactoryInterface -{ - /** - * @inheritdoc - * @throws RuntimeException - */ - public function createStream(string $content = ''): StreamInterface - { - $resource = fopen('php://temp', 'rb+'); - - if (! \is_resource($resource)) { - throw new RuntimeException('Cannot create stream'); - } - - fwrite($resource, $content); - rewind($resource); - return $this->createStreamFromResource($resource); - } - - /** - * @inheritdoc - */ - public function createStreamFromFile(string $file, string $mode = 'rb'): StreamInterface - { - $resource = fopen($file, $mode); - - if (! \is_resource($resource)) { - throw new RuntimeException('Cannot create stream'); - } - - return $this->createStreamFromResource($resource); - } - - /** - * @inheritdoc - */ - public function createStreamFromResource($resource): StreamInterface - { - return new Stream($resource); - } -} diff --git a/src/Diactoros/UploadedFileFactory.php b/src/Diactoros/UploadedFileFactory.php deleted file mode 100644 index daa475c1..00000000 --- a/src/Diactoros/UploadedFileFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Diactoros; - -use Psr\Http\Message\StreamInterface; -use Psr\Http\Message\UploadedFileFactoryInterface; -use Psr\Http\Message\UploadedFileInterface; -use Laminas\Diactoros\UploadedFile; - -final class UploadedFileFactory implements UploadedFileFactoryInterface -{ - /** - * @inheritdoc - */ - public function createUploadedFile( - StreamInterface $stream, - int $size = null, - int $error = \UPLOAD_ERR_OK, - string $clientFilename = null, - string $clientMediaType = null - ): UploadedFileInterface { - if ($size === null) { - $size = (int) $stream->getSize(); - } - - /** @var resource $stream */ - return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); - } -} diff --git a/src/Exception/MetricException.php b/src/Exception/MetricException.php deleted file mode 100644 index d5b738b8..00000000 --- a/src/Exception/MetricException.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -/** - * Spiral Framework. - * - * @license MIT - * @author Anton Titov (Wolfy-J) - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Exception; - -use Spiral\Goridge\Exceptions\RPCException; - -class MetricException extends RPCException -{ -} diff --git a/src/Exception/RoadRunnerException.php b/src/Exception/RoadRunnerException.php deleted file mode 100644 index f83c3dd4..00000000 --- a/src/Exception/RoadRunnerException.php +++ /dev/null @@ -1,14 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Exception; - -class RoadRunnerException extends \Spiral\RoadRunner\Exceptions\RoadRunnerException -{ -} diff --git a/src/Exceptions/RoadRunnerException.php b/src/Exceptions/RoadRunnerException.php deleted file mode 100644 index 43967893..00000000 --- a/src/Exceptions/RoadRunnerException.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php - -/** - * Spiral Framework. - * - * @license MIT - * @author Anton Titov (Wolfy-J) - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner\Exceptions; - -/** - * @deprecated use \Spiral\RoadRunner\Exception\RoadRunnerException instead - */ -class RoadRunnerException extends \RuntimeException -{ -} diff --git a/src/HttpClient.php b/src/HttpClient.php deleted file mode 100644 index 9b9048ca..00000000 --- a/src/HttpClient.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Alex Bond - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner; - -final class HttpClient -{ - /** @var Worker */ - private $worker; - - /** - * @param Worker $worker - */ - public function __construct(Worker $worker) - { - $this->worker = $worker; - } - - /** - * @return Worker - */ - public function getWorker(): Worker - { - return $this->worker; - } - - /** - * @return mixed[]|null Request information as ['ctx'=>[], 'body'=>string] - * or null if termination request or invalid context. - */ - public function acceptRequest(): ?array - { - $body = $this->getWorker()->receive($ctx); - if (empty($body) && empty($ctx)) { - // termination request - return null; - } - - $ctx = json_decode($ctx, true); - if ($ctx === null) { - // invalid context - return null; - } - - return ['ctx' => $ctx, 'body' => $body]; - } - - /** - * Send response to the application server. - * - * @param int $status Http status code - * @param string $body Body of response - * @param string[][] $headers An associative array of the message's headers. Each - * key MUST be a header name, and each value MUST be an array of strings - * for that header. - */ - public function respond(int $status, string $body, array $headers = []): void - { - $sendHeaders = empty($headers) - ? new \stdClass() // this is required to represent empty header set as map and not as array - : $headers; - - $this->getWorker()->send( - $body, - (string) json_encode(['status' => $status, 'headers' => $sendHeaders]) - ); - } -} diff --git a/src/Metrics.php b/src/Metrics.php deleted file mode 100644 index d6b6e1da..00000000 --- a/src/Metrics.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -/** - * Spiral Framework. - * - * @license MIT - * @author Anton Titov (Wolfy-J) - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner; - -use Spiral\Goridge\Exceptions\RPCException; -use Spiral\Goridge\RPC; -use Spiral\RoadRunner\Exception\MetricException; - -/** - * Application metrics. - */ -final class Metrics implements MetricsInterface -{ - /** @var RPC */ - private $rpc; - - /** - * @param RPC $rpc - */ - public function __construct(RPC $rpc) - { - $this->rpc = $rpc; - } - - /** - * @inheritDoc - */ - public function add(string $name, float $value, array $labels = []): void - { - try { - $this->rpc->call('metrics.Add', compact('name', 'value', 'labels')); - } catch (RPCException $e) { - throw new MetricException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * @inheritDoc - */ - public function sub(string $name, float $value, array $labels = []): void - { - try { - $this->rpc->call('metrics.Sub', compact('name', 'value', 'labels')); - } catch (RPCException $e) { - throw new MetricException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * @inheritDoc - */ - public function observe(string $name, float $value, array $labels = []): void - { - try { - $this->rpc->call('metrics.Observe', compact('name', 'value', 'labels')); - } catch (RPCException $e) { - throw new MetricException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * @inheritDoc - */ - public function set(string $name, float $value, array $labels = []): void - { - try { - $this->rpc->call('metrics.Set', compact('name', 'value', 'labels')); - } catch (RPCException $e) { - throw new MetricException($e->getMessage(), $e->getCode(), $e); - } - } -} diff --git a/src/MetricsInterface.php b/src/MetricsInterface.php deleted file mode 100644 index ec2009b0..00000000 --- a/src/MetricsInterface.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php - -/** - * Spiral Framework. - * - * @license MIT - * @author Anton Titov (Wolfy-J) - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner; - -use Spiral\RoadRunner\Exception\MetricException; - -interface MetricsInterface -{ - /** - * Add collector value. Fallback to appropriate method of related collector. - * - * @param string $collector - * @param float $value - * @param mixed[] $labels - * - * @throws MetricException - * @return void - */ - public function add(string $collector, float $value, array $labels = []); - - /** - * Subtract the collector value, only for gauge collector. - * - * @param string $collector - * @param float $value - * @param mixed[] $labels - * - * @throws MetricException - * @return void - */ - public function sub(string $collector, float $value, array $labels = []); - - /** - * Observe collector value, only for histogram and summary collectors. - * - * @param string $collector - * @param float $value - * @param mixed[] $labels - * - * @throws MetricException - * @return void - */ - public function observe(string $collector, float $value, array $labels = []); - - /** - * Set collector value, only for gauge collector. - * - * @param string $collector - * @param float $value - * @param mixed[] $labels - * - * @throws MetricException - * @return void - */ - public function set(string $collector, float $value, array $labels = []); -} diff --git a/src/PSR7Client.php b/src/PSR7Client.php deleted file mode 100644 index 777dd891..00000000 --- a/src/PSR7Client.php +++ /dev/null @@ -1,217 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner; - -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestFactoryInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UploadedFileFactoryInterface; -use Psr\Http\Message\UploadedFileInterface; - -/** - * Manages PSR-7 request and response. - */ -class PSR7Client -{ - /** @var HttpClient */ - private $httpClient; - - /** @var ServerRequestFactoryInterface */ - private $requestFactory; - - /** @var StreamFactoryInterface */ - private $streamFactory; - - /** @var UploadedFileFactoryInterface */ - private $uploadsFactory; - - /** @var mixed[] */ - private $originalServer = []; - - /** @var string[] Valid values for HTTP protocol version */ - private static $allowedVersions = ['1.0', '1.1', '2',]; - - /** - * @param Worker $worker - * @param ServerRequestFactoryInterface|null $requestFactory - * @param StreamFactoryInterface|null $streamFactory - * @param UploadedFileFactoryInterface|null $uploadsFactory - */ - public function __construct( - Worker $worker, - ServerRequestFactoryInterface $requestFactory = null, - StreamFactoryInterface $streamFactory = null, - UploadedFileFactoryInterface $uploadsFactory = null - ) { - $this->httpClient = new HttpClient($worker); - $this->requestFactory = $requestFactory ?? new Diactoros\ServerRequestFactory(); - $this->streamFactory = $streamFactory ?? new Diactoros\StreamFactory(); - $this->uploadsFactory = $uploadsFactory ?? new Diactoros\UploadedFileFactory(); - $this->originalServer = $_SERVER; - } - - /** - * @return Worker - */ - public function getWorker(): Worker - { - return $this->httpClient->getWorker(); - } - - /** - * @return ServerRequestInterface|null - */ - public function acceptRequest(): ?ServerRequestInterface - { - $rawRequest = $this->httpClient->acceptRequest(); - if ($rawRequest === null) { - return null; - } - - $_SERVER = $this->configureServer($rawRequest['ctx']); - - $request = $this->requestFactory->createServerRequest( - $rawRequest['ctx']['method'], - $rawRequest['ctx']['uri'], - $_SERVER - ); - - parse_str($rawRequest['ctx']['rawQuery'], $query); - - $request = $request - ->withProtocolVersion(static::fetchProtocolVersion($rawRequest['ctx']['protocol'])) - ->withCookieParams($rawRequest['ctx']['cookies']) - ->withQueryParams($query) - ->withUploadedFiles($this->wrapUploads($rawRequest['ctx']['uploads'])); - - foreach ($rawRequest['ctx']['attributes'] as $name => $value) { - $request = $request->withAttribute($name, $value); - } - - foreach ($rawRequest['ctx']['headers'] as $name => $value) { - $request = $request->withHeader($name, $value); - } - - if ($rawRequest['ctx']['parsed']) { - return $request->withParsedBody(json_decode($rawRequest['body'], true)); - } - - if ($rawRequest['body'] !== null) { - return $request->withBody($this->streamFactory->createStream($rawRequest['body'])); - } - - return $request; - } - - /** - * Send response to the application server. - * - * @param ResponseInterface $response - */ - public function respond(ResponseInterface $response): void - { - $this->httpClient->respond( - $response->getStatusCode(), - $response->getBody()->__toString(), - $response->getHeaders() - ); - } - - /** - * Returns altered copy of _SERVER variable. Sets ip-address, - * request-time and other values. - * - * @param mixed[] $ctx - * @return mixed[] - */ - protected function configureServer(array $ctx): array - { - $server = $this->originalServer; - - $server['REQUEST_URI'] = $ctx['uri']; - $server['REQUEST_TIME'] = time(); - $server['REQUEST_TIME_FLOAT'] = microtime(true); - $server['REMOTE_ADDR'] = $ctx['attributes']['ipAddress'] ?? $ctx['remoteAddr'] ?? '127.0.0.1'; - $server['REQUEST_METHOD'] = $ctx['method']; - - $server['HTTP_USER_AGENT'] = ''; - foreach ($ctx['headers'] as $key => $value) { - $key = strtoupper(str_replace('-', '_', $key)); - if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) { - $server[$key] = implode(', ', $value); - } else { - $server['HTTP_' . $key] = implode(', ', $value); - } - } - - return $server; - } - - /** - * Wraps all uploaded files with UploadedFile. - * - * @param array[] $files - * - * @return UploadedFileInterface[]|mixed[] - */ - private function wrapUploads($files): array - { - if (empty($files)) { - return []; - } - - $result = []; - foreach ($files as $index => $f) { - if (!isset($f['name'])) { - $result[$index] = $this->wrapUploads($f); - continue; - } - - if (UPLOAD_ERR_OK === $f['error']) { - $stream = $this->streamFactory->createStreamFromFile($f['tmpName']); - } else { - $stream = $this->streamFactory->createStream(); - } - - $result[$index] = $this->uploadsFactory->createUploadedFile( - $stream, - $f['size'], - $f['error'], - $f['name'], - $f['mime'] - ); - } - - return $result; - } - - /** - * Normalize HTTP protocol version to valid values - * - * @param string $version - * @return string - */ - private static function fetchProtocolVersion(string $version): string - { - $v = substr($version, 5); - - if ($v === '2.0') { - return '2'; - } - - // Fallback for values outside of valid protocol versions - if (!in_array($v, static::$allowedVersions, true)) { - return '1.1'; - } - - return $v; - } -} diff --git a/src/Worker.php b/src/Worker.php deleted file mode 100644 index d509562e..00000000 --- a/src/Worker.php +++ /dev/null @@ -1,178 +0,0 @@ -<?php - -/** - * High-performance PHP process supervisor and load balancer written in Go - * - * @author Wolfy-J - */ -declare(strict_types=1); - -namespace Spiral\RoadRunner; - -use Spiral\Goridge\Exceptions\GoridgeException; -use Spiral\Goridge\RelayInterface as Relay; -use Spiral\Goridge\SendPackageRelayInterface; -use Spiral\RoadRunner\Exception\RoadRunnerException; - -/** - * Accepts connection from RoadRunner server over given Goridge relay. - * - * Example: - * - * $worker = new Worker(new Goridge\StreamRelay(STDIN, STDOUT)); - * while ($task = $worker->receive($context)) { - * $worker->send("DONE", json_encode($context)); - * } - */ -class Worker -{ - // Send as response context to request worker termination - public const STOP = '{"stop":true}'; - - /** @var Relay */ - private $relay; - - /** - * @param Relay $relay - */ - public function __construct(Relay $relay) - { - $this->relay = $relay; - } - - /** - * Receive packet of information to process, returns null when process must be stopped. Might - * return Error to wrap error message from server. - * - * @param mixed $header - * @return \Error|null|string - * - * @throws GoridgeException - */ - public function receive(&$header) - { - $body = $this->relay->receiveSync($flags); - - if ($flags & Relay::PAYLOAD_CONTROL) { - if ($this->handleControl($body, $header, $flags)) { - // wait for the next command - return $this->receive($header); - } - - // no context for the termination. - $header = null; - - // Expect process termination - return null; - } - - if ($flags & Relay::PAYLOAD_ERROR) { - return new \Error((string)$body); - } - - return $body; - } - - /** - * Respond to the server with result of task execution and execution context. - * - * Example: - * $worker->respond((string)$response->getBody(), json_encode($response->getHeaders())); - * - * @param string|null $payload - * @param string|null $header - */ - public function send(string $payload = null, string $header = null): void - { - if (!$this->relay instanceof SendPackageRelayInterface) { - if ($header === null) { - $this->relay->send('', Relay::PAYLOAD_CONTROL | Relay::PAYLOAD_NONE); - } else { - $this->relay->send($header, Relay::PAYLOAD_CONTROL | Relay::PAYLOAD_RAW); - } - - $this->relay->send((string)$payload, Relay::PAYLOAD_RAW); - } else { - $this->relay->sendPackage( - (string)$header, - Relay::PAYLOAD_CONTROL | ($header === null ? Relay::PAYLOAD_NONE : Relay::PAYLOAD_RAW), - (string)$payload, - Relay::PAYLOAD_RAW - ); - } - } - - /** - * Respond to the server with an error. Error must be treated as TaskError and might not cause - * worker destruction. - * - * Example: - * - * $worker->error("invalid payload"); - * - * @param string $message - */ - public function error(string $message): void - { - $this->relay->send( - $message, - Relay::PAYLOAD_CONTROL | Relay::PAYLOAD_RAW | Relay::PAYLOAD_ERROR - ); - } - - /** - * Terminate the process. Server must automatically pass task to the next available process. - * Worker will receive StopCommand context after calling this method. - * - * Attention, you MUST use continue; after invoking this method to let rr to properly - * stop worker. - * - * @throws GoridgeException - */ - public function stop(): void - { - $this->send(null, self::STOP); - } - - /** - * Handles incoming control command payload and executes it if required. - * - * @param string $body - * @param mixed $header Exported context (if any). - * @param int $flags - * @return bool True when continue processing. - * - * @throws RoadRunnerException - */ - private function handleControl(string $body = null, &$header = null, int $flags = 0): bool - { - $header = $body; - if ($body === null || $flags & Relay::PAYLOAD_RAW) { - // empty or raw prefix - return true; - } - - $p = json_decode($body, true); - if ($p === false) { - throw new RoadRunnerException('invalid task context, JSON payload is expected'); - } - - // PID negotiation (socket connections only) - if (!empty($p['pid'])) { - $this->relay->send( - sprintf('{"pid":%s}', getmypid()), - Relay::PAYLOAD_CONTROL - ); - } - - // termination request - if (!empty($p['stop'])) { - return false; - } - - // parsed header - $header = $p; - - return true; - } -} diff --git a/state.go b/state.go deleted file mode 100644 index 98451f48..00000000 --- a/state.go +++ /dev/null @@ -1,98 +0,0 @@ -package roadrunner - -import ( - "fmt" - "sync/atomic" -) - -// State represents worker status and updated time. -type State interface { - fmt.Stringer - - // Value returns state value - Value() int64 - - // NumJobs shows how many times worker was invoked - NumExecs() int64 - - // IsActive returns true if worker not Inactive or Stopped - IsActive() bool -} - -const ( - // StateInactive - no associated process - StateInactive int64 = iota - - // StateReady - ready for job. - StateReady - - // StateWorking - working on given payload. - StateWorking - - // StateInvalid - indicates that worker is being disabled and will be removed. - StateInvalid - - // StateStopping - process is being softly stopped. - StateStopping - - // StateStopped - process has been terminated. - StateStopped - - // StateErrored - error state (can't be used). - StateErrored -) - -type state struct { - value int64 - numExecs int64 -} - -func newState(value int64) *state { - return &state{value: value} -} - -// String returns current state as string. -func (s *state) String() string { - switch s.Value() { - case StateInactive: - return "inactive" - case StateReady: - return "ready" - case StateWorking: - return "working" - case StateInvalid: - return "invalid" - case StateStopped: - return "stopped" - case StateErrored: - return "errored" - } - - return "undefined" -} - -// NumExecs returns number of registered worker execs. -func (s *state) NumExecs() int64 { - return atomic.LoadInt64(&s.numExecs) -} - -// Value state returns state value -func (s *state) Value() int64 { - return atomic.LoadInt64(&s.value) -} - -// IsActive returns true if worker not Inactive or Stopped -func (s *state) IsActive() bool { - state := s.Value() - return state == StateWorking || state == StateReady -} - -// change state value (status) -func (s *state) set(value int64) { - atomic.StoreInt64(&s.value, value) -} - -// register new execution atomically -func (s *state) registerExec() { - atomic.AddInt64(&s.numExecs, 1) -} diff --git a/state_test.go b/state_test.go deleted file mode 100644 index c13c5a88..00000000 --- a/state_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_NewState(t *testing.T) { - st := newState(StateErrored) - - assert.Equal(t, "errored", st.String()) - - assert.Equal(t, "inactive", newState(StateInactive).String()) - assert.Equal(t, "ready", newState(StateReady).String()) - assert.Equal(t, "working", newState(StateWorking).String()) - assert.Equal(t, "stopped", newState(StateStopped).String()) - assert.Equal(t, "undefined", newState(1000).String()) -} - -func Test_IsActive(t *testing.T) { - assert.False(t, newState(StateInactive).IsActive()) - assert.True(t, newState(StateReady).IsActive()) - assert.True(t, newState(StateWorking).IsActive()) - assert.False(t, newState(StateStopped).IsActive()) - assert.False(t, newState(StateErrored).IsActive()) -} diff --git a/static_pool.go b/static_pool.go deleted file mode 100644 index c4b6f42d..00000000 --- a/static_pool.go +++ /dev/null @@ -1,370 +0,0 @@ -package roadrunner - -import ( - "fmt" - "os/exec" - "sync" - "sync/atomic" - "time" - - "github.com/pkg/errors" -) - -const ( - // StopRequest can be sent by worker to indicate that restart is required. - StopRequest = "{\"stop\":true}" -) - -// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of workers. -type StaticPool struct { - // pool behaviour - cfg Config - - // worker command creator - cmd func() *exec.Cmd - - // creates and connects to workers - factory Factory - - // active task executions - tmu sync.Mutex - tasks sync.WaitGroup - - // workers circular allocation buf - free chan *Worker - - // number of workers expected to be dead in a buf. - numDead int64 - - // protects state of worker list, does not affect allocation - muw sync.RWMutex - - // all registered workers - workers []*Worker - - // invalid declares set of workers to be removed from the pool. - remove sync.Map - - // pool is being destroyed - inDestroy int32 - destroy chan interface{} - - // lsn is optional callback to handle worker create/destruct/error events. - mul sync.Mutex - lsn func(event int, ctx interface{}) -} - -// NewPool creates new worker pool and task multiplexer. StaticPool will initiate with one worker. -func NewPool(cmd func() *exec.Cmd, factory Factory, cfg Config) (*StaticPool, error) { - if err := cfg.Valid(); err != nil { - return nil, errors.Wrap(err, "config") - } - - p := &StaticPool{ - cfg: cfg, - cmd: cmd, - factory: factory, - workers: make([]*Worker, 0, cfg.NumWorkers), - free: make(chan *Worker, cfg.NumWorkers), - destroy: make(chan interface{}), - } - - // constant number of workers simplify logic - for i := int64(0); i < p.cfg.NumWorkers; i++ { - // to test if worker ready - w, err := p.createWorker() - if err != nil { - p.Destroy() - return nil, err - } - - p.free <- w - } - - return p, nil -} - -// Listen attaches pool event controller. -func (p *StaticPool) Listen(l func(event int, ctx interface{})) { - p.mul.Lock() - defer p.mul.Unlock() - - p.lsn = l - - p.muw.Lock() - for _, w := range p.workers { - w.err.Listen(p.lsn) - } - p.muw.Unlock() -} - -// Config returns associated pool configuration. Immutable. -func (p *StaticPool) Config() Config { - return p.cfg -} - -// Workers returns worker list associated with the pool. -func (p *StaticPool) Workers() (workers []*Worker) { - p.muw.RLock() - defer p.muw.RUnlock() - - workers = append(workers, p.workers...) - - return workers -} - -// Remove forces pool to remove specific worker. -func (p *StaticPool) Remove(w *Worker, err error) bool { - if w.State().Value() != StateReady && w.State().Value() != StateWorking { - // unable to remove inactive worker - return false - } - - if _, ok := p.remove.Load(w); ok { - return false - } - - p.remove.Store(w, err) - return true -} - -// Exec one task with given payload and context, returns result or error. -func (p *StaticPool) Exec(rqs *Payload) (rsp *Payload, err error) { - p.tmu.Lock() - p.tasks.Add(1) - p.tmu.Unlock() - - defer p.tasks.Done() - - w, err := p.allocateWorker() - if err != nil { - return nil, errors.Wrap(err, "unable to allocate worker") - } - - rsp, err = w.Exec(rqs) - - if err != nil { - // soft job errors are allowed - if _, jobError := err.(JobError); jobError { - p.release(w) - return nil, err - } - - p.discardWorker(w, err) - return nil, err - } - - // worker want's to be terminated - if rsp.Body == nil && rsp.Context != nil && string(rsp.Context) == StopRequest { - p.discardWorker(w, err) - return p.Exec(rqs) - } - - p.release(w) - return rsp, nil -} - -// Destroy all underlying workers (but let them to complete the task). -func (p *StaticPool) Destroy() { - atomic.AddInt32(&p.inDestroy, 1) - - p.tmu.Lock() - p.tasks.Wait() - close(p.destroy) - p.tmu.Unlock() - - var wg sync.WaitGroup - for _, w := range p.Workers() { - wg.Add(1) - w.markInvalid() - go func(w *Worker) { - defer wg.Done() - p.destroyWorker(w, nil) - }(w) - } - - wg.Wait() -} - -// finds free worker in a given time interval. Skips dead workers. -func (p *StaticPool) allocateWorker() (w *Worker, err error) { - // TODO loop counts upward, but its variable is bounded downward. - for i := atomic.LoadInt64(&p.numDead); i >= 0; i++ { - // this loop is required to skip issues with dead workers still being in a ring - // (we know how many workers). - select { - case w = <-p.free: - if w.State().Value() != StateReady { - // found expected dead worker - atomic.AddInt64(&p.numDead, ^int64(0)) - continue - } - - if err, remove := p.remove.Load(w); remove { - p.discardWorker(w, err) - - // get next worker - i++ - continue - } - - return w, nil - case <-p.destroy: - return nil, fmt.Errorf("pool has been stopped") - default: - // enable timeout handler - } - - timeout := time.NewTimer(p.cfg.AllocateTimeout) - select { - case <-timeout.C: - return nil, fmt.Errorf("worker timeout (%s)", p.cfg.AllocateTimeout) - case w = <-p.free: - timeout.Stop() - - if w.State().Value() != StateReady { - atomic.AddInt64(&p.numDead, ^int64(0)) - continue - } - - if err, remove := p.remove.Load(w); remove { - p.discardWorker(w, err) - - // get next worker - i++ - continue - } - - return w, nil - case <-p.destroy: - timeout.Stop() - - return nil, fmt.Errorf("pool has been stopped") - } - } - - return nil, fmt.Errorf("all workers are dead (%v)", p.cfg.NumWorkers) -} - -// release releases or replaces the worker. -func (p *StaticPool) release(w *Worker) { - if p.cfg.MaxJobs != 0 && w.State().NumExecs() >= p.cfg.MaxJobs { - p.discardWorker(w, p.cfg.MaxJobs) - return - } - - if err, remove := p.remove.Load(w); remove { - p.discardWorker(w, err) - return - } - - p.free <- w -} - -// creates new worker using associated factory. automatically -// adds worker to the worker list (background) -func (p *StaticPool) createWorker() (*Worker, error) { - w, err := p.factory.SpawnWorker(p.cmd()) - if err != nil { - return nil, err - } - - p.mul.Lock() - if p.lsn != nil { - w.err.Listen(p.lsn) - } - p.mul.Unlock() - - p.throw(EventWorkerConstruct, w) - - p.muw.Lock() - p.workers = append(p.workers, w) - p.muw.Unlock() - - go p.watchWorker(w) - return w, nil -} - -// gentry remove worker -func (p *StaticPool) discardWorker(w *Worker, caused interface{}) { - w.markInvalid() - go p.destroyWorker(w, caused) -} - -// destroyWorker destroys workers and removes it from the pool. -// TODO caused unused -func (p *StaticPool) destroyWorker(w *Worker, caused interface{}) { - go func() { - err := w.Stop() - if err != nil { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - }() - - select { - case <-w.waitDone: - // worker is dead - p.throw(EventWorkerDestruct, w) - - case <-time.NewTimer(p.cfg.DestroyTimeout).C: - // failed to stop process in given time - if err := w.Kill(); err != nil { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - - p.throw(EventWorkerKill, w) - } -} - -// watchWorker watches worker state and replaces it if worker fails. -func (p *StaticPool) watchWorker(w *Worker) { - err := w.Wait() - p.throw(EventWorkerDead, w) - - // detaching - p.muw.Lock() - for i, wc := range p.workers { - if wc == w { - p.workers = append(p.workers[:i], p.workers[i+1:]...) - p.remove.Delete(w) - break - } - } - p.muw.Unlock() - - // registering a dead worker - atomic.AddInt64(&p.numDead, 1) - - // worker have died unexpectedly, pool should attempt to replace it with alive version safely - if err != nil { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - - if !p.destroyed() { - nw, err := p.createWorker() - if err == nil { - p.free <- nw - return - } - - // possible situation when major error causes all PHP scripts to die (for example dead DB) - if len(p.Workers()) == 0 { - p.throw(EventPoolError, err) - } else { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - } -} - -func (p *StaticPool) destroyed() bool { - return atomic.LoadInt32(&p.inDestroy) != 0 -} - -// throw invokes event handler if any. -func (p *StaticPool) throw(event int, ctx interface{}) { - p.mul.Lock() - if p.lsn != nil { - p.lsn(event, ctx) - } - p.mul.Unlock() -} diff --git a/static_pool_test.go b/static_pool_test.go deleted file mode 100644 index 59822186..00000000 --- a/static_pool_test.go +++ /dev/null @@ -1,496 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "log" - "os/exec" - "runtime" - "strconv" - "strings" - "sync" - "testing" - "time" -) - -var cfg = Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, -} - -func Test_NewPool(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - - assert.Equal(t, cfg, p.Config()) - - defer p.Destroy() - - assert.NotNil(t, p) -} - -func Test_StaticPool_Invalid(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/invalid.php") }, - NewPipeFactory(), - cfg, - ) - - assert.Nil(t, p) - assert.Error(t, err) -} - -func Test_ConfigError(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - Config{ - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - - assert.Nil(t, p) - assert.Error(t, err) -} - -func Test_StaticPool_Echo(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - - defer p.Destroy() - - assert.NotNil(t, p) - - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_StaticPool_Echo_NilContext(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - - defer p.Destroy() - - assert.NotNil(t, p) - - res, err := p.Exec(&Payload{Body: []byte("hello"), Context: nil}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_StaticPool_Echo_Context(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "head", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - - defer p.Destroy() - - assert.NotNil(t, p) - - res, err := p.Exec(&Payload{Body: []byte("hello"), Context: []byte("world")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.Nil(t, res.Body) - assert.NotNil(t, res.Context) - - assert.Equal(t, "world", string(res.Context)) -} - -func Test_StaticPool_JobError(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "error", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - defer p.Destroy() - - assert.NotNil(t, p) - - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) - - assert.IsType(t, JobError{}, err) - assert.Equal(t, "hello", err.Error()) -} - -func Test_StaticPool_Broken_Replace(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "broken", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - assert.NotNil(t, p) - - done := make(chan interface{}) - - p.Listen(func(e int, ctx interface{}) { - if err, ok := ctx.(error); ok { - if strings.Contains(err.Error(), "undefined_function()") { - close(done) - } - } - }) - - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) - - <-done - p.Destroy() -} - - -func Test_StaticPool_Broken_FromOutside(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - assert.NoError(t, err) - defer p.Destroy() - - assert.NotNil(t, p) - - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) - assert.Equal(t, runtime.NumCPU(), len(p.Workers())) - - destructed := make(chan interface{}) - p.Listen(func(e int, ctx interface{}) { - if e == EventWorkerConstruct { - destructed <- nil - } - }) - - // killing random worker and expecting pool to replace it - err = p.Workers()[0].cmd.Process.Kill() - if err != nil { - t.Errorf("error killing the process: error %v", err) - } - <-destructed - - for _, w := range p.Workers() { - assert.Equal(t, StateReady, w.state.Value()) - } -} - -func Test_StaticPool_AllocateTimeout(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - AllocateTimeout: time.Nanosecond * 1, - DestroyTimeout: time.Second * 2, - }, - ) - if err != nil { - t.Fatal(err) - } - - done := make(chan interface{}) - go func() { - if p != nil { - _, err := p.Exec(&Payload{Body: []byte("100")}) - assert.NoError(t, err) - close(done) - } else { - panic("Pool is nil") - } - }() - - - // to ensure that worker is already busy - time.Sleep(time.Millisecond * 10) - - _, err = p.Exec(&Payload{Body: []byte("10")}) - if err == nil { - t.Fatal("Test_StaticPool_AllocateTimeout exec should raise error") - } - assert.Contains(t, err.Error(), "worker timeout") - - <-done - p.Destroy() -} - -func Test_StaticPool_Replace_Worker(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "pid", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - MaxJobs: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - assert.NoError(t, err) - defer p.Destroy() - - assert.NotNil(t, p) - - var lastPID string - lastPID = strconv.Itoa(*p.Workers()[0].Pid) - - res, _ := p.Exec(&Payload{Body: []byte("hello")}) - assert.Equal(t, lastPID, string(res.Body)) - - for i := 0; i < 10; i++ { - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.NotEqual(t, lastPID, string(res.Body)) - lastPID = string(res.Body) - } -} - -// identical to replace but controlled on worker side -func Test_StaticPool_Stop_Worker(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "stop", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - assert.NoError(t, err) - defer p.Destroy() - - assert.NotNil(t, p) - - var lastPID string - lastPID = strconv.Itoa(*p.Workers()[0].Pid) - - res, _ := p.Exec(&Payload{Body: []byte("hello")}) - assert.Equal(t, lastPID, string(res.Body)) - - for i := 0; i < 10; i++ { - res, err := p.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.NotEqual(t, lastPID, string(res.Body)) - lastPID = string(res.Body) - } -} - -// identical to replace but controlled on worker side -func Test_Static_Pool_Destroy_And_Close(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - - assert.NotNil(t, p) - assert.NoError(t, err) - - p.Destroy() - _, err = p.Exec(&Payload{Body: []byte("100")}) - assert.Error(t, err) -} - -// identical to replace but controlled on worker side -func Test_Static_Pool_Destroy_And_Close_While_Wait(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - - assert.NotNil(t, p) - assert.NoError(t, err) - - go func() { - _, err := p.Exec(&Payload{Body: []byte("100")}) - if err != nil { - t.Errorf("error executing payload: error %v", err) - } - - }() - time.Sleep(time.Millisecond * 10) - - p.Destroy() - _, err = p.Exec(&Payload{Body: []byte("100")}) - assert.Error(t, err) -} - -// identical to replace but controlled on worker side -func Test_Static_Pool_Handle_Dead(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 5, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - assert.NoError(t, err) - defer p.Destroy() - - assert.NotNil(t, p) - - for _, w := range p.workers { - w.state.value = StateErrored - } - - _, err = p.Exec(&Payload{Body: []byte("hello")}) - assert.Error(t, err) -} - -// identical to replace but controlled on worker side -func Test_Static_Pool_Slow_Destroy(t *testing.T) { - p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/slow-destroy.php", "echo", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 5, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - - assert.NoError(t, err) - assert.NotNil(t, p) - - p.Destroy() -} - -func Benchmark_Pool_Allocate(b *testing.B) { - p, _ := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - defer p.Destroy() - - for n := 0; n < b.N; n++ { - w, err := p.allocateWorker() - if err != nil { - b.Fail() - log.Println(err) - } - - p.free <- w - } -} - -func Benchmark_Pool_Echo(b *testing.B) { - p, _ := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - cfg, - ) - defer p.Destroy() - - for n := 0; n < b.N; n++ { - if _, err := p.Exec(&Payload{Body: []byte("hello")}); err != nil { - b.Fail() - } - } -} - -func Benchmark_Pool_Echo_Batched(b *testing.B) { - p, _ := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second * 100, - DestroyTimeout: time.Second, - }, - ) - defer p.Destroy() - - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func() { - defer wg.Done() - if _, err := p.Exec(&Payload{Body: []byte("hello")}); err != nil { - b.Fail() - log.Println(err) - } - }() - } - - wg.Wait() -} - -func Benchmark_Pool_Echo_Replaced(b *testing.B) { - p, _ := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, - NewPipeFactory(), - Config{ - NumWorkers: 1, - MaxJobs: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - ) - defer p.Destroy() - - for n := 0; n < b.N; n++ { - if _, err := p.Exec(&Payload{Body: []byte("hello")}); err != nil { - b.Fail() - log.Println(err) - } - } -} diff --git a/systemd/rr.service b/systemd/rr.service index 6e4d5914..6e4d5914 100644..100755 --- a/systemd/rr.service +++ b/systemd/rr.service diff --git a/tests/broken.php b/tests/broken.php index 42b4e7c2..1f869b2d 100644 --- a/tests/broken.php +++ b/tests/broken.php @@ -8,7 +8,7 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); -while ($in = $rr->receive($ctx)) { +while ($in = $rr->waitPayload()) { echo undefined_function(); - $rr->send((string)$in); + $rr->send((string)$in->body, null); } diff --git a/tests/client.php b/tests/client.php index 835b1c6c..c00cece1 100644 --- a/tests/client.php +++ b/tests/client.php @@ -3,7 +3,7 @@ use Spiral\Goridge; ini_set('display_errors', 'stderr'); -require dirname(__DIR__) . "/vendor_php/autoload.php"; +require __DIR__ . "/vendor/autoload.php"; if (count($argv) < 3) { die("need 2 arguments"); diff --git a/tests/composer.json b/tests/composer.json new file mode 100644 index 00000000..d4f32be5 --- /dev/null +++ b/tests/composer.json @@ -0,0 +1,13 @@ +{ + "minimum-stability": "beta", + "require": { + "nyholm/psr7": "^1.3", + "spiral/roadrunner": "^2.0", + "spiral/roadrunner-http": "^2.0" + }, + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\": "src/" + } + } +} diff --git a/tests/delay.php b/tests/delay.php index bf9ecc12..f0435b05 100644 --- a/tests/delay.php +++ b/tests/delay.php @@ -8,9 +8,9 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); -while ($in = $rr->receive($ctx)) { +while ($in = $rr->waitPayload()) { try { - usleep($in * 1000); + usleep($in->body * 1000); $rr->send(''); } catch (\Throwable $e) { $rr->error((string)$e); diff --git a/tests/echo.php b/tests/echo.php index 1570e3df..83eec92e 100644 --- a/tests/echo.php +++ b/tests/echo.php @@ -8,9 +8,9 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); -while ($in = $rr->receive($ctx)) { +while ($in = $rr->waitPayload()) { try { - $rr->send((string)$in); + $rr->send((string)$in->body); } catch (\Throwable $e) { $rr->error((string)$e); } diff --git a/tests/error.php b/tests/error.php index 8e1c8d0d..c77e6817 100644 --- a/tests/error.php +++ b/tests/error.php @@ -8,6 +8,6 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); -while ($in = $rr->receive($ctx)) { - $rr->error((string)$in); +while ($in = $rr->waitPayload()) { + $rr->error((string)$in->body); } diff --git a/tests/head.php b/tests/head.php index 88ebd3f2..3c57258f 100644 --- a/tests/head.php +++ b/tests/head.php @@ -8,9 +8,9 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); -while ($in = $rr->receive($ctx)) { +while ($in = $rr->waitPayload()) { try { - $rr->send("", (string)$ctx); + $rr->send("", (string)$in->header); } catch (\Throwable $e) { $rr->error((string)$e); } diff --git a/tests/http/client.php b/tests/http/client.php index 9f21b273..ad5cce24 100644 --- a/tests/http/client.php +++ b/tests/http/client.php @@ -4,7 +4,7 @@ use Spiral\Goridge; use Spiral\RoadRunner; ini_set('display_errors', 'stderr'); -require dirname(__DIR__) . "/../vendor_php/autoload.php"; +require dirname(__DIR__) . "/vendor/autoload.php"; if (count($argv) < 3) { die("need 2 arguments"); @@ -33,12 +33,18 @@ switch ($goridge) { die("invalid protocol selection"); } -$psr7 = new RoadRunner\PSR7Client(new RoadRunner\Worker($relay)); +$psr7 = new RoadRunner\Http\PSR7Worker( + new RoadRunner\Worker($relay), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory() +); + require_once sprintf("%s/%s.php", __DIR__, $test); -while ($req = $psr7->acceptRequest()) { +while ($req = $psr7->waitRequest()) { try { - $psr7->respond(handleRequest($req, new \Zend\Diactoros\Response())); + $psr7->respond(handleRequest($req, new \Nyholm\Psr7\Response())); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } diff --git a/tests/http/slow-client.php b/tests/http/slow-client.php index 4d3963d7..731232f7 100644 --- a/tests/http/slow-client.php +++ b/tests/http/slow-client.php @@ -4,13 +4,13 @@ use Spiral\Goridge; use Spiral\RoadRunner; ini_set('display_errors', 'stderr'); -require dirname(__DIR__) . "/../vendor_php/autoload.php"; +require dirname(__DIR__) . "/vendor/autoload.php"; if (count($argv) < 3) { die("need 2 arguments"); } -list($test, $goridge, $bootDelay) = [$argv[1], $argv[2], $argv[3]]; +[$test, $goridge, $bootDelay] = [$argv[1], $argv[2], $argv[3]]; usleep($bootDelay * 1000); switch ($goridge) { @@ -34,13 +34,19 @@ switch ($goridge) { die("invalid protocol selection"); } -$psr7 = new RoadRunner\PSR7Client(new RoadRunner\Worker($relay)); +$psr7 = new RoadRunner\Http\PSR7Worker( + new RoadRunner\Worker($relay), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory() +); + require_once sprintf("%s/%s.php", __DIR__, $test); -while ($req = $psr7->acceptRequest()) { +while ($req = $psr7->waitRequest()) { try { - $psr7->respond(handleRequest($req, new \Zend\Diactoros\Response())); + $psr7->respond(handleRequest($req, new \Nyholm\Psr7\Response())); } catch (\Throwable $e) { - $psr7->getWorker()->error((string)$e); + $psr7->getWorker()->error((string) $e); } } diff --git a/tests/http/upload.php b/tests/http/upload.php index bb4af766..57526246 100644 --- a/tests/http/upload.php +++ b/tests/http/upload.php @@ -24,7 +24,7 @@ function handleRequest(ServerRequestInterface $req, ResponseInterface $resp): Re 'size' => $v->getSize(), 'mime' => $v->getClientMediaType(), 'error' => $v->getError(), - 'md5' => md5($v->getStream()->__toString()), + 'sha512' => hash('sha512', $v->getStream()->__toString()), ]; } }); diff --git a/tests/memleak.php b/tests/memleak.php new file mode 100644 index 00000000..9a5376f0 --- /dev/null +++ b/tests/memleak.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +use Spiral\Goridge\StreamRelay; +use Spiral\RoadRunner\Worker as RoadRunner; + +require __DIR__ . "/vendor/autoload.php"; + +$rr = new RoadRunner(new StreamRelay(\STDIN, \STDOUT)); +$mem = ''; +while($rr->waitPayload()){ + $mem .= str_repeat(" ", 1024*1024); + $rr->send(""); +} diff --git a/tests/pid.php b/tests/pid.php index bf10a025..f8b2515d 100644 --- a/tests/pid.php +++ b/tests/pid.php @@ -8,7 +8,7 @@ $rr = new RoadRunner\Worker($relay); - while ($in = $rr->receive($ctx)) { + while ($in = $rr->waitPayload()) { try { $rr->send((string)getmypid()); } catch (\Throwable $e) { diff --git a/tests/psr-worker-bench.php b/tests/psr-worker-bench.php new file mode 100644 index 00000000..e6df81ad --- /dev/null +++ b/tests/psr-worker-bench.php @@ -0,0 +1,28 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ +use Spiral\Goridge; +use Spiral\RoadRunner; + +ini_set('display_errors', 'stderr'); +require __DIR__ . "/vendor/autoload.php"; + +$worker = new RoadRunner\Worker(new Goridge\StreamRelay(STDIN, STDOUT)); +$psr7 = new RoadRunner\Http\PSR7Worker( + $worker, + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory() +); + +while ($req = $psr7->waitRequest()) { + try { + $resp = new \Nyholm\Psr7\Response(); + $resp->getBody()->write("hello world"); + + $psr7->respond($resp); + } catch (\Throwable $e) { + $psr7->getWorker()->error((string)$e); + } +} diff --git a/tests/psr-worker.php b/tests/psr-worker.php new file mode 100644 index 00000000..db53eee2 --- /dev/null +++ b/tests/psr-worker.php @@ -0,0 +1,28 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ +use Spiral\Goridge; +use Spiral\RoadRunner; + +ini_set('display_errors', 'stderr'); +require __DIR__ . "/vendor/autoload.php"; + +$worker = new RoadRunner\Worker(new Goridge\StreamRelay(STDIN, STDOUT)); +$psr7 = new RoadRunner\Http\PSR7Worker( + $worker, + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory(), + new \Nyholm\Psr7\Factory\Psr17Factory() +); + +while ($req = $psr7->waitRequest()) { + try { + $resp = new \Nyholm\Psr7\Response(); + $resp->getBody()->write(str_repeat("hello world", 1000)); + + $psr7->respond($resp); + } catch (\Throwable $e) { + $psr7->getWorker()->error((string)$e); + } +} diff --git a/tests/sleep.php b/tests/sleep.php new file mode 100644 index 00000000..e34a6834 --- /dev/null +++ b/tests/sleep.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +use Spiral\Goridge\StreamRelay; +use Spiral\RoadRunner\Worker as RoadRunner; + +require __DIR__ . "/vendor/autoload.php"; + +$rr = new RoadRunner(new StreamRelay(\STDIN, \STDOUT)); + +while($rr->waitPayload()){ + sleep(3); + $rr->send(""); +} diff --git a/tests/slow-client.php b/tests/slow-client.php index ece0a439..7737f0b1 100644 --- a/tests/slow-client.php +++ b/tests/slow-client.php @@ -3,7 +3,7 @@ use Spiral\Goridge; ini_set('display_errors', 'stderr'); -require dirname(__DIR__) . "/vendor_php/autoload.php"; +require __DIR__ . "/vendor/autoload.php"; if (count($argv) < 3) { die("need 2 arguments"); diff --git a/tests/slow-destroy.php b/tests/slow-destroy.php index e2a01af2..900bb68a 100644 --- a/tests/slow-destroy.php +++ b/tests/slow-destroy.php @@ -3,7 +3,7 @@ use Spiral\Goridge; ini_set('display_errors', 'stderr'); -require dirname(__DIR__) . "/vendor_php/autoload.php"; +require __DIR__ . "/vendor/autoload.php"; if (count($argv) < 3) { die("need 2 arguments"); diff --git a/tests/slow-pid.php b/tests/slow-pid.php index 747e7e86..3660cb40 100644 --- a/tests/slow-pid.php +++ b/tests/slow-pid.php @@ -8,7 +8,7 @@ $rr = new RoadRunner\Worker($relay); - while ($in = $rr->receive($ctx)) { + while ($in = $rr->waitPayload()) { try { sleep(1); $rr->send((string)getmypid()); diff --git a/tests/stop.php b/tests/stop.php index 0100ad0f..f83d3f29 100644 --- a/tests/stop.php +++ b/tests/stop.php @@ -9,7 +9,7 @@ use Spiral\RoadRunner; $rr = new RoadRunner\Worker($relay); $used = false; -while ($in = $rr->receive($ctx)) { +while ($in = $rr->waitPayload()) { try { if ($used) { // kill on second attempt diff --git a/tools/process.go b/tools/process.go new file mode 100644 index 00000000..d92755d1 --- /dev/null +++ b/tools/process.go @@ -0,0 +1,44 @@ +package tools + +import ( + "github.com/shirou/gopsutil/process" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/worker" +) + +// ProcessState provides information about specific worker. +type ProcessState struct { + // Pid contains process id. + Pid int `json:"pid"` + + // Status of the worker. + Status string `json:"status"` + + // Number of worker executions. + NumJobs int64 `json:"numExecs"` + + // Created is unix nano timestamp of worker creation time. + Created int64 `json:"created"` + + // MemoryUsage holds the information about worker memory usage in bytes. + // Values might vary for different operating systems and based on RSS. + MemoryUsage uint64 `json:"memoryUsage"` +} + +// WorkerProcessState creates new worker state definition. +func WorkerProcessState(w worker.BaseProcess) (ProcessState, error) { + const op = errors.Op("worker_process state") + p, _ := process.NewProcess(int32(w.Pid())) + i, err := p.MemoryInfo() + if err != nil { + return ProcessState{}, errors.E(op, err) + } + + return ProcessState{ + Pid: int(w.Pid()), + Status: w.State().String(), + NumJobs: w.State().NumExecs(), + Created: w.Created().UnixNano(), + MemoryUsage: i.RSS, + }, nil +} diff --git a/cmd/util/table.go b/tools/worker_table.go index c0e20837..4aeb6ae7 100644 --- a/cmd/util/table.go +++ b/tools/worker_table.go @@ -1,17 +1,18 @@ -package util +package tools import ( - "github.com/dustin/go-humanize" - "github.com/olekukonko/tablewriter" - rrutil "github.com/spiral/roadrunner/util" - "os" + "io" "strconv" "time" + + "github.com/dustin/go-humanize" + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" ) // WorkerTable renders table with information about rr server workers. -func WorkerTable(workers []*rrutil.State) *tablewriter.Table { - tw := tablewriter.NewWriter(os.Stdout) +func WorkerTable(writer io.Writer, workers []ProcessState) *tablewriter.Table { + tw := tablewriter.NewWriter(writer) tw.SetHeader([]string{"PID", "Status", "Execs", "Memory", "Created"}) tw.SetColMinWidth(0, 7) tw.SetColMinWidth(1, 9) @@ -19,13 +20,13 @@ func WorkerTable(workers []*rrutil.State) *tablewriter.Table { tw.SetColMinWidth(3, 7) tw.SetColMinWidth(4, 18) - for _, w := range workers { + for key := range workers { tw.Append([]string{ - strconv.Itoa(w.Pid), - renderStatus(w.Status), - renderJobs(w.NumJobs), - humanize.Bytes(w.MemoryUsage), - renderAlive(time.Unix(0, w.Created)), + strconv.Itoa(workers[key].Pid), + renderStatus(workers[key].Status), + renderJobs(workers[key].NumJobs), + humanize.Bytes(workers[key].MemoryUsage), + renderAlive(time.Unix(0, workers[key].Created)), }) } @@ -35,24 +36,24 @@ func WorkerTable(workers []*rrutil.State) *tablewriter.Table { func renderStatus(status string) string { switch status { case "inactive": - return Sprintf("<yellow>inactive</reset>") + return color.YellowString("inactive") case "ready": - return Sprintf("<cyan>ready</reset>") + return color.CyanString("ready") case "working": - return Sprintf("<green>working</reset>") + return color.GreenString("working") case "invalid": - return Sprintf("<yellow>invalid</reset>") + return color.YellowString("invalid") case "stopped": - return Sprintf("<red>stopped</reset>") + return color.RedString("stopped") case "errored": - return Sprintf("<red>errored</reset>") + return color.RedString("errored") } return status } func renderJobs(number int64) string { - return humanize.Comma(int64(number)) + return humanize.Comma(number) } func renderAlive(t time.Time) string { diff --git a/util/doc.go b/util/doc.go new file mode 100755 index 00000000..a3798715 --- /dev/null +++ b/util/doc.go @@ -0,0 +1,5 @@ +package util + +/* +This package should not contain roadrunner dependencies, only system or third-party +*/ diff --git a/osutil/isolate.go b/util/isolate.go index 9eaf8a44..0ea1dff3 100644..100755 --- a/osutil/isolate.go +++ b/util/isolate.go @@ -1,6 +1,6 @@ // +build !windows -package osutil +package util import ( "fmt" @@ -9,6 +9,8 @@ import ( "os/user" "strconv" "syscall" + + "github.com/spiral/errors" ) // IsolateProcess change gpid for the process to avoid bypassing signals to php processes. @@ -18,19 +20,20 @@ func IsolateProcess(cmd *exec.Cmd) { // ExecuteFromUser may work only if run RR under root user func ExecuteFromUser(cmd *exec.Cmd, u string) error { + const op = errors.Op("execute from user") usr, err := user.Lookup(u) if err != nil { - return err + return errors.E(op, err) } - usrI32, err := strconv.Atoi(usr.Uid) + usrI32, err := strconv.ParseInt(usr.Uid, 10, 32) if err != nil { - return err + return errors.E(op, err) } - grI32, err := strconv.Atoi(usr.Gid) + grI32, err := strconv.ParseInt(usr.Gid, 10, 32) if err != nil { - return err + return errors.E(op, err) } // For more information: @@ -44,7 +47,7 @@ func ExecuteFromUser(cmd *exec.Cmd, u string) error { return fmt.Errorf("unable to test user namespaces due to permissions") } - return fmt.Errorf("failed to stat /proc/self/ns/user: %v", err) + return errors.E(op, errors.Errorf("failed to stat /proc/self/ns/user: %v", err)) } cmd.SysProcAttr.Credential = &syscall.Credential{ diff --git a/osutil/isolate_win.go b/util/isolate_win.go index 52fb5d8a..6756d59f 100644..100755 --- a/osutil/isolate_win.go +++ b/util/isolate_win.go @@ -1,6 +1,6 @@ // +build windows -package osutil +package util import ( "os/exec" @@ -14,4 +14,4 @@ func IsolateProcess(cmd *exec.Cmd) { func ExecuteFromUser(cmd *exec.Cmd, u string) error { return nil -}
\ No newline at end of file +} diff --git a/util/network.go b/util/network.go index d858cb0a..c2475f4a 100644..100755 --- a/util/network.go +++ b/util/network.go @@ -5,11 +5,12 @@ package util import ( "errors" "fmt" - "github.com/valyala/tcplisten" "net" "os" "strings" "syscall" + + "github.com/valyala/tcplisten" ) // CreateListener crates socket listener based on DSN definition. @@ -23,13 +24,19 @@ func CreateListener(address string) (net.Listener, error) { return nil, errors.New("invalid Protocol (tcp://:6001, unix://file.sock)") } - if dsn[0] == "unix" && fileExists(dsn[1]) { - err := syscall.Unlink(dsn[1]) - if err != nil { - return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + // create unix listener + if dsn[0] == "unix" { + // check if the file exist + if fileExists(dsn[1]) { + err := syscall.Unlink(dsn[1]) + if err != nil { + return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + } } + return net.Listen(dsn[0], dsn[1]) } + // configure and create tcp4 listener cfg := tcplisten.Config{ ReusePort: true, DeferAccept: true, @@ -37,12 +44,8 @@ func CreateListener(address string) (net.Listener, error) { Backlog: 0, } - // tcp4 is currently supported - if dsn[0] == "tcp" { - return cfg.NewListener("tcp4", dsn[1]) - } - - return net.Listen(dsn[0], dsn[1]) + // only tcp4 is currently supported + return cfg.NewListener("tcp4", dsn[1]) } // fileExists checks if a file exists and is not a directory before we diff --git a/util/network_test.go b/util/network_test.go index 09157ec0..1dc16e94 100644..100755 --- a/util/network_test.go +++ b/util/network_test.go @@ -3,8 +3,9 @@ package util import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestCreateListener(t *testing.T) { diff --git a/util/network_windows.go b/util/network_windows.go index 843d5779..843d5779 100644..100755 --- a/util/network_windows.go +++ b/util/network_windows.go diff --git a/util/network_windows_test.go b/util/network_windows_test.go index a5a8064e..b6648ed0 100644..100755 --- a/util/network_windows_test.go +++ b/util/network_windows_test.go @@ -13,4 +13,4 @@ func TestCreateListener(t *testing.T) { _, err = CreateListener("aaa://192.168.0.1") assert.Error(t, err, "Invalid Protocol (tcp://:6001, unix://file.sock)") -}
\ No newline at end of file +} diff --git a/util/state.go b/util/state.go deleted file mode 100644 index 29fca945..00000000 --- a/util/state.go +++ /dev/null @@ -1,62 +0,0 @@ -package util - -import ( - "errors" - "github.com/shirou/gopsutil/process" - "github.com/spiral/roadrunner" -) - -// State provides information about specific worker. -type State struct { - // Pid contains process id. - Pid int `json:"pid"` - - // Status of the worker. - Status string `json:"status"` - - // Number of worker executions. - NumJobs int64 `json:"numExecs"` - - // Created is unix nano timestamp of worker creation time. - Created int64 `json:"created"` - - // MemoryUsage holds the information about worker memory usage in bytes. - // Values might vary for different operating systems and based on RSS. - MemoryUsage uint64 `json:"memoryUsage"` -} - -// WorkerState creates new worker state definition. -func WorkerState(w *roadrunner.Worker) (*State, error) { - p, _ := process.NewProcess(int32(*w.Pid)) - i, err := p.MemoryInfo() - if err != nil { - return nil, err - } - - return &State{ - Pid: *w.Pid, - Status: w.State().String(), - NumJobs: w.State().NumExecs(), - Created: w.Created.UnixNano(), - MemoryUsage: i.RSS, - }, nil -} - -// ServerState returns list of all worker states of a given rr server. -func ServerState(rr *roadrunner.Server) ([]*State, error) { - if rr == nil { - return nil, errors.New("rr server is not running") - } - - result := make([]*State, 0) - for _, w := range rr.Workers() { - state, err := WorkerState(w) - if err != nil { - return nil, err - } - - result = append(result, state) - } - - return result, nil -} diff --git a/util/state_test.go b/util/state_test.go deleted file mode 100644 index 2afe682e..00000000 --- a/util/state_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package util - -import ( - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" - "runtime" - "testing" - "time" -) - -func TestServerState(t *testing.T) { - rr := roadrunner.NewServer( - &roadrunner.ServerConfig{ - Command: "php ../tests/client.php echo tcp", - Relay: "tcp://:9007", - RelayTimeout: 10 * time.Second, - Pool: &roadrunner.Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - assert.NoError(t, rr.Start()) - - state, err := ServerState(rr) - assert.NoError(t, err) - - assert.Len(t, state, runtime.NumCPU()) -} - -func TestServerState_Err(t *testing.T) { - _, err := ServerState(nil) - assert.Error(t, err) -} diff --git a/worker.go b/worker.go deleted file mode 100644 index 4fbf4369..00000000 --- a/worker.go +++ /dev/null @@ -1,258 +0,0 @@ -package roadrunner - -import ( - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" -) - -// Worker - supervised process with api over goridge.Relay. -type Worker struct { - // Pid of the process, points to Pid of underlying process and - // can be nil while process is not started. - Pid *int - - // Created indicates at what time worker has been created. - Created time.Time - - // state holds information about current worker state, - // number of worker executions, buf status change time. - // publicly this object is receive-only and protected using Mutex - // and atomic counter. - state *state - - // underlying command with associated process, command must be - // provided to worker from outside in non-started form. CmdSource - // stdErr direction will be handled by worker to aggregate error message. - cmd *exec.Cmd - - // err aggregates stderr output from underlying process. Value can be - // receive only once command is completed and all pipes are closed. - err *errBuffer - - // channel is being closed once command is complete. - waitDone chan interface{} - - // contains information about resulted process state. - endState *os.ProcessState - - // ensures than only one execution can be run at once. - mu sync.Mutex - - // communication bus with underlying process. - rl goridge.Relay -} - -// newWorker creates new worker over given exec.cmd. -func newWorker(cmd *exec.Cmd) (*Worker, error) { - if cmd.Process != nil { - return nil, fmt.Errorf("can't attach to running process") - } - - w := &Worker{ - Created: time.Now(), - cmd: cmd, - err: newErrBuffer(), - waitDone: make(chan interface{}), - state: newState(StateInactive), - } - - // piping all stderr to command errBuffer - w.cmd.Stderr = w.err - - return w, nil -} - -// State return receive-only worker state object, state can be used to safely access -// worker status, time when status changed and number of worker executions. -func (w *Worker) State() State { - return w.state -} - -// String returns worker description. -func (w *Worker) String() string { - state := w.state.String() - if w.Pid != nil { - state = state + ", pid:" + strconv.Itoa(*w.Pid) - } - - return fmt.Sprintf( - "(`%s` [%s], numExecs: %v)", - strings.Join(w.cmd.Args, " "), - state, - w.state.NumExecs(), - ) -} - -// Wait must be called once for each worker, call will be released once worker is -// complete and will return process error (if any), if stderr is presented it's value -// will be wrapped as WorkerError. Method will return error code if php process fails -// to find or start the script. -func (w *Worker) Wait() error { - <-w.waitDone - - // ensure that all receive/send operations are complete - w.mu.Lock() - defer w.mu.Unlock() - - if w.endState.Success() { - w.state.set(StateStopped) - return nil - } - - if w.state.Value() != StateStopping { - w.state.set(StateErrored) - } else { - w.state.set(StateStopped) - } - - if w.err.Len() != 0 { - return errors.New(w.err.String()) - } - - // generic process error - return &exec.ExitError{ProcessState: w.endState} -} - -// Stop sends soft termination command to the worker and waits for process completion. -func (w *Worker) Stop() error { - select { - case <-w.waitDone: - return nil - default: - w.mu.Lock() - defer w.mu.Unlock() - - w.state.set(StateStopping) - err := sendControl(w.rl, &stopCommand{Stop: true}) - - <-w.waitDone - return err - } -} - -// Kill kills underlying process, make sure to call Wait() func to gather -// error log from the stderr. Does not waits for process completion! -func (w *Worker) Kill() error { - select { - case <-w.waitDone: - return nil - default: - w.state.set(StateStopping) - err := w.cmd.Process.Signal(os.Kill) - - <-w.waitDone - return err - } -} - -// Exec sends payload to worker, executes it and returns result or -// error. Make sure to handle worker.Wait() to gather worker level -// errors. Method might return JobError indicating issue with payload. -func (w *Worker) Exec(rqs *Payload) (rsp *Payload, err error) { - w.mu.Lock() - - if rqs == nil { - w.mu.Unlock() - return nil, fmt.Errorf("payload can not be empty") - } - - if w.state.Value() != StateReady { - w.mu.Unlock() - return nil, fmt.Errorf("worker is not ready (%s)", w.state.String()) - } - - w.state.set(StateWorking) - - rsp, err = w.execPayload(rqs) - if err != nil { - if _, ok := err.(JobError); !ok { - w.state.set(StateErrored) - w.state.registerExec() - w.mu.Unlock() - return nil, err - } - } - - w.state.set(StateReady) - w.state.registerExec() - w.mu.Unlock() - return rsp, err -} - -func (w *Worker) markInvalid() { - w.state.set(StateInvalid) -} - -func (w *Worker) start() error { - if err := w.cmd.Start(); err != nil { - close(w.waitDone) - return err - } - - w.Pid = &w.cmd.Process.Pid - - // wait for process to complete - go func() { - w.endState, _ = w.cmd.Process.Wait() - if w.waitDone != nil { - close(w.waitDone) - w.mu.Lock() - defer w.mu.Unlock() - - if w.rl != nil { - err := w.rl.Close() - if err != nil { - w.err.lsn(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - } - - err := w.err.Close() - if err != nil { - w.err.lsn(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - } - }() - - return nil -} - -func (w *Worker) execPayload(rqs *Payload) (rsp *Payload, err error) { - // two things - if err := sendControl(w.rl, rqs.Context); err != nil { - return nil, errors.Wrap(err, "header error") - } - - if err = w.rl.Send(rqs.Body, 0); err != nil { - return nil, errors.Wrap(err, "sender error") - } - - var pr goridge.Prefix - rsp = new(Payload) - - if rsp.Context, pr, err = w.rl.Receive(); err != nil { - return nil, errors.Wrap(err, "worker error") - } - - if !pr.HasFlag(goridge.PayloadControl) { - return nil, fmt.Errorf("malformed worker response") - } - - if pr.HasFlag(goridge.PayloadError) { - return nil, JobError(rsp.Context) - } - - // add streaming support :) - if rsp.Body, _, err = w.rl.Receive(); err != nil { - return nil, errors.Wrap(err, "worker error") - } - - return rsp, nil -} diff --git a/worker_test.go b/worker_test.go deleted file mode 100644 index c21e67cb..00000000 --- a/worker_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "os/exec" - "testing" - "time" -) - -func Test_GetState(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, err := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - assert.Equal(t, StateStopped, w.State().Value()) - }() - - assert.NoError(t, err) - assert.NotNil(t, w) - - assert.Equal(t, StateReady, w.State().Value()) - err = w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } -} - -func Test_Kill(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, err := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.Error(t, w.Wait()) - assert.Equal(t, StateStopped, w.State().Value()) - }() - - assert.NoError(t, err) - assert.NotNil(t, w) - - assert.Equal(t, StateReady, w.State().Value()) - defer func() { - err := w.Kill() - if err != nil { - t.Errorf("error killing the worker: error %v", err) - } - }() -} - -func Test_Echo(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Nil(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_BadPayload(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(nil) - - assert.Error(t, err) - assert.Nil(t, res) - - assert.Equal(t, "payload can not be empty", err.Error()) -} - -func Test_NotStarted_String(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := newWorker(cmd) - assert.Contains(t, w.String(), "php tests/client.php echo pipes") - assert.Contains(t, w.String(), "inactive") - assert.Contains(t, w.String(), "numExecs: 0") -} - -func Test_NotStarted_Exec(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := newWorker(cmd) - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) - - assert.Equal(t, "worker is not ready (inactive)", err.Error()) -} - -func Test_String(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - assert.Contains(t, w.String(), "php tests/client.php echo pipes") - assert.Contains(t, w.String(), "ready") - assert.Contains(t, w.String(), "numExecs: 0") -} - -func Test_Echo_Slow(t *testing.T) { - cmd := exec.Command("php", "tests/slow-client.php", "echo", "pipes", "10", "10") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Nil(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_Broken(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "broken", "pipes") - - w, err := NewPipeFactory().SpawnWorker(cmd) - if err != nil { - t.Fatal(err) - } - - go func() { - err := w.Wait() - assert.Error(t, err) - assert.Contains(t, err.Error(), "undefined_function()") - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - assert.Nil(t, res) - assert.NotNil(t, err) - - time.Sleep(time.Second) - assert.NoError(t, w.Stop()) -} - -func Test_OnStarted(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "broken", "pipes") - assert.Nil(t, cmd.Start()) - - w, err := newWorker(cmd) - assert.Nil(t, w) - assert.NotNil(t, err) - - assert.Equal(t, "can't attach to running process", err.Error()) -} - -func Test_Error(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "error", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - assert.Nil(t, res) - assert.NotNil(t, err) - - assert.IsType(t, JobError{}, err) - assert.Equal(t, "hello", err.Error()) -} - -func Test_NumExecs(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - _, err := w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(1), w.State().NumExecs()) - - _, err = w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(2), w.State().NumExecs()) - - _, err = w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(3), w.State().NumExecs()) -} |