summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValery Piashchynski <[email protected]>2020-10-13 13:55:20 +0300
committerValery Piashchynski <[email protected]>2020-10-13 13:55:20 +0300
commit0dc44d54cfcc9dd3fa09a41136f35a9a8d26b994 (patch)
treeffcb65010bebe9f5b5436192979e64b2402a6ec0
parent08d6b6b7f773f83b286cd48c1a0fbec9a62fb42b (diff)
Initial commit of RR 2.0v2.0.0-alpha1
-rw-r--r--.gitignore26
-rw-r--r--.golangci.yml49
-rw-r--r--.rr.yaml200
-rw-r--r--Dockerfile27
-rw-r--r--_old/.dockerignore (renamed from .dockerignore)0
-rw-r--r--_old/.gitignore8
-rw-r--r--_old/.rr.yaml193
-rw-r--r--_old/CHANGELOG.md (renamed from CHANGELOG.md)0
-rw-r--r--_old/LICENSE (renamed from cmd/rr/LICENSE)4
-rw-r--r--_old/Makefile (renamed from Makefile)0
-rw-r--r--_old/README.md (renamed from README.md)2
-rwxr-xr-x_old/build.sh (renamed from build.sh)0
-rw-r--r--_old/composer.json43
-rw-r--r--_old/container.go371
-rw-r--r--_old/container_test.go533
-rw-r--r--_old/entry.go59
-rw-r--r--_old/entry_test.go16
-rw-r--r--_old/go.mod28
-rw-r--r--_old/go.sum (renamed from go.sum)0
-rw-r--r--_old/phpstan.neon.dist (renamed from phpstan.neon.dist)0
-rwxr-xr-xbin/rr310
-rw-r--r--cmd/rr/cmd/root.go157
-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--cmd/util/table.go60
-rw-r--r--composer.json2
-rw-r--r--composer.lock1143
-rw-r--r--config.go53
-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.go12
-rw-r--r--errors_test.go5
-rw-r--r--factory.go15
-rw-r--r--go.mod25
-rw-r--r--payload.go4
-rw-r--r--pipe_factory.go182
-rw-r--r--pipe_factory_test.go135
-rw-r--r--plugins/config/provider.go15
-rw-r--r--plugins/config/tests/.rr.yaml28
-rw-r--r--plugins/config/tests/config_test.go67
-rw-r--r--plugins/config/tests/plugin1.go54
-rw-r--r--plugins/config/viper.go86
-rw-r--r--plugins/events/broadcaster.go24
-rw-r--r--plugins/factory/app.go133
-rw-r--r--plugins/factory/app_provider.go17
-rw-r--r--plugins/factory/factory.go67
-rw-r--r--plugins/factory/hello.php1
-rw-r--r--plugins/factory/tests/.rr.yaml9
-rw-r--r--plugins/factory/tests/factory_test.go85
-rw-r--r--plugins/factory/tests/hello.php1
-rw-r--r--plugins/factory/tests/plugin_1.go55
-rw-r--r--plugins/factory/tests/plugin_2.go88
-rw-r--r--pool.go117
-rw-r--r--pool_supervisor.go176
-rw-r--r--pool_test.go (renamed from config_test.go)4
-rw-r--r--process_state.go (renamed from util/state.go)36
-rw-r--r--process_state_test.go1
-rw-r--r--protocol.go13
-rw-r--r--protocol_test.go5
-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/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.go1961
-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.go500
-rw-r--r--service/limit/state_filter.go58
-rw-r--r--service/metrics/config.go136
-rw-r--r--service/metrics/config_test.go75
-rw-r--r--service/metrics/rpc.go260
-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/samefile.go9
-rw-r--r--service/reload/samefile_windows.go12
-rw-r--r--service/reload/service.go162
-rw-r--r--service/reload/service_test.go1
-rw-r--r--service/reload/watcher.go409
-rw-r--r--service/reload/watcher_test.go673
-rw-r--r--service/rpc/config.go60
-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.go237
-rw-r--r--socket_factory_test.go303
-rw-r--r--state.go43
-rw-r--r--state_test.go3
-rw-r--r--static_pool.go378
-rw-r--r--static_pool_test.go311
-rw-r--r--sync_worker.go171
-rw-r--r--sync_worker_test.go264
-rw-r--r--util/doc.go5
-rw-r--r--util/isolate.go (renamed from osutil/isolate.go)2
-rw-r--r--util/isolate_win.go (renamed from osutil/isolate_win.go)2
-rw-r--r--util/state_test.go36
-rw-r--r--worker.go459
-rw-r--r--worker_test.go279
-rw-r--r--workers_watcher.go323
166 files changed, 4621 insertions, 18185 deletions
diff --git a/.gitignore b/.gitignore
index 852b8881..a781518f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,18 @@
-.idea
-composer.lock
-vendor
-vendor_php
-builds/
-tests/vendor/
-.rr-sample.yaml
-psr-worker.php \ No newline at end of file
+# 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 \ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 00000000..1d9eb013
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,49 @@
+linters:
+ # please, do not use `enable-all`: it's deprecated and will be removed soon.
+ # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
+ disable-all: true
+ enable:
+ - bodyclose
+ - depguard
+ - dogsled
+# - dupl
+ - gochecknoinits
+ - goconst
+ - gocritic
+ - gocyclo
+ - gofmt
+ - goimports
+ # - golint
+ - goprintffuncname
+ # - gosec
+ # - gosimple
+ - govet
+ - ineffassign
+ - interfacer
+ - misspell
+ - nakedret
+ - 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 2d47a1d5..30d23695 100644
--- a/.rr.yaml
+++ b/.rr.yaml
@@ -1,193 +1,7 @@
-# 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
-
-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]
-
-# 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)
- interval: 1s
-
- # file extensions to watch, defaults to [.php]
- patterns: [".php"]
-
- # list of services to watch
- services:
- http:
- # list of dirs, "" root
- dirs: [""]
-
- # include sub directories
- recursive: true \ No newline at end of file
+app:
+ commmand: "php app.php"
+ user: user
+ group: group
+ env:
+ DEBUG: true
+ APP_KEY: "..." \ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index c857515f..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-FROM golang:1.14.3 as builder
-
-COPY . /src
-
-WORKDIR /src
-
-RUN set -x \
- && apt-get update -y \
- && apt-get install -y bash git \
- && go version \
- && bash ./build.sh \
- && test -f ./.rr.yaml
-
-FROM alpine:latest
-
-LABEL \
- org.opencontainers.image.title="roadrunner" \
- org.opencontainers.image.description="High-performance PHP application server, load-balancer and process manager" \
- org.opencontainers.image.url="https://github.com/spiral/roadrunner" \
- org.opencontainers.image.source="https://github.com/spiral/roadrunner" \
- org.opencontainers.image.vendor="SpiralScout" \
- org.opencontainers.image.licenses="MIT"
-
-COPY --from=builder /src/rr /usr/bin/rr
-COPY --from=builder /src/.rr.yaml /etc/rr.yaml
-
-ENTRYPOINT ["/usr/bin/rr"]
diff --git a/.dockerignore b/_old/.dockerignore
index b817b3c8..b817b3c8 100644
--- a/.dockerignore
+++ b/_old/.dockerignore
diff --git a/_old/.gitignore b/_old/.gitignore
new file mode 100644
index 00000000..e75a6fdb
--- /dev/null
+++ b/_old/.gitignore
@@ -0,0 +1,8 @@
+../.idea
+composer.lock
+vendor
+vendor_php
+builds/
+tests/vendor/
+.rr-sample.yaml
+psr-worker.php \ No newline at end of file
diff --git a/_old/.rr.yaml b/_old/.rr.yaml
new file mode 100644
index 00000000..6ee6fedb
--- /dev/null
+++ b/_old/.rr.yaml
@@ -0,0 +1,193 @@
+# 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
+
+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]
+
+# http plugins 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 plugins provides FastCGI as frontend
+ fcgi:
+ # FastCGI connection DSN. Supported TCP and Unix sockets.
+ address: tcp://0.0.0.0:6920
+
+ # HTTP plugins 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 plugins
+ 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 plugins configuration
+health:
+ # http host to serve health requests.
+ address: localhost:2113
+
+# reload can reset rr servers when files change
+reload:
+ # refresh internval (default 1s)
+ interval: 1s
+
+ # file extensions to watch, defaults to [.php]
+ patterns: [".php"]
+
+ # list of services to watch
+ services:
+ http:
+ # list of dirs, "" root
+ dirs: [""]
+
+ # include sub directories
+ recursive: true \ No newline at end of file
diff --git a/CHANGELOG.md b/_old/CHANGELOG.md
index 4c58de9b..4c58de9b 100644
--- a/CHANGELOG.md
+++ b/_old/CHANGELOG.md
diff --git a/cmd/rr/LICENSE b/_old/LICENSE
index efb98c87..d968467f 100644
--- a/cmd/rr/LICENSE
+++ b/_old/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2018 SpiralScout
+Copyright (c) 2020 Spiral Scout
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ 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
+SOFTWARE.
diff --git a/Makefile b/_old/Makefile
index d7e85f90..d7e85f90 100644
--- a/Makefile
+++ b/_old/Makefile
diff --git a/README.md b/_old/README.md
index 4db5d856..edf575ce 100644
--- a/README.md
+++ b/_old/README.md
@@ -115,4 +115,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).
diff --git a/build.sh b/_old/build.sh
index 77c13fff..77c13fff 100755
--- a/build.sh
+++ b/_old/build.sh
diff --git a/_old/composer.json b/_old/composer.json
new file mode 100644
index 00000000..6b817a2e
--- /dev/null
+++ b/_old/composer.json
@@ -0,0 +1,43 @@
+{
+ "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",
+ "ext-json": "*",
+ "ext-curl": "*",
+ "spiral/goridge": "^2.4.2",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0",
+ "symfony/console": "^2.5.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+ "laminas/laminas-diactoros": "^1.3 || ^2.0"
+ },
+ "config": {
+ "vendor-dir": "vendor_php"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "~0.12"
+ },
+ "scripts": {
+ "analyze": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi"
+ },
+ "autoload": {
+ "psr-4": {
+ "Spiral\\RoadRunner\\": "src/"
+ }
+ },
+ "bin": [
+ "bin/rr"
+ ]
+}
diff --git a/_old/container.go b/_old/container.go
deleted file mode 100644
index daab7339..00000000
--- a/_old/container.go
+++ /dev/null
@@ -1,371 +0,0 @@
-package _old
-
-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/_old/container_test.go b/_old/container_test.go
deleted file mode 100644
index 2f860c41..00000000
--- a/_old/container_test.go
+++ /dev/null
@@ -1,533 +0,0 @@
-package _old
-
-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/_old/entry.go b/_old/entry.go
deleted file mode 100644
index 0b2ad33e..00000000
--- a/_old/entry.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package _old
-
-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/_old/entry_test.go b/_old/entry_test.go
deleted file mode 100644
index 24538561..00000000
--- a/_old/entry_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package _old
-
-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/_old/go.mod b/_old/go.mod
new file mode 100644
index 00000000..0060343d
--- /dev/null
+++ b/_old/go.mod
@@ -0,0 +1,28 @@
+module github.com/spiral/roadrunner
+
+go 1.14
+
+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/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/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.5
+ 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
+)
diff --git a/go.sum b/_old/go.sum
index 9a487c66..9a487c66 100644
--- a/go.sum
+++ b/_old/go.sum
diff --git a/phpstan.neon.dist b/_old/phpstan.neon.dist
index c9ffb648..c9ffb648 100644
--- a/phpstan.neon.dist
+++ b/_old/phpstan.neon.dist
diff --git a/bin/rr b/bin/rr
deleted file mode 100755
index e3e2c1d0..00000000
--- a/bin/rr
+++ /dev/null
@@ -1,310 +0,0 @@
-#!/usr/bin/env php
-<?php
-/**
- * RoadRunner
- * High-performance PHP process supervisor and load balancer written in Go
- *
- * This file responsive for cli commands
- */
-declare(strict_types=1);
-
-foreach ([
- __DIR__ . '/../../../autoload.php',
- __DIR__ . '/../vendor/autoload.php',
- __DIR__ . '/vendor/autoload.php'
- ] as $file) {
- if (file_exists($file)) {
- define('RR_COMPOSER_INSTALL', $file);
-
- break;
- }
-}
-
-unset($file);
-
-if (!defined('RR_COMPOSER_INSTALL')) {
- fwrite(
- STDERR,
- 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
- ' composer install' . PHP_EOL . PHP_EOL .
- 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
- );
-
- die(1);
-}
-
-if (RRHelper::getOSType() !== 'linux' && !class_exists('ZipArchive')) {
- fwrite(STDERR, 'Extension `php-zip` is required.' . PHP_EOL);
- die(1);
-}
-
-if (!function_exists('curl_init')) {
- fwrite(STDERR, 'Extension `php-curl` is required.' . PHP_EOL);
- die(1);
-}
-
-require RR_COMPOSER_INSTALL;
-
-use Symfony\Component\Console\Application;
-use Symfony\Component\Console\Helper\ProgressBar;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
-
-class RRHelper
-{
- /**
- * Returns version of RoadRunner based on build.sh file
- *
- * @return string Version of RoadRunner
- * @throws Exception
- */
- public static function getVersion(): string
- {
- $file = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'build.sh';
- $fileResource = fopen($file, 'r') or die(1);
- while (!feof($fileResource)) {
- $line = fgets($fileResource, 4096);
- $matches = [];
- if (preg_match("/^RR_VERSION=(.*)/", $line, $matches)) {
- return trim($matches[1]);
- }
- }
- fclose($fileResource);
- throw new Exception("Can't find version of RoadRunner");
- }
-
- /**
- * Returns OS Type for filename
- *
- * @return string OS Type
- */
- public static function getOSType(): string
- {
- switch (PHP_OS) {
- case 'Darwin':
- return 'darwin';
- case 'Linux':
- return 'linux';
- case 'FreeBSD':
- return 'freebsd';
- case 'WIN32':
- case 'WINNT':
- case 'Windows':
- return 'windows';
- default:
- return 'linux';
- }
- }
-
- /**
- * @return string
- * @throws Exception
- */
- public static function getSignature(): string
- {
- return 'roadrunner-' . self::getVersion() . '-' . self::getOSType() . '-amd64';
- }
-
- /**
- * Returns generated URL to zip file on GitHub with binary file
- *
- * @return string URL
- * @throws Exception
- */
- public static function getBinaryDownloadUrl(): string
- {
- $ext = '.zip';
- if (self::getOSType() == 'linux') {
- $ext = '.tar.gz';
- }
-
- return 'https://github.com/spiral/roadrunner/releases/download/v'
- . static::getVersion() . '/' . self::getSignature()
- . $ext;
- }
-
- /**
- * Extracts the roadrunner RR binary into given location.
- *
- * @param string $archive
- * @param string $target
- * @throws Exception
- */
- public static function extractBinary(string $archive, string $target)
- {
- if (self::getOSType() !== 'linux') {
- self::extractZIP($archive, $target);
- } else {
- self::extractTAR($archive, $target);
- }
- }
-
- /**
- * @param string $archive
- * @param string $target
- * @throws Exception
- */
- protected static function extractZIP(string $archive, string $target)
- {
- $zip = new ZipArchive();
- $zip->open($archive);
-
- $name = self::getSignature() . '/rr';
- if (self::getOSType() == 'windows') {
- $name .= '.exe';
- }
-
- $stream = $zip->getStream($name);
- if (!is_resource($stream)) {
- return;
- }
-
- $to = fopen($target, 'w');
- stream_copy_to_stream($stream, $to);
- fclose($to);
-
- $zip->close();
- }
-
- /**
- * @param string $archive
- * @param string $target
- * @throws Exception
- */
- protected static function extractTAR(string $archive, string $target)
- {
- $arch = new PharData($archive);
- $arch->extractTo('./', self::getSignature() . '/rr');
-
- copy('./' . self::getSignature() . '/rr', $target);
- unlink('./' . self::getSignature() . '/rr');
- rmdir('./' . self::getSignature());
- }
-}
-
-(new Application('RoadRunner', RRHelper::getVersion()))
- ->register('get-binary')
- ->setDescription("Install or update RoadRunner binaries in specified folder (current folder by default)")
- ->addOption('location', 'l', InputArgument::OPTIONAL, 'destination folder', '.')
- ->setCode(function (InputInterface $input, OutputInterface $output) {
- $output->writeln('<info>Updating binary file of RoadRunner</info>');
-
- $finalFile = $input->getOption('location') . DIRECTORY_SEPARATOR . 'rr';
- if (RRHelper::getOSType() == 'windows') {
- $finalFile .= '.exe';
- }
-
- if (is_file($finalFile)) {
- $version = RRHelper::getVersion();
-
- $previousVersion = preg_match(
- '#Version:.+(\d+\.\d+\.\d+)#',
- (string)shell_exec($finalFile),
- $matches
- ) ? $matches[1] : "";
-
- $output->writeln('<error>RoadRunner binary file already exists!</error>');
- $helper = $this->getHelper('question');
-
- if (version_compare($previousVersion, $version) === 0) {
- $output->writeln(sprintf('<info>Current version: %s</info>', $previousVersion));
- $question = new ConfirmationQuestion(
- sprintf('Skip update to the same version: %s ? [Y/n]', $version)
- );
- if ($helper->ask($input, $output, $question)) {
- return;
- }
- } else {
- $question = new ConfirmationQuestion('Do you want overwrite it? [Y/n]');
- if (!$helper->ask($input, $output, $question)) {
- return;
- }
- }
- }
-
- $output->writeln('<info>Downloading RoadRunner archive for <fg=cyan>' . ucfirst(RRHelper::getOSType()) . '</fg=cyan></info>');
-
- $progressBar = new ProgressBar($output);
- $progressBar->setFormat('verbose');
-
- $zipFileName = 'rr_zip_'.random_int(0, 10000);
- if (RRHelper::getOSType() == 'linux') {
- $zipFileName .= '.tar.gz';
- }
-
- $zipFile = fopen($zipFileName, "w+");
- $curlResource = curl_init();
-
- curl_setopt($curlResource, CURLOPT_URL, RRHelper::getBinaryDownloadUrl());
- curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($curlResource, CURLOPT_BINARYTRANSFER, true);
- curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, false);
- curl_setopt($curlResource, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($curlResource, CURLOPT_FILE, $zipFile);
- curl_setopt($curlResource, CURLOPT_PROGRESSFUNCTION,
- function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use (&$progressBar, $output) {
- if ($download_size == 0) {
- return;
- }
-
- if ($progressBar->getStartTime() === 0) {
- $progressBar->start();
- }
-
- if ($progressBar->getMaxSteps() != $download_size) {
- /**
- * Workaround for symfony < 4.1.x, for example PHP 7.0 will use 3.x
- * feature #26449 Make ProgressBar::setMaxSteps public (ostrolucky)
- */
- $progressBar = new ProgressBar($output, $download_size);
- }
-
- $progressBar->setFormat('[%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% ' . intval($download_size / 1024) . 'KB');
- $progressBar->setProgress($downloaded);
- });
- curl_setopt($curlResource, CURLOPT_NOPROGRESS, false); // needed to make progress function work
- curl_setopt($curlResource, CURLOPT_HEADER, 0);
- curl_exec($curlResource);
- curl_close($curlResource);
- fclose($zipFile);
-
- $progressBar->finish();
- $output->writeln("");
-
- $output->writeln('<info>Unpacking <comment>' . basename(RRHelper::getBinaryDownloadUrl()) . '</comment></info>');
-
- RRHelper::extractBinary($zipFileName, $finalFile);
- unlink($zipFileName);
-
- if (!file_exists($finalFile) || filesize($finalFile) === 0) {
- throw new Exception('Unable to extract the file.');
- }
-
- chmod($finalFile, 0755);
- $output->writeln('<info>Binary file updated!</info>');
- })
- ->getApplication()
- ->register("init-config")
- ->setDescription("Inits default .rr.yaml config in specified folder (current folder by default)")
- ->addOption('location', 'l', InputArgument::OPTIONAL, 'destination folder', '.')
- ->setCode(function (InputInterface $input, OutputInterface $output) {
- if (is_file($input->getOption('location') . DIRECTORY_SEPARATOR . '.rr.yaml')) {
- $output->writeln('<error>Config file already exists!</error>');
- $helper = $this->getHelper('question');
- $question = new ConfirmationQuestion('Do you want overwrite it? [Y/n] ');
-
- if (!$helper->ask($input, $output, $question)) {
- return;
- }
- }
-
- copy(
- __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '.rr.yaml',
- $input->getOption('location') . DIRECTORY_SEPARATOR . '.rr.yaml'
- );
- $output->writeln('<info>Config file created!</info>');
- })
- ->getApplication()
- ->run();
diff --git a/cmd/rr/cmd/root.go b/cmd/rr/cmd/root.go
deleted file mode 100644
index 456cfc6a..00000000
--- a/cmd/rr/cmd/root.go
+++ /dev/null
@@ -1,157 +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,
- 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/cmd/util/table.go b/cmd/util/table.go
deleted file mode 100644
index c0e20837..00000000
--- a/cmd/util/table.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package util
-
-import (
- "github.com/dustin/go-humanize"
- "github.com/olekukonko/tablewriter"
- rrutil "github.com/spiral/roadrunner/util"
- "os"
- "strconv"
- "time"
-)
-
-// WorkerTable renders table with information about rr server workers.
-func WorkerTable(workers []*rrutil.State) *tablewriter.Table {
- tw := tablewriter.NewWriter(os.Stdout)
- tw.SetHeader([]string{"PID", "Status", "Execs", "Memory", "Created"})
- tw.SetColMinWidth(0, 7)
- tw.SetColMinWidth(1, 9)
- tw.SetColMinWidth(2, 7)
- tw.SetColMinWidth(3, 7)
- tw.SetColMinWidth(4, 18)
-
- for _, w := 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)),
- })
- }
-
- return tw
-}
-
-func renderStatus(status string) string {
- switch status {
- case "inactive":
- return Sprintf("<yellow>inactive</reset>")
- case "ready":
- return Sprintf("<cyan>ready</reset>")
- case "working":
- return Sprintf("<green>working</reset>")
- case "invalid":
- return Sprintf("<yellow>invalid</reset>")
- case "stopped":
- return Sprintf("<red>stopped</reset>")
- case "errored":
- return Sprintf("<red>errored</reset>")
- }
-
- return status
-}
-
-func renderJobs(number int64) string {
- return humanize.Comma(int64(number))
-}
-
-func renderAlive(t time.Time) string {
- return humanize.RelTime(t, time.Now(), "ago", "")
-}
diff --git a/composer.json b/composer.json
index 6b817a2e..283eaab1 100644
--- a/composer.json
+++ b/composer.json
@@ -40,4 +40,4 @@
"bin": [
"bin/rr"
]
-}
+} \ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 00000000..7a1094b1
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1143 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "439018483d4d3a37c3d369d2587b8311",
+ "packages": [
+ {
+ "name": "laminas/laminas-diactoros",
+ "version": "2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-diactoros.git",
+ "reference": "36ef09b73e884135d2059cc498c938e90821bb57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/36ef09b73e884135d2059cc498c938e90821bb57",
+ "reference": "36ef09b73e884135d2059cc498c938e90821bb57",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-zendframework-bridge": "^1.0",
+ "php": "^7.1",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "conflict": {
+ "phpspec/prophecy": "<1.9.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "replace": {
+ "zendframework/zend-diactoros": "^2.2.1"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "ext-dom": "*",
+ "ext-gd": "*",
+ "ext-libxml": "*",
+ "http-interop/http-factory-tests": "^0.5.0",
+ "laminas/laminas-coding-standard": "~1.0.0",
+ "php-http/psr7-integration-tests": "^1.0",
+ "phpunit/phpunit": "^7.5.18"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "config-provider": "Laminas\\Diactoros\\ConfigProvider",
+ "module": "Laminas\\Diactoros"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions/create_uploaded_file.php",
+ "src/functions/marshal_headers_from_sapi.php",
+ "src/functions/marshal_method_from_sapi.php",
+ "src/functions/marshal_protocol_version_from_sapi.php",
+ "src/functions/marshal_uri_from_sapi.php",
+ "src/functions/normalize_server.php",
+ "src/functions/normalize_uploaded_files.php",
+ "src/functions/parse_cookie_header.php",
+ "src/functions/create_uploaded_file.legacy.php",
+ "src/functions/marshal_headers_from_sapi.legacy.php",
+ "src/functions/marshal_method_from_sapi.legacy.php",
+ "src/functions/marshal_protocol_version_from_sapi.legacy.php",
+ "src/functions/marshal_uri_from_sapi.legacy.php",
+ "src/functions/normalize_server.legacy.php",
+ "src/functions/normalize_uploaded_files.legacy.php",
+ "src/functions/parse_cookie_header.legacy.php"
+ ],
+ "psr-4": {
+ "Laminas\\Diactoros\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "PSR HTTP Message implementations",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "http",
+ "laminas",
+ "psr",
+ "psr-17",
+ "psr-7"
+ ],
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2020-09-03T14:29:41+00:00"
+ },
+ {
+ "name": "laminas/laminas-zendframework-bridge",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "module": "Laminas\\ZendFrameworkBridge"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Laminas\\ZendFrameworkBridge\\": "src//"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+ "keywords": [
+ "ZendFramework",
+ "autoloading",
+ "laminas",
+ "zf"
+ ],
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2020-09-14T14:23:00+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+ "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "time": "2017-02-14T16:28:37+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2019-04-30T12:38:16+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "spiral/goridge",
+ "version": "v2.4.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spiral/goridge-php.git",
+ "reference": "a7373de7f86a5452f8ad61bd1340dc158626f7f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spiral/goridge-php/zipball/a7373de7f86a5452f8ad61bd1340dc158626f7f8",
+ "reference": "a7373de7f86a5452f8ad61bd1340dc158626f7f8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.23",
+ "phpunit/phpunit": "~8.0",
+ "spiral/code-style": "^1.0"
+ },
+ "type": "goridge",
+ "autoload": {
+ "psr-4": {
+ "Spiral\\Goridge\\": "src/"
+ },
+ "files": [
+ "src/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Anton Titov / Wolfy-J",
+ "email": "[email protected]"
+ }
+ ],
+ "description": "High-performance PHP-to-Golang RPC bridge",
+ "time": "2020-08-14T14:28:30+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v5.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/04c3a31fe8ea94b42c9e2d1acc93d19782133b00",
+ "reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/polyfill-php80": "^1.15",
+ "symfony/service-contracts": "^1.1|^2",
+ "symfony/string": "^5.1"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<4.4",
+ "symfony/dotenv": "<5.1",
+ "symfony/event-dispatcher": "<4.4",
+ "symfony/lock": "<4.4",
+ "symfony/process": "<4.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "^4.4|^5.0",
+ "symfony/dependency-injection": "^4.4|^5.0",
+ "symfony/event-dispatcher": "^4.4|^5.0",
+ "symfony/lock": "^4.4|^5.0",
+ "symfony/process": "^4.4|^5.0",
+ "symfony/var-dumper": "^4.4|^5.0"
+ },
+ "suggest": {
+ "psr/log": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/lock": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Console Component",
+ "homepage": "https://symfony.com",
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-09-18T14:27:32+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
+ "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5",
+ "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
+ "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a",
+ "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
+ "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981",
+ "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.18-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-07-14T12:35:20+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1",
+ "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.0"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.2-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-09-07T11:33:47+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v5.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/4a9afe9d07bac506f75bcee8ed3ce76da5a9343e",
+ "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "~1.15"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^4.4|^5.0",
+ "symfony/http-client": "^4.4|^5.0",
+ "symfony/translation-contracts": "^1.1|^2",
+ "symfony/var-exporter": "^4.4|^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "files": [
+ "Resources/functions.php"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "[email protected]"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony String component",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-09-15T12:23:47+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "phpstan/phpstan",
+ "version": "0.12.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "9419738e20f0c49757be05d22969c1c44c1dff3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9419738e20f0c49757be05d22969c1c44c1dff3b",
+ "reference": "9419738e20f0c49757be05d22969c1c44c1dff3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.12-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/phpstan",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-09-28T09:48:55+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "^7.2",
+ "ext-json": "*",
+ "ext-curl": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "1.1.0"
+}
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/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
index db995721..b9746702 100644
--- a/errors.go
+++ b/errors.go
@@ -1,18 +1,18 @@
package roadrunner
-// JobError is job level error (no worker halt), wraps at top
+// TaskError is job level error (no WorkerProcess halt), wraps at top
// of error context
-type JobError []byte
+type TaskError []byte
// Error converts error context to string
-func (je JobError) Error() string {
- return string(je)
+func (te TaskError) Error() string {
+ return string(te)
}
-// WorkerError is worker related error
+// WorkerError is WorkerProcess related error
type WorkerError struct {
// Worker
- Worker *Worker
+ Worker WorkerBase
// Caused error
Caused error
diff --git a/errors_test.go b/errors_test.go
index 6bb650af..69f1c9ec 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -2,12 +2,13 @@ package roadrunner
import (
"errors"
- "github.com/stretchr/testify/assert"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func Test_JobError_Error(t *testing.T) {
- e := JobError([]byte("error"))
+ e := TaskError([]byte("error"))
assert.Equal(t, "error", e.Error())
}
diff --git a/factory.go b/factory.go
index 3c304824..482d39f8 100644
--- a/factory.go
+++ b/factory.go
@@ -1,13 +1,18 @@
package roadrunner
-import "os/exec"
+import (
+ "context"
+ "os/exec"
+)
-// Factory is responsible of wrapping given command into tasks worker.
+// Factory is responsible of wrapping given command into tasks WorkerProcess.
type Factory interface {
- // SpawnWorker creates new worker process based on given command.
+ // SpawnWorker creates new WorkerProcess process based on given command.
// Process must not be started.
- SpawnWorker(cmd *exec.Cmd) (w *Worker, err error)
+ SpawnWorkerWithContext(context.Context, *exec.Cmd) (WorkerBase, error)
+
+ SpawnWorker(*exec.Cmd) (WorkerBase, error)
// Close the factory and underlying connections.
- Close() error
+ Close(ctx context.Context) error
}
diff --git a/go.mod b/go.mod
index 0060343d..5d3fccb3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,28 +1,21 @@
-module github.com/spiral/roadrunner
+module github.com/roadrunner/v2
-go 1.14
+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/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/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/shirou/gopsutil v2.20.9+incompatible
github.com/spf13/viper v1.7.1
+ github.com/spiral/endure v1.0.0-beta8
github.com/spiral/goridge/v2 v2.4.5
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
+ go.uber.org/multierr v1.6.0
+ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
+ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+ gopkg.in/yaml.v2 v2.2.5 // indirect
)
diff --git a/payload.go b/payload.go
index 154cec95..502dfadd 100644
--- a/payload.go
+++ b/payload.go
@@ -1,12 +1,12 @@
package roadrunner
-// 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/pipe_factory.go b/pipe_factory.go
index 9696a474..a6c94614 100644
--- a/pipe_factory.go
+++ b/pipe_factory.go
@@ -1,78 +1,190 @@
package roadrunner
import (
+ "context"
"fmt"
+ "os/exec"
+ "strings"
+
"github.com/pkg/errors"
"github.com/spiral/goridge/v2"
- "io"
- "os/exec"
)
-// PipeFactory connects to workers using standard
+// PipeFactory connects to stack using standard
// streams (STDIN, STDOUT pipes).
type PipeFactory struct {
}
// NewPipeFactory returns new factory instance and starts
// listening
+
+// todo: review tests
func NewPipeFactory() *PipeFactory {
return &PipeFactory{}
}
-// SpawnWorker creates new worker and connects it to goridge relay,
+type SpawnResult struct {
+ w WorkerBase
+ err error
+}
+
+// SpawnWorker creates new WorkerProcess 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
+func (f *PipeFactory) SpawnWorkerWithContext(ctx context.Context, cmd *exec.Cmd) (WorkerBase, error) {
+ c := make(chan SpawnResult)
+ go func() {
+ w, err := InitBaseWorker(cmd)
+ if err != nil {
+ c <- SpawnResult{
+ w: nil,
+ err: err,
+ }
+ return
+ }
+
+ // TODO why out is in?
+ in, err := cmd.StdoutPipe()
+ if err != nil {
+ c <- SpawnResult{
+ w: nil,
+ err: err,
+ }
+ return
+ }
+
+ // TODO why in is out?
+ out, err := cmd.StdinPipe()
+ if err != nil {
+ c <- SpawnResult{
+ w: nil,
+ err: err,
+ }
+ return
+ }
+
+ // Init new PIPE relay
+ relay := goridge.NewPipeRelay(in, out)
+ w.AttachRelay(relay)
+
+ // Start the worker
+ err = w.Start()
+ if err != nil {
+ c <- SpawnResult{
+ w: nil,
+ err: errors.Wrap(err, "process error"),
+ }
+ return
+ }
+
+ // errors bundle
+ var errs []string
+ if pid, errF := fetchPID(relay); pid != w.Pid() {
+ if errF != nil {
+ errs = append(errs, errF.Error())
+ }
+
+ // todo kill timeout
+ errK := w.Kill(ctx)
+ if errK != nil {
+ errs = append(errs, fmt.Errorf("error killing the worker with PID number %d, Created: %s", w.Pid(), w.Created()).Error())
+ }
+
+ if wErr := w.Wait(ctx); wErr != nil {
+ errs = append(errs, wErr.Error())
+ }
+
+ if len(errs) > 0 {
+ c <- SpawnResult{
+ w: nil,
+ err: errors.New(strings.Join(errs, " : ")),
+ }
+ } else {
+ c <- SpawnResult{
+ w: nil,
+ err: nil,
+ }
+ }
+
+ return
+ }
+
+ // everything ok, set ready state
+ w.State().Set(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
}
+}
- var (
- in io.ReadCloser
- out io.WriteCloser
- )
+func (f *PipeFactory) SpawnWorker(cmd *exec.Cmd) (WorkerBase, error) {
+ w, err := InitBaseWorker(cmd)
+ if err != nil {
+ return nil, err
+ }
- if in, err = cmd.StdoutPipe(); err != nil {
+ // TODO why out is in?
+ in, err := cmd.StdoutPipe()
+ if err != nil {
return nil, err
}
- if out, err = cmd.StdinPipe(); err != nil {
+ // TODO why in is out?
+ out, err := cmd.StdinPipe()
+ if err != nil {
return nil, err
}
- w.rl = goridge.NewPipeRelay(in, out)
+ // Init new PIPE relay
+ relay := goridge.NewPipeRelay(in, out)
+ w.AttachRelay(relay)
- if err := w.start(); err != nil {
+ // Start the worker
+ err = w.Start()
+ if 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.Println(fmt.Sprintf("error killing the worker with PID number %d, Created: %s", w.Pid, w.Created))
- }
- }(w)
+ // errors bundle
+ var errs []string
+ if pid, errF := fetchPID(relay); pid != w.Pid() {
+ if errF != nil {
+ errs = append(errs, errF.Error())
+ }
- 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
- }
+ // todo kill timeout ??
+ errK := w.Kill(context.Background())
+ if errK != nil {
+ errs = append(errs, fmt.Errorf("error killing the worker with PID number %d, Created: %s", w.Pid(), w.Created()).Error())
}
- return nil, errors.Wrap(err, "unable to connect to worker")
+ if wErr := w.Wait(context.Background()); wErr != nil {
+ errs = append(errs, wErr.Error())
+ }
+
+ if len(errs) > 0 {
+ return nil, errors.New(strings.Join(errs, "/"))
+ }
}
- w.state.set(StateReady)
+ // everything ok, set ready state
+ w.State().Set(StateReady)
return w, nil
}
// Close the factory.
-func (f *PipeFactory) Close() error {
+func (f *PipeFactory) Close(ctx context.Context) error {
return nil
}
diff --git a/pipe_factory_test.go b/pipe_factory_test.go
index 14cf1272..93d9ccd8 100644
--- a/pipe_factory_test.go
+++ b/pipe_factory_test.go
@@ -1,24 +1,28 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
+ "context"
"os/exec"
"testing"
"time"
+
+ "github.com/stretchr/testify/assert"
)
func Test_Pipe_Start(t *testing.T) {
+ ctx := context.Background()
cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
assert.NoError(t, err)
assert.NotNil(t, w)
go func() {
- assert.NoError(t, w.Wait())
+ ctx := context.Background()
+ assert.NoError(t, w.Wait(ctx))
}()
- assert.NoError(t, w.Stop())
+ assert.NoError(t, w.Stop(ctx))
}
func Test_Pipe_StartError(t *testing.T) {
@@ -28,7 +32,8 @@ func Test_Pipe_StartError(t *testing.T) {
t.Errorf("error running the command: error %v", err)
}
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
assert.Error(t, err)
assert.Nil(t, w)
}
@@ -40,7 +45,8 @@ func Test_Pipe_PipeError(t *testing.T) {
t.Errorf("error creating the STDIN pipe: error %v", err)
}
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
assert.Error(t, err)
assert.Nil(t, w)
}
@@ -52,14 +58,16 @@ func Test_Pipe_PipeError2(t *testing.T) {
t.Errorf("error creating the STDIN pipe: error %v", err)
}
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, 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)
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
assert.Nil(t, w)
assert.Error(t, err)
@@ -68,27 +76,32 @@ func Test_Pipe_Failboot(t *testing.T) {
func Test_Pipe_Invalid(t *testing.T) {
cmd := exec.Command("php", "tests/invalid.php")
-
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(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")
-
- w, _ := NewPipeFactory().SpawnWorker(cmd)
- go func() {
- assert.NoError(t, w.Wait())
- }()
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
defer func() {
- err := w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
}
}()
- res, err := w.Exec(&Payload{Body: []byte("hello")})
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := sw.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -100,38 +113,41 @@ func Test_Pipe_Echo(t *testing.T) {
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()")
- }()
+ ctx := context.Background()
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
defer func() {
time.Sleep(time.Second)
- err := w.Stop()
- assert.NoError(t, err)
+ err = w.Stop(ctx)
+ assert.Error(t, err)
}()
- res, err := w.Exec(&Payload{Body: []byte("hello")})
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := sw.Exec(ctx, Payload{Body: []byte("hello")})
assert.Error(t, err)
- assert.Nil(t, res)
+ 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.SpawnWorker(cmd)
+ w, _ := f.SpawnWorkerWithContext(context.Background(), cmd)
go func() {
- if w.Wait() != nil {
+ if w.Wait(context.Background()) != nil {
b.Fail()
}
}()
- err := w.Stop()
+ err := w.Stop(context.Background())
if err != nil {
b.Errorf("error stopping the worker: error %v", err)
}
@@ -141,22 +157,67 @@ func Benchmark_Pipe_SpawnWorker_Stop(b *testing.B) {
func Benchmark_Pipe_Worker_ExecEcho(b *testing.B) {
cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
- w, _ := NewPipeFactory().SpawnWorker(cmd)
+ w, _ := NewPipeFactory().SpawnWorkerWithContext(context.Background(), cmd)
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
go func() {
- err := w.Wait()
+ err := w.Wait(context.Background())
if err != nil {
b.Errorf("error waiting the worker: error %v", err)
}
}()
defer func() {
- err := w.Stop()
+ err := w.Stop(context.Background())
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 {
+ if _, err := sw.Exec(context.Background(), 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().SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ //go func() {
+ // for {
+ // select {
+ // case event := <-w.Events():
+ // b.Fatal(event)
+ // }
+ // }
+ // //err := w.Wait()
+ // //if err != nil {
+ // // b.Errorf("error waiting the WorkerProcess: error %v", err)
+ // //}
+ //}()
+ defer func() {
+ err = w.Stop(ctx)
+ if err != nil {
+ b.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ for n := 0; n < b.N; n++ {
+ if _, err := sw.Exec(ctx, Payload{Body: []byte("hello")}); err != nil {
b.Fail()
}
}
diff --git a/plugins/config/provider.go b/plugins/config/provider.go
new file mode 100644
index 00000000..bec417e9
--- /dev/null
+++ b/plugins/config/provider.go
@@ -0,0 +1,15 @@
+package config
+
+type Provider interface {
+ // Unmarshal configuration section into configuration object.
+ //
+ // func (h *HttpService) Init(cp config.Provider) error {
+ // h.config := &HttpConfig{}
+ // if err := configProvider.UnmarshalKey("http", h.config); err != nil {
+ // return err
+ // }
+ // }
+ UnmarshalKey(name string, out interface{}) error
+ // Get used to get config section
+ Get(name string) interface{}
+}
diff --git a/plugins/config/tests/.rr.yaml b/plugins/config/tests/.rr.yaml
new file mode 100644
index 00000000..df9077d0
--- /dev/null
+++ b/plugins/config/tests/.rr.yaml
@@ -0,0 +1,28 @@
+reload:
+ # enable or disable file watcher
+ enabled: true
+ # sync interval
+ interval: 1s
+ # global patterns to sync
+ patterns: [".php"]
+ # list of included for sync services
+ services:
+ http:
+ # 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: ["."]
+ jobs:
+ recursive: false
+ ignore: ["service/metrics"]
+ dirs: ["./jobs"]
+ rpc:
+ recursive: true
+ patterns: [".json"]
+ # to include all project directories from workdir, leave `dirs` empty or add a dot "."
+ dirs: [""]
diff --git a/plugins/config/tests/config_test.go b/plugins/config/tests/config_test.go
new file mode 100644
index 00000000..baeafbd2
--- /dev/null
+++ b/plugins/config/tests/config_test.go
@@ -0,0 +1,67 @@
+package tests
+
+import (
+ "os"
+ "os/signal"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ "github.com/stretchr/testify/assert"
+ "github.com/temporalio/roadrunner-temporal/config"
+)
+
+func TestViperProvider_Init(t *testing.T) {
+ container, err := endure.NewContainer(endure.DebugLevel, endure.RetryOnFail(true))
+ if err != nil {
+ t.Fatal(err)
+ }
+ vp := &config.ViperProvider{}
+ vp.Path = ".rr.yaml"
+ vp.Prefix = "rr"
+ err = container.Register(vp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&Foo{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ errCh, err := container.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // stop by CTRL+C
+ c := make(chan os.Signal)
+ signal.Notify(c, os.Interrupt)
+
+ tt := time.NewTicker(time.Second * 2)
+
+ for {
+ select {
+ case e := <-errCh:
+ assert.NoError(t, e.Error.Err)
+ assert.NoError(t, container.Stop())
+ return
+ case <-c:
+ er := container.Stop()
+ if er != nil {
+ panic(er)
+ }
+ return
+ case <-tt.C:
+ tt.Stop()
+ assert.NoError(t, container.Stop())
+ return
+ }
+ }
+
+}
diff --git a/plugins/config/tests/plugin1.go b/plugins/config/tests/plugin1.go
new file mode 100644
index 00000000..4e7a5317
--- /dev/null
+++ b/plugins/config/tests/plugin1.go
@@ -0,0 +1,54 @@
+package tests
+
+import (
+ "errors"
+ "time"
+
+ "github.com/temporalio/roadrunner-temporal/config"
+)
+
+// ReloadConfig is a Reload configuration point.
+type ReloadConfig struct {
+ Interval time.Duration
+ Patterns []string
+ Services map[string]ServiceConfig
+}
+
+type ServiceConfig struct {
+ Enabled bool
+ Recursive bool
+ Patterns []string
+ Dirs []string
+ Ignore []string
+}
+
+type Foo struct {
+ configProvider config.Provider
+}
+
+
+// Depends on S2 and DB (S3 in the current case)
+func (f *Foo) Init(p config.Provider) error {
+ f.configProvider = p
+ return nil
+}
+
+func (f *Foo) Serve() chan error {
+ errCh := make(chan error, 1)
+
+ r := &ReloadConfig{}
+ err := f.configProvider.UnmarshalKey("reload", r)
+ if err != nil {
+ errCh <- err
+ }
+
+ if len(r.Patterns) == 0 {
+ errCh <- errors.New("should be at least one pattern, but got 0")
+ }
+
+ return errCh
+}
+
+func (f *Foo) Stop() error {
+ return nil
+}
diff --git a/plugins/config/viper.go b/plugins/config/viper.go
new file mode 100644
index 00000000..0362e79b
--- /dev/null
+++ b/plugins/config/viper.go
@@ -0,0 +1,86 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/spf13/viper"
+)
+
+type ViperProvider struct {
+ viper *viper.Viper
+ Path string
+ Prefix string
+}
+
+//////// ENDURE //////////
+func (v *ViperProvider) Init() error {
+ v.viper = viper.New()
+ // read in environment variables that match
+ v.viper.AutomaticEnv()
+ if v.Prefix == "" {
+ return errors.New("prefix should be set")
+ }
+ v.viper.SetEnvPrefix(v.Prefix)
+ if v.Path == "" {
+ return errors.New("path should be set")
+ }
+ v.viper.SetConfigFile(v.Path)
+ v.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ return v.viper.ReadInConfig()
+}
+
+///////////// VIPER ///////////////
+
+// Overwrite overwrites existing config with provided values
+func (v *ViperProvider) Overwrite(values map[string]string) error {
+ if len(values) != 0 {
+ for _, flag := range values {
+ key, value, err := parseFlag(flag)
+ if err != nil {
+ return err
+ }
+ v.viper.Set(key, value)
+ }
+ }
+
+ return nil
+}
+
+//
+func (v *ViperProvider) UnmarshalKey(name string, out interface{}) error {
+ err := v.viper.UnmarshalKey(name, &out)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Get raw config in a form of config section.
+func (v *ViperProvider) Get(name string) interface{} {
+ return v.viper.Get(name)
+}
+
+/////////// PRIVATE //////////////
+
+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
+}
diff --git a/plugins/events/broadcaster.go b/plugins/events/broadcaster.go
new file mode 100644
index 00000000..778b307d
--- /dev/null
+++ b/plugins/events/broadcaster.go
@@ -0,0 +1,24 @@
+package events
+
+type EventListener interface {
+ Handle(event interface{})
+}
+
+type EventBroadcaster struct {
+ listeners []EventListener
+}
+
+func NewEventBroadcaster() *EventBroadcaster {
+ return &EventBroadcaster{}
+}
+
+func (eb *EventBroadcaster) AddListener(l EventListener) {
+ // todo: threadcase
+ eb.listeners = append(eb.listeners, l)
+}
+
+func (eb *EventBroadcaster) Push(e interface{}) {
+ for _, l := range eb.listeners {
+ l.Handle(e)
+ }
+}
diff --git a/plugins/factory/app.go b/plugins/factory/app.go
new file mode 100644
index 00000000..8ed65531
--- /dev/null
+++ b/plugins/factory/app.go
@@ -0,0 +1,133 @@
+package factory
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/temporalio/roadrunner-temporal/config"
+ "github.com/temporalio/roadrunner-temporal/roadrunner"
+ "github.com/temporalio/roadrunner-temporal/roadrunner/util"
+)
+
+// AppConfig config combines factory, pool and cmd configurations.
+type AppConfig struct {
+ Command string
+ User string
+ Group string
+ Env Env
+
+ Relay string
+ // Listen 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.
+ Listen 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
+}
+
+type App struct {
+ cfg *AppConfig
+ configProvider config.Provider
+ factory roadrunner.Factory
+}
+
+func (app *App) Init(provider config.Provider) error {
+ app.cfg = &AppConfig{}
+ app.configProvider = provider
+
+ return nil
+}
+
+func (app *App) Configure() error {
+ err := app.configProvider.UnmarshalKey("app", app.cfg)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (app *App) Close() error {
+ return nil
+}
+
+func (app *App) NewCmd(env Env) (func() *exec.Cmd, error) {
+ var cmdArgs []string
+ // create command according to the config
+ cmdArgs = append(cmdArgs, strings.Split(app.cfg.Command, " ")...)
+
+ return func() *exec.Cmd {
+ cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
+ util.IsolateProcess(cmd)
+
+ // if user is not empty, and OS is linux or macos
+ // execute php worker from that particular user
+ if app.cfg.User != "" {
+ err := util.ExecuteFromUser(cmd, app.cfg.User)
+ if err != nil {
+ return nil
+ }
+ }
+
+ cmd.Env = app.setEnv(env)
+
+ return cmd
+ }, nil
+}
+
+// todo ENV unused
+func (app *App) NewFactory() (roadrunner.Factory, error) {
+ // if Listen is empty or doesn't contain separator, return error
+ if app.cfg.Listen == "" || !strings.Contains(app.cfg.Listen, "://") {
+ return nil, errors.New("relay should be set")
+ }
+
+ lsn, err := util.CreateListener(app.cfg.Listen)
+ if err != nil {
+ return nil, err
+ }
+
+ dsn := strings.Split(app.cfg.Listen, "://")
+ if len(dsn) != 2 {
+ return nil, errors.New("invalid DSN (tcp://:6001, unix://file.sock)")
+ }
+
+ switch dsn[0] {
+ // sockets group
+ case "unix":
+ return roadrunner.NewSocketServer(lsn, app.cfg.RelayTimeout), nil
+ case "tcp":
+ return roadrunner.NewSocketServer(lsn, app.cfg.RelayTimeout), nil
+ // pipes
+ default:
+ return roadrunner.NewPipeFactory(), nil
+ }
+}
+
+func (app *App) Serve() chan error {
+ errCh := make(chan error)
+ return errCh
+}
+
+func (app *App) Stop() error {
+ err := app.factory.Close(context.Background())
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (app *App) setEnv(e Env) []string {
+ env := append(os.Environ(), fmt.Sprintf("RR_RELAY=%s", app.cfg.Relay))
+ for k, v := range e {
+ env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(k), v))
+ }
+
+ return env
+}
diff --git a/plugins/factory/app_provider.go b/plugins/factory/app_provider.go
new file mode 100644
index 00000000..58fc686c
--- /dev/null
+++ b/plugins/factory/app_provider.go
@@ -0,0 +1,17 @@
+package factory
+
+import (
+ "os/exec"
+
+ "github.com/temporalio/roadrunner-temporal/roadrunner"
+)
+
+type Env map[string]string
+
+type Spawner interface {
+ // CmdFactory create new command factory with given env variables.
+ NewCmd(env Env) (func() *exec.Cmd, error)
+
+ // NewFactory inits new factory for workers.
+ NewFactory(env Env) (roadrunner.Factory, error)
+}
diff --git a/plugins/factory/factory.go b/plugins/factory/factory.go
new file mode 100644
index 00000000..74fd241f
--- /dev/null
+++ b/plugins/factory/factory.go
@@ -0,0 +1,67 @@
+package factory
+
+import (
+ "context"
+ "github.com/temporalio/roadrunner-temporal/events"
+
+ "github.com/temporalio/roadrunner-temporal/roadrunner"
+)
+
+type WorkerFactory interface {
+ NewWorker(ctx context.Context, env Env) (roadrunner.WorkerBase, error)
+ NewWorkerPool(ctx context.Context, opt *roadrunner.Config, env Env) (roadrunner.Pool, error)
+}
+
+type WFactory struct {
+ spw Spawner
+ eb *events.EventBroadcaster
+}
+
+func (wf *WFactory) NewWorkerPool(ctx context.Context, opt *roadrunner.Config, env Env) (roadrunner.Pool, error) {
+ cmd, err := wf.spw.NewCmd(env)
+ if err != nil {
+ return nil, err
+ }
+ factory, err := wf.spw.NewFactory(env)
+ if err != nil {
+ return nil, err
+ }
+
+ p, err := roadrunner.NewPool(ctx, cmd, factory, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO event to stop
+ go func() {
+ for e := range p.Events() {
+ wf.eb.Push(e)
+ }
+ }()
+
+ return p, nil
+}
+
+func (wf *WFactory) NewWorker(ctx context.Context, env Env) (roadrunner.WorkerBase, error) {
+ cmd, err := wf.spw.NewCmd(env)
+ if err != nil {
+ return nil, err
+ }
+
+ wb, err := roadrunner.InitBaseWorker(cmd())
+ if err != nil {
+ return nil, err
+ }
+
+ return wb, nil
+}
+
+func (wf *WFactory) Init(app Spawner) error {
+ wf.spw = app
+ wf.eb = events.NewEventBroadcaster()
+ return nil
+}
+
+func (wf *WFactory) AddListener(l events.EventListener) {
+ wf.eb.AddListener(l)
+}
diff --git a/plugins/factory/hello.php b/plugins/factory/hello.php
new file mode 100644
index 00000000..c6199449
--- /dev/null
+++ b/plugins/factory/hello.php
@@ -0,0 +1 @@
+<?php echo "hello -" . time(); \ No newline at end of file
diff --git a/plugins/factory/tests/.rr.yaml b/plugins/factory/tests/.rr.yaml
new file mode 100644
index 00000000..171f51dc
--- /dev/null
+++ b/plugins/factory/tests/.rr.yaml
@@ -0,0 +1,9 @@
+app:
+ command: "php hello.php"
+ user: ""
+ group: ""
+ env:
+ "RR_CONFIG": "/some/place/on/the/C134"
+ "RR_CONFIG2": "C138"
+ relay: "pipes"
+ relayTimeout: "20s" \ No newline at end of file
diff --git a/plugins/factory/tests/factory_test.go b/plugins/factory/tests/factory_test.go
new file mode 100644
index 00000000..880a7cf8
--- /dev/null
+++ b/plugins/factory/tests/factory_test.go
@@ -0,0 +1,85 @@
+package tests
+
+import (
+ "os"
+ "os/signal"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ "github.com/stretchr/testify/assert"
+ "github.com/temporalio/roadrunner-temporal/config"
+ "github.com/temporalio/roadrunner-temporal/factory"
+)
+
+func TestFactory(t *testing.T) {
+ container, err := endure.NewContainer(endure.DebugLevel, endure.RetryOnFail(true))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // config plugin
+ vp := &config.ViperProvider{}
+ vp.Path = ".rr.yaml"
+ vp.Prefix = "rr"
+ err = container.Register(vp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&factory.App{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&factory.WFactory{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&Foo{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&Foo2{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+
+ err = container.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ errCh, err := container.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // stop by CTRL+C
+ c := make(chan os.Signal)
+ signal.Notify(c, os.Interrupt)
+
+ tt := time.NewTicker(time.Second * 2)
+
+ for {
+ select {
+ case e := <-errCh:
+ assert.NoError(t, e.Error.Err)
+ assert.NoError(t, container.Stop())
+ return
+ case <-c:
+ er := container.Stop()
+ if er != nil {
+ panic(er)
+ }
+ return
+ case <-tt.C:
+ tt.Stop()
+ assert.NoError(t, container.Stop())
+ return
+ }
+ }
+
+}
diff --git a/plugins/factory/tests/hello.php b/plugins/factory/tests/hello.php
new file mode 100644
index 00000000..bf9e82cc
--- /dev/null
+++ b/plugins/factory/tests/hello.php
@@ -0,0 +1 @@
+<?php echo "hello1 - " . time(); \ No newline at end of file
diff --git a/plugins/factory/tests/plugin_1.go b/plugins/factory/tests/plugin_1.go
new file mode 100644
index 00000000..a7aba98e
--- /dev/null
+++ b/plugins/factory/tests/plugin_1.go
@@ -0,0 +1,55 @@
+package tests
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/temporalio/roadrunner-temporal/config"
+ "github.com/temporalio/roadrunner-temporal/factory"
+)
+
+type Foo struct {
+ configProvider config.Provider
+ spawner factory.Spawner
+}
+
+func (f *Foo) Init(p config.Provider, spw factory.Spawner) error {
+ f.configProvider = p
+ f.spawner = spw
+ return nil
+}
+
+func (f *Foo) Serve() chan error {
+ errCh := make(chan error, 1)
+
+ r := &factory.AppConfig{}
+ err := f.configProvider.UnmarshalKey("app", r)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ cmd, err := f.spawner.NewCmd(nil)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+ if cmd == nil {
+ errCh <- errors.New("command is nil")
+ return errCh
+ }
+ a := cmd()
+ out, err := a.Output()
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ fmt.Println(string(out))
+
+ return errCh
+}
+
+func (f *Foo) Stop() error {
+ return nil
+}
diff --git a/plugins/factory/tests/plugin_2.go b/plugins/factory/tests/plugin_2.go
new file mode 100644
index 00000000..d0c3ea2c
--- /dev/null
+++ b/plugins/factory/tests/plugin_2.go
@@ -0,0 +1,88 @@
+package tests
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/temporalio/roadrunner-temporal/config"
+ "github.com/temporalio/roadrunner-temporal/factory"
+ "github.com/temporalio/roadrunner-temporal/roadrunner"
+)
+
+type Foo2 struct {
+ configProvider config.Provider
+ wf factory.WorkerFactory
+ spw factory.Spawner
+}
+
+func (f *Foo2) Init(p config.Provider, workerFactory factory.WorkerFactory, spawner factory.Spawner) error {
+ f.configProvider = p
+ f.wf = workerFactory
+ f.spw = spawner
+ return nil
+}
+
+func (f *Foo2) Serve() chan error {
+ errCh := make(chan error, 1)
+
+ r := &factory.AppConfig{}
+ err := f.configProvider.UnmarshalKey("app", r)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ cmd, err := f.spw.NewCmd(nil)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+ if cmd == nil {
+ errCh <- errors.New("command is nil")
+ return errCh
+ }
+ a := cmd()
+ out, err := a.Output()
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ w, err := f.wf.NewWorker(context.Background(), nil)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ _ = w
+
+ poolConfig := &roadrunner.Config{
+ NumWorkers: 10,
+ MaxJobs: 100,
+ AllocateTimeout: time.Second * 10,
+ DestroyTimeout: time.Second * 10,
+ TTL: 1000,
+ IdleTTL: 1000,
+ ExecTTL: time.Second * 10,
+ MaxPoolMemory: 10000,
+ MaxWorkerMemory: 10000,
+ }
+
+ pool, err := f.wf.NewWorkerPool(context.Background(), poolConfig, nil)
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ _ = pool
+
+ fmt.Println(string(out))
+
+ return errCh
+}
+
+func (f *Foo2) Stop() error {
+ return nil
+}
diff --git a/pool.go b/pool.go
index d863e96f..540abb4c 100644
--- a/pool.go
+++ b/pool.go
@@ -1,5 +1,12 @@
package roadrunner
+import (
+ "context"
+ "fmt"
+ "runtime"
+ "time"
+)
+
const (
// EventWorkerConstruct thrown when new worker is spawned.
EventWorkerConstruct = iota + 100
@@ -11,7 +18,7 @@ const (
EventWorkerKill
// EventWorkerError thrown any worker related even happen (passed with WorkerError)
- EventWorkerError
+ EventWorkerEvent
// EventWorkerDead thrown when worker stops worker for any reason.
EventWorkerDead
@@ -20,20 +27,112 @@ const (
EventPoolError
)
+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
+)
+
// Pool managed set of inner worker processes.
type Pool interface {
- // Listen all caused events to attached controller.
- Listen(l func(event int, ctx interface{}))
+ // ATTENTION, YOU SHOULD CONSUME EVENTS, OTHERWISE POOL WILL BLOCK
+ Events() chan PoolEvent
// Exec one task with given payload and context, returns result or error.
- Exec(rqs *Payload) (rsp *Payload, err error)
+ Exec(ctx context.Context, rqs Payload) (Payload, error)
// Workers returns worker list associated with the pool.
- Workers() (workers []*Worker)
+ Workers(ctx context.Context) (workers []WorkerBase)
+
+ RemoveWorker(ctx context.Context, worker WorkerBase) error
+
+ Config() Config
+
+ // Destroy all underlying stack (but let them to complete the task).
+ Destroy(ctx context.Context)
+}
+
+// todo: merge with pool options
+
+// 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 destroy, if timeout reached worker will be killed.
+ DestroyTimeout time.Duration
+
+ // 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 time.Duration
+
+ // MaxPoolMemory defines maximum amount of memory allowed for worker. In megabytes.
+ MaxPoolMemory uint64
+
+ MaxWorkerMemory uint64
+
+ // config from limit plugin, combine TODO
+ // single bootstrap TODO, bool
+ // warmup one worker and start consume requests and then start the rest of the stack
+
+ // max memory for pool
+ // max ttl
+ // max idle ttl
+
+ // ATTACHER interface - delete
+}
+
+// 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")
+ }
- // Remove forces pool to remove specific worker. Return true is this is first remove request on given worker.
- Remove(w *Worker, err error) bool
+ if cfg.ExecTTL == 0 {
+ return fmt.Errorf("pool.ExecTTL must be set")
+ }
- // Destroy all underlying workers (but let them to complete the task).
- Destroy()
+ return nil
}
diff --git a/pool_supervisor.go b/pool_supervisor.go
new file mode 100644
index 00000000..cadf5f9c
--- /dev/null
+++ b/pool_supervisor.go
@@ -0,0 +1,176 @@
+package roadrunner
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+)
+
+const MB = 1024 * 1024
+
+type Supervisor interface {
+ Attach(pool Pool)
+ StartWatching() error
+ StopWatching()
+ Detach()
+}
+
+type staticPoolSupervisor struct {
+ // maxWorkerMemory in MB
+ maxWorkerMemory uint64
+ // maxPoolMemory in MB
+ maxPoolMemory uint64
+ // maxWorkerTTL in seconds
+ maxWorkerTTL uint64
+ // maxWorkerIdle in seconds
+ maxWorkerIdle uint64
+
+ // watchTimeout in seconds
+ watchTimeout uint64
+ stopCh chan struct{}
+
+ pool Pool
+}
+
+/*
+The arguments are:
+maxWorkerMemory - maximum memory allowed for a single worker
+maxPoolMemory - maximum pool memory allowed for a pool of a workers
+maxTtl - maximum ttl for the worker after which it will be killed and replaced
+maxIdle - maximum time to live for the worker in Ready state
+watchTimeout - time between watching for the workers/pool status
+*/
+// TODO might be just wrap the pool and return ControlledPool with included Pool interface
+func NewStaticPoolSupervisor(maxWorkerMemory, maxPoolMemory, maxTtl, maxIdle, watchTimeout uint64) Supervisor {
+ if maxWorkerMemory == 0 {
+ // just set to a big number, 5GB
+ maxPoolMemory = 5000 * MB
+ }
+ if watchTimeout == 0 {
+ watchTimeout = 60
+ }
+ return &staticPoolSupervisor{
+ maxWorkerMemory: maxWorkerMemory,
+ maxPoolMemory: maxPoolMemory,
+ maxWorkerTTL: maxTtl,
+ maxWorkerIdle: maxIdle,
+ stopCh: make(chan struct{}),
+ }
+}
+
+func (sps *staticPoolSupervisor) Attach(pool Pool) {
+ sps.pool = pool
+}
+
+func (sps *staticPoolSupervisor) StartWatching() error {
+ go func() {
+ watchTout := time.NewTicker(time.Second * time.Duration(sps.watchTimeout))
+ for {
+ select {
+ case <-sps.stopCh:
+ watchTout.Stop()
+ return
+ // stop here
+ case <-watchTout.C:
+ err := sps.control()
+ if err != nil {
+ sps.pool.Events() <- PoolEvent{Payload: err}
+ }
+ }
+ }
+ }()
+ return nil
+}
+
+func (sps *staticPoolSupervisor) StopWatching() {
+ sps.stopCh <- struct{}{}
+}
+
+func (sps *staticPoolSupervisor) Detach() {
+
+}
+
+func (sps *staticPoolSupervisor) control() error {
+ if sps.pool == nil {
+ return errors.New("pool should be attached")
+ }
+ now := time.Now()
+ ctx := context.TODO()
+
+ // THIS IS A COPY OF WORKERS
+ workers := sps.pool.Workers(ctx)
+ var totalUsedMemory uint64
+
+ for i := 0; i < len(workers); i++ {
+ if workers[i].State().Value() == StateInvalid {
+ continue
+ }
+
+ s, err := WorkerProcessState(workers[i])
+ if err != nil {
+ panic(err)
+ // push to pool events??
+ }
+
+ if sps.maxWorkerTTL != 0 && now.Sub(workers[i].Created()).Seconds() >= float64(sps.maxWorkerTTL) {
+ err = sps.pool.RemoveWorker(ctx, workers[i])
+ if err != nil {
+ return err
+ }
+
+ // after remove worker we should exclude it from further analysis
+ workers = append(workers[:i], workers[i+1:]...)
+ }
+
+ if sps.maxWorkerMemory != 0 && s.MemoryUsage >= sps.maxWorkerMemory*MB {
+ // TODO events
+ sps.pool.Events() <- PoolEvent{Payload: fmt.Errorf("max allowed memory reached (%vMB)", sps.maxWorkerMemory)}
+ err = sps.pool.RemoveWorker(ctx, workers[i])
+ if err != nil {
+ return err
+ }
+ workers = append(workers[:i], workers[i+1:]...)
+ continue
+ }
+
+ // firs we check maxWorker idle
+ if sps.maxWorkerIdle != 0 {
+ // then check for the worker state
+ if workers[i].State().Value() != 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 int64(sps.maxWorkerIdle)-res <= 0 {
+ sps.pool.Events() <- PoolEvent{Payload: fmt.Errorf("max allowed worker idle time elapsed. actual idle time: %v, max idle time: %v", sps.maxWorkerIdle, res)}
+ err = sps.pool.RemoveWorker(ctx, workers[i])
+ if err != nil {
+ return err
+ }
+ workers = append(workers[:i], workers[i+1:]...)
+ }
+ }
+
+ // the very last step is to calculate pool memory usage (except excluded workers)
+ totalUsedMemory += s.MemoryUsage
+ }
+
+ // if current usage more than max allowed pool memory usage
+ if totalUsedMemory > sps.maxPoolMemory {
+ // destroy pool
+ totalUsedMemory = 0
+ sps.pool.Destroy(ctx)
+ }
+
+ return nil
+}
diff --git a/config_test.go b/pool_test.go
index e51cb2c4..998dd9d4 100644
--- a/config_test.go
+++ b/pool_test.go
@@ -1,9 +1,10 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
"testing"
"time"
+
+ "github.com/stretchr/testify/assert"
)
func Test_NumWorkers(t *testing.T) {
@@ -21,6 +22,7 @@ func Test_NumWorkers_Default(t *testing.T) {
cfg := Config{
AllocateTimeout: time.Second,
DestroyTimeout: time.Second * 10,
+ ExecTTL: time.Second * 5,
}
assert.NoError(t, cfg.InitDefaults())
diff --git a/util/state.go b/process_state.go
index 29fca945..747fa8a8 100644
--- a/util/state.go
+++ b/process_state.go
@@ -1,13 +1,13 @@
-package util
+package roadrunner
import (
- "errors"
+ "context"
+
"github.com/shirou/gopsutil/process"
- "github.com/spiral/roadrunner"
)
-// State provides information about specific worker.
-type State struct {
+// ProcessState provides information about specific worker.
+type ProcessState struct {
// Pid contains process id.
Pid int `json:"pid"`
@@ -25,32 +25,28 @@ type State struct {
MemoryUsage uint64 `json:"memoryUsage"`
}
-// WorkerState creates new worker state definition.
-func WorkerState(w *roadrunner.Worker) (*State, error) {
- p, _ := process.NewProcess(int32(*w.Pid))
+// WorkerProcessState creates new worker state definition.
+func WorkerProcessState(w WorkerBase) (ProcessState, error) {
+ p, _ := process.NewProcess(int32(w.Pid()))
i, err := p.MemoryInfo()
if err != nil {
- return nil, err
+ return ProcessState{}, err
}
- return &State{
- Pid: *w.Pid,
+ return ProcessState{
+ Pid: int(w.Pid()),
Status: w.State().String(),
NumJobs: w.State().NumExecs(),
- Created: w.Created.UnixNano(),
+ 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)
+func PoolState(pool Pool) ([]ProcessState, error) {
+ result := make([]ProcessState, 0)
+ for _, w := range pool.Workers(context.TODO()) {
+ state, err := WorkerProcessState(w)
if err != nil {
return nil, err
}
diff --git a/process_state_test.go b/process_state_test.go
new file mode 100644
index 00000000..3f283dce
--- /dev/null
+++ b/process_state_test.go
@@ -0,0 +1 @@
+package roadrunner
diff --git a/protocol.go b/protocol.go
index b00eb2a4..bdf78296 100644
--- a/protocol.go
+++ b/protocol.go
@@ -2,11 +2,14 @@ package roadrunner
import (
"fmt"
+ "os"
+
json "github.com/json-iterator/go"
"github.com/spiral/goridge/v2"
- "os"
)
+var j = json.ConfigCompatibleWithStandardLibrary
+
type stopCommand struct {
Stop bool `json:"stop"`
}
@@ -20,7 +23,6 @@ func sendControl(rl goridge.Relay, v interface{}) error {
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)
@@ -29,8 +31,9 @@ func sendControl(rl goridge.Relay, v interface{}) error {
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 {
+func fetchPID(rl goridge.Relay) (int64, error) {
+ err := sendControl(rl, pidCommand{Pid: os.Getpid()})
+ if err != nil {
return 0, err
}
@@ -47,5 +50,5 @@ func fetchPID(rl goridge.Relay) (pid int, err error) {
return 0, err
}
- return link.Pid, nil
+ return int64(link.Pid), nil
}
diff --git a/protocol_test.go b/protocol_test.go
index 55c603a5..396ce992 100644
--- a/protocol_test.go
+++ b/protocol_test.go
@@ -1,10 +1,11 @@
package roadrunner
import (
+ "testing"
+
"github.com/pkg/errors"
"github.com/spiral/goridge/v2"
"github.com/stretchr/testify/assert"
- "testing"
)
type relayMock struct {
@@ -36,7 +37,7 @@ func Test_Protocol_Errors(t *testing.T) {
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)
+ assert.Equal(t, int64(100), pid)
_, err = fetchPID(&relayMock{error: true, 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/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 cb1cd728..00000000
--- a/service/http/handler_test.go
+++ /dev/null
@@ -1,1961 +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", "value1")
- form.Add("key", "value2")
-
- 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, `{"key":"value2","arr":{"x":{"y":null}}}`, 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: ":8084", 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 b358c1c1..00000000
--- a/service/limit/service_test.go
+++ /dev/null
@@ -1,500 +0,0 @@
-package limit
-
-import (
- "fmt"
- "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
- 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": ":17029",
- "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 * 100)
- req, err := http.NewRequest("GET", "http://localhost:17029", 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": ":7033",
- "workers":{
- "command": "php ../../tests/http/client.php memleak pipes",
- "pool": {"numWorkers": 1}
- }
- }`,
- limitCfg: `{
- "services": {
- "http": {
- "maxMemory": 10
- }
- }
- }`,
- })
- if err != nil {
- return err
- }
-
- 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 * 100)
-
- lastPID := getPID(s)
-
- req, err := http.NewRequest("GET", "http://localhost:7033", 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 c95fd940..00000000
--- a/service/metrics/config.go
+++ /dev/null
@@ -1,136 +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"`
-}
-
-// 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,
- }
-
- 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 a64e9047..00000000
--- a/service/metrics/config_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package metrics
-
-import (
- json "github.com/json-iterator/go"
- "github.com/prometheus/client_golang/prometheus"
- "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))
-}
-
-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 377d6173..00000000
--- a/service/metrics/rpc.go
+++ /dev/null
@@ -1,260 +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,
- }
-
- 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/samefile.go b/service/reload/samefile.go
deleted file mode 100644
index 80df0431..00000000
--- a/service/reload/samefile.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// +build !windows
-
-package reload
-
-import "os"
-
-func sameFile(fi1, fi2 os.FileInfo) bool {
- return os.SameFile(fi1, fi2)
-}
diff --git a/service/reload/samefile_windows.go b/service/reload/samefile_windows.go
deleted file mode 100644
index 5f70d327..00000000
--- a/service/reload/samefile_windows.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// +build windows
-
-package reload
-
-import "os"
-
-func sameFile(fi1, fi2 os.FileInfo) bool {
- return fi1.ModTime() == fi2.ModTime() &&
- fi1.Size() == fi2.Size() &&
- fi1.Mode() == fi2.Mode() &&
- fi1.IsDir() == fi2.IsDir()
-}
diff --git a/service/reload/service.go b/service/reload/service.go
deleted file mode 100644
index 9c615e0b..00000000
--- a/service/reload/service.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package reload
-
-import (
- "errors"
- "github.com/sirupsen/logrus"
- "github.com/spiral/roadrunner"
- "github.com/spiral/roadrunner/service"
- "os"
- "strings"
- "time"
-)
-
-// 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
- ticker := time.NewTicker(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 ticker
- ticker.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 ticker and wait for Interval time
- // If there is no more events, we restart service only once
- ticker = time.NewTicker(s.cfg.Interval)
- case <-ticker.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:
- ticker.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/service_test.go b/service/reload/service_test.go
deleted file mode 100644
index 7cad4a5d..00000000
--- a/service/reload/service_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package reload
diff --git a/service/reload/watcher.go b/service/reload/watcher.go
deleted file mode 100644
index 027d2d0d..00000000
--- a/service/reload/watcher.go
+++ /dev/null
@@ -1,409 +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{}),
-
- //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++ {
- var pathToFile string
- // BCE check elimination
- // https://go101.org/article/bounds-check-elimination.html
- if len(fileInfoList) != 0 && len(fileInfoList) >= i {
- pathToFile = filepath.Join(pathToFile, fileInfoList[i].Name())
- } else {
- return nil, errors.New("file info list len")
- }
-
- // 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[pathToFile] = 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, config := range w.watcherConfigs {
- go func(sn string, c WatcherConfig) {
- fileList, _ := w.retrieveFileList(sn, c)
- w.pollEvents(c.serviceName, fileList)
- }(serviceName, config)
- }
- }
- }
-
-}
-
-// retrieveFileList get file list for service
-func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) {
- w.mu.Lock()
- defer w.mu.Unlock()
- fileList := make(map[string]os.FileInfo)
- if config.recursive {
- // walk through directories recursively
- for _, dir := range config.directories {
- // full path is workdir/relative_path
- fullPath, err := filepath.Abs(dir)
- if err != nil {
- return nil, err
- }
- list, err := w.retrieveFilesRecursive(serviceName, fullPath)
- if err != nil {
- return nil, err
- }
-
- for k, v := range list {
- fileList[k] = v
- }
- }
- return fileList, nil
- }
-
- for _, dir := range config.directories {
- // full path is workdir/relative_path
- fullPath, err := filepath.Abs(dir)
- 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, info := range w.watcherConfigs[serviceName].files {
- if _, found := files[pth]; !found {
- removes[pth] = info
- }
- }
-
- // Check for created files, writes and chmods.
- for pth, info := range files {
- if info.IsDir() {
- continue
- }
- oldInfo, found := w.watcherConfigs[serviceName].files[pth]
- if !found {
- // A file was created.
- creates[pth] = info
- continue
- }
- if oldInfo.ModTime() != info.ModTime() {
- w.watcherConfigs[serviceName].files[pth] = info
- w.Event <- Event{
- path: pth,
- info: info,
- service: serviceName,
- }
- }
- if oldInfo.Mode() != info.Mode() {
- w.watcherConfigs[serviceName].files[pth] = info
- w.Event <- Event{
- path: pth,
- info: info,
- service: serviceName,
- }
- }
- }
-
- //Check for renames and moves.
- for path1, info1 := range removes {
- for path2, info2 := range creates {
- if sameFile(info1, info2) {
- e := Event{
- path: path2,
- info: info2,
- service: serviceName,
- }
-
- // remove initial path
- delete(w.watcherConfigs[serviceName].files, path1)
- // update with new
- w.watcherConfigs[serviceName].files[path2] = info2
-
-
- w.Event <- e
- }
- }
- }
-
- //Send all the remaining create and remove events.
- for pth, info := range creates {
- w.watcherConfigs[serviceName].files[pth] = info
- w.Event <- Event{
- path: pth,
- info: info,
- service: serviceName,
- }
- }
- for pth, info := range removes {
- delete(w.watcherConfigs[serviceName].files, pth)
- w.Event <- Event{
- path: pth,
- info: info,
- 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 9683d2de..00000000
--- a/service/reload/watcher_test.go
+++ /dev/null
@@ -1,673 +0,0 @@
-package reload
-
-import (
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "testing"
- "time"
-)
-
-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(".", "")
- c := make(chan struct{})
- defer func(name string) {
- err = freeResources(name)
- if err != nil {
- c <- struct{}{}
- t.Fatal(err)
- }
- c <- struct{}{}
- }(tempDir)
-
- if err != nil {
- t.Fatal(err)
- }
- err = ioutil.WriteFile(filepath.Join(tempDir, "file1.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file3.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: []string{"aaa", "txt"},
- }
-
- w, err := NewWatcher([]WatcherConfig{wc})
- if err != nil {
- t.Fatal(err)
- }
-
- // should be 3 files and directory
- if len(w.GetAllFiles(testServiceName)) != 4 {
- t.Fatal("incorrect directories len")
- }
-
- go limitTime(time.Second * 10, t.Name(), c)
-
- go func() {
- go func() {
- time.Sleep(time.Second)
- err2 := ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"),
- []byte{1, 1, 1}, 0755)
- if err2 != nil {
- panic(err2)
- }
- runtime.Goexit()
- }()
-
- go func() {
- for e := range w.Event {
- if e.path != "file2.txt" {
- panic("didn't handle event when write file2")
- }
- w.Stop()
- }
- }()
- }()
-
- err = w.StartPolling(time.Second)
- if err != nil {
- t.Fatal(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(".", "")
- c := make(chan struct{})
- defer func(name string) {
- err = freeResources(name)
- if err != nil {
- c <- struct{}{}
- t.Fatal(err)
- }
- c <- struct{}{}
- }(tempDir)
-
- if err != nil {
- t.Fatal(err)
- }
- err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(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})
- if err != nil {
- t.Fatal(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 limitTime(time.Second * 5, t.Name(), c)
-
- go func() {
- go func() {
- err2 := ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"),
- []byte{1, 1, 1}, 0755)
- if err2 != nil {
- panic(err2)
- }
-
- runtime.Goexit()
- }()
-
- go func() {
- for e := range w.Event {
- fmt.Println(e.info.Name())
- panic("handled event from filtered file")
- }
- }()
- w.Stop()
- runtime.Goexit()
- }()
-
- err = w.StartPolling(time.Second)
- if err != nil {
- t.Fatal(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)
- if err != nil {
- t.Fatal(err)
- }
- }()
-
- nestedDir, err := ioutil.TempDir(tempDir, "nested")
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"),
- []byte{}, 0755)
- 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: nil,
- filePatterns: []string{"aaa", "bbb"},
- }
-
- w, err := NewWatcher([]WatcherConfig{wc})
- if err != nil {
- t.Fatal(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() {
- // 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)
- if err != nil {
- panic(err)
- }
- go func() {
- for e := range w.Event {
- if e.info.Name() != "file4.aaa" {
- panic("wrong handled event from watcher in nested dir")
- }
- w.Stop()
- }
- }()
- }()
-
- err = w.StartPolling(time.Second)
- if err != nil {
- t.Fatal(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})
- if err == nil {
- t.Fatal(err)
- }
-}
-
-func Test_Filter_Directory(t *testing.T) {
- tempDir, err := ioutil.TempDir(".", "")
- c := make(chan struct{})
- defer func(name string) {
- err = freeResources(name)
- if err != nil {
- c <- struct{}{}
- t.Fatal(err)
- }
- c <- struct{}{}
- }(tempDir)
-
- go limitTime(time.Second*10, t.Name(), c)
-
- nestedDir, err := ioutil.TempDir(tempDir, "nested")
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(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() {
- go func() {
- err2 := ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"),
- []byte{1, 1, 1}, 0755)
- if err2 != nil {
- panic(err2)
- }
- }()
-
- go func() {
- for e := range w.Event {
- fmt.Println("file: " + e.info.Name())
- panic("handled event from watcher in nested dir")
- }
- }()
-
- // time sleep is used here because StartPolling is blocking operation
- time.Sleep(time.Second * 5)
- w.Stop()
-
- }()
-
- 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(".", "")
- c := make(chan struct{})
- defer func() {
- err = freeResources(tempDir)
- if err != nil {
- c <- struct{}{}
- t.Fatal(err)
- }
- c <- struct{}{}
- }()
-
- nestedDir, err := ioutil.TempDir(tempDir, "nested")
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
-
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(err)
- }
- err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"),
- []byte{}, 0755)
- if err != nil {
- t.Fatal(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 limitTime(time.Second*10, t.Name(), c)
-
- go func() {
- go func() {
- err2 := copyDir(nestedDir, filepath.Join(tempDir, "copyTo"))
- if err2 != nil {
- panic(err2)
- }
-
- // exit from current goroutine
- runtime.Goexit()
- }()
-
- go func() {
- for range w.Event {
- // here should be event, otherwise we won't stop
- w.Stop()
- }
- }()
- }()
-
- err = w.StartPolling(time.Second)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func limitTime(d time.Duration, name string, free chan struct{}) {
- go func() {
- ticket := time.NewTicker(d)
- for {
- select {
- case <-ticket.C:
- ticket.Stop()
- panic("timeout exceed, test: " + name)
- case <-free:
- ticket.Stop()
- return
- }
- }
- }()
-}
-
-func copyFile(src, dst string) (err error) {
- in, err := os.Open(src)
- if err != nil {
- return
- }
- defer 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 cc492622..00000000
--- a/service/rpc/config.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package rpc
-
-import (
- "errors"
- "net"
- "strings"
-
- "github.com/spiral/roadrunner/service"
- "github.com/spiral/roadrunner/util"
-)
-
-// 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
index 42196588..27558cce 100644
--- a/socket_factory.go
+++ b/socket_factory.go
@@ -1,16 +1,20 @@
package roadrunner
import (
- "fmt"
- "github.com/pkg/errors"
- "github.com/spiral/goridge/v2"
+ "context"
"net"
"os/exec"
+ "strings"
"sync"
"time"
+
+ "github.com/pkg/errors"
+ "github.com/spiral/goridge/v2"
+ "go.uber.org/multierr"
+ "golang.org/x/sync/errgroup"
)
-// SocketFactory connects to external workers using socket server.
+// SocketFactory connects to external stack using socket server.
type SocketFactory struct {
// listens for incoming connections from underlying processes
ls net.Listener
@@ -18,122 +22,199 @@ type SocketFactory struct {
// 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
+ // relays map[int64]*goridge.SocketRelay
+ relays sync.Map
+
+ ErrCh chan error
}
-// NewSocketFactory returns SocketFactory attached to a given socket lsn.
+// todo: review
+
+// NewSocketServer returns SocketFactory attached to a given socket listener.
// tout specifies for how long factory should serve for incoming relay connection
-func NewSocketFactory(ls net.Listener, tout time.Duration) *SocketFactory {
+func NewSocketServer(ls net.Listener, tout time.Duration) Factory {
f := &SocketFactory{
ls: ls,
tout: tout,
- relays: make(map[int]chan *goridge.SocketRelay),
+ relays: sync.Map{},
+ ErrCh: make(chan error, 10),
}
- go f.listen()
+ // 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
}
-// 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 {
+// blocking operation, returns an error
+func (f *SocketFactory) listen() error {
+ errGr := &errgroup.Group{}
+ errGr.Go(func() error {
+ for {
+ conn, err := f.ls.Accept()
+ if err != nil {
+ return err
+ }
+
+ rl := goridge.NewSocketRelay(conn)
+ pid, err := fetchPID(rl)
+ if err != nil {
+ return err
+ }
+
+ f.attachRelayToPid(pid, rl)
+ }
+ })
+
+ return errGr.Wait()
+}
+
+type socketSpawn struct {
+ w WorkerBase
+ err error
+}
+
+// SpawnWorker creates WorkerProcess and connects it to appropriate relay or returns error
+func (f *SocketFactory) SpawnWorkerWithContext(ctx context.Context, cmd *exec.Cmd) (WorkerBase, error) {
+ c := make(chan socketSpawn)
+ go func() {
+ ctx, cancel := context.WithTimeout(ctx, f.tout)
+ defer cancel()
+ w, err := InitBaseWorker(cmd)
+ if err != nil {
+ c <- socketSpawn{
+ w: nil,
+ err: err,
+ }
+ return
+ }
+
+ err = w.Start()
+ if err != nil {
+ c <- socketSpawn{
+ w: nil,
+ err: errors.Wrap(err, "process error"),
+ }
+ return
+ }
+
+ rl, err := f.findRelayWithContext(ctx, w)
+ if err != nil {
+ err = multierr.Combine(
+ err,
+ w.Kill(context.Background()),
+ w.Wait(context.Background()),
+ )
+ c <- socketSpawn{
+ w: nil,
+ err: err,
+ }
+ return
+ }
+
+ w.AttachRelay(rl)
+ w.State().Set(StateReady)
+
+ c <- socketSpawn{
+ w: w,
+ err: nil,
+ }
+ return
+ }()
+
+ 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 *SocketFactory) SpawnWorker(cmd *exec.Cmd) (WorkerBase, error) {
+ ctx := context.Background()
+ w, err := InitBaseWorker(cmd)
+ if err != nil {
return nil, err
}
- if err := w.start(); err != nil {
+ err = w.Start()
+ if err != nil {
return nil, errors.Wrap(err, "process error")
}
- rl, err := f.findRelay(w, f.tout)
+ var errs []string
+ rl, err := f.findRelay(w)
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
- }
+ errs = append(errs, err.Error())
+ err = w.Kill(ctx)
+ if err != nil {
+ errs = append(errs, err.Error())
}
-
- return nil, errors.Wrap(err, "unable to connect to worker")
+ if err = w.Wait(ctx); err != nil {
+ errs = append(errs, err.Error())
+ }
+ return nil, errors.New(strings.Join(errs, "/"))
}
- w.rl = rl
- w.state.set(StateReady)
+ w.AttachRelay(rl)
+ w.State().Set(StateReady)
return w, nil
}
// Close socket factory and underlying socket connection.
-func (f *SocketFactory) Close() error {
+func (f *SocketFactory) Close(ctx context.Context) error {
return f.ls.Close()
}
-// listens for incoming socket connections
-func (f *SocketFactory) listen() {
+// waits for WorkerProcess to connect over socket and returns associated relay of timeout
+func (f *SocketFactory) findRelayWithContext(ctx context.Context, w WorkerBase) (*goridge.SocketRelay, error) {
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
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ tmp, ok := f.relays.Load(w.Pid())
+ if !ok {
+ continue
+ }
+ return tmp.(*goridge.SocketRelay), nil
}
}
}
-// 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)
+func (f *SocketFactory) findRelay(w WorkerBase) (*goridge.SocketRelay, error) {
+ // poll every 1ms for the relay
+ pollDone := time.NewTimer(f.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")
+ case <-pollDone.C:
+ return nil, errors.New("relay timeout")
+ default:
+ tmp, ok := f.relays.Load(w.Pid())
+ if !ok {
+ continue
+ }
+ return tmp.(*goridge.SocketRelay), nil
}
}
}
-// 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
+// chan to store relay associated with specific pid
+func (f *SocketFactory) attachRelayToPid(pid int64, relay *goridge.SocketRelay) {
+ f.relays.Store(pid, relay)
}
-// deletes relay chan associated with specific Pid
-func (f *SocketFactory) cleanChan(pid int) {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- delete(f.relays, pid)
+// deletes relay chan associated with specific pid
+func (f *SocketFactory) removeRelayFromPid(pid int64) {
+ f.relays.Delete(pid)
}
diff --git a/socket_factory_test.go b/socket_factory_test.go
index abb40f16..45443337 100644
--- a/socket_factory_test.go
+++ b/socket_factory_test.go
@@ -1,14 +1,18 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
+ "context"
"net"
"os/exec"
+ "sync"
"testing"
"time"
+
+ "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")
@@ -25,23 +29,23 @@ func Test_Tcp_Start(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "echo", "tcp")
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
assert.NoError(t, err)
assert.NotNil(t, w)
go func() {
- assert.NoError(t, w.Wait())
+ assert.NoError(t, w.Wait(ctx))
}()
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: 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 {
@@ -50,7 +54,7 @@ func Test_Tcp_StartCloseFactory(t *testing.T) {
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 +62,19 @@ func Test_Tcp_StartCloseFactory(t *testing.T) {
}
}()
- w, err := f.SpawnWorker(cmd)
+ w, err := f.SpawnWorkerWithContext(ctx, cmd)
assert.NoError(t, err)
assert.NotNil(t, w)
- go func() {
- assert.NoError(t, w.Wait())
- }()
-
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: 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() {
@@ -93,37 +93,37 @@ func Test_Tcp_StartError(t *testing.T) {
t.Errorf("error executing the command: error %v", err)
}
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(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
-
- ls, err := net.Listen("tcp", "localhost:9007")
- if assert.NoError(t, err) {
- defer func() {
- err3 := ls.Close()
- if err3 != nil {
- t.Errorf("error closing the listener: error %v", err3)
- }
- }()
- } else {
- t.Skip("socket is busy")
- }
-
- cmd := exec.Command("php", "tests/failboot.php")
-
- w, err2 := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
- assert.Nil(t, w)
- assert.Error(t, err2)
- assert.Contains(t, err2.Error(), "failboot")
-}
+// func Test_Tcp_Failboot(t *testing.T) {
+// time.Sleep(time.Millisecond * 10) // to ensure free socket
+//
+// ls, err := net.Listen("tcp", "localhost:9007")
+// if assert.NoError(t, err) {
+// defer func() {
+// err3 := ls.Close()
+// if err3 != nil {
+// t.Errorf("error closing the listener: error %v", err3)
+// }
+// }()
+// } else {
+// t.Skip("socket is busy")
+// }
+//
+// cmd := exec.Command("php", "tests/failboot.php")
+//
+// w, err2 := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(cmd)
+// assert.Nil(t, w)
+// assert.Error(t, err2)
+// assert.Contains(t, err2.Error(), "failboot")
+//}
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() {
@@ -138,15 +138,15 @@ func Test_Tcp_Timeout(t *testing.T) {
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).SpawnWorkerWithContext(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() {
@@ -161,14 +161,14 @@ func Test_Tcp_Invalid(t *testing.T) {
cmd := exec.Command("php", "tests/invalid.php")
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Second*10).SpawnWorkerWithContext(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() {
@@ -183,29 +183,38 @@ func Test_Tcp_Broken(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "broken", "tcp")
- w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
- go func() {
- err := w.Wait()
-
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "undefined_function()")
- }()
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ //go func() {
+ // err := w.Wait()
+ //
+ // assert.Error(t, err)
+ // assert.Contains(t, err.Error(), "undefined_function()")
+ //}()
defer func() {
time.Sleep(time.Second)
- err2 := w.Stop()
- assert.NoError(t, err2)
+ err2 := w.Stop(ctx)
+ // 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 := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res, err := sw.Exec(ctx, Payload{Body: []byte("hello")})
assert.Error(t, err)
- assert.Nil(t, res)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
}
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() {
@@ -220,18 +229,23 @@ func Test_Tcp_Echo(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "echo", "tcp")
- w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
- go func() {
- assert.NoError(t, w.Wait())
- }()
+ w, _ := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
+ //go func() {
+ // assert.NoError(t, w.Wait())
+ //}()
defer func() {
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
}
}()
- res, err := w.Exec(&Payload{Body: []byte("hello")})
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := sw.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -242,6 +256,7 @@ func Test_Tcp_Echo(t *testing.T) {
}
func Test_Unix_Start(t *testing.T) {
+ ctx := context.Background()
ls, err := net.Listen("unix", "sock.unix")
if err == nil {
defer func() {
@@ -256,22 +271,23 @@ func Test_Unix_Start(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "echo", "unix")
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
assert.NoError(t, err)
assert.NotNil(t, w)
- go func() {
- assert.NoError(t, w.Wait())
- }()
+ //go func() {
+ // assert.NoError(t, w.Wait())
+ //}()
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: 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()
@@ -285,7 +301,7 @@ func Test_Unix_Failboot(t *testing.T) {
cmd := exec.Command("php", "tests/failboot.php")
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Second*2).SpawnWorkerWithContext(ctx, cmd)
assert.Nil(t, w)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failboot")
@@ -293,6 +309,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()
@@ -306,13 +323,14 @@ func Test_Unix_Timeout(t *testing.T) {
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).SpawnWorkerWithContext(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() {
@@ -327,12 +345,13 @@ func Test_Unix_Invalid(t *testing.T) {
cmd := exec.Command("php", "tests/invalid.php")
- w, err := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Second*10).SpawnWorkerWithContext(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() {
@@ -347,26 +366,40 @@ func Test_Unix_Broken(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "broken", "unix")
- w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
go func() {
- err := w.Wait()
+ defer wg.Done()
+ err := w.Wait(ctx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "undefined_function()")
}()
defer func() {
time.Sleep(time.Second)
- err = w.Stop()
- assert.NoError(t, err)
+ err = w.Stop(ctx)
+ assert.Error(t, err)
}()
- res, err := w.Exec(&Payload{Body: []byte("hello")})
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := sw.Exec(ctx, 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() {
@@ -381,18 +414,26 @@ func Test_Unix_Echo(t *testing.T) {
cmd := exec.Command("php", "tests/client.php", "echo", "unix")
- w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd)
- go func() {
- assert.NoError(t, w.Wait())
- }()
+ w, err := NewSocketServer(ls, time.Minute).SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ //go func() {
+ // assert.NoError(t, w.Wait())
+ //}()
defer func() {
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
}
}()
- res, err := w.Exec(&Payload{Body: []byte("hello")})
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := sw.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -403,10 +444,11 @@ func Test_Unix_Echo(t *testing.T) {
}
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 +457,33 @@ 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")
- w, _ := f.SpawnWorker(cmd)
- go func() {
- if w.Wait() != nil {
- b.Fail()
- }
- }()
+ w, err := f.SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ b.Fatal(err)
+ }
+ //go func() {
+ // if w.Wait() != nil {
+ // b.Fail()
+ // }
+ //}()
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- b.Errorf("error stopping the worker: error %v", err)
+ b.Errorf("error stopping the WorkerProcess: 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)
}
@@ -448,28 +494,31 @@ func Benchmark_Tcp_Worker_ExecEcho(b *testing.B) {
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).SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ b.Fatal(err)
+ }
defer func() {
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- b.Errorf("error stopping the worker: error %v", err)
+ b.Errorf("error stopping the WorkerProcess: error %v", err)
}
}()
+ sw, err := NewSyncWorker(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(ctx, 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 +531,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()
- }
- }()
-
- err = w.Stop()
+ w, err := f.SpawnWorkerWithContext(ctx, cmd)
if err != nil {
- b.Errorf("error stopping the worker: error %v", err)
+ b.Fatal(err)
+ }
+ err = w.Stop(ctx)
+ if err != nil {
+ b.Errorf("error stopping the WorkerProcess: 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() {
@@ -515,22 +562,24 @@ func Benchmark_Unix_Worker_ExecEcho(b *testing.B) {
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).SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ b.Fatal(err)
+ }
defer func() {
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- b.Errorf("error stopping the worker: error %v", err)
+ b.Errorf("error stopping the WorkerProcess: error %v", err)
}
}()
+ sw, err := NewSyncWorker(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(ctx, Payload{Body: []byte("hello")}); err != nil {
b.Fail()
}
}
diff --git a/state.go b/state.go
index 98451f48..2e36c977 100644
--- a/state.go
+++ b/state.go
@@ -5,18 +5,25 @@ import (
"sync/atomic"
)
-// State represents worker status and updated time.
+// State represents WorkerProcess status and updated time.
type State interface {
fmt.Stringer
// Value returns state value
Value() int64
+ Set(value int64)
- // NumJobs shows how many times worker was invoked
+ // NumJobs shows how many times WorkerProcess was invoked
NumExecs() int64
- // IsActive returns true if worker not Inactive or Stopped
+ // IsActive returns true if WorkerProcess not Inactive or Stopped
IsActive() bool
+
+ RegisterExec()
+
+ SetLastUsed(lu uint64)
+
+ LastUsed() uint64
}
const (
@@ -29,24 +36,35 @@ const (
// StateWorking - working on given payload.
StateWorking
- // StateInvalid - indicates that worker is being disabled and will be removed.
+ // StateInvalid - indicates that WorkerProcess is being disabled and will be removed.
StateInvalid
// StateStopping - process is being softly stopped.
StateStopping
+ StateKilling
+ StateKilled
+
+ // State of worker, when no need to allocate new one
+ StateDestroyed
+
// StateStopped - process has been terminated.
StateStopped
// StateErrored - error state (can't be used).
StateErrored
+
+ StateRemove
)
type state struct {
value int64
numExecs int64
+ // to be lightweight, use UnixNano
+ lastUsed uint64
}
+// Thread safe
func newState(value int64) *state {
return &state{value: value}
}
@@ -71,7 +89,7 @@ func (s *state) String() string {
return "undefined"
}
-// NumExecs returns number of registered worker execs.
+// NumExecs returns number of registered WorkerProcess execs.
func (s *state) NumExecs() int64 {
return atomic.LoadInt64(&s.numExecs)
}
@@ -81,18 +99,27 @@ func (s *state) Value() int64 {
return atomic.LoadInt64(&s.value)
}
-// IsActive returns true if worker not Inactive or Stopped
+// IsActive returns true if WorkerProcess 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) {
+func (s *state) Set(value int64) {
atomic.StoreInt64(&s.value, value)
}
// register new execution atomically
-func (s *state) registerExec() {
+func (s *state) RegisterExec() {
atomic.AddInt64(&s.numExecs, 1)
}
+
+// Update last used time
+func (s *state) SetLastUsed(lu uint64) {
+ atomic.StoreUint64(&s.lastUsed, lu)
+}
+
+func (s *state) LastUsed() uint64 {
+ return atomic.LoadUint64(&s.lastUsed)
+}
diff --git a/state_test.go b/state_test.go
index c13c5a88..10547a4b 100644
--- a/state_test.go
+++ b/state_test.go
@@ -1,8 +1,9 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func Test_NewState(t *testing.T) {
diff --git a/static_pool.go b/static_pool.go
index c7cc6517..1444e95a 100644
--- a/static_pool.go
+++ b/static_pool.go
@@ -1,11 +1,10 @@
package roadrunner
import (
+ "context"
"fmt"
"os/exec"
"sync"
- "sync/atomic"
- "time"
"github.com/pkg/errors"
)
@@ -15,47 +14,32 @@ const (
StopRequest = "{\"stop\":true}"
)
-// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of workers.
+// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of stack.
type StaticPool struct {
// pool behaviour
- cfg Config
+ cfg *Config
// worker command creator
cmd func() *exec.Cmd
- // creates and connects to workers
+ // creates and connects to stack
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
+ ww *WorkersWatcher
- // 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{})
+ events chan PoolEvent
+}
+type PoolEvent struct {
+ Payload 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) {
+// supervisor Supervisor, todo: think about it
+// stack func() (WorkerBase, error),
+func NewPool(ctx context.Context, cmd func() *exec.Cmd, factory Factory, cfg *Config) (Pool, error) {
if err := cfg.Valid(); err != nil {
return nil, errors.Wrap(err, "config")
}
@@ -64,305 +48,145 @@ func NewPool(cmd func() *exec.Cmd, factory Factory, cfg Config) (*StaticPool, er
cfg: cfg,
cmd: cmd,
factory: factory,
- workers: make([]*Worker, 0, cfg.NumWorkers),
- free: make(chan *Worker, cfg.NumWorkers),
- destroy: make(chan interface{}),
+ events: make(chan PoolEvent),
}
- // constant number of workers simplify logic
- for i := int64(0); i < p.cfg.NumWorkers; i++ {
- // to test if worker ready
- w, err := p.createWorker()
+ p.ww = NewWorkerWatcher(func(args ...interface{}) (*SyncWorker, error) {
+ w, err := p.factory.SpawnWorkerWithContext(ctx, p.cmd())
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()
+ sw, err := NewSyncWorker(w)
+ if err != nil {
+ return nil, err
+ }
+ return &sw, nil
+ }, p.cfg.NumWorkers, p.events)
- p.lsn = l
+ workers, err := p.allocateWorkers(ctx, p.cfg.NumWorkers)
+ if err != nil {
+ return nil, err
+ }
- p.muw.Lock()
- for _, w := range p.workers {
- w.err.Listen(p.lsn)
+ // put stack in the pool
+ err = p.ww.AddToWatch(ctx, workers)
+ if err != nil {
+ return nil, err
}
- p.muw.Unlock()
+
+ return p, nil
}
// Config returns associated pool configuration. Immutable.
func (p *StaticPool) Config() Config {
- return p.cfg
+ return *p.cfg
}
// Workers returns worker list associated with the pool.
-func (p *StaticPool) Workers() (workers []*Worker) {
+func (p *StaticPool) Workers(ctx context.Context) (workers []WorkerBase) {
p.muw.RLock()
defer p.muw.RUnlock()
-
- workers = append(workers, p.workers...)
-
- return workers
+ return p.ww.WorkersList(ctx)
}
-// 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
+func (p *StaticPool) RemoveWorker(ctx context.Context, wb WorkerBase) error {
+ return p.ww.RemoveWorker(ctx, wb)
}
// 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")
+func (p *StaticPool) Exec(ctx context.Context, rqs Payload) (Payload, error) {
+ getWorkerCtx, cancel := context.WithTimeout(context.TODO(), p.cfg.AllocateTimeout)
+ defer cancel()
+ w, err := p.ww.GetFreeWorker(getWorkerCtx)
+ if err != nil && errors.Is(err, ErrWatcherStopped) {
+ return EmptyPayload, ErrWatcherStopped
+ } else if err != nil {
+ return EmptyPayload, err
}
- rsp, err = w.Exec(rqs)
+ sw := w.(SyncWorker)
+
+ execCtx, cancel2 := context.WithTimeout(context.TODO(), p.cfg.ExecTTL)
+ defer cancel2()
+ rsp, err := sw.Exec(execCtx, rqs)
if err != nil {
+ errJ := p.checkMaxJobs(ctx, w)
+ if errJ != nil {
+ return EmptyPayload, fmt.Errorf("%v, %v", err, errJ)
+ }
// soft job errors are allowed
- if _, jobError := err.(JobError); jobError {
- p.release(w)
- return nil, err
+ if _, jobError := err.(TaskError); jobError {
+ p.ww.PushWorker(w)
+ return EmptyPayload, err
}
- p.discardWorker(w, err)
- return nil, err
+ sw.State().Set(StateInvalid)
+ errS := w.Stop(ctx)
+ if errS != nil {
+ return EmptyPayload, fmt.Errorf("%v, %v", err, errS)
+ }
+
+ return EmptyPayload, 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) {
- 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")
+ w.State().Set(StateInvalid)
+ err = w.Stop(ctx)
+ if err != nil {
+ panic(err)
}
+ return p.Exec(ctx, rqs)
}
- 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
+ err = p.ww.AllocateNew(ctx)
+ if err != nil {
+ return EmptyPayload, err
+ }
+ } else {
+ p.muw.Lock()
+ p.ww.PushWorker(w)
+ p.muw.Unlock()
}
-
- p.free <- w
+ return rsp, nil
}
-// 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
+// Destroy all underlying stack (but let them to complete the task).
+func (p *StaticPool) Destroy(ctx context.Context) {
+ p.ww.Destroy(ctx)
}
-// gentry remove worker
-func (p *StaticPool) discardWorker(w *Worker, caused interface{}) {
- w.markInvalid()
- go p.destroyWorker(w, caused)
+func (p *StaticPool) Events() chan PoolEvent {
+ return p.events
}
-// destroyWorker destroys workers and removes it from the pool.
-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)
+// allocate required number of stack
+func (p *StaticPool) allocateWorkers(ctx context.Context, numWorkers int64) ([]WorkerBase, error) {
+ var workers []WorkerBase
- 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})
+ // constant number of stack simplify logic
+ for i := int64(0); i < numWorkers; i++ {
+ ctx, cancel := context.WithTimeout(ctx, p.cfg.AllocateTimeout)
+ w, err := p.factory.SpawnWorkerWithContext(ctx, p.cmd())
+ if err != nil {
+ cancel()
+ return nil, err
}
-
- p.throw(EventWorkerKill, w)
+ cancel()
+ workers = append(workers, w)
}
+ return workers, nil
}
-// 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) checkMaxJobs(ctx context.Context, w WorkerBase) error {
+ if p.cfg.MaxJobs != 0 && w.State().NumExecs() >= p.cfg.MaxJobs {
+ err := p.ww.AllocateNew(ctx)
+ if err != nil {
+ return 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()
+ return nil
}
diff --git a/static_pool_test.go b/static_pool_test.go
index 59822186..a2daedd6 100644
--- a/static_pool_test.go
+++ b/static_pool_test.go
@@ -1,43 +1,49 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
+ "context"
+ "fmt"
"log"
"os/exec"
"runtime"
"strconv"
- "strings"
"sync"
"testing"
"time"
+
+ "github.com/stretchr/testify/assert"
)
var cfg = Config{
NumWorkers: int64(runtime.NumCPU()),
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 5,
}
func Test_NewPool(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
assert.Equal(t, cfg, p.Config())
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
}
func Test_StaticPool_Invalid(t *testing.T) {
p, err := NewPool(
+ context.Background(),
func() *exec.Cmd { return exec.Command("php", "tests/invalid.php") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.Nil(t, p)
@@ -46,9 +52,10 @@ func Test_StaticPool_Invalid(t *testing.T) {
func Test_ConfigError(t *testing.T) {
p, err := NewPool(
+ context.Background(),
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
},
@@ -59,18 +66,20 @@ func Test_ConfigError(t *testing.T) {
}
func Test_StaticPool_Echo(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
- res, err := p.Exec(&Payload{Body: []byte("hello")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -81,18 +90,20 @@ func Test_StaticPool_Echo(t *testing.T) {
}
func Test_StaticPool_Echo_NilContext(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
- res, err := p.Exec(&Payload{Body: []byte("hello"), Context: nil})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello"), Context: nil})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -103,18 +114,20 @@ func Test_StaticPool_Echo_NilContext(t *testing.T) {
}
func Test_StaticPool_Echo_Context(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "head", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
- res, err := p.Exec(&Payload{Body: []byte("hello"), Context: []byte("world")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello"), Context: []byte("world")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -125,66 +138,81 @@ func Test_StaticPool_Echo_Context(t *testing.T) {
}
func Test_StaticPool_JobError(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "error", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
- res, err := p.Exec(&Payload{Body: []byte("hello")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.Error(t, err)
- assert.Nil(t, res)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
- assert.IsType(t, JobError{}, err)
+ assert.IsType(t, TaskError{}, err)
assert.Equal(t, "hello", err.Error())
}
func Test_StaticPool_Broken_Replace(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "broken", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
assert.NotNil(t, p)
- done := make(chan interface{})
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
- p.Listen(func(e int, ctx interface{}) {
- if err, ok := ctx.(error); ok {
- if strings.Contains(err.Error(), "undefined_function()") {
- close(done)
+ go func() {
+ for {
+ select {
+ case ev := <-p.Events():
+ wev := ev.Payload.(WorkerEvent)
+ if _, ok := wev.Payload.([]byte); ok {
+ assert.Contains(t, string(wev.Payload.([]byte)), "undefined_function()")
+ wg.Done()
+ return
+ }
}
}
- })
+ }()
- res, err := p.Exec(&Payload{Body: []byte("hello")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.Error(t, err)
- assert.Nil(t, res)
+ assert.Nil(t, res.Context)
+ assert.Nil(t, res.Body)
+ wg.Wait()
- <-done
- p.Destroy()
+ p.Destroy(ctx)
}
-
+//
func Test_StaticPool_Broken_FromOutside(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
- res, err := p.Exec(&Payload{Body: []byte("hello")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -192,90 +220,74 @@ func Test_StaticPool_Broken_FromOutside(t *testing.T) {
assert.Nil(t, res.Context)
assert.Equal(t, "hello", res.String())
- assert.Equal(t, runtime.NumCPU(), len(p.Workers()))
+ assert.Equal(t, runtime.NumCPU(), len(p.Workers(ctx)))
- destructed := make(chan interface{})
- p.Listen(func(e int, ctx interface{}) {
- if e == EventWorkerConstruct {
- destructed <- nil
+ // Consume pool events
+ go func() {
+ for true {
+ select {
+ case ev := <-p.Events():
+ fmt.Println(ev)
+ }
}
- })
+ }()
// killing random worker and expecting pool to replace it
- err = p.Workers()[0].cmd.Process.Kill()
+ err = p.Workers(ctx)[0].Kill(ctx)
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())
+ time.Sleep(time.Second * 2)
+
+ for _, w := range p.Workers(ctx) {
+ assert.Equal(t, StateReady, w.State().Value())
}
}
func Test_StaticPool_AllocateTimeout(t *testing.T) {
p, err := NewPool(
+ context.Background(),
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
AllocateTimeout: time.Nanosecond * 1,
DestroyTimeout: time.Second * 2,
+ ExecTTL: time.Second * 4,
},
)
- 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()
+ assert.Error(t, err)
+ assert.Nil(t, p)
}
func Test_StaticPool_Replace_Worker(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "pid", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
MaxJobs: 1,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 4,
},
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
var lastPID string
- lastPID = strconv.Itoa(*p.Workers()[0].Pid)
+ lastPID = strconv.Itoa(int(p.Workers(ctx)[0].Pid()))
- res, _ := p.Exec(&Payload{Body: []byte("hello")})
+ res, _ := p.Exec(ctx, 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")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -289,28 +301,34 @@ func Test_StaticPool_Replace_Worker(t *testing.T) {
// identical to replace but controlled on worker side
func Test_StaticPool_Stop_Worker(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "stop", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 15,
},
)
assert.NoError(t, err)
- defer p.Destroy()
+ defer p.Destroy(ctx)
assert.NotNil(t, p)
var lastPID string
- lastPID = strconv.Itoa(*p.Workers()[0].Pid)
+ lastPID = strconv.Itoa(int(p.Workers(ctx)[0].Pid()))
- res, _ := p.Exec(&Payload{Body: []byte("hello")})
+ res, err := p.Exec(ctx, 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{Body: []byte("hello")})
+ res, err := p.Exec(ctx, Payload{Body: []byte("hello")})
assert.NoError(t, err)
assert.NotNil(t, res)
@@ -324,33 +342,39 @@ func Test_StaticPool_Stop_Worker(t *testing.T) {
// identical to replace but controlled on worker side
func Test_Static_Pool_Destroy_And_Close(t *testing.T) {
+ ctx := context.Background()
p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 4,
},
)
assert.NotNil(t, p)
assert.NoError(t, err)
- p.Destroy()
- _, err = p.Exec(&Payload{Body: []byte("100")})
+ p.Destroy(ctx)
+ _, err = p.Exec(ctx, 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 := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 4,
},
)
@@ -358,113 +382,106 @@ func Test_Static_Pool_Destroy_And_Close_While_Wait(t *testing.T) {
assert.NoError(t, err)
go func() {
- _, err := p.Exec(&Payload{Body: []byte("100")})
+ _, err := p.Exec(ctx, 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")})
+ p.Destroy(ctx)
+ _, err = p.Exec(ctx, 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)
-}
+// TODO inconsistent state
+//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.stack {
+// 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(
+ context.Background(),
func() *exec.Cmd { return exec.Command("php", "tests/slow-destroy.php", "echo", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 5,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 5,
},
)
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
- }
+ p.Destroy(context.Background())
}
func Benchmark_Pool_Echo(b *testing.B) {
- p, _ := NewPool(
+ ctx := context.Background()
+ p, err := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- cfg,
+ &cfg,
)
- defer p.Destroy()
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ResetTimer()
+ b.ReportAllocs()
for n := 0; n < b.N; n++ {
- if _, err := p.Exec(&Payload{Body: []byte("hello")}); err != nil {
+ if _, err := p.Exec(ctx, Payload{Body: []byte("hello")}); err != nil {
b.Fail()
}
}
}
+//
func Benchmark_Pool_Echo_Batched(b *testing.B) {
+ ctx := context.Background()
p, _ := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: int64(runtime.NumCPU()),
AllocateTimeout: time.Second * 100,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 5,
},
)
- defer p.Destroy()
+ 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{Body: []byte("hello")}); err != nil {
+ if _, err := p.Exec(ctx, Payload{Body: []byte("hello")}); err != nil {
b.Fail()
log.Println(err)
}
@@ -474,21 +491,27 @@ func Benchmark_Pool_Echo_Batched(b *testing.B) {
wg.Wait()
}
+//
func Benchmark_Pool_Echo_Replaced(b *testing.B) {
+ ctx := context.Background()
p, _ := NewPool(
+ ctx,
func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") },
NewPipeFactory(),
- Config{
+ &Config{
NumWorkers: 1,
MaxJobs: 1,
AllocateTimeout: time.Second,
DestroyTimeout: time.Second,
+ ExecTTL: time.Second * 5,
},
)
- defer p.Destroy()
+ defer p.Destroy(ctx)
+ b.ResetTimer()
+ b.ReportAllocs()
for n := 0; n < b.N; n++ {
- if _, err := p.Exec(&Payload{Body: []byte("hello")}); err != nil {
+ if _, err := p.Exec(ctx, Payload{Body: []byte("hello")}); err != nil {
b.Fail()
log.Println(err)
}
diff --git a/sync_worker.go b/sync_worker.go
new file mode 100644
index 00000000..45629f3e
--- /dev/null
+++ b/sync_worker.go
@@ -0,0 +1,171 @@
+package roadrunner
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/spiral/goridge/v2"
+)
+
+var EmptyPayload = Payload{}
+
+type SyncWorker interface {
+ // WorkerBase provides basic functionality for the SyncWorker
+ WorkerBase
+ // Exec used to execute payload on the SyncWorker
+ Exec(ctx context.Context, rqs Payload) (Payload, error)
+}
+
+type taskWorker struct {
+ w WorkerBase
+}
+
+func NewSyncWorker(w WorkerBase) (SyncWorker, error) {
+ return &taskWorker{
+ w: w,
+ }, nil
+}
+
+type twexec struct {
+ payload Payload
+ err error
+}
+
+func (tw *taskWorker) Exec(ctx context.Context, rqs Payload) (Payload, error) {
+ c := make(chan twexec)
+ go func() {
+ if len(rqs.Body) == 0 && len(rqs.Context) == 0 {
+ c <- twexec{
+ payload: EmptyPayload,
+ err: fmt.Errorf("payload can not be empty"),
+ }
+ return
+ }
+
+ if tw.w.State().Value() != StateReady {
+ c <- twexec{
+ payload: EmptyPayload,
+ err: fmt.Errorf("WorkerProcess 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(StateWorking)
+
+ rsp, err := tw.execPayload(rqs)
+ if err != nil {
+ if _, ok := err.(TaskError); !ok {
+ tw.w.State().Set(StateErrored)
+ tw.w.State().RegisterExec()
+ }
+ c <- twexec{
+ payload: EmptyPayload,
+ err: err,
+ }
+ return
+ }
+
+ tw.w.State().Set(StateReady)
+ tw.w.State().RegisterExec()
+ c <- twexec{
+ payload: rsp,
+ err: nil,
+ }
+ return
+ }()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return EmptyPayload, ctx.Err()
+ case res := <-c:
+ if res.err != nil {
+ return EmptyPayload, res.err
+ }
+
+ return res.payload, nil
+ }
+ }
+}
+
+func (tw *taskWorker) execPayload(rqs Payload) (Payload, error) {
+ // two things; todo: merge
+ if err := sendControl(tw.w.Relay(), rqs.Context); err != nil {
+ return EmptyPayload, errors.Wrap(err, "header error")
+ }
+
+ if err := tw.w.Relay().Send(rqs.Body, 0); err != nil {
+ return EmptyPayload, errors.Wrap(err, "sender error")
+ }
+
+ var pr goridge.Prefix
+ rsp := Payload{}
+
+ var err error
+ if rsp.Context, pr, err = tw.w.Relay().Receive(); err != nil {
+ return EmptyPayload, errors.Wrap(err, "WorkerProcess error")
+ }
+
+ if !pr.HasFlag(goridge.PayloadControl) {
+ return EmptyPayload, fmt.Errorf("malformed WorkerProcess response")
+ }
+
+ if pr.HasFlag(goridge.PayloadError) {
+ return EmptyPayload, TaskError(rsp.Context)
+ }
+
+ // add streaming support :)
+ if rsp.Body, pr, err = tw.w.Relay().Receive(); err != nil {
+ return EmptyPayload, errors.Wrap(err, "WorkerProcess error")
+ }
+
+ return rsp, nil
+}
+
+func (tw *taskWorker) String() string {
+ return tw.w.String()
+}
+
+func (tw *taskWorker) Created() time.Time {
+ return tw.w.Created()
+}
+
+func (tw *taskWorker) Events() <-chan WorkerEvent {
+ return tw.w.Events()
+}
+
+func (tw *taskWorker) Pid() int64 {
+ return tw.w.Pid()
+}
+
+func (tw *taskWorker) State() State {
+ return tw.w.State()
+}
+
+func (tw *taskWorker) Start() error {
+ return tw.w.Start()
+}
+
+func (tw *taskWorker) Wait(ctx context.Context) error {
+ return tw.w.Wait(ctx)
+}
+
+func (tw *taskWorker) Stop(ctx context.Context) error {
+ return tw.w.Stop(ctx)
+}
+
+func (tw *taskWorker) Kill(ctx context.Context) error {
+ return tw.w.Kill(ctx)
+}
+
+func (tw *taskWorker) Relay() goridge.Relay {
+ return tw.w.Relay()
+}
+
+func (tw *taskWorker) AttachRelay(rl goridge.Relay) {
+ tw.w.AttachRelay(rl)
+}
diff --git a/sync_worker_test.go b/sync_worker_test.go
new file mode 100644
index 00000000..e1cec4b6
--- /dev/null
+++ b/sync_worker_test.go
@@ -0,0 +1,264 @@
+package roadrunner
+
+import (
+ "context"
+ "errors"
+ "os/exec"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_Echo(t *testing.T) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ res, err := syncWorker.Exec(ctx, 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) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+
+ w, _ := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ res, err := syncWorker.Exec(ctx, EmptyPayload)
+
+ assert.Error(t, err)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
+
+ 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, _ := 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) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+
+ w, _ := InitBaseWorker(cmd)
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := syncWorker.Exec(ctx, Payload{Body: []byte("hello")})
+
+ assert.Error(t, err)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
+
+ assert.Equal(t, "WorkerProcess is not ready (inactive)", err.Error())
+}
+
+func Test_String(t *testing.T) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+
+ w, _ := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: 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().SpawnWorkerWithContext(ctx, cmd)
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := syncWorker.Exec(ctx, 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) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "broken", "pipes")
+
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ assert.NotNil(t, w)
+ tt := time.NewTimer(time.Second * 10)
+ defer wg.Done()
+ for {
+ select {
+ case ev := <-w.Events():
+ assert.Contains(t, string(ev.Payload.([]byte)), "undefined_function()")
+ return
+ case <-tt.C:
+ assert.Error(t, errors.New("no events from worker"))
+ return
+ }
+ }
+ }()
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := syncWorker.Exec(ctx, Payload{Body: []byte("hello")})
+ assert.NotNil(t, err)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
+
+ wg.Wait()
+ assert.Error(t, w.Stop(ctx))
+}
+
+func Test_Error(t *testing.T) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "error", "pipes")
+
+ w, _ := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := syncWorker.Exec(ctx, Payload{Body: []byte("hello")})
+ assert.NotNil(t, err)
+ assert.Nil(t, res.Body)
+ assert.Nil(t, res.Context)
+
+ assert.IsType(t, TaskError{}, err)
+ assert.Equal(t, "hello", err.Error())
+}
+
+func Test_NumExecs(t *testing.T) {
+ ctx := context.Background()
+ cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+
+ w, _ := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ go func() {
+ assert.NoError(t, w.Wait(ctx))
+ }()
+ defer func() {
+ err := w.Stop(ctx)
+ if err != nil {
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
+ }
+ }()
+
+ syncWorker, err := NewSyncWorker(w)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = syncWorker.Exec(ctx, 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(ctx, 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(ctx, 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/util/doc.go b/util/doc.go
new file mode 100644
index 00000000..c6006de4
--- /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..005c430e 100644
--- a/osutil/isolate.go
+++ b/util/isolate.go
@@ -1,6 +1,6 @@
// +build !windows
-package osutil
+package util
import (
"fmt"
diff --git a/osutil/isolate_win.go b/util/isolate_win.go
index 52fb5d8a..77674b3b 100644
--- a/osutil/isolate_win.go
+++ b/util/isolate_win.go
@@ -1,6 +1,6 @@
// +build windows
-package osutil
+package util
import (
"os/exec"
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
index 3740aee8..855a9958 100644
--- a/worker.go
+++ b/worker.go
@@ -1,6 +1,8 @@
package roadrunner
import (
+ "context"
+ "errors"
"fmt"
"os"
"os/exec"
@@ -9,36 +11,102 @@ import (
"sync"
"time"
- "github.com/pkg/errors"
"github.com/spiral/goridge/v2"
+ "go.uber.org/multierr"
)
-// 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
+// EventWorkerKill thrown after WorkerProcess is being forcefully killed.
+const (
+ // EventWorkerError triggered after WorkerProcess. Except payload to be error.
+ EventWorkerError int64 = iota + 100
+
+ // EventWorkerLog triggered on every write to WorkerProcess StdErr pipe (batched). Except payload to be []byte string.
+ EventWorkerLog
+
+ // EventWorkerWaitDone triggered when worker exit from process Wait
+ EventWorkerWaitDone
+
+ EventWorkerBufferClosed
+
+ EventRelayCloseError
+
+ EventWorkerProcessError
+)
+
+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 = 100 * time.Millisecond
+)
+
+// todo: write comment
+type WorkerEvent struct {
+ Event int64
+ Worker WorkerBase
+ Payload interface{}
+}
+
+type WorkerBase interface {
+ fmt.Stringer
+
+ Created() time.Time
+
+ Events() <-chan WorkerEvent
- // Created indicates at what time worker has been created.
- Created time.Time
+ Pid() int64
- // state holds information about current worker state,
- // number of worker executions, buf status change 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() 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(ctx context.Context) error
+
+ // Stop sends soft termination command to the WorkerProcess and waits for process completion.
+ Stop(ctx context.Context) 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(ctx context.Context) error
+ // Relay returns attached to worker goridge relay
+ Relay() goridge.Relay
+ // AttachRelay used to attach goridge relay to the worker process
+ AttachRelay(rl goridge.Relay)
+}
+
+// WorkerProcess - supervised process with api over goridge.Relay.
+type WorkerProcess struct {
+ // created indicates at what time WorkerProcess has been created.
+ created time.Time
+
+ // updates parent supervisor or pool about WorkerProcess events
+ events chan WorkerEvent
+
+ // state holds information about current WorkerProcess state,
+ // number of WorkerProcess 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.
+ // provided to WorkerProcess from outside in non-started form. CmdSource
+ // stdErr direction will be handled by WorkerProcess to aggregate error message.
cmd *exec.Cmd
- // err aggregates stderr output from underlying process. Value can be
+ // pid of the process, points to pid of underlying process and
+ // can be nil while process is not started.
+ pid int
+
+ // errBuffer aggregates stderr output from underlying process. Value can be
// receive only once command is completed and all pipes are closed.
- err *errBuffer
+ errBuffer *errBuffer
// channel is being closed once command is complete.
- waitDone chan interface{}
+ // waitDone chan interface{}
// contains information about resulted process state.
endState *os.ProcessState
@@ -47,212 +115,283 @@ type Worker struct {
mu sync.Mutex
// communication bus with underlying process.
- rl goridge.Relay
+ relay goridge.Relay
}
-// newWorker creates new worker over given exec.cmd.
-func newWorker(cmd *exec.Cmd) (*Worker, error) {
+// InitBaseWorker creates new WorkerProcess over given exec.cmd.
+func InitBaseWorker(cmd *exec.Cmd) (WorkerBase, 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),
+ w := &WorkerProcess{
+ created: time.Now(),
+ events: make(chan WorkerEvent, 10),
+ cmd: cmd,
+ state: newState(StateInactive),
}
+ w.errBuffer = newErrBuffer(w.logCallback)
+
// piping all stderr to command errBuffer
- w.cmd.Stderr = w.err
+ w.cmd.Stderr = w.errBuffer
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 {
+func (w *WorkerProcess) Created() time.Time {
+ return w.created
+}
+
+func (w *WorkerProcess) Pid() int64 {
+ return int64(w.pid)
+}
+
+// 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.
+func (w *WorkerProcess) 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)
+// 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.
+func (w *WorkerProcess) AttachRelay(rl goridge.Relay) {
+ w.relay = rl
+}
+
+// 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.
+func (w *WorkerProcess) Relay() goridge.Relay {
+ return w.relay
+}
+
+// String returns WorkerProcess description. fmt.Stringer interface
+func (w *WorkerProcess) 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, " "),
- state,
+ st,
w.state.NumExecs(),
)
}
-// Wait must be called once for each worker, call will be released once worker is
+func (w *WorkerProcess) Start() error {
+ err := w.cmd.Start()
+ if err != nil {
+ return err
+ }
+
+ w.pid = w.cmd.Process.Pid
+
+ return nil
+}
+
+func (w *WorkerProcess) Events() <-chan WorkerEvent {
+ return w.events
+}
+
+// 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.
-func (w *Worker) Wait() error {
- <-w.waitDone
+// to find or Start the script.
+func (w *WorkerProcess) Wait(ctx context.Context) error {
+ c := make(chan error)
+ go func() {
+ err := multierr.Combine(w.cmd.Wait())
+ w.endState = w.cmd.ProcessState
+ if err != nil {
+ w.state.Set(StateErrored)
+ // if there are messages in the events channel, read it
+ // TODO potentially danger place
+ if len(w.events) > 0 {
+ select {
+ case ev := <-w.events:
+ err = multierr.Append(err, errors.New(string(ev.Payload.([]byte))))
+ }
+ }
+ // if no errors in the events, error might be in the errbuffer
+ if w.errBuffer.Len() > 0 {
+ err = multierr.Append(err, errors.New(w.errBuffer.String()))
+ }
- // ensure that all receive/send operations are complete
- w.mu.Lock()
- defer w.mu.Unlock()
+ c <- multierr.Append(err, w.closeRelay())
+ return
+ }
- if w.endState.Success() {
- w.state.set(StateStopped)
- return nil
- }
+ err = multierr.Append(err, w.closeRelay())
+ if err != nil {
+ w.state.Set(StateErrored)
+ c <- err
+ return
+ }
- if w.state.Value() != StateStopping {
- w.state.set(StateErrored)
- } else {
- w.state.set(StateStopped)
+ if w.endState.Success() {
+ w.state.Set(StateStopped)
+ }
+ c <- nil
+ }()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-c:
+ return err
+ }
}
+}
- if w.err.Len() != 0 {
- return errors.New(w.err.String())
+func (w *WorkerProcess) closeRelay() error {
+ if w.relay != nil {
+ err := w.relay.Close()
+ if err != nil {
+ return err
+ }
}
-
- // generic process error
- return &exec.ExitError{ProcessState: w.endState}
+ return nil
}
-// 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:
+// Stop sends soft termination command to the WorkerProcess and waits for process completion.
+func (w *WorkerProcess) Stop(ctx context.Context) error {
+ c := make(chan error)
+ go func() {
+ var errs []string
+ w.errBuffer.Close()
+ w.state.Set(StateStopping)
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 {
+ err := sendControl(w.relay, &stopCommand{Stop: true})
+ if err != nil {
+ errs = append(errs, err.Error())
+ w.state.Set(StateKilling)
+ err = w.cmd.Process.Kill()
+ if err != nil {
+ errs = append(errs, err.Error())
+ }
+ c <- errors.New(strings.Join(errs, "|"))
+ }
+ w.state.Set(StateStopped)
+ c <- nil
+ }()
select {
- case <-w.waitDone:
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-c:
+ if err != nil {
+ return err
+ }
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) {
+// 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 *WorkerProcess) Kill(ctx context.Context) error {
+ w.state.Set(StateKilling)
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)
+ defer w.mu.Unlock()
+ err := w.cmd.Process.Signal(os.Kill)
if err != nil {
- if _, ok := err.(JobError); !ok {
- w.state.set(StateErrored)
- w.state.registerExec()
- w.mu.Unlock()
- return nil, err
- }
+ return err
}
+ w.state.Set(StateStopped)
+ return nil
+}
- w.state.set(StateReady)
- w.state.registerExec()
- w.mu.Unlock()
- return rsp, err
+func (w *WorkerProcess) logCallback(log []byte) {
+ w.events <- WorkerEvent{Event: EventWorkerLog, Worker: w, Payload: log}
}
-func (w *Worker) markInvalid() {
- w.state.set(StateInvalid)
+// thread safe errBuffer
+type errBuffer struct {
+ mu sync.RWMutex
+ buf []byte
+ last int
+ wait *time.Timer
+ // todo remove update
+ update chan interface{}
+ stop chan interface{}
+ logCallback func(log []byte)
}
-func (w *Worker) start() error {
- if err := w.cmd.Start(); err != nil {
- close(w.waitDone)
- return err
+func newErrBuffer(logCallback func(log []byte)) *errBuffer {
+ eb := &errBuffer{
+ buf: make([]byte, 0),
+ update: make(chan interface{}),
+ wait: time.NewTimer(WaitDuration),
+ stop: make(chan interface{}),
+ logCallback: logCallback,
}
- 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})
+ go func(eb *errBuffer) {
+ for {
+ select {
+ case <-eb.update:
+ eb.wait.Reset(WaitDuration)
+ case <-eb.wait.C:
+ eb.mu.Lock()
+ if len(eb.buf) > eb.last {
+ eb.logCallback(eb.buf[eb.last:])
+ eb.buf = eb.buf[0:0]
+ eb.last = len(eb.buf)
}
- }
-
- err := w.err.Close()
- if err != nil {
- w.err.lsn(EventWorkerError, WorkerError{Worker: w, Caused: err})
+ eb.mu.Unlock()
+ case <-eb.stop:
+ eb.wait.Stop()
+
+ eb.mu.Lock()
+ if len(eb.buf) > eb.last {
+ if eb == nil || eb.logCallback == nil {
+ eb.mu.Unlock()
+ return
+ }
+ eb.logCallback(eb.buf[eb.last:])
+ eb.last = len(eb.buf)
+ }
+ eb.mu.Unlock()
+ return
}
}
- }()
+ }(eb)
- return nil
+ return eb
}
-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")
- }
+// 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.RLock()
+ defer eb.mu.RUnlock()
- if err = w.rl.Send(rqs.Body, 0); err != nil {
- return nil, errors.Wrap(err, "sender error")
- }
+ // currently active message
+ return len(eb.buf)
+}
- var pr goridge.Prefix
- rsp = new(Payload)
+// 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 (eb *errBuffer) Write(p []byte) (int, error) {
+ eb.mu.Lock()
+ eb.buf = append(eb.buf, p...)
+ eb.mu.Unlock()
+ eb.update <- nil
- 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")
- }
+ return len(p), nil
+}
- if pr.HasFlag(goridge.PayloadError) {
- return nil, JobError(rsp.Context)
- }
+// Strings fetches all errBuffer data into string.
+func (eb *errBuffer) String() string {
+ eb.mu.Lock()
+ defer eb.mu.Unlock()
- // add streaming support :)
- if rsp.Body, pr, err = w.rl.Receive(); err != nil {
- return nil, errors.Wrap(err, "worker error")
- }
+ // TODO unsafe operation, use runes
+ return string(eb.buf)
+}
- return rsp, nil
+// Close aggregation timer.
+func (eb *errBuffer) Close() {
+ close(eb.stop)
}
diff --git a/worker_test.go b/worker_test.go
index c21e67cb..a90b7ef2 100644
--- a/worker_test.go
+++ b/worker_test.go
@@ -1,18 +1,21 @@
package roadrunner
import (
- "github.com/stretchr/testify/assert"
+ "context"
"os/exec"
+ "sync"
"testing"
- "time"
+
+ "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().SpawnWorker(cmd)
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
go func() {
- assert.NoError(t, w.Wait())
+ assert.NoError(t, w.Wait(ctx))
assert.Equal(t, StateStopped, w.State().Value())
}()
@@ -20,229 +23,155 @@ func Test_GetState(t *testing.T) {
assert.NotNil(t, w)
assert.Equal(t, StateReady, w.State().Value())
- err = w.Stop()
+ err = w.Stop(ctx)
if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
+ t.Errorf("error stopping the WorkerProcess: error %v", err)
}
}
func Test_Kill(t *testing.T) {
+ ctx := context.Background()
cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd)
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
go func() {
- assert.Error(t, w.Wait())
- assert.Equal(t, StateStopped, w.State().Value())
+ defer wg.Done()
+ assert.Error(t, w.Wait(ctx))
+ // TODO changed from stopped, discuss
+ assert.Equal(t, StateErrored, 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)
- }
- }()
+ err = w.Kill(ctx)
+ if err != nil {
+ t.Errorf("error killing the WorkerProcess: error %v", err)
+ }
+ wg.Wait()
}
-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")})
+func Test_OnStarted(t *testing.T) {
+ cmd := exec.Command("php", "tests/client.php", "broken", "pipes")
+ assert.Nil(t, cmd.Start())
- assert.Nil(t, err)
- assert.NotNil(t, res)
- assert.NotNil(t, res.Body)
- assert.Nil(t, res.Context)
+ w, err := InitBaseWorker(cmd)
+ assert.Nil(t, w)
+ assert.NotNil(t, err)
- assert.Equal(t, "hello", res.String())
+ assert.Equal(t, "can't attach to running process", err.Error())
}
-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())
- }()
+func TestErrBuffer_Write_Len(t *testing.T) {
+ buf := newErrBuffer(nil)
defer func() {
- err := w.Stop()
- if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
- }
+ buf.Close()
}()
- 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")
+ _, 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 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)
+func TestErrBuffer_Write_Event(t *testing.T) {
+ buf := newErrBuffer(nil)
+ defer func() {
+ buf.Close()
+ }()
- assert.Equal(t, "worker is not ready (inactive)", err.Error())
-}
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ buf.logCallback = func(log []byte) {
+ assert.Equal(t, []byte("hello\n"), log)
+ wg.Done()
+ }
-func Test_String(t *testing.T) {
- cmd := exec.Command("php", "tests/client.php", "echo", "pipes")
+ _, err := buf.Write([]byte("hello\n"))
+ if err != nil {
+ t.Errorf("fail to write: error %v", err)
+ }
- 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)
- }
- }()
+ wg.Wait()
- assert.Contains(t, w.String(), "php tests/client.php echo pipes")
- assert.Contains(t, w.String(), "ready")
- assert.Contains(t, w.String(), "numExecs: 0")
+ // messages are read
+ assert.Equal(t, 0, buf.Len())
}
-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())
- }()
+func TestErrBuffer_Write_Event_Separated(t *testing.T) {
+ buf := newErrBuffer(nil)
defer func() {
- err := w.Stop()
- if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
- }
+ buf.Close()
}()
- 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")
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
- w, err := NewPipeFactory().SpawnWorker(cmd)
+ buf.logCallback = func(log []byte) {
+ assert.Equal(t, []byte("hello\nending"), log)
+ wg.Done()
+ }
+ _, err := buf.Write([]byte("hel"))
if err != nil {
- t.Fatal(err)
+ t.Errorf("fail to write: error %v", 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())
+ _, err = buf.Write([]byte("lo\n"))
+ if err != nil {
+ t.Errorf("fail to write: error %v", err)
+ }
- w, err := newWorker(cmd)
- assert.Nil(t, w)
- assert.NotNil(t, err)
+ _, err = buf.Write([]byte("ending"))
+ if err != nil {
+ t.Errorf("fail to write: error %v", err)
+ }
- assert.Equal(t, "can't attach to running process", err.Error())
+ wg.Wait()
+ assert.Equal(t, 0, buf.Len())
+ assert.Equal(t, "", buf.String())
}
-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())
- }()
-
+func TestErrBuffer_Write_Event_Separated_NoListener(t *testing.T) {
+ buf := newErrBuffer(nil)
defer func() {
- err := w.Stop()
- if err != nil {
- t.Errorf("error stopping the worker: error %v", err)
- }
+ buf.Close()
}()
- 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 := buf.Write([]byte("hel"))
+ if err != nil {
+ t.Errorf("fail to write: error %v", err)
+ }
- _, err := w.Exec(&Payload{Body: []byte("hello")})
+ _, err = buf.Write([]byte("lo\n"))
if err != nil {
- t.Errorf("fail to execute payload: error %v", err)
+ t.Errorf("fail to write: error %v", err)
}
- assert.Equal(t, int64(1), w.State().NumExecs())
- _, err = w.Exec(&Payload{Body: []byte("hello")})
+ _, err = buf.Write([]byte("ending"))
if err != nil {
- t.Errorf("fail to execute payload: error %v", err)
+ t.Errorf("fail to write: error %v", err)
}
- assert.Equal(t, int64(2), w.State().NumExecs())
- _, err = w.Exec(&Payload{Body: []byte("hello")})
+ assert.Equal(t, 12, buf.Len())
+ assert.Equal(t, "hello\nending", buf.String())
+}
+
+func TestErrBuffer_Write_Remaining(t *testing.T) {
+ buf := newErrBuffer(nil)
+ defer func() {
+ buf.Close()
+ }()
+
+ _, err := buf.Write([]byte("hel"))
if err != nil {
- t.Errorf("fail to execute payload: error %v", err)
+ t.Errorf("fail to write: error %v", err)
}
- assert.Equal(t, int64(3), w.State().NumExecs())
+
+ assert.Equal(t, 3, buf.Len())
+ assert.Equal(t, "hel", buf.String())
}
diff --git a/workers_watcher.go b/workers_watcher.go
new file mode 100644
index 00000000..f8522c46
--- /dev/null
+++ b/workers_watcher.go
@@ -0,0 +1,323 @@
+package roadrunner
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "time"
+)
+
+var ErrWatcherStopped = errors.New("watcher stopped")
+
+type Stack struct {
+ workers []WorkerBase
+ mutex sync.RWMutex
+ destroy bool
+}
+
+func NewWorkersStack() *Stack {
+ return &Stack{
+ workers: make([]WorkerBase, 0, 12),
+ }
+}
+
+func (stack *Stack) Reset() {
+ stack.mutex.Lock()
+ defer stack.mutex.Unlock()
+
+ stack.workers = nil
+}
+
+func (stack *Stack) Push(w WorkerBase) {
+ stack.mutex.Lock()
+ defer stack.mutex.Unlock()
+ 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() (WorkerBase, 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
+ }
+
+ w := stack.workers[len(stack.workers)-1]
+ stack.workers = stack.workers[:len(stack.workers)-1]
+
+ return w, false
+}
+
+type WorkersWatcher struct {
+ mutex sync.Mutex
+ stack *Stack
+ allocator func(args ...interface{}) (*SyncWorker, error)
+ initialNumWorkers int64
+ actualNumWorkers int64
+ events chan PoolEvent
+}
+
+type WorkerWatcher interface {
+ // AddToWatch used to add stack to wait its state
+ AddToWatch(ctx context.Context, workers []WorkerBase) error
+ // GetFreeWorker provide first free worker
+ GetFreeWorker(ctx context.Context) (WorkerBase, error)
+ // PutWorker enqueues worker back
+ PushWorker(w WorkerBase)
+ // AllocateNew used to allocate new worker and put in into the WorkerWatcher
+ AllocateNew(ctx context.Context) error
+ // Destroy destroys the underlying stack
+ Destroy(ctx context.Context)
+ // WorkersList return all stack w/o removing it from internal storage
+ WorkersList(ctx context.Context) []WorkerBase
+ // RemoveWorker remove worker from the stack
+ RemoveWorker(ctx context.Context, wb WorkerBase) error
+}
+
+// workerCreateFunc can be nil, but in that case, dead stack will not be replaced
+func NewWorkerWatcher(allocator func(args ...interface{}) (*SyncWorker, error), numWorkers int64, events chan PoolEvent) *WorkersWatcher {
+ // todo check if events not nil
+ ww := &WorkersWatcher{
+ stack: NewWorkersStack(),
+ allocator: allocator,
+ initialNumWorkers: numWorkers,
+ actualNumWorkers: numWorkers,
+ events: events,
+ }
+
+ return ww
+}
+
+func (ww *WorkersWatcher) AddToWatch(ctx context.Context, workers []WorkerBase) error {
+ for i := 0; i < len(workers); i++ {
+ sw, err := NewSyncWorker(workers[i])
+ if err != nil {
+ return err
+ }
+ ww.stack.Push(sw)
+ go func(swc WorkerBase) {
+ //ww.mutex.Lock()
+ ww.watch(&swc)
+ ww.wait(ctx, &swc)
+ //ww.mutex.Unlock()
+ }(sw)
+ }
+ return nil
+}
+
+func (ww *WorkersWatcher) GetFreeWorker(ctx context.Context) (WorkerBase, error) {
+ // thread safe operation
+ w, stop := ww.stack.Pop()
+ if stop {
+ return nil, ErrWatcherStopped
+ }
+ // handle worker remove state
+ // in this state worker is destroyed by supervisor
+ if w != nil && w.State().Value() == StateRemove {
+ err := ww.RemoveWorker(ctx, w)
+ if err != nil {
+ return nil, err
+ }
+ // try to get next
+ return ww.GetFreeWorker(ctx)
+ }
+ // no free stack
+ if w == nil {
+ tout := time.NewTicker(time.Second * 180)
+ defer tout.Stop()
+ for {
+ select {
+ default:
+ w, stop = ww.stack.Pop()
+ if stop {
+ return nil, ErrWatcherStopped
+ }
+ if w == nil {
+ continue
+ }
+ ww.actualNumWorkers--
+ return w, nil
+ case <-tout.C:
+ return nil, errors.New("no free stack")
+ }
+ }
+ }
+ ww.actualNumWorkers--
+ return w, nil
+}
+
+func (ww *WorkersWatcher) AllocateNew(ctx context.Context) error {
+ ww.stack.mutex.Lock()
+ sw, err := ww.allocator()
+ if err != nil {
+ return err
+ }
+ ww.addToWatch(*sw)
+ ww.stack.mutex.Unlock()
+ ww.PushWorker(*sw)
+ return nil
+}
+
+func (ww *WorkersWatcher) RemoveWorker(ctx context.Context, wb WorkerBase) error {
+ ww.stack.mutex.Lock()
+ defer ww.stack.mutex.Unlock()
+ pid := wb.Pid()
+ for i := 0; i < len(ww.stack.workers); i++ {
+ if ww.stack.workers[i].Pid() == pid {
+ // found in the stack
+ // remove worker
+ ww.stack.workers = append(ww.stack.workers[:i], ww.stack.workers[i+1:]...)
+ ww.decreaseNumOfActualWorkers()
+
+ wb.State().Set(StateInvalid)
+ err := wb.Kill(ctx)
+ if err != nil {
+ return err
+ }
+ break
+ }
+ }
+ // worker currently handle request, set state Remove
+ wb.State().Set(StateRemove)
+ return nil
+}
+
+// O(1) operation
+func (ww *WorkersWatcher) PushWorker(w WorkerBase) {
+ ww.mutex.Lock()
+ ww.actualNumWorkers++
+ ww.mutex.Unlock()
+ ww.stack.Push(w)
+}
+
+func (ww *WorkersWatcher) ReduceWorkersCount() {
+ ww.mutex.Unlock()
+ ww.actualNumWorkers--
+ ww.mutex.Lock()
+}
+
+// Destroy all underlying stack (but let them to complete the task)
+func (ww *WorkersWatcher) Destroy(ctx context.Context) {
+ ww.stack.mutex.Lock()
+ ww.stack.destroy = true
+ ww.stack.mutex.Unlock()
+
+ tt := time.NewTicker(time.Millisecond * 100)
+ for {
+ select {
+ case <-tt.C:
+ if len(ww.stack.workers) != int(ww.actualNumWorkers) {
+ continue
+ }
+ // 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
+ ww.stack.mutex.Lock()
+ for i := 0; i < len(ww.stack.workers); i++ {
+ // set state for the stack in the stack (unused at the moment)
+ ww.stack.workers[i].State().Set(StateDestroyed)
+ }
+ ww.stack.mutex.Unlock()
+ tt.Stop()
+ // clear
+ ww.stack.Reset()
+ return
+ }
+ }
+}
+
+// Warning, this is O(n) operation
+func (ww *WorkersWatcher) WorkersList(ctx context.Context) []WorkerBase {
+ return ww.stack.workers
+}
+
+func (ww *WorkersWatcher) wait(ctx context.Context, w *WorkerBase) {
+ err := (*w).Wait(ctx)
+ if err != nil {
+ ww.events <- PoolEvent{Payload: WorkerEvent{
+ Event: EventWorkerError,
+ Worker: *w,
+ Payload: err,
+ }}
+ }
+ // If not destroyed, reallocate
+ if (*w).State().Value() != StateDestroyed {
+ pid := (*w).Pid()
+ for i := 0; i < len(ww.stack.workers); i++ {
+ // worker in the stack, reallocating
+ if ww.stack.workers[i].Pid() == pid {
+ ww.stack.mutex.Lock()
+ ww.stack.workers = append(ww.stack.workers[:i], ww.stack.workers[i+1:]...)
+
+ ww.decreaseNumOfActualWorkers()
+
+ ww.stack.mutex.Unlock()
+ err = ww.AllocateNew(ctx)
+ if err != nil {
+ ww.events <- PoolEvent{Payload: WorkerEvent{
+ Event: EventWorkerError,
+ Worker: *w,
+ Payload: err,
+ }}
+ return
+ }
+ return
+ }
+ }
+ // worker not in the stack (not returned), forget and allocate new
+ err = ww.AllocateNew(ctx)
+ if err != nil {
+ ww.events <- PoolEvent{Payload: WorkerEvent{
+ Event: EventWorkerError,
+ Worker: *w,
+ Payload: err,
+ }}
+ return
+ }
+ }
+ return
+}
+
+func (ww *WorkersWatcher) addToWatch(wb WorkerBase) {
+ ww.mutex.Lock()
+ defer ww.mutex.Unlock()
+ go func() {
+ ww.wait(context.Background(), &wb)
+ }()
+}
+
+func (ww *WorkersWatcher) reallocate(wb *WorkerBase) error {
+ sw, err := ww.allocator()
+ if err != nil {
+ return err
+ }
+ *wb = *sw
+ return nil
+}
+
+func (ww *WorkersWatcher) decreaseNumOfActualWorkers() {
+ ww.mutex.Lock()
+ ww.actualNumWorkers--
+ ww.mutex.Unlock()
+}
+
+func (ww *WorkersWatcher) watch(swc *WorkerBase) {
+ // todo make event to stop function
+ go func() {
+ select {
+ case ev := <-(*swc).Events():
+ ww.events <- PoolEvent{Payload: ev}
+ }
+ }()
+}