functions: fix goroutine leak in runner (#394)

* functions: fix goroutine leak in runner

* functions: ensure taskQueue is consumed after context cancellation
This commit is contained in:
C Cirello
2016-12-06 16:11:06 +01:00
committed by GitHub
parent f0fc85b85a
commit 0cdd1db3e1
9 changed files with 122 additions and 67 deletions

View File

@@ -189,12 +189,13 @@ func TestTasksrvURL(t *testing.T) {
} }
} }
func testRunner(t *testing.T) *Runner { func testRunner(t *testing.T) (*Runner, context.CancelFunc) {
r, err := New(NewFuncLogger(), NewMetricLogger()) ctx, cancel := context.WithCancel(context.Background())
r, err := New(ctx, NewFuncLogger(), NewMetricLogger())
if err != nil { if err != nil {
t.Fatal("Test: failed to create new runner") t.Fatal("Test: failed to create new runner")
} }
return r return r, cancel
} }
func TestAsyncRunnersGracefulShutdown(t *testing.T) { func TestAsyncRunnersGracefulShutdown(t *testing.T) {
@@ -217,7 +218,9 @@ func TestAsyncRunnersGracefulShutdown(t *testing.T) {
} }
}() }()
startAsyncRunners(ctx, ts.URL+"/tasks", tasks, testRunner(t)) rnr, cancel := testRunner(t)
defer cancel()
startAsyncRunners(ctx, ts.URL+"/tasks", tasks, rnr)
if err := ctx.Err(); err != context.DeadlineExceeded { if err := ctx.Err(); err != context.DeadlineExceeded {
t.Log(buf.String()) t.Log(buf.String())

View File

@@ -39,7 +39,7 @@ var (
WaitMemoryTimeout = 10 * time.Second WaitMemoryTimeout = 10 * time.Second
) )
func New(flog FuncLogger, mlog MetricLogger) (*Runner, error) { func New(ctx context.Context, flog FuncLogger, mlog MetricLogger) (*Runner, error) {
// TODO: Is this really required for the container drivers? Can we remove it? // TODO: Is this really required for the container drivers? Can we remove it?
env := common.NewEnvironment(func(e *common.Environment) {}) env := common.NewEnvironment(func(e *common.Environment) {})
@@ -58,7 +58,7 @@ func New(flog FuncLogger, mlog MetricLogger) (*Runner, error) {
usedMem: 0, usedMem: 0,
} }
go r.queueHandler() go r.queueHandler(ctx)
return r, nil return r, nil
} }
@@ -67,17 +67,28 @@ func New(flog FuncLogger, mlog MetricLogger) (*Runner, error) {
// If there's memory then send signal to the task to proceed. // If there's memory then send signal to the task to proceed.
// If there's not available memory to run the task it waits // If there's not available memory to run the task it waits
// If the task waits for more than X seconds it timeouts // If the task waits for more than X seconds it timeouts
func (r *Runner) queueHandler() { func (r *Runner) queueHandler(ctx context.Context) {
var task *containerTask consumeQueue:
var waitStart time.Time
var waitTime time.Duration
var timedOut bool
for { for {
select { select {
case task = <-r.taskQueue: case task := <-r.taskQueue:
waitStart = time.Now() r.handleTask(task)
timedOut = false case <-ctx.Done():
break consumeQueue
} }
}
// consume remainders
for len(r.taskQueue) > 0 {
r.handleTask(<-r.taskQueue)
}
}
func (r *Runner) handleTask(task *containerTask) {
waitStart := time.Now()
var waitTime time.Duration
var timedOut bool
// Loop waiting for available memory // Loop waiting for available memory
for !r.checkRequiredMem(task.cfg.Memory) { for !r.checkRequiredMem(task.cfg.Memory) {
@@ -97,13 +108,12 @@ func (r *Runner) queueHandler() {
// Send to a signal to this task saying it cannot run // Send to a signal to this task saying it cannot run
r.mlog.LogCount(task.ctx, metricBaseName+"timeout", 1) r.mlog.LogCount(task.ctx, metricBaseName+"timeout", 1)
task.canRun <- false task.canRun <- false
continue return
} }
// Send a signal to this task saying it can run // Send a signal to this task saying it can run
task.canRun <- true task.canRun <- true
} }
}
func (r *Runner) hasAsyncAvailableMemory() bool { func (r *Runner) hasAsyncAvailableMemory() bool {
r.usedMemMutex.RLock() r.usedMemMutex.RLock()

View File

@@ -14,13 +14,14 @@ import (
func TestRunnerHello(t *testing.T) { func TestRunnerHello(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
runner, err := New(NewFuncLogger(), NewMetricLogger()) ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runner, err := New(ctx, NewFuncLogger(), NewMetricLogger())
if err != nil { if err != nil {
t.Fatalf("Test error during New() - %s", err) t.Fatalf("Test error during New() - %s", err)
} }
ctx := context.Background()
for i, test := range []struct { for i, test := range []struct {
route *models.Route route *models.Route
payload string payload string
@@ -67,13 +68,14 @@ func TestRunnerHello(t *testing.T) {
func TestRunnerError(t *testing.T) { func TestRunnerError(t *testing.T) {
t.Skip() t.Skip()
buf := setLogBuffer() buf := setLogBuffer()
runner, err := New(NewFuncLogger(), NewMetricLogger()) ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runner, err := New(ctx, NewFuncLogger(), NewMetricLogger())
if err != nil { if err != nil {
t.Fatalf("Test error during New() - %s", err) t.Fatalf("Test error during New() - %s", err)
} }
ctx := context.Background()
for i, test := range []struct { for i, test := range []struct {
route *models.Route route *models.Route
payload string payload string

View File

@@ -57,7 +57,8 @@ func TestAppCreate(t *testing.T) {
// success // success
{&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusCreated, nil}, {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusCreated, nil},
} { } {
router := testRouter(test.mock, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.mock, &mqs.Mock{}, rnr, tasks)
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "POST", test.path, body) _, rec := routerRequest(t, router, "POST", test.path, body)
@@ -77,6 +78,7 @@ func TestAppCreate(t *testing.T) {
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} }
cancel()
} }
} }
@@ -99,7 +101,8 @@ func TestAppDelete(t *testing.T) {
}}, }},
}, "/v1/apps/myapp", "", http.StatusOK, nil}, }, "/v1/apps/myapp", "", http.StatusOK, nil},
} { } {
router := testRouter(test.ds, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.ds, &mqs.Mock{}, rnr, tasks)
_, rec := routerRequest(t, router, "DELETE", test.path, nil) _, rec := routerRequest(t, router, "DELETE", test.path, nil)
@@ -118,6 +121,7 @@ func TestAppDelete(t *testing.T) {
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} }
cancel()
} }
} }
@@ -126,7 +130,9 @@ func TestAppList(t *testing.T) {
tasks := mockTasksConduit() tasks := mockTasksConduit()
defer close(tasks) defer close(tasks)
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -161,7 +167,9 @@ func TestAppGet(t *testing.T) {
tasks := mockTasksConduit() tasks := mockTasksConduit()
defer close(tasks) defer close(tasks)
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -213,7 +221,8 @@ func TestAppUpdate(t *testing.T) {
}}, }},
}, "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, }, "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil},
} { } {
router := testRouter(test.mock, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.mock, &mqs.Mock{}, rnr, tasks)
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "PUT", test.path, body) _, rec := routerRequest(t, router, "PUT", test.path, body)
@@ -233,5 +242,7 @@ func TestAppUpdate(t *testing.T) {
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} }
cancel()
} }
} }

