-
Notifications
You must be signed in to change notification settings - Fork 5.5k
grid: Add a new class for tracking HTTP/3 status #16067
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
Changes from 6 commits
54b5881
43935f2
4f57d0d
2028dc7
7ed8c1f
a19e0aa
5a9cc69
627f577
5a95f22
a6d41ed
fdf486c
2aa5191
33a55cf
ed01f6b
0f8b751
b562300
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #include "common/http/http3_status_tracker.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Http { | ||
|
|
||
| namespace { | ||
|
|
||
| // Initially, HTTP/3 is be marked broken for 5 minutes. | ||
| const std::chrono::minutes DefaultExpirationTime{5}; | ||
|
RyanTheOptimist marked this conversation as resolved.
|
||
| // Cap the broken period at just under 1 day. | ||
| const int MaxConsecutiveBrokenCount = 8; | ||
| } // namespace | ||
|
|
||
| Http3StatusTracker::Http3StatusTracker(Event::Dispatcher& dispatcher) | ||
| : expiration_timer_(dispatcher.createTimer([this]() -> void { onExpirationTimeout(); })) {} | ||
|
|
||
| bool Http3StatusTracker::isHttp3Broken() const { return state_ == State::Broken; } | ||
|
|
||
| bool Http3StatusTracker::isHttp3Confirmed() const { return state_ == State::Confirmed; } | ||
|
|
||
| void Http3StatusTracker::markHttp3Broken() { | ||
| ASSERT(state_ != State::Broken); | ||
| state_ = State::Broken; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are valid values for state_ when entering this method?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question! Mentally, I'm modeling this off of the similar code in Chrome. That code runs up at the request/response layer (the HttpNetworkTransaction) which is above the connection establishment layer. As such and given that requests happen in parallel, it's possible for basically any sequence of markBroken/markConfirmed calls to arrive in any order. I suspect that we'll eventually want something similar. But since we're not doing anything like that now, there's no need to permit such state transitions. So I've added ASSERT() calls to make it clear what the valid states are. Thanks for pointing this out. (In any case, this should be reachable from any state other than broken)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be possible to trigger this ASSERT, if there are 2 concurrent attempts to connect to the same endpoint. That said, it's possible that there are protections elsewhere to prevent this from happening or it is relatively unlikely to happen without a burst of requests for that service; we would need the number of requests to exceed the upstream's multiplexing factor and trigger creation of a second connection in order to meet demand.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah! Excellent point! That's very true. Ok, in that case we're back to the Chrome situation where the parallelism means that we can really get any sequence of events in any order. I've removed the ASSERT() calls.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for my own curiosity why would we have multiple concurrent connection attempts to the same endpoint on a given worker thread? Is this related to prefetching or part of the grid logic?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm relatively unclear at this on exactly how this all plays together. From what Antonio said, it sound like if there were a sufficient number of simultaneous requests we might trigger the creation of a second attempt. The other case that I think I heard from alyssa is that it's possible to have multiple calls to ConnectivityGrid::newStream() happen before the first call finishes. This won't result in multiple TCP/QUIC connection attempts because the underlying connection pool will do the right thing. But I think this is transparent to the ConnectivtyGrid; each call to newStream creates a new WrapperCallbacks and (up to) 2 ConnectionAttempts which should mean it's possible to get unexpected state transitions. Happy to do something different if I'm not understanding.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this sounds plausible to me: newStream is non-blocking (like most things in Envoy), so if multiple streams are established before the connection is established so you'd see multiple newStreams come in before the connection is established. No change necessary, I was just curious how this all fit together :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional (I mean in this PR, not optional overall) I wonder if we should start landing docs on this as we start implementing the "real" logic. Generally we land docs when we unhide config (#15926) but the failover logic is sufficiently complicated I think we could land docs for now in source/docs and then move them to docs/ when the PR lands. Your call if we do them now or in a future iteration :-)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs definitely make sense and I'm happy to work on them. I think I'll do that in a follow-up since this PR is (hopefully) basically done at this point. |
||
| if (!expiration_timer_->enabled()) { | ||
| expiration_timer_->enableTimer(std::chrono::duration_cast<std::chrono::milliseconds>( | ||
| DefaultExpirationTime * (1 << consecutive_broken_count_))); | ||
| if (consecutive_broken_count_ < MaxConsecutiveBrokenCount) { | ||
| ++consecutive_broken_count_; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void Http3StatusTracker::markHttp3Confirmed() { | ||
| ASSERT(state_ == State::Pending); | ||
| ASSERT(!expiration_timer_->enabled()); | ||
| state_ = State::Confirmed; | ||
|
RyanTheOptimist marked this conversation as resolved.
Outdated
|
||
| consecutive_broken_count_ = 0; | ||
| } | ||
|
|
||
| void Http3StatusTracker::onExpirationTimeout() { | ||
| ASSERT(state_ == State::Broken); | ||
|
|
||
| state_ = State::Pending; | ||
| } | ||
|
|
||
| } // namespace Http | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| #pragma once | ||
|
|
||
| #include "envoy/event/dispatcher.h" | ||
| #include "envoy/event/timer.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Http { | ||
|
|
||
| // Tracks the status of HTTP/3 being broken for a period of time | ||
| // subject to exponential backoff. | ||
| class Http3StatusTracker { | ||
| public: | ||
| Http3StatusTracker(Event::Dispatcher& dispatcher); | ||
|
RyanTheOptimist marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Returns true if HTTP/3 is broken. | ||
| bool isHttp3Broken() const; | ||
| // Returns true if HTTP/3 is confirmed to be working. | ||
| bool isHttp3Confirmed() const; | ||
|
RyanTheOptimist marked this conversation as resolved.
|
||
| // Marks HTTP/3 broken for a period of time, subject to backoff. | ||
| void markHttp3Broken(); | ||
| // Marks HTTP/3 as confirmed to be working and resets the backoff timeout. | ||
| void markHttp3Confirmed(); | ||
|
|
||
| private: | ||
| enum class State { | ||
| Pending, | ||
| Broken, | ||
| Confirmed, | ||
| }; | ||
|
|
||
| // Called when the expiration timer fires. | ||
| void onExpirationTimeout(); | ||
|
|
||
| State state_{State::Pending}; | ||
| // The number of consecutive times HTTP/3 has been marked broken. | ||
| int consecutive_broken_count_{}; | ||
| // The timer which tracks when HTTP/3 broken status should expire | ||
| Event::TimerPtr expiration_timer_; | ||
| }; | ||
|
|
||
| } // namespace Http | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| #include "common/http/http3_status_tracker.h" | ||
|
|
||
| #include "test/mocks/common.h" | ||
| #include "test/mocks/event/mocks.h" | ||
|
|
||
| #include "gmock/gmock.h" | ||
| #include "gtest/gtest.h" | ||
|
|
||
| using Envoy::Event::MockTimer; | ||
| using testing::_; | ||
| using testing::AnyNumber; | ||
| using testing::NiceMock; | ||
| using testing::Return; | ||
| using testing::StrictMock; | ||
|
|
||
| namespace Envoy { | ||
| namespace Http { | ||
|
|
||
| namespace { | ||
| class Http3StatusTrackerTest : public testing::Test { | ||
| public: | ||
| Http3StatusTrackerTest() | ||
| : timer_(new StrictMock<MockTimer>(&dispatcher_)), tracker_(dispatcher_) {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we usually omit the StrictMock part since by default all the mocks are strict (we're not super consistent here so not a big deal)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, interesting! What's the magic that makes all the Mocks strict? Some #define something somewhere? Very cool. In any case, done.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oooo! Thanks! |
||
|
|
||
| NiceMock<Event::MockDispatcher> dispatcher_; | ||
| StrictMock<MockTimer>* timer_; // Owned by tracker_; | ||
| Http3StatusTracker tracker_; | ||
| }; | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, Initialized) { | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBroken) { | ||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| EXPECT_TRUE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBrokenThenExpires) { | ||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| timer_->invokeCallback(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBrokenWithBackoff) { | ||
| // markBroken will only be called when the time is not enabled. | ||
|
RyanTheOptimist marked this conversation as resolved.
Outdated
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From reading the test it seems like we're the ones calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. I can remove this comment if it doesn't make sense. I wrote the comment because the MockTimer API confused me. I expected that expect() would return true if enableTimer() had been called but disableTimer() or invokeCallback() had not been. In other words, I didn't expect to need to mock out this method. So the comment was saying that when markBroken() is called in this tests, the timer will not have been enabled. Does that make sense? Would you recommend I rephrase or remove the comment?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, I would try to include something that indicates that we're talking about invariants for this is used by prod code. My initial read was that this was trying to explain what we should expect to see in this test, so I expected to see some
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. That being said, I took several stabs at trying to write something up and each time ended up with a bit of an essay that didn't really seem to add any readability to the test, so I've just nuked the comment. (Really the problem was my lack of understanding of how MockTimer worked and so this is probably not the best place to address that :>) |
||
| EXPECT_CALL(*timer_, enabled()).WillRepeatedly(Return(false)); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(10 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| timer_->invokeCallback(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(20 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| EXPECT_TRUE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
|
|
||
| timer_->invokeCallback(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(40 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| EXPECT_TRUE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
|
|
||
| timer_->invokeCallback(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBrokenWithBackoffMax) { | ||
| // markBroken will only be called when the time is not enabled. | ||
|
RyanTheOptimist marked this conversation as resolved.
Outdated
|
||
| EXPECT_CALL(*timer_, enabled()).WillRepeatedly(Return(false)); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(10 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(20 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(40 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(80 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(160 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(320 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(640 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(1280 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
|
|
||
| // Broken period no longer increases. | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(1280 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
| timer_->invokeCallback(); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBrokenThenExpiresThenConfirmedThenBroken) { | ||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
|
RyanTheOptimist marked this conversation as resolved.
|
||
| tracker_.markHttp3Confirmed(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_TRUE(tracker_.isHttp3Confirmed()); | ||
|
|
||
| // markConfirmed will have reset the timeout back to the initial value. | ||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| EXPECT_TRUE(tracker_.isHttp3Broken()); | ||
| EXPECT_FALSE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| TEST_F(Http3StatusTrackerTest, MarkBrokenThenConfirmed) { | ||
| EXPECT_CALL(*timer_, enabled()).WillOnce(Return(false)); | ||
| EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(5 * 60 * 1000), nullptr)); | ||
| tracker_.markHttp3Broken(); | ||
|
|
||
| timer_->invokeCallback(); | ||
|
|
||
| EXPECT_CALL(*timer_, enabled()).WillRepeatedly(Return(false)); | ||
| tracker_.markHttp3Confirmed(); | ||
| EXPECT_FALSE(tracker_.isHttp3Broken()); | ||
| EXPECT_TRUE(tracker_.isHttp3Confirmed()); | ||
| } | ||
|
|
||
| } // namespace | ||
| } // namespace Http | ||
| } // namespace Envoy | ||
Uh oh!
There was an error while loading. Please reload this page.