Skip to content

Commit

Permalink
golang: provide method to refresh route cache (#36863)
Browse files Browse the repository at this point in the history
<!--
!!!ATTENTION!!!

If you are fixing *any* crash or *any* potential security issue, *do
not*
open a pull request in this repo. Please report the issue via emailing
envoy-security@googlegroups.com where the issue will be triaged
appropriately.
Thank you in advance for helping to keep Envoy secure.

!!!ATTENTION!!!

For an explanation of how to fill out the fields, please see the
relevant section
in
[PULL_REQUESTS.md](https://github.com/envoyproxy/envoy/blob/main/PULL_REQUESTS.md)
-->

Commit Message: golang: provide method to refresh route cache
Additional Description:
Risk Level: Low, add new method
Testing: Integration
Docs Changes:
Release Notes:
Platform Specific Features:
[Optional Runtime guard:]
Fixes #36848
[Optional Fixes commit #PR or SHA]
[Optional Deprecated:]
[Optional [API
Considerations](https://github.com/envoyproxy/envoy/blob/main/api/review_checklist.md):]

---------

Signed-off-by: spacewander <spacewanderlzx@gmail.com>
  • Loading branch information
spacewander authored Nov 1, 2024
1 parent 12e9d3b commit 6ff6145
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 16 deletions.
5 changes: 3 additions & 2 deletions contrib/golang/common/go/api/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ extern "C" {
#include <stdatomic.h> // NOLINT(modernize-deprecated-headers)
#endif

#include <stdint.h> // NOLINT(modernize-deprecated-headers)
#include <stdbool.h> // NOLINT(modernize-deprecated-headers)
#include <stdint.h> // NOLINT(modernize-deprecated-headers)

typedef struct { // NOLINT(modernize-use-using)
const char* data;
Expand Down Expand Up @@ -96,7 +97,7 @@ CAPIStatus envoyGoFilterHttpSetTrailer(void* s, void* key_data, int key_len, voi
CAPIStatus envoyGoFilterHttpRemoveTrailer(void* s, void* key_data, int key_len);

/* These APIs have nothing to do with the decode/encode phase, use the pointer of httpRequest. */
CAPIStatus envoyGoFilterHttpClearRouteCache(void* r);
CAPIStatus envoyGoFilterHttpClearRouteCache(void* r, bool refresh);
CAPIStatus envoyGoFilterHttpGetStringValue(void* r, int id, uint64_t* value_data, int* value_len);
CAPIStatus envoyGoFilterHttpGetIntegerValue(void* r, int id, uint64_t* value);

Expand Down
2 changes: 1 addition & 1 deletion contrib/golang/common/go/api/capi.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type HttpCAPI interface {
HttpRemoveTrailer(s unsafe.Pointer, key string)

/* These APIs have nothing to do with the decode/encode phase, use the pointer of httpRequest. */
ClearRouteCache(r unsafe.Pointer)
ClearRouteCache(r unsafe.Pointer, refresh bool)

HttpGetStringValue(r unsafe.Pointer, id int) (string, bool)
HttpGetIntegerValue(r unsafe.Pointer, id int) (uint64, bool)
Expand Down
2 changes: 2 additions & 0 deletions contrib/golang/common/go/api/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ type StreamFilterCallbacks interface {
// ClearRouteCache clears the route cache for the current request, and filtermanager will re-fetch the route in the next filter.
// Please be careful to invoke it, since filtermanager will raise an 404 route_not_found response when failed to re-fetch a route.
ClearRouteCache()
// RefreshRouteCache works like ClearRouteCache, but it will re-fetch the route immediately.
RefreshRouteCache()
Log(level LogType, msg string)
LogLevel() LogType
// GetProperty fetch Envoy attribute and return the value as a string.
Expand Down
7 changes: 4 additions & 3 deletions contrib/golang/filters/http/source/cgo.cc
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,10 @@ CAPIStatus envoyGoFilterHttpRemoveTrailer(void* s, void* key_data, int key_len)
});
}

CAPIStatus envoyGoFilterHttpClearRouteCache(void* r) {
return envoyGoFilterHandlerWrapper(
r, [](std::shared_ptr<Filter>& filter) -> CAPIStatus { return filter->clearRouteCache(); });
CAPIStatus envoyGoFilterHttpClearRouteCache(void* r, bool refresh) {
return envoyGoFilterHandlerWrapper(r, [refresh](std::shared_ptr<Filter>& filter) -> CAPIStatus {
return filter->clearRouteCache(refresh);
});
}

CAPIStatus envoyGoFilterHttpGetStringValue(void* r, int id, uint64_t* value_data, int* value_len) {
Expand Down
4 changes: 2 additions & 2 deletions contrib/golang/filters/http/source/go/pkg/http/capi_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ func (c *httpCApiImpl) HttpRemoveTrailer(s unsafe.Pointer, key string) {
handleCApiStatus(res)
}

func (c *httpCApiImpl) ClearRouteCache(r unsafe.Pointer) {
func (c *httpCApiImpl) ClearRouteCache(r unsafe.Pointer, refresh bool) {
req := (*httpRequest)(r)
res := C.envoyGoFilterHttpClearRouteCache(unsafe.Pointer(req.req))
res := C.envoyGoFilterHttpClearRouteCache(unsafe.Pointer(req.req), C.bool(refresh))
handleCApiStatus(res)
}

Expand Down
6 changes: 5 additions & 1 deletion contrib/golang/filters/http/source/go/pkg/http/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@ func (r *httpRequest) recoverPanic() {
}

func (r *httpRequest) ClearRouteCache() {
cAPI.ClearRouteCache(unsafe.Pointer(r))
cAPI.ClearRouteCache(unsafe.Pointer(r), false)
}

func (r *httpRequest) RefreshRouteCache() {
cAPI.ClearRouteCache(unsafe.Pointer(r), true)
}

func (r *httpRequest) Continue(status api.StatusType) {
Expand Down
20 changes: 14 additions & 6 deletions contrib/golang/filters/http/source/golang_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -893,22 +893,20 @@ CAPIStatus Filter::removeTrailer(ProcessorState& state, absl::string_view key) {
return CAPIStatus::CAPIOK;
}

CAPIStatus Filter::clearRouteCache() {
CAPIStatus Filter::clearRouteCache(bool refresh) {
Thread::LockGuard lock(mutex_);
if (has_destroyed_) {
ENVOY_LOG(debug, "golang filter has been destroyed");
return CAPIStatus::CAPIFilterIsDestroy;
}
if (isThreadSafe()) {
ENVOY_LOG(debug, "golang filter clearing route cache");
decoding_state_.getFilterCallbacks()->downstreamCallbacks()->clearRouteCache();
clearRouteCacheInternal(refresh);
} else {
ENVOY_LOG(debug, "golang filter posting clear route cache callback");
auto weak_ptr = weak_from_this();
getDispatcher().post([this, weak_ptr] {
getDispatcher().post([this, weak_ptr, refresh] {
if (!weak_ptr.expired() && !hasDestroyed()) {
ENVOY_LOG(debug, "golang filter clearing route cache");
decoding_state_.getFilterCallbacks()->downstreamCallbacks()->clearRouteCache();
clearRouteCacheInternal(refresh);
} else {
ENVOY_LOG(info, "golang filter has gone or destroyed in clearRouteCache");
}
Expand All @@ -917,6 +915,16 @@ CAPIStatus Filter::clearRouteCache() {
return CAPIStatus::CAPIOK;
}

void Filter::clearRouteCacheInternal(bool refresh) {
ENVOY_LOG(debug, "golang filter clearing route cache, refresh: {}", refresh);
decoding_state_.getFilterCallbacks()->downstreamCallbacks()->clearRouteCache();
if (refresh) {
// When the route cache is clear, the next call to route() will refresh the cache and return the
// pointer to the latest matched route. We don't need the returned pointer.
decoding_state_.getFilterCallbacks()->route();
}
}

CAPIStatus Filter::getIntegerValue(int id, uint64_t* value) {
// lock until this function return since it may running in a Go thread.
Thread::LockGuard lock(mutex_);
Expand Down
3 changes: 2 additions & 1 deletion contrib/golang/filters/http/source/golang_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ class Filter : public Http::StreamFilter,
void log(const Formatter::HttpFormatterContext& log_context,
const StreamInfo::StreamInfo& info) override;

CAPIStatus clearRouteCache();
CAPIStatus clearRouteCache(bool refresh);
void clearRouteCacheInternal(bool refresh);
CAPIStatus continueStatus(ProcessorState& state, GolangStatus status);

CAPIStatus sendLocalReply(ProcessorState& state, Http::Code response_code, std::string body_text,
Expand Down
72 changes: 72 additions & 0 deletions contrib/golang/filters/http/test/golang_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1229,4 +1229,76 @@ TEST_P(GolangIntegrationTest, EncodeHeadersWithoutData_StopAndBufferWatermark_As
testActionWithoutData("encodeHeadersRet=StopAndBufferWatermark&aysnc=1");
}

TEST_P(GolangIntegrationTest, RefreshRouteCache) {
const std::string& so_id = BASIC;
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) {
const std::string key = "golang";
const auto yaml_fmt =
R"EOF(
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute
plugins_config:
%s:
config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
type_url: map
value:
)EOF";
auto yaml = absl::StrFormat(yaml_fmt, so_id);
ProtobufWkt::Any value;
TestUtility::loadFromYaml(yaml, value);

auto* route_first_matched =
hcm.mutable_route_config()->mutable_virtual_hosts(0)->add_routes();
route_first_matched->mutable_match()->set_prefix("/disney/api");
route_first_matched->mutable_typed_per_filter_config()->insert(
Protobuf::MapPair<std::string, ProtobufWkt::Any>(key, value));
auto* resp_header = route_first_matched->add_response_headers_to_add();
auto* header = resp_header->mutable_header();
header->set_key("add-header-from");
header->set_value("first_matched");
route_first_matched->mutable_route()->set_cluster("cluster_0");

auto* route_second_matched =
hcm.mutable_route_config()->mutable_virtual_hosts(0)->add_routes();
route_second_matched->mutable_match()->set_prefix("/user/api");
resp_header = route_second_matched->add_response_headers_to_add();
header = resp_header->mutable_header();
header->set_key("add-header-from");
header->set_value("second_matched");
route_second_matched->mutable_route()->set_cluster("cluster_0");

auto* route_should_not_matched =
hcm.mutable_route_config()->mutable_virtual_hosts(0)->add_routes();
route_should_not_matched->mutable_match()->set_prefix("/api");
resp_header = route_should_not_matched->add_response_headers_to_add();
header = resp_header->mutable_header();
header->set_key("add-header-from");
header->set_value("should_not_matched");
route_should_not_matched->mutable_route()->set_cluster("cluster_0");
});

initializeBasicFilter(so_id, "test.com");

codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"},
{":path", "/disney/api/xx?refreshRoute=1"},
{":scheme", "http"},
{":authority", "test.com"}};

auto encoder_decoder = codec_client_->startRequest(request_headers, true);
auto response = std::move(encoder_decoder.second);

waitForNextUpstreamRequest();

Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}};
upstream_request_->encodeHeaders(response_headers, true);

ASSERT_TRUE(response->waitForEndStream());
EXPECT_EQ("second_matched", getHeader(response->headers(), "add-header-from"));

cleanup();
}

} // namespace Envoy
9 changes: 9 additions & 0 deletions contrib/golang/filters/http/test/test_data/basic/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type filter struct {
badapi bool // bad api call
newPath string // set new path
clearRoute bool // clear route cache

refreshRoute bool // refresh route cache
}

func parseQuery(path string) url.Values {
Expand Down Expand Up @@ -82,6 +84,7 @@ func (f *filter) initRequest(header api.RequestHeaderMap) {
f.badapi = f.query_params.Get("badapi") != ""
f.newPath = f.query_params.Get("newPath")
f.clearRoute = f.query_params.Get("clearRoute") != ""
f.refreshRoute = f.query_params.Get("refreshRoute") != ""
}

func (f *filter) fail(callbacks api.FilterProcessCallbacks, msg string, a ...any) api.StatusType {
Expand Down Expand Up @@ -265,6 +268,12 @@ func (f *filter) decodeHeaders(header api.RequestHeaderMap, endStream bool) api.
if f.clearRoute {
f.callbacks.ClearRouteCache()
}

if f.refreshRoute {
header.SetPath("/user/api/") // path used to match the new route
f.callbacks.RefreshRouteCache()
header.SetPath("/api/") // path used by the upstream
}
return api.Continue
}

Expand Down

0 comments on commit 6ff6145

Please sign in to comment.