diff --git a/Makefile b/Makefile index db509715..99eb35ca 100755 --- a/Makefile +++ b/Makefile @@ -36,4 +36,9 @@ repush: git reset --soft HEAD^ git add -A git commit -m "repush" - git push gitlab dev --force \ No newline at end of file + git push gitlab dev --force + +rapid-crash: + sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\ + sleep 3 &&\ + sudo docker rm -f test_crash diff --git a/README.md b/README.md index 247d4a7a..0349502e 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr - Auto configuration for docker contaienrs - Auto hot-reload on container state / config file changes - Support HTTP(s), TCP and UDP -- Support HTTP(s) round robin load balancing - Web UI for configuration and monitoring (See [screenshots](screeenshots)) - Written in **[Go](https://go.dev)** @@ -110,14 +109,16 @@ See [providers.example.yml](providers.example.yml) for examples ## Build it yourself -1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already +1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1` -2. Clear cache if you have built this before (go < 1.22) with `go clean -cache` +2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already -3. get dependencies with `make get` +3. Clear cache if you have built this before (go < 1.22) with `go clean -cache` -4. build binary with `make build` +4. get dependencies with `make get` -5. start your container with `make up` (docker) or `bin/go-proxy` (binary) +5. build binary with `make build` + +6. start your container with `make up` (docker) or `bin/go-proxy` (binary) [🔼Back to top](#table-of-content) diff --git a/src/proxy/provider/provider.go b/src/proxy/provider/provider.go index 44e76a55..97a89eaf 100644 --- a/src/proxy/provider/provider.go +++ b/src/proxy/provider/provider.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path" + "time" "github.com/sirupsen/logrus" E "github.com/yusing/go-proxy/error" @@ -30,6 +31,8 @@ type Provider struct { watcherCancel context.CancelFunc l *logrus.Entry + + cooldownCh chan struct{} } type ProviderType string @@ -45,9 +48,10 @@ func newProvider(name string, t ProviderType) *Provider { t: t, routes: R.NewRoutes(), reloadReqCh: make(chan struct{}, 1), + cooldownCh: make(chan struct{}, 1), } p.l = logrus.WithField("provider", p) - + go p.processReloadRequests() return p } func NewFileProvider(filename string) *Provider { @@ -100,7 +104,8 @@ func (p *Provider) StartAllRoutes() E.NestedError { nStarted++ } }) - p.l.Infof("%d routes started, %d failed", nStarted, nFailed) + + p.l.Debugf("%d routes started, %d failed", nStarted, nFailed) return errors.Build() } @@ -120,16 +125,17 @@ func (p *Provider) StopAllRoutes() E.NestedError { nStopped++ } }) - p.l.Infof("%d routes stopped, %d failed", nStopped, nFailed) + p.l.Debugf("%d routes stopped, %d failed", nStopped, nFailed) return errors.Build() } func (p *Provider) ReloadRoutes() { - defer p.l.Info("routes reloaded") - - p.StopAllRoutes() - p.loadRoutes() - p.StartAllRoutes() + select { + case p.reloadReqCh <- struct{}{}: + // Successfully sent reload request + default: + // Reload request already in progress, ignore this request + } } func (p *Provider) GetCurrentRoutes() *R.Routes { @@ -142,15 +148,14 @@ func (p *Provider) watchEvents() { for { select { - case <-p.reloadReqCh: // block until last reload is done - p.ReloadRoutes() - continue // ignore events once after reload + case <-p.watcherCtx.Done(): + return case event, ok := <-events: if !ok { return } l.Info(event) - p.reloadReqCh <- struct{}{} + p.ReloadRoutes() case err, ok := <-errs: if !ok { return @@ -163,6 +168,29 @@ func (p *Provider) watchEvents() { } } +func (p *Provider) processReloadRequests() { + for range p.reloadReqCh { + // prevent busy loop caused by a container + // repeating crashing and restarting + select { + case p.cooldownCh <- struct{}{}: + p.l.Info("Starting to reload routes") + + p.StopAllRoutes() + p.loadRoutes() + p.StartAllRoutes() + + p.l.Info("Routes reloaded") + + go func() { + time.Sleep(reloadCooldown) + <-p.cooldownCh + }() + default: + } + } +} + func (p *Provider) loadRoutes() E.NestedError { entries, err := p.GetProxyEntries() @@ -183,3 +211,5 @@ func (p *Provider) loadRoutes() E.NestedError { }) return errors.Build() } + +const reloadCooldown = 300 * time.Millisecond diff --git a/src/route/http_route.go b/src/route/http_route.go index b5fd3d35..6b6bbe4b 100755 --- a/src/route/http_route.go +++ b/src/route/http_route.go @@ -19,10 +19,9 @@ import ( type ( HTTPRoute struct { - Alias PT.Alias `json:"alias"` - - TargetURL URL - PathPatterns PT.PathPatterns + Alias PT.Alias `json:"alias"` + TargetURL *URL `json:"target_url"` + PathPatterns PT.PathPatterns `json:"path_patterns"` mux *http.ServeMux handler *P.ReverseProxy @@ -53,7 +52,7 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) { if !ok { r = &HTTPRoute{ Alias: entry.Alias, - TargetURL: URL(*entry.URL), + TargetURL: (*URL)(entry.URL), PathPatterns: entry.PathPatterns, handler: rp, } diff --git a/src/watcher/file_watcher.go b/src/watcher/file_watcher.go index a871a115..f4cda622 100644 --- a/src/watcher/file_watcher.go +++ b/src/watcher/file_watcher.go @@ -4,6 +4,7 @@ import ( "context" "path" + "github.com/yusing/go-proxy/common" E "github.com/yusing/go-proxy/error" ) @@ -22,4 +23,4 @@ func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nested return fwHelper.Add(ctx, f) } -var fwHelper = newFileWatcherHelper() +var fwHelper = newFileWatcherHelper(common.ConfigBasePath) diff --git a/src/watcher/file_watcher_helper.go b/src/watcher/file_watcher_helper.go index 58492790..daaa32fe 100644 --- a/src/watcher/file_watcher_helper.go +++ b/src/watcher/file_watcher_helper.go @@ -8,7 +8,6 @@ import ( "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/common" E "github.com/yusing/go-proxy/error" ) @@ -26,14 +25,12 @@ type fileWatcherStream struct { errCh chan E.NestedError } -func newFileWatcherHelper() *fileWatcherHelper { +func newFileWatcherHelper(dirPath string) *fileWatcherHelper { w, err := fsnotify.NewWatcher() if err != nil { logrus.Panicf("unable to create fs watcher: %s", err) } - // watch config path for all changes - err = w.Add(common.ConfigBasePath) - if err != nil { + if err = w.Add(dirPath); err != nil { logrus.Panicf("unable to create fs watcher: %s", err) } helper := &fileWatcherHelper{ @@ -60,26 +57,24 @@ func (h *fileWatcherHelper) Add(ctx context.Context, w *fileWatcher) (<-chan Eve errCh: make(chan E.NestedError), } go func() { - select { - case <-ctx.Done(): - h.Remove(w) - return - case <-s.stopped: - return + for { + select { + case <-ctx.Done(): + s.stopped <- struct{}{} + case <-s.stopped: + h.mu.Lock() + defer h.mu.Unlock() + close(s.eventCh) + close(s.errCh) + delete(h.m, w.filename) + return + } } }() h.m[w.filename] = s return s.eventCh, s.errCh } -func (h *fileWatcherHelper) Remove(w *fileWatcher) { - h.mu.Lock() - defer h.mu.Unlock() - - h.m[w.filename].stopped <- struct{}{} - delete(h.m, w.filename) -} - func (h *fileWatcherHelper) start() { defer h.wg.Done() diff --git a/version.txt b/version.txt index 0a6a4023..51eecf62 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.0-rc1 \ No newline at end of file +0.5.0-rc2 \ No newline at end of file