Skip to content

Conversation

mindyor
Copy link
Contributor

@mindyor mindyor commented Feb 24, 2020

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

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>
@dio dio self-assigned this Feb 24, 2020
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>
@mindyor mindyor changed the title WIP - lua: create async http call lua: create async http call Feb 26, 2020

.. code-block:: lua

headers, body = handle:httpCallAsync(cluster, headers, body, timeout)
Copy link
Contributor

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.

Copy link
Contributor Author

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!

Copy link
Contributor Author

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).

Copy link
Contributor Author

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>
@mindyor
Copy link
Contributor Author

mindyor commented Feb 27, 2020

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: coverage (failed build)

🐱

Caused by: a #10145 (comment) was created by @mindyor.

see: more, trace.

@mindyor mindyor changed the title lua: create async http call lua: create nonblocking http call Feb 27, 2020
Copy link
Member

@dio dio left a 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_;
Copy link
Member

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
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A super nit,

Suggested change
* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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 {
Copy link
Member

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>,
Copy link
Member

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>
Copy link
Contributor

@snowp snowp left a 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.
Copy link
Contributor

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)) {}
Copy link
Contributor

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

Copy link
Member

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.

Copy link
Member

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_; }
Copy link
Contributor

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) {
Copy link
Contributor

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) {}
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, true.

Copy link
Contributor Author

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> {
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -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()
Copy link
Contributor

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.

@mattklein123 @jmarantz @dio

Copy link
Member

@dio dio Feb 28, 2020

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.

Copy link
Member

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

Copy link
Contributor Author

@mindyor mindyor Mar 3, 2020

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>
Copy link
Contributor

@snowp snowp left a 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()) {}
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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() {}
Copy link
Contributor

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.
Copy link
Contributor

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) {
Copy link
Contributor

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)

@@ -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.
Copy link
Contributor

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(
Copy link
Contributor

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
Copy link
Contributor

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)

mindyor added 6 commits March 2, 2020 21:36
…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>
@mindyor mindyor changed the title lua: create nonblocking http call lua: create asynchronous http call Mar 3, 2020
@mindyor mindyor changed the title lua: create asynchronous http call lua: add fire-and-forget functionality to http call Mar 3, 2020
Copy link
Contributor

@snowp snowp left a 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

@@ -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.
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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"));
Copy link
Contributor

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_TRUEs. 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);
Copy link
Member

@dio dio Mar 4, 2020

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?

Copy link
Contributor Author

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.

mindyor added 2 commits March 4, 2020 12:25
Signed-off-by: mindyor <or.mindy@gmail.com>
Signed-off-by: mindyor <or.mindy@gmail.com>
snowp
snowp previously approved these changes Mar 4, 2020
Copy link
Contributor

@snowp snowp left a 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

@mattklein123 mattklein123 self-assigned this Mar 4, 2020
@mindyor
Copy link
Contributor Author

mindyor commented Mar 5, 2020

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: coverage (failed build)

🐱

Caused by: a #10145 (comment) was created by @mindyor.

see: more, trace.

@mindyor
Copy link
Contributor Author

mindyor commented Mar 5, 2020

/retest to understand the coverage job.

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: coverage (failed build)

🐱

Caused by: a #10145 (comment) was created by @mindyor.

see: more, trace.

@mattklein123
Copy link
Member

Coverage is broken right now and being worked on. Please stop retesting. I will take a look at the PR soon.

Copy link
Member

@mattklein123 mattklein123 left a 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;
Copy link
Member

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;
Copy link
Member

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 {}

Copy link
Member

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>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@mattklein123 mattklein123 merged commit 6e9ed57 into envoyproxy:master Mar 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants