Add auto scale down option (#405)
This commit is contained in:
@@ -14,6 +14,12 @@ Routes Minecraft client connections to backend servers based upon the requested
|
|||||||
The host:port bound for servicing API requests (env API_BINDING)
|
The host:port bound for servicing API requests (env API_BINDING)
|
||||||
-auto-scale-up
|
-auto-scale-up
|
||||||
Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP)
|
Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP)
|
||||||
|
-auto-scale-down
|
||||||
|
Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 after all backend connections have stopped and a configurable amount of delay has passed (env AUTO_SCALE_DOWN)
|
||||||
|
-auto-scale-down-after
|
||||||
|
String indicating how long an auto scale down should wait before scaling down a backend server. If a player rejoins the server during this delay, the scale down will be canceled (env AUTO_SCALE_DOWN_AFTER)
|
||||||
|
-auto-scale-allow-deny string
|
||||||
|
Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled (env AUTO_SCALE_ALLOW_DENY)
|
||||||
-clients-to-allow value
|
-clients-to-allow value
|
||||||
Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny. (env CLIENTS_TO_ALLOW)
|
Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny. (env CLIENTS_TO_ALLOW)
|
||||||
-clients-to-deny value
|
-clients-to-deny value
|
||||||
@@ -80,8 +86,6 @@ Routes Minecraft client connections to backend servers based upon the requested
|
|||||||
If set, a POST request that contains connection status notifications will be sent to this HTTP address (env WEBHOOK_URL)
|
If set, a POST request that contains connection status notifications will be sent to this HTTP address (env WEBHOOK_URL)
|
||||||
-record-logins
|
-record-logins
|
||||||
Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend (env RECORD_LOGINS)
|
Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend (env RECORD_LOGINS)
|
||||||
-auto-scale-up-allow-deny string
|
|
||||||
Path to config for server allowlists and denylists. If -auto-scale-up is enabled and a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up (env AUTO_SCALE_UP_ALLOW_DENY)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Multi-Architecture Image
|
## Docker Multi-Architecture Image
|
||||||
@@ -172,9 +176,9 @@ The following shows a JSON file for routes config, where `default-server` can al
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Auto Scale Up Allow/Deny List
|
## Auto Scale Allow/Deny List
|
||||||
|
|
||||||
The allow/deny list configuration allows limiting which players can scale up servers when using the `-auto-scale-up` option or the `AUTO_SCALE_UP` env variable. Global allow/deny lists can be configured that apply to all backend servers, but server-specific lists can be added as well. There are a few important things to note about the configuration:
|
The allow/deny list configuration allows limiting which players can scale up servers when using the `-auto-scale-up` option (`AUTO_SCALE_UP` env variable) and which players can cancel an active down scaler when using the `-auto-scale-down` option (`AUTO_SCALE_DOWN` env variable). Global allow/deny lists can be configured that apply to all backend servers, but server-specific lists can be added as well. There are a few important things to note about the configuration:
|
||||||
- The `mc-router` process will not automatically pick up changes to the config. If updates to the config are made, the router must be restarted.
|
- The `mc-router` process will not automatically pick up changes to the config. If updates to the config are made, the router must be restarted.
|
||||||
- Allowlists always take priority over denylists. This means if a player is included in a sever-specific allowlist and the global denylist, the player will still be considered allowed on that server. If a player is listed in both a global allowlist and denylist, the denylist entry will be ignored.
|
- Allowlists always take priority over denylists. This means if a player is included in a sever-specific allowlist and the global denylist, the player will still be considered allowed on that server. If a player is listed in both a global allowlist and denylist, the denylist entry will be ignored.
|
||||||
- Player entries only require a `uuid` or `name`. Both will be checked if specified, but otherwise a `uuid` will take priority over a `name`.
|
- Player entries only require a `uuid` or `name`. Both will be checked if specified, but otherwise a `uuid` will take priority over a `name`.
|
||||||
@@ -267,13 +271,13 @@ kubectl apply -f https://raw.githubusercontent.com/itzg/mc-router/master/docs/k8
|
|||||||
* I extended the allowed node port range by adding `--service-node-port-range=25000-32767`
|
* I extended the allowed node port range by adding `--service-node-port-range=25000-32767`
|
||||||
to `/etc/kubernetes/manifests/kube-apiserver.yaml`
|
to `/etc/kubernetes/manifests/kube-apiserver.yaml`
|
||||||
|
|
||||||
##### Auto Scale Up
|
##### Auto Scale Up/Down
|
||||||
|
|
||||||
The `-auto-scale-up` flag argument makes the router "wake up" any stopped backend servers, by changing `replicas: 0` to `replicas: 1`.
|
The `-auto-scale-up` flag argument makes the router "wake up" any stopped backend servers by changing `replicas: 0` to `replicas: 1`. The `-auto-scale-down` flag argument makes the router shut down any running backend servers with no active connections by changing `replicas: 1` to `replicas: 0`. The scale down will occur after a configurable (using the `-auto-scale-down-after` argument) waiting period, such as `10m` (10 minutes), `2h` (2 hours), etc. If any players connect to the server during this period the scale down will be canceled. It is recommended to set this value high enough so a temporary player disconnect will not immediately shut down the server (`1m` or higher).
|
||||||
|
|
||||||
This requires using `kind: StatefulSet` instead of `kind: Service` for the Minecraft backend servers.
|
Both options require using `kind: StatefulSet` instead of `kind: Service` for the Minecraft backend servers.
|
||||||
|
|
||||||
It also requires the `ClusterRole` to permit `get` + `update` for `statefulsets` & `statefulsets/scale`,
|
They also require the `ClusterRole` to permit `get` + `update` for `statefulsets` & `statefulsets/scale`,
|
||||||
e.g. like this (or some equivalent more fine-grained one to only watch/list services+statefulsets, and only get+update scale):
|
e.g. like this (or some equivalent more fine-grained one to only watch/list services+statefulsets, and only get+update scale):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
+25
-10
@@ -33,6 +33,13 @@ type WebhookConfig struct {
|
|||||||
RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"`
|
RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutoScale struct {
|
||||||
|
Up bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"`
|
||||||
|
Down bool `default:"false" usage:"Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 on respective backend servers after there are no connections"`
|
||||||
|
DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"`
|
||||||
|
AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"`
|
Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"`
|
||||||
Default string `usage:"host:port of a default Minecraft server to use when mapping not found"`
|
Default string `usage:"host:port of a default Minecraft server to use when mapping not found"`
|
||||||
@@ -44,7 +51,6 @@ type Config struct {
|
|||||||
ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"`
|
ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"`
|
||||||
InKubeCluster bool `usage:"Use in-cluster Kubernetes config"`
|
InKubeCluster bool `usage:"Use in-cluster Kubernetes config"`
|
||||||
KubeConfig string `usage:"The path to a Kubernetes configuration file"`
|
KubeConfig string `usage:"The path to a Kubernetes configuration file"`
|
||||||
AutoScaleUp bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"`
|
|
||||||
InDocker bool `usage:"Use Docker service discovery"`
|
InDocker bool `usage:"Use Docker service discovery"`
|
||||||
InDockerSwarm bool `usage:"Use Docker Swarm service discovery"`
|
InDockerSwarm bool `usage:"Use Docker Swarm service discovery"`
|
||||||
DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"`
|
DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"`
|
||||||
@@ -58,7 +64,7 @@ type Config struct {
|
|||||||
MetricsBackendConfig MetricsBackendConfig
|
MetricsBackendConfig MetricsBackendConfig
|
||||||
RoutesConfig string `usage:"Name or full path to routes config file"`
|
RoutesConfig string `usage:"Name or full path to routes config file"`
|
||||||
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
|
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
|
||||||
AutoScaleUpAllowDeny string `usage:"Path to config for server allowlists and denylists. If -auto-scale-up is enabled and a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up"`
|
AutoScale AutoScale
|
||||||
|
|
||||||
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
|
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
|
||||||
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
|
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
|
||||||
@@ -111,9 +117,9 @@ func main() {
|
|||||||
defer pprof.StopCPUProfile()
|
defer pprof.StopCPUProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
var autoScaleUpAllowDenyConfig *server.AllowDenyConfig = nil
|
var autoScaleAllowDenyConfig *server.AllowDenyConfig = nil
|
||||||
if config.AutoScaleUpAllowDeny != "" {
|
if config.AutoScale.AllowDeny != "" {
|
||||||
autoScaleUpAllowDenyConfig, err = server.ParseAllowDenyConfig(config.AutoScaleUpAllowDeny)
|
autoScaleAllowDenyConfig, err = server.ParseAllowDenyConfig(config.AutoScale.AllowDeny)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("trying to parse autoscale up allow-deny-list file")
|
logrus.WithError(err).Fatal("trying to parse autoscale up allow-deny-list file")
|
||||||
}
|
}
|
||||||
@@ -124,6 +130,15 @@ func main() {
|
|||||||
|
|
||||||
metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
|
metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
|
||||||
|
|
||||||
|
downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "")
|
||||||
|
downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("Unable to parse auto scale down after duration")
|
||||||
|
}
|
||||||
|
// Only one instance should be created
|
||||||
|
server.DownScaler = server.NewDownScaler(ctx, downScalerEnabled, downScalerDelay)
|
||||||
|
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
@@ -152,7 +167,7 @@ func main() {
|
|||||||
trustedIpNets = append(trustedIpNets, ipNet)
|
trustedIpNets = append(trustedIpNets, ipNet)
|
||||||
}
|
}
|
||||||
|
|
||||||
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig)
|
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig)
|
||||||
|
|
||||||
clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
|
clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -185,14 +200,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.InKubeCluster {
|
if config.InKubeCluster {
|
||||||
err = server.K8sWatcher.StartInCluster(config.AutoScaleUp)
|
err = server.K8sWatcher.StartInCluster(config.AutoScale.Up, config.AutoScale.Down)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
||||||
} else {
|
} else {
|
||||||
defer server.K8sWatcher.Stop()
|
defer server.K8sWatcher.Stop()
|
||||||
}
|
}
|
||||||
} else if config.KubeConfig != "" {
|
} else if config.KubeConfig != "" {
|
||||||
err := server.K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScaleUp)
|
err := server.K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
||||||
} else {
|
} else {
|
||||||
@@ -201,7 +216,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.InDocker {
|
if config.InDocker {
|
||||||
err = server.DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval)
|
err = server.DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Unable to start docker integration")
|
logrus.WithError(err).Fatal("Unable to start docker integration")
|
||||||
} else {
|
} else {
|
||||||
@@ -210,7 +225,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.InDockerSwarm {
|
if config.InDockerSwarm {
|
||||||
err = server.DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval)
|
err = server.DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Unable to start docker swarm integration")
|
logrus.WithError(err).Fatal("Unable to start docker swarm integration")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+29
-21
@@ -60,13 +60,14 @@ func (b expvarMetricsBuilder) Start(ctx context.Context) error {
|
|||||||
func (b expvarMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics {
|
func (b expvarMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics {
|
||||||
c := expvarMetrics.NewCounter("connections")
|
c := expvarMetrics.NewCounter("connections")
|
||||||
return &server.ConnectorMetrics{
|
return &server.ConnectorMetrics{
|
||||||
Errors: expvarMetrics.NewCounter("errors").With("subsystem", "connector"),
|
Errors: expvarMetrics.NewCounter("errors").With("subsystem", "connector"),
|
||||||
BytesTransmitted: expvarMetrics.NewCounter("bytes"),
|
BytesTransmitted: expvarMetrics.NewCounter("bytes"),
|
||||||
ConnectionsFrontend: c,
|
ConnectionsFrontend: c,
|
||||||
ConnectionsBackend: c,
|
ConnectionsBackend: c,
|
||||||
ActiveConnections: expvarMetrics.NewGauge("active_connections"),
|
ActiveConnections: expvarMetrics.NewGauge("active_connections"),
|
||||||
ServerActivePlayer: expvarMetrics.NewGauge("server_active_player"),
|
ServerActivePlayer: expvarMetrics.NewGauge("server_active_player"),
|
||||||
ServerLogins: expvarMetrics.NewCounter("server_logins"),
|
ServerLogins: expvarMetrics.NewCounter("server_logins"),
|
||||||
|
ServerActiveConnections: expvarMetrics.NewGauge("server_active_connections"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +81,14 @@ func (b discardMetricsBuilder) Start(ctx context.Context) error {
|
|||||||
|
|
||||||
func (b discardMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics {
|
func (b discardMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics {
|
||||||
return &server.ConnectorMetrics{
|
return &server.ConnectorMetrics{
|
||||||
Errors: discardMetrics.NewCounter(),
|
Errors: discardMetrics.NewCounter(),
|
||||||
BytesTransmitted: discardMetrics.NewCounter(),
|
BytesTransmitted: discardMetrics.NewCounter(),
|
||||||
ConnectionsFrontend: discardMetrics.NewCounter(),
|
ConnectionsFrontend: discardMetrics.NewCounter(),
|
||||||
ConnectionsBackend: discardMetrics.NewCounter(),
|
ConnectionsBackend: discardMetrics.NewCounter(),
|
||||||
ActiveConnections: discardMetrics.NewGauge(),
|
ActiveConnections: discardMetrics.NewGauge(),
|
||||||
ServerActivePlayer: discardMetrics.NewGauge(),
|
ServerActivePlayer: discardMetrics.NewGauge(),
|
||||||
ServerLogins: discardMetrics.NewCounter(),
|
ServerLogins: discardMetrics.NewCounter(),
|
||||||
|
ServerActiveConnections: discardMetrics.NewGauge(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,13 +133,14 @@ func (b *influxMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics
|
|||||||
|
|
||||||
c := metrics.NewCounter("mc_router_connections")
|
c := metrics.NewCounter("mc_router_connections")
|
||||||
return &server.ConnectorMetrics{
|
return &server.ConnectorMetrics{
|
||||||
Errors: metrics.NewCounter("mc_router_errors"),
|
Errors: metrics.NewCounter("mc_router_errors"),
|
||||||
BytesTransmitted: metrics.NewCounter("mc_router_transmitted_bytes"),
|
BytesTransmitted: metrics.NewCounter("mc_router_transmitted_bytes"),
|
||||||
ConnectionsFrontend: c.With("side", "frontend"),
|
ConnectionsFrontend: c.With("side", "frontend"),
|
||||||
ConnectionsBackend: c.With("side", "backend"),
|
ConnectionsBackend: c.With("side", "backend"),
|
||||||
ActiveConnections: metrics.NewGauge("mc_router_connections_active"),
|
ActiveConnections: metrics.NewGauge("mc_router_connections_active"),
|
||||||
ServerActivePlayer: metrics.NewGauge("mc_router_server_player_active"),
|
ServerActivePlayer: metrics.NewGauge("mc_router_server_player_active"),
|
||||||
ServerLogins: metrics.NewCounter("mc_router_server_logins"),
|
ServerLogins: metrics.NewCounter("mc_router_server_logins"),
|
||||||
|
ServerActiveConnections: metrics.NewGauge("mc_router_server_active_connections"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,5 +197,10 @@ func (b prometheusMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetri
|
|||||||
Name: "server_logins",
|
Name: "server_logins",
|
||||||
Help: "The total number of player logins",
|
Help: "The total number of player logins",
|
||||||
}, []string{"player_name", "player_uuid", "server_address"})),
|
}, []string{"player_name", "player_uuid", "server_address"})),
|
||||||
|
ServerActiveConnections: prometheusMetrics.NewGauge(promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "mc_router",
|
||||||
|
Name: "server_active_connections",
|
||||||
|
Help: "The number of active connections per server",
|
||||||
|
}, []string{"server_address"})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-26
@@ -30,13 +30,14 @@ const (
|
|||||||
var noDeadline time.Time
|
var noDeadline time.Time
|
||||||
|
|
||||||
type ConnectorMetrics struct {
|
type ConnectorMetrics struct {
|
||||||
Errors metrics.Counter
|
Errors metrics.Counter
|
||||||
BytesTransmitted metrics.Counter
|
BytesTransmitted metrics.Counter
|
||||||
ConnectionsFrontend metrics.Counter
|
ConnectionsFrontend metrics.Counter
|
||||||
ConnectionsBackend metrics.Counter
|
ConnectionsBackend metrics.Counter
|
||||||
ActiveConnections metrics.Gauge
|
ActiveConnections metrics.Gauge
|
||||||
ServerActivePlayer metrics.Gauge
|
ServerActivePlayer metrics.Gauge
|
||||||
ServerLogins metrics.Counter
|
ServerLogins metrics.Counter
|
||||||
|
ServerActiveConnections metrics.Gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientInfo struct {
|
type ClientInfo struct {
|
||||||
@@ -67,7 +68,48 @@ func (p *PlayerInfo) String() string {
|
|||||||
return fmt.Sprintf("%s/%s", p.Name, p.Uuid)
|
return fmt.Sprintf("%s/%s", p.Name, p.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerMetrics struct {
|
||||||
|
sync.RWMutex
|
||||||
|
activeConnections map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerMetrics() *ServerMetrics {
|
||||||
|
return &ServerMetrics{
|
||||||
|
activeConnections: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerMetrics) IncrementActiveConnections(serverAddress string) {
|
||||||
|
sm.Lock()
|
||||||
|
defer sm.Unlock()
|
||||||
|
if _, ok := sm.activeConnections[serverAddress]; !ok {
|
||||||
|
sm.activeConnections[serverAddress] = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sm.activeConnections[serverAddress] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerMetrics) DecrementActiveConnections(serverAddress string) {
|
||||||
|
sm.Lock()
|
||||||
|
defer sm.Unlock()
|
||||||
|
if activeConnections, ok := sm.activeConnections[serverAddress]; ok && activeConnections <= 0 {
|
||||||
|
sm.activeConnections[serverAddress] = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sm.activeConnections[serverAddress] -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerMetrics) ActiveConnectionsValue(serverAddress string) int {
|
||||||
|
sm.Lock()
|
||||||
|
defer sm.Unlock()
|
||||||
|
if activeConnections, ok := sm.activeConnections[serverAddress]; ok {
|
||||||
|
return activeConnections
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig) *Connector {
|
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig) *Connector {
|
||||||
|
|
||||||
return &Connector{
|
return &Connector{
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
sendProxyProto: sendProxyProto,
|
sendProxyProto: sendProxyProto,
|
||||||
@@ -76,6 +118,7 @@ func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyPr
|
|||||||
trustedProxyNets: trustedProxyNets,
|
trustedProxyNets: trustedProxyNets,
|
||||||
recordLogins: recordLogins,
|
recordLogins: recordLogins,
|
||||||
autoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig,
|
autoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig,
|
||||||
|
serverMetrics: NewServerMetrics(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +131,7 @@ type Connector struct {
|
|||||||
trustedProxyNets []*net.IPNet
|
trustedProxyNets []*net.IPNet
|
||||||
|
|
||||||
activeConnections int32
|
activeConnections int32
|
||||||
|
serverMetrics *ServerMetrics
|
||||||
connectionsCond *sync.Cond
|
connectionsCond *sync.Cond
|
||||||
ngrokToken string
|
ngrokToken string
|
||||||
clientFilter *ClientFilter
|
clientFilter *ClientFilter
|
||||||
@@ -348,10 +392,48 @@ func (c *Connector) readPlayerInfo(bufferedReader *bufio.Reader, clientAddr net.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Connector) cleanupBackendConnection(ctx context.Context, clientAddr net.Addr, serverAddress string, playerInfo *PlayerInfo, backendHostPort string, cleanupMetrics bool, checkScaleDown bool) {
|
||||||
|
if c.connectionNotifier != nil {
|
||||||
|
err := c.connectionNotifier.NotifyDisconnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Warn("failed to notify disconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleanupMetrics {
|
||||||
|
c.metrics.ActiveConnections.Set(float64(
|
||||||
|
atomic.AddInt32(&c.activeConnections, -1)))
|
||||||
|
|
||||||
|
c.serverMetrics.DecrementActiveConnections(serverAddress)
|
||||||
|
c.metrics.ServerActiveConnections.
|
||||||
|
With("server_address", serverAddress).
|
||||||
|
Set(float64(c.serverMetrics.ActiveConnectionsValue(serverAddress)))
|
||||||
|
|
||||||
|
if c.recordLogins && playerInfo != nil {
|
||||||
|
c.metrics.ServerActivePlayer.
|
||||||
|
With("player_name", playerInfo.Name).
|
||||||
|
With("player_uuid", playerInfo.Uuid.String()).
|
||||||
|
With("server_address", serverAddress).
|
||||||
|
Set(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if checkScaleDown && c.serverMetrics.ActiveConnectionsValue(serverAddress) <= 0 {
|
||||||
|
DownScaler.Begin(serverAddress)
|
||||||
|
}
|
||||||
|
c.connectionsCond.Signal()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.Conn,
|
func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.Conn,
|
||||||
clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) {
|
clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) {
|
||||||
|
|
||||||
backendHostPort, resolvedHost, waker := Routes.FindBackendForServerAddress(ctx, serverAddress)
|
backendHostPort, resolvedHost, waker, _ := Routes.FindBackendForServerAddress(ctx, serverAddress)
|
||||||
|
cleanupMetrics := false
|
||||||
|
cleanupCheckScaleDown := false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
c.cleanupBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, cleanupMetrics, cleanupCheckScaleDown)
|
||||||
|
}()
|
||||||
|
|
||||||
if waker != nil && nextState > mcproto.StateStatus {
|
if waker != nil && nextState > mcproto.StateStatus {
|
||||||
serverAllowsPlayer := c.autoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo)
|
serverAllowsPlayer := c.autoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo)
|
||||||
logrus.
|
logrus.
|
||||||
@@ -361,6 +443,9 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
|
|||||||
WithField("serverAllowsPlayer", serverAllowsPlayer).
|
WithField("serverAllowsPlayer", serverAllowsPlayer).
|
||||||
Debug("checked if player is allowed to wake up the server")
|
Debug("checked if player is allowed to wake up the server")
|
||||||
if serverAllowsPlayer {
|
if serverAllowsPlayer {
|
||||||
|
// Cancel down scaler if active before scale up
|
||||||
|
DownScaler.Cancel(serverAddress)
|
||||||
|
cleanupCheckScaleDown = true
|
||||||
if err := waker(ctx); err != nil {
|
if err := waker(ctx); err != nil {
|
||||||
logrus.WithFields(logrus.Fields{"serverAddress": serverAddress}).WithError(err).Error("failed to wake up backend")
|
logrus.WithFields(logrus.Fields{"serverAddress": serverAddress}).WithError(err).Error("failed to wake up backend")
|
||||||
c.metrics.Errors.With("type", "wakeup_failed").Add(1)
|
c.metrics.Errors.With("type", "wakeup_failed").Add(1)
|
||||||
@@ -426,6 +511,12 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
|
|||||||
|
|
||||||
c.metrics.ActiveConnections.Set(float64(
|
c.metrics.ActiveConnections.Set(float64(
|
||||||
atomic.AddInt32(&c.activeConnections, 1)))
|
atomic.AddInt32(&c.activeConnections, 1)))
|
||||||
|
|
||||||
|
c.serverMetrics.IncrementActiveConnections(serverAddress)
|
||||||
|
c.metrics.ServerActiveConnections.
|
||||||
|
With("server_address", serverAddress).
|
||||||
|
Set(float64(c.serverMetrics.ActiveConnectionsValue(serverAddress)))
|
||||||
|
|
||||||
if c.recordLogins && playerInfo != nil {
|
if c.recordLogins && playerInfo != nil {
|
||||||
logrus.
|
logrus.
|
||||||
WithField("client", clientAddr).
|
WithField("client", clientAddr).
|
||||||
@@ -446,24 +537,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
|
|||||||
Add(1)
|
Add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
cleanupMetrics = true
|
||||||
if c.connectionNotifier != nil {
|
|
||||||
err := c.connectionNotifier.NotifyDisconnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Warn("failed to notify disconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.metrics.ActiveConnections.Set(float64(
|
|
||||||
atomic.AddInt32(&c.activeConnections, -1)))
|
|
||||||
if c.recordLogins && playerInfo != nil {
|
|
||||||
c.metrics.ServerActivePlayer.
|
|
||||||
With("player_name", playerInfo.Name).
|
|
||||||
With("player_uuid", playerInfo.Uuid.String()).
|
|
||||||
With("server_address", serverAddress).
|
|
||||||
Set(0)
|
|
||||||
}
|
|
||||||
c.connectionsCond.Signal()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// PROXY protocol implementation
|
// PROXY protocol implementation
|
||||||
if c.sendProxyProto {
|
if c.sendProxyProto {
|
||||||
|
|||||||
+25
-6
@@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IDockerWatcher interface {
|
type IDockerWatcher interface {
|
||||||
Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error
|
Start(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool) error
|
||||||
Stop()
|
Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,19 +31,38 @@ var DockerWatcher IDockerWatcher = &dockerWatcherImpl{}
|
|||||||
|
|
||||||
type dockerWatcherImpl struct {
|
type dockerWatcherImpl struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
autoScaleUp bool
|
||||||
|
autoScaleDown bool
|
||||||
client *client.Client
|
client *client.Client
|
||||||
contextCancel context.CancelFunc
|
contextCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) func(ctx context.Context) error {
|
func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) ScalerFunc {
|
||||||
|
if !w.autoScaleUp {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
logrus.Fatal("Auto scale up is not yet supported for docker")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error {
|
func (w *dockerWatcherImpl) makeSleeperFunc(_ *routableContainer) ScalerFunc {
|
||||||
|
if !w.autoScaleDown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
logrus.Fatal("Auto scale down is not yet supported for docker")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
w.autoScaleUp = autoScaleUp
|
||||||
|
w.autoScaleDown = autoScaleDown
|
||||||
|
|
||||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||||
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
|
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
|
||||||
|
|
||||||
@@ -75,7 +94,7 @@ func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshInte
|
|||||||
for _, c := range initialContainers {
|
for _, c := range initialContainers {
|
||||||
containerMap[c.externalContainerName] = c
|
containerMap[c.externalContainerName] = c
|
||||||
if c.externalContainerName != "" {
|
if c.externalContainerName != "" {
|
||||||
Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, w.makeWakerFunc(c))
|
Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, w.makeWakerFunc(c), w.makeSleeperFunc(c))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(c.containerEndpoint)
|
Routes.SetDefaultRoute(c.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -97,7 +116,7 @@ func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshInte
|
|||||||
containerMap[rs.externalContainerName] = rs
|
containerMap[rs.externalContainerName] = rs
|
||||||
logrus.WithField("routableContainer", rs).Debug("ADD")
|
logrus.WithField("routableContainer", rs).Debug("ADD")
|
||||||
if rs.externalContainerName != "" {
|
if rs.externalContainerName != "" {
|
||||||
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs))
|
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(rs.containerEndpoint)
|
Routes.SetDefaultRoute(rs.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -105,7 +124,7 @@ func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshInte
|
|||||||
containerMap[rs.externalContainerName] = rs
|
containerMap[rs.externalContainerName] = rs
|
||||||
if rs.externalContainerName != "" {
|
if rs.externalContainerName != "" {
|
||||||
Routes.DeleteMapping(rs.externalContainerName)
|
Routes.DeleteMapping(rs.externalContainerName)
|
||||||
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs))
|
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(rs.containerEndpoint)
|
Routes.SetDefaultRoute(rs.containerEndpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-5
@@ -23,19 +23,38 @@ var DockerSwarmWatcher IDockerWatcher = &dockerSwarmWatcherImpl{}
|
|||||||
|
|
||||||
type dockerSwarmWatcherImpl struct {
|
type dockerSwarmWatcherImpl struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
autoScaleUp bool
|
||||||
|
autoScaleDown bool
|
||||||
client *client.Client
|
client *client.Client
|
||||||
contextCancel context.CancelFunc
|
contextCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *dockerSwarmWatcherImpl) makeWakerFunc(_ *routableService) func(ctx context.Context) error {
|
func (w *dockerSwarmWatcherImpl) makeWakerFunc(_ *routableService) ScalerFunc {
|
||||||
|
if !w.autoScaleUp {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
logrus.Fatal("Auto scale up is not yet supported for docker swarm")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error {
|
func (w *dockerSwarmWatcherImpl) makeSleeperFunc(_ *routableService) ScalerFunc {
|
||||||
|
if !w.autoScaleDown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
logrus.Fatal("Auto scale down is not yet supported for docker swarm")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
w.autoScaleUp = autoScaleUp
|
||||||
|
w.autoScaleDown = autoScaleDown
|
||||||
|
|
||||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||||
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
|
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
|
||||||
|
|
||||||
@@ -67,7 +86,7 @@ func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refres
|
|||||||
for _, s := range initialServices {
|
for _, s := range initialServices {
|
||||||
serviceMap[s.externalServiceName] = s
|
serviceMap[s.externalServiceName] = s
|
||||||
if s.externalServiceName != "" {
|
if s.externalServiceName != "" {
|
||||||
Routes.CreateMapping(s.externalServiceName, s.containerEndpoint, w.makeWakerFunc(s))
|
Routes.CreateMapping(s.externalServiceName, s.containerEndpoint, w.makeWakerFunc(s), w.makeSleeperFunc(s))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(s.containerEndpoint)
|
Routes.SetDefaultRoute(s.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -89,7 +108,7 @@ func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refres
|
|||||||
serviceMap[rs.externalServiceName] = rs
|
serviceMap[rs.externalServiceName] = rs
|
||||||
logrus.WithField("routableService", rs).Debug("ADD")
|
logrus.WithField("routableService", rs).Debug("ADD")
|
||||||
if rs.externalServiceName != "" {
|
if rs.externalServiceName != "" {
|
||||||
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs))
|
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(rs.containerEndpoint)
|
Routes.SetDefaultRoute(rs.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -97,7 +116,7 @@ func (w *dockerSwarmWatcherImpl) Start(socket string, timeoutSeconds int, refres
|
|||||||
serviceMap[rs.externalServiceName] = rs
|
serviceMap[rs.externalServiceName] = rs
|
||||||
if rs.externalServiceName != "" {
|
if rs.externalServiceName != "" {
|
||||||
Routes.DeleteMapping(rs.externalServiceName)
|
Routes.DeleteMapping(rs.externalServiceName)
|
||||||
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs))
|
Routes.CreateMapping(rs.externalServiceName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(rs.containerEndpoint)
|
Routes.SetDefaultRoute(rs.containerEndpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IDownScaler interface {
|
||||||
|
Reset()
|
||||||
|
Begin(serverAddress string)
|
||||||
|
Cancel(serverAddress string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var DownScaler IDownScaler
|
||||||
|
|
||||||
|
func NewDownScaler(ctx context.Context, enabled bool, delay time.Duration) IDownScaler {
|
||||||
|
ds := &downScalerImpl{
|
||||||
|
enabled: enabled,
|
||||||
|
delay: delay,
|
||||||
|
parentContext: ctx,
|
||||||
|
contextCancellations: make(map[string]context.CancelFunc),
|
||||||
|
}
|
||||||
|
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
type downScalerImpl struct {
|
||||||
|
sync.RWMutex
|
||||||
|
enabled bool
|
||||||
|
delay time.Duration
|
||||||
|
parentContext context.Context
|
||||||
|
contextCancellations map[string]context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *downScalerImpl) Reset() {
|
||||||
|
// Cancel all existing scale down routines
|
||||||
|
for _, scaleDownCancel := range ds.contextCancellations {
|
||||||
|
scaleDownCancel()
|
||||||
|
}
|
||||||
|
ds.contextCancellations = make(map[string]context.CancelFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *downScalerImpl) Begin(serverAddress string) {
|
||||||
|
ds.Lock()
|
||||||
|
defer ds.Unlock()
|
||||||
|
|
||||||
|
if !ds.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an existing scale down routine exists, cancel it
|
||||||
|
if scaleDownCancel, ok := ds.contextCancellations[serverAddress]; ok {
|
||||||
|
scaleDownCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithField("serverAddress", serverAddress).Debug("Beginning scale down")
|
||||||
|
scaleDownContext, scaleDownContextCancellation := context.WithCancel(ds.parentContext)
|
||||||
|
ds.contextCancellations[serverAddress] = scaleDownContextCancellation
|
||||||
|
go ds.scaleDown(scaleDownContext, serverAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *downScalerImpl) Cancel(serverAddress string) {
|
||||||
|
ds.Lock()
|
||||||
|
defer ds.Unlock()
|
||||||
|
|
||||||
|
if !ds.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if scaleDownContextCancellation, ok := ds.contextCancellations[serverAddress]; ok {
|
||||||
|
logrus.WithField("serverAddress", serverAddress).Debug("Canceling scale down")
|
||||||
|
scaleDownContextCancellation()
|
||||||
|
delete(ds.contextCancellations, serverAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *downScalerImpl) scaleDown(ctx context.Context, serverAddress string) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(ds.delay):
|
||||||
|
_, _, _, sleeper := Routes.FindBackendForServerAddress(ctx, serverAddress)
|
||||||
|
if sleeper == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sleeper(ctx); err != nil {
|
||||||
|
logrus.WithField("serverAddress", serverAddress).WithError(err).Error("failed to scale down backend")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-17
@@ -27,8 +27,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IK8sWatcher interface {
|
type IK8sWatcher interface {
|
||||||
StartWithConfig(kubeConfigFile string, autoScaleUp bool) error
|
StartWithConfig(kubeConfigFile string, autoScaleUp bool, autoScaleDown bool) error
|
||||||
StartInCluster(autoScaleUp bool) error
|
StartInCluster(autoScaleUp bool, autoScaleDown bool) error
|
||||||
Stop()
|
Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,8 @@ var K8sWatcher IK8sWatcher = &k8sWatcherImpl{}
|
|||||||
|
|
||||||
type k8sWatcherImpl struct {
|
type k8sWatcherImpl struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
autoScaleUp bool
|
||||||
|
autoScaleDown bool
|
||||||
// The key in mappings is a Service, and the value the StatefulSet name
|
// The key in mappings is a Service, and the value the StatefulSet name
|
||||||
mappings map[string]string
|
mappings map[string]string
|
||||||
|
|
||||||
@@ -43,26 +45,28 @@ type k8sWatcherImpl struct {
|
|||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *k8sWatcherImpl) StartInCluster(autoScaleUp bool) error {
|
func (w *k8sWatcherImpl) StartInCluster(autoScaleUp bool, autoScaleDown bool) error {
|
||||||
config, err := rest.InClusterConfig()
|
config, err := rest.InClusterConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Unable to load in-cluster config")
|
return errors.Wrap(err, "Unable to load in-cluster config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.startWithLoadedConfig(config, autoScaleUp)
|
return w.startWithLoadedConfig(config, autoScaleUp, autoScaleDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *k8sWatcherImpl) StartWithConfig(kubeConfigFile string, autoScaleUp bool) error {
|
func (w *k8sWatcherImpl) StartWithConfig(kubeConfigFile string, autoScaleUp bool, autoScaleDown bool) error {
|
||||||
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile)
|
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Could not load kube config file")
|
return errors.Wrap(err, "Could not load kube config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.startWithLoadedConfig(config, autoScaleUp)
|
return w.startWithLoadedConfig(config, autoScaleUp, autoScaleDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config, autoScaleUp bool) error {
|
func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config, autoScaleUp bool, autoScaleDown bool) error {
|
||||||
w.stop = make(chan struct{}, 1)
|
w.stop = make(chan struct{}, 1)
|
||||||
|
w.autoScaleUp = autoScaleUp
|
||||||
|
w.autoScaleDown = autoScaleDown
|
||||||
|
|
||||||
clientset, err := kubernetes.NewForConfig(config)
|
clientset, err := kubernetes.NewForConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,7 +92,7 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config, autoScaleUp
|
|||||||
go serviceController.Run(w.stop)
|
go serviceController.Run(w.stop)
|
||||||
|
|
||||||
w.mappings = make(map[string]string)
|
w.mappings = make(map[string]string)
|
||||||
if autoScaleUp {
|
if autoScaleUp || autoScaleDown {
|
||||||
_, statefulSetController := cache.NewInformer(
|
_, statefulSetController := cache.NewInformer(
|
||||||
cache.NewListWatchFromClient(
|
cache.NewListWatchFromClient(
|
||||||
clientset.AppsV1().RESTClient(),
|
clientset.AppsV1().RESTClient(),
|
||||||
@@ -156,7 +160,7 @@ func (w *k8sWatcherImpl) handleUpdate(oldObj interface{}, newObj interface{}) {
|
|||||||
"new": newRoutableService,
|
"new": newRoutableService,
|
||||||
}).Debug("UPDATE")
|
}).Debug("UPDATE")
|
||||||
if newRoutableService.externalServiceName != "" {
|
if newRoutableService.externalServiceName != "" {
|
||||||
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint, newRoutableService.autoScaleUp)
|
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint, newRoutableService.autoScaleUp, newRoutableService.autoScaleDown)
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(newRoutableService.containerEndpoint)
|
Routes.SetDefaultRoute(newRoutableService.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -187,7 +191,7 @@ func (w *k8sWatcherImpl) handleAdd(obj interface{}) {
|
|||||||
logrus.WithField("routableService", routableService).Debug("ADD")
|
logrus.WithField("routableService", routableService).Debug("ADD")
|
||||||
|
|
||||||
if routableService.externalServiceName != "" {
|
if routableService.externalServiceName != "" {
|
||||||
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint, routableService.autoScaleUp)
|
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint, routableService.autoScaleUp, routableService.autoScaleDown)
|
||||||
} else {
|
} else {
|
||||||
Routes.SetDefaultRoute(routableService.containerEndpoint)
|
Routes.SetDefaultRoute(routableService.containerEndpoint)
|
||||||
}
|
}
|
||||||
@@ -204,7 +208,8 @@ func (w *k8sWatcherImpl) Stop() {
|
|||||||
type routableService struct {
|
type routableService struct {
|
||||||
externalServiceName string
|
externalServiceName string
|
||||||
containerEndpoint string
|
containerEndpoint string
|
||||||
autoScaleUp func(ctx context.Context) error
|
autoScaleUp ScalerFunc
|
||||||
|
autoScaleDown ScalerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// obj is expected to be a *v1.Service
|
// obj is expected to be a *v1.Service
|
||||||
@@ -239,12 +244,19 @@ func (w *k8sWatcherImpl) buildDetails(service *core.Service, externalServiceName
|
|||||||
rs := &routableService{
|
rs := &routableService{
|
||||||
externalServiceName: externalServiceName,
|
externalServiceName: externalServiceName,
|
||||||
containerEndpoint: net.JoinHostPort(clusterIp, port),
|
containerEndpoint: net.JoinHostPort(clusterIp, port),
|
||||||
autoScaleUp: w.buildScaleUpFunction(service),
|
autoScaleUp: w.buildScaleFunction(service, 0, 1),
|
||||||
|
autoScaleDown: w.buildScaleFunction(service, 1, 0),
|
||||||
}
|
}
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *k8sWatcherImpl) buildScaleUpFunction(service *core.Service) func(ctx context.Context) error {
|
func (w *k8sWatcherImpl) buildScaleFunction(service *core.Service, from int32, to int32) ScalerFunc {
|
||||||
|
if from <= to && !w.autoScaleUp {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if from >= to && !w.autoScaleDown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
serviceName := service.Name
|
serviceName := service.Name
|
||||||
if statefulSetName, exists := w.mappings[serviceName]; exists {
|
if statefulSetName, exists := w.mappings[serviceName]; exists {
|
||||||
@@ -255,7 +267,7 @@ func (w *k8sWatcherImpl) buildScaleUpFunction(service *core.Service) func(ctx co
|
|||||||
"statefulSet": statefulSetName,
|
"statefulSet": statefulSetName,
|
||||||
"replicas": replicas,
|
"replicas": replicas,
|
||||||
}).Debug("StatefulSet of Service Replicas")
|
}).Debug("StatefulSet of Service Replicas")
|
||||||
if replicas == 0 {
|
if replicas == from {
|
||||||
if _, err := w.clientset.AppsV1().StatefulSets(service.Namespace).UpdateScale(ctx, statefulSetName, &autoscaling.Scale{
|
if _, err := w.clientset.AppsV1().StatefulSets(service.Namespace).UpdateScale(ctx, statefulSetName, &autoscaling.Scale{
|
||||||
ObjectMeta: meta.ObjectMeta{
|
ObjectMeta: meta.ObjectMeta{
|
||||||
Name: scale.Name,
|
Name: scale.Name,
|
||||||
@@ -263,15 +275,15 @@ func (w *k8sWatcherImpl) buildScaleUpFunction(service *core.Service) func(ctx co
|
|||||||
UID: scale.UID,
|
UID: scale.UID,
|
||||||
ResourceVersion: scale.ResourceVersion,
|
ResourceVersion: scale.ResourceVersion,
|
||||||
},
|
},
|
||||||
Spec: autoscaling.ScaleSpec{Replicas: 1}}, meta.UpdateOptions{},
|
Spec: autoscaling.ScaleSpec{Replicas: to}}, meta.UpdateOptions{},
|
||||||
); err == nil {
|
); err == nil {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"service": serviceName,
|
"service": serviceName,
|
||||||
"statefulSet": statefulSetName,
|
"statefulSet": statefulSetName,
|
||||||
"replicas": replicas,
|
"replicas": replicas,
|
||||||
}).Info("StatefulSet Replicas Autoscaled from 0 to 1 (wake up)")
|
}).Infof("StatefulSet Replicas Autoscaled from %d to %d", from, to)
|
||||||
} else {
|
} else {
|
||||||
return errors.Wrap(err, "UpdateScale for Replicas=1 failed for StatefulSet: "+statefulSetName)
|
return errors.Wrapf(err, "UpdateScale for Replicas=%d failed for StatefulSet: %s", to, statefulSetName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+9
-4
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -78,6 +79,8 @@ func TestK8sWatcherImpl_handleAddThenUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// DownScaler needs to be instantiated
|
||||||
|
DownScaler = NewDownScaler(context.Background(), false, 1 * time.Second)
|
||||||
Routes.Reset()
|
Routes.Reset()
|
||||||
|
|
||||||
watcher := &k8sWatcherImpl{}
|
watcher := &k8sWatcherImpl{}
|
||||||
@@ -87,7 +90,7 @@ func TestK8sWatcherImpl_handleAddThenUpdate(t *testing.T) {
|
|||||||
|
|
||||||
watcher.handleAdd(&initialSvc)
|
watcher.handleAdd(&initialSvc)
|
||||||
for _, s := range test.initial.scenarios {
|
for _, s := range test.initial.scenarios {
|
||||||
backend, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
backend, _, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
||||||
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ func TestK8sWatcherImpl_handleAddThenUpdate(t *testing.T) {
|
|||||||
|
|
||||||
watcher.handleUpdate(&initialSvc, &updatedSvc)
|
watcher.handleUpdate(&initialSvc, &updatedSvc)
|
||||||
for _, s := range test.update.scenarios {
|
for _, s := range test.update.scenarios {
|
||||||
backend, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
backend, _, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
||||||
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -149,6 +152,8 @@ func TestK8sWatcherImpl_handleAddThenDelete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// DownScaler needs to be instantiated
|
||||||
|
DownScaler = NewDownScaler(context.Background(), false, 1 * time.Second)
|
||||||
Routes.Reset()
|
Routes.Reset()
|
||||||
|
|
||||||
watcher := &k8sWatcherImpl{}
|
watcher := &k8sWatcherImpl{}
|
||||||
@@ -158,13 +163,13 @@ func TestK8sWatcherImpl_handleAddThenDelete(t *testing.T) {
|
|||||||
|
|
||||||
watcher.handleAdd(&initialSvc)
|
watcher.handleAdd(&initialSvc)
|
||||||
for _, s := range test.initial.scenarios {
|
for _, s := range test.initial.scenarios {
|
||||||
backend, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
backend, _, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
||||||
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher.handleDelete(&initialSvc)
|
watcher.handleDelete(&initialSvc)
|
||||||
for _, s := range test.delete {
|
for _, s := range test.delete {
|
||||||
backend, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
backend, _, _, _ := Routes.FindBackendForServerAddress(context.Background(), s.given)
|
||||||
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+22
-10
@@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ScalerFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
var EmptyScalerFunc = func(ctx context.Context) error { return nil }
|
||||||
|
|
||||||
var tcpShieldPattern = regexp.MustCompile("///.*")
|
var tcpShieldPattern = regexp.MustCompile("///.*")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -70,7 +74,7 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Routes.CreateMapping(definition.ServerAddress, definition.Backend, func(ctx context.Context) error { return nil })
|
Routes.CreateMapping(definition.ServerAddress, definition.Backend, EmptyScalerFunc, EmptyScalerFunc)
|
||||||
RoutesConfig.AddMapping(definition.ServerAddress, definition.Backend)
|
RoutesConfig.AddMapping(definition.ServerAddress, definition.Backend)
|
||||||
writer.WriteHeader(http.StatusCreated)
|
writer.WriteHeader(http.StatusCreated)
|
||||||
}
|
}
|
||||||
@@ -102,10 +106,11 @@ type IRoutes interface {
|
|||||||
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
|
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
|
||||||
// Otherwise, an empty string is returned. Also returns the normalized version of the given serverAddress.
|
// Otherwise, an empty string is returned. Also returns the normalized version of the given serverAddress.
|
||||||
// The 3rd value returned is an (optional) "waker" function which a caller must invoke to wake up serverAddress.
|
// The 3rd value returned is an (optional) "waker" function which a caller must invoke to wake up serverAddress.
|
||||||
FindBackendForServerAddress(ctx context.Context, serverAddress string) (string, string, func(ctx context.Context) error)
|
// The 4th value returned is an (optional) "sleeper" function which a caller must invoke to shut down serverAddress.
|
||||||
|
FindBackendForServerAddress(ctx context.Context, serverAddress string) (string, string, ScalerFunc, ScalerFunc)
|
||||||
GetMappings() map[string]string
|
GetMappings() map[string]string
|
||||||
DeleteMapping(serverAddress string) bool
|
DeleteMapping(serverAddress string) bool
|
||||||
CreateMapping(serverAddress string, backend string, waker func(ctx context.Context) error)
|
CreateMapping(serverAddress string, backend string, waker ScalerFunc, sleeper ScalerFunc)
|
||||||
SetDefaultRoute(backend string)
|
SetDefaultRoute(backend string)
|
||||||
SimplifySRV(srvEnabled bool)
|
SimplifySRV(srvEnabled bool)
|
||||||
}
|
}
|
||||||
@@ -122,13 +127,14 @@ func NewRoutes() IRoutes {
|
|||||||
|
|
||||||
func (r *routesImpl) RegisterAll(mappings map[string]string) {
|
func (r *routesImpl) RegisterAll(mappings map[string]string) {
|
||||||
for k, v := range mappings {
|
for k, v := range mappings {
|
||||||
r.CreateMapping(k, v, func(ctx context.Context) error { return nil })
|
r.CreateMapping(k, v, EmptyScalerFunc, EmptyScalerFunc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mapping struct {
|
type mapping struct {
|
||||||
backend string
|
backend string
|
||||||
waker func(ctx context.Context) error
|
waker ScalerFunc
|
||||||
|
sleeper ScalerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type routesImpl struct {
|
type routesImpl struct {
|
||||||
@@ -140,6 +146,7 @@ type routesImpl struct {
|
|||||||
|
|
||||||
func (r *routesImpl) Reset() {
|
func (r *routesImpl) Reset() {
|
||||||
r.mappings = make(map[string]mapping)
|
r.mappings = make(map[string]mapping)
|
||||||
|
DownScaler.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *routesImpl) SetDefaultRoute(backend string) {
|
func (r *routesImpl) SetDefaultRoute(backend string) {
|
||||||
@@ -154,7 +161,7 @@ func (r *routesImpl) SimplifySRV(srvEnabled bool) {
|
|||||||
r.simplifySRV = srvEnabled
|
r.simplifySRV = srvEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *routesImpl) FindBackendForServerAddress(_ context.Context, serverAddress string) (string, string, func(ctx context.Context) error) {
|
func (r *routesImpl) FindBackendForServerAddress(_ context.Context, serverAddress string) (string, string, ScalerFunc, ScalerFunc) {
|
||||||
r.RLock()
|
r.RLock()
|
||||||
defer r.RUnlock()
|
defer r.RUnlock()
|
||||||
|
|
||||||
@@ -190,10 +197,10 @@ func (r *routesImpl) FindBackendForServerAddress(_ context.Context, serverAddres
|
|||||||
|
|
||||||
if r.mappings != nil {
|
if r.mappings != nil {
|
||||||
if mapping, exists := r.mappings[serverAddress]; exists {
|
if mapping, exists := r.mappings[serverAddress]; exists {
|
||||||
return mapping.backend, serverAddress, mapping.waker
|
return mapping.backend, serverAddress, mapping.waker, mapping.sleeper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r.defaultRoute, serverAddress, nil
|
return r.defaultRoute, serverAddress, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *routesImpl) GetMappings() map[string]string {
|
func (r *routesImpl) GetMappings() map[string]string {
|
||||||
@@ -212,6 +219,8 @@ func (r *routesImpl) DeleteMapping(serverAddress string) bool {
|
|||||||
defer r.Unlock()
|
defer r.Unlock()
|
||||||
logrus.WithField("serverAddress", serverAddress).Info("Deleting route")
|
logrus.WithField("serverAddress", serverAddress).Info("Deleting route")
|
||||||
|
|
||||||
|
DownScaler.Cancel(serverAddress)
|
||||||
|
|
||||||
if _, ok := r.mappings[serverAddress]; ok {
|
if _, ok := r.mappings[serverAddress]; ok {
|
||||||
delete(r.mappings, serverAddress)
|
delete(r.mappings, serverAddress)
|
||||||
return true
|
return true
|
||||||
@@ -220,7 +229,7 @@ func (r *routesImpl) DeleteMapping(serverAddress string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *routesImpl) CreateMapping(serverAddress string, backend string, waker func(ctx context.Context) error) {
|
func (r *routesImpl) CreateMapping(serverAddress string, backend string, waker ScalerFunc, sleeper ScalerFunc) {
|
||||||
r.Lock()
|
r.Lock()
|
||||||
defer r.Unlock()
|
defer r.Unlock()
|
||||||
|
|
||||||
@@ -230,5 +239,8 @@ func (r *routesImpl) CreateMapping(serverAddress string, backend string, waker f
|
|||||||
"serverAddress": serverAddress,
|
"serverAddress": serverAddress,
|
||||||
"backend": backend,
|
"backend": backend,
|
||||||
}).Info("Created route mapping")
|
}).Info("Created route mapping")
|
||||||
r.mappings[serverAddress] = mapping{backend: backend, waker: waker}
|
r.mappings[serverAddress] = mapping{backend: backend, waker: waker, sleeper: sleeper}
|
||||||
|
|
||||||
|
// Trigger auto scale down when mapping is created to ensure servers are shut down if router restarts
|
||||||
|
DownScaler.Begin(serverAddress)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ func Test_routesImpl_FindBackendForServerAddress(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := NewRoutes()
|
r := NewRoutes()
|
||||||
|
|
||||||
r.CreateMapping(tt.mapping.serverAddress, tt.mapping.backend, func(ctx context.Context) error { return nil })
|
r.CreateMapping(tt.mapping.serverAddress, tt.mapping.backend, EmptyScalerFunc, EmptyScalerFunc)
|
||||||
|
|
||||||
if got, server, _ := r.FindBackendForServerAddress(context.Background(), tt.args.serverAddress); got != tt.want {
|
if got, server, _, _ := r.FindBackendForServerAddress(context.Background(), tt.args.serverAddress); got != tt.want {
|
||||||
t.Errorf("routesImpl.FindBackendForServerAddress() = %v, want %v", got, tt.want)
|
t.Errorf("routesImpl.FindBackendForServerAddress() = %v, want %v", got, tt.want)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, tt.mapping.serverAddress, server)
|
assert.Equal(t, tt.mapping.serverAddress, server)
|
||||||
|
|||||||
Reference in New Issue
Block a user