Skip to content

Commit 3354810

Browse files
committed
Propagate route harvest cancellation
Pass harvest contexts through Java
1 parent 4a7d78e commit 3354810

8 files changed

Lines changed: 206 additions & 62 deletions

File tree

pkg/internal/transform/route/harvest/harvester.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package harvest // import "go.opentelemetry.io/obi/pkg/internal/transform/route/
55

66
import (
77
"context"
8+
"errors"
89
"log/slog"
910
"os"
1011
"path/filepath"
@@ -28,7 +29,7 @@ type RouteHarvester struct {
2829
mux *sync.Mutex
2930

3031
// testing related
31-
javaExtractRoutes func(fileInfo *exec.FileInfo) (*RouteHarvesterResult, error)
32+
javaExtractRoutes func(ctx context.Context, fileInfo *exec.FileInfo) (*RouteHarvesterResult, error)
3233
nodeExtractRoutes func(pid app.PID) (*RouteHarvesterResult, error)
3334
}
3435

@@ -108,7 +109,7 @@ func (h *RouteHarvester) HarvestRoutes(fileInfo *exec.FileInfo) (*RouteHarvester
108109
switch fileInfo.SDKLanguage() {
109110
case svc.InstrumentableJava:
110111
if _, ok := h.disabled[svc.InstrumentableJava]; !ok {
111-
r, err := h.javaExtractRoutes(fileInfo)
112+
r, err := h.javaExtractRoutes(ctx, fileInfo)
112113
if err != nil {
113114
resultChan <- result{err: err}
114115
return
@@ -138,6 +139,10 @@ func (h *RouteHarvester) HarvestRoutes(fileInfo *exec.FileInfo) (*RouteHarvester
138139
// Wait for either completion or timeout
139140
select {
140141
case result := <-resultChan:
142+
if errors.Is(result.err, context.DeadlineExceeded) {
143+
h.log.Warn("route harvesting timed out", "timeout", h.timeout, "pid", fileInfo.Pid())
144+
return nil, &HarvestError{Message: "route harvesting timed out"}
145+
}
141146
return result.r, result.err
142147
case <-ctx.Done():
143148
h.log.Warn("route harvesting timed out", "timeout", h.timeout, "pid", fileInfo.Pid())

pkg/internal/transform/route/harvest/harvester_test.go

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package harvest
55

66
import (
7+
"context"
78
"errors"
89
"os"
910
"path/filepath"
@@ -21,56 +22,64 @@ import (
2122
)
2223

2324
// successfulExtractRoutes simulates a successful route extraction
24-
func successfulExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
25+
func successfulExtractRoutes(context.Context, app.PID) (*RouteHarvesterResult, error) {
2526
return &RouteHarvesterResult{
2627
Routes: []string{"/api/users", "/api/orders"},
2728
Kind: CompleteRoutes,
2829
}, nil
2930
}
3031

3132
// errorExtractRoutes simulates an error during route extraction
32-
func errorExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
33+
func errorExtractRoutes(context.Context, app.PID) (*RouteHarvesterResult, error) {
3334
return nil, errors.New("failed to connect to Java process")
3435
}
3536

3637
// timeoutExtractRoutes simulates a slow operation that will timeout
37-
func timeoutExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
38-
// Sleep longer than any reasonable timeout
39-
time.Sleep(5 * time.Second)
40-
return &RouteHarvesterResult{
41-
Routes: []string{"/api/delayed"},
42-
Kind: CompleteRoutes,
43-
}, nil
38+
func timeoutExtractRoutes(ctx context.Context, _ app.PID) (*RouteHarvesterResult, error) {
39+
<-ctx.Done()
40+
return nil, ctx.Err()
4441
}
4542

4643
// panicExtractRoutes simulates a panic during route extraction
47-
func panicExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
44+
func panicExtractRoutes(context.Context, app.PID) (*RouteHarvesterResult, error) {
4845
panic("unexpected error in java route extraction")
4946
}
5047

5148
// slowButSuccessfulExtractRoutes simulates a slow but successful operation
52-
func slowButSuccessfulExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
53-
time.Sleep(50 * time.Millisecond) // Slow but within timeout
49+
func slowButSuccessfulExtractRoutes(ctx context.Context, _ app.PID) (*RouteHarvesterResult, error) {
50+
select {
51+
case <-ctx.Done():
52+
return nil, ctx.Err()
53+
case <-time.After(50 * time.Millisecond): // Slow but within timeout
54+
}
5455
return &RouteHarvesterResult{
5556
Routes: []string{"/api/slow"},
5657
Kind: PartialRoutes,
5758
}, nil
5859
}
5960

6061
// emptyResultExtractRoutes simulates successful extraction with no routes
61-
func emptyResultExtractRoutes(app.PID) (*RouteHarvesterResult, error) {
62+
func emptyResultExtractRoutes(context.Context, app.PID) (*RouteHarvesterResult, error) {
6263
return &RouteHarvesterResult{
6364
Routes: []string{},
6465
Kind: CompleteRoutes,
6566
}, nil
6667
}
6768

68-
func javaExtract(fn func(app.PID) (*RouteHarvesterResult, error)) func(*exec.FileInfo) (*RouteHarvesterResult, error) {
69-
return func(fileInfo *exec.FileInfo) (*RouteHarvesterResult, error) {
70-
return fn(fileInfo.Pid())
69+
func javaExtract(fn func(context.Context, app.PID) (*RouteHarvesterResult, error)) func(context.Context, *exec.FileInfo) (*RouteHarvesterResult, error) {
70+
return func(ctx context.Context, fileInfo *exec.FileInfo) (*RouteHarvesterResult, error) {
71+
return fn(ctx, fileInfo.Pid())
7172
}
7273
}
7374

75+
func successfulNodeExtractRoutes(pid app.PID) (*RouteHarvesterResult, error) {
76+
return successfulExtractRoutes(context.Background(), pid)
77+
}
78+
79+
func errorNodeExtractRoutes(pid app.PID) (*RouteHarvesterResult, error) {
80+
return errorExtractRoutes(context.Background(), pid)
81+
}
82+
7483
func createTestFileInfo(language svc.InstrumentableType) *exec.FileInfo {
7584
return exec.New(exec.Init{
7685
Pid: 12345,
@@ -130,6 +139,24 @@ func TestHarvestRoutes_Timeout(t *testing.T) {
130139
assert.Greater(t, elapsed, 90*time.Millisecond)
131140
}
132141

142+
func TestHarvestRoutes_ContextDeadlineResultReturnsTimeout(t *testing.T) {
143+
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 1*time.Second)
144+
harvester.javaExtractRoutes = func(context.Context, *exec.FileInfo) (*RouteHarvesterResult, error) {
145+
return nil, context.DeadlineExceeded
146+
}
147+
148+
fileInfo := createTestFileInfo(svc.InstrumentableJava)
149+
150+
result, err := harvester.HarvestRoutes(fileInfo)
151+
152+
require.Error(t, err)
153+
assert.Nil(t, result)
154+
155+
var harvestErr *HarvestError
156+
require.ErrorAs(t, err, &harvestErr)
157+
assert.Equal(t, "route harvesting timed out", harvestErr.Message)
158+
}
159+
133160
func TestHarvestRoutes_Panic(t *testing.T) {
134161
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 1*time.Second)
135162
harvester.javaExtractRoutes = javaExtract(panicExtractRoutes)
@@ -178,7 +205,7 @@ func TestHarvestRoutes_EmptyResult(t *testing.T) {
178205
func TestHarvestRoutes_NonJavaLanguage(t *testing.T) {
179206
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 1*time.Second)
180207
// javaExtractRoutes should not be called for non-Java languages
181-
harvester.javaExtractRoutes = func(_ *exec.FileInfo) (*RouteHarvesterResult, error) {
208+
harvester.javaExtractRoutes = func(context.Context, *exec.FileInfo) (*RouteHarvesterResult, error) {
182209
t.Fatal("javaExtractRoutes should not be called for non-Java languages")
183210
return nil, nil
184211
}
@@ -193,7 +220,14 @@ func TestHarvestRoutes_NonJavaLanguage(t *testing.T) {
193220

194221
func TestHarvestRoutes_MultipleTimeouts(t *testing.T) {
195222
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 50*time.Millisecond)
196-
harvester.javaExtractRoutes = javaExtract(timeoutExtractRoutes)
223+
224+
exited := make(chan struct{}, 3)
225+
harvester.javaExtractRoutes = func(ctx context.Context, fileInfo *exec.FileInfo) (*RouteHarvesterResult, error) {
226+
defer func() {
227+
exited <- struct{}{}
228+
}()
229+
return timeoutExtractRoutes(ctx, fileInfo.Pid())
230+
}
197231

198232
fileInfo := createTestFileInfo(svc.InstrumentableJava)
199233

@@ -208,11 +242,15 @@ func TestHarvestRoutes_MultipleTimeouts(t *testing.T) {
208242
require.ErrorAs(t, err, &harvestErr, "iteration %d should return HarvestError", i)
209243
assert.Equal(t, "route harvesting timed out", harvestErr.Message, "iteration %d should have timeout message", i)
210244
}
245+
246+
require.Eventually(t, func() bool {
247+
return len(exited) == 3
248+
}, time.Second, time.Millisecond)
211249
}
212250

213251
func TestHarvestNodejsRoutes_Successful(t *testing.T) {
214252
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 1*time.Second)
215-
harvester.nodeExtractRoutes = successfulExtractRoutes
253+
harvester.nodeExtractRoutes = successfulNodeExtractRoutes
216254

217255
fileInfo := createTestFileInfo(svc.InstrumentableNodejs)
218256

@@ -226,7 +264,7 @@ func TestHarvestNodejsRoutes_Successful(t *testing.T) {
226264

227265
func TestHarvestNodejsRoutes_Error(t *testing.T) {
228266
harvester := NewRouteHarvester(&services.RouteHarvestingConfig{}, []services.RouteHarvesterLanguage{}, 1*time.Second)
229-
harvester.nodeExtractRoutes = errorExtractRoutes
267+
harvester.nodeExtractRoutes = errorNodeExtractRoutes
230268

231269
fileInfo := createTestFileInfo(svc.InstrumentableNodejs)
232270

pkg/internal/transform/route/harvest/java.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package harvest // import "go.opentelemetry.io/obi/pkg/internal/transform/route/harvest"
77

88
import (
9+
"context"
910
"fmt"
1011
"log/slog"
1112

@@ -23,8 +24,8 @@ func NewJavaRoutesHarvester() *JavaRoutes {
2324
}
2425
}
2526

26-
func (h *JavaRoutes) ExtractRoutes(fileInfo *exec.FileInfo) (*RouteHarvesterResult, error) {
27-
routes, err := javaharvest.ExtractRoutes(fileInfo)
27+
func (h *JavaRoutes) ExtractRoutes(ctx context.Context, fileInfo *exec.FileInfo) (*RouteHarvesterResult, error) {
28+
routes, err := javaharvest.ExtractRoutes(ctx, fileInfo)
2829
if err != nil {
2930
return nil, fmt.Errorf("extracting Java routes from class files: %w", err)
3031
}

0 commit comments

Comments
 (0)