Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send VFP_END with last data for v1f chunked & read ahead #3809

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

nigoroll
Copy link
Member

While working on request body caching improvements, it was noticed that, for chunked encoding, we do not know the request body size before we have attempted to read at least one additional byte because the chunked fetch did not return VFP_END with the last chunk, but rather only as a zero-length additional chunk.

This commit changes chunked encoding processing to read the next chunk header before returning the last bytes of the previous chunk. This allows to return VFP_END properly.

Test cases are adjusted accordingly.

Diff is best viewed with -b option

nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request May 29, 2022
With varnishcache#3809 in place, we can now differentiate properly between the cache
size being exactly right and too small.
nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request May 29, 2022
With varnishcache#3809 in place, we can now differentiate properly between the cache
size being exactly right and too small.
@bsdphk
Copy link
Contributor

bsdphk commented May 30, 2022

We should only attempt to read the next chunked header if we have read-ahead bytes.

Blocking for the next chunked header will break some web-applications which rely on complete concurrent delivery of chunks.

@nigoroll
Copy link
Member Author

We should only attempt to read the next chunked header if we have read-ahead bytes.

This is implemented with the last force-push as of 24b9776. Regarding the possible reduction of system calls, I would volunteer to do this next.

I also restructered the commits and noticed another issue on the way: We accepted chunks without the chunk-end (CR)?LF. This is fixed in 4417928

nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request May 30, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
@nigoroll nigoroll force-pushed the v1f_chunked_vfp_end branch 2 times, most recently from 9226b17 to 7c1160e Compare May 31, 2022 10:09
nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request May 31, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
@nigoroll nigoroll changed the title Send VFP_END with last data for v1f chunked Send VFP_END with last data for v1f chunked & read ahead May 31, 2022
@nigoroll
Copy link
Member Author

I have updated this PR with a readahead implementation for chunked encoding.
First things first, for s00003.vtc, this brings down the number of read() calls from v1f_read() from 76 in 55e4a20 to 33 readv() calls in 819a5df (factor 2.3 improvement). This test is not a best case for the patch.
The change is split into three commits:

  • Prepare state for v1f_chunked_* to hold the buffer
  • Change v1f_read() to readv() to prepare for reads into the data and readahead buffers
  • The actual readahead implementation

@nigoroll
Copy link
Member Author

nigoroll commented Jun 3, 2022

Note on the last force-push 3573c33: While working on #3798 together with this PR, I noticed that we can not put the V1F chunked state on any workspace. See commit message of 6d9f54f for details.

nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request Jun 3, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
@dridi
Copy link
Member

dridi commented Jun 13, 2022

While this looks overall correct, it also complicates chunked parsing a great deal.

Someone suggested (I think @bsdphk) that maybe it was time to have a receive buffer at the session level where we could always read up to the remaining buffer size and not be worried about cross-request boundaries. I suspect this would also simplify h2 parsing a great deal.

nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request Jun 24, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request Jun 24, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
Until now, we read the (CR)?LF at the end of a chunk as part of the
next chunk header (see: /* Skip leading whitespace */).

For a follow up commit, we are going to want to know if the next chunk
header is available for read, so we now consume the chunk end as part
of the chunk itself.

This also fixes a corner case: We previously accepted chunks with a
missing end-of-chunk (see fix of r01729.vtc).

Ref: https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
... which we are going to need in a follow up commit.

No functional changes, diff best viewed with -b
While working on request body caching improvements, it was noticed
that, for chunked encoding, we did not know the request body size
before we attempted to read at least one additional byte because
the chunked fetch did not return VFP_END with the last chunk, but
rather only as a zero-length additional chunk.

This commit changes chunked encoding processing to return VFP_END
opportunistically if the next chunk header is available.

Implementation:

Unless there is pipeline readahead data available, the test for
available data implies a poll(2) call. Relative to the existing cost
of the current implementation, this cost is not considered relevant.

To improve efficiency, we should consider a generic readahead for
chunked encoding, probably by making the file descriptor non blocking
and adding a readahead buffer.
It seems this test now shows no data loss more frequently, which I
hope should be fine for the purpose of the test?
This commit contains only the init/fini mechanics for the
state/buffer, it is not used yet.

