diff options
254 files changed, 12942 insertions, 14615 deletions
diff --git a/.dockerignore b/.dockerignore index b817b3c8..bfa82a3d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,4 +7,4 @@ /tests /bin composer.json -vendor_php +vendor_php
\ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 448a1aa4..448a1aa4 100644..100755 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8c4b568f..8c4b568f 100644..100755 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md diff --git a/.gitignore b/.gitignore index 8a2f7d68..fb4afc29 100644..100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ .idea composer.lock vendor diff --git a/.golangci.yml b/.golangci.yml new file mode 100755 index 00000000..7a49f0c8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,62 @@ +run: + skip-files: + - plugins/http/tests/http_test.go + - plugins/http/tests/plugin_test_old.go + - plugins/http/tests/rpc_test_old.go + - plugins/http/tests/config_test.go + - plugins/static/tests/static_plugin_test.go + - plugins/headers/tests/old.go +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + # - dupl + - errcheck + - exhaustive + # - funlen + - gochecknoinits + # - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + # - gomnd + - goprintffuncname + - gosec + # - gosimple + - govet + - ineffassign + - interfacer + # - lll + - misspell + - nakedret + # - noctx + - nolintlint + - rowserrcheck + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + # - unparam + # - unused + - varcheck + - whitespace + + # don't enable: + # - asciicheck + # - gochecknoglobals + # - gocognit + # - godot + # - godox + # - goerr113 + # - maligned + # - nestif + # - prealloc + # - testpackage + # - wsl @@ -1,193 +1,39 @@ -# 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] + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" -# http service configuration. http: - # http host to listen. - address: 0.0.0.0:8080 + debug: true + address: 127.0.0.1:18903 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s 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 + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt fcgi: - # FastCGI connection DSN. Supported TCP and Unix sockets. - address: tcp://0.0.0.0:6920 - - # HTTP service provides HTTP2 transport + address: tcp://0.0.0.0:7921 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 + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ae0b283a..ae0b283a 100644..100755 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md diff --git a/README.md b/README.md deleted file mode 100644 index 4db5d856..00000000 --- a/README.md +++ /dev/null @@ -1,118 +0,0 @@ -<p align="center"> - <img src="https://user-images.githubusercontent.com/796136/50286124-6f7f3780-046f-11e9-9f45-e8fedd4f786d.png" height="75px" alt="RoadRunner"> -</p> -<p align="center"> - <a href="https://packagist.org/packages/spiral/roadrunner"><img src="https://poser.pugx.org/spiral/roadrunner/version"></a> - <a href="https://pkg.go.dev/github.com/spiral/roadrunner?tab=doc"><img src="https://godoc.org/github.com/spiral/roadrunner?status.svg"></a> - <a href="https://github.com/spiral/roadrunner/actions"><img src="https://github.com/spiral/roadrunner/workflows/CI/badge.svg" alt=""></a> - <a href="https://goreportcard.com/report/github.com/spiral/roadrunner"><img src="https://goreportcard.com/badge/github.com/spiral/roadrunner"></a> - <a href="https://scrutinizer-ci.com/g/spiral/roadrunner/?branch=master"><img src="https://scrutinizer-ci.com/g/spiral/roadrunner/badges/quality-score.png"></a> - <a href="https://codecov.io/gh/spiral/roadrunner/"><img src="https://codecov.io/gh/spiral/roadrunner/branch/master/graph/badge.svg"></a> - <a href="https://lgtm.com/projects/g/spiral/roadrunner/alerts/"><img alt="Total alerts" src="https://img.shields.io/lgtm/alerts/g/spiral/roadrunner.svg?logo=lgtm&logoWidth=18"/></a> - <a href="https://discord.gg/TFeEmCs"><img src="https://img.shields.io/badge/discord-chat-magenta.svg"></a> -</p> - -RoadRunner is an open-source (MIT licensed) high-performance PHP application server, load balancer, and process manager. -It supports running as a service with the ability to extend its functionality on a per-project basis. - -RoadRunner includes PSR-7/PSR-17 compatible HTTP and HTTP/2 server and can be used to replace classic Nginx+FPM setup with much greater performance and flexibility. - -<p align="center"> - <a href="https://roadrunner.dev/"><b>Official Website</b></a> | - <a href="https://roadrunner.dev/docs"><b>Documentation</b></a> -</p> - -Features: --------- -- Production-ready -- PCI DSS compliant -- PSR-7 HTTP server (file uploads, error handling, static files, hot reload, middlewares, event listeners) -- HTTPS and HTTP/2 support (including HTTP/2 Push, H2C) -- Fully customizable server, FastCGI support -- Flexible environment configuration -- No external PHP dependencies (64bit version required), drop-in (based on [Goridge](https://github.com/spiral/goridge)) -- Load balancer, process manager and task pipeline -- Frontend agnostic ([Queue](https://github.com/spiral/jobs), PSR-7, [GRPC](https://github.com/spiral/php-grpc), etc) -- Integrated metrics (Prometheus) -- Works over TCP, UNIX sockets and standard pipes -- Automatic worker replacement and safe PHP process destruction -- Worker create/allocate/destroy timeouts -- Max jobs per worker -- Worker lifecycle management (controller) - - maxMemory (graceful stop) - - TTL (graceful stop) - - idleTTL (graceful stop) - - execTTL (brute, max_execution_time) -- Payload context and body -- Protocol, worker and job level error management (including PHP errors) -- Very fast (~250k rpc calls per second on Ryzen 1700X using 16 threads) -- Integrations with Symfony, [Laravel](https://github.com/spiral/roadrunner-laravel), Slim, CakePHP, Zend Expressive -- Application server for [Spiral](https://github.com/spiral/framework) -- Automatic reloading on file changes -- Works on Windows (Unix sockets (AF_UNIX) supported on Windows 10) - -Installation: --------- -To install: - -``` -$ composer require spiral/roadrunner -$ ./vendor/bin/rr get-binary -``` - -> For getting roadrunner binary file you can use our docker image: `spiralscout/roadrunner:X.X.X` (more information about image and tags can be found [here](https://hub.docker.com/r/spiralscout/roadrunner/)) - -Extensions: --------- -| Extension | Current Status -| --- | --- -spiral/jobs | [![Latest Stable Version](https://poser.pugx.org/spiral/jobs/version)](https://packagist.org/packages/spiral/jobs) [![Build Status](https://travis-ci.org/spiral/jobs.svg?branch=master)](https://travis-ci.org/spiral/jobs) [![Codecov](https://codecov.io/gh/spiral/jobs/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/jobs/) -spiral/php-grpc | [![Latest Stable Version](https://poser.pugx.org/spiral/php-grpc/version)](https://packagist.org/packages/spiral/php-grpc) [![Build Status](https://travis-ci.org/spiral/php-grpc.svg?branch=master)](https://travis-ci.org/spiral/php-grpc) [![Codecov](https://codecov.io/gh/spiral/php-grpc/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/php-grpc/) -spiral/broadcast | [![Latest Stable Version](https://poser.pugx.org/spiral/broadcast/version)](https://packagist.org/packages/spiral/broadcast) [![Build Status](https://travis-ci.org/spiral/broadcast.svg?branch=master)](https://travis-ci.org/spiral/broadcast) [![Codecov](https://codecov.io/gh/spiral/broadcast/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/broadcast/) -spiral/broadcast-ws | [![Latest Stable Version](https://poser.pugx.org/spiral/broadcast-ws/version)](https://packagist.org/packages/spiral/broadcast-ws) [![Build Status](https://travis-ci.org/spiral/broadcast-ws.svg?branch=master)](https://travis-ci.org/spiral/broadcast-ws) [![Codecov](https://codecov.io/gh/spiral/broadcast-ws/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/broadcast-ws/) - -Example: --------- - -```php -<?php -// worker.php -ini_set('display_errors', 'stderr'); -include "vendor/autoload.php"; - -$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT); -$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay)); - -while ($req = $psr7->acceptRequest()) { - try { - $resp = new \Zend\Diactoros\Response(); - $resp->getBody()->write("hello world"); - - $psr7->respond($resp); - } catch (\Throwable $e) { - $psr7->getWorker()->error((string)$e); - } -} -``` - -Configuration can be located in `.rr.yaml` file ([full sample](https://github.com/spiral/roadrunner/blob/master/.rr.yaml)): - -```yaml -http: - address: 0.0.0.0:8080 - workers.command: "php worker.php" -``` - -> Read more in [Documentation](https://roadrunner.dev/docs). - -Run: ----- -To run application server: - -``` -$ ./rr serve -v -d -``` - -License: --------- -The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com). diff --git a/bors.toml b/bors.toml index e56a268a..e56a268a 100644..100755 --- a/bors.toml +++ b/bors.toml diff --git a/cmd/rr/LICENSE b/cmd/rr/LICENSE deleted file mode 100644 index efb98c87..00000000 --- a/cmd/rr/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 SpiralScout - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.
\ No newline at end of file diff --git a/cmd/rr/cmd/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/codecov.yml b/codecov.yml new file mode 100644 index 00000000..e34b27ea --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: on + patch: on
\ No newline at end of file diff --git a/composer.json b/composer.json index 3d6b3d22..e3017b97 100644..100755 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "ext-json": "*", "ext-curl": "*", "spiral/goridge": "^2.4.2", @@ -41,4 +41,4 @@ "bin": [ "bin/rr" ] -} +}
\ No newline at end of file diff --git a/config.go b/config.go deleted file mode 100644 index 649ad9f4..00000000 --- a/config.go +++ /dev/null @@ -1,53 +0,0 @@ -package roadrunner - -import ( - "fmt" - "runtime" - "time" -) - -// Config defines basic behaviour of worker creation and handling process. -type Config struct { - // NumWorkers defines how many sub-processes can be run at once. This value - // might be doubled by Swapper while hot-swap. - NumWorkers int64 - - // MaxJobs defines how many executions is allowed for the worker until - // it's destruction. set 1 to create new process for each new task, 0 to let - // worker handle as many tasks as it can. - MaxJobs int64 - - // AllocateTimeout defines for how long pool will be waiting for a worker to - // be freed to handle the task. - AllocateTimeout time.Duration - - // DestroyTimeout defines for how long pool should be waiting for worker to - // properly stop, if timeout reached worker will be killed. - DestroyTimeout time.Duration -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (cfg *Config) InitDefaults() error { - cfg.AllocateTimeout = time.Minute - cfg.DestroyTimeout = time.Minute - cfg.NumWorkers = int64(runtime.NumCPU()) - - return nil -} - -// Valid returns error if config not valid. -func (cfg *Config) Valid() error { - if cfg.NumWorkers == 0 { - return fmt.Errorf("pool.NumWorkers must be set") - } - - if cfg.AllocateTimeout == 0 { - return fmt.Errorf("pool.AllocateTimeout must be set") - } - - if cfg.DestroyTimeout == 0 { - return fmt.Errorf("pool.DestroyTimeout must be set") - } - - return nil -} diff --git a/config_test.go b/config_test.go deleted file mode 100644 index e51cb2c4..00000000 --- a/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func Test_NumWorkers(t *testing.T) { - cfg := Config{ - AllocateTimeout: time.Second, - DestroyTimeout: time.Second * 10, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.NumWorkers must be set", err.Error()) -} - -func Test_NumWorkers_Default(t *testing.T) { - cfg := Config{ - AllocateTimeout: time.Second, - DestroyTimeout: time.Second * 10, - } - - assert.NoError(t, cfg.InitDefaults()) - err := cfg.Valid() - assert.Nil(t, err) -} - -func Test_AllocateTimeout(t *testing.T) { - cfg := Config{ - NumWorkers: 10, - DestroyTimeout: time.Second * 10, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.AllocateTimeout must be set", err.Error()) -} - -func Test_DestroyTimeout(t *testing.T) { - cfg := Config{ - NumWorkers: 10, - AllocateTimeout: time.Second, - } - err := cfg.Valid() - - assert.NotNil(t, err) - assert.Equal(t, "pool.DestroyTimeout must be set", err.Error()) -} diff --git a/controller.go b/controller.go deleted file mode 100644 index 020ea4dd..00000000 --- a/controller.go +++ /dev/null @@ -1,16 +0,0 @@ -package roadrunner - -// Controller observes pool state and decides if any worker must be destroyed. -type Controller interface { - // Lock controller on given pool instance. - Attach(p Pool) Controller - - // Detach pool watching. - Detach() -} - -// Attacher defines the ability to attach rr controller. -type Attacher interface { - // Attach attaches controller to the service. - Attach(c Controller) -}
\ No newline at end of file diff --git a/controller_test.go b/controller_test.go deleted file mode 100644 index d177feda..00000000 --- a/controller_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package roadrunner - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "runtime" - "testing" - "time" -) - -type eWatcher struct { - p Pool - onAttach func(p Pool) - onDetach func(p Pool) -} - -func (w *eWatcher) Attach(p Pool) Controller { - wp := &eWatcher{p: p, onAttach: w.onAttach, onDetach: w.onDetach} - - if wp.onAttach != nil { - wp.onAttach(p) - } - - return wp -} - -func (w *eWatcher) Detach() { - if w.onDetach != nil { - w.onDetach(w.p) - } -} - -func (w *eWatcher) remove(wr *Worker, err error) { - w.p.Remove(wr, err) -} - -func Test_WatcherWatch(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_WatcherReattach(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - oldWatcher := rr.pController - - assert.NoError(t, rr.Reset()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - assert.NotEqual(t, oldWatcher, rr.pController) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_WatcherAttachDetachSequence(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - var attachedPool Pool - - rr.Attach(&eWatcher{ - onAttach: func(p Pool) { - attachedPool = p - }, - onDetach: func(p Pool) { - assert.Equal(t, attachedPool, p) - }, - }) - assert.NoError(t, rr.Start()) - - assert.NotNil(t, rr.pController) - assert.Equal(t, rr.pController.(*eWatcher).p, rr.pool) - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - - assert.NoError(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_RemoveWorkerOnAllocation(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php pid pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - wr := rr.Workers()[0] - - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%v", *wr.Pid), res.String()) - lastPid := res.String() - - rr.pController.(*eWatcher).remove(wr, nil) - - res, err = rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.NotEqual(t, lastPid, res.String()) - - assert.NotEqual(t, StateReady, wr.state.Value()) - - _, ok := rr.pool.(*StaticPool).remove.Load(wr) - assert.False(t, ok) -} - -func Test_RemoveWorkerAfterTask(t *testing.T) { - rr := NewServer( - &ServerConfig{ - Command: "php tests/client.php slow-pid pipes", - Relay: "pipes", - Pool: &Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }) - defer rr.Stop() - - rr.Attach(&eWatcher{}) - assert.NoError(t, rr.Start()) - - wr := rr.Workers()[0] - lastPid := "" - - wait := make(chan interface{}) - go func() { - res, err := rr.Exec(&Payload{Body: []byte("hello")}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%v", *wr.Pid), res.String()) - lastPid = res.String() - - close(wait) - }() - - // wait for worker execution to be in progress - time.Sleep(time.Millisecond * 250) - rr.pController.(*eWatcher).remove(wr, nil) - - <-wait - - // must be replaced - assert.NotEqual(t, lastPid, fmt.Sprintf("%v", rr.Workers()[0])) - - // must not be registered within the pool - rr.pController.(*eWatcher).remove(wr, nil) -} diff --git a/error_buffer.go b/error_buffer.go deleted file mode 100644 index 0fc020c7..00000000 --- a/error_buffer.go +++ /dev/null @@ -1,113 +0,0 @@ -package roadrunner - -import ( - "sync" - "time" -) - -const ( - // EventStderrOutput - is triggered when worker sends data into stderr. The context - // is error message ([]byte). - EventStderrOutput = 1900 - - // WaitDuration - for how long error buffer should attempt to aggregate error messages - // before merging output together since lastError update (required to keep error update together). - WaitDuration = 100 * time.Millisecond -) - -// thread safe errBuffer -type errBuffer struct { - mu sync.Mutex - buf []byte - last int - wait *time.Timer - update chan interface{} - stop chan interface{} - lsn func(event int, ctx interface{}) -} - -func newErrBuffer() *errBuffer { - eb := &errBuffer{ - buf: make([]byte, 0), - update: make(chan interface{}), - wait: time.NewTimer(WaitDuration), - stop: make(chan interface{}), - } - - go func() { - for { - select { - case <-eb.update: - eb.wait.Reset(WaitDuration) - case <-eb.wait.C: - eb.mu.Lock() - if len(eb.buf) > eb.last { - if eb.lsn != nil { - eb.lsn(EventStderrOutput, eb.buf[eb.last:]) - eb.buf = eb.buf[0:0] - } - - eb.last = len(eb.buf) - } - eb.mu.Unlock() - case <-eb.stop: - eb.wait.Stop() - - eb.mu.Lock() - if len(eb.buf) > eb.last { - if eb.lsn != nil { - eb.lsn(EventStderrOutput, eb.buf[eb.last:]) - } - - eb.last = len(eb.buf) - } - eb.mu.Unlock() - return - } - } - }() - - return eb -} - -// Listen attaches error stream even listener. -func (eb *errBuffer) Listen(l func(event int, ctx interface{})) { - eb.mu.Lock() - eb.lsn = l - eb.mu.Unlock() -} - -// Len returns the number of buf of the unread portion of the errBuffer; -// buf.Len() == len(buf.Bytes()). -func (eb *errBuffer) Len() int { - eb.mu.Lock() - defer eb.mu.Unlock() - - // currently active message - return len(eb.buf) -} - -// Write appends the contents of pool to the errBuffer, growing the errBuffer as -// needed. The return value n is the length of pool; err is always nil. -func (eb *errBuffer) Write(p []byte) (int, error) { - eb.mu.Lock() - eb.buf = append(eb.buf, p...) - eb.mu.Unlock() - eb.update <- nil - - return len(p), nil -} - -// Strings fetches all errBuffer data into string. -func (eb *errBuffer) String() string { - eb.mu.Lock() - defer eb.mu.Unlock() - - return string(eb.buf) -} - -// Close aggregation timer. -func (eb *errBuffer) Close() error { - close(eb.stop) - return nil -} diff --git a/error_buffer_test.go b/error_buffer_test.go deleted file mode 100644 index c163ea43..00000000 --- a/error_buffer_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package roadrunner - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestErrBuffer_Write_Len(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hello")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - assert.Equal(t, 5, buf.Len()) - assert.Equal(t, "hello", buf.String()) -} - -func TestErrBuffer_Write_Event(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - tr := make(chan interface{}) - buf.Listen(func(event int, ctx interface{}) { - assert.Equal(t, EventStderrOutput, event) - assert.Equal(t, []byte("hello\n"), ctx) - close(tr) - }) - - _, err := buf.Write([]byte("hello\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - <-tr - - // messages are read - assert.Equal(t, 0, buf.Len()) -} - -func TestErrBuffer_Write_Event_Separated(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - tr := make(chan interface{}) - buf.Listen(func(event int, ctx interface{}) { - assert.Equal(t, EventStderrOutput, event) - assert.Equal(t, []byte("hello\nending"), ctx) - close(tr) - }) - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("lo\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("ending")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - <-tr - assert.Equal(t, 0, buf.Len()) - assert.Equal(t, "", buf.String()) -} - -func TestErrBuffer_Write_Event_Separated_NoListener(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("lo\n")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - _, err = buf.Write([]byte("ending")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - assert.Equal(t, 12, buf.Len()) - assert.Equal(t, "hello\nending", buf.String()) -} - -func TestErrBuffer_Write_Remaining(t *testing.T) { - buf := newErrBuffer() - defer func() { - err := buf.Close() - if err != nil { - t.Errorf("error during closing the buffer: error %v", err) - } - }() - - _, err := buf.Write([]byte("hel")) - if err != nil { - t.Errorf("fail to write: error %v", err) - } - - assert.Equal(t, 3, buf.Len()) - assert.Equal(t, "hel", buf.String()) -} diff --git a/errors.go b/errors.go deleted file mode 100644 index db995721..00000000 --- a/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package roadrunner - -// JobError is job level error (no worker halt), wraps at top -// of error context -type JobError []byte - -// Error converts error context to string -func (je JobError) Error() string { - return string(je) -} - -// WorkerError is worker related error -type WorkerError struct { - // Worker - Worker *Worker - - // Caused error - Caused error -} - -// Error converts error context to string -func (e WorkerError) Error() string { - return e.Caused.Error() -} diff --git a/errors_test.go b/errors_test.go deleted file mode 100644 index 6bb650af..00000000 --- a/errors_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package roadrunner - -import ( - "errors" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_JobError_Error(t *testing.T) { - e := JobError([]byte("error")) - assert.Equal(t, "error", e.Error()) -} - -func Test_WorkerError_Error(t *testing.T) { - e := WorkerError{Worker: nil, Caused: errors.New("error")} - assert.Equal(t, "error", e.Error()) -} diff --git a/factory.go b/factory.go index 3c304824..482d39f8 100644..100755 --- 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 } @@ -1,28 +1,27 @@ -module github.com/spiral/roadrunner +module github.com/spiral/roadrunner/v2 go 1.15 require ( github.com/NYTimes/gziphandler v1.1.1 - github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect - github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 - github.com/cenkalti/backoff/v4 v4.0.0 - github.com/dustin/go-humanize v1.0.0 - github.com/go-ole/go-ole v1.2.4 // indirect + github.com/fatih/color v1.10.0 + github.com/gofiber/fiber/v2 v2.2.3 + github.com/golang/mock v1.4.4 + github.com/hashicorp/go-multierror v1.1.0 github.com/json-iterator/go v1.1.10 - github.com/mattn/go-colorable v0.1.7 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d - github.com/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/prometheus/client_golang v1.8.0 + github.com/shirou/gopsutil v3.20.11+incompatible github.com/spf13/viper v1.7.1 + github.com/spiral/endure v1.0.0-beta20 + github.com/spiral/errors v1.0.5 github.com/spiral/goridge/v2 v2.4.6 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 + go.uber.org/zap v1.16.0 + golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 + golang.org/x/sys v0.0.0-20201202213521-69691e467435 ) @@ -14,19 +14,34 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -34,53 +49,85 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 h1:uxxtrnACqI9zK4ENDMf0WpXfUsHP5V8liuq5QdgDISU= -github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= -github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= -github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= +github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.38.1/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofiber/fiber/v2 v2.2.3 h1:mnaX66dpJOQYS0uii2Nd8fVzpqXMjiXqI5ci1QhM0EI= +github.com/gofiber/fiber/v2 v2.2.3/go.mod h1:Aso7/M+EQOinVkWp4LUYjdlTpKTBoCk2Qo4djnMsyHE= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -89,8 +136,12 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -98,33 +149,53 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -134,10 +205,16 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -145,44 +222,57 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= +github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -190,110 +280,206 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= +github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= +github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w= -github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto= +github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= +github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spiral/endure v1.0.0-beta20 h1:QD3EJ6CRLgeo/6trfnlUcQhH3vrK8Hvf9ceDpde+yss= +github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLqJUgcV+nqxn+o= +github.com/spiral/endure v1.0.0-beta9 h1:PNiVit9DCucmhZLd4RgoiVL7Y5DBmweUwadFyulAct8= +github.com/spiral/endure v1.0.0-beta9/go.mod h1:EhC6CKaSQum/gz1zRqkyu4LqFOlngVTGbXK69pebmxQ= +github.com/spiral/errors v1.0.4/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= +github.com/spiral/errors v1.0.5 h1:TwlR9cZtTgnZrSngcEUpyiMO9yJ45gdQ+XcrCRoCCAM= +github.com/spiral/errors v1.0.5/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= github.com/spiral/goridge/v2 v2.4.6 h1:9u/mrxCtOSy0lnumrpPCSOlGBX/Vprid/hFsnzWrd6k= github.com/spiral/goridge/v2 v2.4.6/go.mod h1:mYjL+Ny7nVfLqjRwIYV2pUSQ61eazvVclHII6FfZfYc= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.17.0 h1:P8/koH4aSnJ4xbd0cUUFEGQs3jQqIxoDDyRQrUiAkqg= +github.com/valyala/fasthttp v1.17.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yookoala/gofast v0.4.0 h1:dLBjghcsbbZNOEHN8N1X/gh9S6srmJed4WQfG7DlKwo= github.com/yookoala/gofast v0.4.0/go.mod h1:rfbkoKaQG1bnuTUZcmV3vAlnfpF4FTq8WbQJf2vcpg8= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -307,30 +493,46 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -338,15 +540,20 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -354,49 +561,83 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180726210403-bfb5194568d3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516 h1:E8xavSjXY8LFvcMSu/8Fjztt+SerwKnuAUOdS+aCXUM= +golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -405,43 +646,78 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.6 h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc= +honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/interfaces/informer/interface.go b/interfaces/informer/interface.go new file mode 100644 index 00000000..a8d32841 --- /dev/null +++ b/interfaces/informer/interface.go @@ -0,0 +1,8 @@ +package informer + +import "github.com/spiral/roadrunner/v2" + +// Informer used to get workers from particular plugin or set of plugins +type Informer interface { + Workers() []roadrunner.WorkerBase +} diff --git a/interfaces/log/interface.go b/interfaces/log/interface.go new file mode 100644 index 00000000..e2f2ce31 --- /dev/null +++ b/interfaces/log/interface.go @@ -0,0 +1,16 @@ +package log + +type ( + // Logger is an general RR log interface + Logger interface { + Debug(msg string, keyvals ...interface{}) + Info(msg string, keyvals ...interface{}) + Warn(msg string, keyvals ...interface{}) + Error(msg string, keyvals ...interface{}) + } +) + +// With creates a child logger and adds structured context to it +type WithLogger interface { + With(keyvals ...interface{}) Logger +} diff --git a/interfaces/metrics/interface.go b/interfaces/metrics/interface.go new file mode 100644 index 00000000..505c3d7b --- /dev/null +++ b/interfaces/metrics/interface.go @@ -0,0 +1,9 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type StatProvider interface { + MetricsCollector() []prometheus.Collector +} diff --git a/interfaces/resetter/interface.go b/interfaces/resetter/interface.go new file mode 100644 index 00000000..3fa48cf3 --- /dev/null +++ b/interfaces/resetter/interface.go @@ -0,0 +1,5 @@ +package resetter + +type Resetter interface { + Reset() error +} diff --git a/interfaces/rpc/interface.go b/interfaces/rpc/interface.go new file mode 100644 index 00000000..683fd2ec --- /dev/null +++ b/interfaces/rpc/interface.go @@ -0,0 +1,7 @@ +package rpc + +// RPCer declares the ability to create set of public RPC methods. +type RPCer interface { + // Provides RPC methods for the given service. + RPC() interface{} +} diff --git a/interfaces/server/interface.go b/interfaces/server/interface.go new file mode 100644 index 00000000..2dae30c5 --- /dev/null +++ b/interfaces/server/interface.go @@ -0,0 +1,17 @@ +package server + +import ( + "context" + "os/exec" + + "github.com/spiral/roadrunner/v2" +) + +type Env map[string]string + +// Server creates workers for the application. +type Server interface { + CmdFactory(env Env) (func() *exec.Cmd, error) + NewWorker(ctx context.Context, env Env) (roadrunner.WorkerBase, error) + NewWorkerPool(ctx context.Context, opt roadrunner.PoolConfig, env Env) (roadrunner.Pool, error) +} diff --git a/interfaces/status/interface.go b/interfaces/status/interface.go new file mode 100644 index 00000000..0a92bc52 --- /dev/null +++ b/interfaces/status/interface.go @@ -0,0 +1,11 @@ +package status + +// Status consists of status code from the service +type Status struct { + Code int +} + +// Checker interface used to get latest status from plugin +type Checker interface { + Status() Status +} diff --git a/mocks/mock_log.go b/mocks/mock_log.go new file mode 100644 index 00000000..25c6a793 --- /dev/null +++ b/mocks/mock_log.go @@ -0,0 +1,150 @@ +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + log "github.com/spiral/roadrunner/v2/interfaces/log" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +func (m *MockLogger) Init() error { + mock := &MockLogger{ctrl: m.ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return nil +} + +// Debug mocks base method. +func (m *MockLogger) Debug(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Warn mocks base method. +func (m *MockLogger) Warn(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +func (mr *MockLoggerMockRecorder) Init() error { + return nil +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// MockWithLogger is a mock of WithLogger interface. +type MockWithLogger struct { + ctrl *gomock.Controller + recorder *MockWithLoggerMockRecorder +} + +// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger. +type MockWithLoggerMockRecorder struct { + mock *MockWithLogger +} + +// NewMockWithLogger creates a new mock instance. +func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger { + mock := &MockWithLogger{ctrl: ctrl} + mock.recorder = &MockWithLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder { + return m.recorder +} + +// With mocks base method. +func (m *MockWithLogger) With(keyvals ...interface{}) log.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keyvals { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "With", varargs...) + ret0, _ := ret[0].(log.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...) +} diff --git a/payload.go b/payload.go index 154cec95..502dfadd 100644..100755 --- 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/phpstan.neon.dist b/phpstan.neon.dist index a6ef3b8a..b5fec74d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,4 @@ parameters: level: 'max' paths: - - src + - src
\ No newline at end of file diff --git a/pipe_factory.go b/pipe_factory.go index 9696a474..3e98bd4e 100644..100755 --- a/pipe_factory.go +++ b/pipe_factory.go @@ -1,78 +1,161 @@ package roadrunner import ( - "fmt" - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" - "io" + "context" "os/exec" + + "github.com/spiral/errors" + "github.com/spiral/goridge/v2" + "go.uber.org/multierr" ) -// PipeFactory connects to workers using standard +// PipeFactory connects to stack using standard // streams (STDIN, STDOUT pipes). -type PipeFactory struct { -} +type PipeFactory struct{} // NewPipeFactory returns new factory instance and starts // listening -func NewPipeFactory() *PipeFactory { + +// todo: review tests +func NewPipeFactory() Factory { 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) + const op = errors.Op("spawn worker with context") + go func() { + w, err := InitBaseWorker(cmd) + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // TODO why out is in? + in, err := cmd.StdoutPipe() + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } - var ( - in io.ReadCloser - out io.WriteCloser - ) + // TODO why in is out? + out, err := cmd.StdinPipe() + if err != nil { + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } - if in, err = cmd.StdoutPipe(); err != nil { - return nil, err + // 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.E(op, err), + } + return + } + + // errors bundle + pid, err := fetchPID(relay) + if pid != w.Pid() || err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + c <- SpawnResult{ + w: nil, + err: errors.E(op, err), + } + return + } + + // everything ok, set ready state + w.State().Set(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 } +} - if out, err = cmd.StdinPipe(); err != nil { - return nil, err +func (f *PipeFactory) SpawnWorker(cmd *exec.Cmd) (WorkerBase, error) { + const op = errors.Op("spawn worker") + w, err := InitBaseWorker(cmd) + if err != nil { + return nil, errors.E(op, err) } - w.rl = goridge.NewPipeRelay(in, out) + // TODO why out is in? + in, err := cmd.StdoutPipe() + if err != nil { + return nil, errors.E(op, err) + } - if err := w.start(); err != nil { - return nil, errors.Wrap(err, "process error") + // TODO why in is out? + out, err := cmd.StdinPipe() + if err != nil { + return nil, errors.E(op, err) } - 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) - - 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 - } - } + // Init new PIPE relay + relay := goridge.NewPipeRelay(in, out) + w.AttachRelay(relay) + + // Start the worker + err = w.Start() + if err != nil { + return nil, errors.E(op, err) + } - return nil, errors.Wrap(err, "unable to connect to worker") + // errors bundle + if pid, err := fetchPID(relay); pid != w.Pid() { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + return nil, errors.E(op, err) } - 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..bdb861de 100644..100755 --- a/pipe_factory_test.go +++ b/pipe_factory_test.go @@ -1,16 +1,19 @@ 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) @@ -18,7 +21,7 @@ func Test_Pipe_Start(t *testing.T) { assert.NoError(t, w.Wait()) }() - assert.NoError(t, w.Stop()) + assert.NoError(t, w.Stop(ctx)) } func Test_Pipe_StartError(t *testing.T) { @@ -28,7 +31,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 +44,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 +57,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 +75,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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -100,38 +112,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(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 { b.Fail() } }() - err := w.Stop() + err := w.Stop(context.Background()) if err != nil { b.Errorf("error stopping the worker: error %v", err) } @@ -141,7 +156,13 @@ 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() if err != nil { @@ -149,14 +170,68 @@ func Benchmark_Pipe_Worker_ExecEcho(b *testing.B) { } }() 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(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) + } + + 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(Payload{Body: []byte("hello")}); err != nil { + b.Fail() + } + } +} + +func Benchmark_Pipe_Worker_ExecEchoWithoutContext(b *testing.B) { + cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + ctx := context.Background() + w, err := NewPipeFactory().SpawnWorkerWithContext(ctx, cmd) + if err != nil { + b.Fatal(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(Payload{Body: []byte("hello")}); err != nil { b.Fail() } } diff --git a/plugins/checker/config.go b/plugins/checker/config.go new file mode 100644 index 00000000..5f952592 --- /dev/null +++ b/plugins/checker/config.go @@ -0,0 +1,5 @@ +package checker + +type Config struct { + Address string +} diff --git a/plugins/checker/plugin.go b/plugins/checker/plugin.go new file mode 100644 index 00000000..e6250697 --- /dev/null +++ b/plugins/checker/plugin.go @@ -0,0 +1,152 @@ +package checker + +import ( + "fmt" + "net/http" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/status" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +const ( + // PluginName declares public plugin name. + PluginName = "status" +) + +type Plugin struct { + registry map[string]status.Checker + server *fiber.App + log log.Logger + cfg *Config +} + +func (c *Plugin) Init(log log.Logger, cfg config.Configurer) error { + const op = errors.Op("status plugin init") + err := cfg.UnmarshalKey(PluginName, &c.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + if c.cfg == nil { + return errors.E(errors.Disabled) + } + + c.registry = make(map[string]status.Checker) + c.log = log + return nil +} + +func (c *Plugin) Serve() chan error { + errCh := make(chan error, 1) + c.server = fiber.New(fiber.Config{ + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 5, + IdleTimeout: time.Second * 5, + }) + c.server.Group("/v1", c.healthHandler) + c.server.Use(logger.New()) + c.server.Use("/health", c.healthHandler) + + go func() { + err := c.server.Listen(c.cfg.Address) + if err != nil { + errCh <- err + } + }() + + return errCh +} + +func (c *Plugin) Stop() error { + const op = errors.Op("checker stop") + err := c.server.Shutdown() + if err != nil { + return errors.E(op, err) + } + return nil +} + +// Reset named service. +func (c *Plugin) Status(name string) (status.Status, error) { + const op = errors.Op("get status") + svc, ok := c.registry[name] + if !ok { + return status.Status{}, errors.E(op, errors.Errorf("no such service: %s", name)) + } + + return svc.Status(), nil +} + +// CollectTarget collecting services which can provide Status. +func (c *Plugin) CollectTarget(name endure.Named, r status.Checker) error { + c.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (c *Plugin) Collects() []interface{} { + return []interface{}{ + c.CollectTarget, + } +} + +// Name of the service. +func (c *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (c *Plugin) RPC() interface{} { + return &rpc{srv: c, log: c.log} +} + +type Plugins struct { + Plugins []string `query:"plugin"` +} + +const template string = "Service: %s: Status: %d\n" + +func (c *Plugin) healthHandler(ctx *fiber.Ctx) error { + const op = errors.Op("health_handler") + plugins := &Plugins{} + err := ctx.QueryParser(plugins) + if err != nil { + return errors.E(op, err) + } + + if len(plugins.Plugins) == 0 { + ctx.Status(http.StatusOK) + _, _ = ctx.WriteString("No plugins provided in query. Query should be in form of: /v1/health?plugin=plugin1&plugin=plugin2 \n") + return nil + } + + failed := false + // iterate over all provided plugins + for i := 0; i < len(plugins.Plugins); i++ { + // check if the plugin exists + if plugin, ok := c.registry[plugins.Plugins[i]]; ok { + st := plugin.Status() + if st.Code >= 500 { + failed = true + continue + } else if st.Code >= 100 && st.Code <= 400 { + _, _ = ctx.WriteString(fmt.Sprintf(template, plugins.Plugins[i], st.Code)) + } + } else { + _, _ = ctx.WriteString(fmt.Sprintf("Service: %s not found", plugins.Plugins[i])) + } + } + if failed { + ctx.Status(http.StatusInternalServerError) + return nil + } + + ctx.Status(http.StatusOK) + return nil +} diff --git a/plugins/checker/rpc.go b/plugins/checker/rpc.go new file mode 100644 index 00000000..d03d1638 --- /dev/null +++ b/plugins/checker/rpc.go @@ -0,0 +1,28 @@ +package checker + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/status" +) + +type rpc struct { + srv *Plugin + log log.Logger +} + +// Status return current status of the provided plugin +func (rpc *rpc) Status(service string, status *status.Status) error { + const op = errors.Op("status") + rpc.log.Debug("started Status method", "service", service) + st, err := rpc.srv.Status(service) + if err != nil { + return errors.E(op, err) + } + + *status = st + + rpc.log.Debug("status code", "code", st.Code) + rpc.log.Debug("successfully finished Status method") + return nil +} diff --git a/plugins/checker/tests/configs/.rr-checker-init.yaml b/plugins/checker/tests/configs/.rr-checker-init.yaml new file mode 100755 index 00000000..5943551b --- /dev/null +++ b/plugins/checker/tests/configs/.rr-checker-init.yaml @@ -0,0 +1,29 @@ +rpc: + listen: tcp://127.0.0.1:6005 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +status: + address: "127.0.0.1:34333" + +http: + debug: true + address: 127.0.0.1:11933 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/checker/tests/plugin_test.go b/plugins/checker/tests/plugin_test.go new file mode 100644 index 00000000..c93b68af --- /dev/null +++ b/plugins/checker/tests/plugin_test.go @@ -0,0 +1,188 @@ +package tests + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2/interfaces/status" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestStatusHttp(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatus", checkHTTPStatus) + wg.Wait() +} + +const resp = `Service: http: Status: 200 +Service: rpc not found` + +func checkHTTPStatus(t *testing.T) { + req, err := http.NewRequest("GET", "http://127.0.0.1:34333/v1/health?plugin=http&plugin=rpc", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, resp, string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestStatusRPC(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatusRpc", checkRPCStatus) + wg.Wait() +} + +func checkRPCStatus(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6005") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + + st := &status.Status{} + + err = client.Call("status.Status", "http", &st) + assert.NoError(t, err) + assert.Equal(t, st.Code, 200) +} diff --git a/plugins/config/configurer.go b/plugins/config/configurer.go new file mode 100755 index 00000000..00010eae --- /dev/null +++ b/plugins/config/configurer.go @@ -0,0 +1,19 @@ +package config + +type Configurer interface { + // UnmarshalKey reads configuration section into configuration object. + // + // func (h *HttpService) Init(cp config.Configurer) 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{} + + // Has checks if config section exists. + Has(name string) bool +} diff --git a/plugins/config/plugin.go b/plugins/config/plugin.go new file mode 100755 index 00000000..2555d28a --- /dev/null +++ b/plugins/config/plugin.go @@ -0,0 +1,91 @@ +package config + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/viper" +) + +type Viper struct { + viper *viper.Viper + Path string + Prefix string +} + +// Inits config provider. +func (v *Viper) 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() +} + +// Overwrite overwrites existing config with provided values +func (v *Viper) 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 +} + +// UnmarshalKey reads configuration section into configuration object. +func (v *Viper) 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 *Viper) Get(name string) interface{} { + return v.viper.Get(name) +} + +// Has checks if config section exists. +func (v *Viper) Has(name string) bool { + return v.viper.IsSet(name) +} + +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.ReplaceAll(value, fmt.Sprintf("\\%s", string(escape)), string(escape)) + } + + return value +} diff --git a/plugins/config/tests/.rr.yaml b/plugins/config/tests/.rr.yaml new file mode 100755 index 00000000..732a1366 --- /dev/null +++ b/plugins/config/tests/.rr.yaml @@ -0,0 +1,18 @@ +reload: + enabled: true + interval: 1s + patterns: [".php"] + services: + http: + recursive: true + ignore: ["vendor"] + patterns: [".php", ".go",".md",] + dirs: ["."] + jobs: + recursive: false + ignore: ["service/metrics"] + dirs: ["./jobs"] + rpc: + recursive: true + patterns: [".json"] + dirs: [""] diff --git a/plugins/config/tests/config_test.go b/plugins/config/tests/config_test.go new file mode 100755 index 00000000..422e7eee --- /dev/null +++ b/plugins/config/tests/config_test.go @@ -0,0 +1,66 @@ +package tests + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/stretchr/testify/assert" +) + +func TestViperProvider_Init(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + vp := &config.Viper{} + 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, 1) + signal.Notify(c, os.Interrupt) + + tt := time.NewTicker(time.Second * 2) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + 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 100755 index 00000000..a276c15f --- /dev/null +++ b/plugins/config/tests/plugin1.go @@ -0,0 +1,53 @@ +package tests + +import ( + "errors" + "time" + + "github.com/spiral/roadrunner/v2/plugins/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.Configurer +} + +// Depends on S2 and DB (S3 in the current case) +func (f *Foo) Init(p config.Configurer) 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/gzip/plugin.go b/plugins/gzip/plugin.go new file mode 100644 index 00000000..e5b9e4f5 --- /dev/null +++ b/plugins/gzip/plugin.go @@ -0,0 +1,25 @@ +package gzip + +import ( + "net/http" + + "github.com/NYTimes/gziphandler" +) + +const PluginName = "gzip" + +type Gzip struct{} + +func (g *Gzip) Init() error { + return nil +} + +func (g *Gzip) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + gziphandler.GzipHandler(next).ServeHTTP(w, r) + } +} + +func (g *Gzip) Name() string { + return PluginName +} diff --git a/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml b/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml new file mode 100644 index 00000000..df02c043 --- /dev/null +++ b/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml @@ -0,0 +1,22 @@ +server: + command: "php psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18103 + maxRequestSize: 1024 + middleware: [ "gzip", "foo" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/gzip/tests/configs/.rr-http-withGzip.yaml b/plugins/gzip/tests/configs/.rr-http-withGzip.yaml new file mode 100644 index 00000000..b91a9aad --- /dev/null +++ b/plugins/gzip/tests/configs/.rr-http-withGzip.yaml @@ -0,0 +1,22 @@ +server: + command: "php psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18953 + maxRequestSize: 1024 + middleware: [ "gzip" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/gzip/tests/plugin_test.go b/plugins/gzip/tests/plugin_test.go new file mode 100644 index 00000000..39979895 --- /dev/null +++ b/plugins/gzip/tests/plugin_test.go @@ -0,0 +1,170 @@ +package tests + +import ( + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/mocks" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestGzipPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-withGzip.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 10) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("GzipCheckHeader", headerCheck) + wg.Wait() +} + +func headerCheck(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18953", nil) + assert.NoError(t, err) + client := &http.Client{ + Transport: &http.Transport{ + DisableCompression: false, + }, + } + + r, err := client.Do(req) + assert.NoError(t, err) + assert.True(t, r.Uncompressed) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestMiddlewareNotExist(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-middlewareNotExist.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Warn("requested middleware does not exist", "requested", "foo") + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 5) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + wg.Wait() +} diff --git a/plugins/gzip/tests/psr-worker.php b/plugins/gzip/tests/psr-worker.php new file mode 100644 index 00000000..ed936bde --- /dev/null +++ b/plugins/gzip/tests/psr-worker.php @@ -0,0 +1,23 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ +use Spiral\Goridge; +use Spiral\RoadRunner; + +ini_set('display_errors', 'stderr'); +require dirname(__DIR__) . "/../../vendor_php/autoload.php"; + +$worker = new RoadRunner\Worker(new Goridge\StreamRelay(STDIN, STDOUT)); +$psr7 = new RoadRunner\PSR7Client($worker); + +while ($req = $psr7->acceptRequest()) { + try { + $resp = new \Zend\Diactoros\Response(); + $resp->getBody()->write(str_repeat("hello world", 1000)); + + $psr7->respond($resp); + } catch (\Throwable $e) { + $psr7->getWorker()->error((string)$e); + } +}
\ No newline at end of file diff --git a/service/headers/config.go b/plugins/headers/config.go index f9af1df2..8d4e29c2 100644 --- a/service/headers/config.go +++ b/plugins/headers/config.go @@ -1,17 +1,17 @@ package headers -import "github.com/spiral/roadrunner/service" - // Config declares headers service configuration. type Config struct { - // CORS settings. - CORS *CORSConfig + Headers struct { + // CORS settings. + CORS *CORSConfig - // Request headers to add to every payload send to PHP. - Request map[string]string + // 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 + // Response headers to add to every payload generated by PHP. + Response map[string]string + } } // CORSConfig headers configuration. @@ -34,8 +34,3 @@ type CORSConfig struct { // 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/plugins/headers/plugin.go b/plugins/headers/plugin.go new file mode 100644 index 00000000..f1c6e6f3 --- /dev/null +++ b/plugins/headers/plugin.go @@ -0,0 +1,117 @@ +package headers + +import ( + "net/http" + "strconv" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ID contains default service name. +const PluginName = "headers" +const RootPluginName = "http" + +// Service serves headers files. Potentially convert into middleware? +type Plugin 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 *Plugin) Init(cfg config.Configurer) error { + const op = errors.Op("headers plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Headers.Request != nil { + for k, v := range s.cfg.Headers.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Headers.Response != nil { + for k, v := range s.cfg.Headers.Response { + w.Header().Set(k, v) + } + } + + if s.cfg.Headers.CORS != nil { + if r.Method == http.MethodOptions { + s.preflightRequest(w) + return + } + s.corsHeaders(w) + } + + next.ServeHTTP(w, r) + } +} + +func (s *Plugin) Name() string { + return PluginName +} + +// configure OPTIONS response +func (s *Plugin) preflightRequest(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.AllowedMethods != "" { + headers.Set("Access-Control-Allow-Methods", s.cfg.Headers.CORS.AllowedMethods) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } + + if s.cfg.Headers.CORS.MaxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.Headers.CORS.MaxAge)) + } + + w.WriteHeader(http.StatusOK) +} + +// configure CORS headers +func (s *Plugin) corsHeaders(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.ExposedHeaders != "" { + headers.Set("Access-Control-Expose-Headers", s.cfg.Headers.CORS.ExposedHeaders) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } +} diff --git a/plugins/headers/tests/configs/.rr-cors-headers.yaml b/plugins/headers/tests/configs/.rr-cors-headers.yaml new file mode 100644 index 00000000..5c1a200b --- /dev/null +++ b/plugins/headers/tests/configs/.rr-cors-headers.yaml @@ -0,0 +1,37 @@ +server: + command: "php ../../../tests/http/client.php headers pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22855 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/headers/tests/configs/.rr-headers-init.yaml b/plugins/headers/tests/configs/.rr-headers-init.yaml new file mode 100644 index 00000000..252fe8f3 --- /dev/null +++ b/plugins/headers/tests/configs/.rr-headers-init.yaml @@ -0,0 +1,37 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:33453 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "Example-Request-Header": "Value" + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/headers/tests/configs/.rr-req-headers.yaml b/plugins/headers/tests/configs/.rr-req-headers.yaml new file mode 100644 index 00000000..9256e98d --- /dev/null +++ b/plugins/headers/tests/configs/.rr-req-headers.yaml @@ -0,0 +1,30 @@ +server: + command: "php ../../../tests/http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22655 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/headers/tests/configs/.rr-res-headers.yaml b/plugins/headers/tests/configs/.rr-res-headers.yaml new file mode 100644 index 00000000..1bca2c3d --- /dev/null +++ b/plugins/headers/tests/configs/.rr-res-headers.yaml @@ -0,0 +1,30 @@ +server: + command: "php ../../../tests/http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22455 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/headers/tests/headers_plugin_test.go b/plugins/headers/tests/headers_plugin_test.go new file mode 100644 index 00000000..f1de8cb9 --- /dev/null +++ b/plugins/headers/tests/headers_plugin_test.go @@ -0,0 +1,359 @@ +package tests + +import ( + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/headers" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestHeadersInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-headers-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 5) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + wg.Wait() +} + +func TestRequestHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-req-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("RequestHeaders", reqHeaders) + wg.Wait() +} + +func reqHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22655?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestResponseHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-res-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ResponseHeaders", resHeaders) + wg.Wait() +} + +func resHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22455?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "output-header", r.Header.Get("output")) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestCORSHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-cors-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CORSHeaders", corsHeaders) + t.Run("CORSHeadersPass", corsHeadersPass) + wg.Wait() +} + +func corsHeadersPass(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, 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) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func corsHeaders(t *testing.T) { + req, err := http.NewRequest("OPTIONS", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, 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) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} diff --git a/service/http/attributes/attributes.go b/plugins/http/attributes/attributes.go index 77d6ea69..4c453766 100644 --- a/service/http/attributes/attributes.go +++ b/plugins/http/attributes/attributes.go @@ -6,9 +6,18 @@ import ( "net/http" ) -type attrKey int +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { return k.name } -const contextKey attrKey = iota +var ( + // PsrContextKey is a context key. It can be used in the http attributes + PsrContextKey = &contextKey{"psr_attributes"} +) type attrs map[string]interface{} @@ -30,12 +39,12 @@ func (v attrs) del(key string) { // 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{})) + return r.WithContext(context.WithValue(r.Context(), PsrContextKey, attrs{})) } // All returns all context attributes. func All(r *http.Request) map[string]interface{} { - v := r.Context().Value(contextKey) + v := r.Context().Value(PsrContextKey) if v == nil { return attrs{} } @@ -46,7 +55,7 @@ func All(r *http.Request) map[string]interface{} { // 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) + v := r.Context().Value(PsrContextKey) if v == nil { return nil } @@ -57,7 +66,7 @@ func Get(r *http.Request, key string) interface{} { // 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) + v := r.Context().Value(PsrContextKey) if v == nil { return errors.New("unable to find `psr:attributes` context key") } diff --git a/service/http/attributes/attributes_test.go b/plugins/http/attributes/attributes_test.go index 2360fd12..5622deb4 100644 --- a/service/http/attributes/attributes_test.go +++ b/plugins/http/attributes/attributes_test.go @@ -1,9 +1,10 @@ package attributes import ( - "github.com/stretchr/testify/assert" "net/http" "testing" + + "github.com/stretchr/testify/assert" ) func TestAllAttributes(t *testing.T) { @@ -70,10 +71,7 @@ func TestSetAttribute(t *testing.T) { 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.Error(t, err) assert.Equal(t, Get(r, "key"), nil) } diff --git a/service/http/config.go b/plugins/http/config.go index 00f61652..d6efe310 100644 --- a/service/http/config.go +++ b/plugins/http/config.go @@ -1,22 +1,44 @@ package http import ( - "errors" - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" "net" "os" + "runtime" "strings" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" ) +type Cidrs []*net.IPNet + +func (c *Cidrs) IsTrusted(ip string) bool { + if len(*c) == 0 { + return false + } + + i := net.ParseIP(ip) + if i == nil { + return false + } + + for _, cird := range *c { + if cird.Contains(i) { + return true + } + } + + return false +} + // 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 + SSL *SSLConfig // FCGI configuration. You can use FastCGI without HTTP server. FCGI *FCGIConfig @@ -25,17 +47,25 @@ type Config struct { HTTP2 *HTTP2Config // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. - MaxRequestSize int64 + MaxRequestSize uint64 // 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 + // Pool configures worker pool. + Pool *roadrunner.PoolConfig + + // Env is environment variables passed to the http pool + Env map[string]string + + // List of the middleware names (order will be preserved) + Middleware []string + + // slice of net.IPNet + cidrs Cidrs } // FCGIConfig for FastCGI server. @@ -87,7 +117,7 @@ func (c *Config) EnableHTTP() bool { return c.Address != "" } -// EnableTLS returns true if rr must listen TLS connections. +// EnableTLS returns true if pool must listen TLS connections. func (c *Config) EnableTLS() bool { return c.SSL.Key != "" || c.SSL.Cert != "" || c.SSL.RootCA != "" } @@ -108,9 +138,17 @@ func (c *Config) EnableFCGI() 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 { - if c.Workers == nil { - c.Workers = &roadrunner.ServerConfig{} +func (c *Config) InitDefaults() error { + if c.Pool == nil { + // default pool + c.Pool = &roadrunner.PoolConfig{ + Debug: false, + NumWorkers: int64(runtime.NumCPU()), + MaxJobs: 1000, + AllocateTimeout: time.Second * 60, + DestroyTimeout: time.Second * 60, + Supervisor: nil, + } } if c.HTTP2 == nil { @@ -125,6 +163,10 @@ func (c *Config) Hydrate(cfg service.Config) error { c.Uploads = &UploadsConfig{} } + if c.SSL == nil { + c.SSL = &SSLConfig{} + } + if c.SSL.Port == 0 { c.SSL.Port = 443 } @@ -137,16 +179,6 @@ func (c *Config) Hydrate(cfg service.Config) error { 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 @@ -161,24 +193,27 @@ func (c *Config) Hydrate(cfg service.Config) error { } } - if err := c.parseCIDRs(); err != nil { + cidrs, err := ParseCIDRs(c.TrustedSubnets) + if err != nil { return err } + c.cidrs = cidrs return c.Valid() } -func (c *Config) parseCIDRs() error { - for _, cidr := range c.TrustedSubnets { +func ParseCIDRs(subnets []string) (Cidrs, error) { + c := make(Cidrs, 0, len(subnets)) + for _, cidr := range subnets { _, cr, err := net.ParseCIDR(cidr) if err != nil { - return err + return nil, err } - c.cidrs = append(c.cidrs, cr) + c = append(c, cr) } - return nil + return c, nil } // IsTrusted if api can be trusted to use X-Real-Ip, X-Forwarded-For @@ -203,38 +238,31 @@ func (c *Config) IsTrusted(ip string) bool { // Valid validates the configuration. func (c *Config) Valid() error { + const op = errors.Op("validation") if c.Uploads == nil { - return errors.New("malformed uploads config") + return errors.E(op, errors.Str("malformed uploads config")) } if c.HTTP2 == nil { - return errors.New("malformed http2 config") + return errors.E(op, errors.Str("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.Pool == nil { + return errors.E(op, "malformed pool config") } 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)") + return errors.E(op, errors.Str("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") + return errors.E(op, errors.Str("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 errors.E(op, errors.Errorf("key file '%s' does not exists", c.SSL.Key)) } return err @@ -242,7 +270,7 @@ func (c *Config) Valid() error { 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 errors.E(op, errors.Errorf("cert file '%s' does not exists", c.SSL.Cert)) } return err @@ -252,7 +280,7 @@ func (c *Config) Valid() error { 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 errors.E(op, errors.Errorf("root ca path provided, but path '%s' does not exists", c.SSL.RootCA)) } return err } diff --git a/service/http/constants.go b/plugins/http/constants.go index a25f52a4..773d1f46 100644 --- a/service/http/constants.go +++ b/plugins/http/constants.go @@ -3,4 +3,4 @@ package http import "net/http" var http2pushHeaderKey = http.CanonicalHeaderKey("http2-push") -var trailerHeaderKey = http.CanonicalHeaderKey("trailer") +var TrailerHeaderKey = http.CanonicalHeaderKey("trailer") diff --git a/service/http/errors.go b/plugins/http/errors.go index fb8762ef..fb8762ef 100644 --- a/service/http/errors.go +++ b/plugins/http/errors.go diff --git a/service/http/errors_windows.go b/plugins/http/errors_windows.go index 3d0ba04c..3d0ba04c 100644 --- a/service/http/errors_windows.go +++ b/plugins/http/errors_windows.go diff --git a/service/http/handler.go b/plugins/http/handler.go index eca05483..74b038ff 100644 --- a/service/http/handler.go +++ b/plugins/http/handler.go @@ -1,7 +1,6 @@ package http import ( - "fmt" "net" "net/http" "strconv" @@ -9,9 +8,11 @@ import ( "sync" "time" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" + "github.com/hashicorp/go-multierror" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/util" ) const ( @@ -22,6 +23,11 @@ const ( EventError ) +type Handle interface { + AddListener(l util.EventListener) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + // ErrorEvent represents singular http error event. type ErrorEvent struct { // Request contains client request, must not be stored. @@ -60,16 +66,30 @@ func (e *ResponseEvent) Elapsed() time.Duration { // 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{}) +type handler struct { + maxRequestSize uint64 + uploads UploadsConfig + trusted Cidrs + log log.Logger + pool roadrunner.Pool + mul sync.Mutex + lsn util.EventListener +} + +func NewHandler(maxReqSize uint64, uploads UploadsConfig, trusted Cidrs, pool roadrunner.Pool) (Handle, error) { + if pool == nil { + return nil, errors.E(errors.Str("pool should be initialized")) + } + return &handler{ + maxRequestSize: maxReqSize * roadrunner.MB, + uploads: uploads, + pool: pool, + trusted: trusted, + }, nil } // Listen attaches handler event controller. -func (h *Handler) Listen(l func(event int, ctx interface{})) { +func (h *handler) AddListener(l util.EventListener) { h.mul.Lock() defer h.mul.Unlock() @@ -77,23 +97,19 @@ func (h *Handler) Listen(l func(event int, ctx interface{})) { } // 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) { +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + const op = errors.Op("ServeHTTP") 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 - } + if h.maxRequestSize != 0 { + err := h.maxSize(w, r, start, op) + if err != nil { + return } } - req, err := NewRequest(r, h.cfg.Uploads) + req, err := NewRequest(r, h.uploads) if err != nil { h.handleError(w, r, err, start) return @@ -111,7 +127,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - rsp, err := h.rr.Exec(p) + rsp, err := h.pool.Exec(p) if err != nil { h.handleError(w, r, err, start) return @@ -130,44 +146,58 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (h *handler) maxSize(w http.ResponseWriter, r *http.Request, start time.Time, op errors.Op) error { + 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 err + } else if size > int64(h.maxRequestSize) { + h.handleError(w, r, errors.E(op, errors.Str("request body max size is exceeded")), start) + return err + } + } + return nil +} + // handleError sends error. -func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error, start time.Time) { +func (h *handler) handleError(w http.ResponseWriter, r *http.Request, err error, start time.Time) { + h.mul.Lock() + defer h.mul.Unlock() // 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)}) + h.throw(ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) return } + err = multierror.Append(err) // 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 { + err = multierror.Append(err2, err) // 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)}) + h.throw(ErrorEvent{Request: r, Error: errors.E(err), start: start, elapsed: time.Since(start)}) return } - h.throw(EventError, &ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) + h.throw(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)}) +func (h *handler) handleResponse(req *Request, resp *Response, start time.Time) { + h.throw(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() - +func (h *handler) throw(ctx interface{}) { if h.lsn != nil { - h.lsn(event, ctx) + h.lsn(ctx) } } // get real ip passing multiple proxy -func (h *Handler) resolveIP(r *Request) { - if !h.cfg.IsTrusted(r.RemoteAddr) { +func (h *handler) resolveIP(r *Request) { + if h.trusted.IsTrusted(r.RemoteAddr) == false { return } @@ -187,7 +217,7 @@ func (h *Handler) resolveIP(r *Request) { } // 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 + // In general case, we only expect X-Real-Ip header. If it exist, we get the IP address 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. diff --git a/service/http/parse.go b/plugins/http/parse.go index 9b58d328..d4a1604b 100644 --- a/service/http/parse.go +++ b/plugins/http/parse.go @@ -30,7 +30,7 @@ func parseData(r *http.Request) dataTree { // pushes value into data tree. func (d dataTree) push(k string, v []string) { - keys := fetchIndexes(k) + keys := FetchIndexes(k) if len(keys) <= MaxLevel { d.mount(keys, v) } @@ -60,7 +60,7 @@ func (d dataTree) mount(i []string, v []string) { } // parse incoming dataTree request into JSON (including contentMultipart form dataTree) -func parseUploads(r *http.Request, cfg *UploadsConfig) *Uploads { +func parseUploads(r *http.Request, cfg UploadsConfig) *Uploads { u := &Uploads{ cfg: cfg, tree: make(fileTree), @@ -82,7 +82,7 @@ func parseUploads(r *http.Request, cfg *UploadsConfig) *Uploads { // pushes new file upload into it's proper place. func (d fileTree) push(k string, v []*FileUpload) { - keys := fetchIndexes(k) + keys := FetchIndexes(k) if len(keys) <= MaxLevel { d.mount(keys, v) } @@ -111,8 +111,8 @@ func (d fileTree) mount(i []string, v []*FileUpload) { d[i[0]].(fileTree).mount(i[1:], v) } -// fetchIndexes parses input name and splits it into separate indexes list. -func fetchIndexes(s string) []string { +// FetchIndexes parses input name and splits it into separate indexes list. +func FetchIndexes(s string) []string { var ( pos int ch string diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go new file mode 100644 index 00000000..371cdb91 --- /dev/null +++ b/plugins/http/plugin.go @@ -0,0 +1,541 @@ +package http + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/http/fcgi" + "net/url" + "strings" + "sync" + + "github.com/hashicorp/go-multierror" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" + factory "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/interfaces/status" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/util" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "golang.org/x/sys/cpu" +) + +const ( + // PluginName declares plugin name. + PluginName = "http" + + // EventInitSSL thrown at moment of https initialization. SSL server passed as context. + EventInitSSL = 750 +) + +// Middleware interface +type Middleware interface { + Middleware(f http.Handler) http.HandlerFunc +} + +type middleware map[string]Middleware + +// Service manages pool, http servers. +type Plugin struct { + sync.Mutex + + configurer config.Configurer + server factory.Server + log log.Logger + + cfg *Config + // middlewares to chain + mdwr middleware + // Event listener to stdout + listener util.EventListener + + // Pool which attached to all servers + pool roadrunner.Pool + + // servers RR handler + handler Handle + + // servers + http *http.Server + https *http.Server + fcgi *http.Server +} + +// AddListener attaches server event controller. +func (s *Plugin) AddListener(listener util.EventListener) { + // save listeners for Reset + s.listener = listener + s.pool.AddListener(listener) +} + +// 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 *Plugin) Init(cfg config.Configurer, log log.Logger, server factory.Server) error { + const op = errors.Op("http Init") + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, err) + } + + err = s.cfg.InitDefaults() + if err != nil { + return errors.E(op, err) + } + + s.configurer = cfg + s.log = log + s.mdwr = make(map[string]Middleware) + + if !s.cfg.EnableHTTP() && !s.cfg.EnableTLS() && !s.cfg.EnableFCGI() { + return errors.E(op, errors.Disabled) + } + + s.pool, err = server.NewWorkerPool(context.Background(), roadrunner.PoolConfig{ + Debug: s.cfg.Pool.Debug, + NumWorkers: s.cfg.Pool.NumWorkers, + MaxJobs: s.cfg.Pool.MaxJobs, + AllocateTimeout: s.cfg.Pool.AllocateTimeout, + DestroyTimeout: s.cfg.Pool.DestroyTimeout, + Supervisor: s.cfg.Pool.Supervisor, + }, s.cfg.Env) + if err != nil { + return errors.E(op, err) + } + + s.server = server + + s.AddListener(s.logCallback) + + return nil +} + +func (s *Plugin) logCallback(event interface{}) { + switch ev := event.(type) { + case ResponseEvent: + s.log.Debug("http handler response received", "elapsed", ev.Elapsed().String(), "remote address", ev.Request.RemoteAddr) + case ErrorEvent: + s.log.Error("error event received", "elapsed", ev.Elapsed().String(), "error", ev.Error) + case roadrunner.WorkerEvent: + s.log.Debug("worker event received", "event", ev.Event, "worker state", ev.Worker.State()) + default: + fmt.Println(event) + } +} + +// Serve serves the svc. +func (s *Plugin) Serve() chan error { + s.Lock() + defer s.Unlock() + + const op = errors.Op("serve http") + errCh := make(chan error, 2) + + var err error + s.handler, err = NewHandler( + s.cfg.MaxRequestSize, + *s.cfg.Uploads, + s.cfg.cidrs, + s.pool, + ) + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + if s.listener != nil { + s.handler.AddListener(s.listener) + } + + 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 { + errCh <- errors.E(op, err) + return errCh + } + } + + if s.cfg.EnableHTTP2() { + if err := s.initHTTP2(); err != nil { + errCh <- errors.E(op, err) + return errCh + } + } + } + + if s.cfg.EnableFCGI() { + s.fcgi = &http.Server{Handler: s} + } + + if s.http != nil { + go func() { + httpErr := s.http.ListenAndServe() + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + if s.https != nil { + go func() { + httpErr := s.https.ListenAndServeTLS( + s.cfg.SSL.Cert, + s.cfg.SSL.Key, + ) + + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + if s.fcgi != nil { + go func() { + httpErr := s.serveFCGI() + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + if len(s.mdwr) > 0 { + s.addMiddlewares() + } + + return errCh +} + +// Stop stops the http. +func (s *Plugin) Stop() error { + s.Lock() + defer s.Unlock() + + var err error + if s.fcgi != nil { + err = s.fcgi.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the fcgi server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + if s.https != nil { + err = s.https.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the https server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + if s.http != nil { + err = s.http.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the http server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + return err +} + +// ServeHTTP handles connection using set of middleware and pool PSR-7 server. +func (s *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if headerContainsUpgrade(r, s) { + http.Error(w, "server does not support upgrade header", http.StatusInternalServerError) + return + } + + if s.redirect(w, r) { + return + } + + if s.https != nil && r.TLS != nil { + w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") + } + + r = attributes.Init(r) + // protect the case, when user send Reset and we are replacing handler with pool + s.Lock() + s.handler.ServeHTTP(w, r) + s.Unlock() +} + +// Server returns associated pool workers +func (s *Plugin) Workers() []roadrunner.WorkerBase { + return s.pool.Workers() +} + +func (s *Plugin) Name() string { + return PluginName +} + +func (s *Plugin) Reset() error { + s.Lock() + defer s.Unlock() + const op = errors.Op("http reset") + s.log.Info("Resetting http plugin") + s.pool.Destroy(context.Background()) + + // re-read the config + err := s.configurer.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, err) + } + + s.pool, err = s.server.NewWorkerPool(context.Background(), roadrunner.PoolConfig{ + Debug: s.cfg.Pool.Debug, + NumWorkers: s.cfg.Pool.NumWorkers, + MaxJobs: s.cfg.Pool.MaxJobs, + AllocateTimeout: s.cfg.Pool.AllocateTimeout, + DestroyTimeout: s.cfg.Pool.DestroyTimeout, + Supervisor: s.cfg.Pool.Supervisor, + }, s.cfg.Env) + if err != nil { + return errors.E(op, err) + } + + s.handler, err = NewHandler( + s.cfg.MaxRequestSize, + *s.cfg.Uploads, + s.cfg.cidrs, + s.pool, + ) + if err != nil { + return errors.E(op, err) + } + + // restore original listeners + s.pool.AddListener(s.listener) + + return nil +} + +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.AddMiddleware, + } +} + +func (s *Plugin) AddMiddleware(name endure.Named, m Middleware) { + s.mdwr[name.Name()] = m +} + +// Status return status of the particular plugin +func (s *Plugin) Status() status.Status { + workers := s.Workers() + for i := 0; i < len(workers); i++ { + if workers[i].State().IsActive() { + return status.Status{ + Code: http.StatusOK, + } + } + } + // if there are no workers, threat this as error + return status.Status{ + Code: http.StatusInternalServerError, + } +} + +func (s *Plugin) redirect(w http.ResponseWriter, r *http.Request) bool { + 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 true + } + return false +} + +func headerContainsUpgrade(r *http.Request, s *Plugin) bool { + if _, ok := r.Header["Upgrade"]; ok { + // https://golang.org/pkg/net/http/#Hijacker + s.log.Error("server does not support Upgrade header") + return true + } + return false +} + +// append RootCA to the https server TLS config +func (s *Plugin) appendRootCa() error { + const op = errors.Op("append root CA") + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil + } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + CA, err := ioutil.ReadFile(s.cfg.SSL.RootCA) + if err != nil { + return err + } + + // should append our CA cert + ok := rootCAs.AppendCertsFromPEM(CA) + if !ok { + return errors.E(op, errors.Str("could not append Certs from PEM")) + } + // disable "G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)" + // #nosec G402 + cfg := &tls.Config{ + InsecureSkipVerify: false, + RootCAs: rootCAs, + } + s.http.TLSConfig = cfg + + return nil +} + +// Init https server +func (s *Plugin) 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, + }, + } + + return server +} + +// init http/2 server +func (s *Plugin) initHTTP2() error { + return http2.ConfigureServer(s.https, &http2.Server{ + MaxConcurrentStreams: s.cfg.HTTP2.MaxConcurrentStreams, + }) +} + +// serveFCGI starts FastCGI server. +func (s *Plugin) 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 +} + +// tlsAddr replaces listen or host port with port configured by SSL config. +func (s *Plugin) 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 +} + +func (s *Plugin) addMiddlewares() { + if s.http != nil { + applyMiddlewares(s.http, s.mdwr, s.cfg.Middleware, s.log) + } + if s.https != nil { + applyMiddlewares(s.https, s.mdwr, s.cfg.Middleware, s.log) + } + + if s.fcgi != nil { + applyMiddlewares(s.fcgi, s.mdwr, s.cfg.Middleware, s.log) + } +} + +func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log log.Logger) { + for i := 0; i < len(order); i++ { + if mdwr, ok := middlewares[order[i]]; ok { + server.Handler = mdwr.Middleware(server.Handler) + } else { + log.Warn("requested middleware does not exist", "requested", order[i]) + } + } +} diff --git a/service/http/request.go b/plugins/http/request.go index 8da5440f..640bdec2 100644 --- a/service/http/request.go +++ b/plugins/http/request.go @@ -8,12 +8,14 @@ import ( "net/url" "strings" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service/http/attributes" + j "github.com/json-iterator/go" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" ) +var json = j.ConfigCompatibleWithStandardLibrary + const ( defaultMaxMemory = 32 << 20 // 32 MB contentNone = iota + 900 @@ -68,8 +70,8 @@ func fetchIP(pair string) string { } // NewRequest creates new PSR7 compatible request using net/http request. -func NewRequest(r *http.Request, cfg *UploadsConfig) (req *Request, err error) { - req = &Request{ +func NewRequest(r *http.Request, cfg UploadsConfig) (*Request, error) { + req := &Request{ RemoteAddr: fetchIP(r.RemoteAddr), Protocol: r.Proto, Method: r.Method, @@ -91,18 +93,19 @@ func NewRequest(r *http.Request, cfg *UploadsConfig) (req *Request, err error) { return req, nil case contentStream: + var err error req.body, err = ioutil.ReadAll(r.Body) return req, err case contentMultipart: - if err = r.ParseMultipartForm(defaultMaxMemory); err != nil { + if err := r.ParseMultipartForm(defaultMaxMemory); err != nil { return nil, err } req.Uploads = parseUploads(r, cfg) fallthrough case contentFormData: - if err = r.ParseForm(); err != nil { + if err := r.ParseForm(); err != nil { return nil, err } @@ -114,7 +117,7 @@ func NewRequest(r *http.Request, cfg *UploadsConfig) (req *Request, err error) { } // Open moves all uploaded files to temporary directory so it can be given to php later. -func (r *Request) Open(log *logrus.Logger) { +func (r *Request) Open(log log.Logger) { if r.Uploads == nil { return } @@ -123,7 +126,7 @@ func (r *Request) Open(log *logrus.Logger) { } // Close clears all temp file uploads -func (r *Request) Close(log *logrus.Logger) { +func (r *Request) Close(log log.Logger) { if r.Uploads == nil { return } @@ -133,17 +136,17 @@ func (r *Request) Close(log *logrus.Logger) { // 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{} +func (r *Request) Payload() (roadrunner.Payload, error) { + p := roadrunner.Payload{} - j := json.ConfigCompatibleWithStandardLibrary - if p.Context, err = j.Marshal(r); err != nil { - return nil, err + var err error + if p.Context, err = json.Marshal(r); err != nil { + return roadrunner.EmptyPayload, err } if r.Parsed { - if p.Body, err = j.Marshal(r.body); err != nil { - return nil, err + if p.Body, err = json.Marshal(r.body); err != nil { + return roadrunner.EmptyPayload, err } } else if r.body != nil { p.Body = r.body.([]byte) diff --git a/service/http/response.go b/plugins/http/response.go index f34754be..e3ac2756 100644 --- a/service/http/response.go +++ b/plugins/http/response.go @@ -4,13 +4,11 @@ import ( "io" "net/http" "strings" + "sync" - json "github.com/json-iterator/go" - - "github.com/spiral/roadrunner" + "github.com/spiral/roadrunner/v2" ) - // Response handles PSR7 response logic. type Response struct { // Status contains response status. @@ -19,15 +17,15 @@ type Response struct { // Header contains list of response headers. Headers map[string][]string `json:"headers"` - // associated body payload. - body interface{} + // associated Body payload. + Body interface{} + sync.Mutex } -// 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 { +// NewResponse creates new response based on given pool payload. +func NewResponse(p roadrunner.Payload) (*Response, error) { + r := &Response{Body: p.Body} + if err := json.Unmarshal(p.Context, r); err != nil { return nil, err } @@ -56,14 +54,14 @@ func (r *Response) Write(w http.ResponseWriter) error { w.WriteHeader(r.Status) - if data, ok := r.body.([]byte); ok { + 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 rc, ok := r.Body.(io.Reader); ok { if _, err := io.Copy(w, rc); err != nil { return err } @@ -87,7 +85,7 @@ func handlePushHeaders(h map[string][]string) []string { } func handleTrailers(h map[string][]string) { - trailers, ok := h[trailerHeaderKey] + trailers, ok := h[TrailerHeaderKey] if !ok { return } @@ -103,5 +101,5 @@ func handleTrailers(h map[string][]string) { } } - delete(h, trailerHeaderKey) + delete(h, TrailerHeaderKey) } diff --git a/plugins/http/tests/config_test.go b/plugins/http/tests/config_test.go new file mode 100644 index 00000000..068bd66e --- /dev/null +++ b/plugins/http/tests/config_test.go @@ -0,0 +1,330 @@ +package tests + +//import ( +// "os" +// "testing" +// "time" +// +// json "github.com/json-iterator/go" +// "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)) +//} +// +//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/plugins/http/tests/configs/.rr-broken-pipes.yaml b/plugins/http/tests/configs/.rr-broken-pipes.yaml new file mode 100644 index 00000000..aacc303e --- /dev/null +++ b/plugins/http/tests/configs/.rr-broken-pipes.yaml @@ -0,0 +1,29 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php broken pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12384 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + + diff --git a/plugins/http/tests/configs/.rr-echoErr.yaml b/plugins/http/tests/configs/.rr-echoErr.yaml new file mode 100644 index 00000000..6ecdbb2a --- /dev/null +++ b/plugins/http/tests/configs/.rr-echoErr.yaml @@ -0,0 +1,28 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echoerr pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:8080 + maxRequestSize: 1024 + middleware: [ "pluginMiddleware", "pluginMiddleware2" ] + uploads: + forbid: [ "" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-env.yaml b/plugins/http/tests/configs/.rr-env.yaml new file mode 100644 index 00000000..c9fdc798 --- /dev/null +++ b/plugins/http/tests/configs/.rr-env.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php env pipes" + user: "" + group: "" + env: + "env_key": "ENV_VALUE" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12084 + maxRequestSize: 1024 + middleware: [ "" ] + env: + "RR_HTTP": "true" + "env_key": "ENV_VALUE" + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml new file mode 100644 index 00000000..dbd19445 --- /dev/null +++ b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php request-uri pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8082 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8890 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://127.0.0.1:6921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-fcgi.yaml b/plugins/http/tests/configs/.rr-fcgi.yaml new file mode 100644 index 00000000..0cbd6d02 --- /dev/null +++ b/plugins/http/tests/configs/.rr-fcgi.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8081 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8889 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-h2c.yaml b/plugins/http/tests/configs/.rr-h2c.yaml new file mode 100644 index 00000000..316daea9 --- /dev/null +++ b/plugins/http/tests/configs/.rr-h2c.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8083 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8891 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: true + h2c: true + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-http.yaml b/plugins/http/tests/configs/.rr-http.yaml new file mode 100644 index 00000000..7b91f735 --- /dev/null +++ b/plugins/http/tests/configs/.rr-http.yaml @@ -0,0 +1,41 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18903 + maxRequestSize: 1024 + middleware: [ "pluginMiddleware", "pluginMiddleware2" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:7921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 + + diff --git a/plugins/http/tests/configs/.rr-init.yaml b/plugins/http/tests/configs/.rr-init.yaml new file mode 100644 index 00000000..50aa91ec --- /dev/null +++ b/plugins/http/tests/configs/.rr-init.yaml @@ -0,0 +1,41 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:15395 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:7921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 + + diff --git a/plugins/http/tests/configs/.rr-resetter.yaml b/plugins/http/tests/configs/.rr-resetter.yaml new file mode 100644 index 00000000..b46b21f5 --- /dev/null +++ b/plugins/http/tests/configs/.rr-resetter.yaml @@ -0,0 +1,28 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:10084 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + diff --git a/plugins/http/tests/configs/.rr-ssl-push.yaml b/plugins/http/tests/configs/.rr-ssl-push.yaml new file mode 100644 index 00000000..90a99192 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl-push.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php push pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8086 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8894 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-ssl-redirect.yaml b/plugins/http/tests/configs/.rr-ssl-redirect.yaml new file mode 100644 index 00000000..1878ba53 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl-redirect.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8087 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8895 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-ssl.yaml b/plugins/http/tests/configs/.rr-ssl.yaml new file mode 100644 index 00000000..127c1678 --- /dev/null +++ b/plugins/http/tests/configs/.rr-ssl.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8085 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8893 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128
\ No newline at end of file diff --git a/service/http/fixtures/server.crt b/plugins/http/tests/fixtures/server.crt index 24d67fd7..24d67fd7 100644 --- a/service/http/fixtures/server.crt +++ b/plugins/http/tests/fixtures/server.crt diff --git a/service/http/fixtures/server.key b/plugins/http/tests/fixtures/server.key index 7501dd46..7501dd46 100644 --- a/service/http/fixtures/server.key +++ b/plugins/http/tests/fixtures/server.key diff --git a/service/http/handler_test.go b/plugins/http/tests/handler_test.go index 951bcbfd..0c6a39ef 100644 --- a/service/http/handler_test.go +++ b/plugins/http/tests/handler_test.go @@ -1,90 +1,43 @@ -package http +package tests import ( "bytes" "context" - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" "io/ioutil" "mime/multipart" - "net/http" - "net/http/httptest" "net/url" - "os" + "os/exec" "runtime" "strings" + + "github.com/spiral/roadrunner/v2" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" + + "net/http" + "os" "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() +func TestHandler_Echo(t *testing.T) { + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - return "", nil, err + t.Fatal(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() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -93,99 +46,52 @@ func TestHandler_Echo(t *testing.T) { t.Errorf("error during the shutdown: error %v", err) } }() - - go func() { - err := hs.ListenAndServe() + go func(server *http.Server) { + err := server.ListenAndServe() if err != nil && err != http.ErrServerClosed { t.Errorf("error listening the interface: error %v", err) } - }() + }(hs) time.Sleep(time.Millisecond * 10) body, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() 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) + _, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, nil) + assert.Error(t, err) } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "header", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8078", Handler: h} defer func() { @@ -214,7 +120,6 @@ func TestHandler_Headers(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -228,27 +133,26 @@ func TestHandler_Headers(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "user-agent", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8088", Handler: h} defer func() { @@ -277,7 +181,6 @@ func TestHandler_Empty_User_Agent(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -290,27 +193,26 @@ func TestHandler_Empty_User_Agent(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "user-agent", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8088", Handler: h} defer func() { @@ -339,7 +241,6 @@ func TestHandler_User_Agent(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -352,27 +253,26 @@ func TestHandler_User_Agent(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "cookie", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8079", Handler: h} defer func() { @@ -401,7 +301,6 @@ func TestHandler_Cookies(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -419,27 +318,26 @@ func TestHandler_Cookies(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8090", Handler: h} defer func() { @@ -472,7 +370,6 @@ func TestHandler_JsonPayload_POST(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -485,27 +382,26 @@ func TestHandler_JsonPayload_POST(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8081", Handler: h} defer func() { @@ -546,27 +442,26 @@ func TestHandler_JsonPayload_PUT(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "payload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8082", Handler: h} defer func() { @@ -595,7 +490,6 @@ func TestHandler_JsonPayload_PATCH(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -608,27 +502,26 @@ func TestHandler_JsonPayload_PATCH(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8083", Handler: h} defer func() { @@ -668,7 +561,6 @@ func TestHandler_FormData_POST(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -678,31 +570,31 @@ func TestHandler_FormData_POST(t *testing.T) { 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)) + // Sorted + 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8083", Handler: h} defer func() { @@ -723,6 +615,7 @@ func TestHandler_FormData_POST_Overwrite(t *testing.T) { form := url.Values{} form.Add("key", "value") + form.Add("key", "value2") form.Add("name[]", "name1") form.Add("name[]", "name2") form.Add("name[]", "name3") @@ -742,7 +635,6 @@ func TestHandler_FormData_POST_Overwrite(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -752,31 +644,30 @@ func TestHandler_FormData_POST_Overwrite(t *testing.T) { 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)) + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value2","name":["name1","name2","name3"]}`, string(b)) } func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8083", Handler: h} defer func() { @@ -816,7 +707,6 @@ func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -830,29 +720,28 @@ func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { } func TestHandler_FormData_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":18084", Handler: h} + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":17834", Handler: h} defer func() { err := hs.Shutdown(context.Background()) if err != nil { @@ -890,7 +779,6 @@ func TestHandler_FormData_PUT(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -904,27 +792,26 @@ func TestHandler_FormData_PUT(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8085", Handler: h} defer func() { @@ -964,7 +851,6 @@ func TestHandler_FormData_PATCH(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -974,31 +860,30 @@ func TestHandler_FormData_PATCH(t *testing.T) { 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)) + 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8019", Handler: h} defer func() { @@ -1018,7 +903,7 @@ func TestHandler_Multipart_POST(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") + err = w.WriteField("key", "value") if err != nil { t.Errorf("error writing the field: error %v", err) } @@ -1080,7 +965,6 @@ func TestHandler_Multipart_POST(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -1090,31 +974,30 @@ func TestHandler_Multipart_POST(t *testing.T) { 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)) + 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8020", Handler: h} defer func() { @@ -1134,7 +1017,7 @@ func TestHandler_Multipart_PUT(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") + err = w.WriteField("key", "value") if err != nil { t.Errorf("error writing the field: error %v", err) } @@ -1196,7 +1079,6 @@ func TestHandler_Multipart_PUT(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -1210,27 +1092,26 @@ func TestHandler_Multipart_PUT(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "data", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8021", Handler: h} defer func() { @@ -1251,7 +1132,7 @@ func TestHandler_Multipart_PATCH(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") + err = w.WriteField("key", "value") if err != nil { t.Errorf("error writing the field: error %v", err) } @@ -1314,7 +1195,6 @@ func TestHandler_Multipart_PATCH(t *testing.T) { err := r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -1328,27 +1208,26 @@ func TestHandler_Multipart_PATCH(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1368,31 +1247,33 @@ func TestHandler_Error(t *testing.T) { _, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error2", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1412,31 +1293,33 @@ func TestHandler_Error2(t *testing.T) { _, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "pid", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1447,7 +1330,7 @@ func TestHandler_Error3(t *testing.T) { }() go func() { - err := hs.ListenAndServe() + err = hs.ListenAndServe() if err != nil && err != http.ErrServerClosed { t.Errorf("error listening the interface: error %v", err) } @@ -1465,10 +1348,9 @@ func TestHandler_Error3(t *testing.T) { r, err := http.DefaultClient.Do(req) assert.NoError(t, err) defer func() { - err := r.Body.Close() + err = r.Body.Close() if err != nil { t.Errorf("error during the closing Body: error %v", err) - } }() @@ -1477,27 +1359,26 @@ func TestHandler_Error3(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1516,18 +1397,21 @@ func TestHandler_ResponseDuration(t *testing.T) { 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 { + h.AddListener(func(event interface{}) { + switch t := event.(type) { + case httpPlugin.ResponseEvent: + if t.Elapsed() > 0 { close(gotresp) } + default: } }) body, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() <-gotresp @@ -1536,27 +1420,26 @@ func TestHandler_ResponseDuration(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echoDelay", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1575,19 +1458,21 @@ func TestHandler_ResponseDurationDelayed(t *testing.T) { 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 { + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ResponseEvent: + if tp.Elapsed() > time.Second { close(gotresp) } + default: } }) body, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) - + defer func() { + _ = r.Body.Close() + }() <-gotresp assert.Equal(t, 201, r.StatusCode) @@ -1595,27 +1480,26 @@ func TestHandler_ResponseDurationDelayed(t *testing.T) { } 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "error", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1626,7 +1510,7 @@ func TestHandler_ErrorDuration(t *testing.T) { }() go func() { - err := hs.ListenAndServe() + err = hs.ListenAndServe() if err != nil && err != http.ErrServerClosed { t.Errorf("error listening the interface: error %v", err) } @@ -1634,18 +1518,21 @@ func TestHandler_ErrorDuration(t *testing.T) { 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 { + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ErrorEvent: + if tp.Elapsed() > 0 { close(goterr) } + default: } }) _, r, err := get("http://localhost:8177/?hello=world") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() <-goterr @@ -1653,41 +1540,40 @@ func TestHandler_ErrorDuration(t *testing.T) { } 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() + trusted := []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", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) + t.Fatal(err) } + defer func() { + pool.Destroy(context.Background()) + }() - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} defer func() { @@ -1707,48 +1593,50 @@ func TestHandler_IP(t *testing.T) { body, r, err := get("http://127.0.0.1:8177/") assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() 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() + trusted := []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", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) + t.Fatal(err) } + defer func() { + pool.Destroy(context.Background()) + }() - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + hs := &http.Server{Addr: "127.0.0.1:8179", Handler: h} defer func() { err := hs.Shutdown(context.Background()) if err != nil { @@ -1764,52 +1652,55 @@ func TestHandler_XRealIP(t *testing.T) { }() time.Sleep(time.Millisecond * 10) - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ + body, r, err := getHeader("http://127.0.0.1:8179/", map[string]string{ "X-Real-Ip": "200.0.0.1", }) assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() 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() + trusted := []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", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} defer func() { @@ -1834,45 +1725,47 @@ func TestHandler_XForwardedFor(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, r.StatusCode) assert.Equal(t, "101.0.0.1", body) + _ = r.Body.Close() 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) + _ = r.Body.Close() 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() + trusted := []string{ + "10.0.0.0/8", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "ip", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} defer func() { @@ -1895,35 +1788,32 @@ func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { }) assert.NoError(t, err) + _ = r.Body.Close() 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() + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "echo", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: int64(runtime.NumCPU()), + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) if err != nil { - b.Errorf("error starting the worker pool: error %v", err) + b.Fatal(err) } - defer h.rr.Stop() + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(b, err) hs := &http.Server{Addr: ":8177", Handler: h} defer func() { @@ -1934,13 +1824,15 @@ func BenchmarkHandler_Listen_Echo(b *testing.B) { }() go func() { - err := hs.ListenAndServe() + err = hs.ListenAndServe() if err != nil && err != http.ErrServerClosed { b.Errorf("error listening the interface: error %v", err) } }() time.Sleep(time.Millisecond * 10) + b.ResetTimer() + b.ReportAllocs() bb := "WORLD" for n := 0; n < b.N; n++ { r, err := http.Get("http://localhost:8177/?hello=world") diff --git a/plugins/http/tests/http_test.go b/plugins/http/tests/http_test.go new file mode 100644 index 00000000..06bf3f5d --- /dev/null +++ b/plugins/http/tests/http_test.go @@ -0,0 +1,1130 @@ +package tests + +import ( + "bytes" + "crypto/tls" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/mocks" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/yookoala/gofast" + + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +var sslClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +func TestHTTPInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 5) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + wg.Wait() +} + +func TestHTTPInformerReset(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &informer.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + tt := time.NewTimer(time.Second * 10) + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("HTTPInformerTest", informerTest) + t.Run("HTTPEchoTestBefore", echoHTTP) + t.Run("HTTPResetTest", resetTest) + t.Run("HTTPEchoTestAfter", echoHTTP) + + wg.Wait() +} + +func echoHTTP(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:10084?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func resetTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "http", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NoError(t, err) + if services[0] != "http" { + t.Fatal("no enough services") + } +} + +func informerTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + list := struct { + // Workers is list of workers. + Workers []roadrunner.ProcessState `json:"workers"` + }{} + + err = client.Call("informer.Workers", "http", &list) + assert.NoError(t, err) + assert.Len(t, list.Workers, 2) +} + +func TestSSL(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLEcho", sslEcho) + t.Run("SSLNoRedirect", sslNoRedirect) + t.Run("fCGIecho", fcgiEcho) + wg.Wait() +} + +func sslNoRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8085?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) + } +} + +func sslEcho(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8893?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) + } +} + +func fcgiEcho(t *testing.T) { + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6920") + + 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)) +} + +func TestSSLRedirect(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-redirect.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLRedirect", sslRedirect) + wg.Wait() +} + +func sslRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8087?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) + } +} + +func TestSSLPushPipes(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-push.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + tt := time.NewTimer(time.Second * 10) + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLPush", sslPush) + wg.Wait() +} + +func sslPush(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8894?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) + } +} + +func TestFastCGI_RequestUri(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-fcgi-reqUri.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("FastCGIServiceRequestUri", fcgiReqURI) + wg.Wait() +} + +func fcgiReqURI(t *testing.T) { + time.Sleep(time.Second * 2) + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "127.0.0.1:6921") + + 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)) +} + +func TestH2CUpgrade(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("H2cUpgrade", h2cUpgrade) + wg.Wait() +} + +func h2cUpgrade(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(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 { + t.Fatal(err) + } + + assert.Equal(t, "101 Switching Protocols", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestH2C(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("H2c", h2c) + wg.Wait() +} + +func h2c(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("Connection", "HTTP2-Settings") + req.Header.Add("HTTP2-Settings", "") + + r, err2 := http.DefaultClient.Do(req) + if err2 != nil { + t.Fatal(err) + } + + assert.Equal(t, "201 Created", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestHttpMiddleware(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 15) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("MiddlewareTest", middleware) + wg.Wait() +} + +func middleware(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18903?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) + + req, err = http.NewRequest("GET", "http://localhost:18903/halt", nil) + assert.NoError(t, err) + + r, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 500, r.StatusCode) + assert.Equal(t, "halted", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEchoErr(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-echoErr.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1") + mockLogger.EXPECT().Debug("WORLD", "pid", gomock.Any()) + mockLogger.EXPECT().Debug("worker event received", "event", roadrunner.EventWorkerLog, "worker state", gomock.Any()) + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("HttpEchoError", echoError) + wg.Wait() +} + +func echoError(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8080?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEnvVariables(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-env.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("EnvVariablesTest", envVarsTest) + wg.Wait() +} + +func envVarsTest(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:12084", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "ENV_VALUE", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpBrokenPipes(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-broken-pipes.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.Error(t, err) + + _, err = cont.Serve() + assert.Error(t, err) +} + +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 + } + defer func() { + _ = r.Body.Close() + }() + 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 +} diff --git a/service/http/parse_test.go b/plugins/http/tests/parse_test.go index f95a3f9d..a93bc059 100644 --- a/service/http/parse_test.go +++ b/plugins/http/tests/parse_test.go @@ -1,6 +1,10 @@ -package http +package tests -import "testing" +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/http" +) var samples = []struct { in string @@ -16,20 +20,18 @@ var samples = []struct { } 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) - } - }) + for i := 0; i < len(samples); i++ { + r := http.FetchIndexes(samples[i].in) + if !same(r, samples[i].out) { + t.Errorf("got %q, want %q", r, samples[i].out) + } } } func BenchmarkConfig_FetchIndexes(b *testing.B) { for _, tt := range samples { for n := 0; n < b.N; n++ { - r := fetchIndexes(tt.in) + r := http.FetchIndexes(tt.in) if !same(r, tt.out) { b.Fail() } diff --git a/plugins/http/tests/plugin1.go b/plugins/http/tests/plugin1.go new file mode 100644 index 00000000..1cbca744 --- /dev/null +++ b/plugins/http/tests/plugin1.go @@ -0,0 +1,25 @@ +package tests + +import "github.com/spiral/roadrunner/v2/plugins/config" + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "http_test.plugin1" +} diff --git a/plugins/http/tests/plugin_middleware.go b/plugins/http/tests/plugin_middleware.go new file mode 100644 index 00000000..de829d34 --- /dev/null +++ b/plugins/http/tests/plugin_middleware.go @@ -0,0 +1,61 @@ +package tests + +import ( + "net/http" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type PluginMiddleware struct { + config config.Configurer +} + +func (p *PluginMiddleware) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware) Middleware(next http.Handler) 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 { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware) Name() string { + return "pluginMiddleware" +} + +type PluginMiddleware2 struct { + config config.Configurer +} + +func (p *PluginMiddleware2) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware2) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/boom" { + w.WriteHeader(555) + _, err := w.Write([]byte("boom")) + if err != nil { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware2) Name() string { + return "pluginMiddleware2" +} diff --git a/service/http/response_test.go b/plugins/http/tests/response_test.go index 1f394276..2bfe7d56 100644 --- a/service/http/response_test.go +++ b/plugins/http/tests/response_test.go @@ -1,4 +1,4 @@ -package http +package tests import ( "bytes" @@ -6,7 +6,8 @@ import ( "net/http" "testing" - "github.com/spiral/roadrunner" + "github.com/spiral/roadrunner/v2" + http2 "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) @@ -44,13 +45,13 @@ func (tw *testWriter) Push(target string, opts *http.PushOptions) error { } func TestNewResponse_Error(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{Context: []byte(`invalid payload`)}) + r, err := http2.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{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), Body: []byte(`sample body`), }) @@ -67,7 +68,7 @@ func TestNewResponse_Write(t *testing.T) { } func TestNewResponse_Stream(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), }) @@ -76,8 +77,8 @@ func TestNewResponse_Stream(t *testing.T) { t.Fatal("response is nil") } - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") assert.NoError(t, err) assert.NotNil(t, r) @@ -91,7 +92,7 @@ func TestNewResponse_Stream(t *testing.T) { } func TestNewResponse_StreamError(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), }) @@ -100,8 +101,8 @@ func TestNewResponse_StreamError(t *testing.T) { t.Fatal("response is nil") } - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") assert.NoError(t, err) assert.NotNil(t, r) @@ -111,7 +112,7 @@ func TestNewResponse_StreamError(t *testing.T) { } func TestWrite_HandlesPush(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), }) @@ -126,7 +127,7 @@ func TestWrite_HandlesPush(t *testing.T) { } func TestWrite_HandlesTrailers(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), }) @@ -136,7 +137,7 @@ func TestWrite_HandlesTrailers(t *testing.T) { 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[http2.TrailerHeaderKey]) assert.Nil(t, w.h["foo"]) //nolint:golint,staticcheck assert.Nil(t, w.h["baz"]) //nolint:golint,staticcheck @@ -145,7 +146,7 @@ func TestWrite_HandlesTrailers(t *testing.T) { } func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ + r, err := http2.NewResponse(roadrunner.Payload{ Context: []byte( `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), }) diff --git a/service/http/uploads_config_test.go b/plugins/http/tests/uploads_config_test.go index 2b6ceebc..497cd54f 100644 --- a/service/http/uploads_config_test.go +++ b/plugins/http/tests/uploads_config_test.go @@ -1,13 +1,15 @@ -package http +package tests import ( - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" ) func TestFsConfig_Forbids(t *testing.T) { - cfg := UploadsConfig{Forbid: []string{".php"}} + cfg := http.UploadsConfig{Forbid: []string{".php"}} assert.True(t, cfg.Forbids("index.php")) assert.True(t, cfg.Forbids("index.PHP")) @@ -16,9 +18,9 @@ func TestFsConfig_Forbids(t *testing.T) { } func TestFsConfig_TmpFallback(t *testing.T) { - cfg := UploadsConfig{Dir: "test"} + cfg := http.UploadsConfig{Dir: "test"} assert.Equal(t, "test", cfg.TmpDir()) - cfg = UploadsConfig{Dir: ""} + cfg = http.UploadsConfig{Dir: ""} assert.Equal(t, os.TempDir(), cfg.TmpDir()) } diff --git a/service/http/uploads_test.go b/plugins/http/tests/uploads_test.go index 08177c72..ee244c06 100644 --- a/service/http/uploads_test.go +++ b/plugins/http/tests/uploads_test.go @@ -1,45 +1,48 @@ -package http +package tests import ( "bytes" "context" - "crypto/md5" + "crypto/sha512" "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" + "os/exec" "testing" "time" + + j "github.com/json-iterator/go" + "github.com/spiral/roadrunner/v2" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" ) +var json = j.ConfigCompatibleWithStandardLibrary + +const testFile = "uploads_test.go" + 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, - }, - }), + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8021", Handler: h} defer func() { @@ -60,7 +63,7 @@ func TestHandler_Upload_File(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - f := mustOpen("uploads_test.go") + f := mustOpen(testFile) defer func() { err := f.Close() if err != nil { @@ -100,33 +103,29 @@ func TestHandler_Upload_File(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, r.StatusCode) - fs := fileString("uploads_test.go", 0, "application/octet-stream") + fs := fileString(testFile, 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, - }, - }), + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8021", Handler: h} defer func() { @@ -147,7 +146,7 @@ func TestHandler_Upload_NestedFile(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - f := mustOpen("uploads_test.go") + f := mustOpen(testFile) defer func() { err := f.Close() if err != nil { @@ -187,33 +186,29 @@ func TestHandler_Upload_NestedFile(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, r.StatusCode) - fs := fileString("uploads_test.go", 0, "application/octet-stream") + fs := fileString(testFile, 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, - }, - }), + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: "-------", + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8021", Handler: h} defer func() { @@ -234,7 +229,7 @@ func TestHandler_Upload_File_NoTmpDir(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - f := mustOpen("uploads_test.go") + f := mustOpen(testFile) defer func() { err := f.Close() if err != nil { @@ -274,33 +269,29 @@ func TestHandler_Upload_File_NoTmpDir(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, r.StatusCode) - fs := fileString("uploads_test.go", 5, "application/octet-stream") + fs := fileString(testFile, 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, - }, - }), + pool, err := roadrunner.NewPool(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../../tests/http/client.php", "upload", "pipes") }, + roadrunner.NewPipeFactory(), + roadrunner.PoolConfig{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, nil, pool) + assert.NoError(t, err) hs := &http.Server{Addr: ":8021", Handler: h} defer func() { @@ -321,7 +312,7 @@ func TestHandler_Upload_File_Forbids(t *testing.T) { var mb bytes.Buffer w := multipart.NewWriter(&mb) - f := mustOpen("uploads_test.go") + f := mustOpen(testFile) defer func() { err := f.Close() if err != nil { @@ -361,13 +352,13 @@ func TestHandler_Upload_File_Forbids(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 200, r.StatusCode) - fs := fileString("uploads_test.go", 7, "application/octet-stream") + fs := fileString(testFile, 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.True(t, exists(testFile)) assert.False(t, exists("uploads_test.")) } @@ -380,11 +371,11 @@ func mustOpen(f string) *os.File { } type fInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - Mime string `json:"mime"` - Error int `json:"error"` - MD5 string `json:"md5,omitempty"` + Name string `json:"name"` + Size int64 `json:"size"` + Mime string `json:"mime"` + Error int `json:"error"` + Sha512 string `json:"sha512,omitempty"` } func fileString(f string, errNo int, mime string) string { @@ -405,30 +396,36 @@ func fileString(f string, errNo int, mime string) string { } }() - h := md5.New() + h := sha512.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)), + Name: s.Name(), + Size: s.Size(), + Error: errNo, + Mime: mime, + Sha512: hex.EncodeToString(h.Sum(nil)), } if errNo != 0 { - v.MD5 = "" + v.Sha512 = "" v.Size = 0 } - j := json.ConfigCompatibleWithStandardLibrary - r, err := j.Marshal(v) + r, err := json.Marshal(v) if err != nil { fmt.Println(fmt.Errorf("error marshalling fInfo, error: %v", err)) } return string(r) +} +// 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.go b/plugins/http/uploads.go index 39a9eaf2..5fddb75d 100644 --- a/service/http/uploads.go +++ b/plugins/http/uploads.go @@ -1,9 +1,8 @@ package http import ( - "fmt" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" + "github.com/spiral/roadrunner/v2/interfaces/log" + "io" "io/ioutil" "mime/multipart" @@ -31,7 +30,7 @@ const ( // Uploads tree manages uploaded files tree and temporary files. type Uploads struct { // associated temp directory and forbidden extensions. - cfg *UploadsConfig + cfg UploadsConfig // pre processed data tree for Uploads. tree fileTree @@ -42,13 +41,12 @@ type Uploads struct { // MarshalJSON marshal tree tree into JSON. func (u *Uploads) MarshalJSON() ([]byte, error) { - j := json.ConfigCompatibleWithStandardLibrary - return j.Marshal(u.tree) + return json.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) { +func (u *Uploads) Open(log log.Logger) { var wg sync.WaitGroup for _, f := range u.list { wg.Add(1) @@ -56,7 +54,7 @@ func (u *Uploads) Open(log *logrus.Logger) { defer wg.Done() err := f.Open(u.cfg) if err != nil && log != nil { - log.Error(fmt.Errorf("error opening the file: error %v", err)) + log.Error("error opening the file", "err", err) } }(f) } @@ -65,12 +63,12 @@ func (u *Uploads) Open(log *logrus.Logger) { } // Clear deletes all temporary files. -func (u *Uploads) Clear(log *logrus.Logger) { +func (u *Uploads) Clear(log log.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)) + log.Error("error removing the file", "err", err) } } } @@ -114,7 +112,7 @@ func NewUpload(f *multipart.FileHeader) *FileUpload { // STACK // DEFER FILE CLOSE (2) // DEFER TMP CLOSE (1) -func (f *FileUpload) Open(cfg *UploadsConfig) (err error) { +func (f *FileUpload) Open(cfg UploadsConfig) (err error) { if cfg.Forbids(f.Name) { f.Error = UploadErrorExtension return nil diff --git a/service/http/uploads_config.go b/plugins/http/uploads_config.go index 3f655064..4c20c8e8 100644 --- a/service/http/uploads_config.go +++ b/plugins/http/uploads_config.go @@ -19,6 +19,7 @@ type UploadsConfig struct { // InitDefaults sets missing values to their default values. func (cfg *UploadsConfig) InitDefaults() error { cfg.Forbid = []string{".php", ".exe", ".bat"} + cfg.Dir = os.TempDir() return nil } diff --git a/plugins/informer/plugin.go b/plugins/informer/plugin.go new file mode 100644 index 00000000..f3013394 --- /dev/null +++ b/plugins/informer/plugin.go @@ -0,0 +1,56 @@ +package informer + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/informer" + "github.com/spiral/roadrunner/v2/interfaces/log" +) + +const PluginName = "informer" + +type Plugin struct { + registry map[string]informer.Informer + log log.Logger +} + +func (p *Plugin) Init(log log.Logger) error { + p.registry = make(map[string]informer.Informer) + p.log = log + return nil +} + +// Workers provides WorkerBase slice with workers for the requested plugin +func (p *Plugin) Workers(name string) ([]roadrunner.WorkerBase, error) { + const op = errors.Op("get workers") + svc, ok := p.registry[name] + if !ok { + return nil, errors.E(op, errors.Errorf("no such service: %s", name)) + } + + return svc.Workers(), nil +} + +// CollectTarget resettable service. +func (p *Plugin) CollectTarget(name endure.Named, r informer.Informer) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.CollectTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} diff --git a/plugins/informer/rpc.go b/plugins/informer/rpc.go new file mode 100644 index 00000000..d6b7bf01 --- /dev/null +++ b/plugins/informer/rpc.go @@ -0,0 +1,53 @@ +package informer + +import ( + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" +) + +type rpc struct { + srv *Plugin + log log.Logger +} + +// WorkerList contains list of workers. +type WorkerList struct { + // Workers is list of workers. + Workers []roadrunner.ProcessState `json:"workers"` +} + +// List all resettable services. +func (rpc *rpc) List(_ bool, list *[]string) error { + rpc.log.Debug("Started List method") + *list = make([]string, 0, len(rpc.srv.registry)) + + for name := range rpc.srv.registry { + *list = append(*list, name) + } + rpc.log.Debug("list of services", "list", *list) + + rpc.log.Debug("successfully finished List method") + return nil +} + +// Workers state of a given service. +func (rpc *rpc) Workers(service string, list *WorkerList) error { + rpc.log.Debug("started Workers method", "service", service) + workers, err := rpc.srv.Workers(service) + if err != nil { + return err + } + + list.Workers = make([]roadrunner.ProcessState, 0) + for _, w := range workers { + ps, err := roadrunner.WorkerProcessState(w) + if err != nil { + continue + } + + list.Workers = append(list.Workers, ps) + } + rpc.log.Debug("list of workers", "workers", list.Workers) + rpc.log.Debug("successfully finished Workers method") + return nil +} diff --git a/plugins/informer/tests/.rr-informer.yaml b/plugins/informer/tests/.rr-informer.yaml new file mode 100644 index 00000000..83ecd582 --- /dev/null +++ b/plugins/informer/tests/.rr-informer.yaml @@ -0,0 +1,13 @@ +server: + command: "php ../../../tests/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false
\ No newline at end of file diff --git a/plugins/informer/tests/informer_test.go b/plugins/informer/tests/informer_test.go new file mode 100644 index 00000000..fbe33a7d --- /dev/null +++ b/plugins/informer/tests/informer_test.go @@ -0,0 +1,97 @@ +package tests + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestInformerInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-informer.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &informer.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 15) + + t.Run("InformerRpcTest", informerRPCTest) + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func informerRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + list := struct { + // Workers is list of workers. + Workers []roadrunner.ProcessState `json:"workers"` + }{} + + err = client.Call("informer.Workers", "informer.plugin1", &list) + assert.NoError(t, err) + assert.Len(t, list.Workers, 10) +} diff --git a/plugins/informer/tests/test_plugin.go b/plugins/informer/tests/test_plugin.go new file mode 100644 index 00000000..473b6de7 --- /dev/null +++ b/plugins/informer/tests/test_plugin.go @@ -0,0 +1,58 @@ +package tests + +import ( + "context" + "time" + + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +var testPoolConfig = roadrunner.PoolConfig{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &roadrunner.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "informer.plugin1" +} + +func (p1 *Plugin1) Workers() []roadrunner.WorkerBase { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + return pool.Workers() +} diff --git a/plugins/logger/config.go b/plugins/logger/config.go new file mode 100644 index 00000000..f7a5742c --- /dev/null +++ b/plugins/logger/config.go @@ -0,0 +1,94 @@ +package logger + +import ( + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// ChannelConfig configures loggers per channel. +type ChannelConfig struct { + // Dedicated channels per logger. By default logger allocated via named logger. + Channels map[string]Config `json:"channels" yaml:"channels"` +} + +type Config struct { + // Mode configures logger based on some default template (development, production, off). + Mode string `json:"mode" yaml:"mode"` + + // Level is the minimum enabled logging level. Note that this is a dynamic + // level, so calling ChannelConfig.Level.SetLevel will atomically change the log + // level of all loggers descended from this config. + Level string `json:"level" yaml:"level"` + + // Encoding sets the logger's encoding. Valid values are "json" and + // "console", as well as any third-party encodings registered via + // RegisterEncoder. + Encoding string `json:"encoding" yaml:"encoding"` + + // Output is a list of URLs or file paths to write logging output to. + // See Open for details. + Output []string `json:"output" yaml:"output"` + + // ErrorOutput is a list of URLs to write internal logger errors to. + // The default is standard error. + // + // Note that this setting only affects internal errors; for sample code that + // sends error-level logs to a different location from info- and debug-level + // logs, see the package-level AdvancedConfiguration example. + ErrorOutput []string `json:"errorOutput" yaml:"errorOutput"` +} + +// ZapConfig converts config into Zap configuration. +func (cfg *Config) BuildLogger() (*zap.Logger, error) { + var zCfg zap.Config + switch strings.ToLower(cfg.Mode) { + case "off", "none": + return zap.NewNop(), nil + case "production": + zCfg = zap.NewProductionConfig() + case "development": + zCfg = zap.NewDevelopmentConfig() + default: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + NameKey: "name", + EncodeName: ColoredHashedNameEncoder, + EncodeLevel: ColoredLevelEncoder, + EncodeTime: UTCTimeEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + } + + if cfg.Level != "" { + level := zap.NewAtomicLevel() + if err := level.UnmarshalText([]byte(cfg.Level)); err == nil { + zCfg.Level = level + } + } + + if cfg.Encoding != "" { + zCfg.Encoding = cfg.Encoding + } + + if len(cfg.Output) != 0 { + zCfg.OutputPaths = cfg.Output + } + + if len(cfg.ErrorOutput) != 0 { + zCfg.ErrorOutputPaths = cfg.ErrorOutput + } + + // todo: https://github.com/uber-go/zap/blob/master/FAQ.md#does-zap-support-log-rotation + + return zCfg.Build() +} diff --git a/plugins/logger/encoder.go b/plugins/logger/encoder.go new file mode 100644 index 00000000..4ff583c4 --- /dev/null +++ b/plugins/logger/encoder.go @@ -0,0 +1,66 @@ +package logger + +import ( + "hash/fnv" + "strings" + "time" + + "github.com/fatih/color" + "go.uber.org/zap/zapcore" +) + +var colorMap = []func(string, ...interface{}) string{ + color.HiYellowString, + color.HiGreenString, + color.HiBlueString, + color.HiRedString, + color.HiCyanString, + color.HiMagentaString, +} + +// ColoredLevelEncoder colorizes log levels. +func ColoredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + enc.AppendString(color.HiWhiteString(level.CapitalString())) + case zapcore.InfoLevel: + enc.AppendString(color.HiCyanString(level.CapitalString())) + case zapcore.WarnLevel: + enc.AppendString(color.HiYellowString(level.CapitalString())) + case zapcore.ErrorLevel, zapcore.DPanicLevel: + enc.AppendString(color.HiRedString(level.CapitalString())) + case zapcore.PanicLevel, zapcore.FatalLevel: + enc.AppendString(color.HiMagentaString(level.CapitalString())) + } +} + +// ColoredNameEncoder colorizes service names. +func ColoredNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + enc.AppendString(color.HiGreenString(s)) +} + +// ColoredHashedNameEncoder colorizes service names and assigns different colors to different names. +func ColoredHashedNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + colorID := stringHash(s, len(colorMap)) + enc.AppendString(colorMap[colorID](s)) +} + +// UTCTimeEncoder encodes time into short UTC specific timestamp. +func UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006/01/02 15:04:05")) +} + +// returns string hash +func stringHash(name string, base int) int { + h := fnv.New32a() + _, _ = h.Write([]byte(name)) + return int(h.Sum32()) % base +} diff --git a/plugins/logger/plugin.go b/plugins/logger/plugin.go new file mode 100644 index 00000000..64b77a64 --- /dev/null +++ b/plugins/logger/plugin.go @@ -0,0 +1,65 @@ +package logger + +import ( + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/plugins/config" + "go.uber.org/zap" +) + +// PluginName declares plugin name. +const PluginName = "logs" + +// ZapLogger manages zap logger. +type ZapLogger struct { + base *zap.Logger + cfg Config + channels ChannelConfig +} + +// Init logger service. +func (z *ZapLogger) Init(cfg config.Configurer) error { + err := cfg.UnmarshalKey(PluginName, &z.cfg) + if err != nil { + return err + } + + err = cfg.UnmarshalKey(PluginName, &z.channels) + if err != nil { + return err + } + + z.base, err = z.cfg.BuildLogger() + return err +} + +// DefaultLogger returns default logger. +func (z *ZapLogger) DefaultLogger() (log.Logger, error) { + return NewZapAdapter(z.base), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) NamedLogger(name string) (log.Logger, error) { + if cfg, ok := z.channels.Channels[name]; ok { + l, err := cfg.BuildLogger() + if err != nil { + return nil, err + } + return NewZapAdapter(l), nil + } + + return NewZapAdapter(z.base.Named(name)), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) ServiceLogger(n endure.Named) (log.Logger, error) { + return z.NamedLogger(n.Name()) +} + +// Provides declares factory methods. +func (z *ZapLogger) Provides() []interface{} { + return []interface{}{ + z.ServiceLogger, + z.DefaultLogger, + } +} diff --git a/plugins/logger/tests/.rr.yaml b/plugins/logger/tests/.rr.yaml new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/logger/tests/.rr.yaml diff --git a/plugins/logger/tests/logger_test.go b/plugins/logger/tests/logger_test.go new file mode 100644 index 00000000..1df74c47 --- /dev/null +++ b/plugins/logger/tests/logger_test.go @@ -0,0 +1,74 @@ +package tests + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/stretchr/testify/assert" +) + +func TestLogger(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + 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, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + 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/logger/tests/plugin.go b/plugins/logger/tests/plugin.go new file mode 100644 index 00000000..32238f63 --- /dev/null +++ b/plugins/logger/tests/plugin.go @@ -0,0 +1,40 @@ +package tests + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin struct { + config config.Configurer + log log.Logger +} + +func (p1 *Plugin) Init(cfg config.Configurer, log log.Logger) error { + p1.config = cfg + p1.log = log + return nil +} + +func (p1 *Plugin) Serve() chan error { + errCh := make(chan error, 1) + p1.log.Error("error", "test", errors.E(errors.Str("test"))) + p1.log.Info("error", "test", errors.E(errors.Str("test"))) + p1.log.Debug("error", "test", errors.E(errors.Str("test"))) + p1.log.Warn("error", "test", errors.E(errors.Str("test"))) + + p1.log.Error("error", "test") + p1.log.Info("error", "test") + p1.log.Debug("error", "test") + p1.log.Warn("error", "test") + return errCh +} + +func (p1 *Plugin) Stop() error { + return nil +} + +func (p1 *Plugin) Name() string { + return "logger_plugin" +} diff --git a/plugins/logger/zap_adapter.go b/plugins/logger/zap_adapter.go new file mode 100644 index 00000000..41e6d8f9 --- /dev/null +++ b/plugins/logger/zap_adapter.go @@ -0,0 +1,57 @@ +package logger + +import ( + "fmt" + + "github.com/spiral/roadrunner/v2/interfaces/log" + "go.uber.org/zap" +) + +type ZapAdapter struct { + zl *zap.Logger +} + +// Create NewZapAdapter which uses general log interface +func NewZapAdapter(zapLogger *zap.Logger) *ZapAdapter { + return &ZapAdapter{ + zl: zapLogger.WithOptions(zap.AddCallerSkip(1)), + } +} + +func (log *ZapAdapter) fields(keyvals []interface{}) []zap.Field { + // we should have even number of keys and values + if len(keyvals)%2 != 0 { + return []zap.Field{zap.Error(fmt.Errorf("odd number of keyvals pairs: %v", keyvals))} + } + + var fields []zap.Field + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyvals[i]) + } + fields = append(fields, zap.Any(key, keyvals[i+1])) + } + + return fields +} + +func (log *ZapAdapter) Debug(msg string, keyvals ...interface{}) { + log.zl.Debug(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Info(msg string, keyvals ...interface{}) { + log.zl.Info(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Warn(msg string, keyvals ...interface{}) { + log.zl.Warn(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Error(msg string, keyvals ...interface{}) { + log.zl.Error(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) With(keyvals ...interface{}) log.Logger { + return NewZapAdapter(log.zl.With(log.fields(keyvals)...)) +} diff --git a/service/metrics/config.go b/plugins/metrics/config.go index c95fd940..933b7eb8 100644 --- a/service/metrics/config.go +++ b/plugins/metrics/config.go @@ -2,8 +2,8 @@ package metrics import ( "fmt" + "github.com/prometheus/client_golang/prometheus" - "github.com/spiral/roadrunner/service" ) // Config configures metrics service. @@ -56,11 +56,6 @@ type Collector struct { 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 { @@ -134,3 +129,7 @@ func (c *Config) getCollectors() (map[string]prometheus.Collector, error) { return collectors, nil } + +func (c *Config) InitDefaults() { + +} diff --git a/service/metrics/config_test.go b/plugins/metrics/config_test.go index a64e9047..665ec9cd 100644 --- a/service/metrics/config_test.go +++ b/plugins/metrics/config_test.go @@ -1,47 +1,56 @@ package metrics import ( - json "github.com/json-iterator/go" + "bytes" + "testing" + + j "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) -} +var json = j.ConfigCompatibleWithStandardLibrary func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"request": {"From": "Something"}}`} + cfg := `{"request": {"From": "Something"}}` c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) - assert.NoError(t, c.Hydrate(cfg)) + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } } func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} + cfg := `{"dir": "/dir/"` c := &Config{} - assert.Error(t, c.Hydrate(cfg)) + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + assert.Error(t, err) } func Test_Config_Metrics(t *testing.T) { - cfg := &mockCfg{`{ + cfg := `{ "collect":{ "metric1":{"type": "gauge"}, "metric2":{ "type": "counter"}, "metric3":{"type": "summary"}, "metric4":{"type": "histogram"} } -}`} +}` c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) - assert.NoError(t, c.Hydrate(cfg)) + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } m, err := c.getCollectors() assert.NoError(t, err) @@ -53,17 +62,22 @@ func Test_Config_Metrics(t *testing.T) { } func Test_Config_MetricsVector(t *testing.T) { - cfg := &mockCfg{`{ + cfg := `{ "collect":{ "metric1":{"type": "gauge","labels":["label"]}, "metric2":{ "type": "counter","labels":["label"]}, "metric3":{"type": "summary","labels":["label"]}, "metric4":{"type": "histogram","labels":["label"]} } -}`} +}` c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) - assert.NoError(t, c.Hydrate(cfg)) + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } m, err := c.getCollectors() assert.NoError(t, err) diff --git a/plugins/metrics/doc.go b/plugins/metrics/doc.go new file mode 100644 index 00000000..1abe097a --- /dev/null +++ b/plugins/metrics/doc.go @@ -0,0 +1 @@ +package metrics diff --git a/plugins/metrics/plugin.go b/plugins/metrics/plugin.go new file mode 100644 index 00000000..c115826b --- /dev/null +++ b/plugins/metrics/plugin.go @@ -0,0 +1,230 @@ +package metrics + +import ( + "context" + "crypto/tls" + "net/http" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/metrics" + "github.com/spiral/roadrunner/v2/plugins/config" + "golang.org/x/sys/cpu" +) + +const ( + // PluginName declares plugin name. + PluginName = "metrics" + // maxHeaderSize declares max header size for prometheus server + maxHeaderSize = 1024 * 1024 * 100 // 104MB +) + +type statsProvider struct { + collectors []prometheus.Collector + name string +} + +// Plugin to manage application metrics using Prometheus. +type Plugin struct { + cfg Config + log log.Logger + mu sync.Mutex // all receivers are pointers + http *http.Server + collectors sync.Map // all receivers are pointers + registry *prometheus.Registry +} + +// Init service. +func (m *Plugin) Init(cfg config.Configurer, log log.Logger) error { + const op = errors.Op("Metrics Init") + err := cfg.UnmarshalKey(PluginName, &m.cfg) + if err != nil { + return err + } + + // TODO figure out what is Init + m.cfg.InitDefaults() + + m.log = log + m.registry = prometheus.NewRegistry() + + // Default + err = m.registry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) + if err != nil { + return errors.E(op, err) + } + + // Default + err = m.registry.Register(prometheus.NewGoCollector()) + if err != nil { + return errors.E(op, err) + } + + collectors, err := m.cfg.getCollectors() + if err != nil { + return errors.E(op, err) + } + + // Register invocation will be later in the Serve method + for k, v := range collectors { + m.collectors.Store(k, statsProvider{ + collectors: []prometheus.Collector{v}, + name: k, + }) + } + return nil +} + +// Register new prometheus collector. +func (m *Plugin) Register(c prometheus.Collector) error { + return m.registry.Register(c) +} + +// Serve prometheus metrics service. +func (m *Plugin) Serve() chan error { + errCh := make(chan error, 1) + m.collectors.Range(func(key, value interface{}) bool { + // key - name + // value - statsProvider struct + c := value.(statsProvider) + for _, v := range c.collectors { + if err := m.registry.Register(v); err != nil { + errCh <- err + return false + } + } + + return true + }) + + 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...) + + m.http = &http.Server{ + Addr: m.cfg.Address, + Handler: promhttp.HandlerFor(m.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, + }, + } + + go func() { + err := m.http.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + errCh <- err + return + } + }() + + return errCh +} + +// Stop prometheus metrics service. +func (m *Plugin) Stop() error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.http != nil { + // timeout is 10 seconds + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + err := m.http.Shutdown(ctx) + if err != nil { + // Function should be Stop() error + m.log.Error("stop error", "error", errors.Errorf("error shutting down the metrics server: error %v", err)) + } + } + return nil +} + +// Collects used to collect all plugins which implement metrics.StatProvider interface (and Named) +func (m *Plugin) Collects() []interface{} { + return []interface{}{ + m.AddStatProvider, + } +} + +// Collector returns application specific collector by name or nil if collector not found. +func (m *Plugin) AddStatProvider(name endure.Named, stat metrics.StatProvider) error { + m.collectors.Store(name.Name(), statsProvider{ + collectors: stat.MetricsCollector(), + name: name.Name(), + }) + return nil +} + +// RPC interface satisfaction +func (m *Plugin) Name() string { + return PluginName +} + +// RPC interface satisfaction +func (m *Plugin) RPC() interface{} { + return &rpcServer{ + svc: m, + log: m.log, + } +} diff --git a/plugins/metrics/rpc.go b/plugins/metrics/rpc.go new file mode 100644 index 00000000..b8897098 --- /dev/null +++ b/plugins/metrics/rpc.go @@ -0,0 +1,294 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" +) + +type rpcServer struct { + svc *Plugin + log log.Logger +} + +// 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) error { + const op = errors.Op("Add metric") + rpc.log.Info("Adding metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "collector", m.Name) + return errors.E(op, errors.Errorf("undefined collector %s, try first Declare the desired collector", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Add(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + case prometheus.Counter: + c.Add(m.Value) + + case *prometheus.CounterVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + + default: + return errors.E(op, errors.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 + rpc.log.Info("new metric successfully added", "name", m.Name, "labels", m.Labels, "value", m.Value) + return nil +} + +// Sub subtract the value from the specific metric (gauge only). +func (rpc *rpcServer) Sub(m *Metric, ok *bool) error { + const op = errors.Op("Subtracting metric") + rpc.log.Info("Subtracting value from metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + // can it be nil ??? I guess can't + return errors.E(op, errors.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 { + rpc.log.Error("required labels for collector, but none was provided", "name", m.Name, "value", m.Value) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Sub(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Sub`", m.Name)) + } + rpc.log.Info("Subtracting operation applied successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} + +// Observe the value (histogram and summary only). +func (rpc *rpcServer) Observe(m *Metric, ok *bool) error { + const op = errors.Op("Observe metrics") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case *prometheus.SummaryVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + return errors.E(op, err) + } + observer.Observe(m.Value) + + case prometheus.Histogram: + c.Observe(m.Value) + + case *prometheus.HistogramVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + observer.Observe(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Observe`", m.Name)) + } + + rpc.log.Info("observe operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *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(nc *NamedCollector, ok *bool) error { + const op = errors.Op("Declare metric") + rpc.log.Info("Declaring new metric", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + _, exist := rpc.svc.collectors.Load(nc.Name) + if exist { + rpc.log.Error("metric with provided name already exist", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + return errors.E(op, errors.Errorf("tried to register existing collector with the name `%s`", nc.Name)) + } + + var collector prometheus.Collector + switch nc.Type { + case Histogram: + opts := prometheus.HistogramOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + Buckets: nc.Buckets, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewHistogramVec(opts, nc.Labels) + } else { + collector = prometheus.NewHistogram(opts) + } + case Gauge: + opts := prometheus.GaugeOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewGaugeVec(opts, nc.Labels) + } else { + collector = prometheus.NewGauge(opts) + } + case Counter: + opts := prometheus.CounterOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewCounterVec(opts, nc.Labels) + } else { + collector = prometheus.NewCounter(opts) + } + case Summary: + opts := prometheus.SummaryOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewSummaryVec(opts, nc.Labels) + } else { + collector = prometheus.NewSummary(opts) + } + + default: + return errors.E(op, errors.Errorf("unknown collector type %s", nc.Type)) + } + + // add collector to sync.Map + rpc.svc.collectors.Store(nc.Name, collector) + // that method might panic, we handle it by recover + err := rpc.svc.Register(collector) + if err != nil { + *ok = false + return errors.E(op, err) + } + + rpc.log.Info("metric successfully added", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + + *ok = true + return nil +} + +// Set the metric value (only for gaude). +func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) { + const op = errors.Op("Set metric") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.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 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Set(m.Value) + + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method Set", m.Name)) + } + + rpc.log.Info("set operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} diff --git a/plugins/metrics/tests/.rr-test.yaml b/plugins/metrics/tests/.rr-test.yaml new file mode 100644 index 00000000..79343e3c --- /dev/null +++ b/plugins/metrics/tests/.rr-test.yaml @@ -0,0 +1,13 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +metrics: + # prometheus client address (path /metrics added automatically) + address: localhost:2112 + collect: + app_metric: + type: histogram + help: "Custom application metric" + labels: [ "type" ] + buckets: [ 0.1, 0.2, 0.3, 1.0 ]
\ No newline at end of file diff --git a/plugins/metrics/tests/docker-compose.yml b/plugins/metrics/tests/docker-compose.yml new file mode 100644 index 00000000..610633b4 --- /dev/null +++ b/plugins/metrics/tests/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus + ports: + - 9090:9090 diff --git a/plugins/metrics/tests/metrics_test.go b/plugins/metrics/tests/metrics_test.go new file mode 100644 index 00000000..f9014c95 --- /dev/null +++ b/plugins/metrics/tests/metrics_test.go @@ -0,0 +1,739 @@ +package tests + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +const dialAddr = "127.0.0.1:6001" +const dialNetwork = "tcp" +const getAddr = "http://localhost:2112/metrics" + +// get request and return body +func get() (string, error) { + r, err := http.Get(getAddr) + if err != nil { + return "", err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", err + } + + err = r.Body.Close() + if err != nil { + return "", err + } + // unsafe + return string(b), err +} + +func TestMetricsInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsGaugeCollector(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + time.Sleep(time.Second) + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + assert.Contains(t, out, "my_gauge 100") + assert.Contains(t, out, "my_gauge2 100") + + out, err = get() + assert.NoError(t, err) + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsDifferentRPCCalls(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + tt := time.NewTimer(time.Minute * 3) + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("DeclareMetric", declareMetricsTest) + genericOut, err := get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector") + + t.Run("AddMetric", addMetricsTest) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector 10000") + + t.Run("SetMetric", setMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "user_gauge_collector 100") + + t.Run("VectorMetric", vectorMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "gauge_2_collector{section=\"first\",type=\"core\"} 100") + + t.Run("MissingSection", missingSection) + t.Run("SetWithoutLabels", setWithoutLabels) + t.Run("SetOnHistogram", setOnHistogram) + t.Run("MetricSub", subMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subMetric 1") + + t.Run("SubVector", subVector) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subVector{section=\"first\",type=\"core\"} 1") + + t.Run("RegisterHistogram", registerHistogram) + + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, `TYPE histogram_registerHistogram`) + + // check buckets + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.1"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.2"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.5"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="+Inf"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_sum 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_count 0`) + + t.Run("CounterMetric", counterMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "HELP default_default_counter_CounterMetric test_counter") + assert.Contains(t, genericOut, `default_default_counter_CounterMetric{section="section2",type="type2"}`) + + t.Run("ObserveMetric", observeMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "observe_observeMetric") + + t.Run("ObserveMetricNotEnoughLabels", observeMetricNotEnoughLabels) + + close(sig) +} + +func observeMetricNotEnoughLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetricNotEnoughLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.Error(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test"}, + }, &ret)) + assert.False(t, ret) +} + +func observeMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test", "test2"}, + }, &ret)) + assert.True(t, ret) +} + +func counterMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "counter_CounterMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_counter", + Type: metrics.Counter, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ + Name: "counter_CounterMetric", + Value: 100.0, + Labels: []string{"type2", "section2"}, + }, &ret)) + assert.True(t, ret) +} + +func registerHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_registerHistogram", + Collector: metrics.Collector{ + Help: "test_histogram", + Type: metrics.Histogram, + Buckets: []float64{0.1, 0.2, 0.5}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "histogram_registerHistogram", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.Error(t, err) + assert.False(t, ret) +} + +func subVector(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subVector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 100000, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 99999, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func subMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 100000, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 99999, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setOnHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_setOnHistogram", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setOnHistogram", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func setWithoutLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_setWithoutLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setWithoutLabels", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func missingSection(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_missing_section_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_missing_section_collector", + Value: 100.0, + Labels: []string{"missing"}, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func vectorMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_2_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_2_collector", + Value: 100.0, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "user_gauge_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "user_gauge_collector", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func addMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + m := metrics.Metric{ + Name: "test_metrics_named_collector", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func declareMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "test_metrics_named_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Counter, + Help: "NO HELP!", + Labels: nil, + Buckets: nil, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} diff --git a/plugins/metrics/tests/plugin1.go b/plugins/metrics/tests/plugin1.go new file mode 100644 index 00000000..b48c415d --- /dev/null +++ b/plugins/metrics/tests/plugin1.go @@ -0,0 +1,46 @@ +package tests + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "metrics_test.plugin1" +} + +func (p1 *Plugin1) MetricsCollector() []prometheus.Collector { + collector := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge", + Help: "My gauge value", + }) + + collector.Set(100) + + collector2 := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge2", + Help: "My gauge2 value", + }) + + collector2.Set(100) + return []prometheus.Collector{collector, collector2} +} diff --git a/plugins/resetter/plugin.go b/plugins/resetter/plugin.go new file mode 100644 index 00000000..99e02aef --- /dev/null +++ b/plugins/resetter/plugin.go @@ -0,0 +1,54 @@ +package resetter + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/resetter" +) + +const PluginName = "resetter" + +type Plugin struct { + registry map[string]resetter.Resetter + log log.Logger +} + +func (p *Plugin) Init(log log.Logger) error { + p.registry = make(map[string]resetter.Resetter) + p.log = log + return nil +} + +// Reset named service. +func (p *Plugin) Reset(name string) error { + svc, ok := p.registry[name] + if !ok { + return errors.E("no such service", errors.Str(name)) + } + + return svc.Reset() +} + +// RegisterTarget resettable service. +func (p *Plugin) RegisterTarget(name endure.Named, r resetter.Resetter) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.RegisterTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} diff --git a/plugins/resetter/rpc.go b/plugins/resetter/rpc.go new file mode 100644 index 00000000..ecc51bb3 --- /dev/null +++ b/plugins/resetter/rpc.go @@ -0,0 +1,30 @@ +package resetter + +import "github.com/spiral/roadrunner/v2/interfaces/log" + +type rpc struct { + srv *Plugin + log log.Logger +} + +// List all resettable services. +func (rpc *rpc) List(_ bool, list *[]string) error { + rpc.log.Debug("started List method") + *list = make([]string, 0) + + for name := range rpc.srv.registry { + *list = append(*list, name) + } + rpc.log.Debug("services list", "services", *list) + + rpc.log.Debug("finished List method") + return nil +} + +// Reset named service. +func (rpc *rpc) Reset(service string, done *bool) error { + rpc.log.Debug("started Reset method for the service", "service", service) + defer rpc.log.Debug("finished Reset method for the service", "service", service) + *done = true + return rpc.srv.Reset(service) +} diff --git a/plugins/resetter/tests/.rr-resetter.yaml b/plugins/resetter/tests/.rr-resetter.yaml new file mode 100644 index 00000000..83ecd582 --- /dev/null +++ b/plugins/resetter/tests/.rr-resetter.yaml @@ -0,0 +1,13 @@ +server: + command: "php ../../../tests/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false
\ No newline at end of file diff --git a/plugins/resetter/tests/resetter_test.go b/plugins/resetter/tests/resetter_test.go new file mode 100644 index 00000000..a1873dd4 --- /dev/null +++ b/plugins/resetter/tests/resetter_test.go @@ -0,0 +1,101 @@ +package tests + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestInformerInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &resetter.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 15) + + t.Run("InformerRpcTest", resetterRPCTest) + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func resetterRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "resetter.plugin1", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NoError(t, err) + if services[0] != "resetter.plugin1" { + t.Fatal("no enough services") + } +} diff --git a/plugins/resetter/tests/test_plugin.go b/plugins/resetter/tests/test_plugin.go new file mode 100644 index 00000000..9f48a43f --- /dev/null +++ b/plugins/resetter/tests/test_plugin.go @@ -0,0 +1,66 @@ +package tests + +import ( + "context" + "time" + + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +var testPoolConfig = roadrunner.PoolConfig{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &roadrunner.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "resetter.plugin1" +} + +func (p1 *Plugin1) Reset() error { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + pool.Destroy(context.Background()) + + pool, err = p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + _ = pool + + return nil +} diff --git a/service/rpc/config.go b/plugins/rpc/config.go index a4cf0f91..719fd5e3 100644..100755 --- a/service/rpc/config.go +++ b/plugins/rpc/config.go @@ -2,36 +2,26 @@ package rpc import ( "errors" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/util" "net" "strings" + + "github.com/spiral/roadrunner/v2/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() + // Disabled disables RPC service. + Disabled bool } // 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 +func (c *Config) InitDefaults() { + if c.Listen == "" { + c.Listen = "tcp://127.0.0.1:6001" + } } // Valid returns nil if config is valid. diff --git a/service/rpc/config_test.go b/plugins/rpc/config_test.go index 1ecd71b3..8b1d974a 100644..100755 --- a/service/rpc/config_test.go +++ b/plugins/rpc/config_test.go @@ -1,39 +1,18 @@ package rpc import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" "testing" + + j "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" ) +var json = j.ConfigCompatibleWithStandardLibrary + 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)) + return json.Unmarshal([]byte(cfg.cfg), out) } func TestConfig_Listener(t *testing.T) { @@ -154,10 +133,6 @@ func Test_Config_DialerErrorMethod(t *testing.T) { 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) + c.InitDefaults() assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) } diff --git a/plugins/rpc/doc/plugin_arch.drawio b/plugins/rpc/doc/plugin_arch.drawio new file mode 100755 index 00000000..dec5f0b2 --- /dev/null +++ b/plugins/rpc/doc/plugin_arch.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2020-10-19T17:14:19.125Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="2J39x4EyFr1zaE9BXKM4" version="13.7.9" type="device"><diagram id="q2oMKs6VHyn7y0AfAXBL" name="Page-1">7Vttc9o4EP41zLQfksE2GPIxQHPXu7RlQntt7ptiC1sX2XJlOUB//a1sGdtIJDQFnE6YyUys1YutfR7trlai44yj5R8cJeEH5mPasbv+suNMOrZtORcO/JOSVSEZWv1CEHDiq0aVYEZ+YCXsKmlGfJw2GgrGqCBJU+ixOMaeaMgQ52zRbDZntPnWBAVYE8w8RHXpV+KLUEkt96Kq+BOTIFSvHtqDoiJCZWM1kzREPlvURM67jjPmjIniKVqOMZXKK/VS9LvaUrv+MI5jsUuHL/zu0yx7//HT3Pln8vfN59vvS/usVHMqVuWMsQ8KUEXGRcgCFiP6rpKOOMtiH8thu1Cq2lwzloDQAuF/WIiVQhNlgoEoFBFVtXhJxLfa860c6ryvSpOlGjkvrMpCLPjqW71Q6yWLVbe8VPabs1hcoYhQKRizjBPMYcIf8UJVqq+8gGKhC6mArTpWohQG8lSrfz88xF8/ds/+uiLe7MsXtLiyZ2clVxEPsHik3WDNBFhCmEUYvh36cUyRIA/N70CKy8G6XQU3PCjEfwZ9q030K8RvazVPoV8BftvA+7dE33KOBP9jX/mAaKbedDOFkbpTmgUk1qjRBH4REoFnCcr1sADj3wT55xVv0PMD5gIvayJdU6rWGSi3otyMYw3OlWRRme21VwlrFtsdHEi9jqbe9zERha+ak0DTL0xVNJWIKAliePZAMaA+ZyQVQsA5XaqKiPh+sShxSn6gu3woiU7CSCzyCfVHnf5EjgXrMC103go+3Q18hho6QwM4pfPcOzg9DZwJTnDspyBk8Rqk8ylnDxCB8N8DLcveD1z2BlxWWa4vpu4x8epreOmuK/YvZcQnIaAoTYm34XeO5kMMun/aFRjdj45QDYG+AYBStrMHUW+YSgpWBOgNtxCgHKJwgapXPercGKhvbwxkbQxUKEYbKCfJetrP542r8aa0vt0U9gsE1rpzKfWVeK97ia+Xc41glolhB1viA32Jj+3O5YhIXc9loAHFEczdpRKWO95Ay/2eyZ1UrqqzQq8S14tkmeurrIanQP0vRvmVQYA052WwVAwHE7+rXrHBp/bCI3f4tPu1jMGReyCwLT06KoLPVPDMExnHmvrSBYkoinGpIVWz07oUcm8y8kJC/Wu0YpmcXiqQd1+WRiHj5AcMi0qIoJqXMNhuo8VM9lQLO1/oeFqiY22IPqBlo+E1SoUSeIxSlKTkbj2NCGwhiUdMCBbt0/k8P47uuQarULapE8Vye4diytDg+ke7R2hAKHaPx4wyIMYkZgWBCKUbopJDFM/FVgalsOEhcXCdt5n0KsmNUoUUMeg7p3kgEoI/wHG+axZIbPUHI9DyWIYl4BnsMZStqpw7iwT22WMWw1wQycHFwKMFTsUvU+Tx1fk0cUr34e7GE/tQBqV0SxpNpJGeYf6QK+VNjMX5TeK9PbGlTbb07ZbZYl1sYUsKTCEeltvAIlKr+aNuSqHqxJw2mTMwBC7HZY6eOSiYMydYni3IeHH8aILnxIk9c8Lq9tomxQ7pCUpyqAszUZ4lWc/iw3qXqQjwOc+8n1kaSRydJI6BEBTdYTqF3WixH57woq1h0/ryueDsGLAOD0UFPeNQ2AcYPmT+G7FK8NvCTMjHkzdply1HdCfmIzhDHvMIR3Av9jDVrKTOjjnUCzPaRzpN1Ra+Ciafk9Xo/nK6wmAsfpMMhrZ+DazZmsHoNTNdPcvgD1xDpmuwB4dgpIX9dLxY8aTKdZ78wp7osn2t/lQyw8SZg3kFPTmqcSZGkTIsgNeJLS2yxZTMOCpb9IizMigcByQFmyITGlYxV4A2o0iqyc+PvOGvYYPmTNbl2Xgzq17Wgdie/Ia1cYFkqO8pHftAx2FGVPUMVVJkul8VLK61cXJl67gc6pTSbAvcVgJ245259TW5Vm5M1k6i9xPlO7uG+b1Ww3zdOVdXCk5h/pHsgtM0C64p7WNywqWz3j8tdsgLX0tXHJ+itiNFbVsu176UIN/SL7xMOQOFR2lOl7a9fN3MP4rYHpbzxq7dsGk/1O1QMzT6nYOAqSAZFqaPvY78hYecQIBjzJGQgbNgsk2UeaH8Ji93RdLvefdY3ohDeZyNlx7G8iGjJMqvA5/pV61fE9YGy93fU6ANxer3NcWNwupXSs67/wE=</diagram></mxfile>
\ No newline at end of file diff --git a/plugins/rpc/plugin.go b/plugins/rpc/plugin.go new file mode 100755 index 00000000..24624d91 --- /dev/null +++ b/plugins/rpc/plugin.go @@ -0,0 +1,165 @@ +package rpc + +import ( + "net" + "net/rpc" + "sync/atomic" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/goridge/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" + rpc_ "github.com/spiral/roadrunner/v2/interfaces/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// PluginName contains default plugin name. +const PluginName = "RPC" + +type pluggable struct { + service rpc_.RPCer + name string +} + +// Plugin is RPC service. +type Plugin struct { + cfg Config + log log.Logger + rpc *rpc.Server + services []pluggable + listener net.Listener + closed *uint32 +} + +// Init rpc service. Must return true if service is enabled. +func (s *Plugin) Init(cfg config.Configurer, log log.Logger) error { + const op = errors.Op("RPC Init") + if !cfg.Has(PluginName) { + return errors.E(op, errors.Disabled) + } + + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return err + } + s.cfg.InitDefaults() + + if s.cfg.Disabled { + return errors.E(op, errors.Disabled) + } + + s.log = log + state := uint32(0) + s.closed = &state + atomic.StoreUint32(s.closed, 0) + + return s.cfg.Valid() +} + +// Serve serves the service. +func (s *Plugin) Serve() chan error { + const op = errors.Op("register service") + errCh := make(chan error, 1) + + s.rpc = rpc.NewServer() + + services := make([]string, 0, len(s.services)) + + // Attach all services + for i := 0; i < len(s.services); i++ { + err := s.Register(s.services[i].name, s.services[i].service.RPC()) + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + services = append(services, s.services[i].name) + } + + var err error + s.listener, err = s.cfg.Listener() + if err != nil { + errCh <- err + return errCh + } + + s.log.Debug("Started RPC service", "address", s.cfg.Listen, "services", services) + + go func() { + for { + conn, err := s.listener.Accept() + if err != nil { + if atomic.LoadUint32(s.closed) == 1 { + // just log and continue, this is not a critical issue, we just called Stop + s.log.Error("listener accept error, connection closed", "error", err) + return + } + + s.log.Error("listener accept error", "error", err) + errCh <- errors.E(errors.Op("listener accept"), errors.Serve, err) + return + } + + go s.rpc.ServeCodec(goridge.NewCodec(conn)) + } + }() + + return errCh +} + +// Stop stops the service. +func (s *Plugin) Stop() error { + // store closed state + atomic.StoreUint32(s.closed, 1) + err := s.listener.Close() + if err != nil { + return errors.E(errors.Op("stop RPC socket"), err) + } + return nil +} + +// Name contains service name. +func (s *Plugin) Name() string { + return PluginName +} + +// Depends declares services to collect for RPC. +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.RegisterPlugin, + } +} + +// RegisterPlugin registers RPC service plugin. +func (s *Plugin) RegisterPlugin(name endure.Named, p rpc_.RPCer) { + s.services = append(s.services, pluggable{ + service: p, + name: name.Name(), + }) +} + +// 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 *Plugin) Register(name string, svc interface{}) error { + if s.rpc == nil { + return errors.E("RPC service is not configured") + } + + return s.rpc.RegisterName(name, svc) +} + +// Client creates new RPC client. +func (s *Plugin) Client() (*rpc.Client, error) { + conn, err := s.cfg.Dialer() + if err != nil { + return nil, err + } + + return rpc.NewClientWithCodec(goridge.NewClientCodec(conn)), nil +} diff --git a/plugins/rpc/tests/.rr-rpc-disabled.yaml b/plugins/rpc/tests/.rr-rpc-disabled.yaml new file mode 100644 index 00000000..624fb3c5 --- /dev/null +++ b/plugins/rpc/tests/.rr-rpc-disabled.yaml @@ -0,0 +1,3 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: true
\ No newline at end of file diff --git a/plugins/rpc/tests/.rr.yaml b/plugins/rpc/tests/.rr.yaml new file mode 100644 index 00000000..76e8b440 --- /dev/null +++ b/plugins/rpc/tests/.rr.yaml @@ -0,0 +1,3 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false
\ No newline at end of file diff --git a/plugins/rpc/tests/plugin1.go b/plugins/rpc/tests/plugin1.go new file mode 100644 index 00000000..79e98ed4 --- /dev/null +++ b/plugins/rpc/tests/plugin1.go @@ -0,0 +1,42 @@ +package tests + +import ( + "fmt" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "rpc_test.plugin1" +} + +func (p1 *Plugin1) RPC() interface{} { + return &PluginRPC{srv: p1} +} + +type PluginRPC struct { + srv *Plugin1 +} + +func (r *PluginRPC) Hello(in string, out *string) error { + *out = fmt.Sprintf("Hello, username: %s", in) + return nil +} diff --git a/plugins/rpc/tests/plugin2.go b/plugins/rpc/tests/plugin2.go new file mode 100644 index 00000000..854bf097 --- /dev/null +++ b/plugins/rpc/tests/plugin2.go @@ -0,0 +1,54 @@ +package tests + +import ( + "net" + "net/rpc" + "time" + + "github.com/spiral/errors" + "github.com/spiral/goridge/v2" +) + +// plugin2 makes a call to the plugin1 via RPC +// this is just a simulation of external call FOR TEST +// you don't need to do such things :) +type Plugin2 struct { +} + +func (p2 *Plugin2) Init() error { + return nil +} + +func (p2 *Plugin2) Serve() chan error { + errCh := make(chan error, 1) + + go func() { + time.Sleep(time.Second * 3) + + conn, err := net.Dial("tcp", "127.0.0.1:6001") + if err != nil { + errCh <- errors.E(errors.Serve, err) + return + } + client := rpc.NewClientWithCodec(goridge.NewClientCodec(conn)) + var ret string + err = client.Call("rpc_test.plugin1.Hello", "Valery", &ret) + if err != nil { + errCh <- err + return + } + if ret != "Hello, username: Valery" { + errCh <- errors.E("wrong response") + return + } + // to stop exec + errCh <- errors.E(errors.Disabled) + return + }() + + return errCh +} + +func (p2 *Plugin2) Stop() error { + return nil +} diff --git a/plugins/rpc/tests/rpc_test.go b/plugins/rpc/tests/rpc_test.go new file mode 100644 index 00000000..88267dfb --- /dev/null +++ b/plugins/rpc/tests/rpc_test.go @@ -0,0 +1,169 @@ +package tests + +import ( + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +// graph https://bit.ly/3ensdNb +func TestRpcInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = ".rr.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + tt := time.NewTimer(time.Second * 10) + + for { + select { + case e := <-ch: + // just stop, this is ok + if errors.Is(errors.Disabled, e.Error) { + return + } + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + assert.Fail(t, "timeout") + } + } +} + +// graph https://bit.ly/3ensdNb +func TestRpcDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = ".rr-rpc-disabled.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + tt := time.NewTimer(time.Second * 20) + + for { + select { + case e := <-ch: + // RPC is turned off, should be and dial error + if errors.Is(errors.Disabled, e.Error) { + assert.FailNow(t, "should not be disabled error") + } + assert.Error(t, e.Error) + err = cont.Stop() + assert.Error(t, err) + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + return + } + } +} diff --git a/plugins/server/config.go b/plugins/server/config.go new file mode 100644 index 00000000..147ae0f7 --- /dev/null +++ b/plugins/server/config.go @@ -0,0 +1,41 @@ +package server + +import ( + "time" + + "github.com/spiral/roadrunner/v2/interfaces/server" +) + +// Config config combines factory, pool and cmd configurations. +type Config struct { + // Command to run as application. + Command string + + // User to run application under. + User string + + // Group to run application under. + Group string + + // Env represents application environment. + Env server.Env + + // 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. + Relay string + + // RelayTimeout defines for how long socket factory will be waiting for worker connection. This config section + // must not change on re-configuration. Defaults to 60s. + RelayTimeout time.Duration +} + +func (cfg *Config) InitDefaults() { + if cfg.Relay == "" { + cfg.Relay = "pipes" + } + + if cfg.RelayTimeout == 0 { + cfg.RelayTimeout = time.Second * 60 + } +} diff --git a/plugins/server/plugin.go b/plugins/server/plugin.go new file mode 100644 index 00000000..ea6d42eb --- /dev/null +++ b/plugins/server/plugin.go @@ -0,0 +1,176 @@ +package server + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/util" +) + +const PluginName = "server" + +// Plugin manages worker +type Plugin struct { + cfg Config + log log.Logger + factory roadrunner.Factory +} + +// Init application provider. +func (server *Plugin) Init(cfg config.Configurer, log log.Logger) error { + const op = errors.Op("Init") + err := cfg.UnmarshalKey(PluginName, &server.cfg) + if err != nil { + return errors.E(op, errors.Init, err) + } + server.cfg.InitDefaults() + server.log = log + + server.factory, err = server.initFactory() + if err != nil { + return errors.E(errors.Op("Init factory"), err) + } + + return nil +} + +// Name contains service name. +func (server *Plugin) Name() string { + return PluginName +} + +func (server *Plugin) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (server *Plugin) Stop() error { + if server.factory == nil { + return nil + } + + return server.factory.Close(context.Background()) +} + +// CmdFactory provides worker command factory assocated with given context. +func (server *Plugin) CmdFactory(env server.Env) (func() *exec.Cmd, error) { + const op = errors.Op("cmd factory") + var cmdArgs []string + + // create command according to the config + cmdArgs = append(cmdArgs, strings.Split(server.cfg.Command, " ")...) + if len(cmdArgs) < 2 { + return nil, errors.E(op, errors.Str("should be in form of `php <script>")) + } + if cmdArgs[0] != "php" { + return nil, errors.E(op, errors.Str("first arg in command should be `php`")) + } + return func() *exec.Cmd { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec + util.IsolateProcess(cmd) + + // if user is not empty, and OS is linux or macos + // execute php worker from that particular user + if server.cfg.User != "" { + err := util.ExecuteFromUser(cmd, server.cfg.User) + if err != nil { + return nil + } + } + + cmd.Env = server.setEnv(env) + + return cmd + }, nil +} + +// NewWorker issues new standalone worker. +func (server *Plugin) NewWorker(ctx context.Context, env server.Env) (roadrunner.WorkerBase, error) { + const op = errors.Op("new worker") + spawnCmd, err := server.CmdFactory(env) + if err != nil { + return nil, errors.E(op, err) + } + + w, err := server.factory.SpawnWorkerWithContext(ctx, spawnCmd()) + if err != nil { + return nil, errors.E(op, err) + } + + w.AddListener(server.collectLogs) + + return w, nil +} + +// NewWorkerPool issues new worker pool. +func (server *Plugin) NewWorkerPool(ctx context.Context, opt roadrunner.PoolConfig, env server.Env) (roadrunner.Pool, error) { + spawnCmd, err := server.CmdFactory(env) + if err != nil { + return nil, err + } + + p, err := roadrunner.NewPool(ctx, spawnCmd, server.factory, opt) + if err != nil { + return nil, err + } + + p.AddListener(server.collectLogs) + + return p, nil +} + +// creates relay and worker factory. +func (server *Plugin) initFactory() (roadrunner.Factory, error) { + const op = errors.Op("network factory init") + if server.cfg.Relay == "" || server.cfg.Relay == "pipes" { + return roadrunner.NewPipeFactory(), nil + } + + dsn := strings.Split(server.cfg.Relay, "://") + if len(dsn) != 2 { + return nil, errors.E(op, errors.Network, errors.Str("invalid DSN (tcp://:6001, unix://file.sock)")) + } + + lsn, err := util.CreateListener(server.cfg.Relay) + if err != nil { + return nil, errors.E(op, errors.Network, err) + } + + switch dsn[0] { + // sockets group + case "unix": + return roadrunner.NewSocketServer(lsn, server.cfg.RelayTimeout), nil + case "tcp": + return roadrunner.NewSocketServer(lsn, server.cfg.RelayTimeout), nil + default: + return nil, errors.E(op, errors.Network, errors.Str("invalid DSN (tcp://:6001, unix://file.sock)")) + } +} + +func (server *Plugin) setEnv(e server.Env) []string { + env := append(os.Environ(), fmt.Sprintf("RR_RELAY=%s", server.cfg.Relay)) + for k, v := range e { + env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(k), v)) + } + + return env +} + +func (server *Plugin) collectLogs(event interface{}) { + if we, ok := event.(roadrunner.WorkerEvent); ok { + switch we.Event { + case roadrunner.EventWorkerError: + server.log.Error(we.Payload.(error).Error(), "pid", we.Worker.Pid()) + case roadrunner.EventWorkerLog: + server.log.Debug(strings.TrimRight(string(we.Payload.([]byte)), " \n\t"), "pid", we.Worker.Pid()) + } + } +} diff --git a/plugins/server/tests/configs/.rr-no-app-section.yaml b/plugins/server/tests/configs/.rr-no-app-section.yaml new file mode 100644 index 00000000..b6e3ea93 --- /dev/null +++ b/plugins/server/tests/configs/.rr-no-app-section.yaml @@ -0,0 +1,9 @@ +server: + command: "php ../../../tests/client.php echo pipes" + 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/server/tests/configs/.rr-sockets.yaml b/plugins/server/tests/configs/.rr-sockets.yaml new file mode 100644 index 00000000..ab1239aa --- /dev/null +++ b/plugins/server/tests/configs/.rr-sockets.yaml @@ -0,0 +1,9 @@ +server: + command: "php socket.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "unix://unix.sock" + relayTimeout: "20s"
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr-tcp.yaml b/plugins/server/tests/configs/.rr-tcp.yaml new file mode 100644 index 00000000..f53bffcc --- /dev/null +++ b/plugins/server/tests/configs/.rr-tcp.yaml @@ -0,0 +1,9 @@ +server: + command: "php tcp.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "tcp://localhost:9999" + relayTimeout: "20s"
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr-wrong-command.yaml b/plugins/server/tests/configs/.rr-wrong-command.yaml new file mode 100644 index 00000000..d2c087a6 --- /dev/null +++ b/plugins/server/tests/configs/.rr-wrong-command.yaml @@ -0,0 +1,9 @@ +server: + command: "php some_absent_file.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" diff --git a/plugins/server/tests/configs/.rr-wrong-relay.yaml b/plugins/server/tests/configs/.rr-wrong-relay.yaml new file mode 100644 index 00000000..1dd73d73 --- /dev/null +++ b/plugins/server/tests/configs/.rr-wrong-relay.yaml @@ -0,0 +1,9 @@ +server: + command: "php ../../../tests/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pupes" + relayTimeout: "20s"
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr.yaml b/plugins/server/tests/configs/.rr.yaml new file mode 100644 index 00000000..b6e3ea93 --- /dev/null +++ b/plugins/server/tests/configs/.rr.yaml @@ -0,0 +1,9 @@ +server: + command: "php ../../../tests/client.php echo pipes" + 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/server/tests/plugin_pipes.go b/plugins/server/tests/plugin_pipes.go new file mode 100644 index 00000000..fbd37e12 --- /dev/null +++ b/plugins/server/tests/plugin_pipes.go @@ -0,0 +1,131 @@ +package tests + +import ( + "context" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" + plugin "github.com/spiral/roadrunner/v2/plugins/server" +) + +const ConfigSection = "server" +const Response = "test" + +var testPoolConfig = roadrunner.PoolConfig{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &roadrunner.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +type Foo struct { + configProvider config.Configurer + wf server.Server + pool roadrunner.Pool +} + +func (f *Foo) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo) Serve() chan error { + const op = errors.Op("serve") + + // test payload for echo + r := roadrunner.Payload{ + Context: nil, + Body: []byte(Response), + } + + errCh := make(chan error, 1) + + conf := &plugin.Config{} + var err error + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := roadrunner.NewSyncWorker(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop(context.Background()) + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/plugins/server/tests/plugin_sockets.go b/plugins/server/tests/plugin_sockets.go new file mode 100644 index 00000000..4942d4c5 --- /dev/null +++ b/plugins/server/tests/plugin_sockets.go @@ -0,0 +1,112 @@ +package tests + +import ( + "context" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" + plugin "github.com/spiral/roadrunner/v2/plugins/server" +) + +type Foo2 struct { + configProvider config.Configurer + wf server.Server + pool roadrunner.Pool +} + +func (f *Foo2) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo2) Serve() chan error { + const op = errors.Op("serve") + var err error + errCh := make(chan error, 1) + conf := &plugin.Config{} + + // test payload for echo + r := roadrunner.Payload{ + Context: nil, + Body: []byte(Response), + } + + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := roadrunner.NewSyncWorker(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop(context.Background()) + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo2) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/plugins/server/tests/plugin_tcp.go b/plugins/server/tests/plugin_tcp.go new file mode 100644 index 00000000..89757a02 --- /dev/null +++ b/plugins/server/tests/plugin_tcp.go @@ -0,0 +1,112 @@ +package tests + +import ( + "context" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2" + "github.com/spiral/roadrunner/v2/interfaces/server" + "github.com/spiral/roadrunner/v2/plugins/config" + plugin "github.com/spiral/roadrunner/v2/plugins/server" +) + +type Foo3 struct { + configProvider config.Configurer + wf server.Server + pool roadrunner.Pool +} + +func (f *Foo3) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo3) Serve() chan error { + const op = errors.Op("serve") + var err error + errCh := make(chan error, 1) + conf := &plugin.Config{} + + // test payload for echo + r := roadrunner.Payload{ + Context: nil, + Body: []byte(Response), + } + + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := roadrunner.NewSyncWorker(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop(context.Background()) + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo3) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/plugins/server/tests/server_test.go b/plugins/server/tests/server_test.go new file mode 100644 index 00000000..bc374a9e --- /dev/null +++ b/plugins/server/tests/server_test.go @@ -0,0 +1,356 @@ +package tests + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestAppPipes(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + 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, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + 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 + } + } +} + +func TestAppSockets(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-sockets.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo2{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + 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, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + 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 + } + } +} + +func TestAppTCP(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-tcp.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + 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, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + 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 + } + } +} + +func TestAppWrongConfig(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rrrrrrrrrr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + assert.Error(t, container.Init()) +} + +func TestAppWrongRelay(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-relay.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + assert.Error(t, err) + + _, err = container.Serve() + assert.Error(t, err) +} + +func TestAppWrongCommand(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-command.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + _, err = container.Serve() + assert.Error(t, err) +} + +func TestAppNoAppSectionInConfig(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-command.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + _, err = container.Serve() + assert.Error(t, err) +} diff --git a/plugins/server/tests/socket.php b/plugins/server/tests/socket.php new file mode 100644 index 00000000..143c3ce4 --- /dev/null +++ b/plugins/server/tests/socket.php @@ -0,0 +1,25 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ + +use Spiral\Goridge; +use Spiral\RoadRunner; + +require dirname(__DIR__) . "/../../vendor_php/autoload.php"; + +$relay = new Goridge\SocketRelay( + "unix.sock", + null, + Goridge\SocketRelay::SOCK_UNIX + ); + +$rr = new RoadRunner\Worker($relay); + +while ($in = $rr->receive($ctx)) { + try { + $rr->send((string)$in); + } catch (\Throwable $e) { + $rr->error((string)$e); + } +} diff --git a/plugins/server/tests/tcp.php b/plugins/server/tests/tcp.php new file mode 100644 index 00000000..2d6fb00a --- /dev/null +++ b/plugins/server/tests/tcp.php @@ -0,0 +1,20 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ + +use Spiral\Goridge; +use Spiral\RoadRunner; + +require dirname(__DIR__) . "/../../vendor_php/autoload.php"; + +$relay = new Goridge\SocketRelay("localhost", 9999); +$rr = new RoadRunner\Worker($relay); + +while ($in = $rr->receive($ctx)) { + try { + $rr->send((string)$in); + } catch (\Throwable $e) { + $rr->error((string)$e); + } +}
\ No newline at end of file diff --git a/plugins/static/config.go b/plugins/static/config.go new file mode 100644 index 00000000..f5d26b2d --- /dev/null +++ b/plugins/static/config.go @@ -0,0 +1,76 @@ +package static + +import ( + "os" + "path" + "strings" + + "github.com/spiral/errors" +) + +// Config describes file location and controls access to them. +type Config struct { + Static 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 + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + const op = errors.Op("static plugin validation") + st, err := os.Stat(c.Static.Dir) + if err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Static.Dir)) + } + + return err + } + + if !st.IsDir() { + return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Static.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.Static.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.Static.Always { + if ext == v { + return true + } + } + + return false +} diff --git a/plugins/static/config_test.go b/plugins/static/config_test.go new file mode 100644 index 00000000..de88ded3 --- /dev/null +++ b/plugins/static/config_test.go @@ -0,0 +1,48 @@ +package static + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_Forbids(t *testing.T) { + cfg := Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}} + + 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{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./"}}).Valid()) + + assert.Error(t, (&Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./config.go"}}).Valid()) + + assert.Error(t, (&Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./dir/"}}).Valid()) +} diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go new file mode 100644 index 00000000..cf5cee25 --- /dev/null +++ b/plugins/static/plugin.go @@ -0,0 +1,110 @@ +package static + +import ( + "net/http" + "path" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ID contains default service name. +const PluginName = "static" + +const RootPluginName = "http" + +// Plugin serves static files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config + + log log.Logger + + // 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 *Plugin) Init(cfg config.Configurer, log log.Logger) error { + const op = errors.Op("static plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.root = http.Dir(s.cfg.Static.Dir) + + err = s.cfg.Valid() + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Static.Request != nil { + for k, v := range s.cfg.Static.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Static.Response != nil { + for k, v := range s.cfg.Static.Response { + w.Header().Set(k, v) + } + } + + if !s.handleStatic(w, r) { + next.ServeHTTP(w, r) + } + } +} + +func (s *Plugin) 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 { + s.log.Error("file open error", "error", err) + if s.cfg.AlwaysServe(fPath) { + w.WriteHeader(404) + return true + } + + return false + } + defer func() { + err = f.Close() + if err != nil { + s.log.Error("file closing error", "error", err) + } + }() + + 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/plugins/static/tests/configs/.rr-http-static-disabled.yaml b/plugins/static/tests/configs/.rr-http-static-disabled.yaml new file mode 100644 index 00000000..d0b9b388 --- /dev/null +++ b/plugins/static/tests/configs/.rr-http-static-disabled.yaml @@ -0,0 +1,30 @@ +server: + command: "php ../../../tests/http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:21234 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + 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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "abc" #not exists + forbid: [ ".php", ".htaccess" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/static/tests/configs/.rr-http-static-files-disable.yaml b/plugins/static/tests/configs/.rr-http-static-files-disable.yaml new file mode 100644 index 00000000..a3d814a3 --- /dev/null +++ b/plugins/static/tests/configs/.rr-http-static-files-disable.yaml @@ -0,0 +1,30 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:45877 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + 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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/static/tests/configs/.rr-http-static-files.yaml b/plugins/static/tests/configs/.rr-http-static-files.yaml new file mode 100644 index 00000000..35938b80 --- /dev/null +++ b/plugins/static/tests/configs/.rr-http-static-files.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../../tests/http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:34653 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + 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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php", ".htaccess" ] + always: [ ".ico" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/static/tests/configs/.rr-http-static.yaml b/plugins/static/tests/configs/.rr-http-static.yaml new file mode 100644 index 00000000..80a1fa7f --- /dev/null +++ b/plugins/static/tests/configs/.rr-http-static.yaml @@ -0,0 +1,29 @@ +server: + command: "php ../../../tests/http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:21603 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + 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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ "" ] + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/plugins/static/tests/static_plugin_test.go b/plugins/static/tests/static_plugin_test.go new file mode 100644 index 00000000..528d5eea --- /dev/null +++ b/plugins/static/tests/static_plugin_test.go @@ -0,0 +1,423 @@ +package tests + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/mocks" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/stretchr/testify/assert" +) + +func TestStaticPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSample", serveStaticSample) + t.Run("StaticNotForbid", staticNotForbid) + t.Run("StaticHeaders", staticHeaders) + wg.Wait() +} + +func staticHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:21603/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)) +} + +func staticNotForbid(t *testing.T) { + b, r, err := get("http://localhost:21603/client.php") + assert.NoError(t, err) + assert.Equal(t, all("../../../tests/client.php"), b) + assert.Equal(t, all("../../../tests/client.php"), b) + _ = r.Body.Close() +} + +func serveStaticSample(t *testing.T) { + b, r, err := get("http://localhost:21603/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample", b) + _ = r.Body.Close() +} + +func TestStaticDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-disabled.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticDisabled", staticDisabled) + wg.Wait() +} + +func staticDisabled(t *testing.T) { + _, r, err := get("http://localhost:21234/sample.txt") + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestStaticFilesDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files-disable.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticFilesDisabled", staticFilesDisabled) + wg.Wait() +} + +func staticFilesDisabled(t *testing.T) { + b, r, err := get("http://localhost:45877/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func TestStaticFilesForbid(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").AnyTimes() + mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticTestFilesDir", staticTestFilesDir) + t.Run("StaticNotFound", staticNotFound) + t.Run("StaticFilesForbid", staticFilesForbid) + t.Run("StaticFilesAlways", staticFilesAlways) + wg.Wait() +} + +func staticTestFilesDir(t *testing.T) { + b, r, err := get("http://localhost:34653/http?hello=world") + assert.NoError(t, err) + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func staticNotFound(t *testing.T) { + b, _, _ := get("http://localhost:34653/client.XXX?hello=world") + assert.Equal(t, "WORLD", b) +} + +func staticFilesAlways(t *testing.T) { + _, r, err := get("http://localhost:34653/favicon.ico") + assert.NoError(t, err) + assert.Equal(t, 404, r.StatusCode) + _ = r.Body.Close() +} + +func staticFilesForbid(t *testing.T) { + b, r, err := get("http://localhost:34653/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +// HELPERS +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 all(fn string) string { + f, _ := os.Open(fn) + + b := new(bytes.Buffer) + _, err := io.Copy(b, f) + if err != nil { + return "" + } + + err = f.Close() + if err != nil { + return "" + } + + return b.String() +} @@ -1,39 +1,139 @@ package roadrunner +import ( + "context" + "runtime" + "time" + + "github.com/spiral/roadrunner/v2/util" +) + +// PoolEvent triggered by pool on different events. Pool as also trigger WorkerEvent in case of log. +type PoolEvent struct { + // Event type, see below. + Event int64 + + // Payload depends on event type, typically it's worker or error. + Payload interface{} +} + const ( // EventWorkerConstruct thrown when new worker is spawned. - EventWorkerConstruct = iota + 100 + EventWorkerConstruct = iota + 7800 // EventWorkerDestruct thrown after worker destruction. EventWorkerDestruct - // EventWorkerKill thrown after worker is being forcefully killed. - EventWorkerKill + // EventPoolError caused on pool wide errors. + EventPoolError - // EventWorkerError thrown any worker related even happen (passed with WorkerError) - EventWorkerError + // EventSupervisorError triggered when supervisor can not complete work. + EventSupervisorError - // EventWorkerDead thrown when worker stops worker for any reason. - EventWorkerDead + // EventNoFreeWorkers triggered when there are no free workers in the stack and timeout for worker allocate elapsed + EventNoFreeWorkers - // EventPoolError caused on pool wide errors - EventPoolError + // todo: EventMaxMemory caused when worker consumes more memory than allowed. + EventMaxMemory + + // todo: EventTTL thrown when worker is removed due TTL being reached. Context is rr.WorkerError + EventTTL + + // todo: EventIdleTTL triggered when worker spends too much time at rest. + EventIdleTTL + + // todo: EventExecTTL triggered when worker spends too much time doing the task (max_execution_time). + EventExecTTL ) // Pool managed set of inner worker processes. type Pool interface { - // Listen all caused events to attached controller. - Listen(l func(event int, ctx interface{})) + // AddListener connects event listener to the pool. + AddListener(listener util.EventListener) + + // GetConfig returns pool configuration. + GetConfig() PoolConfig - // Exec one task with given payload and context, returns result or error. - Exec(rqs *Payload) (rsp *Payload, err error) + // Exec + Exec(rqs Payload) (Payload, error) + + ExecWithContext(ctx context.Context, rqs Payload) (Payload, error) // Workers returns worker list associated with the pool. - Workers() (workers []*Worker) + Workers() (workers []WorkerBase) + + // Remove worker from the pool. + RemoveWorker(worker WorkerBase) error + + // Destroy all underlying stack (but let them to complete the task). + Destroy(ctx context.Context) +} - // Remove forces pool to remove specific worker. Return true is this is first remove request on given worker. - Remove(w *Worker, err error) bool +// Configures the pool behaviour. +type PoolConfig struct { + // Debug flag creates new fresh worker before every request. + Debug bool + + // NumWorkers defines how many sub-processes can be run at once. This value + // might be doubled by Swapper while hot-swap. Defaults to number of CPU cores. + NumWorkers int64 + + // MaxJobs defines how many executions is allowed for the worker until + // it's destruction. set 1 to create new process for each new task, 0 to let + // worker handle as many tasks as it can. + MaxJobs int64 + + // AllocateTimeout defines for how long pool will be waiting for a worker to + // be freed to handle the task. Defaults to 60s. + AllocateTimeout time.Duration + + // DestroyTimeout defines for how long pool should be waiting for worker to + // properly destroy, if timeout reached worker will be killed. Defaults to 60s. + DestroyTimeout time.Duration + + // Supervision config to limit worker and pool memory usage. + Supervisor *SupervisorConfig +} + +// InitDefaults enables default config values. +func (cfg *PoolConfig) InitDefaults() { + if cfg.NumWorkers == 0 { + cfg.NumWorkers = int64(runtime.NumCPU()) + } + + if cfg.AllocateTimeout == 0 { + cfg.AllocateTimeout = time.Minute + } + + if cfg.DestroyTimeout == 0 { + cfg.DestroyTimeout = time.Minute + } + if cfg.Supervisor == nil { + return + } + cfg.Supervisor.InitDefaults() +} + +type SupervisorConfig struct { + // WatchTick defines how often to check the state of worker. + WatchTick uint64 + + // TTL defines maximum time worker is allowed to live. + TTL uint64 + + // IdleTTL defines maximum duration worker can spend in idle mode. Disabled when 0. + IdleTTL uint64 + + // ExecTTL defines maximum lifetime per job. + ExecTTL uint64 + + // MaxWorkerMemory limits memory per worker. + MaxWorkerMemory uint64 +} - // Destroy all underlying workers (but let them to complete the task). - Destroy() +// InitDefaults enables default config values. +func (cfg *SupervisorConfig) InitDefaults() { + if cfg.WatchTick == 0 { + cfg.WatchTick = 1 + } } diff --git a/util/state.go b/process_state.go index 29fca945..1291a904 100644..100755 --- a/util/state.go +++ b/process_state.go @@ -1,13 +1,12 @@ -package util +package roadrunner import ( - "errors" "github.com/shirou/gopsutil/process" - "github.com/spiral/roadrunner" + "github.com/spiral/errors" ) -// 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 +24,29 @@ 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) { + const op = errors.Op("worker_process state") + p, _ := process.NewProcess(int32(w.Pid())) i, err := p.MemoryInfo() if err != nil { - return nil, err + return ProcessState{}, errors.E(op, 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() { + state, err := WorkerProcessState(w) if err != nil { return nil, err } diff --git a/protocol.go b/protocol.go index b00eb2a4..fe1f23a7 100644..100755 --- a/protocol.go +++ b/protocol.go @@ -1,12 +1,15 @@ package roadrunner import ( - "fmt" - json "github.com/json-iterator/go" - "github.com/spiral/goridge/v2" "os" + + j "github.com/json-iterator/go" + "github.com/spiral/errors" + "github.com/spiral/goridge/v2" ) +var json = j.ConfigCompatibleWithStandardLibrary + type stopCommand struct { Stop bool `json:"stop"` } @@ -16,36 +19,43 @@ type pidCommand struct { } func sendControl(rl goridge.Relay, v interface{}) error { + const op = errors.Op("send control") if data, ok := v.([]byte); ok { - return rl.Send(data, goridge.PayloadControl|goridge.PayloadRaw) + err := rl.Send(data, goridge.PayloadControl|goridge.PayloadRaw) + if err != nil { + return errors.E(op, err) + } + return nil } - j := json.ConfigCompatibleWithStandardLibrary - data, err := j.Marshal(v) + data, err := json.Marshal(v) if err != nil { - return fmt.Errorf("invalid payload: %s", err) + return errors.E(op, errors.Errorf("invalid payload: %s", err)) } return rl.Send(data, goridge.PayloadControl) } -func fetchPID(rl goridge.Relay) (pid int, err error) { - if err := sendControl(rl, pidCommand{Pid: os.Getpid()}); err != nil { - return 0, err +func fetchPID(rl goridge.Relay) (int64, error) { + const op = errors.Op("fetchPID") + err := sendControl(rl, pidCommand{Pid: os.Getpid()}) + if err != nil { + return 0, errors.E(op, err) } body, p, err := rl.Receive() if err != nil { - return 0, err + return 0, errors.E(op, err) } if !p.HasFlag(goridge.PayloadControl) { - return 0, fmt.Errorf("unexpected response, header is missing") + return 0, errors.E(op, errors.Str("unexpected response, header is missing")) } link := &pidCommand{} - if err := json.Unmarshal(body, link); err != nil { - return 0, err + err = json.Unmarshal(body, link) + if err != nil { + return 0, errors.E(op, err) } - return link.Pid, nil + return int64(link.Pid), nil } diff --git a/protocol_test.go b/protocol_test.go index 55c603a5..396ce992 100644..100755 --- 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/container.go b/service/container.go deleted file mode 100644 index 77a6dfc0..00000000 --- a/service/container.go +++ /dev/null @@ -1,371 +0,0 @@ -package service - -import ( - "fmt" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "reflect" - "sync" -) - -var errNoConfig = fmt.Errorf("no config has been provided") -var errTempFix223 = fmt.Errorf("temporary error for fix #223") // meant no error here, just shutdown the server - -// InitMethod contains name of the method to be automatically invoked while service initialization. Must return -// (bool, error). Container can be requested as well. Config can be requested in a form -// of service.Config or pointer to service specific config struct (automatically unmarshalled), config argument must -// implement service.HydrateConfig. -const InitMethod = "Init" - -// Service can serve. Services can provide Init method which must return (bool, error) signature and might accept -// other services and/or configs as dependency. -type Service interface { - // Serve serves. - Serve() error - - // Detach stops the service. - Stop() -} - -// Container controls all internal RR services and provides plugin based system. -type Container interface { - // Register add new service to the container under given name. - Register(name string, service interface{}) - - // Reconfigure configures all underlying services with given configuration. - Init(cfg Config) error - - // Check if svc has been registered. - Has(service string) bool - - // get returns svc instance by it's name or nil if svc not found. Method returns current service status - // as second value. - Get(service string) (svc interface{}, status int) - - // Serve all configured services. Non blocking. - Serve() error - - // Close all active services. - Stop() - - // List service names. - List() []string -} - -// Config provides ability to slice configuration sections and unmarshal configuration data into -// given structure. -type Config interface { - // get nested config section (sub-map), returns nil if section not found. - Get(service string) Config - - // Unmarshal unmarshal config data into given struct. - Unmarshal(out interface{}) error -} - -// HydrateConfig provides ability to automatically hydrate config with values using -// service.Config as the source. -type HydrateConfig interface { - // Hydrate must populate config values using given config source. - // Must return error if config is not valid. - Hydrate(cfg Config) error -} - -// DefaultsConfig declares ability to be initated without config data provided. -type DefaultsConfig interface { - // InitDefaults allows to init blank config with pre-defined set of default values. - InitDefaults() error -} - -type container struct { - log logrus.FieldLogger - mu sync.Mutex - services []*entry - errc chan struct { - name string - err error - } -} - -// NewContainer creates new service container. -func NewContainer(log logrus.FieldLogger) Container { - return &container{ - log: log, - services: make([]*entry, 0), - errc: make(chan struct { - name string - err error - }, 1), - } -} - -// Register add new service to the container under given name. -func (c *container) Register(name string, service interface{}) { - c.mu.Lock() - defer c.mu.Unlock() - - c.services = append(c.services, &entry{ - name: name, - svc: service, - status: StatusInactive, - }) -} - -// Check hasStatus svc has been registered. -func (c *container) Has(target string) bool { - c.mu.Lock() - defer c.mu.Unlock() - - for _, e := range c.services { - if e.name == target { - return true - } - } - - return false -} - -// get returns svc instance by it's name or nil if svc not found. -func (c *container) Get(target string) (svc interface{}, status int) { - c.mu.Lock() - defer c.mu.Unlock() - - for _, e := range c.services { - if e.name == target { - return e.svc, e.getStatus() - } - } - - return nil, StatusUndefined -} - -// Init configures all underlying services with given configuration. -func (c *container) Init(cfg Config) error { - for _, e := range c.services { - if e.getStatus() >= StatusOK { - return fmt.Errorf("service [%s] has already been configured", e.name) - } - - // inject service dependencies - if ok, err := c.initService(e.svc, cfg.Get(e.name)); err != nil { - // soft error (skipping) - if err == errNoConfig { - c.log.Debugf("[%s]: disabled", e.name) - continue - } - - return errors.Wrap(err, fmt.Sprintf("[%s]", e.name)) - } else if ok { - e.setStatus(StatusOK) - } else { - c.log.Debugf("[%s]: disabled", e.name) - } - } - - return nil -} - -// Serve all configured services. Non blocking. -func (c *container) Serve() error { - var running = 0 - for _, e := range c.services { - if e.hasStatus(StatusOK) && e.canServe() { - running++ - c.log.Debugf("[%s]: started", e.name) - go func(e *entry) { - e.setStatus(StatusServing) - defer e.setStatus(StatusStopped) - if err := e.svc.(Service).Serve(); err != nil { - c.errc <- struct { - name string - err error - }{name: e.name, err: errors.Wrap(err, fmt.Sprintf("[%s]", e.name))} - } else { - c.errc <- struct { - name string - err error - }{name: e.name, err: errTempFix223} - } - }(e) - } - } - - // simple handler to handle empty configs - if running == 0 { - return nil - } - - for fail := range c.errc { - if fail.err == errTempFix223 { - // if we call stop, then stop all plugins - break - } else { - c.log.Errorf("[%s]: %s", fail.name, fail.err) - c.Stop() - return fail.err - } - } - - return nil -} - -// Detach sends stop command to all running services. -func (c *container) Stop() { - for _, e := range c.services { - if e.hasStatus(StatusServing) { - e.setStatus(StatusStopping) - e.svc.(Service).Stop() - e.setStatus(StatusStopped) - - c.log.Debugf("[%s]: stopped", e.name) - } - } -} - -// List all service names. -func (c *container) List() []string { - names := make([]string, 0, len(c.services)) - for _, e := range c.services { - names = append(names, e.name) - } - - return names -} - -// calls Init method with automatically resolved arguments. -func (c *container) initService(s interface{}, segment Config) (bool, error) { - r := reflect.TypeOf(s) - - m, ok := r.MethodByName(InitMethod) - if !ok { - // no Init method is presented, assuming service does not need initialization. - return true, nil - } - - if err := c.verifySignature(m); err != nil { - return false, err - } - - // hydrating - values, err := c.resolveValues(s, m, segment) - if err != nil { - return false, err - } - - // initiating service - out := m.Func.Call(values) - - if out[1].IsNil() { - return out[0].Bool(), nil - } - - return out[0].Bool(), out[1].Interface().(error) -} - -// resolveValues returns slice of call arguments for service Init method. -func (c *container) resolveValues(s interface{}, m reflect.Method, cfg Config) (values []reflect.Value, err error) { - for i := 0; i < m.Type.NumIn(); i++ { - v := m.Type.In(i) - - switch { - case v.ConvertibleTo(reflect.ValueOf(s).Type()): // service itself - values = append(values, reflect.ValueOf(s)) - - case v.Implements(reflect.TypeOf((*Container)(nil)).Elem()): // container - values = append(values, reflect.ValueOf(c)) - - case v.Implements(reflect.TypeOf((*logrus.StdLogger)(nil)).Elem()), - v.Implements(reflect.TypeOf((*logrus.FieldLogger)(nil)).Elem()), - v.ConvertibleTo(reflect.ValueOf(c.log).Type()): // logger - values = append(values, reflect.ValueOf(c.log)) - - case v.Implements(reflect.TypeOf((*HydrateConfig)(nil)).Elem()): // injectable config - sc := reflect.New(v.Elem()) - - if dsc, ok := sc.Interface().(DefaultsConfig); ok { - err := dsc.InitDefaults() - if err != nil { - return nil, err - } - if cfg == nil { - values = append(values, sc) - continue - } - - } else if cfg == nil { - return nil, errNoConfig - } - - if err := sc.Interface().(HydrateConfig).Hydrate(cfg); err != nil { - return nil, err - } - - values = append(values, sc) - - case v.Implements(reflect.TypeOf((*Config)(nil)).Elem()): // generic config section - if cfg == nil { - return nil, errNoConfig - } - - values = append(values, reflect.ValueOf(cfg)) - - default: // dependency on other service (resolution to nil if service can't be found) - value, err := c.resolveValue(v) - if err != nil { - return nil, err - } - - values = append(values, value) - } - } - - return -} - -// verifySignature checks if Init method has valid signature -func (c *container) verifySignature(m reflect.Method) error { - if m.Type.NumOut() != 2 { - return fmt.Errorf("method Init must have exact 2 return values") - } - - if m.Type.Out(0).Kind() != reflect.Bool { - return fmt.Errorf("first return value of Init method must be bool type") - } - - if !m.Type.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { - return fmt.Errorf("second return value of Init method value must be error type") - } - - return nil -} - -func (c *container) resolveValue(v reflect.Type) (reflect.Value, error) { - value := reflect.Value{} - for _, e := range c.services { - if !e.hasStatus(StatusOK) { - continue - } - - if v.Kind() == reflect.Interface && reflect.TypeOf(e.svc).Implements(v) { - if value.IsValid() { - return value, fmt.Errorf("disambiguous dependency `%s`", v) - } - - value = reflect.ValueOf(e.svc) - } - - if v.ConvertibleTo(reflect.ValueOf(e.svc).Type()) { - if value.IsValid() { - return value, fmt.Errorf("disambiguous dependency `%s`", v) - } - - value = reflect.ValueOf(e.svc) - } - } - - if !value.IsValid() { - // placeholder (make sure to check inside the method) - value = reflect.New(v).Elem() - } - - return value, nil -} diff --git a/service/container_test.go b/service/container_test.go deleted file mode 100644 index b3ec7054..00000000 --- a/service/container_test.go +++ /dev/null @@ -1,533 +0,0 @@ -package service - -import ( - "errors" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/stretchr/testify/assert" - "sync" - "testing" - "time" -) - -type testService struct { - mu sync.Mutex - waitForServe chan interface{} - delay time.Duration - ok bool - cfg Config - c Container - cfgE, serveE error - done chan interface{} -} - -func (t *testService) Init(cfg Config, c Container) (enabled bool, err error) { - t.cfg = cfg - t.c = c - t.done = make(chan interface{}) - return t.ok, t.cfgE -} - -func (t *testService) Serve() error { - time.Sleep(t.delay) - - if t.serveE != nil { - return t.serveE - } - - if c := t.waitChan(); c != nil { - close(c) - t.setChan(nil) - } - - <-t.done - return nil -} - -func (t *testService) Stop() { - close(t.done) -} - -func (t *testService) waitChan() chan interface{} { - t.mu.Lock() - defer t.mu.Unlock() - - return t.waitForServe -} - -func (t *testService) setChan(c chan interface{}) { - t.mu.Lock() - defer t.mu.Unlock() - - t.waitForServe = c -} - -type testCfg struct{ cfg string } - -func (cfg *testCfg) Get(name string) Config { - vars := make(map[string]interface{}) - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.cfg), &vars) - if err != nil { - panic("error unmarshalling the cfg.cfg value") - } - - v, ok := vars[name] - if !ok { - return nil - } - - d, _ := j.Marshal(v) - return &testCfg{cfg: string(d)} -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -// Config defines RPC service config. -type dConfig struct { - // Indicates if RPC connection is enabled. - Value string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *dConfig) Hydrate(cfg Config) error { - return cfg.Unmarshal(c) -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *dConfig) InitDefaults() error { - c.Value = "default" - - return nil -} - -type dService struct { - Cfg *dConfig -} - -func (s *dService) Init(cfg *dConfig) (bool, error) { - s.Cfg = cfg - return true, nil -} - -func TestContainer_Register(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) -} - -func TestContainer_Has(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) - - assert.True(t, c.Has("test")) - assert.False(t, c.Has("another")) -} - -func TestContainer_List(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - - assert.Equal(t, 0, len(hook.Entries)) - assert.Equal(t, 1, len(c.List())) - - assert.True(t, c.Has("test")) - assert.False(t, c.Has("another")) -} - -func TestContainer_Get(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - assert.Equal(t, 0, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) - - s, st = c.Get("another") - assert.Nil(t, s) - assert.Equal(t, StatusUndefined, st) -} - -func TestContainer_Stop_NotStarted(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testService{}) - assert.Equal(t, 0, len(hook.Entries)) - - c.Stop() -} - -func TestContainer_Configure(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusOK, st) -} - -func TestContainer_Init_Default(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &dService{} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{}`})) - - s, st := c.Get("test") - assert.IsType(t, &dService{}, s) - assert.Equal(t, StatusOK, st) - - assert.Equal(t, "default", svc.Cfg.Value) -} - -func TestContainer_Init_Default_Overwrite(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &dService{} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":{"value": "something"}}`})) - - s, st := c.Get("test") - assert.IsType(t, &dService{}, s) - assert.Equal(t, StatusOK, st) - - assert.Equal(t, "something", svc.Cfg.Value) -} - -func TestContainer_ConfigureNull(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"another":"something"}`})) - assert.Equal(t, 1, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureDisabled(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: false} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - assert.Equal(t, 1, len(hook.Entries)) - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureError(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: false, - cfgE: errors.New("configure error"), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - err := c.Init(&testCfg{`{"test":"something"}`}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "configure error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusInactive, st) -} - -func TestContainer_ConfigureTwice(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - assert.Error(t, c.Init(&testCfg{`{"test":"something"}`})) -} - -// bug #276 test -func TestContainer_ServeEmptyContainer(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ok: true} - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - - go assert.NoError(t, c.Serve()) - - time.Sleep(time.Millisecond * 500) - - c.Stop() -} - -func TestContainer_Serve(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - waitForServe: make(chan interface{}), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - go func() { - assert.NoError(t, c.Serve()) - }() - - <-svc.waitChan() - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusServing, st) - - c.Stop() - - s, st = c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -func TestContainer_ServeError(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - waitForServe: make(chan interface{}), - serveE: errors.New("serve error"), - } - - c := NewContainer(logger) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something"}`})) - - err := c.Serve() - assert.Error(t, err) - assert.Contains(t, err.Error(), "serve error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -func TestContainer_ServeErrorMultiple(t *testing.T) { - logger, hook := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - svc := &testService{ - ok: true, - delay: time.Millisecond * 10, - waitForServe: make(chan interface{}), - serveE: errors.New("serve error"), - } - - svc2 := &testService{ - ok: true, - waitForServe: make(chan interface{}), - } - - c := NewContainer(logger) - c.Register("test2", svc2) - c.Register("test", svc) - assert.Equal(t, 0, len(hook.Entries)) - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) - - err := c.Serve() - assert.Error(t, err) - assert.Contains(t, err.Error(), "serve error") - assert.Contains(t, err.Error(), "test") - - s, st := c.Get("test") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) - - s, st = c.Get("test2") - assert.IsType(t, &testService{}, s) - assert.Equal(t, StatusStopped, st) -} - -type testInitA struct{} - -func (t *testInitA) Init() error { - return nil -} - -type testInitB struct{} - -func (t *testInitB) Init() (int, error) { - return 0, nil -} - -func TestContainer_InitErrorA(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitA{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -func TestContainer_InitErrorB(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitB{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -type testInitC struct{} - -func (r *testInitC) Test() bool { - return true -} - -func TestContainer_NoInit(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":"something-else"}`})) -} - -type testInitD struct { - c *testInitC //nolint:golint,unused,structcheck -} - -type DCfg struct { - V string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *DCfg) Hydrate(cfg Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - if c.V == "fail" { - return errors.New("failed config") - } - - return nil -} - -func (t *testInitD) Init(r *testInitC, c Container, cfg *DCfg) (bool, error) { - if r == nil { - return false, errors.New("unable to find testInitC") - } - - if c == nil { - return false, errors.New("unable to find Container") - } - - if cfg.V != "ok" { - return false, errors.New("invalid config") - } - - return false, nil -} - -func TestContainer_InitDependency(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - c.Register("test2", &testInitD{}) - - assert.NoError(t, c.Init(&testCfg{`{"test":"something", "test2":{"v":"ok"}}`})) -} - -func TestContainer_InitDependencyFail(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test", &testInitC{}) - c.Register("test2", &testInitD{}) - - assert.Error(t, c.Init(&testCfg{`{"test":"something", "test2":{"v":"fail"}}`})) -} - -func TestContainer_InitDependencyEmpty(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := NewContainer(logger) - c.Register("test2", &testInitD{}) - - assert.Contains(t, c.Init(&testCfg{`{"test2":{"v":"ok"}}`}).Error(), "testInitC") -} diff --git a/service/entry.go b/service/entry.go deleted file mode 100644 index 497742d1..00000000 --- a/service/entry.go +++ /dev/null @@ -1,59 +0,0 @@ -package service - -import ( - "sync" -) - -const ( - // StatusUndefined when service bus can not find the service. - StatusUndefined = iota - - // StatusInactive when service has been registered in container. - StatusInactive - - // StatusOK when service has been properly configured. - StatusOK - - // StatusServing when service is currently done. - StatusServing - - // StatusStopping when service is currently stopping. - StatusStopping - - // StatusStopped when service being stopped. - StatusStopped -) - -// entry creates association between service instance and given name. -type entry struct { - name string - svc interface{} - mu sync.Mutex - status int -} - -// status returns service status -func (e *entry) getStatus() int { - e.mu.Lock() - defer e.mu.Unlock() - - return e.status -} - -// setStarted indicates that service hasStatus status. -func (e *entry) setStatus(status int) { - e.mu.Lock() - defer e.mu.Unlock() - e.status = status -} - -// hasStatus checks if entry in specific status -func (e *entry) hasStatus(status int) bool { - return e.getStatus() == status -} - -// canServe returns true is service can serve. -func (e *entry) canServe() bool { - _, ok := e.svc.(Service) - return ok -} diff --git a/service/entry_test.go b/service/entry_test.go deleted file mode 100644 index b5c71a10..00000000 --- a/service/entry_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package service - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestEntry_CanServeFalse(t *testing.T) { - e := &entry{svc: nil} - assert.False(t, e.canServe()) -} - -func TestEntry_CanServeTrue(t *testing.T) { - e := &entry{svc: &testService{}} - assert.True(t, e.canServe()) -} diff --git a/service/env/config.go b/service/env/config.go deleted file mode 100644 index a7da695e..00000000 --- a/service/env/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package env - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config defines set of env values for RR workers. -type Config struct { - // values to set as worker _ENV. - Values map[string]string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(&c.Values) -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *Config) InitDefaults() error { - c.Values = make(map[string]string) - return nil -} diff --git a/service/env/config_test.go b/service/env/config_test.go deleted file mode 100644 index a526990d..00000000 --- a/service/env/config_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package env - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"key":"value"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 1) -} - -func Test_Config_Hydrate_Empty(t *testing.T) { - cfg := &mockCfg{`{}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 0) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("Test_Config_Defaults failed: error %v", err) - } - assert.Len(t, c.Values, 0) -} diff --git a/service/env/environment.go b/service/env/environment.go deleted file mode 100644 index ab8febf7..00000000 --- a/service/env/environment.go +++ /dev/null @@ -1,23 +0,0 @@ -package env - -// Environment aggregates list of environment variables. This interface can be used in custom implementation to drive -// values from external sources. -type Environment interface { - Setter - Getter - - // Copy all environment values. - Copy(setter Setter) error -} - -// Setter provides ability to set environment value. -type Setter interface { - // SetEnv sets or creates environment value. - SetEnv(key, value string) -} - -// Getter provides ability to set environment value. -type Getter interface { - // GetEnv must return list of env variables. - GetEnv() (map[string]string, error) -} diff --git a/service/env/service.go b/service/env/service.go deleted file mode 100644 index 83175b36..00000000 --- a/service/env/service.go +++ /dev/null @@ -1,55 +0,0 @@ -package env - -// ID contains default service name. -const ID = "env" - -// Service provides ability to map _ENV values from config file. -type Service struct { - // values is default set of values. - values map[string]string -} - -// NewService creates new env service instance for given rr version. -func NewService(defaults map[string]string) *Service { - s := &Service{values: defaults} - return s -} - -// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config) (bool, error) { - if s.values == nil { - s.values = make(map[string]string) - s.values["RR"] = "true" - } - - for k, v := range cfg.Values { - s.values[k] = v - } - - return true, nil -} - -// GetEnv must return list of env variables. -func (s *Service) GetEnv() (map[string]string, error) { - return s.values, nil -} - -// SetEnv sets or creates environment value. -func (s *Service) SetEnv(key, value string) { - s.values[key] = value -} - -// Copy all environment values. -func (s *Service) Copy(setter Setter) error { - values, err := s.GetEnv() - if err != nil { - return err - } - - for k, v := range values { - setter.SetEnv(k, v) - } - - return nil -} diff --git a/service/env/service_test.go b/service/env/service_test.go deleted file mode 100644 index 19cc03c7..00000000 --- a/service/env/service_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package env - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_NewService(t *testing.T) { - s := NewService(map[string]string{"version": "test"}) - assert.Len(t, s.values, 1) -} - -func Test_Init(t *testing.T) { - var err error - s := &Service{} - _, err = s.Init(&Config{}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 1) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Equal(t, "true", values["RR"]) -} - -func Test_Extend(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 2) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value", values["key"]) -} - -func Test_Set(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - s.SetEnv("key", "value-new") - s.SetEnv("other", "new") - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} - -func Test_Copy(t *testing.T) { - s1 := NewService(map[string]string{"RR": "version"}) - s2 := NewService(map[string]string{}) - - s1.SetEnv("key", "value-new") - s1.SetEnv("other", "new") - - assert.NoError(t, s1.Copy(s2)) - - values, err := s2.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} diff --git a/service/gzip/config.go b/service/gzip/config.go deleted file mode 100644 index 00ac559d..00000000 --- a/service/gzip/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package gzip - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config describes file location and controls access to them. -type Config struct { - Enable bool -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Enable = true - - return nil -} diff --git a/service/gzip/config_test.go b/service/gzip/config_test.go deleted file mode 100644 index c2168166..00000000 --- a/service/gzip/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"enable": true}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &mockCfg{`{"enable": "invalid"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"enable": 1}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - assert.Equal(t, true, c.Enable) -} diff --git a/service/gzip/service.go b/service/gzip/service.go deleted file mode 100644 index 231ba4d9..00000000 --- a/service/gzip/service.go +++ /dev/null @@ -1,36 +0,0 @@ -package gzip - -import ( - "errors" - "github.com/NYTimes/gziphandler" - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" -) - -// ID contains default service name. -const ID = "gzip" -var httpNotInitialized = errors.New("http service should be defined properly in config to use gzip") - -type Service struct { - cfg *Config -} - -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - s.cfg = cfg - if !s.cfg.Enable { - return false, nil - } - if r == nil { - return false, httpNotInitialized - } - - r.AddMiddleware(s.middleware) - - return true, nil -} - -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - gziphandler.GzipHandler(f).ServeHTTP(w, r) - } -} diff --git a/service/gzip/service_test.go b/service/gzip/service_test.go deleted file mode 100644 index 778bdacd..00000000 --- a/service/gzip/service_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "testing" -) - -type testCfg struct { - gzip string - httpCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.gzip} - } - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Disabled(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{cfg: &Config{Enable: true}}) - - assert.NoError(t, c.Init(&testCfg{ - httpCfg: `{ - "address": ":6029", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - } - }`, - gzip: `{"enable":false}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -// TEST bug #275 -func Test_Bug275(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - httpCfg: "", - gzip: `{"enable":true}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} diff --git a/service/headers/config_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/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/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/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/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/limit/config.go b/service/limit/config.go deleted file mode 100644 index 203db11b..00000000 --- a/service/limit/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "time" -) - -// Config of Limit service. -type Config struct { - // Interval defines the update duration for underlying controllers, default 1s. - Interval time.Duration - - // Services declares list of services to be watched. - Services map[string]*controllerConfig -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - // Always use second based definition for time durations - if c.Interval < time.Microsecond { - c.Interval = time.Second * time.Duration(c.Interval.Nanoseconds()) - } - - return nil -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Interval = time.Second - - return nil -} - -// Controllers returns list of defined Services -func (c *Config) Controllers(l listener) (controllers map[string]roadrunner.Controller) { - controllers = make(map[string]roadrunner.Controller) - - for name, cfg := range c.Services { - controllers[name] = &controller{lsn: l, tick: c.Interval, cfg: cfg} - } - - return controllers -} diff --git a/service/limit/config_test.go b/service/limit/config_test.go deleted file mode 100644 index c79836b8..00000000 --- a/service/limit/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package limit - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"enable: true}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Controller_Default(t *testing.T) { - cfg := &mockCfg{` -{ - "services":{ - "http": { - "TTL": 1 - } - } -} -`} - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("failed to InitDefaults: error %v", err) - } - - assert.NoError(t, c.Hydrate(cfg)) - assert.Equal(t, time.Second, c.Interval) - - list := c.Controllers(func(event int, ctx interface{}) { - }) - - sc := list["http"] - - assert.Equal(t, time.Second, sc.(*controller).tick) -} diff --git a/service/limit/controller.go b/service/limit/controller.go deleted file mode 100644 index 24a158f7..00000000 --- a/service/limit/controller.go +++ /dev/null @@ -1,166 +0,0 @@ -package limit - -import ( - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/util" - "time" -) - -const ( - // EventMaxMemory caused when worker consumes more memory than allowed. - EventMaxMemory = iota + 8000 - - // EventTTL thrown when worker is removed due TTL being reached. Context is rr.WorkerError - EventTTL - - // EventIdleTTL triggered when worker spends too much time at rest. - EventIdleTTL - - // EventExecTTL triggered when worker spends too much time doing the task (max_execution_time). - EventExecTTL -) - -// handles controller events -type listener func(event int, ctx interface{}) - -// defines the controller behaviour -type controllerConfig struct { - // MaxMemory defines maximum amount of memory allowed for worker. In megabytes. - MaxMemory uint64 - - // TTL defines maximum time worker is allowed to live. - TTL int64 - - // IdleTTL defines maximum duration worker can spend in idle mode. - IdleTTL int64 - - // ExecTTL defines maximum lifetime per job. - ExecTTL int64 -} - -type controller struct { - lsn listener - tick time.Duration - cfg *controllerConfig - - // list of workers which are currently working - sw *stateFilter - - stop chan interface{} -} - -// control the pool state -func (c *controller) control(p roadrunner.Pool) { - c.loadWorkers(p) - - now := time.Now() - - if c.cfg.ExecTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateWorking, - now.Add(-time.Second*time.Duration(c.cfg.ExecTTL)), - ) { - eID := w.State().NumExecs() - err := fmt.Errorf("max exec time reached (%vs)", c.cfg.ExecTTL) - - // make sure worker still on initial request - if p.Remove(w, err) && w.State().NumExecs() == eID { - go func() { - err := w.Kill() - if err != nil { - fmt.Printf("error killing worker with PID number: %d, created: %s", w.Pid, w.Created) - } - }() - c.report(EventExecTTL, w, err) - } - } - } - - // locale workers which are in idle mode for too long - if c.cfg.IdleTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateReady, - now.Add(-time.Second*time.Duration(c.cfg.IdleTTL)), - ) { - err := fmt.Errorf("max idle time reached (%vs)", c.cfg.IdleTTL) - if p.Remove(w, err) { - c.report(EventIdleTTL, w, err) - } - } - } -} - -func (c *controller) loadWorkers(p roadrunner.Pool) { - now := time.Now() - - for _, w := range p.Workers() { - if w.State().Value() == roadrunner.StateInvalid { - // skip duplicate assessment - continue - } - - s, err := util.WorkerState(w) - if err != nil { - continue - } - - if c.cfg.TTL != 0 && now.Sub(w.Created).Seconds() >= float64(c.cfg.TTL) { - err := fmt.Errorf("max TTL reached (%vs)", c.cfg.TTL) - if p.Remove(w, err) { - c.report(EventTTL, w, err) - } - continue - } - - if c.cfg.MaxMemory != 0 && s.MemoryUsage >= c.cfg.MaxMemory*1024*1024 { - err := fmt.Errorf("max allowed memory reached (%vMB)", c.cfg.MaxMemory) - if p.Remove(w, err) { - c.report(EventMaxMemory, w, err) - } - continue - } - - // control the worker state changes - c.sw.push(w) - } - - c.sw.sync(now) -} - -// throw controller event -func (c *controller) report(event int, worker *roadrunner.Worker, caused error) { - if c.lsn != nil { - c.lsn(event, roadrunner.WorkerError{Worker: worker, Caused: caused}) - } -} - -// Attach controller to the pool -func (c *controller) Attach(pool roadrunner.Pool) roadrunner.Controller { - wp := &controller{ - tick: c.tick, - lsn: c.lsn, - cfg: c.cfg, - sw: newStateFilter(), - stop: make(chan interface{}), - } - - go func(wp *controller, pool roadrunner.Pool) { - ticker := time.NewTicker(wp.tick) - for { - select { - case <-ticker.C: - wp.control(pool) - case <-wp.stop: - return - } - } - }(wp, pool) - - return wp -} - -// Detach controller from the pool. -func (c *controller) Detach() { - close(c.stop) -} diff --git a/service/limit/service.go b/service/limit/service.go deleted file mode 100644 index c0b4139c..00000000 --- a/service/limit/service.go +++ /dev/null @@ -1,39 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" -) - -// ID defines controller service name. -const ID = "limit" - -// Service to control the state of rr service inside other services. -type Service struct { - lsns []func(event int, ctx interface{}) -} - -// Init controller service -func (s *Service) Init(cfg *Config, c service.Container) (bool, error) { - // mount Services to designated services - for id, watcher := range cfg.Controllers(s.throw) { - svc, _ := c.Get(id) - if ctrl, ok := svc.(roadrunner.Attacher); ok { - ctrl.Attach(watcher) - } - } - - return true, nil -} - -// AddListener attaches server event controller. -func (s *Service) AddListener(l func(event int, ctx interface{})) { - s.lsns = append(s.lsns, l) -} - -// throw handles service, server and pool events. -func (s *Service) throw(event int, ctx interface{}) { - for _, l := range s.lsns { - l(event, ctx) - } -} diff --git a/service/limit/service_test.go b/service/limit/service_test.go deleted file mode 100644 index fae73166..00000000 --- a/service/limit/service_test.go +++ /dev/null @@ -1,498 +0,0 @@ -package limit - -import ( - "fmt" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" -) - -type testCfg struct { - httpCfg string - limitCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - if cfg.httpCfg == "" { - return nil - } - - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.limitCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - - if cl, ok := out.(*Config); ok { - // to speed up tests - cl.Interval = time.Millisecond - } - - return err -} - -func Test_Service_PidEcho(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":27029", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 800) - req, err := http.NewRequest("GET", "http://localhost:27029", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - assert.Equal(t, getPID(s), string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - c.Stop() - return nil - - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7030", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7030", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7030?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusIdleTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7031", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "idleTtl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventIdleTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7031", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7031?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - return nil - }, bkoff) - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxExecTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7032", - "workers":{ - "command": "php ../../tests/http/client.php stuck pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "execTTL": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventExecTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:7032", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - assert.Equal(t, 500, r.StatusCode) - - <-captured - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxMemoryUsage(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":10033", - "workers":{ - "command": "php ../../tests/http/client.php memleak pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "maxMemory": 10 - } - } - }`, - }) - if err != nil { - return err - } - - time.Sleep(time.Second * 3) - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - once := false - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventMaxMemory && !once { - close(captured) - once = true - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:10033", nil) - if err != nil { - return err - } - - for { - select { - case <-captured: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - assert.NotEqual(t, lastPID, getPID(s)) - c.Stop() - return nil - default: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - c.Stop() - return nil - } - } - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} -func getPID(s interface{}) string { - if len(s.(*rrhttp.Service).Server().Workers()) > 0 { - w := s.(*rrhttp.Service).Server().Workers()[0] - return fmt.Sprintf("%v", *w.Pid) - } else { - panic("no workers") - } -} diff --git a/service/limit/state_filter.go b/service/limit/state_filter.go deleted file mode 100644 index cd2eca94..00000000 --- a/service/limit/state_filter.go +++ /dev/null @@ -1,58 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "time" -) - -type stateFilter struct { - prev map[*roadrunner.Worker]state - next map[*roadrunner.Worker]state -} - -type state struct { - state int64 - numExecs int64 - since time.Time -} - -func newStateFilter() *stateFilter { - return &stateFilter{ - prev: make(map[*roadrunner.Worker]state), - next: make(map[*roadrunner.Worker]state), - } -} - -// add new worker to be watched -func (sw *stateFilter) push(w *roadrunner.Worker) { - sw.next[w] = state{state: w.State().Value(), numExecs: w.State().NumExecs()} -} - -// update worker states. -func (sw *stateFilter) sync(t time.Time) { - for w := range sw.prev { - if _, ok := sw.next[w]; !ok { - delete(sw.prev, w) - } - } - - for w, s := range sw.next { - ps, ok := sw.prev[w] - if !ok || ps.state != s.state || ps.numExecs != s.numExecs { - sw.prev[w] = state{state: s.state, numExecs: s.numExecs, since: t} - } - - delete(sw.next, w) - } -} - -// find all workers which spend given amount of time in a specific state. -func (sw *stateFilter) find(state int64, since time.Time) (workers []*roadrunner.Worker) { - for w, s := range sw.prev { - if s.state == state && s.since.Before(since) { - workers = append(workers, w) - } - } - - return -} diff --git a/service/metrics/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 721495c3..00000000 --- a/service/reload/watcher.go +++ /dev/null @@ -1,400 +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++ { - // if file in ignored --> continue - if _, ignored := w.watcherConfigs[serviceName].ignored[path]; ignored { - continue - } - - // if filename does not contain pattern --> ignore that file - if w.watcherConfigs[serviceName].filePatterns != nil && w.watcherConfigs[serviceName].filterHooks != nil { - err = w.watcherConfigs[serviceName].filterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].filePatterns) - if err == ErrorSkip { - continue outer - } - } - - filesList[fileInfoList[i].Name()] = fileInfoList[i] - } - - return filesList, nil -} - -func (w *Watcher) StartPolling(duration time.Duration) error { - w.mu.Lock() - if w.started { - w.mu.Unlock() - return errors.New("already started") - } - - w.started = true - w.mu.Unlock() - - return w.waitEvent(duration) -} - -// this is blocking operation -func (w *Watcher) waitEvent(d time.Duration) error { - ticker := time.NewTicker(d) - for { - select { - case <-w.close: - ticker.Stop() - // just exit - // no matter for the pollEvents - return nil - case <-ticker.C: - // this is not very effective way - // because we have to wait on Lock - // better is to listen files in parallel, but, since that would be used in debug... TODO - for serviceName, 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/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..472e5a05 100644..100755 --- a/socket_factory.go +++ b/socket_factory.go @@ -1,16 +1,21 @@ package roadrunner import ( - "fmt" - "github.com/pkg/errors" - "github.com/spiral/goridge/v2" + "context" "net" "os/exec" "sync" "time" + + "github.com/shirou/gopsutil/process" + "github.com/spiral/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 +23,203 @@ 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 { - return nil, err - } - - if err := w.start(); err != nil { - return nil, errors.Wrap(err, "process error") - } +// 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, err := f.findRelay(w, f.tout) - if err != nil { - go func(w *Worker) { - err := w.Kill() + rl := goridge.NewSocketRelay(conn) + pid, err := fetchPID(rl) if err != nil { - fmt.Println(fmt.Errorf("error killing the worker %v", err)) + return err } - }(w) - if wErr := w.Wait(); wErr != nil { - if _, ok := wErr.(*exec.ExitError); ok { - err = errors.Wrap(wErr, err.Error()) - } else { - err = wErr + 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) { + const op = errors.Op("spawn_worker_with_context") + c := make(chan socketSpawn) + go func() { + ctx, cancel := context.WithTimeout(ctx, f.tout) + defer cancel() + w, err := InitBaseWorker(cmd) + if err != nil { + c <- socketSpawn{ + w: nil, + err: err, } + return } - return nil, errors.Wrap(err, "unable to connect to worker") + err = w.Start() + if err != nil { + c <- socketSpawn{ + w: nil, + err: errors.E(op, err), + } + return + } + + rl, err := f.findRelayWithContext(ctx, w) + if err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + + c <- socketSpawn{ + w: nil, + err: errors.E(op, err), + } + return + } + + w.AttachRelay(rl) + w.State().Set(StateReady) + + c <- socketSpawn{ + w: w, + err: nil, + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-c: + if res.err != nil { + return nil, res.err + } + + return res.w, nil + } +} + +func (f *SocketFactory) SpawnWorker(cmd *exec.Cmd) (WorkerBase, error) { + const op = errors.Op("spawn_worker") + w, err := InitBaseWorker(cmd) + if err != nil { + return nil, err + } + + err = w.Start() + if err != nil { + return nil, errors.E(op, err) + } + + rl, err := f.findRelay(w) + if err != nil { + err = multierr.Combine( + err, + w.Kill(), + w.Wait(), + ) + return nil, err } - w.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) { + ticker := time.NewTicker(time.Millisecond * 100) 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() + case <-ticker.C: + _, err := process.NewProcess(int32(w.Pid())) + if err != nil { + return nil, err + } + default: + tmp, ok := f.relays.Load(w.Pid()) + if !ok { + continue + } + return tmp.(*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) { + const op = errors.Op("find_relay") + // 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.E(op, errors.Str("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..ab6927bd 100644..100755 --- 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,7 +29,7 @@ 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) @@ -33,15 +37,15 @@ func Test_Tcp_Start(t *testing.T) { 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_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,13 +93,14 @@ 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 + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { @@ -115,7 +116,7 @@ func Test_Tcp_Failboot(t *testing.T) { cmd := exec.Command("php", "tests/failboot.php") - w, err2 := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + w, err2 := NewSocketServer(ls, time.Second*5).SpawnWorkerWithContext(ctx, cmd) assert.Nil(t, w) assert.Error(t, err2) assert.Contains(t, err2.Error(), "failboot") @@ -123,7 +124,7 @@ func Test_Tcp_Failboot(t *testing.T) { func Test_Tcp_Timeout(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -138,15 +139,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 +162,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*1).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 +184,41 @@ func Test_Tcp_Broken(t *testing.T) { cmd := exec.Command("php", "tests/client.php", "broken", "tcp") - 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() { + defer wg.Done() 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(Payload{Body: []byte("hello")}) assert.Error(t, err) - assert.Nil(t, res) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + wg.Wait() } func Test_Tcp_Echo(t *testing.T) { time.Sleep(time.Millisecond * 10) // to ensure free socket - + ctx := context.Background() ls, err := net.Listen("tcp", "localhost:9007") if assert.NoError(t, err) { defer func() { @@ -220,18 +233,23 @@ func Test_Tcp_Echo(t *testing.T) { cmd := exec.Command("php", "tests/client.php", "echo", "tcp") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + 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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -242,6 +260,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,7 +275,7 @@ 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) @@ -264,14 +283,15 @@ func Test_Unix_Start(t *testing.T) { 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 +305,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*5).SpawnWorkerWithContext(ctx, cmd) assert.Nil(t, w) assert.Error(t, err) assert.Contains(t, err.Error(), "failboot") @@ -293,6 +313,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 +327,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 +349,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,8 +370,14 @@ 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() { + defer wg.Done() err := w.Wait() assert.Error(t, err) assert.Contains(t, err.Error(), "undefined_function()") @@ -356,17 +385,25 @@ func Test_Unix_Broken(t *testing.T) { 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(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 +418,26 @@ func Test_Unix_Echo(t *testing.T) { cmd := exec.Command("php", "tests/client.php", "echo", "unix") - w, _ := NewSocketFactory(ls, time.Minute).SpawnWorker(cmd) + 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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -403,10 +448,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 +461,31 @@ func Benchmark_Tcp_SpawnWorker_Stop(b *testing.B) { b.Skip("socket is busy") } - f := NewSocketFactory(ls, time.Minute) + f := NewSocketServer(ls, time.Minute) for n := 0; n < b.N; n++ { cmd := exec.Command("php", "tests/client.php", "echo", "tcp") - w, _ := f.SpawnWorker(cmd) + w, err := f.SpawnWorkerWithContext(ctx, cmd) + if err != nil { + b.Fatal(err) + } go func() { - if w.Wait() != nil { - b.Fail() - } + assert.NoError(b, w.Wait()) }() - 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 +496,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(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 +533,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 +564,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(Payload{Body: []byte("hello")}); err != nil { b.Fail() } } @@ -5,18 +5,24 @@ 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 - - // NumJobs shows how many times worker was invoked + // Set sets the state + Set(value int64) + // 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 using to registering php executions + RegisterExec() + // SetLastUsed sets worker last used time + SetLastUsed(lu uint64) + // LastUsed return worker last used time + LastUsed() uint64 } const ( @@ -29,24 +35,34 @@ 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 + + // 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 +87,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 +97,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 + val := s.Value() + return val == StateWorking || val == 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..100755 --- 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 c4b6f42d..b626a499 100644..100755 --- a/static_pool.go +++ b/static_pool.go @@ -1,370 +1,348 @@ package roadrunner import ( - "fmt" + "context" "os/exec" - "sync" - "sync/atomic" - "time" - "github.com/pkg/errors" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/util" ) -const ( - // StopRequest can be sent by worker to indicate that restart is required. - StopRequest = "{\"stop\":true}" -) +// StopRequest can be sent by worker to indicate that restart is required. +const StopRequest = "{\"stop\":true}" -// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of workers. -type StaticPool struct { - // pool behaviour - cfg Config +var bCtx = context.Background() - // worker command creator - cmd func() *exec.Cmd +// Allocator is responsible for worker allocation in the pool +type Allocator func() (WorkerBase, error) - // creates and connects to workers - factory Factory +// ErrorEncoder encode error or make a decision based on the error type +type ErrorEncoder func(err error, w WorkerBase) (Payload, error) - // active task executions - tmu sync.Mutex - tasks sync.WaitGroup +// PoolBefore is set of functions that executes BEFORE Exec +type Before func(req Payload) Payload - // workers circular allocation buf - free chan *Worker +// PoolAfter is set of functions that executes AFTER Exec +type After func(req Payload, resp Payload) Payload - // number of workers expected to be dead in a buf. - numDead int64 +type PoolOptions func(p *StaticPool) - // protects state of worker list, does not affect allocation - muw sync.RWMutex +// StaticPool controls worker creation, destruction and task routing. Pool uses fixed amount of stack. +type StaticPool struct { + cfg PoolConfig - // all registered workers - workers []*Worker + // worker command creator + cmd func() *exec.Cmd + + // creates and connects to stack + factory Factory - // invalid declares set of workers to be removed from the pool. - remove sync.Map + // distributes the events + events util.EventsHandler - // pool is being destroyed - inDestroy int32 - destroy chan interface{} + // manages worker states and TTLs + ww WorkerWatcher - // lsn is optional callback to handle worker create/destruct/error events. - mul sync.Mutex - lsn func(event int, ctx interface{}) + // allocate new worker + allocator Allocator + + errEncoder ErrorEncoder + before []Before + after []After } // NewPool creates new worker pool and task multiplexer. StaticPool will initiate with one worker. -func NewPool(cmd func() *exec.Cmd, factory Factory, cfg Config) (*StaticPool, error) { - if err := cfg.Valid(); err != nil { - return nil, errors.Wrap(err, "config") +func NewPool(ctx context.Context, cmd func() *exec.Cmd, factory Factory, cfg PoolConfig, options ...PoolOptions) (Pool, error) { + const op = errors.Op("NewPool") + if factory == nil { + return nil, errors.E(op, errors.Str("no factory initialized")) + } + cfg.InitDefaults() + + if cfg.Debug { + cfg.NumWorkers = 0 + cfg.MaxJobs = 1 } p := &StaticPool{ cfg: cfg, cmd: cmd, factory: factory, - workers: make([]*Worker, 0, cfg.NumWorkers), - free: make(chan *Worker, cfg.NumWorkers), - destroy: make(chan interface{}), + events: util.NewEventsHandler(), + after: make([]After, 0, 0), + before: make([]Before, 0, 0), } - // constant number of workers simplify logic - for i := int64(0); i < p.cfg.NumWorkers; i++ { - // to test if worker ready - w, err := p.createWorker() - if err != nil { - p.Destroy() - return nil, err - } + p.allocator = newPoolAllocator(factory, cmd) + p.ww = newWorkerWatcher(p.allocator, p.cfg.NumWorkers, p.events) - p.free <- w + workers, err := p.allocateWorkers(ctx, p.cfg.NumWorkers) + if err != nil { + return nil, errors.E(op, err) } - return p, nil -} + // put stack in the pool + err = p.ww.AddToWatch(workers) + if err != nil { + return nil, errors.E(op, err) + } -// Listen attaches pool event controller. -func (p *StaticPool) Listen(l func(event int, ctx interface{})) { - p.mul.Lock() - defer p.mul.Unlock() + p.errEncoder = defaultErrEncoder(p) - p.lsn = l + // add pool options + for i := 0; i < len(options); i++ { + options[i](p) + } - p.muw.Lock() - for _, w := range p.workers { - w.err.Listen(p.lsn) + // if supervised config not nil, guess, that pool wanted to be supervised + if cfg.Supervisor != nil { + sp := newPoolWatcher(p, p.events, p.cfg.Supervisor) + // start watcher timer + sp.Start() + return sp, nil } - p.muw.Unlock() -} -// Config returns associated pool configuration. Immutable. -func (p *StaticPool) Config() Config { - return p.cfg + return p, nil } -// Workers returns worker list associated with the pool. -func (p *StaticPool) Workers() (workers []*Worker) { - p.muw.RLock() - defer p.muw.RUnlock() - - workers = append(workers, p.workers...) - - return workers +func PoolBefore(before ...Before) PoolOptions { + return func(p *StaticPool) { + p.before = append(p.before, before...) + } } -// 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 +func PoolAfter(after ...After) PoolOptions { + return func(p *StaticPool) { + p.after = append(p.after, after...) } +} - if _, ok := p.remove.Load(w); ok { - return false - } +// AddListener connects event listener to the pool. +func (sp *StaticPool) AddListener(listener util.EventListener) { + sp.events.AddListener(listener) +} - p.remove.Store(w, err) - return true +// PoolConfig returns associated pool configuration. Immutable. +func (sp *StaticPool) GetConfig() PoolConfig { + return sp.cfg } -// 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() +// Workers returns worker list associated with the pool. +func (sp *StaticPool) Workers() (workers []WorkerBase) { + return sp.ww.WorkersList() +} - defer p.tasks.Done() +func (sp *StaticPool) RemoveWorker(wb WorkerBase) error { + return sp.ww.RemoveWorker(wb) +} - w, err := p.allocateWorker() +func (sp *StaticPool) Exec(p Payload) (Payload, error) { + const op = errors.Op("exec") + if sp.cfg.Debug { + return sp.execDebug(p) + } + ctxGetFree, cancel := context.WithTimeout(context.Background(), sp.cfg.AllocateTimeout) + defer cancel() + w, err := sp.getWorker(ctxGetFree, op) if err != nil { - return nil, errors.Wrap(err, "unable to allocate worker") + return EmptyPayload, errors.E(op, err) } - rsp, err = w.Exec(rqs) + sw := w.(SyncWorker) - if err != nil { - // soft job errors are allowed - if _, jobError := err.(JobError); jobError { - p.release(w) - return nil, err + if len(sp.before) > 0 { + for i := 0; i < len(sp.before); i++ { + p = sp.before[i](p) } + } - p.discardWorker(w, err) - return nil, err + rsp, err := sw.Exec(p) + if err != nil { + return sp.errEncoder(err, sw) } // 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) + sw.State().Set(StateInvalid) + err = sw.Stop(bCtx) + if err != nil { + sp.events.Push(WorkerEvent{Event: EventWorkerError, Worker: sw, Payload: errors.E(op, err)}) + } + + return sp.Exec(p) } - p.release(w) - return rsp, nil -} + if sp.cfg.MaxJobs != 0 && sw.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + return EmptyPayload, errors.E(op, err) + } + } else { + sp.ww.PushWorker(sw) + } -// 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) + if len(sp.after) > 0 { + for i := 0; i < len(sp.after); i++ { + rsp = sp.after[i](p, rsp) + } } - wg.Wait() + return rsp, nil } -// finds free worker in a given time interval. Skips dead workers. -func (p *StaticPool) allocateWorker() (w *Worker, err error) { - // TODO loop counts upward, but its variable is bounded downward. - for i := atomic.LoadInt64(&p.numDead); i >= 0; i++ { - // this loop is required to skip issues with dead workers still being in a ring - // (we know how many workers). - select { - case w = <-p.free: - if w.State().Value() != StateReady { - // found expected dead worker - atomic.AddInt64(&p.numDead, ^int64(0)) - continue - } - - if err, remove := p.remove.Load(w); remove { - p.discardWorker(w, err) +func (sp *StaticPool) ExecWithContext(ctx context.Context, rqs Payload) (Payload, error) { + const op = errors.Op("exec with context") + ctxGetFree, cancel := context.WithTimeout(context.Background(), sp.cfg.AllocateTimeout) + defer cancel() + w, err := sp.getWorker(ctxGetFree, op) + if err != nil { + return EmptyPayload, errors.E(op, err) + } - // get next worker - i++ - continue - } + sw := w.(SyncWorker) - return w, nil - case <-p.destroy: - return nil, fmt.Errorf("pool has been stopped") - default: - // enable timeout handler + // apply all before function + if len(sp.before) > 0 { + for i := 0; i < len(sp.before); i++ { + rqs = sp.before[i](rqs) } + } - 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() + rsp, err := sw.ExecWithContext(ctx, rqs) + if err != nil { + return sp.errEncoder(err, sw) + } - return nil, fmt.Errorf("pool has been stopped") + // worker want's to be terminated + if rsp.Body == nil && rsp.Context != nil && string(rsp.Context) == StopRequest { + sw.State().Set(StateInvalid) + err = sw.Stop(bCtx) + if err != nil { + sp.events.Push(WorkerEvent{Event: EventWorkerError, Worker: sw, Payload: errors.E(op, err)}) } - } - return nil, fmt.Errorf("all workers are dead (%v)", p.cfg.NumWorkers) -} + return sp.Exec(rqs) + } -// 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 sp.cfg.MaxJobs != 0 && sw.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + return EmptyPayload, errors.E(op, err) + } + } else { + sp.ww.PushWorker(sw) } - if err, remove := p.remove.Load(w); remove { - p.discardWorker(w, err) - return + // apply all after functions + if len(sp.after) > 0 { + for i := 0; i < len(sp.after); i++ { + rsp = sp.after[i](rqs, rsp) + } } - 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()) +func (sp *StaticPool) getWorker(ctxGetFree context.Context, op errors.Op) (WorkerBase, error) { + // GetFreeWorker function consumes context with timeout + w, err := sp.ww.GetFreeWorker(ctxGetFree) if err != nil { - return nil, err - } - - p.mul.Lock() - if p.lsn != nil { - w.err.Listen(p.lsn) + // if the error is of kind NoFreeWorkers, it means, that we can't get worker from the stack during the allocate timeout + if errors.Is(errors.NoFreeWorkers, err) { + sp.events.Push(PoolEvent{Event: EventNoFreeWorkers, Payload: errors.E(op, err)}) + return nil, errors.E(op, err) + } + // else if err not nil - return error + return nil, errors.E(op, err) } - p.mul.Unlock() - - p.throw(EventWorkerConstruct, w) - - p.muw.Lock() - p.workers = append(p.workers, w) - p.muw.Unlock() - - go p.watchWorker(w) return w, nil } -// gentry remove worker -func (p *StaticPool) discardWorker(w *Worker, caused interface{}) { - w.markInvalid() - go p.destroyWorker(w, caused) +// Destroy all underlying stack (but let them to complete the task). +func (sp *StaticPool) Destroy(ctx context.Context) { + sp.ww.Destroy(ctx) } -// destroyWorker destroys workers and removes it from the pool. -// TODO caused unused -func (p *StaticPool) destroyWorker(w *Worker, caused interface{}) { - go func() { - err := w.Stop() - if err != nil { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) +func defaultErrEncoder(sp *StaticPool) ErrorEncoder { + return func(err error, w WorkerBase) (Payload, error) { + const op = errors.Op("error encoder") + // soft job errors are allowed + if errors.Is(errors.ErrSoftJob, err) { + if sp.cfg.MaxJobs != 0 && w.State().NumExecs() >= sp.cfg.MaxJobs { + err = sp.ww.AllocateNew() + if err != nil { + sp.events.Push(PoolEvent{Event: EventWorkerConstruct, Payload: errors.E(op, err)}) + } + + w.State().Set(StateInvalid) + err = w.Stop(bCtx) + if err != nil { + sp.events.Push(WorkerEvent{Event: EventWorkerError, Worker: w, Payload: errors.E(op, err)}) + } + } else { + sp.ww.PushWorker(w) + } + + return EmptyPayload, errors.E(op, err) } - }() - select { - case <-w.waitDone: - // worker is dead - p.throw(EventWorkerDestruct, w) + w.State().Set(StateInvalid) + sp.events.Push(PoolEvent{Event: EventWorkerDestruct, Payload: w}) + errS := w.Stop(bCtx) - 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}) + if errS != nil { + return EmptyPayload, errors.E(op, errors.Errorf("%v, %v", err, errS)) } - p.throw(EventWorkerKill, w) + return EmptyPayload, errors.E(op, err) } } -// 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 +func newPoolAllocator(factory Factory, cmd func() *exec.Cmd) Allocator { + return func() (WorkerBase, error) { + w, err := factory.SpawnWorkerWithContext(bCtx, cmd()) + if err != nil { + return nil, err + } + + sw, err := NewSyncWorker(w) + if err != nil { + return nil, err } + return sw, nil } - 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 +func (sp *StaticPool) execDebug(p Payload) (Payload, error) { + sw, err := sp.allocator() if err != nil { - p.throw(EventWorkerError, WorkerError{Worker: w, Caused: err}) + return EmptyPayload, err } - if !p.destroyed() { - nw, err := p.createWorker() - if err == nil { - p.free <- nw - return - } + r, err := sw.(SyncWorker).Exec(p) - // 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}) - } + if stopErr := sw.Stop(context.Background()); stopErr != nil { + sp.events.Push(WorkerEvent{Event: EventWorkerError, Worker: sw, Payload: err}) } -} -func (p *StaticPool) destroyed() bool { - return atomic.LoadInt32(&p.inDestroy) != 0 + return r, err } -// 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) +// allocate required number of stack +func (sp *StaticPool) allocateWorkers(ctx context.Context, numWorkers int64) ([]WorkerBase, error) { + const op = errors.Op("allocate workers") + var workers []WorkerBase + + // constant number of stack simplify logic + for i := int64(0); i < numWorkers; i++ { + ctx, cancel := context.WithTimeout(ctx, sp.cfg.AllocateTimeout) + w, err := sp.factory.SpawnWorkerWithContext(ctx, sp.cmd()) + if err != nil { + cancel() + return nil, errors.E(op, errors.WorkerAllocate, err) + } + workers = append(workers, w) + cancel() } - p.mul.Unlock() + return workers, nil } diff --git a/static_pool_test.go b/static_pool_test.go index 59822186..747f26c4 100644..100755 --- a/static_pool_test.go +++ b/static_pool_test.go @@ -1,7 +1,7 @@ package roadrunner import ( - "github.com/stretchr/testify/assert" + "context" "log" "os/exec" "runtime" @@ -10,31 +10,35 @@ import ( "sync" "testing" "time" + + "github.com/spiral/errors" + "github.com/stretchr/testify/assert" ) -var cfg = Config{ +var cfg = PoolConfig{ NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, + AllocateTimeout: time.Second * 5, + DestroyTimeout: 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, ) 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, @@ -44,33 +48,36 @@ func Test_StaticPool_Invalid(t *testing.T) { assert.Error(t, err) } -func Test_ConfigError(t *testing.T) { +func Test_ConfigNoErrorInitDefaults(t *testing.T) { p, err := NewPool( + context.Background(), func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, NewPipeFactory(), - Config{ + PoolConfig{ AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, ) - assert.Nil(t, p) - assert.Error(t, err) + assert.NotNil(t, p) + assert.NoError(t, err) } 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, ) 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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -81,18 +88,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, ) 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(Payload{Body: []byte("hello"), Context: nil}) assert.NoError(t, err) assert.NotNil(t, res) @@ -103,18 +112,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, ) 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(Payload{Body: []byte("hello"), Context: []byte("world")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -125,27 +136,35 @@ 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, ) 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(Payload{Body: []byte("hello")}) assert.Error(t, err) - assert.Nil(t, res) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + if errors.Is(errors.ErrSoftJob, err) == false { + t.Fatal("error should be of type errors.Exec") + } - assert.IsType(t, JobError{}, err) - assert.Equal(t, "hello", err.Error()) + assert.Contains(t, err.Error(), "hello") } 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, @@ -153,38 +172,44 @@ func Test_StaticPool_Broken_Replace(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, p) - done := make(chan interface{}) + block := make(chan struct{}) - p.Listen(func(e int, ctx interface{}) { - if err, ok := ctx.(error); ok { - if strings.Contains(err.Error(), "undefined_function()") { - close(done) + p.AddListener(func(event interface{}) { + if wev, ok := event.(WorkerEvent); ok { + if wev.Event == EventWorkerLog { + e := string(wev.Payload.([]byte)) + if strings.ContainsAny(e, "undefined_function()") { + block <- struct{}{} + return + } } } }) - res, err := p.Exec(&Payload{Body: []byte("hello")}) - + res, err := p.ExecWithContext(ctx, Payload{Body: []byte("hello")}) assert.Error(t, err) - assert.Nil(t, res) + assert.Nil(t, res.Context) + assert.Nil(t, res.Body) - <-done - p.Destroy() -} + <-block + 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, ) 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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -194,88 +219,115 @@ func Test_StaticPool_Broken_FromOutside(t *testing.T) { assert.Equal(t, "hello", res.String()) assert.Equal(t, runtime.NumCPU(), len(p.Workers())) - destructed := make(chan interface{}) - p.Listen(func(e int, ctx interface{}) { - if e == EventWorkerConstruct { - destructed <- nil + // Consume pool events + wg := sync.WaitGroup{} + wg.Add(1) + p.AddListener(func(event interface{}) { + if pe, ok := event.(PoolEvent); ok { + if pe.Event == EventWorkerConstruct { + wg.Done() + } } }) // killing random worker and expecting pool to replace it - err = p.Workers()[0].cmd.Process.Kill() + err = p.Workers()[0].Kill() if err != nil { t.Errorf("error killing the process: error %v", err) } - <-destructed - for _, w := range p.Workers() { - assert.Equal(t, StateReady, w.state.Value()) + wg.Wait() + + list := p.Workers() + for _, w := range list { + assert.Equal(t, StateReady, w.State().Value()) } + wg.Wait() } 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{ + PoolConfig{ NumWorkers: 1, AllocateTimeout: time.Nanosecond * 1, DestroyTimeout: time.Second * 2, }, ) - if err != nil { - t.Fatal(err) + assert.Error(t, err) + if !errors.Is(errors.WorkerAllocate, err) { + t.Fatal("error should be of type WorkerAllocate") } + assert.Nil(t, p) +} - 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") - } - }() +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(), + PoolConfig{ + NumWorkers: 1, + MaxJobs: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + ) + assert.NoError(t, err) + defer p.Destroy(ctx) + assert.NotNil(t, p) - // to ensure that worker is already busy - time.Sleep(time.Millisecond * 10) + var lastPID string + lastPID = strconv.Itoa(int(p.Workers()[0].Pid())) - _, 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") + res, _ := p.Exec(Payload{Body: []byte("hello")}) + assert.Equal(t, lastPID, string(res.Body)) + + for i := 0; i < 10; i++ { + res, err := p.Exec(Payload{Body: []byte("hello")}) - <-done - p.Destroy() + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Nil(t, res.Context) + + assert.NotEqual(t, lastPID, string(res.Body)) + lastPID = string(res.Body) + } } -func Test_StaticPool_Replace_Worker(t *testing.T) { +func Test_StaticPool_Debug_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{ - NumWorkers: 1, - MaxJobs: 1, + PoolConfig{ + Debug: true, AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, ) assert.NoError(t, err) - defer p.Destroy() + defer p.Destroy(ctx) assert.NotNil(t, p) + assert.Len(t, p.Workers(), 0) + var lastPID string - lastPID = strconv.Itoa(*p.Workers()[0].Pid) + res, _ := p.Exec(Payload{Body: []byte("hello")}) + assert.NotEqual(t, lastPID, string(res.Body)) - res, _ := p.Exec(&Payload{Body: []byte("hello")}) - assert.Equal(t, lastPID, string(res.Body)) + assert.Len(t, p.Workers(), 0) for i := 0; i < 10; i++ { - res, err := p.Exec(&Payload{Body: []byte("hello")}) + assert.Len(t, p.Workers(), 0) + res, err := p.Exec(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -289,28 +341,33 @@ 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{ + PoolConfig{ NumWorkers: 1, AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, ) 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()[0].Pid())) - res, _ := p.Exec(&Payload{Body: []byte("hello")}) + res, err := p.Exec(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(Payload{Body: []byte("hello")}) assert.NoError(t, err) assert.NotNil(t, res) @@ -324,10 +381,12 @@ 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{ + PoolConfig{ NumWorkers: 1, AllocateTimeout: time.Second, DestroyTimeout: time.Second, @@ -337,17 +396,19 @@ func Test_Static_Pool_Destroy_And_Close(t *testing.T) { assert.NotNil(t, p) assert.NoError(t, err) - p.Destroy() - _, err = p.Exec(&Payload{Body: []byte("100")}) + p.Destroy(ctx) + _, err = p.Exec(Payload{Body: []byte("100")}) assert.Error(t, err) } // identical to replace but controlled on worker side func Test_Static_Pool_Destroy_And_Close_While_Wait(t *testing.T) { + ctx := context.Background() p, err := NewPool( + ctx, func() *exec.Cmd { return exec.Command("php", "tests/client.php", "delay", "pipes") }, NewPipeFactory(), - Config{ + PoolConfig{ NumWorkers: 1, AllocateTimeout: time.Second, DestroyTimeout: time.Second, @@ -358,49 +419,51 @@ 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(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(Payload{Body: []byte("100")}) assert.Error(t, err) } // identical to replace but controlled on worker side func Test_Static_Pool_Handle_Dead(t *testing.T) { + ctx := context.Background() p, err := NewPool( - func() *exec.Cmd { return exec.Command("php", "tests/client.php", "echo", "pipes") }, + context.Background(), + func() *exec.Cmd { return exec.Command("php", "tests/slow-destroy.php", "echo", "pipes") }, NewPipeFactory(), - Config{ + PoolConfig{ NumWorkers: 5, AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, ) assert.NoError(t, err) - defer p.Destroy() + defer p.Destroy(ctx) assert.NotNil(t, p) - for _, w := range p.workers { - w.state.value = StateErrored + for _, w := range p.Workers() { + w.State().Set(StateErrored) } - _, err = p.Exec(&Payload{Body: []byte("hello")}) + _, 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{ + PoolConfig{ NumWorkers: 5, AllocateTimeout: time.Second, DestroyTimeout: time.Second, @@ -410,61 +473,51 @@ func Test_Static_Pool_Slow_Destroy(t *testing.T) { 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, ) - 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(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{ + PoolConfig{ NumWorkers: int64(runtime.NumCPU()), AllocateTimeout: time.Second * 100, DestroyTimeout: time.Second, }, ) - 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(Payload{Body: []byte("hello")}); err != nil { b.Fail() log.Println(err) } @@ -474,21 +527,26 @@ 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{ + PoolConfig{ NumWorkers: 1, MaxJobs: 1, AllocateTimeout: time.Second, DestroyTimeout: time.Second, }, ) - 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(Payload{Body: []byte("hello")}); err != nil { b.Fail() log.Println(err) } diff --git a/supervisor_pool.go b/supervisor_pool.go new file mode 100755 index 00000000..dfec5559 --- /dev/null +++ b/supervisor_pool.go @@ -0,0 +1,204 @@ +package roadrunner + +import ( + "context" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/util" +) + +const MB = 1024 * 1024 + +type SupervisedPool interface { + Pool + // Start used to start watching process for all pool workers + Start() +} + +type supervisedPool struct { + cfg *SupervisorConfig + events util.EventsHandler + pool Pool + stopCh chan struct{} + mu *sync.RWMutex +} + +func newPoolWatcher(pool Pool, events util.EventsHandler, cfg *SupervisorConfig) SupervisedPool { + sp := &supervisedPool{ + cfg: cfg, + events: events, + pool: pool, + mu: &sync.RWMutex{}, + stopCh: make(chan struct{}), + } + return sp +} + +type ttlExec struct { + err error + p Payload +} + +func (sp *supervisedPool) ExecWithContext(ctx context.Context, rqs Payload) (Payload, error) { + const op = errors.Op("exec_supervised") + if sp.cfg.ExecTTL == 0 { + return sp.pool.Exec(rqs) + } + + c := make(chan ttlExec, 1) + ctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(sp.cfg.ExecTTL)) + defer cancel() + go func() { + res, err := sp.pool.ExecWithContext(ctx, rqs) + if err != nil { + c <- ttlExec{ + err: errors.E(op, err), + p: EmptyPayload, + } + } + + c <- ttlExec{ + err: nil, + p: res, + } + }() + + for { + select { + case <-ctx.Done(): + return EmptyPayload, errors.E(op, errors.TimeOut, ctx.Err()) + case res := <-c: + if res.err != nil { + return EmptyPayload, res.err + } + + return res.p, nil + } + } +} + +func (sp *supervisedPool) Exec(p Payload) (Payload, error) { + const op = errors.Op("supervised exec") + rsp, err := sp.pool.Exec(p) + if err != nil { + return EmptyPayload, errors.E(op, err) + } + return rsp, nil +} + +func (sp *supervisedPool) AddListener(listener util.EventListener) { + sp.pool.AddListener(listener) +} + +func (sp *supervisedPool) GetConfig() PoolConfig { + return sp.pool.GetConfig() +} + +func (sp *supervisedPool) Workers() (workers []WorkerBase) { + sp.mu.Lock() + defer sp.mu.Unlock() + return sp.pool.Workers() +} + +func (sp *supervisedPool) RemoveWorker(worker WorkerBase) error { + return sp.pool.RemoveWorker(worker) +} + +func (sp *supervisedPool) Destroy(ctx context.Context) { + sp.pool.Destroy(ctx) +} + +func (sp *supervisedPool) Start() { + go func() { + watchTout := time.NewTicker(time.Second * time.Duration(sp.cfg.WatchTick)) + for { + select { + case <-sp.stopCh: + watchTout.Stop() + return + // stop here + case <-watchTout.C: + sp.mu.Lock() + sp.control() + sp.mu.Unlock() + } + } + }() +} + +func (sp *supervisedPool) Stop() { + sp.stopCh <- struct{}{} +} + +func (sp *supervisedPool) control() { + now := time.Now() + const op = errors.Op("supervised pool control tick") + + // THIS IS A COPY OF WORKERS + workers := sp.pool.Workers() + + for i := 0; i < len(workers); i++ { + if workers[i].State().Value() == StateInvalid { + continue + } + + s, err := WorkerProcessState(workers[i]) + if err != nil { + // worker not longer valid for supervision + continue + } + + if sp.cfg.TTL != 0 && now.Sub(workers[i].Created()).Seconds() >= float64(sp.cfg.TTL) { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(PoolEvent{Event: EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(PoolEvent{Event: EventTTL, Payload: workers[i]}) + continue + } + + if sp.cfg.MaxWorkerMemory != 0 && s.MemoryUsage >= sp.cfg.MaxWorkerMemory*MB { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(PoolEvent{Event: EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(PoolEvent{Event: EventMaxMemory, Payload: workers[i]}) + continue + } + + // firs we check maxWorker idle + if sp.cfg.IdleTTL != 0 { + // then check for the worker state + if workers[i].State().Value() != StateReady { + continue + } + + /* + Calculate idle time + If worker in the StateReady, we read it LastUsed timestamp as UnixNano uint64 + 2. For example maxWorkerIdle is equal to 5sec, then, if (time.Now - LastUsed) > maxWorkerIdle + we are guessing that worker overlap idle time and has to be killed + */ + + // get last used unix nano + lu := workers[i].State().LastUsed() + + // convert last used to unixNano and sub time.now + res := int64(lu) - now.UnixNano() + + // maxWorkerIdle more than diff between now and last used + if sp.cfg.IdleTTL-uint64(res) <= 0 { + err = sp.pool.RemoveWorker(workers[i]) + if err != nil { + sp.events.Push(PoolEvent{Event: EventSupervisorError, Payload: errors.E(op, err)}) + return + } + sp.events.Push(PoolEvent{Event: EventIdleTTL, Payload: workers[i]}) + } + } + } +} diff --git a/supervisor_test.go b/supervisor_test.go new file mode 100644 index 00000000..08ea356d --- /dev/null +++ b/supervisor_test.go @@ -0,0 +1,150 @@ +package roadrunner + +import ( + "context" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var cfgSupervised = PoolConfig{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 100, + MaxWorkerMemory: 100, + }, +} + +func TestSupervisedPool_Exec(t *testing.T) { + ctx := context.Background() + p, err := NewPool( + ctx, + func() *exec.Cmd { return exec.Command("php", "tests/memleak.php", "pipes") }, + NewPipeFactory(), + cfgSupervised, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + stopCh := make(chan struct{}) + defer p.Destroy(context.Background()) + + go func() { + for { + select { + case <-stopCh: + return + default: + workers := p.Workers() + if len(workers) > 0 { + s, err := WorkerProcessState(workers[0]) + assert.NoError(t, err) + assert.NotNil(t, s) + // since this is soft limit, double max memory limit watch + if (s.MemoryUsage / MB) > cfgSupervised.Supervisor.MaxWorkerMemory*2 { + assert.Fail(t, "max memory reached") + } + } + } + } + }() + + for i := 0; i < 100; i++ { + time.Sleep(time.Millisecond * 50) + _, err = p.Exec(Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + assert.NoError(t, err) + } + + stopCh <- struct{}{} +} + +func TestSupervisedPool_ExecTTL_TimedOut(t *testing.T) { + var cfgExecTTL = PoolConfig{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 1, + MaxWorkerMemory: 100, + }, + } + ctx := context.Background() + p, err := NewPool( + ctx, + func() *exec.Cmd { return exec.Command("php", "tests/sleep.php", "pipes") }, + NewPipeFactory(), + cfgExecTTL, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + defer p.Destroy(context.Background()) + + pid := p.Workers()[0].Pid() + + resp, err := p.ExecWithContext(context.Background(), Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + + assert.Error(t, err) + assert.Empty(t, resp) + + time.Sleep(time.Second * 1) + // should be new worker with new pid + assert.NotEqual(t, pid, p.Workers()[0].Pid()) +} + +func TestSupervisedPool_ExecTTL_OK(t *testing.T) { + var cfgExecTTL = PoolConfig{ + NumWorkers: int64(1), + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + Supervisor: &SupervisorConfig{ + WatchTick: 1, + TTL: 100, + IdleTTL: 100, + ExecTTL: 4, + MaxWorkerMemory: 100, + }, + } + ctx := context.Background() + p, err := NewPool( + ctx, + func() *exec.Cmd { return exec.Command("php", "tests/sleep.php", "pipes") }, + NewPipeFactory(), + cfgExecTTL, + ) + + assert.NoError(t, err) + assert.NotNil(t, p) + defer p.Destroy(context.Background()) + + pid := p.Workers()[0].Pid() + + time.Sleep(time.Millisecond * 100) + resp, err := p.Exec(Payload{ + Context: []byte(""), + Body: []byte("foo"), + }) + + assert.NoError(t, err) + assert.Empty(t, resp) + + time.Sleep(time.Second * 1) + // should be the same pid + assert.Equal(t, pid, p.Workers()[0].Pid()) +} diff --git a/sync_worker.go b/sync_worker.go new file mode 100755 index 00000000..7e4d21cc --- /dev/null +++ b/sync_worker.go @@ -0,0 +1,212 @@ +package roadrunner + +import ( + "context" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/util" + "go.uber.org/multierr" + + "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, there is no TIMEOUTS + Exec(rqs Payload) (Payload, error) + // ExecWithContext used to handle Exec with TTL + ExecWithContext(ctx context.Context, p Payload) (Payload, error) +} + +type syncWorker struct { + w WorkerBase +} + +// NewSyncWorker creates SyncWorker from WorkerBasa +func NewSyncWorker(w WorkerBase) (SyncWorker, error) { + return &syncWorker{ + w: w, + }, nil +} + +// Exec payload without TTL timeout. +func (tw *syncWorker) Exec(p Payload) (Payload, error) { + const op = errors.Op("sync worker Exec") + if len(p.Body) == 0 && len(p.Context) == 0 { + return EmptyPayload, errors.E(op, errors.Str("payload can not be empty")) + } + + if tw.w.State().Value() != StateReady { + return EmptyPayload, errors.E(op, errors.Errorf("WorkerProcess is not ready (%s)", tw.w.State().String())) + } + + // set last used time + tw.w.State().SetLastUsed(uint64(time.Now().UnixNano())) + tw.w.State().Set(StateWorking) + + rsp, err := tw.execPayload(p) + if err != nil { + // just to be more verbose + if errors.Is(errors.ErrSoftJob, err) == false { + tw.w.State().Set(StateErrored) + tw.w.State().RegisterExec() + } + return EmptyPayload, err + } + + tw.w.State().Set(StateReady) + tw.w.State().RegisterExec() + + return rsp, nil +} + +type wexec struct { + payload Payload + err error +} + +// Exec payload without TTL timeout. +func (tw *syncWorker) ExecWithContext(ctx context.Context, p Payload) (Payload, error) { + const op = errors.Op("ExecWithContext") + c := make(chan wexec, 1) + go func() { + if len(p.Body) == 0 && len(p.Context) == 0 { + c <- wexec{ + payload: EmptyPayload, + err: errors.E(op, errors.Str("payload can not be empty")), + } + return + } + + if tw.w.State().Value() != StateReady { + c <- wexec{ + payload: EmptyPayload, + err: errors.E(op, errors.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(p) + if err != nil { + // just to be more verbose + if errors.Is(errors.ErrSoftJob, err) == false { + tw.w.State().Set(StateErrored) + tw.w.State().RegisterExec() + } + c <- wexec{ + payload: EmptyPayload, + err: errors.E(op, err), + } + return + } + + tw.w.State().Set(StateReady) + tw.w.State().RegisterExec() + + c <- wexec{ + payload: rsp, + err: nil, + } + }() + + select { + case <-ctx.Done(): + err := multierr.Combine(tw.Kill()) + if err != nil { + return EmptyPayload, multierr.Append(err, ctx.Err()) + } + return EmptyPayload, ctx.Err() + case res := <-c: + if res.err != nil { + return EmptyPayload, res.err + } + return res.payload, nil + } +} + +func (tw *syncWorker) execPayload(p Payload) (Payload, error) { + const op = errors.Op("exec payload") + // two things; todo: merge + if err := sendControl(tw.w.Relay(), p.Context); err != nil { + return EmptyPayload, errors.E(op, err, "header error") + } + + if err := tw.w.Relay().Send(p.Body, 0); err != nil { + return EmptyPayload, errors.E(op, 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.E(op, err, "WorkerProcess error") + } + + if !pr.HasFlag(goridge.PayloadControl) { + return EmptyPayload, errors.E(op, errors.Str("malformed WorkerProcess response")) + } + + if pr.HasFlag(goridge.PayloadError) { + return EmptyPayload, errors.E(op, errors.ErrSoftJob, errors.Str(string(rsp.Context))) + } + + // add streaming support :) + if rsp.Body, pr, err = tw.w.Relay().Receive(); err != nil { + return EmptyPayload, errors.E(op, err, "WorkerProcess error") + } + + return rsp, nil +} + +func (tw *syncWorker) String() string { + return tw.w.String() +} + +func (tw *syncWorker) Pid() int64 { + return tw.w.Pid() +} + +func (tw *syncWorker) Created() time.Time { + return tw.w.Created() +} + +func (tw *syncWorker) AddListener(listener util.EventListener) { + tw.w.AddListener(listener) +} + +func (tw *syncWorker) State() State { + return tw.w.State() +} + +func (tw *syncWorker) Start() error { + return tw.w.Start() +} + +func (tw *syncWorker) Wait() error { + return tw.w.Wait() +} + +func (tw *syncWorker) Stop(ctx context.Context) error { + return tw.w.Stop(ctx) +} + +func (tw *syncWorker) Kill() error { + return tw.w.Kill() +} + +func (tw *syncWorker) Relay() goridge.Relay { + return tw.w.Relay() +} + +func (tw *syncWorker) AttachRelay(rl goridge.Relay) { + tw.w.AttachRelay(rl) +} diff --git a/sync_worker_test.go b/sync_worker_test.go new file mode 100755 index 00000000..30b5bae8 --- /dev/null +++ b/sync_worker_test.go @@ -0,0 +1,263 @@ +package roadrunner + +import ( + "context" + "os/exec" + "strings" + "sync" + "testing" + "time" + + "github.com/spiral/errors" + "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()) + }() + defer func() { + err := w.Stop(ctx) + if err != nil { + t.Errorf("error stopping the WorkerProcess: error %v", err) + } + }() + + res, err := syncWorker.Exec(Payload{Body: []byte("hello")}) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.NotNil(t, res.Body) + assert.Nil(t, res.Context) + + assert.Equal(t, "hello", res.String()) +} + +func Test_BadPayload(t *testing.T) { + 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()) + }() + defer func() { + err := w.Stop(ctx) + if err != nil { + t.Errorf("error stopping the WorkerProcess: error %v", err) + } + }() + + res, err := syncWorker.Exec(EmptyPayload) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + assert.Contains(t, err.Error(), "payload can not be empty") +} + +func Test_NotStarted_String(t *testing.T) { + cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + + w, _ := InitBaseWorker(cmd) + assert.Contains(t, w.String(), "php tests/client.php echo pipes") + assert.Contains(t, w.String(), "inactive") + assert.Contains(t, w.String(), "numExecs: 0") +} + +func Test_NotStarted_Exec(t *testing.T) { + cmd := exec.Command("php", "tests/client.php", "echo", "pipes") + + w, _ := InitBaseWorker(cmd) + + syncWorker, err := NewSyncWorker(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(Payload{Body: []byte("hello")}) + + assert.Error(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + assert.Contains(t, err.Error(), "WorkerProcess is not ready (inactive)") +} + +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()) + }() + 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()) + }() + 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(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) + } + + data := "" + mu := &sync.Mutex{} + w.AddListener(func(event interface{}) { + if wev, ok := event.(WorkerEvent); ok { + mu.Lock() + data = string(wev.Payload.([]byte)) + mu.Unlock() + } + }) + + syncWorker, err := NewSyncWorker(w) + if err != nil { + t.Fatal(err) + } + + res, err := syncWorker.Exec(Payload{Body: []byte("hello")}) + assert.NotNil(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + time.Sleep(time.Second * 3) + mu.Lock() + if strings.ContainsAny(data, "undefined_function()") == false { + t.Fail() + } + mu.Unlock() + assert.Error(t, w.Stop(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()) + }() + + 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(Payload{Body: []byte("hello")}) + assert.NotNil(t, err) + assert.Nil(t, res.Body) + assert.Nil(t, res.Context) + + if errors.Is(errors.ErrSoftJob, err) == false { + t.Fatal("error should be of type errors.ErrSoftJob") + } + assert.Contains(t, err.Error(), "exec payload: SoftJobError: hello") +} + +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()) + }() + 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(Payload{Body: []byte("hello")}) + if err != nil { + t.Errorf("fail to execute payload: error %v", err) + } + assert.Equal(t, int64(1), w.State().NumExecs()) + + _, err = syncWorker.Exec(Payload{Body: []byte("hello")}) + if err != nil { + t.Errorf("fail to execute payload: error %v", err) + } + assert.Equal(t, int64(2), w.State().NumExecs()) + + _, err = syncWorker.Exec(Payload{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/systemd/rr.service b/systemd/rr.service index 6e4d5914..6e4d5914 100644..100755 --- a/systemd/rr.service +++ b/systemd/rr.service diff --git a/tests/http/upload.php b/tests/http/upload.php index bb4af766..57526246 100644 --- a/tests/http/upload.php +++ b/tests/http/upload.php @@ -24,7 +24,7 @@ function handleRequest(ServerRequestInterface $req, ResponseInterface $resp): Re 'size' => $v->getSize(), 'mime' => $v->getClientMediaType(), 'error' => $v->getError(), - 'md5' => md5($v->getStream()->__toString()), + 'sha512' => hash('sha512', $v->getStream()->__toString()), ]; } }); diff --git a/tests/memleak.php b/tests/memleak.php new file mode 100644 index 00000000..b78a76c0 --- /dev/null +++ b/tests/memleak.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +use Spiral\Goridge\StreamRelay; +use Spiral\RoadRunner\Worker as RoadRunner; + +require dirname(__DIR__) . "/vendor_php/autoload.php"; + +$rr = new RoadRunner(new StreamRelay(\STDIN, \STDOUT)); +$mem = ''; +while($rr->receive($ctx)){ + $mem .= str_repeat(" ", 1024*1024); + $rr->send(""); +}
\ No newline at end of file diff --git a/tests/sleep.php b/tests/sleep.php new file mode 100644 index 00000000..b3ea8235 --- /dev/null +++ b/tests/sleep.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +use Spiral\Goridge\StreamRelay; +use Spiral\RoadRunner\Worker as RoadRunner; + +require dirname(__DIR__) . "/vendor_php/autoload.php"; + +$rr = new RoadRunner(new StreamRelay(\STDIN, \STDOUT)); + +while($rr->receive($ctx)){ + sleep(3); + $rr->send(""); +}
\ No newline at end of file diff --git a/util/doc.go b/util/doc.go new file mode 100755 index 00000000..a3798715 --- /dev/null +++ b/util/doc.go @@ -0,0 +1,5 @@ +package util + +/* +This package should not contain roadrunner dependencies, only system or third-party +*/ diff --git a/util/events.go b/util/events.go new file mode 100755 index 00000000..43a3e646 --- /dev/null +++ b/util/events.go @@ -0,0 +1,36 @@ +package util + +type EventsHandler interface { + NumListeners() int + AddListener(listener EventListener) + Push(e interface{}) +} + +// Event listener listens for the events produced by worker, worker pool or other service. +type EventListener func(event interface{}) + +// EventHandler helps to broadcast events to multiple listeners. +type EventHandler struct { + listeners []EventListener +} + +func NewEventsHandler() EventsHandler { + return &EventHandler{listeners: make([]EventListener, 0, 2)} +} + +// NumListeners returns number of event listeners. +func (eb *EventHandler) NumListeners() int { + return len(eb.listeners) +} + +// AddListener registers new event listener. +func (eb *EventHandler) AddListener(listener EventListener) { + eb.listeners = append(eb.listeners, listener) +} + +// Push broadcast events across all event listeners. +func (eb *EventHandler) Push(e interface{}) { + for _, listener := range eb.listeners { + listener(e) + } +} diff --git a/osutil/isolate.go b/util/isolate.go index 9eaf8a44..2bdb9d6c 100644..100755 --- a/osutil/isolate.go +++ b/util/isolate.go @@ -1,6 +1,6 @@ // +build !windows -package osutil +package util import ( "fmt" @@ -9,6 +9,8 @@ import ( "os/user" "strconv" "syscall" + + "github.com/spiral/errors" ) // IsolateProcess change gpid for the process to avoid bypassing signals to php processes. @@ -18,19 +20,20 @@ func IsolateProcess(cmd *exec.Cmd) { // ExecuteFromUser may work only if run RR under root user func ExecuteFromUser(cmd *exec.Cmd, u string) error { + const op = errors.Op("execute from user") usr, err := user.Lookup(u) if err != nil { - return err + return errors.E(op, err) } usrI32, err := strconv.Atoi(usr.Uid) if err != nil { - return err + return errors.E(op, err) } grI32, err := strconv.Atoi(usr.Gid) if err != nil { - return err + return errors.E(op, err) } // For more information: @@ -44,7 +47,7 @@ func ExecuteFromUser(cmd *exec.Cmd, u string) error { return fmt.Errorf("unable to test user namespaces due to permissions") } - return fmt.Errorf("failed to stat /proc/self/ns/user: %v", err) + return errors.E(op, errors.Errorf("failed to stat /proc/self/ns/user: %v", err)) } cmd.SysProcAttr.Credential = &syscall.Credential{ diff --git a/osutil/isolate_win.go b/util/isolate_win.go index 52fb5d8a..6756d59f 100644..100755 --- a/osutil/isolate_win.go +++ b/util/isolate_win.go @@ -1,6 +1,6 @@ // +build windows -package osutil +package util import ( "os/exec" @@ -14,4 +14,4 @@ func IsolateProcess(cmd *exec.Cmd) { func ExecuteFromUser(cmd *exec.Cmd, u string) error { return nil -}
\ No newline at end of file +} diff --git a/util/network.go b/util/network.go index d858cb0a..c2475f4a 100644..100755 --- a/util/network.go +++ b/util/network.go @@ -5,11 +5,12 @@ package util import ( "errors" "fmt" - "github.com/valyala/tcplisten" "net" "os" "strings" "syscall" + + "github.com/valyala/tcplisten" ) // CreateListener crates socket listener based on DSN definition. @@ -23,13 +24,19 @@ func CreateListener(address string) (net.Listener, error) { return nil, errors.New("invalid Protocol (tcp://:6001, unix://file.sock)") } - if dsn[0] == "unix" && fileExists(dsn[1]) { - err := syscall.Unlink(dsn[1]) - if err != nil { - return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + // create unix listener + if dsn[0] == "unix" { + // check if the file exist + if fileExists(dsn[1]) { + err := syscall.Unlink(dsn[1]) + if err != nil { + return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + } } + return net.Listen(dsn[0], dsn[1]) } + // configure and create tcp4 listener cfg := tcplisten.Config{ ReusePort: true, DeferAccept: true, @@ -37,12 +44,8 @@ func CreateListener(address string) (net.Listener, error) { Backlog: 0, } - // tcp4 is currently supported - if dsn[0] == "tcp" { - return cfg.NewListener("tcp4", dsn[1]) - } - - return net.Listen(dsn[0], dsn[1]) + // only tcp4 is currently supported + return cfg.NewListener("tcp4", dsn[1]) } // fileExists checks if a file exists and is not a directory before we diff --git a/util/network_test.go b/util/network_test.go index 09157ec0..1dc16e94 100644..100755 --- a/util/network_test.go +++ b/util/network_test.go @@ -3,8 +3,9 @@ package util import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestCreateListener(t *testing.T) { diff --git a/util/network_windows.go b/util/network_windows.go index 843d5779..843d5779 100644..100755 --- a/util/network_windows.go +++ b/util/network_windows.go diff --git a/util/network_windows_test.go b/util/network_windows_test.go index a5a8064e..b6648ed0 100644..100755 --- a/util/network_windows_test.go +++ b/util/network_windows_test.go @@ -13,4 +13,4 @@ func TestCreateListener(t *testing.T) { _, err = CreateListener("aaa://192.168.0.1") assert.Error(t, err, "Invalid Protocol (tcp://:6001, unix://file.sock)") -}
\ No newline at end of file +} diff --git a/util/state_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 4fbf4369..402e9b90 100644..100755 --- a/worker.go +++ b/worker.go @@ -1,6 +1,8 @@ package roadrunner import ( + "bytes" + "context" "fmt" "os" "os/exec" @@ -9,36 +11,120 @@ import ( "sync" "time" - "github.com/pkg/errors" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/util" + "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 +const ( + // WaitDuration - for how long error buffer should attempt to aggregate error messages + // before merging output together since lastError update (required to keep error update together). + WaitDuration = 25 * time.Millisecond +) + +// EventWorkerKill thrown after WorkerProcess is being forcefully killed. +const ( + // EventWorkerError triggered after WorkerProcess. Except payload to be error. + EventWorkerError Event = iota + 200 + + // EventWorkerLog triggered on every write to WorkerProcess StdErr pipe (batched). Except payload to be []byte string. + EventWorkerLog +) + +type Event int64 + +func (ev Event) String() string { + switch ev { + case EventWorkerError: + return "EventWorkerError" + case EventWorkerLog: + return "EventWorkerLog" + } + return "Unknown event type" +} + +// WorkerEvent wraps worker events. +type WorkerEvent struct { + // Event id, see below. + Event Event + + // Worker triggered the event. + Worker WorkerBase + + // Event specific payload. + Payload interface{} +} - // Created indicates at what time worker has been created. - Created time.Time +type WorkerBase interface { + fmt.Stringer - // state holds information about current worker state, - // number of worker executions, buf status change time. + // Pid returns worker pid. + Pid() int64 + + // Created returns time worker was created at. + Created() time.Time + + // AddListener attaches listener to consume worker events. + AddListener(listener util.EventListener) + + // 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() 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() 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 util.EventsHandler + + // 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 + + // stderr aggregates stderr output from underlying process. Value can be // receive only once command is completed and all pipes are closed. - err *errBuffer + stderr *bytes.Buffer // 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 +133,182 @@ 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: &util.EventHandler{}, + cmd: cmd, + state: newState(StateInactive), + stderr: new(bytes.Buffer), } - // piping all stderr to command errBuffer - w.cmd.Stderr = w.err + // small buffer optimization + // at this point we know, that stderr will contain huge messages + w.stderr.Grow(1024) + w.cmd.Stderr = w 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 { +// Pid returns worker pid. +func (w *WorkerProcess) Pid() int64 { + return int64(w.pid) +} + +// Created returns time worker was created at. +func (w *WorkerProcess) Created() time.Time { + return w.created +} + +// AddListener registers new worker event listener. +func (w *WorkerProcess) AddListener(listener util.EventListener) { + w.events.AddListener(listener) +} + +// 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 -// complete and will return process error (if any), if stderr is presented it's value -// will be wrapped as WorkerError. Method will return error code if php process fails -// to find or start the script. -func (w *Worker) Wait() error { - <-w.waitDone - - // ensure that all receive/send operations are complete - w.mu.Lock() - defer w.mu.Unlock() - - if w.endState.Success() { - w.state.set(StateStopped) - return nil - } - - if w.state.Value() != StateStopping { - w.state.set(StateErrored) - } else { - w.state.set(StateStopped) - } - - if w.err.Len() != 0 { - return errors.New(w.err.String()) +func (w *WorkerProcess) Start() error { + err := w.cmd.Start() + if err != nil { + return err } - - // generic process error - return &exec.ExitError{ProcessState: w.endState} + w.pid = w.cmd.Process.Pid + 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: - w.mu.Lock() - defer w.mu.Unlock() +// 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 *WorkerProcess) Wait() error { + const op = errors.Op("worker process wait") + err := multierr.Combine(w.cmd.Wait()) + // at this point according to the documentation (see cmd.Wait comment) + // if worker finishes with an error, message will be written to the stderr first + // and then w.cmd.Wait return an error + w.endState = w.cmd.ProcessState + if err != nil { + w.state.Set(StateErrored) - w.state.set(StateStopping) - err := sendControl(w.rl, &stopCommand{Stop: true}) + // if process return code > 0, here will be an error from stderr (if presents) + if w.stderr.Len() > 0 { + err = multierr.Append(err, errors.E(op, errors.Str(w.stderr.String()))) + } - <-w.waitDone - return err + return multierr.Append(err, w.closeRelay()) } -} - -// Kill kills underlying process, make sure to call Wait() func to gather -// error log from the stderr. Does not waits for process completion! -func (w *Worker) Kill() error { - select { - case <-w.waitDone: - return nil - default: - w.state.set(StateStopping) - err := w.cmd.Process.Signal(os.Kill) - <-w.waitDone + err = multierr.Append(err, w.closeRelay()) + if err != nil { + w.state.Set(StateErrored) return err } -} - -// Exec sends payload to worker, executes it and returns result or -// error. Make sure to handle worker.Wait() to gather worker level -// errors. Method might return JobError indicating issue with payload. -func (w *Worker) Exec(rqs *Payload) (rsp *Payload, err error) { - w.mu.Lock() - if rqs == nil { - w.mu.Unlock() - return nil, fmt.Errorf("payload can not be empty") - } - - if w.state.Value() != StateReady { - w.mu.Unlock() - return nil, fmt.Errorf("worker is not ready (%s)", w.state.String()) + if w.endState.Success() { + w.state.Set(StateStopped) } - w.state.set(StateWorking) + return nil +} - rsp, err = w.execPayload(rqs) - if err != nil { - if _, ok := err.(JobError); !ok { - w.state.set(StateErrored) - w.state.registerExec() - w.mu.Unlock() - return nil, err +func (w *WorkerProcess) closeRelay() error { + if w.relay != nil { + err := w.relay.Close() + if err != nil { + return err } } - - w.state.set(StateReady) - w.state.registerExec() - w.mu.Unlock() - return rsp, err -} - -func (w *Worker) markInvalid() { - w.state.set(StateInvalid) + return nil } -func (w *Worker) start() error { - if err := w.cmd.Start(); err != nil { - close(w.waitDone) - return err - } - - w.Pid = &w.cmd.Process.Pid +// 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) - // wait for process to complete go func() { - w.endState, _ = w.cmd.Process.Wait() - if w.waitDone != nil { - close(w.waitDone) - w.mu.Lock() - defer w.mu.Unlock() - - if w.rl != nil { - err := w.rl.Close() - if err != nil { - w.err.lsn(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } - } - - err := w.err.Close() - if err != nil { - w.err.lsn(EventWorkerError, WorkerError{Worker: w, Caused: err}) - } + var err error + w.state.Set(StateStopping) + err = multierr.Append(err, sendControl(w.relay, &stopCommand{Stop: true})) + if err != nil { + w.state.Set(StateKilling) + c <- multierr.Append(err, w.cmd.Process.Kill()) } + w.state.Set(StateStopped) + c <- nil }() - return nil -} - -func (w *Worker) execPayload(rqs *Payload) (rsp *Payload, err error) { - // two things - if err := sendControl(w.rl, rqs.Context); err != nil { - return nil, errors.Wrap(err, "header error") - } - - if err = w.rl.Send(rqs.Body, 0); err != nil { - return nil, errors.Wrap(err, "sender error") - } - - var pr goridge.Prefix - rsp = new(Payload) - - if rsp.Context, pr, err = w.rl.Receive(); err != nil { - return nil, errors.Wrap(err, "worker error") - } - - if !pr.HasFlag(goridge.PayloadControl) { - return nil, fmt.Errorf("malformed worker response") + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-c: + if err != nil { + return err + } + return nil } +} - if pr.HasFlag(goridge.PayloadError) { - return nil, JobError(rsp.Context) +// 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() error { + w.state.Set(StateKilling) + err := w.cmd.Process.Signal(os.Kill) + if err != nil { + return err } + w.state.Set(StateStopped) + return nil +} - // add streaming support :) - if rsp.Body, _, err = w.rl.Receive(); err != nil { - return nil, errors.Wrap(err, "worker error") - } +// Write appends the contents of pool to the errBuffer, growing the errBuffer as +// needed. The return value n is the length of pool; errBuffer is always nil. +func (w *WorkerProcess) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + // clean all previous messages in the stderr + w.stderr.Truncate(0) + w.events.Push(WorkerEvent{Event: EventWorkerLog, Worker: w, Payload: p}) + // write new message + w.stderr.Write(p) - return rsp, nil + return len(p), nil } diff --git a/worker_test.go b/worker_test.go index c21e67cb..e82d383e 100644..100755 --- a/worker_test.go +++ b/worker_test.go @@ -1,16 +1,19 @@ 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.Equal(t, StateStopped, w.State().Value()) @@ -20,229 +23,44 @@ 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() { + defer wg.Done() assert.Error(t, w.Wait()) - assert.Equal(t, StateStopped, w.State().Value()) + // 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) - } - }() -} - -func Test_Echo(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Nil(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_BadPayload(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(nil) - - assert.Error(t, err) - assert.Nil(t, res) - - assert.Equal(t, "payload can not be empty", err.Error()) -} - -func Test_NotStarted_String(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := newWorker(cmd) - assert.Contains(t, w.String(), "php tests/client.php echo pipes") - assert.Contains(t, w.String(), "inactive") - assert.Contains(t, w.String(), "numExecs: 0") -} - -func Test_NotStarted_Exec(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := newWorker(cmd) - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Error(t, err) - assert.Nil(t, res) - - assert.Equal(t, "worker is not ready (inactive)", err.Error()) -} - -func Test_String(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - assert.Contains(t, w.String(), "php tests/client.php echo pipes") - assert.Contains(t, w.String(), "ready") - assert.Contains(t, w.String(), "numExecs: 0") -} - -func Test_Echo_Slow(t *testing.T) { - cmd := exec.Command("php", "tests/slow-client.php", "echo", "pipes", "10", "10") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - - assert.Nil(t, err) - assert.NotNil(t, res) - assert.NotNil(t, res.Body) - assert.Nil(t, res.Context) - - assert.Equal(t, "hello", res.String()) -} - -func Test_Broken(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "broken", "pipes") - - w, err := NewPipeFactory().SpawnWorker(cmd) + err = w.Kill() if err != nil { - t.Fatal(err) + t.Errorf("error killing the WorkerProcess: 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()) + wg.Wait() } func Test_OnStarted(t *testing.T) { cmd := exec.Command("php", "tests/client.php", "broken", "pipes") assert.Nil(t, cmd.Start()) - w, err := newWorker(cmd) + w, err := InitBaseWorker(cmd) assert.Nil(t, w) assert.NotNil(t, err) assert.Equal(t, "can't attach to running process", err.Error()) } - -func Test_Error(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "error", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - res, err := w.Exec(&Payload{Body: []byte("hello")}) - assert.Nil(t, res) - assert.NotNil(t, err) - - assert.IsType(t, JobError{}, err) - assert.Equal(t, "hello", err.Error()) -} - -func Test_NumExecs(t *testing.T) { - cmd := exec.Command("php", "tests/client.php", "echo", "pipes") - - w, _ := NewPipeFactory().SpawnWorker(cmd) - go func() { - assert.NoError(t, w.Wait()) - }() - defer func() { - err := w.Stop() - if err != nil { - t.Errorf("error stopping the worker: error %v", err) - } - }() - - _, err := w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(1), w.State().NumExecs()) - - _, err = w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(2), w.State().NumExecs()) - - _, err = w.Exec(&Payload{Body: []byte("hello")}) - if err != nil { - t.Errorf("fail to execute payload: error %v", err) - } - assert.Equal(t, int64(3), w.State().NumExecs()) -} diff --git a/worker_watcher.go b/worker_watcher.go new file mode 100755 index 00000000..8bc147d0 --- /dev/null +++ b/worker_watcher.go @@ -0,0 +1,330 @@ +package roadrunner + +import ( + "context" + "runtime" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/util" +) + +type Stack struct { + workers []WorkerBase + mutex sync.RWMutex + destroy bool +} + +func NewWorkersStack() *Stack { + return &Stack{ + workers: make([]WorkerBase, 0, runtime.NumCPU()), + } +} + +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 WorkerWatcher interface { + // AddToWatch used to add stack to wait its state + AddToWatch(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() error + + // Destroy destroys the underlying stack + Destroy(ctx context.Context) + + // WorkersList return all stack w/o removing it from internal storage + WorkersList() []WorkerBase + + // RemoveWorker remove worker from the stack + RemoveWorker(wb WorkerBase) error +} + +// workerCreateFunc can be nil, but in that case, dead stack will not be replaced +func newWorkerWatcher(allocator Allocator, numWorkers int64, events util.EventsHandler) WorkerWatcher { + ww := &workerWatcher{ + stack: NewWorkersStack(), + allocator: allocator, + initialNumWorkers: numWorkers, + actualNumWorkers: numWorkers, + events: events, + } + + return ww +} + +type workerWatcher struct { + mutex sync.RWMutex + stack *Stack + allocator Allocator + initialNumWorkers int64 + actualNumWorkers int64 + events util.EventsHandler +} + +func (ww *workerWatcher) AddToWatch(workers []WorkerBase) error { + for i := 0; i < len(workers); i++ { + sw, err := NewSyncWorker(workers[i]) + if err != nil { + return err + } + ww.stack.Push(sw) + sw.AddListener(ww.events.Push) + + go func(swc WorkerBase) { + ww.wait(swc) + }(sw) + } + return nil +} + +func (ww *workerWatcher) GetFreeWorker(ctx context.Context) (WorkerBase, error) { + const op = errors.Op("GetFreeWorker") + // thread safe operation + w, stop := ww.stack.Pop() + if stop { + return nil, errors.E(op, errors.ErrWatcherStopped) + } + + // handle worker remove state + // in this state worker is destroyed by supervisor + if w != nil && w.State().Value() == StateRemove { + err := ww.RemoveWorker(w) + if err != nil { + return nil, err + } + // try to get next + return ww.GetFreeWorker(ctx) + } + // no free stack + if w == nil { + for { + select { + default: + w, stop = ww.stack.Pop() + if stop { + return nil, errors.E(op, errors.ErrWatcherStopped) + } + if w == nil { + continue + } + ww.ReduceWorkersCount() + return w, nil + case <-ctx.Done(): + return nil, errors.E(op, errors.NoFreeWorkers, errors.Str("no free workers in the stack, timeout exceed")) + } + } + } + + ww.ReduceWorkersCount() + return w, nil +} + +func (ww *workerWatcher) AllocateNew() error { + ww.stack.mutex.Lock() + const op = errors.Op("allocate new worker") + sw, err := ww.allocator() + if err != nil { + return errors.E(op, errors.WorkerAllocate, err) + } + + ww.addToWatch(sw) + ww.stack.mutex.Unlock() + ww.PushWorker(sw) + + ww.events.Push(PoolEvent{ + Event: EventWorkerConstruct, + Payload: sw, + }) + + return nil +} + +func (ww *workerWatcher) RemoveWorker(wb WorkerBase) error { + ww.stack.mutex.Lock() + const op = errors.Op("remove worker") + 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.ReduceWorkersCount() + + wb.State().Set(StateInvalid) + err := wb.Kill() + if err != nil { + return errors.E(op, err) + } + break + } + } + // worker currently handle request, set state Remove + wb.State().Set(StateRemove) + return nil +} + +// O(1) operation +func (ww *workerWatcher) PushWorker(w WorkerBase) { + ww.IncreaseWorkersCount() + ww.stack.Push(w) +} + +func (ww *workerWatcher) ReduceWorkersCount() { + ww.mutex.Lock() + ww.actualNumWorkers-- + ww.mutex.Unlock() +} +func (ww *workerWatcher) IncreaseWorkersCount() { + ww.mutex.Lock() + ww.actualNumWorkers++ + ww.mutex.Unlock() +} + +// Destroy all underlying stack (but let them to complete the task) +func (ww *workerWatcher) 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: + ww.stack.mutex.Lock() + if len(ww.stack.workers) != int(ww.actualNumWorkers) { + ww.stack.mutex.Unlock() + continue + } + ww.stack.mutex.Unlock() + // unnecessary mutex, but + // just to make sure. All stack at this moment are in the stack + // Pop operation is blocked, push can't be done, since it's not possible to pop + 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, and it will return copy of the actual workers +func (ww *workerWatcher) WorkersList() []WorkerBase { + ww.stack.mutex.Lock() + defer ww.stack.mutex.Unlock() + workersCopy := make([]WorkerBase, 0, 1) + for _, v := range ww.stack.workers { + sw := v.(SyncWorker) + workersCopy = append(workersCopy, sw) + } + + return workersCopy +} + +func (ww *workerWatcher) wait(w WorkerBase) { + const op = errors.Op("process wait") + err := w.Wait() + if err != nil { + ww.events.Push(WorkerEvent{ + Event: EventWorkerError, + Worker: w, + Payload: errors.E(op, err), + }) + } + + if w.State().Value() == StateDestroyed { + // worker was manually destroyed, no need to replace + return + } + + pid := w.Pid() + ww.stack.mutex.Lock() + for i := 0; i < len(ww.stack.workers); i++ { + // worker in the stack, reallocating + if ww.stack.workers[i].Pid() == pid { + ww.stack.workers = append(ww.stack.workers[:i], ww.stack.workers[i+1:]...) + ww.ReduceWorkersCount() + ww.stack.mutex.Unlock() + + err = ww.AllocateNew() + if err != nil { + ww.events.Push(PoolEvent{ + Event: EventPoolError, + Payload: errors.E(op, err), + }) + } + + return + } + } + + ww.stack.mutex.Unlock() + + // worker not in the stack (not returned), forget and allocate new + err = ww.AllocateNew() + if err != nil { + ww.events.Push(PoolEvent{ + Event: EventPoolError, + Payload: errors.E(op, err), + }) + return + } +} + +func (ww *workerWatcher) addToWatch(wb WorkerBase) { + ww.mutex.Lock() + defer ww.mutex.Unlock() + go func() { + ww.wait(wb) + }() +} |