summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rwxr-xr-x[-rw-r--r--].github/ISSUE_TEMPLATE/bug_report.md0
-rwxr-xr-x[-rw-r--r--].github/ISSUE_TEMPLATE/feature_request.md0
-rw-r--r--.github/dependabot.yml12
-rw-r--r--.github/workflows/build.yml138
-rwxr-xr-x[-rw-r--r--].gitignore21
-rwxr-xr-x.golangci.yml62
-rwxr-xr-x[-rw-r--r--].rr.yaml264
-rw-r--r--CHANGELOG.md6
-rwxr-xr-x[-rw-r--r--]CODE_OF_CONDUCT.md0
-rwxr-xr-x[-rw-r--r--]LICENSE0
-rwxr-xr-xMakefile19
-rw-r--r--README.md49
-rwxr-xr-x[-rw-r--r--]bors.toml26
-rw-r--r--cmd/cli/reset.go104
-rw-r--r--cmd/cli/root.go97
-rw-r--r--cmd/cli/serve.go63
-rw-r--r--cmd/cli/workers.go118
-rw-r--r--cmd/main.go51
-rw-r--r--cmd/rr/LICENSE21
-rw-r--r--cmd/rr/cmd/root.go158
-rw-r--r--cmd/rr/cmd/serve.go63
-rw-r--r--cmd/rr/cmd/stop.go51
-rw-r--r--cmd/rr/cmd/version.go9
-rw-r--r--cmd/rr/http/debug.go138
-rw-r--r--cmd/rr/http/metrics.go123
-rw-r--r--cmd/rr/http/reset.go53
-rw-r--r--cmd/rr/http/workers.go100
-rw-r--r--cmd/rr/limit/debug.go71
-rw-r--r--cmd/rr/limit/metrics.go63
-rw-r--r--cmd/rr/main.go59
-rw-r--r--cmd/util/config.go181
-rw-r--r--cmd/util/cprint.go47
-rw-r--r--cmd/util/debug.go61
-rw-r--r--cmd/util/exit.go15
-rw-r--r--cmd/util/rpc.go18
-rw-r--r--codecov.yml2
-rw-r--r--composer.json44
-rw-r--r--config.go53
-rw-r--r--config_test.go51
-rw-r--r--controller.go16
-rw-r--r--controller_test.go216
-rw-r--r--error_buffer.go113
-rw-r--r--error_buffer_test.go131
-rw-r--r--errors.go24
-rw-r--r--errors_test.go17
-rw-r--r--factory.go13
-rwxr-xr-x[-rw-r--r--]go.mod47
-rwxr-xr-x[-rw-r--r--]go.sum388
-rw-r--r--interfaces/events/handler.go10
-rw-r--r--interfaces/events/pool_events.go65
-rw-r--r--interfaces/events/worker_events.go34
-rw-r--r--interfaces/pool/pool.go100
-rw-r--r--interfaces/worker/factory.go20
-rw-r--r--interfaces/worker/watcher.go26
-rw-r--r--interfaces/worker/worker.go59
-rwxr-xr-xinternal/protocol.go94
-rwxr-xr-xinternal/state.go122
-rwxr-xr-xinternal/state_test.go27
-rw-r--r--phpstan.neon.dist4
-rw-r--r--pipe_factory.go79
-rw-r--r--pipe_factory_test.go163
-rwxr-xr-xpkg/events/events.go38
-rwxr-xr-x[-rw-r--r--]pkg/payload/payload.go (renamed from payload.go)6
-rwxr-xr-xpkg/pipe/pipe_factory.go163
-rwxr-xr-xpkg/pipe/pipe_factory_test.go511
-rw-r--r--pkg/pool/config.go75
-rwxr-xr-xpkg/pool/static_pool.go324
-rwxr-xr-xpkg/pool/static_pool_test.go570
-rwxr-xr-xpkg/pool/supervisor_pool.go206
-rw-r--r--pkg/pool/supervisor_test.go155
-rwxr-xr-xpkg/socket/socket_factory.go229
-rwxr-xr-x[-rw-r--r--]pkg/socket/socket_factory_test.go (renamed from socket_factory_test.go)249
-rwxr-xr-xpkg/worker/sync_worker.go224
-rwxr-xr-xpkg/worker/sync_worker_test.go37
-rwxr-xr-xpkg/worker/worker.go315
-rwxr-xr-xpkg/worker/worker_test.go19
-rwxr-xr-xpkg/worker_watcher/worker_watcher.go302
-rw-r--r--pool.go39
-rw-r--r--protocol.go51
-rw-r--r--protocol_test.go46
-rw-r--r--server.go255
-rw-r--r--server_config.go168
-rw-r--r--server_config_test.go174
-rw-r--r--server_test.go253
-rw-r--r--service/container.go371
-rw-r--r--service/container_test.go533
-rw-r--r--service/entry.go59
-rw-r--r--service/entry_test.go16
-rw-r--r--service/env/config.go22
-rw-r--r--service/env/config_test.go41
-rw-r--r--service/env/environment.go23
-rw-r--r--service/env/service.go55
-rw-r--r--service/env/service_test.go80
-rw-r--r--service/gzip/config.go22
-rw-r--r--service/gzip/config_test.go46
-rw-r--r--service/gzip/service.go36
-rw-r--r--service/gzip/service_test.go72
-rw-r--r--service/headers/config.go41
-rw-r--r--service/headers/config_test.go30
-rw-r--r--service/headers/service.go113
-rw-r--r--service/headers/service_test.go340
-rw-r--r--service/health/config.go32
-rw-r--r--service/health/config_test.go46
-rw-r--r--service/health/service.go116
-rw-r--r--service/health/service_test.go317
-rw-r--r--service/http/attributes/attributes.go76
-rw-r--r--service/http/attributes/attributes_test.go79
-rw-r--r--service/http/config.go263
-rw-r--r--service/http/config_test.go331
-rw-r--r--service/http/constants.go6
-rw-r--r--service/http/errors.go25
-rw-r--r--service/http/errors_windows.go27
-rw-r--r--service/http/fcgi_test.go105
-rw-r--r--service/http/fixtures/server.crt15
-rw-r--r--service/http/fixtures/server.key9
-rw-r--r--service/http/h2c_test.go83
-rw-r--r--service/http/handler.go208
-rw-r--r--service/http/handler_test.go1967
-rw-r--r--service/http/parse.go147
-rw-r--r--service/http/parse_test.go52
-rw-r--r--service/http/request.go183
-rw-r--r--service/http/response.go107
-rw-r--r--service/http/response_test.go162
-rw-r--r--service/http/rpc.go34
-rw-r--r--service/http/rpc_test.go221
-rw-r--r--service/http/service.go427
-rw-r--r--service/http/service_test.go759
-rw-r--r--service/http/ssl_test.go254
-rw-r--r--service/http/uploads.go160
-rw-r--r--service/http/uploads_config.go45
-rw-r--r--service/http/uploads_config_test.go24
-rw-r--r--service/http/uploads_test.go434
-rw-r--r--service/limit/config.go48
-rw-r--r--service/limit/config_test.go51
-rw-r--r--service/limit/controller.go166
-rw-r--r--service/limit/service.go39
-rw-r--r--service/limit/service_test.go498
-rw-r--r--service/limit/state_filter.go58
-rw-r--r--service/metrics/config.go140
-rw-r--r--service/metrics/config_test.go76
-rw-r--r--service/metrics/rpc.go263
-rw-r--r--service/metrics/rpc_test.go861
-rw-r--r--service/metrics/service.go191
-rw-r--r--service/metrics/service_test.go247
-rw-r--r--service/reload/config.go72
-rw-r--r--service/reload/config_test.go63
-rw-r--r--service/reload/service.go163
-rw-r--r--service/reload/watcher.go372
-rw-r--r--service/reload/watcher_test.go578
-rw-r--r--service/rpc/config.go59
-rw-r--r--service/rpc/config_test.go163
-rw-r--r--service/rpc/service.go124
-rw-r--r--service/rpc/service_test.go96
-rw-r--r--service/rpc/system.go18
-rw-r--r--service/static/config.go82
-rw-r--r--service/static/config_test.go45
-rw-r--r--service/static/service.go87
-rw-r--r--service/static/service_test.go531
-rw-r--r--socket_factory.go139
-rw-r--r--src/Diactoros/ServerRequestFactory.php28
-rw-r--r--src/Diactoros/StreamFactory.php57
-rw-r--r--src/Diactoros/UploadedFileFactory.php36
-rw-r--r--src/Exception/MetricException.php17
-rw-r--r--src/Exception/RoadRunnerException.php14
-rw-r--r--src/Exceptions/RoadRunnerException.php18
-rw-r--r--src/HttpClient.php74
-rw-r--r--src/Metrics.php80
-rw-r--r--src/MetricsInterface.php64
-rw-r--r--src/PSR7Client.php217
-rw-r--r--src/Worker.php178
-rw-r--r--state.go98
-rw-r--r--state_test.go26
-rw-r--r--static_pool.go370
-rw-r--r--static_pool_test.go496
-rwxr-xr-x[-rw-r--r--]systemd/rr.service0
-rw-r--r--tests/broken.php4
-rw-r--r--tests/client.php2
-rw-r--r--tests/composer.json13
-rw-r--r--tests/delay.php4
-rw-r--r--tests/echo.php4
-rw-r--r--tests/error.php4
-rw-r--r--tests/head.php4
-rw-r--r--tests/http/client.php14
-rw-r--r--tests/http/slow-client.php18
-rw-r--r--tests/http/upload.php2
-rw-r--r--tests/memleak.php15
-rw-r--r--tests/pid.php2
-rw-r--r--tests/psr-worker-bench.php28
-rw-r--r--tests/psr-worker.php28
-rw-r--r--tests/sleep.php15
-rw-r--r--tests/slow-client.php2
-rw-r--r--tests/slow-destroy.php2
-rw-r--r--tests/slow-pid.php2
-rw-r--r--tests/stop.php2
-rw-r--r--tools/process.go44
-rw-r--r--tools/worker_table.go (renamed from cmd/util/table.go)41
-rwxr-xr-xutil/doc.go5
-rwxr-xr-x[-rw-r--r--]util/isolate.go (renamed from osutil/isolate.go)17
-rwxr-xr-x[-rw-r--r--]util/isolate_win.go (renamed from osutil/isolate_win.go)4
-rwxr-xr-x[-rw-r--r--]util/network.go25
-rwxr-xr-x[-rw-r--r--]util/network_test.go3
-rwxr-xr-x[-rw-r--r--]util/network_windows.go0
-rwxr-xr-x[-rw-r--r--]util/network_windows_test.go2
-rw-r--r--util/state.go62
-rw-r--r--util/state_test.go36
-rw-r--r--worker.go258
-rw-r--r--worker_test.go248
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
diff --git a/.rr.yaml b/.rr.yaml
index 24fcb63d..39199b86 100644..100755
--- a/.rr.yaml
+++ b/.rr.yaml
@@ -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
diff --git a/LICENSE b/LICENSE
index d968467f..d968467f 100644..100755
--- a/LICENSE
+++ b/LICENSE
diff --git a/Makefile b/Makefile
index 9ad158ba..1b903d6f 100755
--- a/Makefile
+++ b/Makefile
@@ -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 ./...
diff --git a/README.md b/README.md
index 4db5d856..bef2413b 100644
--- a/README.md
+++ b/README.md
@@ -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
-}
diff --git a/go.mod b/go.mod
index ccaff45e..cc86f14e 100644..100755
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index b91bd265..fb358b24 100644..100755
--- a/go.sum
+++ b/go.sum
@@ -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())
-}