9
9
"time"
10
10
11
11
"github.com/hashicorp/golang-lru/v2/expirable"
12
+ "go.opentelemetry.io/otel/trace"
13
+ "go.opentelemetry.io/otel/trace/noop"
12
14
)
13
15
14
16
// Loader is the function type for loading data
@@ -17,11 +19,6 @@ type Loader[K comparable, V any] func(context.Context, []K) []Result[V]
17
19
// Interface is a `DataLoader` Interface which defines a public API for loading data from a particular
18
20
// data back-end with unique keys such as the `id` column of a SQL table or
19
21
// document name in a MongoDB database, given a batch loading function.
20
- //
21
- // Each `DataLoader` instance should contain a unique memoized cache. Use caution when
22
- // used in long-lived applications or those which serve many users with
23
- // different access permissions and consider creating a new instance per
24
- // web request.
25
22
type Interface [K comparable , V any ] interface {
26
23
// Load loads a single key
27
24
Load (context.Context , K ) Result [V ]
@@ -47,6 +44,8 @@ type config struct {
47
44
CacheSize int
48
45
// CacheExpire is the duration to expire cache items, Default is 1 minute
49
46
CacheExpire time.Duration
47
+ // TracerProvider is the tracer provider to use for tracing
48
+ TracerProvider trace.TracerProvider
50
49
}
51
50
52
51
// dataLoader is the main struct for the dataloader
@@ -56,6 +55,7 @@ type dataLoader[K comparable, V any] struct {
56
55
config config
57
56
mu sync.Mutex
58
57
batch []K
58
+ batchCtx []context.Context
59
59
chs map [K ][]chan Result [V ]
60
60
stopSchedule chan struct {}
61
61
}
@@ -90,11 +90,17 @@ func New[K comparable, V any](loader Loader[K, V], options ...Option) Interface[
90
90
91
91
// Load loads a single key
92
92
func (d * dataLoader [K , V ]) Load (ctx context.Context , key K ) Result [V ] {
93
+ ctx , span := d .startTrace (ctx , "dataLoader.Load" )
94
+ defer span .End ()
95
+
93
96
return <- d .goLoad (ctx , key )
94
97
}
95
98
96
99
// LoadMany loads multiple keys
97
100
func (d * dataLoader [K , V ]) LoadMany (ctx context.Context , keys []K ) []Result [V ] {
101
+ ctx , span := d .startTrace (ctx , "dataLoader.LoadMany" )
102
+ defer span .End ()
103
+
98
104
chs := make ([]<- chan Result [V ], len (keys ))
99
105
for i , key := range keys {
100
106
chs [i ] = d .goLoad (ctx , key )
@@ -104,12 +110,14 @@ func (d *dataLoader[K, V]) LoadMany(ctx context.Context, keys []K) []Result[V] {
104
110
for i , ch := range chs {
105
111
results [i ] = <- ch
106
112
}
107
-
108
113
return results
109
114
}
110
115
111
116
// LoadMap loads multiple keys and returns a map of results
112
117
func (d * dataLoader [K , V ]) LoadMap (ctx context.Context , keys []K ) map [K ]Result [V ] {
118
+ ctx , span := d .startTrace (ctx , "dataLoader.LoadMap" )
119
+ defer span .End ()
120
+
113
121
chs := make ([]<- chan Result [V ], len (keys ))
114
122
for i , key := range keys {
115
123
chs [i ] = d .goLoad (ctx , key )
@@ -167,6 +175,10 @@ func (d *dataLoader[K, V]) goLoad(ctx context.Context, key K) <-chan Result[V] {
167
175
168
176
// Lock the DataLoader
169
177
d .mu .Lock ()
178
+ if d .config .TracerProvider != nil {
179
+ d .batchCtx = append (d .batchCtx , ctx )
180
+ }
181
+
170
182
if len (d .batch ) == 0 {
171
183
// If there are no keys in the current batch, schedule a new batch timer
172
184
d .stopSchedule = make (chan struct {})
@@ -187,7 +199,7 @@ func (d *dataLoader[K, V]) goLoad(ctx context.Context, key K) <-chan Result[V] {
187
199
// If the current batch is full, start processing it
188
200
if len (d .batch ) >= d .config .BatchSize {
189
201
// spawn a new goroutine to process the batch
190
- go d .processBatch (ctx , d .batch , d .chs )
202
+ go d .processBatch (ctx , d .batch , d .batchCtx , d . chs )
191
203
close (d .stopSchedule )
192
204
// Create a new batch, and a new set of channels
193
205
d .reset ()
@@ -205,7 +217,7 @@ func (d *dataLoader[K, V]) scheduleBatch(ctx context.Context, stopSchedule <-cha
205
217
case <- time .After (d .config .Wait ):
206
218
d .mu .Lock ()
207
219
if len (d .batch ) > 0 {
208
- go d .processBatch (ctx , d .batch , d .chs )
220
+ go d .processBatch (ctx , d .batch , d .batchCtx , d . chs )
209
221
d .reset ()
210
222
}
211
223
d .mu .Unlock ()
@@ -215,7 +227,7 @@ func (d *dataLoader[K, V]) scheduleBatch(ctx context.Context, stopSchedule <-cha
215
227
}
216
228
217
229
// processBatch processes a batch of keys
218
- func (d * dataLoader [K , V ]) processBatch (ctx context.Context , keys []K , chs map [K ][]chan Result [V ]) {
230
+ func (d * dataLoader [K , V ]) processBatch (ctx context.Context , keys []K , batchCtx []context. Context , chs map [K ][]chan Result [V ]) {
219
231
defer func () {
220
232
if r := recover (); r != nil {
221
233
const size = 64 << 10
@@ -233,20 +245,37 @@ func (d *dataLoader[K, V]) processBatch(ctx context.Context, keys []K, chs map[K
233
245
return
234
246
}
235
247
}()
236
- results := d .loader (ctx , keys )
237
248
249
+ if d .config .TracerProvider != nil {
250
+ // Create a span with links to the batch contexts, which enables trace propagation
251
+ // We should deduplicate identical batch contexts to avoid creating duplicate links.
252
+ links := make ([]trace.Link , 0 , len (keys ))
253
+ seen := make (map [context.Context ]struct {}, len (batchCtx ))
254
+ for _ , bCtx := range batchCtx {
255
+ if _ , ok := seen [bCtx ]; ok {
256
+ continue
257
+ }
258
+ links = append (links , trace.Link {SpanContext : trace .SpanContextFromContext (bCtx )})
259
+ seen [bCtx ] = struct {}{}
260
+ }
261
+ var span trace.Span
262
+ ctx , span = d .config .TracerProvider .Tracer ("dataLoader" ).Start (ctx , "dataLoader.Batch" , trace .WithLinks (links ... ))
263
+ defer span .End ()
264
+ }
265
+
266
+ results := d .loader (ctx , keys )
238
267
for i , key := range keys {
239
268
if results [i ].err == nil && d .cache != nil {
240
269
d .cache .Add (key , results [i ].data )
241
270
}
242
-
243
271
sendResult (chs [key ], results [i ])
244
272
}
245
273
}
246
274
247
275
// reset resets the DataLoader
248
276
func (d * dataLoader [K , V ]) reset () {
249
277
d .batch = make ([]K , 0 , d .config .BatchSize )
278
+ d .batchCtx = make ([]context.Context , 0 , d .config .BatchSize )
250
279
d .chs = make (map [K ][]chan Result [V ], d .config .BatchSize )
251
280
}
252
281
@@ -257,3 +286,11 @@ func sendResult[V any](chs []chan Result[V], result Result[V]) {
257
286
close (ch )
258
287
}
259
288
}
289
+
290
+ // startTrace starts a trace span
291
+ func (d * dataLoader [K , V ]) startTrace (ctx context.Context , name string ) (context.Context , trace.Span ) {
292
+ if d .config .TracerProvider != nil {
293
+ return d .config .TracerProvider .Tracer ("dataLoader" ).Start (ctx , name )
294
+ }
295
+ return ctx , noop.Span {}
296
+ }
0 commit comments