View File

@@ -36,7 +36,8 @@ func TestRouteCreate(t *testing.T) {
// success // success
{&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusCreated, nil}, {&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusCreated, nil},
} { } {
router := testRouter(test.mock, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.mock, &mqs.Mock{}, rnr, tasks)
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "POST", test.path, body) _, rec := routerRequest(t, router, "POST", test.path, body)
@@ -56,6 +57,7 @@ func TestRouteCreate(t *testing.T) {
i, test.expectedError.Error(), resp.Error.Message) i, test.expectedError.Error(), resp.Error.Message)
} }
} }
cancel()
} }
} }
@@ -79,7 +81,8 @@ func TestRouteDelete(t *testing.T) {
}, },
}, "/v1/apps/a/routes/myroute", "", http.StatusOK, nil}, }, "/v1/apps/a/routes/myroute", "", http.StatusOK, nil},
} { } {
router := testRouter(test.ds, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.ds, &mqs.Mock{}, rnr, tasks)
_, rec := routerRequest(t, router, "DELETE", test.path, nil) _, rec := routerRequest(t, router, "DELETE", test.path, nil)
if rec.Code != test.expectedCode { if rec.Code != test.expectedCode {
@@ -97,6 +100,7 @@ func TestRouteDelete(t *testing.T) {
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} }
cancel()
} }
} }
@@ -105,7 +109,9 @@ func TestRouteList(t *testing.T) {
tasks := mockTasksConduit() tasks := mockTasksConduit()
defer close(tasks) defer close(tasks)
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -140,7 +146,10 @@ func TestRouteGet(t *testing.T) {
tasks := mockTasksConduit() tasks := mockTasksConduit()
defer close(tasks) defer close(tasks)
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{}, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -196,7 +205,8 @@ func TestRouteUpdate(t *testing.T) {
}, },
}, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusOK, nil}, }, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusOK, nil},
} { } {
router := testRouter(test.ds, &mqs.Mock{}, testRunner(t), tasks) rnr, cancel := testRunner(t)
router := testRouter(test.ds, &mqs.Mock{}, rnr, tasks)
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
@@ -217,5 +227,6 @@ func TestRouteUpdate(t *testing.T) {
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} }
cancel()
} }
} }

