Skip to content

Commit

Permalink
Implement autosuspend (#3733)
Browse files Browse the repository at this point in the history
* Implement autosuspend

This updates fly-go and switches flyctl to using the new tri-state
fly.MachineAutostop type for the `auto_stop_machines` setting.
Autosuspend can be enabled by setting `auto_stop_machines = "suspend"`.
(Note that `fly.toml` parsing continues to support the original boolean
settings for this field.)

* Update the `fly machines` autostop flag

This flag now accepts the new string values for autostop ("off", "stop",
"suspend") in addition to the original boolean ones.
  • Loading branch information
matttpt authored Jul 19, 2024
1 parent caf68ff commit a239842
Show file tree
Hide file tree
Showing 19 changed files with 89 additions and 39 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/superfly/fly-go v0.1.19-0.20240702095246-59db1fe4ffe8
github.com/superfly/fly-go v0.1.19-0.20240716210409-e3d434ec3f18
github.com/superfly/graphql v0.2.4
github.com/superfly/lfsc-go v0.1.1
github.com/superfly/macaroon v0.2.13
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/superfly/fly-go v0.1.19-0.20240702095246-59db1fe4ffe8 h1:0f8iTBctVg1NzldSYsnw+yvgZGW9X2QXxitq/BIwdRs=
github.com/superfly/fly-go v0.1.19-0.20240702095246-59db1fe4ffe8/go.mod h1:JQke/BwoZqrWurqYkypSlcSo7bIUgCI3eVnqMC6AUj0=
github.com/superfly/fly-go v0.1.19-0.20240716210409-e3d434ec3f18 h1:lXtwecpu2Ynwm1mkSl7pcJFFM86HzJNUsrNC4vswrYg=
github.com/superfly/fly-go v0.1.19-0.20240716210409-e3d434ec3f18/go.mod h1:JQke/BwoZqrWurqYkypSlcSo7bIUgCI3eVnqMC6AUj0=
github.com/superfly/graphql v0.2.4 h1:Av8hSk4x8WvKJ6MTnEwrLknSVSGPc7DWpgT3z/kt3PU=
github.com/superfly/graphql v0.2.4/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc=
github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2Q=
Expand Down
4 changes: 2 additions & 2 deletions internal/appconfig/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestToDefinition(t *testing.T) {
"internal_port": int64(8080),
"force_https": true,
"auto_start_machines": false,
"auto_stop_machines": false,
"auto_stop_machines": "off",
"min_machines_running": int64(0),
"concurrency": map[string]any{
"type": "donuts",
Expand Down Expand Up @@ -331,7 +331,7 @@ func TestToDefinition(t *testing.T) {
"protocol": "tcp",
"processes": []any{"app"},
"auto_start_machines": false,
"auto_stop_machines": false,
"auto_stop_machines": "off",
"min_machines_running": int64(1),
"concurrency": map[string]any{
"type": "requests",
Expand Down
8 changes: 4 additions & 4 deletions internal/appconfig/machines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ func TestToMachineConfig_services(t *testing.T) {
Protocol: "tcp",
InternalPort: 8080,
Autostart: fly.Pointer(true),
Autostop: fly.Pointer(true),
Autostop: fly.Pointer(fly.MachineAutostopStop),
Ports: []fly.MachinePort{
{Port: fly.Pointer(80), Handlers: []string{"http"}, ForceHTTPS: true},
{Port: fly.Pointer(443), Handlers: []string{"http", "tls"}, ForceHTTPS: false},
Expand All @@ -643,13 +643,13 @@ func TestToMachineConfig_services(t *testing.T) {
Protocol: "tcp",
InternalPort: 1000,
Autostart: fly.Pointer(true),
Autostop: fly.Pointer(true),
Autostop: fly.Pointer(fly.MachineAutostopStop),
},
{
Protocol: "tcp",
InternalPort: 1001,
Autostart: fly.Pointer(false),
Autostop: fly.Pointer(false),
Autostop: fly.Pointer(fly.MachineAutostopOff),
},
{
Protocol: "tcp",
Expand All @@ -659,7 +659,7 @@ func TestToMachineConfig_services(t *testing.T) {
{
Protocol: "tcp",
InternalPort: 1003,
Autostop: fly.Pointer(true),
Autostop: fly.Pointer(fly.MachineAutostopStop),
},
{
Protocol: "tcp",
Expand Down
7 changes: 4 additions & 3 deletions internal/appconfig/serde_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ func TestLoadTOMLAppConfigOldFormat(t *testing.T) {
}},
Services: []Service{
{
InternalPort: 8080,
InternalPort: 8080,
AutoStopMachines: fly.Pointer(fly.MachineAutostopOff),
Ports: []fly.MachinePort{
{
Port: fly.Pointer(80),
Expand Down Expand Up @@ -416,7 +417,7 @@ func TestLoadTOMLAppConfigReferenceFormat(t *testing.T) {
InternalPort: 8080,
ForceHTTPS: true,
AutoStartMachines: fly.Pointer(false),
AutoStopMachines: fly.Pointer(false),
AutoStopMachines: fly.Pointer(fly.MachineAutostopOff),
MinMachinesRunning: fly.Pointer(0),
Concurrency: &fly.MachineServiceConcurrency{
Type: "donuts",
Expand Down Expand Up @@ -536,7 +537,7 @@ func TestLoadTOMLAppConfigReferenceFormat(t *testing.T) {
Protocol: "tcp",
Processes: []string{"app"},
AutoStartMachines: fly.Pointer(false),
AutoStopMachines: fly.Pointer(false),
AutoStopMachines: fly.Pointer(fly.MachineAutostopOff),
MinMachinesRunning: fly.Pointer(1),

Concurrency: &fly.MachineServiceConcurrency{
Expand Down
4 changes: 2 additions & 2 deletions internal/appconfig/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Service struct {
InternalPort int `json:"internal_port,omitempty" toml:"internal_port"`
// AutoStopMachines and AutoStartMachines should not have omitempty for TOML. The encoder
// already omits nil since it can't be represented, and omitempty makes it omit false as well.
AutoStopMachines *bool `json:"auto_stop_machines,omitempty" toml:"auto_stop_machines"`
AutoStopMachines *fly.MachineAutostop `json:"auto_stop_machines,omitempty" toml:"auto_stop_machines"`
AutoStartMachines *bool `json:"auto_start_machines,omitempty" toml:"auto_start_machines"`
MinMachinesRunning *int `json:"min_machines_running,omitempty" toml:"min_machines_running,omitempty"`
Ports []fly.MachinePort `json:"ports,omitempty" toml:"ports"`
Expand Down Expand Up @@ -57,7 +57,7 @@ type HTTPService struct {
InternalPort int `json:"internal_port,omitempty" toml:"internal_port,omitempty" validate:"required,numeric"`
ForceHTTPS bool `toml:"force_https,omitempty" json:"force_https,omitempty"`
// AutoStopMachines and AutoStartMachines should not have omitempty for TOML; see the note in Service.
AutoStopMachines *bool `json:"auto_stop_machines,omitempty" toml:"auto_stop_machines"`
AutoStopMachines *fly.MachineAutostop `json:"auto_stop_machines,omitempty" toml:"auto_stop_machines"`
AutoStartMachines *bool `json:"auto_start_machines,omitempty" toml:"auto_start_machines"`
MinMachinesRunning *int `json:"min_machines_running,omitempty" toml:"min_machines_running,omitempty"`
Processes []string `json:"processes,omitempty" toml:"processes,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions internal/appconfig/testdata/full-reference.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ host_dedication_id = "06031957"
internal_port = 8080
force_https = true
auto_start_machines = false
auto_stop_machines = false
auto_stop_machines = "off"
min_machines_running = 0

[[http_service.checks]]
Expand Down Expand Up @@ -174,7 +174,7 @@ host_dedication_id = "06031957"
protocol = "tcp"
processes = ["app"]
auto_start_machines = false
auto_stop_machines = false
auto_stop_machines = "off"
min_machines_running = 1

[services.concurrency]
Expand Down
3 changes: 3 additions & 0 deletions internal/appconfig/testdata/old-format.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ build_target = "thalayer"
# Old concurrency format
concurrency = "12,23"

# Autostop specified with a boolean
auto_stop_machines = false

[[services.ports]]
# Stringified port
port = "80"
Expand Down
8 changes: 4 additions & 4 deletions internal/appconfig/testdata/tomachine-services.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ primary_region = "scl"
internal_port = 8080
force_https = true
auto_start_machines = true
auto_stop_machines = true
auto_stop_machines = "stop"

[[services]]
protocol = "tcp"
internal_port = 1000
auto_start_machines = true
auto_stop_machines = true
auto_stop_machines = "stop"

[[services]]
protocol = "tcp"
internal_port = 1001
auto_start_machines = false
auto_stop_machines = false
auto_stop_machines = "off"

[[services]]
protocol = "tcp"
Expand All @@ -27,7 +27,7 @@ primary_region = "scl"
[[services]]
protocol = "tcp"
internal_port = 1003
auto_stop_machines = true
auto_stop_machines = "stop"

[[services]]
protocol = "tcp"
Expand Down
2 changes: 1 addition & 1 deletion internal/build/imgsrc/ensure_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func createBuilder(ctx context.Context, org *fly.Organization, region, builderNa
{
Protocol: "tcp",
InternalPort: 8080,
Autostop: fly.BoolPointer(false),
Autostop: fly.Pointer(fly.MachineAutostopOff),
Autostart: fly.BoolPointer(true),
MinMachinesRunning: fly.IntPointer(0),
Ports: []fly.MachinePort{
Expand Down
30 changes: 27 additions & 3 deletions internal/command/deploy/machines_deploymachinesapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/sourcegraph/conc/pool"
fly "github.com/superfly/fly-go"
"github.com/superfly/flyctl/helpers"
"github.com/superfly/flyctl/internal/appconfig"
machcmd "github.com/superfly/flyctl/internal/command/machine"
"github.com/superfly/flyctl/internal/flapsutil"
"github.com/superfly/flyctl/internal/flyerr"
Expand Down Expand Up @@ -199,6 +200,7 @@ func (md *machineDeployment) deployCanaryMachines(ctx context.Context) (err erro
// Create machines for new process groups
func (md *machineDeployment) deployCreateMachinesForGroups(ctx context.Context, processGroupMachineDiff ProcessGroupsDiff) (err error) {
groupsWithAutostopEnabled := make(map[string]bool)
groupsWithAutosuspendEnabled := make(map[string]bool)
groups := maps.Keys(processGroupMachineDiff.groupsNeedingMachines)
total := len(groups)
slices.Sort(groups)
Expand All @@ -223,9 +225,22 @@ func (md *machineDeployment) deployCreateMachinesForGroups(ctx context.Context,
}

services := groupConfig.AllServices()
for _, s := range services {
if s.AutoStopMachines != nil && *s.AutoStopMachines {
if len(services) > 0 {
// The proxy will use the most restrictive (which, in terms
// of the fly.MachineAutostop type, is the least) autostop
// setting across all of the group's services.
autostopSettings := lo.Map(services, func(s appconfig.Service, _ int) fly.MachineAutostop {
if s.AutoStopMachines != nil {
return *s.AutoStopMachines
} else {
return fly.MachineAutostopOff
}
})
switch slices.Min(autostopSettings) {
case fly.MachineAutostopStop:
groupsWithAutostopEnabled[name] = true
case fly.MachineAutostopSuspend:
groupsWithAutosuspendEnabled[name] = true
}
}

Expand Down Expand Up @@ -267,7 +282,16 @@ func (md *machineDeployment) deployCreateMachinesForGroups(ctx context.Context,
groupNames := lo.Keys(groupsWithAutostopEnabled)
slices.Sort(groupNames)
fmt.Fprintf(md.io.Out,
"\n%s The machines for [%s] have services with 'auto_stop_machines = true' that will be stopped when idling\n\n",
"\n%s The machines for [%s] have services with 'auto_stop_machines = \"stop\"' that will be stopped when idling\n\n",
md.colorize.Yellow("NOTE:"),
md.colorize.Bold(strings.Join(groupNames, ",")),
)
}
if len(groupsWithAutosuspendEnabled) > 0 {
groupNames := lo.Keys(groupsWithAutosuspendEnabled)
slices.Sort(groupNames)
fmt.Fprintf(md.io.Out,
"\n%s The machines for [%s] have services with 'auto_stop_machines = \"suspend\"' that will be suspended when idling\n\n",
md.colorize.Yellow("NOTE:"),
md.colorize.Bold(strings.Join(groupNames, ",")),
)
Expand Down
6 changes: 3 additions & 3 deletions internal/command/deploy/machines_launchinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func (md *machineDeployment) setMachineReleaseData(mConfig *fly.MachineConfig) {
}
}

// Skip launching currently-stopped machines if:
// Skip launching currently-stopped or suspended machines if:
// * any services use autoscaling (autostop or autostart).
// * it is a standby machine
func skipLaunch(origMachineRaw *fly.Machine, mConfig *fly.MachineConfig) bool {
Expand All @@ -208,9 +208,9 @@ func skipLaunch(origMachineRaw *fly.Machine, mConfig *fly.MachineConfig) bool {
return false
case len(mConfig.Standbys) > 0:
return true
case origMachineRaw.State == fly.MachineStateStopped:
case origMachineRaw.State == fly.MachineStateStopped || origMachineRaw.State == fly.MachineStateSuspended:
for _, s := range mConfig.Services {
if (s.Autostop != nil && *s.Autostop) || (s.Autostart != nil && *s.Autostart) {
if (s.Autostop != nil && *s.Autostop != fly.MachineAutostopOff) || (s.Autostart != nil && *s.Autostart) {
return true
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/command/launch/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (state *launchState) updateConfig(ctx context.Context) {
state.appConfig.HTTPService = &appconfig.HTTPService{
ForceHTTPS: true,
AutoStartMachines: fly.Pointer(true),
AutoStopMachines: fly.Pointer(true),
AutoStopMachines: fly.Pointer(fly.MachineAutostopStop),
MinMachinesRunning: fly.Pointer(0),
Processes: []string{"app"},
}
Expand Down
26 changes: 22 additions & 4 deletions internal/command/machine/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -112,10 +113,11 @@ var sharedFlags = flag.Set{
Description: "Automatically start a stopped Machine when a network request is received",
Default: true,
},
flag.Bool{
flag.String{
Name: "autostop",
Description: "Automatically stop a Machine when there are no network requests for it",
Default: true,
Description: "Automatically stop a Machine when there are no network requests for it. Options include 'off', 'stop', and 'suspend'.",
Default: "off",
NoOptDefVal: "stop",
},
flag.String{
Name: "restart",
Expand Down Expand Up @@ -774,7 +776,23 @@ func determineMachineConfig(
}

if flag.IsSpecified(ctx, "autostop") {
s.Autostop = fly.Pointer(flag.GetBool(ctx, "autostop"))
// We'll try to parse it as a boolean first for backward
// compatibility. (strconv.ParseBool is what the pflag
// library uses for booleans under the hood.)
asString := flag.GetString(ctx, "autostop")
if asBool, err := strconv.ParseBool(asString); err == nil {
if asBool {
s.Autostop = fly.Pointer(fly.MachineAutostopStop)
} else {
s.Autostop = fly.Pointer(fly.MachineAutostopOff)
}
} else {
var value fly.MachineAutostop
if err := value.UnmarshalText([]byte(asString)); err != nil {
return nil, err
}
s.Autostop = fly.Pointer(value)
}
}

if flag.IsSpecified(ctx, "autostart") {
Expand Down
4 changes: 4 additions & 0 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type String struct {
Shorthand string
Description string
Default string
NoOptDefVal string
ConfName string
EnvName string
Hidden bool
Expand All @@ -110,6 +111,9 @@ func (s String) addTo(cmd *cobra.Command) {

f := flags.Lookup(s.Name)
f.Hidden = s.Hidden
if s.NoOptDefVal != "" {
f.NoOptDefVal = s.NoOptDefVal
}

// Aliases
for _, name := range s.Aliases {
Expand Down
2 changes: 1 addition & 1 deletion test/preflight/apps_v2_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestAppsV2Example(t *testing.T) {
require.NotNil(t, firstMachine.Config.Services[0].Autostart)
require.NotNil(t, firstMachine.Config.Services[0].Autostop)
require.True(t, *firstMachine.Config.Services[0].Autostart)
require.True(t, *firstMachine.Config.Services[0].Autostop)
require.Equal(t, fly.MachineAutostopOff, *firstMachine.Config.Services[0].Autostop)

secondReg := f.PrimaryRegion()
if len(f.OtherRegions()) > 0 {
Expand Down
2 changes: 1 addition & 1 deletion test/preflight/fixtures/deploy-node/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ primary_region = '{{region}}'
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
processes = ['app']
Expand Down
4 changes: 2 additions & 2 deletions test/preflight/fly_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestFlyLaunchV2(t *testing.T) {
"http_service": map[string]any{
"force_https": true,
"internal_port": int64(8080),
"auto_stop_machines": true,
"auto_stop_machines": "stop",
"auto_start_machines": true,
"min_machines_running": int64(0),
"processes": []any{"app"},
Expand Down Expand Up @@ -212,7 +212,7 @@ func TestFlyLaunchHA(t *testing.T) {
[http_service]
internal_port = 80
auto_start_machines = true
auto_stop_machines = true
auto_stop_machines = "stop"
processes = ["app"]
`)

Expand Down
Loading

0 comments on commit a239842

Please sign in to comment.