-
Notifications
You must be signed in to change notification settings - Fork 5.1k
lua: add fire-and-forget functionality to http call #10145
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
Conversation
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
…er constructor Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
|
||
.. code-block:: lua | ||
|
||
headers, body = handle:httpCallAsync(cluster, headers, body, timeout) |
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.
drive-by comment; I know nothing about Lua but I would've expected, and would hope you would not return headers/body from an async function, but would instead have the user supply another optional argument, which would be some sort of closure or lambda to run when the async fetch was complete, passing the headers and body to that function.
Even better, I think 2 lambdas should be supplied: one that is passed the headers when available. The second is called with body-chunks, until there are no more. It would receive a bool param that would be 'true' when the request is complete.
Actually I haven't looked deeply at the Envoy infrastructure for HTTP Fetches, but modeling the Lua interface based on that pattern (which might differ from my suggestion above) would be even better.
The phrase 'fire and forget' doesn't seem good to me from a resource usage perspective.
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.
Ah, you're correct, this does not return headers/body. Pushed a change. Thanks for noticing that!
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.
Resource/memory wise, it looks like there are checks in the pipeline for this (I ran into it in previous pushes, and resolved them).
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.
@jmarantz and I talked about this offline. We will rename this httpCallNonblocking
and proceed without callbacks.
Signed-off-by: mindyor <or.mindy@gmail.com>
/retest |
🔨 rebuilding |
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
Thanks for working on this! Overall it is good. Flushing some comments, mostly nits.
@@ -285,6 +297,38 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan | |||
State state_{State::Running}; | |||
std::function<void()> yield_callback_; | |||
Http::AsyncClient::Request* http_request_{}; | |||
FireAndForgetWriter* fireAndForgetWriter_; |
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.
WDYT of making this a unique_ptr
?
}; | ||
|
||
/** | ||
* Implementation that takes incoming requests and implements |
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.
Sorry, could you expand this a little bit? I'm a bit confused by "takes incoming requests".
}; | ||
|
||
/** | ||
* A class with shared code for building and making http calls |
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 super nit,
* A class with shared code for building and making http calls | |
* A class with shared code for building and making HTTP calls. |
@@ -355,6 +355,52 @@ name: envoy.lua | |||
EXPECT_EQ("nope", response->body()); | |||
} | |||
|
|||
// Upstream fire and forget async call |
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.
// Upstream fire and forget async call | |
// Upstream fire-and-forget asynchronous call. |
@@ -799,6 +799,59 @@ TEST_F(LuaHttpFilterTest, HttpCall) { | |||
callbacks->onSuccess(std::move(response_message)); | |||
} | |||
|
|||
// Basic Asynchronous, Fire and Forget HTTP request flow. |
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.
// Basic Asynchronous, Fire and Forget HTTP request flow. | |
// Basic asynchronous, fire-and-forget HTTP request flow. |
int FireAndForgetWriter::luaHttpCallNonblocking(lua_State* state) { | ||
if (LuaFilterUtil::makeHttpCall(state, filter_, *this)) { | ||
return 0; | ||
} else { |
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.
Superfluous else
? We can remove this, but I don't feel strongly about it.
* Implementation that takes incoming requests and implements | ||
* "fire and forget" behavior using an async client. | ||
*/ | ||
class FireAndForgetWriter : public Filters::Common::Lua::BaseLuaObject<FireAndForgetWriter>, |
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.
Since this writer currently is an HTTP async client "writer". Can we name it as FireAndForgetHttpWriter
or HttpAsyncClientWriter
or similar?
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
This looks like a great start, thanks for working on this!
|
||
handle:httpCallNonblocking(cluster, headers, body, timeout) | ||
|
||
Makes an HTTP call to an upstream host. Same behavior as httpCall, except that Envoy will fire and forget. |
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 would suggest spelling out what fire and forget means here wrt continuation of the filter chain
@@ -23,7 +23,10 @@ StreamHandleWrapper::StreamHandleWrapper(Filters::Common::Lua::Coroutine& corout | |||
if (state_ == State::Running) { | |||
throw Filters::Common::Lua::LuaException("script performed an unexpected yield"); | |||
} | |||
}) {} | |||
}), | |||
fireAndForgetHttpWriter_(new FireAndForgetHttpWriter(filter)) {} |
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.
no reason to heap allocate this, just use it as a non-pointer value
FireAndForgetHttpWriter fire_and_forget_http_writer_;
and just fire_and_forget_http_writer_(filter)
in the constructor
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.
This actually does need to be heap allocated because the success/failure callbacks can live beyond the filter, but not in the way it is done here. The Lua filter config needs to have a fire and forget writer that works identically to the router's shadow writer. Take a look at the pattern that is used there.
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.
(Or yes per other comment just pass static callbacks and that should be fine also).
}), | ||
fireAndForgetHttpWriter_(new FireAndForgetHttpWriter(filter)) {} | ||
|
||
StreamHandleWrapper::~StreamHandleWrapper() { delete fireAndForgetHttpWriter_; } |
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.
if you are going to be doing heap allocations please use std::unique_ptr/std::shared_ptr to manage the memory
|
||
Http::AsyncClient::Request* | ||
LuaFilterUtil::makeHttpCall(lua_State* state, Filter& filter, | ||
Http::AsyncClient::Callbacks& callbacksListener) { |
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.
nit: callbacksListener -> callbacks (or at the very least snake case parameter names)
@@ -485,6 +430,81 @@ int StreamHandleWrapper::luaImportPublicKey(lua_State* state) { | |||
return 1; | |||
} | |||
|
|||
FireAndForgetHttpWriter::FireAndForgetHttpWriter(Filter& filter) : filter_(filter) {} |
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 don't think this class buys us a whole lot, how about just having a NoopCallbacks
that we pass to makeHttpCall
?
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.
Hum, true.
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.
Good idea. Pushed.
/** | ||
* A class with shared code for building and making HTTP calls. | ||
*/ | ||
class LuaFilterUtil : public Filters::Common::Lua::BaseLuaObject<LuaFilterUtil> { |
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.
Instead of making these utility functions exposed outside of this class/translation unit i'd suggest putting them in the anonymous namespace in the .cc file. This tends to be the preferred way of doing helper functions that are only used in one .cc file and that doesn't benefit from being a member function
you can look for namespace {
in existing code to see what an anonymous namespace looks like
|
||
waitForNextUpstreamRequest(); | ||
|
||
upstream_request_->encodeHeaders(default_response_headers_, true); |
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 know this is tested in the unit test but it would be nice to assert that the headers/body of the upstream request matches what we specified in the call
class FireAndForgetHttpWriter : public Filters::Common::Lua::BaseLuaObject<FireAndForgetHttpWriter>, | ||
public Http::AsyncClient::Callbacks { | ||
public: | ||
FireAndForgetHttpWriter(Filter& filter); |
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.
use explicit
for single arg ctors: https://google.github.io/styleguide/cppguide.html#Implicit_Conversions
@@ -244,6 +244,23 @@ data to send. *timeout* is an integer that specifies the call timeout in millise | |||
Returns *headers* which is a table of response headers. Returns *body* which is the string response | |||
body. May be nil if there is no body. | |||
|
|||
httpCallNonblocking() |
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'm not a huge fan of this name (it sounds a bit clunky/verbose) and it worries me a bit if we are going to end up with one new function per variation of a HTTP call (httpCallNonblockingWithCallbacks
kinda situation)
are we able to/can we add parameters to existing LUA filters? it'd be nice to either have one or two (e.g. httpCall
and asyncHttpCall
) functions with options for whether the response should be dropped, callbacks to invoke on response etc.
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.
Are we able to/can we add parameters to existing LUA filters?
I think we can, yeah. This is a reasonable/great ask, yeah we should do this.
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.
Yes I think we can do this and SGTM
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.
Makes sense.
Removed the new function, adding a parameter asynchronous
. Hope that looks cleaner!
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
Thanks for iterating on this, its looking pretty good
@@ -23,7 +90,10 @@ StreamHandleWrapper::StreamHandleWrapper(Filters::Common::Lua::Coroutine& corout | |||
if (state_ == State::Running) { | |||
throw Filters::Common::Lua::LuaException("script performed an unexpected yield"); | |||
} | |||
}) {} | |||
}), | |||
noopCallbacks_(new NoopCallbacks()) {} |
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.
Since NoopCallbacks
doesn't hold any state, I think it would be clearer to define it as a ConstSingleton
and just pass the reference to the singleton object. That way we avoid having to deal with this memory management.
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.
vc'd. created static object instead.
* @param 4 (int): Timeout in milliseconds for the call. | ||
* @return headers (table), body (string/nil) | ||
*/ | ||
DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaHttpCallBlocking); |
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.
is still necessary now that we're using the parameterized function instead? as in, they don't need to be LUA functions
@@ -485,6 +507,8 @@ int StreamHandleWrapper::luaImportPublicKey(lua_State* state) { | |||
return 1; | |||
} | |||
|
|||
NoopCallbacks::NoopCallbacks() {} |
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.
no reason to explicitly define this, its the same as the default constructor
@@ -156,10 +159,31 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan | |||
* @param 2 (table): A table of HTTP headers. :method, :path, and :authority must be defined. | |||
* @param 3 (string): Body. Can be nil. | |||
* @param 4 (int): Timeout in milliseconds for the call. | |||
* @param 5 (bool): Flag. If true, filter continues without waiting for HTTP response from upstream service. |
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.
this is optional right? probably want to explicitly specify the default behavior
if (timeout_ms > 0) { | ||
timeout = std::chrono::milliseconds(timeout_ms); | ||
bool asynchronous = lua_toboolean(state, 6); | ||
if(asynchronous) { |
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.
it'd nice if we're consistent with naming here, something like
if (async) {
asyncHttpCall(state);
} else {
syncHttpCall(state);
}
instead of using blocking/async interchangeably. it'd be nice if we had this consistency both here and elsewhere (e.g. in the tests)
docs/root/intro/version_history.rst
Outdated
@@ -9,6 +9,7 @@ Version history | |||
minimum. | |||
* config: use type URL to select an extension whenever the config type URL (or its previous versions) uniquely identify a typed extension, see :ref:`extension configuration <config_overview_extension_configuration>`. | |||
* http: fixing a bug in HTTP/1.0 responses where Connection: keep-alive was not appended for connections which were kept alive. | |||
* lua: added `httpCallNonblocking` function. Same as `httpCall` but fire-and-forget. |
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.
this needs to be updated
Http::TestRequestTrailerMapImpl request_trailers{{"foo", "bar"}}; | ||
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); | ||
|
||
Http::ResponseMessagePtr response_message(new Http::ResponseMessageImpl( |
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.
this seems unused?
@@ -799,6 +799,123 @@ TEST_F(LuaHttpFilterTest, HttpCall) { | |||
callbacks->onSuccess(std::move(response_message)); | |||
} | |||
|
|||
// Basic HTTP request flow. Async flag set to false |
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.
nit: proper punctuation in comments (end with period)
…e async/sync over blocking terminology Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
Thanks, this is really close to done imo
docs/root/intro/version_history.rst
Outdated
@@ -20,6 +20,7 @@ Version history | |||
* listener filters: listener filter extensions use the "envoy.filters.listener" name space. A | |||
mapping of extension names is available in the :ref:`deprecated <deprecated>` documentation. | |||
* listeners: fixed issue where :ref:`TLS inspector listener filter <config_listener_filters_tls_inspector>` could have been bypassed by a client using only TLS 1.3. | |||
* lua: added optional asynchronous behavior `httpCall` function. Makes fire-and-forget call upstream- ignores response. |
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'd do something like * lua: added a parameter to `httpCall` that makes it possible to have the call be asynchronous.
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.
Good idea.
absl::optional<std::chrono::milliseconds> timeout; | ||
if (timeout_ms > 0) { | ||
timeout = std::chrono::milliseconds(timeout_ms); | ||
bool asynchronous = lua_toboolean(state, 6); |
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.
nit: const bool
|
||
waitForNextUpstreamRequest(); | ||
|
||
EXPECT_THAT(lua_request_->headers(), HeaderValueOf(Http::Headers::get().Method, "POST")); |
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.
id move these next to the above ASSERT_TRUE
s. also maybe a small comment like // Sanity checking that we sent the expected data.
Signed-off-by: mindyor <or.mindy@gmail.com>
absl::optional<std::chrono::milliseconds> timeout; | ||
if (timeout_ms > 0) { | ||
timeout = std::chrono::milliseconds(timeout_ms); | ||
const bool asynchronous = lua_toboolean(state, 6); |
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.
Should we test with lua_isboolean
here? Since lua_toboolean
returns 1 for any Lua value different from false and nil. E.g. typo like faIse (yes that's capital "I") can turn it out to be true. WDYT?
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.
Fair point. I'll add that to my next push.
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
LGTM, thanks for adding this!
@dio any more feedback?
@mattklein123 Wanna take a look as well? In particular the way the API turned out
/retest |
🔨 rebuilding |
/retest to understand the coverage job. |
🔨 rebuilding |
Coverage is broken right now and being worked on. Please stop retesting. I will take a look at the PR soon. |
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.
Nice, great work! Just a few nits and LGTM! Thank you!
/wait
if (body != nullptr) { | ||
message->body() = std::make_unique<Buffer::OwnedImpl>(body, body_size); | ||
message->headers().setContentLength(body_size); | ||
const int asyncFlagIndex = 6; |
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.
nit: async_flag_index
@@ -86,6 +86,8 @@ class FilterCallbacks { | |||
|
|||
class Filter; | |||
|
|||
class NoopCallbacks; |
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 don't think this needs forward declaration?
public: | ||
// Http::AsyncClient::Callbacks | ||
void onSuccess(Http::ResponseMessagePtr&&) override {} | ||
|
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.
nit: del newline
Signed-off-by: mindyor <or.mindy@gmail.com>
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.
Thanks!
Description:
Envoy's Lua function
httpCall
is synchronous - Envoy waits until the upstream request completes before continuing.This PR adds an optional flag to
httpCall
enabling the request to be asynchronous - Envoy does not wait until the upstream service responds, and in fact ignores the response.httpCall
continues to be synchronous by default.Risk Level: Medium?
Testing: added a unit test and an integration test
Docs Changes: added doc change to lua_filter.rst
Release Notes: added release note to version_history.rst