caching: Stream cached responses in chunks and handle downstream backpressure #13054
caching: Stream cached responses in chunks and handle downstream backpressure #13054yosrym93 wants to merge 10 commits intoenvoyproxy:masterfrom
Conversation
Signed-off-by: Yosry Ahmed <yosryahmed@google.com>
…ized according to the buffer limit Signed-off-by: Yosry Ahmed <yosryahmed@google.com>
Signed-off-by: Yosry Ahmed <yosryahmed@google.com>
|
Background:
In this PR:
1 & 2 are already implemented and tested. |
|
To subscribe to receive watermark events we need to call Currently, the CacheFilter inherits from Alternatively, the CacheFilter could create an internal class/struct that inherits from |
|
The chunk size used to fetch the body from the cache is equal to the encoding buffer limit. This way, we fetch as much data in every request to the cache as we can send downstream. |
|
If the high watermark callback was invoked AFTER a body chunk is already request from the cache, this request will be completed, and that chunk will be sent downstream. This may result in crossing the encoding buffer limit, but it should be okay as this is only a soft limit, and this should not happen frequently. |
|
According to https://github.com/envoyproxy/envoy/blob/master/test/integration/protocol_integration_test.cc#L1036 we don't need to handle non-compliant 304 bodies. Envoy already handles them. |
Signed-off-by: Yosry Ahmed <yosryahmed@google.com>
|
@toddmgreer I think this addresses all your comments. |
| dir | ||
| dirname | ||
| djb | ||
| dont |
There was a problem hiding this comment.
The comment that has ContinueAndDontEndStream. "Dont" is seen as a spelling error.
There was a problem hiding this comment.
There was a problem hiding this comment.
This is a spelling error. "Don't"
Signed-off-by: Yosry Ahmed <yosryahmed@google.com>
|
@yosrym93 is this still a draft or is it ready for review? I can help review the watermark logic (or @alyssawilk potentially) but want to make sure this is ready for review. /wait-any |
The PR is only missing tests for the watermark logic, but the logic code should be complete. |
|
I will wait for the tests, thanks. /wait |
Patching envoyproxy#13054 Signed-off-by: Zhongyi Shi <zhongyi@chromium.org>
…essure Signed-off-by: Zhongyi Shi <zhongyi@chromium.org>
|
@yosrym93 is this ready for review? If so can you mark it non-draft? /wait-any |
|
I added a test in cache_filter_test.cc which should test the downstream watermark handling. Should be ready for review now. A side note: it looks like the test dispatcher once run, cannot be interrupted (unless I miss something). It would be nice if there's a mode to run the event loop one by one, something similar to chromium's TestTestRunner::RunNextTask(). That made me wonder how Envoy test complicated scheduling logic, i.e., verifying callbacks are posted with right delay. |
|
@yanavlasov could you help set this PR ready for review? I don't have permission to do that. Besides, I might also need you to kick off the retry on presubmit to see if infra failures goes away. Thanks! @yosrym93 If you happen to see this message early, it would be nice to set the PR ready for review. Thanks! |
Done. Glad to see progress on this PR! |
|
Happy to review; please ping once @toddmgreer finishes review. Thanks! |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@toddmgreer the change is ready for review, PTAL, thanks! |
jmarantz
left a comment
There was a problem hiding this comment.
basically looks great; need to do another quick pass over the tests.
Some minor nits.
test/extensions/filters/http/cache/cache_filter_integration_test.cc
Outdated
Show resolved
Hide resolved
|
|
||
| // Make sure validation conditional headers are added | ||
| const Http::TestRequestHeaderMapImpl injected_headers = { | ||
| {"if-none-match", etag_}, {"if-modified-since", response_last_modified_}}; |
There was a problem hiding this comment.
To keep it simple and verbose, is that okay to keep it as is rather than using values in headers.h? The dic needs std::string pairs, so we will need to use something like Http::CustomHeaders.get().ifNoneMatch.get(), which is not clean.
Besides, I checked other places where TestRequestHeaderMapImpl, it seems we generally follow a explicit key-value pair setting. WDYT?
There was a problem hiding this comment.
I'm OK either way. One way to make this a little less verbose than what you suggest is to make a temp for Http::CustomHeaders.get() and use that throughout this function. But it's up to you.
Signed-off-by: Zhongyi Shi <zhongyi@chromium.org>
|
Comments addressed, PATL, thanks! Sorry if I didn't figure out the best way to use the tool (bear with me if the comments notifications are not bundled). I am still trying to get familiar with this git native PR process (compared to a extremely convenient Gerrit workflow). |
|
@jmarantz This change is ready for review. BTW, I don't have permission to resolve conversations, please do resolve if you have that button. Thanks! |
|
|
||
| // Make sure validation conditional headers are added | ||
| const Http::TestRequestHeaderMapImpl injected_headers = { | ||
| {"if-none-match", etag_}, {"if-modified-since", response_last_modified_}}; |
There was a problem hiding this comment.
I'm OK either way. One way to make this a little less verbose than what you suggest is to make a temp for Http::CustomHeaders.get() and use that throughout this function. But it's up to you.
| EXPECT_CALL(encoder_callbacks_, | ||
| injectEncodedDataToFilterChain( | ||
| testing::Property(&Buffer::Instance::toString, | ||
| testing::Eq(std::string(buffer_limit_, 'a'))), |
There was a problem hiding this comment.
I was thinking about this testcase. I think it would give us more confidence in the correctness of the system if you used a buffer_limit of something much smaller, say 6. Then you can use a payload of "1234567890" and make sure the first chunk is "123456" and the second is "7890".
Right now you are comparing against a bunch of 'a's and I'm wondering if a bug could creep in where we take the wrong span of that string in the logic, but your test doesn't care.
WDYT?
There was a problem hiding this comment.
Good point! Thanks for doing a thorough review. I didn't pay much attention on the existing code but focused on downstream watermark handling test :P
| // Test that a body with size exactly equal to the buffer limit will be encoded in 1 chunk. | ||
| TEST_F(CacheChunkSizeTest, EqualBufferLimit) { | ||
| request_headers_.setHost("EqualBufferLimit"); | ||
| const std::string body = std::string(buffer_limit_, 'a'); |
There was a problem hiding this comment.
again I'd consider using smaller limits and more interesting strings to compare.
jmarantz
left a comment
There was a problem hiding this comment.
btw this is great code, super clean and easy to read. Thanks for doing this!
I'm really just having nits and testing questions that are probably academic.
Signed-off-by: Zhongyi Shi <zhongyi@chromium.org>
|
Thanks for the thorough review, really appreciate it! It was @yosrym93 did the great work, I only added one additional test to ensure there's test coverage on downstream watermark handling. LMK if there's any other concern. Thanks! |
| request_headers_.setHost("DownstreamPressureHandling"); | ||
| const int chunks_count = 3; | ||
| const uint64_t body_size = buffer_limit_ * chunks_count; | ||
| const uint64_t body_size = getBufferLimit() * chunks_count; |
There was a problem hiding this comment.
We are still just testing a bunch of "a"s below; should we change these too?
jmarantz
left a comment
There was a problem hiding this comment.
Modulo my previous comment I'll pass this to @envoyproxy/senior-maintainers in parallel with possibly addressing that.
Nice job (all who worked on it)!
mattklein123
left a comment
There was a problem hiding this comment.
Thanks for this. A few questions/comments to get started.
/wait
| // TODO(yosrym93): Make sure this is the right place to add the callbacks. | ||
| decoder_callbacks_->addDownstreamWatermarkCallbacks(*this); |
There was a problem hiding this comment.
I would recommend initializing this as early as possible (in setEncoderFilterCallbacks)
There was a problem hiding this comment.
I assume you really meant setEncoderFilterCallbacks rather than a typo here. Since I'm stepping up on the implementation code, I did get confused on why the registration is only provided available via StreamDecoderFilterCallbacks. What if a pure encoder filter (who doesn't have decoder_callbacks) wants to listen to downstream watermark changes? In other words, there's no way you could call: encoder_callbacks_->addDownstreamWatermarkCallbacks(*this).
There was a problem hiding this comment.
I think it's just an implementation oversight because no one has needed it before.
| bool request_allows_inserts_ = false; | ||
|
|
||
| // These are used to keep track of whether we should fetch more data from the cache. | ||
| int high_watermark_calls_ = 0; |
| dir | ||
| dirname | ||
| djb | ||
| dont |
There was a problem hiding this comment.
This is a spelling error. "Don't"
| // Continue encoding the headers but do not end the stream as the response body is yet to be | ||
| // injected. | ||
| return Http::FilterHeadersStatus::ContinueAndDontEndStream; |
There was a problem hiding this comment.
Does this correctly handle the case where the cached response is a header only response?
| if (filter_state_ == FilterState::EncodeServingFromCache) { | ||
| // Stop the encoding stream until the cached response is fetched & added to the encoding stream. | ||
| return Http::FilterDataStatus::StopIterationAndBuffer; | ||
| } |
There was a problem hiding this comment.
Why is this removed? Can this no longer happen? Should the enum be removed?
| void CacheFilter::onBelowWriteBufferLowWatermark() { | ||
| ASSERT(high_watermark_calls_ > 0); | ||
| --high_watermark_calls_; | ||
| if (!remaining_ranges_.empty()) { | ||
| // Fetching the cached response body was stopped, continue if possible. | ||
| maybeGetBody(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Are there cases in which we stream through a router response and this might get triggered? I don't see any guards in maybeGetBody() to confirm that we are actually doing a fetch?
|
This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
This pull request has been automatically closed because it has not had activity in the last 37 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
Commit Message:
The CacheFilter now streams cached responses in chunks and handles downstream backpressure.
Signed-off-by: Yosry Ahmed yosryahmed@google.com
Additional Description:
The CacheFilter now:
Risk Level: Low
Testing: Unit / Integration
Docs Changes: N/A
Release Notes: N/A
Fixes #9835