Use duration type for options for Docker auto-scaling delays (#524)

This commit is contained in:
Geoff Bourne
2026-02-15 20:12:53 -06:00
committed by GitHub
parent 21f349c2da
commit 8f4b64a9eb
11 changed files with 117 additions and 47 deletions
+2
View File
@@ -7,3 +7,5 @@
/dist/ /dist/
/*.private.env.json /*.private.env.json
/kustomization.yml /kustomization.yml
/*.pem
+22
View File
@@ -0,0 +1,22 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="run (in docker)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="mc-router" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="--debug --in-docker" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
</ENTRIES>
</EXTENSION>
<kind value="DIRECTORY" />
<package value="github.com/itzg/mc-router" />
<directory value="$PROJECT_DIR$/cmd/mc-router" />
<filePath value="$PROJECT_DIR$/cmd/mc-router/main.go" />
<method v="2" />
</configuration>
</component>
+22
View File
@@ -0,0 +1,22 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="run (in docker, auto scale)" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="mc-router" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="--mapping localhost=localhost:25566 --in-docker --docker-socket=&quot;&quot; --auto-scale-up --auto-scale-down --auto-scale-down-after=30s" />
<EXTENSION ID="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
</ENTRIES>
</EXTENSION>
<kind value="DIRECTORY" />
<package value="github.com/itzg/mc-router" />
<directory value="$PROJECT_DIR$/cmd/mc-router" />
<filePath value="$PROJECT_DIR$/cmd/mc-router/main.go" />
<method v="2" />
</configuration>
</component>
+25
View File
@@ -744,6 +744,31 @@ then add the _Artifact Registry Reader_ Role to the _Compute Engine default serv
then use e.g. `gcloud auth configure-docker europe-docker.pkg.dev` or equivalent one time (to create a `~/.docker/config.json`), then use e.g. `gcloud auth configure-docker europe-docker.pkg.dev` or equivalent one time (to create a `~/.docker/config.json`),
and then use e.g. `--default-repo=europe-docker.pkg.dev/YOUR-PROJECT/YOUR-ARTIFACT-REGISTRY` option for `skaffold dev`. and then use e.g. `--default-repo=europe-docker.pkg.dev/YOUR-PROJECT/YOUR-ARTIFACT-REGISTRY` option for `skaffold dev`.
### Running in devcontainer
This approach is useful for testing changes for [Docker auto scaling](#docker-auto-scale-updown).
With IntelliJ Ultimate, [use these instructions](https://www.jetbrains.com/help/idea/start-dev-container-inside-ide.html). It is recommended to use the option to mount sources.
![Start devcontainer in IntelliJ](docs/intellij-devcontainer.png)
Use the example compose file [in examples/docker-discovery](examples/docker-discovery/compose.yml) or similar with `network_mode` set to "bridge" to ensure that the mc-router instance running within the devcontainer can reach the backend servers.
When applying the `mc-router.host` label to containers to be auto-discovered, it's easiest to use an external host of "localhost":
```yaml
vanilla:
image: itzg/minecraft-server
environment:
EULA: "TRUE"
labels:
mc-router.host: "localhost"
```
Run one of the labeled services by clicking the run icon in the gutter.
### Performing snapshot release with Docker ### Performing snapshot release with Docker
```bash ```bash
Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

+1 -1
View File
@@ -14,7 +14,7 @@ services:
environment: environment:
EULA: "TRUE" EULA: "TRUE"
labels: labels:
mc-router.host: "localhost.itzg.me" mc-router.host: "localhost"
# To allow for routing from devcontainer # To allow for routing from devcontainer
network_mode: bridge network_mode: bridge
paper: paper:
+4 -4
View File
@@ -10,7 +10,7 @@ type WebhookConfig struct {
type AutoScale struct { type AutoScale struct {
Up bool `usage:"Scale from zero on access. For Kubernetes, increases StatefulSet replicas from 0 to 1. For Docker, starts or unpauses the container when accessed"` Up bool `usage:"Scale from zero on access. For Kubernetes, increases StatefulSet replicas from 0 to 1. For Docker, starts or unpauses the container when accessed"`
Down bool `default:"false" usage:"Scale to zero after idle. For Kubernetes, decreases StatefulSet replicas from 1 to 0. For Docker, gracefully stops the container when there are no connections"` Down bool `default:"false" usage:"Scale to zero after idle. For Kubernetes, decreases StatefulSet replicas from 1 to 0. For Docker, gracefully stops the container when there are no connections"`
DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` DownAfter time.Duration `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"` 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"`
AsleepMOTD string `usage:"MOTD to display when auto-scaled down servers are accessed; if empty, no status will be served"` AsleepMOTD string `usage:"MOTD to display when auto-scaled down servers are accessed; if empty, no status will be served"`
} }
@@ -37,9 +37,9 @@ type Config struct {
KubeNamespace string `usage:"The namespace to watch or blank for all, which is the default"` KubeNamespace string `usage:"The namespace to watch or blank for all, which is the default"`
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 `usage:"Path to Docker socket to use"`
DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"` DockerTimeout time.Duration `usage:"Timeout (as duration) for the Docker integrations"`
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"` DockerRefreshInterval time.Duration `default:"15s" usage:"Refresh interval (as duration) for the Docker integrations"`
DockerApiVersion string `usage:"Instead of auto-negotiating, use specific Docker API version"` DockerApiVersion string `usage:"Instead of auto-negotiating, use specific Docker API version"`
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"` MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"`
MetricsBackendConfig MetricsBackendConfig MetricsBackendConfig MetricsBackendConfig
+12 -11
View File
@@ -32,8 +32,8 @@ type dockerWatcherConfig struct {
autoScaleUp bool autoScaleUp bool
autoScaleDown bool autoScaleDown bool
socket string socket string
timeoutSeconds int timeout time.Duration
refreshIntervalSeconds int refreshInterval time.Duration
apiVersion string apiVersion string
} }
@@ -47,12 +47,12 @@ func (c *dockerWatcherConfig) apiVersionOpt() client.Opt {
} }
} }
func NewDockerWatcher(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { func NewDockerWatcher(socket string, timeout time.Duration, refreshInterval time.Duration, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher {
return &dockerWatcherImpl{ return &dockerWatcherImpl{
config: dockerWatcherConfig{ config: dockerWatcherConfig{
socket: socket, socket: socket,
timeoutSeconds: timeoutSeconds, timeout: timeout,
refreshIntervalSeconds: refreshIntervalSeconds, refreshInterval: refreshInterval,
autoScaleUp: autoScaleUp, autoScaleUp: autoScaleUp,
autoScaleDown: autoScaleDown, autoScaleDown: autoScaleDown,
apiVersion: dockerApiVersion, apiVersion: dockerApiVersion,
@@ -225,24 +225,25 @@ func (w *dockerWatcherImpl) monitorContainers(ctx context.Context) error {
func (w *dockerWatcherImpl) Start(ctx context.Context) error { func (w *dockerWatcherImpl) Start(ctx context.Context) error {
var err error var err error
timeout := time.Duration(w.config.timeoutSeconds) * time.Second
refreshInterval := time.Duration(w.config.refreshIntervalSeconds) * time.Second
opts := []client.Opt{ opts := []client.Opt{
client.WithHost(w.config.socket), client.FromEnv,
client.WithTimeout(timeout), client.WithTimeout(w.config.timeout),
client.WithHTTPHeaders(map[string]string{ client.WithHTTPHeaders(map[string]string{
"User-Agent": "mc-router ", "User-Agent": "mc-router ",
}), }),
w.config.apiVersionOpt(), w.config.apiVersionOpt(),
} }
if w.config.socket != "" {
opts = append(opts, client.WithHost(w.config.socket))
}
w.client, err = client.NewClientWithOpts(opts...) w.client, err = client.NewClientWithOpts(opts...)
if err != nil { if err != nil {
return err return err
} }
ticker := time.NewTicker(refreshInterval) // TODO: replace all this with events listening
ticker := time.NewTicker(w.config.refreshInterval)
logrus.Trace("Performing initial listing of Docker containers") logrus.Trace("Performing initial listing of Docker containers")
initialContainers, err := w.listContainers(ctx) initialContainers, err := w.listContainers(ctx)
+5 -8
View File
@@ -19,12 +19,12 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func NewDockerSwarmWatcher(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { func NewDockerSwarmWatcher(socket string, timeout time.Duration, refreshInterval time.Duration, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher {
return &dockerSwarmWatcherImpl{ return &dockerSwarmWatcherImpl{
config: dockerWatcherConfig{ config: dockerWatcherConfig{
socket: socket, socket: socket,
timeoutSeconds: timeoutSeconds, timeout: timeout,
refreshIntervalSeconds: refreshIntervalSeconds, refreshInterval: refreshInterval,
autoScaleUp: autoScaleUp, autoScaleUp: autoScaleUp,
autoScaleDown: autoScaleDown, autoScaleDown: autoScaleDown,
apiVersion: dockerApiVersion, apiVersion: dockerApiVersion,
@@ -61,12 +61,9 @@ func (w *dockerSwarmWatcherImpl) makeSleeperFunc(_ *routableService) SleeperFunc
func (w *dockerSwarmWatcherImpl) Start(ctx context.Context) error { func (w *dockerSwarmWatcherImpl) Start(ctx context.Context) error {
var err error var err error
timeout := time.Duration(w.config.timeoutSeconds) * time.Second
refreshInterval := time.Duration(w.config.refreshIntervalSeconds) * time.Second
opts := []client.Opt{ opts := []client.Opt{
client.WithHost(w.config.socket), client.WithHost(w.config.socket),
client.WithTimeout(timeout), client.WithTimeout(w.config.timeout),
client.WithHTTPHeaders(map[string]string{ client.WithHTTPHeaders(map[string]string{
"User-Agent": "mc-router ", "User-Agent": "mc-router ",
}), }),
@@ -78,7 +75,7 @@ func (w *dockerSwarmWatcherImpl) Start(ctx context.Context) error {
return err return err
} }
ticker := time.NewTicker(refreshInterval) ticker := time.NewTicker(w.config.refreshInterval)
serviceMap := map[string]*routableService{} serviceMap := map[string]*routableService{}
logrus.Trace("Performing initial listing of Docker containers") logrus.Trace("Performing initial listing of Docker containers")
+6 -1
View File
@@ -56,7 +56,6 @@ func (ds *downScalerImpl) Begin(backendEndpoint string) {
scaleDownCancel() scaleDownCancel()
} }
logrus.WithField("backendEndpoint", backendEndpoint).Debug("Beginning scale down")
scaleDownContext, scaleDownContextCancellation := context.WithCancel(ds.parentContext) scaleDownContext, scaleDownContextCancellation := context.WithCancel(ds.parentContext)
ds.contextCancellations[backendEndpoint] = scaleDownContextCancellation ds.contextCancellations[backendEndpoint] = scaleDownContextCancellation
go ds.scaleDown(scaleDownContext, backendEndpoint) go ds.scaleDown(scaleDownContext, backendEndpoint)
@@ -78,12 +77,18 @@ func (ds *downScalerImpl) Cancel(backendEndpoint string) {
} }
func (ds *downScalerImpl) scaleDown(ctx context.Context, backendEndpoint string) { func (ds *downScalerImpl) scaleDown(ctx context.Context, backendEndpoint string) {
logrus.WithField("backendEndpoint", backendEndpoint).
WithField("delay", ds.delay).
Debug("Starting scale-down timer")
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(ds.delay): case <-time.After(ds.delay):
sleepers := Routes.GetSleepers(backendEndpoint) sleepers := Routes.GetSleepers(backendEndpoint)
logrus.WithField("backendEndpoint", backendEndpoint).
WithField("sleepers", len(sleepers)).
Debug("Found sleepers to use")
if len(sleepers) == 0 { if len(sleepers) == 0 {
return return
} }
+1 -5
View File
@@ -7,7 +7,6 @@ import (
"os" "os"
"runtime/pprof" "runtime/pprof"
"strconv" "strconv"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -50,10 +49,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) {
metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig) metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "" || config.InDocker) downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "" || config.InDocker)
downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter) downScalerDelay := config.AutoScale.DownAfter
if err != nil {
return nil, fmt.Errorf("could not parse auto-scale-down-after duration: %w", err)
}
// Only one instance should be created // Only one instance should be created
DownScaler = NewDownScaler(ctx, downScalerEnabled, downScalerDelay) DownScaler = NewDownScaler(ctx, downScalerEnabled, downScalerDelay)