View File

@@ -76,7 +76,8 @@ func TestRouteRunnerAsyncExecution(t *testing.T) {
wg.Add(1) wg.Add(1)
fmt.Println("About to start router") fmt.Println("About to start router")
router := testRouterAsync(ds, mq, testRunner(t), tasks, func(_ context.Context, _ models.MessageQueue, task *models.Task) (*models.Task, error) { rnr, cancel := testRunner(t)
router := testRouterAsync(ds, mq, rnr, tasks, func(_ context.Context, _ models.MessageQueue, task *models.Task) (*models.Task, error) {
if test.body != task.Payload { if test.body != task.Payload {
t.Errorf("Test %d: Expected task Payload to be the same as the test body", i) t.Errorf("Test %d: Expected task Payload to be the same as the test body", i)
} }
@@ -109,5 +110,6 @@ func TestRouteRunnerAsyncExecution(t *testing.T) {
} }
wg.Wait() wg.Wait()
cancel()
} }
} }

View File

@@ -14,23 +14,27 @@ import (
"github.com/iron-io/functions/api/runner/task" "github.com/iron-io/functions/api/runner/task"
) )
func testRunner(t *testing.T) *runner.Runner { func testRunner(t *testing.T) (*runner.Runner, context.CancelFunc) {
r, err := runner.New(runner.NewFuncLogger(), runner.NewMetricLogger()) ctx, cancel := context.WithCancel(context.Background())
r, err := runner.New(ctx, runner.NewFuncLogger(), runner.NewMetricLogger())
if err != nil { if err != nil {
t.Fatal("Test: failed to create new runner") t.Fatal("Test: failed to create new runner")
} }
return r return r, cancel
} }
func TestRouteRunnerGet(t *testing.T) { func TestRouteRunnerGet(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
tasks := mockTasksConduit() tasks := mockTasksConduit()
rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{ router := testRouter(&datastore.Mock{
Apps: []*models.App{ Apps: []*models.App{
{Name: "myapp", Config: models.Config{}}, {Name: "myapp", Config: models.Config{}},
}, },
}, &mqs.Mock{}, testRunner(t), tasks) }, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -66,11 +70,14 @@ func TestRouteRunnerPost(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
tasks := mockTasksConduit() tasks := mockTasksConduit()
rnr, cancel := testRunner(t)
defer cancel()
router := testRouter(&datastore.Mock{ router := testRouter(&datastore.Mock{
Apps: []*models.App{ Apps: []*models.App{
{Name: "myapp", Config: models.Config{}}, {Name: "myapp", Config: models.Config{}},
}, },
}, &mqs.Mock{}, testRunner(t), tasks) }, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -111,7 +118,10 @@ func TestRouteRunnerExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go runner.StartWorkers(ctx, testRunner(t), tasks) rnr, cancelrnr := testRunner(t)
defer cancelrnr()
go runner.StartWorkers(ctx, rnr, tasks)
router := testRouter(&datastore.Mock{ router := testRouter(&datastore.Mock{
Apps: []*models.App{ Apps: []*models.App{
@@ -121,7 +131,7 @@ func TestRouteRunnerExecution(t *testing.T) {
{Path: "/myroute", AppName: "myapp", Image: "iron/hello", Headers: map[string][]string{"X-Function": {"Test"}}}, {Path: "/myroute", AppName: "myapp", Image: "iron/hello", Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/myerror", AppName: "myapp", Image: "iron/error", Headers: map[string][]string{"X-Function": {"Test"}}}, {Path: "/myerror", AppName: "myapp", Image: "iron/error", Headers: map[string][]string{"X-Function": {"Test"}}},
}, },
}, &mqs.Mock{}, testRunner(t), tasks) }, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string
@@ -167,7 +177,9 @@ func TestRouteRunnerTimeout(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go runner.StartWorkers(ctx, testRunner(t), tasks) rnr, cancelrnr := testRunner(t)
defer cancelrnr()
go runner.StartWorkers(ctx, rnr, tasks)
router := testRouter(&datastore.Mock{ router := testRouter(&datastore.Mock{
Apps: []*models.App{ Apps: []*models.App{
@@ -176,7 +188,7 @@ func TestRouteRunnerTimeout(t *testing.T) {
Routes: []*models.Route{ Routes: []*models.Route{
{Path: "/sleeper", AppName: "myapp", Image: "iron/sleeper", Timeout: 1}, {Path: "/sleeper", AppName: "myapp", Image: "iron/sleeper", Timeout: 1},
}, },
}, &mqs.Mock{}, testRunner(t), tasks) }, &mqs.Mock{}, rnr, tasks)
for i, test := range []struct { for i, test := range []struct {
path string path string

View File

@@ -94,9 +94,13 @@ func TestFullStack(t *testing.T) {
tasks := make(chan task.Request) tasks := make(chan task.Request)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go runner.StartWorkers(ctx, testRunner(t), tasks)
router := testRouter(ds, &mqs.Mock{}, testRunner(t), tasks) rnr, rnrcancel := testRunner(t)
defer rnrcancel()
go runner.StartWorkers(ctx, rnr, tasks)
router := testRouter(ds, &mqs.Mock{}, rnr, tasks)
for _, test := range []struct { for _, test := range []struct {
name string name string

View File

@@ -73,7 +73,7 @@ func main() {
metricLogger := runner.NewMetricLogger() metricLogger := runner.NewMetricLogger()
funcLogger := runner.NewFuncLogger() funcLogger := runner.NewFuncLogger()
rnr, err := runner.New(funcLogger, metricLogger) rnr, err := runner.New(ctx, funcLogger, metricLogger)
if err != nil { if err != nil {
log.WithError(err).Fatalln("Failed to create a runner") log.WithError(err).Fatalln("Failed to create a runner")
} }