Repository: kythe/kythe
Pull request #4714 (2020-10-14 18:26:57)

feat(tooling): add optional analysis timeout to driver


Patch:
diff --git a/kythe/go/platform/analysis/driver/driver.go b/kythe/go/platform/analysis/driver/driver.go
index 401d1e767..48f9769af 100644
--- a/kythe/go/platform/analysis/driver/driver.go
+++ b/kythe/go/platform/analysis/driver/driver.go
@@ -22,6 +22,7 @@ import (
 	"context"
 	goerrors "errors"
 	"log"
+	"time"
 
 	"kythe.io/kythe/go/platform/analysis"
 
@@ -80,9 +81,17 @@ var (
 	ErrEndOfQueue = goerrors.New("end of queue")
 )
 
+// AnalysisOptions contains extra configuration for analysis requests.
+type AnalysisOptions struct {
+	// Timeout, if nonzero, sets the given timeout for each analysis request.
+	Timeout time.Duration
+}
+
 // Driver sends compilations sequentially from a queue to an analyzer.
 type Driver struct {
 	Analyzer        analysis.CompilationAnalyzer
+	AnalysisOptions AnalysisOptions
+
 	FileDataService string
 	Context         Context             // if nil, callbacks are no-ops
 	WriteOutput     analysis.OutputFunc // if nil, output is discarded
@@ -131,12 +140,7 @@ func (d *Driver) Run(ctx context.Context, queue Queue) error {
 			}
 			err := ErrRetry
 			for err == ErrRetry {
-				err = d.analysisError(ctx, cu, d.Analyzer.Analyze(ctx, &apb.AnalysisRequest{
-					Compilation:     cu.Unit,
-					FileDataService: d.FileDataService,
-					Revision:        cu.Revision,
-					BuildId:         cu.BuildID,
-				}, d.writeOutput))
+				err = d.analysisError(ctx, cu, d.runAnalysis(ctx, cu))
 			}
 			if terr := d.teardown(ctx, cu); terr != nil {
 				if err == nil {
@@ -152,3 +156,17 @@ func (d *Driver) Run(ctx context.Context, queue Queue) error {
 		}
 	}
 }
+
+func (d *Driver) runAnalysis(ctx context.Context, cu Compilation) error {
+	if d.AnalysisOptions.Timeout != 0 {
+		var cancel func()
+		ctx, cancel = context.WithTimeout(ctx, d.AnalysisOptions.Timeout)
+		defer cancel()
+	}
+	return d.Analyzer.Analyze(ctx, &apb.AnalysisRequest{
+		Compilation:     cu.Unit,
+		FileDataService: d.FileDataService,
+		Revision:        cu.Revision,
+		BuildId:         cu.BuildID,
+	}, d.writeOutput)
+}


Test patch:
diff --git a/kythe/go/platform/analysis/driver/driver_test.go b/kythe/go/platform/analysis/driver/driver_test.go
index 316d1ad9a..ae1302c4a 100644
--- a/kythe/go/platform/analysis/driver/driver_test.go
+++ b/kythe/go/platform/analysis/driver/driver_test.go
@@ -21,8 +21,10 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log"
 	"strings"
 	"testing"
+	"time"
 
 	"kythe.io/kythe/go/platform/analysis"
 	"kythe.io/kythe/go/test/testutil"
@@ -41,6 +43,8 @@ type mock struct {
 	AnalyzeError error
 	OutputError  error
 
+	AnalysisDuration time.Duration
+
 	OutputIndex int
 	Requests    []*apb.AnalysisRequest
 }
@@ -63,6 +67,10 @@ const buildID = "aabbcc"
 
 // Analyze implements the analysis.CompilationAnalyzer interface.
 func (m *mock) Analyze(ctx context.Context, req *apb.AnalysisRequest, out analysis.OutputFunc) error {
+	if m.AnalysisDuration != 0 {
+		log.Printf("Waiting %s for analysis request", m.AnalysisDuration)
+		time.Sleep(m.AnalysisDuration)
+	}
 	m.OutputIndex = 0
 	m.Requests = append(m.Requests, req)
 	for _, o := range m.Outputs {
@@ -77,6 +85,9 @@ func (m *mock) Analyze(ctx context.Context, req *apb.AnalysisRequest, out analys
 	if req.BuildId != buildID {
 		m.t.Errorf("Missing build ID")
 	}
+	if err := ctx.Err(); err != nil {
+		return err
+	}
 	return m.AnalyzeError
 }
 
@@ -116,7 +127,7 @@ func (t testContext) AnalysisError(ctx context.Context, unit Compilation, err er
 	if t.analysisError != nil {
 		return t.analysisError(ctx, unit, err)
 	}
-	return nil
+	return err
 }
 
 func TestDriverInvalid(t *testing.T) {
@@ -293,6 +304,29 @@ func TestDriverTeardown(t *testing.T) {
 	}
 }
 
+func TestDriverTimeout(t *testing.T) {
+	timeout := 10 * time.Millisecond
+	m := &mock{
+		t:            t,
+		Outputs:      outs("a", "b", "c"),
+		Compilations: comps("target1", "target2"),
+
+		AnalysisDuration: timeout * 3,
+	}
+	d := &Driver{
+		Analyzer:        m,
+		AnalysisOptions: AnalysisOptions{Timeout: timeout},
+		WriteOutput:     m.out(),
+		Context:         testContext{},
+	}
+	if err := d.Run(context.Background(), m); !errors.Is(err, context.DeadlineExceeded) {
+		t.Errorf("Expected error %v; found %v", context.DeadlineExceeded, err)
+	}
+	if len(m.Requests) != 1 {
+		t.Errorf("Expected 1 AnalysisRequest; found %v", m.Requests)
+	}
+}
+
 func outs(vals ...string) (as []*apb.AnalysisOutput) {
 	for _, val := range vals {
 		as = append(as, &apb.AnalysisOutput{Value: []byte(val)})