The state is allocated on the heap to be prepared for partial caching
(see varnishcache#3798), where the V1F layer is left and control returns to VCL:

- the worker workspace aws is not an option because it must be freed
  to its original state before returning to VCL.

- the task (req) workspace is not an option because of rollbacks.

While I would have preferred to avoid a heap allocation, it will be
payed off by the reduction of system calls in follow up commits.
This is to support optional readaheads:

The first io vector is for read data which the caller absolutely
needs, any other io vectors are for potential readahead.

The readahead is not used yet, for now, all v1f_read() calls
use the IOV() macro to just wrap the existing single buffer.
We maintain a readahead buffer, which gets filled whenever we parse
chunk headers and read chunks.

The implementation works at the V1F layer only. Because we currently
have no way to return, from this layer, data to the HTC pipeline, we
must take great care to never over-read the current request.

The existing code treats the CR in the CRLF end sequence for chunked
encoding as optional, which, in general, leads to a lower safe
readahead than if we required CR.

For reading chunks, the readahead case is quite trivial: After each
chunk, at least one NL needs to follow, plus the last 0 bytes chunk
header (0 + NL), plus at least one NL for the end of trailers,
totaling to four bytes of safe readahead.

In pratice, usually two bytes will be read ahead, the CRLF ending the
chunk. For the final chunk, the safe readahead is too conservative to
read all of the usual CRLF 0 CRLF CRLF sequence, but the four bytes
readahead is sufficient to trigger reading the final zero chunk
header, such that we will return VFP_END properly.

For reading chunk headers, the minimal safe readahead assumption is
for the shortest possible end chunk, 0 NL NL. We very conservatively
update the readahead, such that we will usually process a chunk header
with two reads.
nigoroll added a commit to nigoroll/varnish-cache that referenced this pull request Jun 24, 2022
With varnishcache#3809 in place, VFP_END is now signalled opportunistically.

Thus, if we see VFP_OK with a full buffer, we still need one extra
read to ensure that it does not return VFP_END.
@nigoroll
Copy link
Member Author

While this looks overall correct, it also complicates chunked parsing a great deal.

I doubt a generic receive buffer, while useful, would change this in any relevant way. We would still need to ensure that the number of bytes required by each parsing step are available. The only thing a generic receive buffer would improve is the readahead, which would not need to be restricted to the current body.

Copy link
Member

@dridi dridi left a comment

Choose a reason for hiding this comment

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

I found a minor suggestion during a second pass on the patch series.

I'm still thinking we may be taking this problem from the wrong end. The last couple commits in particular drastically increase in complexity despite attempts at containing it.

The chunked header parser in current master is not just incomplete because it may forget to complain about missing [CR]LF delimiters, it's also incapable of parsing a chunked extension (even if it's only to ignore them).

I think we should merge the patch series to the point where VFP_END is sent with the last chunk (and squash the test case stabilization to avoid git-bisect surprises). The read-ahead part should be handled separately.

Comment on lines 166 to 171
cll = strtoumax(buf, &q, 16);
if (q == NULL || *q != '\0')
return (VFP_Error(vc, "chunked header number syntax"));
cl = (ssize_t)cll;
if (cl < 0 || (uintmax_t)cl != cll)
return (VFP_Error(vc, "bogusly large chunk size"));
Copy link
Member

Choose a reason for hiding this comment

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

VNUM_hex()? I'm aware you didn't really touch this part, I simply noticed.

Copy link
Member Author

@nigoroll nigoroll Jul 12, 2022

Choose a reason for hiding this comment

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

no opinion. Why do we even have vnum_uint () and do not just use strtoumax()?

Copy link
Member

Choose a reason for hiding this comment

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

To optionally work with [b..e) ranges, not just null-terminated strings.

@nigoroll
Copy link
Member Author

I'm still thinking we may be taking this problem from the wrong end. The last couple commits in particular drastically increase in complexity despite attempts at containing it.

I honestly do not see the complexity increase. Can you please point to one or two examples?

The chunked header parser in current master is not just incomplete because it may forget to complain about missing [CR]LF delimiters, it's also incapable of parsing a chunked extension (even if it's only to ignore them).

Good point, we should handle extensions, but I believe this topic is orthogonal to this PR and related work.

Being at it, we should also double check if we properly handle trailers. I had made an attempt to get trailer support in ages ago with #2477, but even if we do not add that, we should still at least somehow handle trailers (ignore? error?).

