Skip to content

Commit c9af890

Browse files
authored
feat: expose concurrency limits and inflight resolves (#1501)
<!-- Important: Before developing new features, please open an issue to discuss your ideas with the maintainers. This ensures project alignment and helps avoid unnecessary work for you. Thank you for your contribution! Please provide a detailed description below and ensure you've met all the requirements. Squashed commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard to facilitate changelog generation. Please ensure your PR title follows the Conventional Commits specification, using the appropriate type (e.g., feat, fix, docs) and scope. Examples of good PR titles: - 💥feat!: change implementation in an non-backward compatible way - ✨feat(auth): add support for OAuth2 login - 🐞fix(router): add support for custom metrics - 📚docs(README): update installation instructions - 🧹chore(deps): bump dependencies to latest versions --> @coderabbitai summary ## Checklist - [ ] I have discussed my proposed changes in an issue and have received approval to proceed. - [ ] I have followed the coding standards of the project. - [ ] Tests or benchmarks have been added or updated. ## Open Source AI Manifesto This project follows the principles of the [Open Source AI Manifesto](https://human-oss.dev). Please ensure your contribution aligns with its principles. <!-- Please add any additional information or context regarding your changes here. -->
1 parent def8796 commit c9af890

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

v2/pkg/engine/resolve/resolve.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ func (r *Resolver) SetAsyncErrorWriter(w AsyncErrorWriter) {
106106
r.errorFormatter = w
107107
}
108108

109+
// MaxConcurrentResolves returns the configured maximum number of concurrent
110+
// resolves (the size of the resolver semaphore). Use only for stats.
111+
func (r *Resolver) MaxConcurrentResolves() int {
112+
return cap(r.maxConcurrency)
113+
}
114+
115+
// InflightResolves returns the number of resolves currently holding a
116+
// semaphore slot. The value is a snapshot and may change immediately after
117+
// being read. Use only for stats.
118+
func (r *Resolver) InflightResolves() int {
119+
return cap(r.maxConcurrency) - len(r.maxConcurrency)
120+
}
121+
109122
type tools struct {
110123
resolvable *Resolvable
111124
loader *Loader
@@ -338,6 +351,16 @@ type GraphQLResolveInfo struct {
338351

339352
// ResolveDeduplicated indicates whether the resolution of the entire operation was deduplicated via single flight
340353
ResolveDeduplicated bool
354+
355+
// ResponseResolveStartTime is the time when GraphQL response completion and rendering started.
356+
ResponseResolveStartTime time.Time
357+
// ResponseResolveDuration is the time spent completing and rendering the GraphQL response.
358+
ResponseResolveDuration time.Duration
359+
360+
// ResponseWriteStartTime is the time when the resolved response started writing to the client.
361+
ResponseWriteStartTime time.Time
362+
// ResponseWriteDuration is the time spent writing the resolved response to the client.
363+
ResponseWriteDuration time.Duration
341364
}
342365

343366
func (r *Resolver) ResolveGraphQLResponse(ctx *Context, response *GraphQLResponse, data []byte, writer io.Writer) (*GraphQLResolveInfo, error) {
@@ -364,7 +387,10 @@ func (r *Resolver) ResolveGraphQLResponse(ctx *Context, response *GraphQLRespons
364387
}
365388
}
366389

390+
responseResolveStart := time.Now()
367391
err = t.resolvable.Resolve(ctx.ctx, response.Data, response.Fetches, writer)
392+
resp.ResponseResolveStartTime = responseResolveStart
393+
resp.ResponseResolveDuration = time.Since(responseResolveStart)
368394
if err != nil {
369395
return nil, err
370396
}
@@ -389,7 +415,10 @@ func (r *Resolver) ArenaResolveGraphQLResponse(ctx *Context, response *GraphQLRe
389415
if ctx.SetDeduplicationData != nil && inflight.SharedData != nil {
390416
ctx.SetDeduplicationData(ctx.ctx, inflight.SharedData)
391417
}
418+
responseWriteStart := time.Now()
392419
_, err = writer.Write(inflight.Data)
420+
resp.ResponseWriteStartTime = responseWriteStart
421+
resp.ResponseWriteDuration = time.Since(responseWriteStart)
393422
return resp, err
394423
}
395424

@@ -423,7 +452,10 @@ func (r *Resolver) ArenaResolveGraphQLResponse(ctx *Context, response *GraphQLRe
423452
// only when loading is done, acquire an arena for the response buffer
424453
responseArena := r.responseBufferPool.Acquire(ctx.Request.ID)
425454
buf := arena.NewArenaBuffer(responseArena.Arena)
455+
responseResolveStart := time.Now()
426456
err = t.resolvable.Resolve(ctx.ctx, response.Data, response.Fetches, buf)
457+
resp.ResponseResolveStartTime = responseResolveStart
458+
resp.ResponseResolveDuration = time.Since(responseResolveStart)
427459
if err != nil {
428460
r.inboundRequestSingleFlight.FinishErr(inflight, err)
429461
r.resolveArenaPool.Release(resolveArena)
@@ -439,7 +471,10 @@ func (r *Resolver) ArenaResolveGraphQLResponse(ctx *Context, response *GraphQLRe
439471
// this includes flushing and syscalls
440472
// as such, it can take some time
441473
// which is why we split the arenas and released the first one
474+
responseWriteStart := time.Now()
442475
_, err = writer.Write(buf.Bytes())
476+
resp.ResponseWriteStartTime = responseWriteStart
477+
resp.ResponseWriteDuration = time.Since(responseWriteStart)
443478
// Extract data from the leader's context to share with singleflight followers.
444479
// This runs after the leader has fully resolved and written its response, so all
445480
// subgraph response headers have been accumulated on the leader's context.

v2/pkg/engine/resolve/resolve_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,30 @@ func (customErrResolve) Resolve(_ *Context, value []byte) ([]byte, error) {
232232
return nil, errors.New("custom error")
233233
}
234234

235+
func TestResolver_ConcurrencyGetters(t *testing.T) {
236+
ctx, cancel := context.WithCancel(t.Context())
237+
defer cancel()
238+
239+
r := New(ctx, ResolverOptions{
240+
MaxConcurrency: 4,
241+
AsyncErrorWriter: &TestErrorWriter{},
242+
})
243+
244+
require.Equal(t, 4, r.MaxConcurrentResolves())
245+
require.Equal(t, 0, r.InflightResolves())
246+
247+
<-r.maxConcurrency
248+
<-r.maxConcurrency
249+
require.Equal(t, 2, r.InflightResolves())
250+
require.Equal(t, 4, r.MaxConcurrentResolves())
251+
252+
r.maxConcurrency <- struct{}{}
253+
require.Equal(t, 1, r.InflightResolves())
254+
255+
r.maxConcurrency <- struct{}{}
256+
require.Equal(t, 0, r.InflightResolves())
257+
}
258+
235259
func TestResolver_ResolveNode(t *testing.T) {
236260
testFn := func(enableSingleFlight bool, fn func(t *testing.T, ctrl *gomock.Controller) (response *GraphQLResponse, ctx Context, expectedOutput string)) func(t *testing.T) {
237261
ctrl := gomock.NewController(t)

0 commit comments

Comments
 (0)