diff options
67 files changed, 1908 insertions, 97 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 070350a0..f69d672a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,12 +3,18 @@ name: build on: push: pull_request: + branches: + # Branches from forks have the form 'user:branch-name' so we only run + # this job on pull_request events for branches that look like fork + # branches. Without this we would end up running this job twice for non + # forked PRs, once for the push and then once for opening the PR. + - '**:**' jobs: golang: name: Build (Go ${{ matrix.go }}, PHP ${{ matrix.php }}, OS ${{matrix.os}}) runs-on: ${{ matrix.os }} - timeout-minutes: 15 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -96,6 +102,7 @@ jobs: go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/static_root.txt -covermode=atomic ./plugins/static go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/headers.txt -covermode=atomic ./plugins/headers/tests go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/checker.txt -covermode=atomic ./plugins/checker/tests + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/reload.txt -covermode=atomic ./plugins/reload/tests cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt - uses: codecov/codecov-action@v1 # Docs: <https://github.com/codecov/codecov-action> @@ -105,17 +112,18 @@ jobs: file: ./coverage-ci/summary.txt fail_ci_if_error: false - golangci-check: + golangci-lint: name: Golang-CI (lint) runs-on: ubuntu-20.04 steps: - name: Check out code - uses: actions/checkout@v1 + uses: actions/checkout@v2 - - name: golangci-lint - uses: reviewdog/action-golangci-lint@v1 # action page: <https://github.com/reviewdog/action-golangci-lint> + - name: Run linter + uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action> with: - github_token: ${{ secrets.github_token }} + version: v1.33 # without patch version + only-new-issues: false # show only new issues if it's a pull request # image: # name: Build docker image @@ -21,4 +21,7 @@ vendor vendor_php builds/ tests/vendor/ -.rr-sample.yaml
\ No newline at end of file +.rr-sample.yaml +unit_tests +unit_tests_copied +dir1
\ No newline at end of file @@ -39,6 +39,7 @@ test: ## Run application tests go test -v -race -cover -tags=debug -covermode=atomic ./plugins/static/tests go test -v -race -cover -tags=debug -covermode=atomic ./plugins/headers/tests go test -v -race -cover -tags=debug -covermode=atomic ./plugins/checker/tests + go test -v -race -cover -tags=debug -covermode=atomic ./plugins/reload/tests lint: ## Run application linters go fmt ./... @@ -1,10 +1,18 @@ status = [ -'Build (Go 1.14, PHP 7.4)', -'Build (Go 1.15, PHP 7.4)', -'Build (Go 1.14, PHP 8.0)', -'Build (Go 1.15, PHP 8.0)', -'Golang-CI (lint)', -'Build docker image', + 'Build (Go 1.14, PHP 7.4, OS ubuntu-latest)', + 'Build (Go 1.14, PHP 7.4, OS windows-latest)', + 'Build (Go 1.14, PHP 7.4, OS macos-latest)', + 'Build (Go 1.15, PHP 7.4, OS ubuntu-latest)', + 'Build (Go 1.15, PHP 7.4, OS windows-latest)', + 'Build (Go 1.15, PHP 7.4, OS macos-latest)', + 'Build (Go 1.14, PHP 8.0, OS ubuntu-latest)', + 'Build (Go 1.14, PHP 8.0, OS windows-latest)', + 'Build (Go 1.14, PHP 8.0, OS macos-latest)', + 'Build (Go 1.15, PHP 8.0, OS ubuntu-latest)', + 'Build (Go 1.15, PHP 8.0, OS windows-latest)', + 'Build (Go 1.15, PHP 8.0, OS macos-latest)', + 'Golang-CI (lint)', + 'Build docker image', ] required_approvals = 1 @@ -15,7 +15,7 @@ require ( 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/errors v1.0.6 github.com/spiral/goridge/v3 v3.0.0-beta7 github.com/stretchr/testify v1.6.1 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a @@ -380,8 +380,8 @@ github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLq 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/v3 v3.0.0-beta6 h1:R+MQy93vUWn7zOvdFt8m3WMiTuLoP921IikQpGe9xXo= -github.com/spiral/goridge/v3 v3.0.0-beta6/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE= +github.com/spiral/errors v1.0.6 h1:berk5ShEILSw6DplUVv9Ea1wGdk2WlVKQpuvDngll0U= +github.com/spiral/errors v1.0.6/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= github.com/spiral/goridge/v3 v3.0.0-beta7 h1:rJmfVFC/clN7XgsONcu185l36cPJ+MfcFkQSifQXFCM= github.com/spiral/goridge/v3 v3.0.0-beta7/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= diff --git a/interfaces/resetter/interface.go b/interfaces/resetter/interface.go index 3fa48cf3..47d8d791 100644 --- a/interfaces/resetter/interface.go +++ b/interfaces/resetter/interface.go @@ -1,5 +1,17 @@ package resetter -type Resetter interface { +// If plugin implements Resettable interface, than it state can be resetted without reload in runtime via RPC/HTTP +type Resettable interface { + // Reset reload all plugins Reset() error } + +// Resetter interface is the Resetter plugin main interface +type Resetter interface { + // Reset all registered plugins + ResetAll() error + // Reset by plugin name + ResetByName(string) error + // GetAll registered plugins + GetAll() []string +} diff --git a/plugins/checker/tests/configs/.rr-checker-init.yaml b/plugins/checker/tests/configs/.rr-checker-init.yaml index 5943551b..0aba90c5 100755 --- a/plugins/checker/tests/configs/.rr-checker-init.yaml +++ b/plugins/checker/tests/configs/.rr-checker-init.yaml @@ -13,7 +13,9 @@ server: status: address: "127.0.0.1:34333" - +logs: + mode: development + level: error http: debug: true address: 127.0.0.1:11933 diff --git a/plugins/checker/tests/plugin_test.go b/plugins/checker/tests/plugin_test.go index a37fc08e..02a7f953 100644 --- a/plugins/checker/tests/plugin_test.go +++ b/plugins/checker/tests/plugin_test.go @@ -25,7 +25,7 @@ import ( ) func TestStatusHttp(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -109,7 +109,7 @@ func checkHTTPStatus(t *testing.T) { } func TestStatusRPC(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ diff --git a/plugins/config/tests/config_test.go b/plugins/config/tests/config_test.go index 422e7eee..91ddc4ae 100755 --- a/plugins/config/tests/config_test.go +++ b/plugins/config/tests/config_test.go @@ -12,7 +12,7 @@ import ( ) func TestViperProvider_Init(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml b/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml index 3dc5f9df..08c9b0ff 100644 --- a/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml +++ b/plugins/gzip/tests/configs/.rr-http-middlewareNotExist.yaml @@ -19,4 +19,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ 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 index 38fdfe47..3475d5dd 100644 --- a/plugins/gzip/tests/configs/.rr-http-withGzip.yaml +++ b/plugins/gzip/tests/configs/.rr-http-withGzip.yaml @@ -19,4 +19,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/gzip/tests/plugin_test.go b/plugins/gzip/tests/plugin_test.go index 39979895..97291ebe 100644 --- a/plugins/gzip/tests/plugin_test.go +++ b/plugins/gzip/tests/plugin_test.go @@ -21,7 +21,7 @@ import ( ) func TestGzipPlugin(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -102,7 +102,7 @@ func headerCheck(t *testing.T) { } func TestMiddlewareNotExist(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ diff --git a/plugins/headers/tests/configs/.rr-cors-headers.yaml b/plugins/headers/tests/configs/.rr-cors-headers.yaml index 5c1a200b..df985809 100644 --- a/plugins/headers/tests/configs/.rr-cors-headers.yaml +++ b/plugins/headers/tests/configs/.rr-cors-headers.yaml @@ -33,5 +33,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/headers/tests/configs/.rr-headers-init.yaml b/plugins/headers/tests/configs/.rr-headers-init.yaml index 252fe8f3..21a4373a 100644 --- a/plugins/headers/tests/configs/.rr-headers-init.yaml +++ b/plugins/headers/tests/configs/.rr-headers-init.yaml @@ -33,5 +33,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/headers/tests/configs/.rr-req-headers.yaml b/plugins/headers/tests/configs/.rr-req-headers.yaml index 9256e98d..bf305227 100644 --- a/plugins/headers/tests/configs/.rr-req-headers.yaml +++ b/plugins/headers/tests/configs/.rr-req-headers.yaml @@ -26,5 +26,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/headers/tests/configs/.rr-res-headers.yaml b/plugins/headers/tests/configs/.rr-res-headers.yaml index 1bca2c3d..ae354051 100644 --- a/plugins/headers/tests/configs/.rr-res-headers.yaml +++ b/plugins/headers/tests/configs/.rr-res-headers.yaml @@ -26,5 +26,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/headers/tests/headers_plugin_test.go b/plugins/headers/tests/headers_plugin_test.go index f1de8cb9..73aedb2c 100644 --- a/plugins/headers/tests/headers_plugin_test.go +++ b/plugins/headers/tests/headers_plugin_test.go @@ -20,7 +20,7 @@ import ( ) func TestHeadersInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -83,7 +83,7 @@ func TestHeadersInit(t *testing.T) { } func TestRequestHeaders(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -166,7 +166,7 @@ func reqHeaders(t *testing.T) { } func TestResponseHeaders(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -250,7 +250,7 @@ func resHeaders(t *testing.T) { } func TestCORSHeaders(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go index a6399489..13299da1 100644 --- a/plugins/http/plugin.go +++ b/plugins/http/plugin.go @@ -296,7 +296,7 @@ func (s *Plugin) Reset() error { s.Lock() defer s.Unlock() const op = errors.Op("http reset") - s.log.Info("Resetting http plugin") + s.log.Info("HTTP plugin got restart request. Restarting...") s.pool.Destroy(context.Background()) // re-read the config @@ -317,6 +317,7 @@ func (s *Plugin) Reset() error { return errors.E(op, err) } + s.log.Info("HTTP workers Pool successfully restarted") s.handler, err = NewHandler( s.cfg.MaxRequestSize, *s.cfg.Uploads, @@ -329,7 +330,9 @@ func (s *Plugin) Reset() error { // restore original listeners s.pool.AddListener(s.listener) + s.log.Info("HTTP listeners successfully re-added") + s.log.Info("HTTP plugin successfully restarted") return nil } diff --git a/plugins/http/tests/configs/.rr-broken-pipes.yaml b/plugins/http/tests/configs/.rr-broken-pipes.yaml index aacc303e..e57d0b86 100644 --- a/plugins/http/tests/configs/.rr-broken-pipes.yaml +++ b/plugins/http/tests/configs/.rr-broken-pipes.yaml @@ -24,6 +24,8 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-echoErr.yaml b/plugins/http/tests/configs/.rr-echoErr.yaml index 6ecdbb2a..24946c88 100644 --- a/plugins/http/tests/configs/.rr-echoErr.yaml +++ b/plugins/http/tests/configs/.rr-echoErr.yaml @@ -24,5 +24,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-env.yaml b/plugins/http/tests/configs/.rr-env.yaml index c9fdc798..e29f66cc 100644 --- a/plugins/http/tests/configs/.rr-env.yaml +++ b/plugins/http/tests/configs/.rr-env.yaml @@ -27,5 +27,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml index dbd19445..3009c30e 100644 --- a/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml +++ b/plugins/http/tests/configs/.rr-fcgi-reqUri.yaml @@ -32,4 +32,7 @@ http: http2: enabled: false h2c: false - maxConcurrentStreams: 128
\ No newline at end of file + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-fcgi.yaml b/plugins/http/tests/configs/.rr-fcgi.yaml index 0cbd6d02..45b6dbd0 100644 --- a/plugins/http/tests/configs/.rr-fcgi.yaml +++ b/plugins/http/tests/configs/.rr-fcgi.yaml @@ -32,4 +32,7 @@ http: http2: enabled: false h2c: false - maxConcurrentStreams: 128
\ No newline at end of file + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-h2c.yaml b/plugins/http/tests/configs/.rr-h2c.yaml index d1b24338..cc42e3bf 100644 --- a/plugins/http/tests/configs/.rr-h2c.yaml +++ b/plugins/http/tests/configs/.rr-h2c.yaml @@ -23,4 +23,7 @@ http: http2: enabled: true h2c: true - maxConcurrentStreams: 128
\ No newline at end of file + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-http.yaml b/plugins/http/tests/configs/.rr-http.yaml index 7b91f735..c6868f8c 100644 --- a/plugins/http/tests/configs/.rr-http.yaml +++ b/plugins/http/tests/configs/.rr-http.yaml @@ -37,5 +37,8 @@ http: enabled: false h2c: false maxConcurrentStreams: 128 +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-init.yaml b/plugins/http/tests/configs/.rr-init.yaml index 50aa91ec..70b9642b 100644 --- a/plugins/http/tests/configs/.rr-init.yaml +++ b/plugins/http/tests/configs/.rr-init.yaml @@ -37,5 +37,7 @@ http: enabled: false h2c: false maxConcurrentStreams: 128 - +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-resetter.yaml b/plugins/http/tests/configs/.rr-resetter.yaml index b46b21f5..f2134812 100644 --- a/plugins/http/tests/configs/.rr-resetter.yaml +++ b/plugins/http/tests/configs/.rr-resetter.yaml @@ -24,5 +24,7 @@ http: maxJobs: 0 allocateTimeout: 60s destroyTimeout: 60s - +logs: + mode: development + level: error diff --git a/plugins/http/tests/configs/.rr-ssl-push.yaml b/plugins/http/tests/configs/.rr-ssl-push.yaml index 02de906a..3aea683c 100644 --- a/plugins/http/tests/configs/.rr-ssl-push.yaml +++ b/plugins/http/tests/configs/.rr-ssl-push.yaml @@ -26,4 +26,6 @@ http: redirect: true cert: fixtures/server.crt key: fixtures/server.key - # rootCa: root.crt
\ No newline at end of file +logs: + mode: development + level: error
\ 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 index 0ba1753e..4d889734 100644 --- a/plugins/http/tests/configs/.rr-ssl-redirect.yaml +++ b/plugins/http/tests/configs/.rr-ssl-redirect.yaml @@ -26,4 +26,6 @@ http: redirect: true cert: fixtures/server.crt key: fixtures/server.key - # rootCa: root.crt
\ No newline at end of file +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/http/tests/configs/.rr-ssl.yaml b/plugins/http/tests/configs/.rr-ssl.yaml index fb54d3fa..83b5a2dc 100644 --- a/plugins/http/tests/configs/.rr-ssl.yaml +++ b/plugins/http/tests/configs/.rr-ssl.yaml @@ -32,4 +32,7 @@ http: http2: enabled: false h2c: false - maxConcurrentStreams: 128
\ No newline at end of file + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/http/tests/http_test.go b/plugins/http/tests/http_test.go index f68cd42c..c8dd4b38 100644 --- a/plugins/http/tests/http_test.go +++ b/plugins/http/tests/http_test.go @@ -41,7 +41,7 @@ var sslClient = &http.Client{ } func TestHTTPInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -104,7 +104,7 @@ func TestHTTPInit(t *testing.T) { } func TestHTTPInformerReset(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -225,7 +225,7 @@ func informerTest(t *testing.T) { } func TestSSL(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -353,7 +353,7 @@ func fcgiEcho(t *testing.T) { } func TestSSLRedirect(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -439,7 +439,7 @@ func sslRedirect(t *testing.T) { } func TestSSLPushPipes(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -527,7 +527,7 @@ func sslPush(t *testing.T) { } func TestFastCGI_RequestUri(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -611,7 +611,7 @@ func fcgiReqURI(t *testing.T) { } func TestH2CUpgrade(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -700,7 +700,7 @@ func h2cUpgrade(t *testing.T) { } func TestH2C(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -788,7 +788,7 @@ func h2c(t *testing.T) { } func TestHttpMiddleware(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -887,7 +887,7 @@ func middleware(t *testing.T) { } func TestHttpEchoErr(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -977,7 +977,7 @@ func echoError(t *testing.T) { } func TestHttpEnvVariables(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -1061,7 +1061,7 @@ func envVarsTest(t *testing.T) { } func TestHttpBrokenPipes(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ diff --git a/plugins/informer/tests/.rr-informer.yaml b/plugins/informer/tests/.rr-informer.yaml index 83ecd582..266933fd 100644 --- a/plugins/informer/tests/.rr-informer.yaml +++ b/plugins/informer/tests/.rr-informer.yaml @@ -10,4 +10,7 @@ server: rpc: listen: tcp://127.0.0.1:6001 - disabled: false
\ No newline at end of file + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/informer/tests/informer_test.go b/plugins/informer/tests/informer_test.go index 9e21e7ea..193e84bb 100644 --- a/plugins/informer/tests/informer_test.go +++ b/plugins/informer/tests/informer_test.go @@ -21,7 +21,7 @@ import ( ) func TestInformerInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/logger/tests/.rr.yaml b/plugins/logger/tests/.rr.yaml index e69de29b..cb555ec3 100644 --- a/plugins/logger/tests/.rr.yaml +++ b/plugins/logger/tests/.rr.yaml @@ -0,0 +1,3 @@ +logs: + mode: development + level: debug
\ No newline at end of file diff --git a/plugins/logger/tests/logger_test.go b/plugins/logger/tests/logger_test.go index 1df74c47..3e6faf1f 100644 --- a/plugins/logger/tests/logger_test.go +++ b/plugins/logger/tests/logger_test.go @@ -13,7 +13,7 @@ import ( ) func TestLogger(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/metrics/tests/.rr-test.yaml b/plugins/metrics/tests/.rr-test.yaml index 79343e3c..37c50395 100644 --- a/plugins/metrics/tests/.rr-test.yaml +++ b/plugins/metrics/tests/.rr-test.yaml @@ -10,4 +10,7 @@ metrics: type: histogram help: "Custom application metric" labels: [ "type" ] - buckets: [ 0.1, 0.2, 0.3, 1.0 ]
\ No newline at end of file + buckets: [ 0.1, 0.2, 0.3, 1.0 ] +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/metrics/tests/metrics_test.go b/plugins/metrics/tests/metrics_test.go index 57b10aa4..2d3a3c27 100644 --- a/plugins/metrics/tests/metrics_test.go +++ b/plugins/metrics/tests/metrics_test.go @@ -45,7 +45,7 @@ func get() (string, error) { } func TestMetricsInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -107,7 +107,7 @@ func TestMetricsInit(t *testing.T) { } func TestMetricsGaugeCollector(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -174,7 +174,7 @@ func TestMetricsGaugeCollector(t *testing.T) { } func TestMetricsDifferentRPCCalls(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/reload/config.go b/plugins/reload/config.go new file mode 100644 index 00000000..9ca2c0dc --- /dev/null +++ b/plugins/reload/config.go @@ -0,0 +1,58 @@ +package reload + +import ( + "time" + + "github.com/spiral/errors" +) + +// 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 +} + +// InitDefaults sets missing values to their default values. +func InitDefaults(c *Config) { + c.Interval = time.Second + c.Patterns = []string{".php"} +} + +// Valid validates the configuration. +func (c *Config) Valid() error { + const op = errors.Op("config validation [reload plugin]") + if c.Interval < time.Second { + return errors.E(op, errors.Str("too short interval")) + } + + if c.Services == nil { + return errors.E(op, errors.Str("should add at least 1 service")) + } else if len(c.Services) == 0 { + return errors.E(op, errors.Str("service initialized, however, no config added")) + } + + return nil +} diff --git a/plugins/reload/plugin.go b/plugins/reload/plugin.go new file mode 100644 index 00000000..555ddb82 --- /dev/null +++ b/plugins/reload/plugin.go @@ -0,0 +1,158 @@ +package reload + +import ( + "os" + "strings" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" + "github.com/spiral/roadrunner/v2/interfaces/resetter" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// PluginName contains default plugin name. +const PluginName string = "reload" +const thresholdChanBuffer uint = 1000 + +type Plugin struct { + cfg *Config + log log.Logger + watcher *Watcher + services map[string]interface{} + res resetter.Resetter + stopc chan struct{} +} + +// Init controller service +func (s *Plugin) Init(cfg config.Configurer, log log.Logger, res resetter.Resetter) error { + const op = errors.Op("reload plugin init") + s.cfg = &Config{} + InitDefaults(s.cfg) + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + // disable plugin in case of error + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.res = res + s.stopc = make(chan struct{}) + s.services = make(map[string]interface{}) + + var configs []WatcherConfig + + for serviceName, serviceConfig := range s.cfg.Services { + ignored, err := ConvertIgnored(serviceConfig.Ignore) + if err != nil { + return errors.E(op, err) + } + configs = append(configs, WatcherConfig{ + ServiceName: serviceName, + Recursive: serviceConfig.Recursive, + Directories: serviceConfig.Dirs, + FilterHooks: func(filename string, patterns []string) error { + for i := 0; i < len(patterns); i++ { + if strings.Contains(filename, patterns[i]) { + return nil + } + } + return errors.E(op, errors.Skip) + }, + Files: make(map[string]os.FileInfo), + Ignored: ignored, + FilePatterns: append(serviceConfig.Patterns, s.cfg.Patterns...), + }) + } + + s.watcher, err = NewWatcher(configs, s.log) + if err != nil { + return errors.E(op, err) + } + + return nil +} + +func (s *Plugin) Serve() chan error { + const op = errors.Op("reload plugin serve") + errCh := make(chan error, 1) + if s.cfg.Interval < time.Second { + errCh <- errors.E(op, errors.Str("reload interval is too fast")) + return errCh + } + + // 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 + }, thresholdChanBuffer) + + // use the same interval + timer := time.NewTimer(s.cfg.Interval) + + 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, len(s.cfg.Services)) + + go func() { + for { + select { + case cfg := <-treshholdc: + // logic is following: + // restart + timer.Stop() + // replace previous value in map by more recent without adding new one + updated[cfg.service] = cfg.serviceConfig + // if we getting a lot of events, we shouldn't restart particular service on each of it (user doing batch move or very fast typing) + // instead, we are resetting the timer and wait for s.cfg.Interval time + // If there is no more events, we restart service only once + timer.Reset(s.cfg.Interval) + case <-timer.C: + if len(updated) > 0 { + for name := range updated { + err := s.res.ResetByName(name) + if err != nil { + errCh <- errors.E(op, err) + return + } + } + // zero map + updated = make(map[string]ServiceConfig, len(s.cfg.Services)) + } + case <-s.stopc: + timer.Stop() + return + } + } + }() + + go func() { + err := s.watcher.StartPolling(s.cfg.Interval) + if err != nil { + errCh <- errors.E(op, err) + return + } + }() + + return errCh +} + +func (s *Plugin) Stop() error { + s.watcher.Stop() + s.stopc <- struct{}{} + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} diff --git a/plugins/reload/tests/config_test.go b/plugins/reload/tests/config_test.go new file mode 100644 index 00000000..5bb64b6b --- /dev/null +++ b/plugins/reload/tests/config_test.go @@ -0,0 +1,63 @@ +package tests + +import ( + "testing" + "time" + + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/stretchr/testify/assert" +) + +func Test_Config_Valid(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: services, + } + assert.NoError(t, cfg.Valid()) +} + +func Test_Fake_ServiceConfig(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + cfg := &reload.Config{ + Interval: time.Microsecond, + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_Interval(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Enabled: false, + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Millisecond, // should crash here + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_NoServiceConfig(t *testing.T) { + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: nil, + } + assert.Error(t, cfg.Valid()) +} diff --git a/plugins/reload/tests/configs/.rr-reload-2.yaml b/plugins/reload/tests/configs/.rr-reload-2.yaml new file mode 100644 index 00000000..5be3179b --- /dev/null +++ b/plugins/reload/tests/configs/.rr-reload-2.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../../tests/psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:27388' + 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 +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/plugins/reload/tests/configs/.rr-reload-3.yaml b/plugins/reload/tests/configs/.rr-reload-3.yaml new file mode 100644 index 00000000..b97ed667 --- /dev/null +++ b/plugins/reload/tests/configs/.rr-reload-3.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../../tests/psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:37388' + 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 +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: true diff --git a/plugins/reload/tests/configs/.rr-reload-4.yaml b/plugins/reload/tests/configs/.rr-reload-4.yaml new file mode 100644 index 00000000..b664b836 --- /dev/null +++ b/plugins/reload/tests/configs/.rr-reload-4.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../../tests/psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22766' + 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 +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .aaa + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: false diff --git a/plugins/reload/tests/configs/.rr-reload.yaml b/plugins/reload/tests/configs/.rr-reload.yaml new file mode 100644 index 00000000..5e223db3 --- /dev/null +++ b/plugins/reload/tests/configs/.rr-reload.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../../tests/psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22388' + 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 +logs: + mode: development + level: error +reload: + interval: 1s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/plugins/reload/tests/reload_plugin_test.go b/plugins/reload/tests/reload_plugin_test.go new file mode 100644 index 00000000..8fb9474f --- /dev/null +++ b/plugins/reload/tests/reload_plugin_test.go @@ -0,0 +1,822 @@ +package tests + +import ( + "io" + "io/ioutil" + "math/rand" + "net/http" + "os" + "os/signal" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +const testDir string = "unit_tests" +const testCopyToDir string = "unit_tests_copied" +const hugeNumberOfFiles uint = 5000 + +func TestReloadInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + }() + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.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) + + 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 + } + } + }() + + t.Run("ReloadTestInit", reloadTestInit) + reloadHTTPLiveAfterReset(t, "22388") + + wg.Wait() +} + +func reloadTestInit(t *testing.T) { + err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec + []byte{}, 0755) + assert.NoError(t, err) +} + +func TestReloadHugeNumberOfFiles(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + err = os.Mkdir(testCopyToDir, 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + }() + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.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) + + tt := time.NewTimer(time.Second * 100) + + 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 + } + } + }() + + t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles) + ttt := time.Now() + t.Run("ReloadRandomlyChangeFile", randomlyChangeFile) + if time.Since(ttt).Seconds() > 80 { + t.Fatal("spend too much time on reloading") + } + reloadHTTPLiveAfterReset(t, "22388") + + wg.Wait() +} + +func randomlyChangeFile(t *testing.T) { + // we know, that directory contains 5000 files (0-4999) + // let's try to randomly change it + for i := 0; i < 100; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func reloadHugeNumberOfFiles(t *testing.T) { + for i := uint(0); i < hugeNumberOfFiles; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } +} + +// Should be events only about creating files with txt ext +func TestReloadFilterFileExt(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-2.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + }() + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.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) + + tt := time.NewTimer(time.Second * 40) + + 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 + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) + ttt := time.Now() + t.Run("ReloadFilteredExt", reloadFilteredExt) + if time.Since(ttt).Seconds() > 20 { + t.Fatal("spend too much time on reloading") + } + + reloadHTTPLiveAfterReset(t, "27388") + + wg.Wait() +} + +func reloadMakeFiles(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func reloadFilteredExt(t *testing.T) { + // change files with abc extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".abc"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } + + // change files with def extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".def"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +// Should be events only about creating files with txt ext +func TestReloadCopy3k(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-3.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources("dir1")) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + err = os.Mkdir(testCopyToDir, 0755) + assert.NoError(t, err) + err = os.Mkdir("dir1", 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources("dir1")) + }() + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.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) + + tt := time.NewTimer(time.Second * 180) + + 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 + } + } + }() + + // Scenario + // 1 + // Create 3k files with txt, abc, def extensions + // Copy files to the unit_tests_copy dir + // 2 + // Delete both dirs, recreate + // Create 3k files with txt, abc, def extensions + // Move files to the unit_tests_copy dir + // 3 + // Recursive + + t.Run("ReloadMake3kFiles", reloadMake3kFiles) + ttt := time.Now() + t.Run("ReloadCopyFiles", reloadCopyFiles) + if time.Since(ttt).Seconds() > 100 { + t.Fatal("spend too much time on copy") + } + + t.Run("ReloadRecursiveDirsSupport", copyFilesRecursive) + t.Run("RandomChangesInRecursiveDirs", randomChangesInRecursiveDirs) + t.Run("RemoveFilesSupport", removeFilesSupport) + t.Run("ReloadMoveSupport", reloadMoveSupport) + + reloadHTTPLiveAfterReset(t, "37388") + + wg.Wait() +} + +func reloadMoveSupport(t *testing.T) { + t.Run("MoveSupportCopy", copyFilesRecursive) + // move some files + for i := 0; i < 50; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(200)) // nolint:gosec + rDir := rand.Int63n(9) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + // move file + err := os.Rename(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]), filepath.Join(dirs[rDir+1], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + assert.NoError(t, err) + } +} + +func removeFilesSupport(t *testing.T) { + // remove some files + for i := 0; i < 50; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(200)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + err := os.Remove(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + assert.NoError(t, err) + } +} + +func randomChangesInRecursiveDirs(t *testing.T) { + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + filenames := []string{ + "file_", // should be update + "foo_", // should be created + "bar_", // should be created + } + for i := 0; i < 50; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(200)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + rName := rand.Int63n(3) // nolint:gosec + + err := ioutil.WriteFile(filepath.Join(dirs[rDir], filenames[rName]+strconv.Itoa(int(rNum))+ext[rExt]), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func copyFilesRecursive(t *testing.T) { + err := copyDir(testDir, "dir1") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10") + assert.NoError(t, err) +} + +func reloadCopyFiles(t *testing.T) { + err := copyDir(testDir, testCopyToDir) + assert.NoError(t, err) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + err = os.Mkdir(testCopyToDir, 0755) + assert.NoError(t, err) + + // recreate files + for i := uint(0); i < 200; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 200; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 200; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } + + err = copyDir(testDir, testCopyToDir) + assert.NoError(t, err) +} + +func reloadMake3kFiles(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func TestReloadNoRecursion(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-4.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources("dir1")) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + err = os.Mkdir("dir1", 0755) + assert.NoError(t, err) + + err = os.Mkdir(testCopyToDir, 0755) + assert.NoError(t, err) + + defer func() { + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources("dir1")) + }() + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.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) + + tt := time.NewTimer(time.Second * 30) + + 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 + } + } + }() + t.Run("ReloadMakeFiles", reloadMakeFiles) // make files in the testDir + t.Run("ReloadCopyFilesRecursive", reloadCopyFiles) + + reloadHTTPLiveAfterReset(t, "22766") + + wg.Wait() +} + +// ======================================================================== + +func reloadHTTPLiveAfterReset(t *testing.T, port string) { + req, err := http.NewRequest("GET", "http://localhost:"+port, nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "hello world", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func freeResources(path string) error { + return os.RemoveAll(path) +} + +func makeFile(filename string) error { + return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec +} + +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return errors.E(errors.Str("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return err + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + + 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 err + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = copyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return errors.E(err) + } + defer func() { + _ = in.Close() + }() + + out, err := os.Create(dst) + if err != nil { + return errors.E(err) + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, in) + if err != nil { + return errors.E(err) + } + + err = out.Sync() + if err != nil { + return errors.E(err) + } + + si, err := os.Stat(src) + if err != nil { + return errors.E(err) + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/plugins/reload/watcher.go b/plugins/reload/watcher.go new file mode 100644 index 00000000..55e1d9d5 --- /dev/null +++ b/plugins/reload/watcher.go @@ -0,0 +1,372 @@ +package reload + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/log" +) + +// 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 + + // logger + log log.Logger +} + +// Options is used to set Watcher Options +type Options func(*Watcher) + +// NewWatcher returns new instance of File Watcher +func NewWatcher(configs []WatcherConfig, log log.Logger, options ...Options) (*Watcher, error) { + w := &Watcher{ + Event: make(chan Event), + mu: &sync.Mutex{}, + + log: log, + + 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 +} + +// 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) { + const op = errors.Op("retrieve") + 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 errors.Is(errors.Skip, err) { + continue outer + } + } + + filesList[fileInfoList[i].Name()] = fileInfoList[i] + } + + return filesList, nil +} + +func (w *Watcher) StartPolling(duration time.Duration) error { + w.mu.Lock() + const op = errors.Op("start polling") + if w.started { + w.mu.Unlock() + return errors.E(op, errors.Str("already started")) + } + + w.started = true + w.mu.Unlock() + + return w.waitEvent(duration) +} + +// this is blocking operation +func (w *Watcher) waitEvent(d time.Duration) error { + ticker := time.NewTicker(d) + for { + select { + case <-w.close: + ticker.Stop() + // just exit + // no matter for the pollEvents + return nil + case <-ticker.C: + // this is not very effective way + // because we have to wait on Lock + // better is to listen files in parallel, but, since that would be used in debug... TODO + for serviceName := range w.watcherConfigs { + // TODO sync approach + fileList, _ := w.retrieveFileList(serviceName, w.watcherConfigs[serviceName]) + w.pollEvents(w.watcherConfigs[serviceName].ServiceName, fileList) + } + } + } +} + +// retrieveFileList get file list for service +func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + if config.Recursive { + // walk through directories recursively + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + list, err := w.retrieveFilesRecursive(serviceName, fullPath) + if err != nil { + return nil, err + } + + for k := range list { + fileList[k] = list[k] + } + } + return fileList, nil + } + + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + + // list is pathToFiles with files + list, err := w.retrieveFilesSingle(serviceName, fullPath) + if err != nil { + return nil, err + } + + for pathToFile, file := range list { + fileList[pathToFile] = file + } + } + + return fileList, nil +} + +func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + + return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // If path is ignored and it's a directory, skip the directory. If it's + // ignored and it's a single file, skip the file. + _, ignored := w.watcherConfigs[serviceName].Ignored[path] + if ignored { + if info.IsDir() { + // if it's dir, ignore whole + return filepath.SkipDir + } + return nil + } + + // if filename does not contain pattern --> ignore that file + err = w.watcherConfigs[serviceName].FilterHooks(info.Name(), w.watcherConfigs[serviceName].FilePatterns) + if errors.Is(errors.Skip, err) { + return nil + } + + // Add the path and it's info to the file list. + fileList[path] = info + return nil + }) +} + +func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) { + w.mu.Lock() + defer w.mu.Unlock() + + // Store create and remove events for use to check for rename events. + creates := make(map[string]os.FileInfo) + removes := make(map[string]os.FileInfo) + + // Check for removed files. + for pth := range w.watcherConfigs[serviceName].Files { + if _, found := files[pth]; !found { + removes[pth] = w.watcherConfigs[serviceName].Files[pth] + w.log.Debug("file added to the list of removed files", "path", pth, "name", w.watcherConfigs[serviceName].Files[pth].Name(), "size", w.watcherConfigs[serviceName].Files[pth].Size()) + } + } + + // Check for created files, writes and chmods. + for pth := range files { + if files[pth].IsDir() { + continue + } + oldInfo, found := w.watcherConfigs[serviceName].Files[pth] + if !found { + // A file was created. + creates[pth] = files[pth] + w.log.Debug("file was created", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + continue + } + + if oldInfo.ModTime() != files[pth].ModTime() || oldInfo.Mode() != files[pth].Mode() { + w.watcherConfigs[serviceName].Files[pth] = files[pth] + w.log.Debug("file was updated", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + w.Event <- Event{ + Path: pth, + Info: files[pth], + service: serviceName, + } + } + } + + // Send all the remaining create and remove events. + for pth := range creates { + // add file to the plugin watch files + w.watcherConfigs[serviceName].Files[pth] = creates[pth] + w.log.Debug("file was added to watcher", "path", pth, "name", creates[pth].Name(), "size", creates[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: creates[pth], + service: serviceName, + } + } + + for pth := range removes { + // delete path from the config + delete(w.watcherConfigs[serviceName].Files, pth) + w.log.Debug("file was removed from watcher", "path", pth, "name", removes[pth].Name(), "size", removes[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: removes[pth], + service: serviceName, + } + } +} + +func (w *Watcher) Stop() { + w.close <- struct{}{} +} diff --git a/plugins/resetter/plugin.go b/plugins/resetter/plugin.go index 99e02aef..8dc5cc31 100644 --- a/plugins/resetter/plugin.go +++ b/plugins/resetter/plugin.go @@ -10,12 +10,39 @@ import ( const PluginName = "resetter" type Plugin struct { - registry map[string]resetter.Resetter + registry map[string]resetter.Resettable log log.Logger } +func (p *Plugin) ResetAll() error { + const op = errors.Op("reset all") + for name := range p.registry { + err := p.registry[name].Reset() + if err != nil { + return errors.E(op, err) + } + } + return nil +} + +func (p *Plugin) ResetByName(plugin string) error { + const op = errors.Op("reset by name") + if plugin, ok := p.registry[plugin]; ok { + return plugin.Reset() + } + return errors.E(op, errors.Errorf("can't find plugin: %s", plugin)) +} + +func (p *Plugin) GetAll() []string { + all := make([]string, 0, len(p.registry)) + for name := range p.registry { + all = append(all, name) + } + return all +} + func (p *Plugin) Init(log log.Logger) error { - p.registry = make(map[string]resetter.Resetter) + p.registry = make(map[string]resetter.Resettable) p.log = log return nil } @@ -31,7 +58,7 @@ func (p *Plugin) Reset(name string) error { } // RegisterTarget resettable service. -func (p *Plugin) RegisterTarget(name endure.Named, r resetter.Resetter) error { +func (p *Plugin) RegisterTarget(name endure.Named, r resetter.Resettable) error { p.registry[name.Name()] = r return nil } diff --git a/plugins/resetter/rpc.go b/plugins/resetter/rpc.go index ecc51bb3..344c6681 100644 --- a/plugins/resetter/rpc.go +++ b/plugins/resetter/rpc.go @@ -7,7 +7,7 @@ type rpc struct { log log.Logger } -// List all resettable services. +// List all resettable plugins. func (rpc *rpc) List(_ bool, list *[]string) error { rpc.log.Debug("started List method") *list = make([]string, 0) @@ -21,7 +21,7 @@ func (rpc *rpc) List(_ bool, list *[]string) error { return nil } -// Reset named service. +// Reset named plugin. 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) diff --git a/plugins/resetter/tests/.rr-resetter.yaml b/plugins/resetter/tests/.rr-resetter.yaml index 83ecd582..266933fd 100644 --- a/plugins/resetter/tests/.rr-resetter.yaml +++ b/plugins/resetter/tests/.rr-resetter.yaml @@ -10,4 +10,7 @@ server: rpc: listen: tcp://127.0.0.1:6001 - disabled: false
\ No newline at end of file + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/resetter/tests/resetter_test.go b/plugins/resetter/tests/resetter_test.go index 3bfccf47..45de67e3 100644 --- a/plugins/resetter/tests/resetter_test.go +++ b/plugins/resetter/tests/resetter_test.go @@ -20,7 +20,7 @@ import ( ) func TestResetterInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/rpc/tests/.rr-rpc-disabled.yaml b/plugins/rpc/tests/.rr-rpc-disabled.yaml index 624fb3c5..d5c185e7 100644 --- a/plugins/rpc/tests/.rr-rpc-disabled.yaml +++ b/plugins/rpc/tests/.rr-rpc-disabled.yaml @@ -1,3 +1,6 @@ rpc: listen: tcp://127.0.0.1:6001 - disabled: true
\ No newline at end of file + disabled: true +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/rpc/tests/.rr.yaml b/plugins/rpc/tests/.rr.yaml index 76e8b440..d2cb6c70 100644 --- a/plugins/rpc/tests/.rr.yaml +++ b/plugins/rpc/tests/.rr.yaml @@ -1,3 +1,6 @@ rpc: listen: tcp://127.0.0.1:6001 - disabled: false
\ No newline at end of file + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/rpc/tests/rpc_test.go b/plugins/rpc/tests/rpc_test.go index 88267dfb..0344da6b 100644 --- a/plugins/rpc/tests/rpc_test.go +++ b/plugins/rpc/tests/rpc_test.go @@ -17,7 +17,7 @@ import ( // graph https://bit.ly/3ensdNb func TestRpcInit(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -96,7 +96,7 @@ func TestRpcInit(t *testing.T) { // graph https://bit.ly/3ensdNb func TestRpcDisabled(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/server/tests/configs/.rr-no-app-section.yaml b/plugins/server/tests/configs/.rr-no-app-section.yaml index b6e3ea93..5266e83d 100644 --- a/plugins/server/tests/configs/.rr-no-app-section.yaml +++ b/plugins/server/tests/configs/.rr-no-app-section.yaml @@ -6,4 +6,7 @@ server: "RR_CONFIG": "/some/place/on/the/C134" "RR_CONFIG2": "C138" relay: "pipes" - relayTimeout: "20s"
\ No newline at end of file + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr-sockets.yaml b/plugins/server/tests/configs/.rr-sockets.yaml index ab1239aa..6b5b6bf5 100644 --- a/plugins/server/tests/configs/.rr-sockets.yaml +++ b/plugins/server/tests/configs/.rr-sockets.yaml @@ -6,4 +6,7 @@ server: "RR_CONFIG": "/some/place/on/the/C134" "RR_CONFIG2": "C138" relay: "unix://unix.sock" - relayTimeout: "20s"
\ No newline at end of file + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr-tcp.yaml b/plugins/server/tests/configs/.rr-tcp.yaml index f53bffcc..ee1d450a 100644 --- a/plugins/server/tests/configs/.rr-tcp.yaml +++ b/plugins/server/tests/configs/.rr-tcp.yaml @@ -6,4 +6,7 @@ server: "RR_CONFIG": "/some/place/on/the/C134" "RR_CONFIG2": "C138" relay: "tcp://localhost:9999" - relayTimeout: "20s"
\ No newline at end of file + relayTimeout: "20s" +logs: + mode: development + level: error
\ 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 index d2c087a6..e66349dd 100644 --- a/plugins/server/tests/configs/.rr-wrong-command.yaml +++ b/plugins/server/tests/configs/.rr-wrong-command.yaml @@ -7,3 +7,6 @@ server: "RR_CONFIG2": "C138" relay: "pipes" relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr-wrong-relay.yaml b/plugins/server/tests/configs/.rr-wrong-relay.yaml index 1dd73d73..98894c7a 100644 --- a/plugins/server/tests/configs/.rr-wrong-relay.yaml +++ b/plugins/server/tests/configs/.rr-wrong-relay.yaml @@ -6,4 +6,7 @@ server: "RR_CONFIG": "/some/place/on/the/C134" "RR_CONFIG2": "C138" relay: "pupes" - relayTimeout: "20s"
\ No newline at end of file + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/server/tests/configs/.rr.yaml b/plugins/server/tests/configs/.rr.yaml index b6e3ea93..5266e83d 100644 --- a/plugins/server/tests/configs/.rr.yaml +++ b/plugins/server/tests/configs/.rr.yaml @@ -6,4 +6,7 @@ server: "RR_CONFIG": "/some/place/on/the/C134" "RR_CONFIG2": "C138" relay: "pipes" - relayTimeout: "20s"
\ No newline at end of file + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/server/tests/server_test.go b/plugins/server/tests/server_test.go index bc374a9e..faf01b11 100644 --- a/plugins/server/tests/server_test.go +++ b/plugins/server/tests/server_test.go @@ -14,7 +14,7 @@ import ( ) func TestAppPipes(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -80,7 +80,7 @@ func TestAppPipes(t *testing.T) { } func TestAppSockets(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -146,7 +146,7 @@ func TestAppSockets(t *testing.T) { } func TestAppTCP(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func TestAppTCP(t *testing.T) { } func TestAppWrongConfig(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -244,7 +244,7 @@ func TestAppWrongConfig(t *testing.T) { } func TestAppWrongRelay(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -280,7 +280,7 @@ func TestAppWrongRelay(t *testing.T) { } func TestAppWrongCommand(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -318,7 +318,7 @@ func TestAppWrongCommand(t *testing.T) { } func TestAppNoAppSectionInConfig(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.DebugLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } diff --git a/plugins/static/tests/configs/.rr-http-static-disabled.yaml b/plugins/static/tests/configs/.rr-http-static-disabled.yaml index d0b9b388..e8917c06 100644 --- a/plugins/static/tests/configs/.rr-http-static-disabled.yaml +++ b/plugins/static/tests/configs/.rr-http-static-disabled.yaml @@ -27,4 +27,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ 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 index a3d814a3..1cae9ed7 100644 --- a/plugins/static/tests/configs/.rr-http-static-files-disable.yaml +++ b/plugins/static/tests/configs/.rr-http-static-files-disable.yaml @@ -27,4 +27,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ 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 index 35938b80..32d0a6c7 100644 --- a/plugins/static/tests/configs/.rr-http-static-files.yaml +++ b/plugins/static/tests/configs/.rr-http-static-files.yaml @@ -28,4 +28,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ 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 index 80a1fa7f..d3bd05f5 100644 --- a/plugins/static/tests/configs/.rr-http-static.yaml +++ b/plugins/static/tests/configs/.rr-http-static.yaml @@ -26,4 +26,7 @@ http: numWorkers: 2 maxJobs: 0 allocateTimeout: 60s - destroyTimeout: 60s
\ No newline at end of file + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/plugins/static/tests/static_plugin_test.go b/plugins/static/tests/static_plugin_test.go index 528d5eea..5bad54bf 100644 --- a/plugins/static/tests/static_plugin_test.go +++ b/plugins/static/tests/static_plugin_test.go @@ -25,7 +25,7 @@ import ( ) func TestStaticPlugin(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -133,7 +133,7 @@ func serveStaticSample(t *testing.T) { } func TestStaticDisabled(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -206,7 +206,7 @@ func staticDisabled(t *testing.T) { } func TestStaticFilesDisabled(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -282,7 +282,7 @@ func staticFilesDisabled(t *testing.T) { } func TestStaticFilesForbid(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) cfg := &config.Viper{ @@ -23,6 +23,9 @@ const ( // WaitDuration - for how long error buffer should attempt to aggregate error messages // before merging output together since lastError update (required to keep error update together). WaitDuration = 25 * time.Millisecond + + // ReadBufSize used to make a slice with specified length to read from stderr + ReadBufSize = 10240 // Kb ) // EventWorkerKill thrown after WorkerProcess is being forcefully killed. @@ -60,7 +63,7 @@ type WorkerEvent struct { var pool = sync.Pool{ New: func() interface{} { - buf := make([]byte, 10240) + buf := make([]byte, ReadBufSize) return &buf }, } @@ -164,7 +167,7 @@ func InitBaseWorker(cmd *exec.Cmd) (WorkerBase, error) { // small buffer optimization // at this point we know, that stderr will contain huge messages - w.stderr.Grow(10240) + w.stderr.Grow(ReadBufSize) go func() { w.watch() |