sandbox: reduce container starvation

Creating a container in the sandbox takes 500ms to 1s. The sandbox was
creating containers serially, but serving requests in parallel. This
means that we can be starved for workers with a trivial number of
requests.

In addition, the sandbox in production is not CPU bound, meaning we
probably have room to do some extra work while serving a request.

This CL introduces a worker pool to create containers. It also changes
the readyContainer chan to unbuffered to avoid having twice as many
containers as we expect while idle (the container waiting to be sent
plus those already in the channel's buffer).

Updates golang/go#25224
Updates golang/go#38530

Change-Id: I0e535cf65409c3dbf32329577a1c0687c2614a0d
Reviewed-on: https://go-review.googlesource.com/c/playground/+/229981
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go
index 350eeea..ffeb829 100644
--- a/sandbox/sandbox.go
+++ b/sandbox/sandbox.go
@@ -117,7 +117,7 @@
 	}
 	log.Printf("Go playground sandbox starting.")
 
-	readyContainer = make(chan *Container, *numWorkers)
+	readyContainer = make(chan *Container)
 	runSem = make(chan struct{}, *numWorkers)
 	go handleSignals()
 
@@ -148,7 +148,7 @@
 	mux.Handle("/", ochttp.WithRouteTag(http.HandlerFunc(rootHandler), "/"))
 	mux.Handle("/run", ochttp.WithRouteTag(http.HandlerFunc(runHandler), "/run"))
 
-	go makeWorkers()
+	makeWorkers()
 	go internal.PeriodicallyDo(context.Background(), 10*time.Second, func(ctx context.Context, _ time.Time) {
 		countDockerContainers(ctx)
 	})
@@ -364,6 +364,12 @@
 func makeWorkers() {
 	ctx := context.Background()
 	stats.Record(ctx, mMaxContainers.M(int64(*numWorkers)))
+	for i := 0; i < *numWorkers; i++ {
+		go workerLoop(ctx)
+	}
+}
+
+func workerLoop(ctx context.Context) {
 	for {
 		c, err := startContainer(ctx)
 		if err != nil {