I think we should merge the patch series to the point where VFP_END is sent with the last chunk (and squash the test case stabilization to avoid git-bisect surprises). The read-ahead part should be handled separately.

WFM, any progress is welcome

@dridi
Copy link
Member

dridi commented Jul 12, 2022

I honestly do not see the complexity increase. Can you please point to one or two examples?

The last 3 commits.

Good point, we should handle extensions, but I believe this topic is orthogonal to this PR and related work.

Agreed.

Being at it, we should also double check if we properly handle trailers. I had made an attempt to get trailer support in ages ago with #2477, but even if we do not add that, we should still at least somehow handle trailers (ignore? error?).

Agreed, we should start with ignoring them. An error is what we already get today when we expect "0[CR]LF[CR]LF" for the last chunk, because that technically embeds the end of trailers.

In a previous discussion we had discussed new vcl_*_end subroutine, and vcl_backend_end would be the place to inspect trailers I suppose. That, in general, poses a storage problem. Also, orthogonal.

An alternative could be to send VFP_END when we see the last chunk (0[CR]LF) and wait for the end of trailers outside of the VFP machinery for a very simple reason: trailers aren't body bytes.

@nigoroll
Copy link
Member Author

nigoroll commented Aug 1, 2022

Bugwash:

  • can trailer handling be extracted from VFP in any sane way?
  • Show how trailer support in VCL could look like, ideas:
    • explicit body fetch?
    • vcl_backend_end ? (but what about request headers ?)

Copy link
Member

@dridi dridi left a comment

Choose a reason for hiding this comment

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

I was having another look at #4035 and remembered that we have this one open, and now it has a merge conflict to solve.

Anyway, revisiting this I have a few comments:

Comment on lines 199 to 203
if (vfe->priv2 == -1) {
vfps = v1f_chunked_hdr(vc, htc, &vfe->priv2);
if (vfps != VFP_OK)
return (vfps);
}
Copy link
Member

Choose a reason for hiding this comment

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

We should introduce a macro for this magic -1.

Comment on lines +238 to +240
v1fcp = calloc(1, sz);
AN(v1fcp);
v1fcp->magic = V1F_CHUNKED_PRIV_MAGIC;
Copy link
Member

Choose a reason for hiding this comment

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

We could now use ALLOC_FLEX_OBJ() here.

@nigoroll
Copy link
Member Author

nigoroll commented May 29, 2024

Following up here on the topic of trailer support, motivated by #4035 (comment)

As a personal preface, I would like to add that this triggers some frustration on my end. take this iteratively and not involve VCL support on day 1 was exactly what I tried to do in #2477 and various other tickets. It irritates me that when I mention the feedback (pushback, if you will) which I received at the time from the same group of people now working on a new attempt, things seem to have changed and "not having a clear VCL concept" suddenly seems to be OK.

So now that I got this off my chest, let's leave the past behind - I also want to have trailer support and I will be happy to get back to these old tickets and make progress. Also I do want to collaborate.

How to we proceed here? Do we have any new suggestions on the table?

@dridi
Copy link
Member

dridi commented Jun 4, 2024

I offer you my apologies if I contributed to your frustration on this specific topic. Until you brought it up, I didn't remember your former attempt at dealing with trailers.

Here is where we stand right now, based on #3809 (review):

  • we cherry-picked the commits I mentioned as a starting point
  • we moved the trailer fetch outside of the VFP stack (implemented in V1L, I think)
    • for beresp this requires a new VDI method (implemented in VBE)
    • for req this requires pipelining adjustments

At this point, first milestone, we preserve HTTP/1 framing in the presence of trailers, but we effectively drop them. I believe @walid-git got this working already (he's doing most of the work, when I say "we" I mostly refer to him).

What we are planning next:

  • h2 trailers for req
  • storing trailers (new object attribute)
  • forwarding trailers as-is back and forth for pass transactions
    • guarded by an experimental::pass_trailers flag

This second milestone would allow us to see how well Varnish integrates with applications relying on trailers, with caveats such as the risk of breaking check sums when filters are involved, hence the initial experimental status. I believe this also becomes a good basis to discuss how to expose trailers to VCL.

I will try to revisit #2477 and see what we may reconsider with our current plan.

edit: I submitted prematurely because of ctrl+return keyboard accident.

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.

3 participants