mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 16:58:31 +02:00
fixed loadbalancer with idlewatcher, fixed reload issue
This commit is contained in:
@@ -37,7 +37,7 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
task.GlobalTask("close docker clients").OnComplete("", func() {
|
||||
task.GlobalTask("close docker clients").OnFinished("", func() {
|
||||
clientMap.RangeAllParallel(func(_ string, c Client) {
|
||||
if c.Connected() {
|
||||
c.Client.Close()
|
||||
@@ -70,7 +70,7 @@ func (c *SharedClient) Close() error {
|
||||
// Returns:
|
||||
// - Client: the Docker client connection.
|
||||
// - error: an error if the connection failed.
|
||||
func ConnectClient(host string) (Client, E.NestedError) {
|
||||
func ConnectClient(host string) (Client, E.Error) {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
@@ -18,17 +20,15 @@ type templateData struct {
|
||||
var loadingPage []byte
|
||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||
|
||||
const headerCheckRedirect = "X-Goproxy-Check-Redirect"
|
||||
|
||||
func (w *Watcher) makeLoadingPageBody() []byte {
|
||||
msg := fmt.Sprintf("%s is starting...", w.ContainerName)
|
||||
|
||||
data := new(templateData)
|
||||
data.CheckRedirectHeader = headerCheckRedirect
|
||||
data.CheckRedirectHeader = common.HeaderCheckRedirect
|
||||
data.Title = w.ContainerName
|
||||
data.Message = strings.ReplaceAll(msg, " ", " ")
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(headerCheckRedirect)))
|
||||
buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(common.HeaderCheckRedirect)))
|
||||
err := loadingPageTmpl.Execute(buf, data)
|
||||
if err != nil { // should never happen in production
|
||||
panic(err)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package idlewatcher
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
StopMethodKill StopMethod = "kill"
|
||||
)
|
||||
|
||||
func ValidateConfig(cont *docker.Container) (cfg *Config, res E.NestedError) {
|
||||
func ValidateConfig(cont *docker.Container) (cfg *Config, res E.Error) {
|
||||
if cont == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func ValidateConfig(cont *docker.Container) (cfg *Config, res E.NestedError) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateDurationPostitive(value string) (time.Duration, E.NestedError) {
|
||||
func validateDurationPostitive(value string) (time.Duration, E.Error) {
|
||||
d, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, E.Invalid("duration", value).With(err)
|
||||
@@ -91,7 +91,7 @@ func validateDurationPostitive(value string) (time.Duration, E.NestedError) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func validateSignal(s string) (Signal, E.NestedError) {
|
||||
func validateSignal(s string) (Signal, E.Error) {
|
||||
switch s {
|
||||
case "", "SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT",
|
||||
"INT", "TERM", "HUP", "QUIT":
|
||||
@@ -101,7 +101,7 @@ func validateSignal(s string) (Signal, E.NestedError) {
|
||||
return "", E.Invalid("signal", s)
|
||||
}
|
||||
|
||||
func validateStopMethod(s string) (StopMethod, E.NestedError) {
|
||||
func validateStopMethod(s string) (StopMethod, E.Error) {
|
||||
sm := StopMethod(s)
|
||||
switch sm {
|
||||
case StopMethodPause, StopMethodStop, StopMethodKill:
|
||||
14
internal/docker/idlewatcher/types/waker.go
Normal file
14
internal/docker/idlewatcher/types/waker.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type Waker interface {
|
||||
health.HealthMonitor
|
||||
http.Handler
|
||||
net.Stream
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
@@ -14,12 +14,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type Waker interface {
|
||||
health.HealthMonitor
|
||||
http.Handler
|
||||
net.Stream
|
||||
}
|
||||
|
||||
type waker struct {
|
||||
_ U.NoCopy
|
||||
|
||||
@@ -37,7 +31,7 @@ const (
|
||||
|
||||
// TODO: support stream
|
||||
|
||||
func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.NestedError) {
|
||||
func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
||||
hcCfg := entry.HealthCheckConfig()
|
||||
hcCfg.Timeout = idleWakerCheckTimeout
|
||||
|
||||
@@ -62,24 +56,26 @@ func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReversePr
|
||||
}
|
||||
|
||||
// lifetime should follow route provider
|
||||
func NewHTTPWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy) (Waker, E.NestedError) {
|
||||
func NewHTTPWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy) (Waker, E.Error) {
|
||||
return newWaker(providerSubTask, entry, rp, nil)
|
||||
}
|
||||
|
||||
func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Stream) (Waker, E.NestedError) {
|
||||
func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Stream) (Waker, E.Error) {
|
||||
return newWaker(providerSubTask, entry, nil, stream)
|
||||
}
|
||||
|
||||
// Start implements health.HealthMonitor.
|
||||
func (w *Watcher) Start(routeSubTask task.Task) E.NestedError {
|
||||
w.task.OnComplete("stop route", func() {
|
||||
routeSubTask.Parent().Finish("watcher stopped")
|
||||
func (w *Watcher) Start(routeSubTask task.Task) E.Error {
|
||||
routeSubTask.Finish("ignored")
|
||||
w.task.OnCancel("stop route", func() {
|
||||
routeSubTask.Parent().Finish(w.task.FinishCause())
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finish implements health.HealthMonitor.
|
||||
func (w *Watcher) Finish(reason string) {}
|
||||
func (w *Watcher) Finish(reason any) {
|
||||
}
|
||||
|
||||
// Name implements health.HealthMonitor.
|
||||
func (w *Watcher) Name() string {
|
||||
@@ -109,6 +105,7 @@ func (w *Watcher) Status() health.Status {
|
||||
healthy, _, err := w.hc.CheckHealth()
|
||||
switch {
|
||||
case err != nil:
|
||||
w.ready.Store(false)
|
||||
return health.StatusError
|
||||
case healthy:
|
||||
w.ready.Store(true)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
@@ -37,7 +38,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
accept := gphttp.GetAccept(r.Header)
|
||||
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
||||
|
||||
isCheckRedirect := r.Header.Get(headerCheckRedirect) != ""
|
||||
isCheckRedirect := r.Header.Get(common.HeaderCheckRedirect) != ""
|
||||
if !isCheckRedirect && acceptHTML {
|
||||
// Send a loading response to the client
|
||||
body := w.makeLoadingPageBody()
|
||||
@@ -56,14 +57,14 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
ctx, cancel := context.WithTimeoutCause(r.Context(), w.WakeTimeout, errors.New("wake timeout"))
|
||||
defer cancel()
|
||||
|
||||
checkCancelled := func() bool {
|
||||
checkCanceled := func() bool {
|
||||
select {
|
||||
case <-w.task.Context().Done():
|
||||
w.l.Debugf("wake cancelled: %s", context.Cause(w.task.Context()))
|
||||
w.l.Debugf("wake canceled: %s", context.Cause(w.task.Context()))
|
||||
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
w.l.Debugf("wake cancelled: %s", context.Cause(ctx))
|
||||
w.l.Debugf("wake canceled: %s", context.Cause(ctx))
|
||||
http.Error(rw, "Waking timed out", http.StatusGatewayTimeout)
|
||||
return true
|
||||
default:
|
||||
@@ -71,7 +72,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
}
|
||||
}
|
||||
|
||||
if checkCancelled() {
|
||||
if checkCanceled() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -84,14 +85,14 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
}
|
||||
|
||||
for {
|
||||
if checkCancelled() {
|
||||
if checkCanceled() {
|
||||
return false
|
||||
}
|
||||
|
||||
if w.Status() == health.StatusHealthy {
|
||||
w.resetIdleTimer()
|
||||
if isCheckRedirect {
|
||||
logrus.Debugf("container %s is ready, redirecting...", w.String())
|
||||
logrus.Debugf("container %s is ready, redirecting to %s ...", w.String(), w.hc.URL())
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,44 +8,47 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
// Setup implements types.Stream.
|
||||
func (w *Watcher) Addr() net.Addr {
|
||||
return w.stream.Addr()
|
||||
}
|
||||
|
||||
func (w *Watcher) Setup() error {
|
||||
return w.stream.Setup()
|
||||
}
|
||||
|
||||
// Accept implements types.Stream.
|
||||
func (w *Watcher) Accept() (conn types.StreamConn, err error) {
|
||||
func (w *Watcher) Accept() (conn net.Conn, err error) {
|
||||
conn, err = w.stream.Accept()
|
||||
// timeout means no connection is accepted
|
||||
var nErr *net.OpError
|
||||
ok := errors.As(err, &nErr)
|
||||
if ok && nErr.Timeout() {
|
||||
if err != nil {
|
||||
logrus.Errorf("accept failed with error: %s", err)
|
||||
return
|
||||
}
|
||||
if err := w.wakeFromStream(); err != nil {
|
||||
return nil, err
|
||||
w.l.Error(err)
|
||||
}
|
||||
return w.stream.Accept()
|
||||
}
|
||||
|
||||
// CloseListeners implements types.Stream.
|
||||
func (w *Watcher) CloseListeners() {
|
||||
w.stream.CloseListeners()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle implements types.Stream.
|
||||
func (w *Watcher) Handle(conn types.StreamConn) error {
|
||||
func (w *Watcher) Handle(conn net.Conn) error {
|
||||
if err := w.wakeFromStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.stream.Handle(conn)
|
||||
}
|
||||
|
||||
// Close implements types.Stream.
|
||||
func (w *Watcher) Close() error {
|
||||
return w.stream.Close()
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeFromStream() error {
|
||||
w.resetIdleTimer()
|
||||
|
||||
// pass through if container is already ready
|
||||
if w.ready.Load() {
|
||||
return nil
|
||||
@@ -66,11 +69,11 @@ func (w *Watcher) wakeFromStream() error {
|
||||
select {
|
||||
case <-w.task.Context().Done():
|
||||
cause := w.task.FinishCause()
|
||||
w.l.Debugf("wake cancelled: %s", cause)
|
||||
w.l.Debugf("wake canceled: %s", cause)
|
||||
return cause
|
||||
case <-ctx.Done():
|
||||
cause := context.Cause(ctx)
|
||||
w.l.Debugf("wake cancelled: %s", cause)
|
||||
w.l.Debugf("wake canceled: %s", cause)
|
||||
return cause
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/config"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
@@ -49,7 +49,7 @@ var (
|
||||
|
||||
const dockerReqTimeout = 3 * time.Second
|
||||
|
||||
func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker) (*Watcher, E.NestedError) {
|
||||
func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker) (*Watcher, E.Error) {
|
||||
failure := E.Failure("idle_watcher register")
|
||||
cfg := entry.IdlewatcherConfig()
|
||||
|
||||
@@ -66,6 +66,7 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
||||
w.Config = cfg
|
||||
w.waker = waker
|
||||
w.resetIdleTimer()
|
||||
providerSubtask.Finish("used existing watcher")
|
||||
return w, nil
|
||||
}
|
||||
|
||||
@@ -88,13 +89,11 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
||||
go func() {
|
||||
cause := w.watchUntilDestroy()
|
||||
|
||||
watcherMapMu.Lock()
|
||||
watcherMap.Delete(w.ContainerID)
|
||||
watcherMapMu.Unlock()
|
||||
|
||||
w.ticker.Stop()
|
||||
w.client.Close()
|
||||
w.task.Finish(cause.Error())
|
||||
w.task.Finish(cause)
|
||||
}()
|
||||
|
||||
return w, nil
|
||||
@@ -146,7 +145,7 @@ func (w *Watcher) wakeIfStopped() error {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), dockerReqTimeout)
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), w.WakeTimeout)
|
||||
defer cancel()
|
||||
|
||||
// !Hard coded here since theres no constants from Docker API
|
||||
@@ -175,7 +174,7 @@ func (w *Watcher) getStopCallback() StopCallback {
|
||||
panic("should not reach here")
|
||||
}
|
||||
return func() error {
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), dockerReqTimeout)
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.StopTimeout)*time.Second)
|
||||
defer cancel()
|
||||
return cb(ctx)
|
||||
}
|
||||
@@ -186,8 +185,8 @@ func (w *Watcher) resetIdleTimer() {
|
||||
w.ticker.Reset(w.IdleTimeout)
|
||||
}
|
||||
|
||||
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask task.Task, eventCh <-chan events.Event, errCh <-chan E.NestedError) {
|
||||
eventTask = w.task.Subtask("watcher for %s", w.ContainerID)
|
||||
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask task.Task, eventCh <-chan events.Event, errCh <-chan E.Error) {
|
||||
eventTask = w.task.Subtask("docker event watcher")
|
||||
eventCh, errCh = dockerWatcher.EventsWithOptions(eventTask.Context(), W.DockerListOptions{
|
||||
Filters: W.NewDockerFilter(
|
||||
W.DockerFilterContainer,
|
||||
@@ -218,13 +217,12 @@ func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask tas
|
||||
func (w *Watcher) watchUntilDestroy() error {
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
eventTask, dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
|
||||
defer eventTask.Finish("stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.task.Context().Done():
|
||||
cause := context.Cause(w.task.Context())
|
||||
w.l.Debugf("watcher stopped by context done: %s", cause)
|
||||
return cause
|
||||
return w.task.FinishCause()
|
||||
case err := <-dockerEventErrCh:
|
||||
if err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("docker watcher", err))
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func Inspect(dockerHost string, containerID string) (*Container, E.NestedError) {
|
||||
func Inspect(dockerHost string, containerID string) (*Container, E.Error) {
|
||||
client, err := ConnectClient(dockerHost)
|
||||
defer client.Close()
|
||||
|
||||
@@ -19,7 +19,7 @@ func Inspect(dockerHost string, containerID string) (*Container, E.NestedError)
|
||||
return client.Inspect(containerID)
|
||||
}
|
||||
|
||||
func (c Client) Inspect(containerID string) (*Container, E.NestedError) {
|
||||
func (c Client) Inspect(containerID string) (*Container, E.Error) {
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func (l *Label) String() string {
|
||||
//
|
||||
// Returns:
|
||||
// - error: an error if the field does not exist.
|
||||
func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||
func ApplyLabel[T any](obj *T, l *Label) E.Error {
|
||||
if obj == nil {
|
||||
return E.Invalid("nil object", l)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||
func ParseLabel(label string, value string) (*Label, E.Error) {
|
||||
parts := strings.Split(label, ".")
|
||||
|
||||
if len(parts) < 2 {
|
||||
|
||||
@@ -11,11 +11,6 @@ import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type ClientInfo struct {
|
||||
Client Client
|
||||
Containers []types.Container
|
||||
}
|
||||
|
||||
var listOptions = container.ListOptions{
|
||||
// created|restarting|running|removing|paused|exited|dead
|
||||
// Filters: filters.NewArgs(
|
||||
@@ -28,28 +23,21 @@ var listOptions = container.ListOptions{
|
||||
All: true,
|
||||
}
|
||||
|
||||
func GetClientInfo(clientHost string, getContainer bool) (*ClientInfo, E.NestedError) {
|
||||
func ListContainers(clientHost string) ([]types.Container, E.Error) {
|
||||
dockerClient, err := ConnectClient(clientHost)
|
||||
if err.HasError() {
|
||||
return nil, E.FailWith("connect to docker", err)
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker client connection timeout"))
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("list containers timeout"))
|
||||
defer cancel()
|
||||
|
||||
var containers []types.Container
|
||||
if getContainer {
|
||||
containers, err = E.Check(dockerClient.ContainerList(ctx, listOptions))
|
||||
if err.HasError() {
|
||||
return nil, E.FailWith("list containers", err)
|
||||
}
|
||||
containers, err := E.Check(dockerClient.ContainerList(ctx, listOptions))
|
||||
if err.HasError() {
|
||||
return nil, E.FailWith("list containers", err)
|
||||
}
|
||||
|
||||
return &ClientInfo{
|
||||
Client: dockerClient,
|
||||
Containers: containers,
|
||||
}, nil
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func IsErrConnectionFailed(err error) bool {
|
||||
Reference in New Issue
Block a user