Skip to content

Commit 952b4f5

Browse files
committed
Add min-idle time for server.
the autoscaler will shutdown any server that is not busy but has reached the min_age parameter (30m currently) This feature adds a new flag that basically resets the age to 0 every time a new build is executed. This will allow us to decrease the min-age but allow the servers to be used for a longer period of time during busy hours, thus avoiding starting up too many servers and decreasing the build startup times This is a feature added by a contributor to the original autoscaler code. harness is very slow to accept contributions so the PR has sat there for about 1 month. drone#125
1 parent 91109d6 commit 952b4f5

File tree

11 files changed

+203
-4
lines changed

11 files changed

+203
-4
lines changed

config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ type (
3232
}
3333

3434
Pool struct {
35-
Min int `default:"2"`
36-
Max int `default:"4"`
37-
MinAge time.Duration `default:"55m" split_words:"true"`
35+
Min int `default:"2"`
36+
Max int `default:"4"`
37+
MinAge time.Duration `default:"55m" split_words:"true"`
38+
MinIdle time.Duration `default:"0" split_words:"true"`
3839
}
3940

4041
Check struct {

config/load_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ func TestDefaults(t *testing.T) {
3535
if got, want := conf.Pool.MinAge, time.Minute*55; got != want {
3636
t.Errorf("Want default DRONE_POOL_MIN_AGE of %d, got %d", want, got)
3737
}
38+
if got, want := conf.Pool.MinIdle, time.Minute*0; got != want {
39+
t.Errorf("Want default DRONE_POOL_MIN_IDLE of %d, got %d", want, got)
40+
}
3841

3942
if got, want := conf.Check.Interval, time.Minute; got != want {
4043
t.Errorf("Want default DRONE_INSTALL_CHECK_INTERVAL of %s, got %s", want, got)
@@ -74,6 +77,7 @@ func TestLoad(t *testing.T) {
7477
"DRONE_LOGS_PRETTY": "true",
7578
"DRONE_CAPACITY_BUFFER": "3",
7679
"DRONE_POOL_MIN_AGE": "1h",
80+
"DRONE_POOL_MIN_IDLE": "15m",
7781
"DRONE_POOL_MIN": "1",
7882
"DRONE_POOL_MAX": "5",
7983
"DRONE_SERVER_HOST": "drone.company.com",
@@ -205,7 +209,8 @@ var jsonConfig = []byte(`{
205209
"Pool": {
206210
"Min": 1,
207211
"Max": 5,
208-
"MinAge": 3600000000000
212+
"MinAge": 3600000000000,
213+
"MinIdle": 900000000000
209214
},
210215
"Server": {
211216
"Host": "drone.company.com",

engine/engine.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func New(
100100
kernel: config.Agent.Kernel,
101101
buffer: config.CapacityBuffer,
102102
ttu: config.Pool.MinAge,
103+
tti: config.Pool.MinIdle,
103104
min: config.Pool.Min,
104105
max: config.Pool.Max,
105106
cap: config.Agent.Concurrency,

engine/planner.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type planner struct {
3030
cap int // capacity per-server
3131
buffer int // buffer capacity to have warm and ready
3232
ttu time.Duration // minimum server age
33+
tti time.Duration // minimum server idle time
3334
labels map[string]string
3435

3536
client drone.Client
@@ -76,6 +77,16 @@ func (p *planner) Plan(ctx context.Context) error {
7677

7778
ctx = logger.WithContext(ctx, log)
7879

80+
// if MinIdle is being used, track busy servers
81+
if p.tti > 0 {
82+
_, err = p.updateBusy(ctx)
83+
if err != nil {
84+
log.WithError(err).
85+
Errorln("cannot check for busy servers")
86+
return err
87+
}
88+
}
89+
7990
free := max(capacity-running-p.buffer, 0)
8091
diff := serverDiff(pending, free, p.cap)
8192

@@ -104,6 +115,45 @@ func (p *planner) Plan(ctx context.Context) error {
104115
return nil
105116
}
106117

118+
119+
// helper function checks for busy running instances and updates idle timer
120+
func (p *planner) updateBusy(ctx context.Context) (count int, err error) {
121+
logger := logger.FromContext(ctx)
122+
123+
servers, err := p.servers.ListState(ctx, autoscaler.StateRunning)
124+
if err != nil {
125+
logger.WithError(err).
126+
Errorln("cannot fetch server list")
127+
return count, err
128+
}
129+
130+
// check for busy servers to update idle timers
131+
busy, err := p.listBusy(ctx)
132+
if err != nil {
133+
logger.WithError(err).
134+
Errorln("cannot ascertain busy server list")
135+
return count, err
136+
}
137+
138+
for _, server := range servers {
139+
if _, ok := busy[server.Name]; ok {
140+
err := p.servers.Busy(ctx, server)
141+
if err != nil {
142+
logger.WithError(err).
143+
WithField("server", server.Name).
144+
WithField("updated", server.Updated).
145+
Errorln("cannot update server as busy")
146+
}
147+
logger.WithField("server", server.Name).
148+
Debugln("updated busy server")
149+
count++
150+
}
151+
}
152+
logger.Debugf("found %d busy servers", count)
153+
return count, nil
154+
}
155+
156+
107157
// helper function allocates n new server instances.
108158
func (p *planner) alloc(ctx context.Context, n int) error {
109159
logger := logger.FromContext(ctx)
@@ -186,6 +236,16 @@ func (p *planner) mark(ctx context.Context, n int) error {
186236
continue
187237
}
188238

239+
// skip servers that have not reached a min idle time
240+
if time.Now().Before(time.Unix(server.LastBusy, 0).Add(p.tti)) {
241+
logger.
242+
WithField("server", server.Name).
243+
WithField("idle", timeDiff(time.Now(), time.Unix(server.LastBusy, 0))).
244+
WithField("min-idle", p.tti).
245+
Debugln("server min-idle not reached")
246+
continue
247+
}
248+
189249
idle = append(idle, server)
190250
logger.WithField("server", server.Name).
191251
Debugln("server is idle")

engine/planner_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,51 @@ func TestScale_MinAge(t *testing.T) {
399399
}
400400
}
401401

402+
// This test verifies that idle servers are not
403+
// garbage collected until the min-idle is reached.
404+
func TestScale_MinIdle(t *testing.T) {
405+
controller := gomock.NewController(t)
406+
defer controller.Finish()
407+
408+
// x2 capacity
409+
servers := []*autoscaler.Server{
410+
{Name: "server1", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix(), LastBusy: time.Now().Add(time.Minute * 10 * -1).Unix()},
411+
{Name: "server2", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix(), LastBusy: time.Now().Add(time.Minute * 60 * -1).Unix()},
412+
}
413+
414+
// x0 running builds
415+
// x0 pending builds
416+
builds := []*drone.Stage{}
417+
418+
store := mocks.NewMockServerStore(controller)
419+
store.EXPECT().List(gomock.Any()).Return(servers, nil)
420+
store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)
421+
store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)
422+
// we should expect a call to shut down only server2 since the last busy
423+
// time of 1 hour ago satisfies the MinIdle (tti) of 30 minutes.
424+
store.EXPECT().Update(gomock.Any(), servers[1]).Return(nil)
425+
426+
client := mocks.NewMockClient(controller)
427+
client.EXPECT().Queue().Return(builds, nil)
428+
client.EXPECT().Queue().Return(builds, nil)
429+
client.EXPECT().Queue().Return(builds, nil)
430+
431+
p := planner{
432+
cap: 2,
433+
min: 1,
434+
max: 4,
435+
ttu: 0,
436+
tti: time.Minute * 30,
437+
client: client,
438+
servers: store,
439+
}
440+
441+
err := p.Plan(context.TODO())
442+
if err != nil {
443+
t.Error(err)
444+
}
445+
}
446+
402447
func TestPlan_ShutdownIdle(t *testing.T) {
403448
controller := gomock.NewController(t)
404449
defer controller.Finish()

mocks/mock_server.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type ServerStore interface {
5252
// Update the server record in the store.
5353
Update(context.Context, *Server) error
5454

55+
// Update the server record that it is busy.
56+
Busy(context.Context, *Server) error
57+
5558
// Delete the server record from the store.
5659
Delete(context.Context, *Server) error
5760

@@ -81,4 +84,5 @@ type Server struct {
8184
Updated int64 `db:"server_updated" json:"updated"`
8285
Started int64 `db:"server_started" json:"started"`
8386
Stopped int64 `db:"server_stopped" json:"stopped"`
87+
LastBusy int64 `db:"server_lastbusy" json:"lastbusy"`
8488
}

store/migrate/mysql/ddl_gen.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ var migrations = []struct {
2020
name: "create-index-server-state",
2121
stmt: createIndexServerState,
2222
},
23+
{
24+
name: "alter-table-servers-add-column-server-lastbusy",
25+
stmt: alterTableServersAddColumnServerLastbusy,
26+
},
2327
}
2428

2529
// Migrate performs the database migration. If the migration fails
@@ -131,3 +135,11 @@ CREATE INDEX ix_servers_id ON servers (server_id);
131135
var createIndexServerState = `
132136
CREATE INDEX ix_servers_state ON servers (server_state);
133137
`
138+
139+
//
140+
// 002_add_column_lastbusy.sql
141+
//
142+
143+
var alterTableServersAddColumnServerLastbusy = `
144+
ALTER TABLE servers ADD COLUMN server_lastbusy INTEGER NOT NULL DEFAULT 0;
145+
`

store/migrate/postgres/ddl_gen.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ var migrations = []struct {
2020
name: "create-index-server-state",
2121
stmt: createIndexServerState,
2222
},
23+
{
24+
name: "alter-table-servers-add-column-server-lastbusy",
25+
stmt: alterTableServersAddColumnServerLastbusy,
26+
},
2327
}
2428

2529
// Migrate performs the database migration. If the migration fails
@@ -131,3 +135,11 @@ CREATE INDEX ix_servers_id ON servers (server_id);
131135
var createIndexServerState = `
132136
CREATE INDEX ix_servers_state ON servers (server_state);
133137
`
138+
139+
//
140+
// 002_add_column_lastbusy.sql
141+
//
142+
143+
var alterTableServersAddColumnServerLastbusy = `
144+
ALTER TABLE servers ADD COLUMN server_lastbusy INTEGER NOT NULL DEFAULT 0;
145+
`

store/migrate/sqlite/ddl_gen.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ var migrations = []struct {
2020
name: "create-index-server-state",
2121
stmt: createIndexServerState,
2222
},
23+
{
24+
name: "alter-table-servers-add-column-server-lastbusy",
25+
stmt: alterTableServersAddColumnServerLastbusy,
26+
},
2327
}
2428

2529
// Migrate performs the database migration. If the migration fails
@@ -131,3 +135,11 @@ CREATE INDEX IF NOT EXISTS ix_servers_id ON servers (server_id);
131135
var createIndexServerState = `
132136
CREATE INDEX IF NOT EXISTS ix_servers_state ON servers (server_state);
133137
`
138+
139+
//
140+
// 002_add_column_lastbusy.sql
141+
//
142+
143+
var alterTableServersAddColumnServerLastbusy = `
144+
ALTER TABLE servers ADD COLUMN server_lastbusy INTEGER NOT NULL DEFAULT 0;
145+
`

0 commit comments

Comments
 (0)