v0.5-rc2: added reload cooldown, fixed auto reload, updated API

This commit is contained in:
yusing 2024-09-17 00:10:25 +08:00
parent 996b418ea9
commit c0ebd9f8c0
7 changed files with 76 additions and 45 deletions

View file

@ -37,3 +37,8 @@ repush:
git add -A git add -A
git commit -m "repush" git commit -m "repush"
git push gitlab dev --force 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

View file

@ -24,7 +24,6 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
- Auto configuration for docker contaienrs - Auto configuration for docker contaienrs
- Auto hot-reload on container state / config file changes - Auto hot-reload on container state / config file changes
- Support HTTP(s), TCP and UDP - Support HTTP(s), TCP and UDP
- Support HTTP(s) round robin load balancing
- Web UI for configuration and monitoring (See [screenshots](screeenshots)) - Web UI for configuration and monitoring (See [screenshots](screeenshots))
- Written in **[Go](https://go.dev)** - Written in **[Go](https://go.dev)**
@ -110,14 +109,16 @@ See [providers.example.yml](providers.example.yml) for examples
## Build it yourself ## 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) [🔼Back to top](#table-of-content)

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"path" "path"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
@ -30,6 +31,8 @@ type Provider struct {
watcherCancel context.CancelFunc watcherCancel context.CancelFunc
l *logrus.Entry l *logrus.Entry
cooldownCh chan struct{}
} }
type ProviderType string type ProviderType string
@ -45,9 +48,10 @@ func newProvider(name string, t ProviderType) *Provider {
t: t, t: t,
routes: R.NewRoutes(), routes: R.NewRoutes(),
reloadReqCh: make(chan struct{}, 1), reloadReqCh: make(chan struct{}, 1),
cooldownCh: make(chan struct{}, 1),
} }
p.l = logrus.WithField("provider", p) p.l = logrus.WithField("provider", p)
go p.processReloadRequests()
return p return p
} }
func NewFileProvider(filename string) *Provider { func NewFileProvider(filename string) *Provider {
@ -100,7 +104,8 @@ func (p *Provider) StartAllRoutes() E.NestedError {
nStarted++ nStarted++
} }
}) })
p.l.Infof("%d routes started, %d failed", nStarted, nFailed)
p.l.Debugf("%d routes started, %d failed", nStarted, nFailed)
return errors.Build() return errors.Build()
} }
@ -120,16 +125,17 @@ func (p *Provider) StopAllRoutes() E.NestedError {
nStopped++ nStopped++
} }
}) })
p.l.Infof("%d routes stopped, %d failed", nStopped, nFailed) p.l.Debugf("%d routes stopped, %d failed", nStopped, nFailed)
return errors.Build() return errors.Build()
} }
func (p *Provider) ReloadRoutes() { func (p *Provider) ReloadRoutes() {
defer p.l.Info("routes reloaded") select {
case p.reloadReqCh <- struct{}{}:
p.StopAllRoutes() // Successfully sent reload request
p.loadRoutes() default:
p.StartAllRoutes() // Reload request already in progress, ignore this request
}
} }
func (p *Provider) GetCurrentRoutes() *R.Routes { func (p *Provider) GetCurrentRoutes() *R.Routes {
@ -142,15 +148,14 @@ func (p *Provider) watchEvents() {
for { for {
select { select {
case <-p.reloadReqCh: // block until last reload is done case <-p.watcherCtx.Done():
p.ReloadRoutes() return
continue // ignore events once after reload
case event, ok := <-events: case event, ok := <-events:
if !ok { if !ok {
return return
} }
l.Info(event) l.Info(event)
p.reloadReqCh <- struct{}{} p.ReloadRoutes()
case err, ok := <-errs: case err, ok := <-errs:
if !ok { if !ok {
return 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 { func (p *Provider) loadRoutes() E.NestedError {
entries, err := p.GetProxyEntries() entries, err := p.GetProxyEntries()
@ -183,3 +211,5 @@ func (p *Provider) loadRoutes() E.NestedError {
}) })
return errors.Build() return errors.Build()
} }
const reloadCooldown = 300 * time.Millisecond

View file

@ -20,9 +20,8 @@ import (
type ( type (
HTTPRoute struct { HTTPRoute struct {
Alias PT.Alias `json:"alias"` Alias PT.Alias `json:"alias"`
TargetURL *URL `json:"target_url"`
TargetURL URL PathPatterns PT.PathPatterns `json:"path_patterns"`
PathPatterns PT.PathPatterns
mux *http.ServeMux mux *http.ServeMux
handler *P.ReverseProxy handler *P.ReverseProxy
@ -53,7 +52,7 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
if !ok { if !ok {
r = &HTTPRoute{ r = &HTTPRoute{
Alias: entry.Alias, Alias: entry.Alias,
TargetURL: URL(*entry.URL), TargetURL: (*URL)(entry.URL),
PathPatterns: entry.PathPatterns, PathPatterns: entry.PathPatterns,
handler: rp, handler: rp,
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"path" "path"
"github.com/yusing/go-proxy/common"
E "github.com/yusing/go-proxy/error" 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) return fwHelper.Add(ctx, f)
} }
var fwHelper = newFileWatcherHelper() var fwHelper = newFileWatcherHelper(common.ConfigBasePath)

View file

@ -8,7 +8,6 @@ import (
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/common"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
) )
@ -26,14 +25,12 @@ type fileWatcherStream struct {
errCh chan E.NestedError errCh chan E.NestedError
} }
func newFileWatcherHelper() *fileWatcherHelper { func newFileWatcherHelper(dirPath string) *fileWatcherHelper {
w, err := fsnotify.NewWatcher() w, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
logrus.Panicf("unable to create fs watcher: %s", err) logrus.Panicf("unable to create fs watcher: %s", err)
} }
// watch config path for all changes if err = w.Add(dirPath); err != nil {
err = w.Add(common.ConfigBasePath)
if err != nil {
logrus.Panicf("unable to create fs watcher: %s", err) logrus.Panicf("unable to create fs watcher: %s", err)
} }
helper := &fileWatcherHelper{ helper := &fileWatcherHelper{
@ -60,26 +57,24 @@ func (h *fileWatcherHelper) Add(ctx context.Context, w *fileWatcher) (<-chan Eve
errCh: make(chan E.NestedError), errCh: make(chan E.NestedError),
} }
go func() { go func() {
for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
h.Remove(w) s.stopped <- struct{}{}
return
case <-s.stopped: case <-s.stopped:
h.mu.Lock()
defer h.mu.Unlock()
close(s.eventCh)
close(s.errCh)
delete(h.m, w.filename)
return return
} }
}
}() }()
h.m[w.filename] = s h.m[w.filename] = s
return s.eventCh, s.errCh 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() { func (h *fileWatcherHelper) start() {
defer h.wg.Done() defer h.wg.Done()

View file

@ -1 +1 @@
0.5.0-rc1 0.5.0-rc2