Skip to content

app/vlselect: expose the VL-Partial-Response HTTP header#1438

Open
vadimalekseev wants to merge 1 commit into
masterfrom
partial-response-header
Open

app/vlselect: expose the VL-Partial-Response HTTP header#1438
vadimalekseev wants to merge 1 commit into
masterfrom
partial-response-header

Conversation

@vadimalekseev
Copy link
Copy Markdown
Member

@vadimalekseev vadimalekseev commented May 20, 2026

The header value can be true or false, depending on whether the query was executed partially or completely. For streaming queries that do not require buffering the final result in memory, this header will contain the value unknown.

Closes #718
Updates #1208

@vadimalekseev vadimalekseev force-pushed the partial-response-header branch from 0e007d4 to 528bd4a Compare May 20, 2026 15:28
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread app/vlstorage/netselect/netselect.go
@vadimalekseev vadimalekseev force-pushed the partial-response-header branch 2 times, most recently from c0a7f4a to 8e03dd7 Compare May 20, 2026 15:41
@vadimalekseev vadimalekseev requested review from func25 and valyala May 20, 2026 15:47
Comment thread lib/logstorage/query_stats.go Outdated
@vadimalekseev vadimalekseev force-pushed the partial-response-header branch 3 times, most recently from d9dc261 to 3622e0a Compare May 21, 2026 08:14
Comment on lines 799 to 802
if err == nil {
// At least a single vlstorage returned full response.
return nil
return true, nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may hide non-availability errors from the user when at least one vlstorage node succeeds, e.g.,[nil, nonAvailabilityError] returns no the first nil element here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did I understand correctly that this issue also exists in the master branch?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that the current behavior was correct, but it turns out this logic was correct before and was accidentally regressed by 348f103. So this bug already exists on master

Comment on lines +1825 to +1826
qc := qs.QueryCompleteness
h.Set("VL-Partial-Response", getPartialResponseHeaderValue(qc))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This read may race with atomic writes to qs.QueryCompleteness. Adding a method such as QueryStats.GetQueryCompleteness(), which uses atomic.LoadUint32(), and use it here?

Copy link
Copy Markdown
Member Author

@vadimalekseev vadimalekseev May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, there was such a method, but I removed it to simplify the code, because:

  1. Only one goroutine can call writeResponseHeaders.
  2. The other goroutines wait for the first one to finish calling writeResponseHeaders here:
    before updating the QueryCompleteness value here:
    defer qctx.QueryStats.UpdateAtomic(qsLocal)

Should we explicitly sync this, do you think?

Copy link
Copy Markdown
Member Author

@vadimalekseev vadimalekseev May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I double-checked the code, and the fact that it doesn't need synchronization is really counter-intuitive. I will add explicit synchronization

See the comment bellow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though this counter-intuitive behavior is basically the core of the whole PR: we cant let another goroutine write QueryCompletenessComplete, otherwise we'll set the partial response to 'false' for a streaming request.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More explicit (but complex) implementation described here: #718 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I don't understand, I don't mean waiting until all goroutines finish before writing the header. I mean using atomic load for reading QueryCompleteness, since the field is updated via atomic/CAS elsewhere. This doesn't change the timing. Could you explain more?

Copy link
Copy Markdown
Member Author

@vadimalekseev vadimalekseev May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @func25, I've updated the runQuery function, so QueryStats updates only after the wg.Done(). Could you please check again?

Previously, there actually could have been a race condition if an empty response was received from vlstorage. In that case, we would never have called writeBlock, which used to act as a mutex due to writeResponseHeadersOnce.

This could lead to a situation where one vlstorage returned an empty response, while the second one was unavailable. In this case, we could potentially set QueryCompletenessComplete instead of Partial. The current code should avoid this situation.

Comment thread app/vlselect/logsql/logsql.go Outdated
Comment thread lib/logstorage/query_stats.go
@vadimalekseev vadimalekseev force-pushed the partial-response-header branch 2 times, most recently from 4f71f8d to da98e20 Compare May 21, 2026 09:46
Comment on lines +786 to 796
noErrors := true
for _, err := range errs {
if err != nil {
noErrors = false
break
}
return nil
}
if noErrors {
// Not a partial response.
return false, nil
}
Copy link
Copy Markdown
Member

@func25 func25 May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you fix the issue in master, I think this snippet is not needed, it can be replaced with

var unavailableErr error
successes := 0

for _, err := range errs {
	if err == nil {
		successes++
		continue
	}
        if !isUnavailableBackendError(err) {
	        ...
        }
        unavailableErr = err
}

Comment thread lib/logstorage/query_stats.go
@vadimalekseev vadimalekseev force-pushed the partial-response-header branch from da98e20 to 8a7885c Compare May 22, 2026 09:21
The header value can be `true` or `false`, depending on whether the query was executed partially or completely.
For streaming queries that do not require buffering the final result in memory, this header will contain the value `unknown`.
@vadimalekseev vadimalekseev force-pushed the partial-response-header branch from 8a7885c to 8e0b506 Compare May 22, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

query: expose partial response header

2 participants