-
Notifications
You must be signed in to change notification settings - Fork 5.5k
health check: gracefully handle GOAWAY in grpc health checker #11324
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 2 commits
e4ddb8e
70263a6
a069698
725caf8
58d2143
00a1fea
1828a2b
64c6aef
6ba45a4
c5958dc
b474edd
9171180
73e7308
dc5effd
7c7d930
41ea1e1
a66c176
bf9c59c
878bd4a
541eb54
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 |
|---|---|---|
|
|
@@ -703,17 +703,24 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onInterval() { | |
| void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onResetStream(Http::StreamResetReason, | ||
| absl::string_view) { | ||
| const bool expected_reset = expect_reset_; | ||
| const bool goaway = received_goaway_; | ||
| resetState(); | ||
|
|
||
| if (expected_reset) { | ||
| // Stream reset was initiated by us (bogus gRPC response, timeout or cluster host is going | ||
| // away). In these cases health check failure has already been reported, so just return. | ||
| // away). In these cases health check failure has already been reported and a GOAWAY (if any) | ||
| // has already been handled, so just return. | ||
| return; | ||
| } | ||
|
|
||
| ENVOY_CONN_LOG(debug, "connection/stream error health_flags={}", *client_, | ||
| HostUtility::healthFlagsToString(*host_)); | ||
|
|
||
| if (goaway) { | ||
| // Stream reset was unexpected, so GOAWAY hasn't been handled yet. | ||
| client_->close(); | ||
| } | ||
|
|
||
| // TODO(baranov1ch): according to all HTTP standards, we should check if reason is one of | ||
| // Http::StreamResetReason::RemoteRefusedStreamReset (which may mean GOAWAY), | ||
| // Http::StreamResetReason::RemoteReset or Http::StreamResetReason::ConnectionTermination (both | ||
|
|
@@ -725,13 +732,12 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onResetStream(Http::St | |
| void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onGoAway() { | ||
| ENVOY_CONN_LOG(debug, "connection going away health_flags={}", *client_, | ||
| HostUtility::healthFlagsToString(*host_)); | ||
| // Even if we have active health check probe, fail it on GOAWAY and schedule new one. | ||
| // If we have an active health check probe, allow it to complete before closing the connection. | ||
| if (request_encoder_) { | ||
| handleFailure(envoy::data::core::v3::NETWORK); | ||
| expect_reset_ = true; | ||
| request_encoder_->getStream().resetStream(Http::StreamResetReason::LocalReset); | ||
| received_goaway_ = true; | ||
| } else { | ||
| client_->close(); | ||
| } | ||
| client_->close(); | ||
| } | ||
|
|
||
| bool GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::isHealthCheckSucceeded( | ||
|
|
@@ -757,6 +763,8 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onRpcComplete( | |
| handleFailure(envoy::data::core::v3::ACTIVE); | ||
| } | ||
|
|
||
| const bool goaway = received_goaway_; | ||
|
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. Mind adding a comment here explaining why we have to read the value here? Its a bit less clear than on L705 since we're not unconditionally calling |
||
|
|
||
| // |end_stream| will be false if we decided to stop healthcheck before HTTP stream has ended - | ||
| // invalid gRPC payload, unexpected message stream or wrong content-type. | ||
| if (end_stream) { | ||
|
|
@@ -767,7 +775,7 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onRpcComplete( | |
| request_encoder_->getStream().resetStream(Http::StreamResetReason::LocalReset); | ||
| } | ||
|
|
||
| if (!parent_.reuse_connection_) { | ||
| if (!parent_.reuse_connection_ || goaway) { | ||
| client_->close(); | ||
| } | ||
| } | ||
|
|
@@ -777,13 +785,18 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::resetState() { | |
| request_encoder_ = nullptr; | ||
| decoder_ = Grpc::Decoder(); | ||
| health_check_response_.reset(); | ||
| received_goaway_ = false; | ||
| } | ||
|
|
||
| void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onTimeout() { | ||
| ENVOY_CONN_LOG(debug, "connection/stream timeout health_flags={}", *client_, | ||
| HostUtility::healthFlagsToString(*host_)); | ||
| expect_reset_ = true; | ||
|
snowp marked this conversation as resolved.
|
||
| request_encoder_->getStream().resetStream(Http::StreamResetReason::LocalReset); | ||
| if (received_goaway_) { | ||
| client_->close(); | ||
| } else { | ||
| request_encoder_->getStream().resetStream(Http::StreamResetReason::LocalReset); | ||
| } | ||
| } | ||
|
|
||
| void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::logHealthCheckStatus( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -4529,21 +4529,176 @@ TEST_F(GrpcHealthCheckerImplTest, GrpcFailUnknownHealthStatus) { | |||
| cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY is interpreted as connection close event. | ||||
| // Test receiving GOAWAY is handled gracefully while a check is in progress. | ||||
|
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. This tells me that the code intentionally treated GOAWAY as failures, which makes me wonder if this is the desired behavior for some? Does this mean that this should be configurable?
Member
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. This might depend on the reason for the GOAWAY? I.e. a
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 see, that makes sense. It looks like the GOAWAY error code isn't provided in the callback at the moment, but I think it would be reasonable to do that? It seems that the It may also be worth mentioning that the http2 connection pool implementation doesn't change behaviour based on the GOAWAY error code at the moment (see here). It transitions the active client to the draining state if it has active requests.
Member
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. Right, if the GOAWAY error code is NO_ERROR we should just treat it as "don't reuse connection", not an error. We may have to add some additional error code info to the callback. Note that @antoniovicente just opened #11420, though IIRC nghttp2 does provide the reason code inside the frame here: envoy/source/common/http/http2/codec_impl.cc Line 582 in 5d8f7d7
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.
Yep - it looks like it's provided in I'm happy to update the Does something along these lines look right?
Member
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. We should not leak nghttp2 specific structs to the public interface, so at least for the error code we should do a mapping. For your use case maybe just have an enum with {NO_ERROR, OTHER} and @antoniovicente can fill out more stuff later as needed?
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. enum with {NO_ERROR, OTHER} sounds like a good start |
||||
| TEST_F(GrpcHealthCheckerImplTest, GoAwayProbeInProgress) { | ||||
| // FailureType::Network will be issued, it will render host unhealthy only if unhealthy_threshold | ||||
| // is reached. | ||||
| setupHCWithUnhealthyThreshold(1); | ||||
| expectSingleHealthcheck(HealthTransition::Changed); | ||||
| setupHCWithUnhealthyThreshold(/*threshold=*/1); | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_ = { | ||||
| makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; | ||||
|
|
||||
| expectSessionCreate(); | ||||
| expectHealthcheckStart(0); | ||||
| health_checker_->start(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); | ||||
|
|
||||
| // GOAWAY during check should be handle gracefully. | ||||
| test_sessions_[0]->codec_client_->raiseGoAway(); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(true); | ||||
|
|
||||
| // GOAWAY should cause a new connection to be created. | ||||
| expectClientCreate(0); | ||||
| expectHealthcheckStart(0); | ||||
| test_sessions_[0]->interval_timer_->invokeCallback(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Test host state haven't changed. | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(true); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY closes connection after an in progress probe times outs. | ||||
| TEST_F(GrpcHealthCheckerImplTest, GoAwayProbeInProgressTimeout) { | ||||
| setupHCWithUnhealthyThreshold(/*threshold=*/1); | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_ = { | ||||
| makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; | ||||
|
|
||||
| expectSessionCreate(); | ||||
| expectHealthcheckStart(0); | ||||
| EXPECT_CALL(event_logger_, logUnhealthy(_, _, _, true)); | ||||
| health_checker_->start(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Unhealthy threshold is 1 so first timeout causes unhealthy | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); | ||||
| EXPECT_CALL(event_logger_, logEjectUnhealthy(_, _, _)); | ||||
|
|
||||
| // GOAWAY during check should be handled gracefully. | ||||
| test_sessions_[0]->codec_client_->raiseGoAway(); | ||||
| expectHostHealthy(true); | ||||
|
|
||||
| test_sessions_[0]->timeout_timer_->invokeCallback(); | ||||
| expectHostHealthy(false); | ||||
|
|
||||
| // GOAWAY should cause a new connection to be created. | ||||
| expectClientCreate(0); | ||||
| expectHealthcheckStart(0); | ||||
| test_sessions_[0]->interval_timer_->invokeCallback(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Healthy threshold is 2, so the we'ere pending a state change. | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::ChangePending)); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(false); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY closes connection after an unexpected stream reset. | ||||
| TEST_F(GrpcHealthCheckerImplTest, GoAwayProbeInProgressStreamReset) { | ||||
| setupHCWithUnhealthyThreshold(/*threshold=*/1); | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_ = { | ||||
| makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; | ||||
|
|
||||
| expectSessionCreate(); | ||||
| expectHealthcheckStart(0); | ||||
| EXPECT_CALL(event_logger_, logUnhealthy(_, _, _, true)); | ||||
| health_checker_->start(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Unhealthy threshold is 1 so first stream reset causes unhealthy | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); | ||||
| EXPECT_CALL(event_logger_, logEjectUnhealthy(_, _, _)); | ||||
|
|
||||
| // GOAWAY during check should be handled gracefully. | ||||
| test_sessions_[0]->codec_client_->raiseGoAway(); | ||||
| expectHostHealthy(true); | ||||
|
|
||||
| EXPECT_TRUE(cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->healthFlagGet( | ||||
| Host::HealthFlag::FAILED_ACTIVE_HC)); | ||||
| EXPECT_EQ(Host::Health::Unhealthy, | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); | ||||
| test_sessions_[0]->request_encoder_.stream_.resetStream(Http::StreamResetReason::RemoteReset); | ||||
| expectHostHealthy(false); | ||||
|
|
||||
| // GOAWAY should cause a new connection to be created. | ||||
| expectClientCreate(0); | ||||
| expectHealthcheckStart(0); | ||||
| test_sessions_[0]->interval_timer_->invokeCallback(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Healthy threshold is 2, so the we'ere pending a state change. | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::ChangePending)); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(false); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY closes connection after a bad response. | ||||
| TEST_F(GrpcHealthCheckerImplTest, GoAwayProbeInProgressBadResponse) { | ||||
| setupHCWithUnhealthyThreshold(/*threshold=*/1); | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_ = { | ||||
| makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; | ||||
|
|
||||
| expectSessionCreate(); | ||||
| expectHealthcheckStart(0); | ||||
| EXPECT_CALL(event_logger_, logUnhealthy(_, _, _, true)); | ||||
| health_checker_->start(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Unhealthy threshold is 1 so first bad response causes unhealthy | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); | ||||
| EXPECT_CALL(event_logger_, logEjectUnhealthy(_, _, _)); | ||||
|
|
||||
| // GOAWAY during check should be handled gracefully. | ||||
| test_sessions_[0]->codec_client_->raiseGoAway(); | ||||
| expectHostHealthy(true); | ||||
|
|
||||
| respondResponseSpec(0, ResponseSpec{{{":status", "200"}, {"content-type", "application/grpc"}}, | ||||
| {ResponseSpec::invalidChunk()}, | ||||
| {}}); | ||||
| expectHostHealthy(false); | ||||
|
|
||||
| // GOAWAY should cause a new connection to be created. | ||||
| expectClientCreate(0); | ||||
| expectHealthcheckStart(0); | ||||
| test_sessions_[0]->interval_timer_->invokeCallback(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Healthy threshold is 2, so the we'ere pending a state change. | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::ChangePending)); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(false); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY and a connection close. | ||||
| TEST_F(GrpcHealthCheckerImplTest, GoAwayProbeInProgressConnectionClose) { | ||||
| setupHCWithUnhealthyThreshold(/*threshold=*/1); | ||||
| cluster_->prioritySet().getMockHostSet(0)->hosts_ = { | ||||
| makeTestHost(cluster_->info_, "tcp://127.0.0.1:80")}; | ||||
|
|
||||
| expectSessionCreate(); | ||||
| expectHealthcheckStart(0); | ||||
| EXPECT_CALL(event_logger_, logUnhealthy(_, _, _, true)); | ||||
| health_checker_->start(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Unhealthy threshold is 1 so first bad response causes unhealthy | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); | ||||
| EXPECT_CALL(event_logger_, logEjectUnhealthy(_, _, _)); | ||||
|
|
||||
| // GOAWAY during check should be handled gracefully. | ||||
| test_sessions_[0]->codec_client_->raiseGoAway(); | ||||
| expectHostHealthy(true); | ||||
|
|
||||
| test_sessions_[0]->client_connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); | ||||
| expectHostHealthy(false); | ||||
|
|
||||
| // GOAWAY should cause a new connection to be created. | ||||
| expectClientCreate(0); | ||||
| expectHealthcheckStart(0); | ||||
| test_sessions_[0]->interval_timer_->invokeCallback(); | ||||
|
|
||||
| expectHealthcheckStop(0); | ||||
| // Healthy threshold is 2, so the we'ere pending a state change. | ||||
| EXPECT_CALL(*this, onHostStatus(_, HealthTransition::ChangePending)); | ||||
| respondServiceStatus(0, grpc::health::v1::HealthCheckResponse::SERVING); | ||||
| expectHostHealthy(false); | ||||
| } | ||||
|
|
||||
| // Test receiving GOAWAY between checks affects nothing. | ||||
|
|
||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A one liner comment here about when this connection would eventually get closed would be nice. IIUC it's either on the next timeout or when the remote closes it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a comment. It would be closed when the active check completes (
onRpcComplete) or other conditions likeonResetStreamandonTimeout.