Conversation
Signed-off-by: Matt Klein <mklein@lyft.com>
|
w00t!! I think we should create a lua_recipes folder where we dump a set of curated scripts for Envoy. |
|
|
||
| set -e | ||
|
|
||
| VERSION=2.0.5 |
There was a problem hiding this comment.
Suggest moving to 2.1-beta. OpenResty reported 40% speedups for its CDN workloads
There was a problem hiding this comment.
I would prefer to stay on the release version for now. There are going to be bugs in the rest of the code and I would prefer to not be worrying about bugs in the runtime.
There was a problem hiding this comment.
Commenting well after the fact, was it on your radar that luajit is functionally abandonware? (https://www.freelists.org/post/luajit/Looking-for-new-LuaJIT-maintainers) I'm I'm not sure if that changes your opinion any.
Given it's been abandoned for years I'm not convinced 2.1 will ever make it out of beta. Google's been using 2.1 (AFIK exclusively) since 2015 so I'd hope the kinks are pretty much worked out. It turns out the generated lua in Envoy tests isn't 100% compatible with 2.1 which means we have a preference for upgrading upstream to 2.1 as well. :-)
There was a problem hiding this comment.
Yeah, I am aware of the situation. It's not completely abandoned. They are actually working on shipping 2.1. Sure, I can upgrade to 2.1 and fix whatever issues show up.
There was a problem hiding this comment.
@alyssawilk what error are you seeing exactly? I kind of suspect the issue is you aren't compiling internally w/ 5.2 compat options: https://github.com/envoyproxy/envoy/blob/master/ci/build_container/build_recipes/luajit.sh#L35
You probably are going to have to build LuaJIT twice with different defines. :(
| buffering body data so that when the call completes upstream headers can be modified. | ||
| * Performing a direct response and skipping further filter iteration. For example, a script | ||
| could make an upstream HTTP call for authentication, and then directly respond with a 403 | ||
| response code. |
There was a problem hiding this comment.
Can sleeps be used? Or timer routines? Coz that will allow doing custom fault injection scripts. If not, suggest mentioning this. Use of timers is common in OpenResty world.
There was a problem hiding this comment.
I will add an msleep() method as a follow up when I break apart the PR. It will be trivial to add.
| .. code-block:: lua | ||
|
|
||
| -- Called on the request path. | ||
| function envoy_on_request(request_handle) |
There was a problem hiding this comment.
If the name of this function is fixed, it would be clearer if this requirement is specified above (Envoy will call a predefined set of callbacks in request and response path).
There was a problem hiding this comment.
Will reorganize slightly to make this more clear.
| A script can define either or both of these functions. During the request path, Envoy will | ||
| run *envoy_on_request* as a coroutine, passing an API handle. During the response path, Envoy will | ||
| run *envoy_on_response* as a coroutine, passing an API handle. | ||
|
|
There was a problem hiding this comment.
Ah. Ignore my previous comment
htuch
left a comment
There was a problem hiding this comment.
This is awesome. Very excited to see this extensibility. Some comments from a first pass on docs.
|
|
||
| The design of the filter and Lua support at a high level is as follows: | ||
|
|
||
| * All Lua environments are :ref:`per worker thread <arch_overview_threading>`. This means that |
There was a problem hiding this comment.
Do we want persistent state in Lua that extends beyond a single request? Why not have C++ objects hold the per-worker persistent state, accessible via a FFI API, and have Lua state ephemeral?
There was a problem hiding this comment.
I think it would be better to avoid FFI for the public API. It doesn't make for nice Lua APIs, because you can't pass a table into an FFI function and do anything meaningful with it. Or at least that's my recollection of the luajit ffi; it's been a few years since I've used it.
But I do like the idea of being able to store state across requests
There was a problem hiding this comment.
There are a few things at play here:
-
FFI vs. not. Based on my research I recommend we do not use FFI at all. It's not safe and programmers can cause crashes. I would rather we sacrifice perf for a full jail that is safe. (FFI also I believe can really mess up the jitter and in some cases can produce slower code).
-
I did quite a lot of research in terms of how to hide state between requests. First, Lua is not thread safe, so basically the high level state must be per thread. Creating a new state for each request would have a couple of downsides: 1) We would need to re-JIT for every request. 2) There would be no support at all for global/init data. I think it's important for us to allow people to have global state. In the future, I think we could support true global state of some type (maybe just string map, etc.).
There was a problem hiding this comment.
Also, the whole DeathRef situation is to make sure that people cannot incorrectly use objects across requests, but they can store global/expensive state if they really need to.
There was a problem hiding this comment.
OK, thanks for the followup. I'm new to Lua embeddings, just wanted to make sure the design space is fully covered, which knowing the folks involved, was a given anyway ;)
| safe to write. Very complex or high performance use cases are assumed to use the native C++ filter | ||
| API. | ||
|
|
||
| * Inspection of headers, body, and trailers while streaming in either the request flow, response |
There was a problem hiding this comment.
I assume that once we have routing capabilities in filters, this is would be one of the things we'd also want to support (doesn't seem too hard, gives a lot of power..)?
There was a problem hiding this comment.
What do you mean by routing capabilities?
There was a problem hiding this comment.
@htuch do you mean giving the filter the ability to retrigger route computation after it has modified headers, etc? Almost like pre-routing in netfilter. This is turning out to be a requirement in Istio land as well, where people want to decode a JWT token, extract a user ID and then do the routing based on it. One option is to have the filter run before the base connection manager code and the route table check (but that seems complicated). Another option is to just provide an API to the the filter to kick the route computation again and store the upstream cluster in the cache (we used to have this a while ago, before we replaced it with the cached route in active stream filter).
IIUC, what @htuch is implying is that Lua filter could mutate headers (and potentially the path, doing stuff like /foo/bar/baz -> /foo-bar-baz/api and then recompute the routes to determine the upstream cluster. Alternatively, I can imagine doing stuff like taking a JWT token, decoding it, extracting user ID and sticking it into a Cookie header ;), and then doign header based matches.
There was a problem hiding this comment.
@rshriram Yes and yeah, we also have an internal need for this. I think you answered @mattklein123's question here, and the issue is largely orthogonal to Lua filters, but when thinking about filter APIs in general, this is something that would be good to have in Envoy, so just a friendly reminder that there is demand (and possibly cycles available to work on this at our end).
There was a problem hiding this comment.
My thinking on this is to just call clearRouteCache() (https://github.com/envoyproxy/envoy/blob/master/include/envoy/http/filter.h#L118) any time a script modifies the headers to avoid scripters needing to understand the idea of route caching at all (I have to admit that when I don't need to obsess over perf at all times it's pretty nice!). I will add this in. In the future, we will likely expose the route object with a wrapper also so scripts can do more stuff based on the chosen route.
| iterator = handle:bodyChunks() | ||
|
|
||
| Returns an iterator that can be used to iterate through all received body chunks as they arrive. | ||
| Envoy will yield rthe script in between chunks, but *will not buffer* them. This can be used by |
|
|
||
| handle:log(level, message) | ||
|
|
||
| Logs a message using Envoy's application logging. *level* is an integer in the range 0 - 5 which |
There was a problem hiding this comment.
Are there no symbolic constants in Lua for this?
There was a problem hiding this comment.
Not really. Lua basically has strings and ints. We could do a string match, which might be fine? Otherwise we have ints. That's it.
There was a problem hiding this comment.
The only lua-ish thing you could do is pass in a table that maps the symbolic names to the integer constant. Or provide a separate wrapper-function for each log-level that just calls the log() you defined above: logError(), logInfo(), etc.
There was a problem hiding this comment.
Yeah, I'm happy to do log*() if that is preferable.
| Logs a message using Envoy's application logging. *level* is an integer in the range 0 - 5 which | ||
| maps to the spdlog levels *trace* - *critical*. *message* is a string to log. | ||
|
|
||
| httpCall() |
There was a problem hiding this comment.
It will be awesome when we can do gRPC here BTW, but I know this is not for MVP.
There was a problem hiding this comment.
I honestly have no idea how this would work. The issue is protos, not making the call. Definitely not in the MVP. I will put it in as a potential follow up.
There was a problem hiding this comment.
I have started crying :( .... gRPC in lua...
There was a problem hiding this comment.
Yeah, I know the protos are the issue. There are libraries out there, e.g. https://github.com/urbanairship/protobuf-lua, but NFI how close to being useful for general purpose gRPC in a filter they will be.
|
|
||
| .. code-block:: lua | ||
|
|
||
| headers:iterate( |
There was a problem hiding this comment.
It would be nice if you could make pairs() work for the type. See http://lua-users.org/wiki/GeneralizedPairsAndIpairs
There was a problem hiding this comment.
This has to do with the underlying header map API. Given that we have to copy to strings anyway, and perf doesn't matter as much, I can probably also supports a pairs() iterator. Let me look into it.
There was a problem hiding this comment.
FYI looks like to get generalized pairs we would need to enable -DLUAJIT_ENABLE_LUA52COMPAT. Are we OK with that? http://luajit.org/extensions.html
There was a problem hiding this comment.
I'm generally in favor of those 5.2 features, but I don't feel strongly about it.
There was a problem hiding this comment.
doing pairs() is a little annoying without adding #1851, but I can do it in a slightly hacky way that we can clean up later so at least the Lua API is consistent. Will change this up and enable LUAJIT_ENABLE_LUA52COMPAT and document.
|
Could you add a wait-for-fd api for low level integrations? |
What does that mean exactly? |
|
|
||
| .. _config_http_filters_lua_header_wrapper: | ||
|
|
||
| Header object API |
There was a problem hiding this comment.
Copy the https://daurnimator.github.io/lua-http/0.2/#http.headers api.
There was a problem hiding this comment.
I took a look. For now I'm skipping it as am trying to provide a very simple initial offering. We can reconsider in the future depending on demand.
There was a problem hiding this comment.
It's just as simple. It is just different method names and some slightly different functionality.
This is the sort of class that you'll have huge issues changing in future due to backwards compatibility concerns, so get it right on the first go!
| yield_callback(); | ||
| } else { | ||
| state_ = State::Finished; | ||
| const char* error = lua_tostring(coroutine_state_.get(), -1); |
To allow users to utilitise existing C and lua libraries (instead of reimplmenting network protocols), you need to provide the ability to pause the current coroutine until a file descriptor is ready (or a timeout expires). |
Maybe. Let's see how this evolves. The general goal is to allow people a very controlled set of functionality and not to have this be fully generic. We can see what people want to build once the basic functionality is in. |
I can guarantee that someone will want to make a HTTP api call to an external service. |
Making a call to an external service is already supported, via Envoy. I'm not sure why anyone would want to use a different library to do this. |
|
@ggreenway @htuch @rshriram I updated with the major comments (switch to log*, use pairs() to iterate). I also opened a tracking issue for small follow ups. Unless there are objections I'm going to start breaking this into small PRs. |
However HTTP was only an example. Others will want to use libraries such as nanomsg, or modbus to communicate with external daemons. A "wait-for-fd" API allows all of the above. |
@daurnimator I think it's unlikely we will support something like this in the near term but please feel free to open up a tracking ticket and we can consider it for the future. Thanks. |
|
|
||
| In the currently implementation, headers cannot be modified during iteration. Additionally, if | ||
| it is desired to modify headers after iteration, the iteration must be completed. Meaning, do | ||
| not use `break` or any other mechanism to exit the loop early. This may be relaxed in the future. |
There was a problem hiding this comment.
given that we are in scripting land, does it make sense in the initial cut to simply copy out the entire header map to lua, and then copy it back into envoy?
like
hdrs = headers:clone() // full copy
.. do lua stuff [ipairs/pairs/etc]
headers:reset(hdrs)There was a problem hiding this comment.
Meh, I think the key/value and direct modification is nicer? I think I can fix any usability issues that people encounter.
There was a problem hiding this comment.
direct modification is nice. But the text suggests that one cannot modify keys during iteration, and that the iteration has to be completed (not even break).
There was a problem hiding this comment.
Let me think about this. I could see additionally adding a clone/merge API that operates on tables, but IMO that should be separate.
|
sgtm |
Signed-off-by: Matt Klein <mklein@lyft.com>
Signed-off-by: Matt Klein <mklein@lyft.com>
Signed-off-by: Matt Klein <mklein@lyft.com>
Signed-off-by: Matt Klein <mklein@lyft.com>
|
@htuch just pushed commit which properly invalidates wrappers across yields which will prevent use of headers after they have been changed. |
htuch
left a comment
There was a problem hiding this comment.
LGTM, I only have some very minor feedback.
|
|
||
| The filter only supports loading Lua code in-line in the configuration. If local filesystem code | ||
| is desired, a trivial in-line script can be used to load the rest of the code from the local | ||
| environment. |
There was a problem hiding this comment.
This is probably another place need to think about sandboxing if we want to support multi-tenancy fully.
| { | ||
| "name": "lua", | ||
| "config": { | ||
| "inline_code": "..." |
There was a problem hiding this comment.
Can you add a v2 proto definition?
| "nope") | ||
| end | ||
|
|
||
| Stream handle API |
There was a problem hiding this comment.
Not sure if this should come before/after examples..
| handle:respond(headers, body) | ||
|
|
||
| Respond immediately and do not continue further filter iteration. This call is *only valid in | ||
| the request flow*. *headers* is a table of key/value pairs to send. Note that the *:status* header |
There was a problem hiding this comment.
Is there some other limit to validity? I'm thinking that you've partially processed a request stream, the response has started and then you try and :respond() in the request flow.
| -- Called on the request path. | ||
| function envoy_on_request(request_handle) | ||
| -- Wait for the entire request body and add a request header with the body size. | ||
| request_handle:headers():add("request_body_size", request_handle:body():byteSize()) |
There was a problem hiding this comment.
These have changed to length() I think..
| saw_body_ = true; | ||
|
|
||
| if (state_ == State::WaitForBodyChunk) { | ||
| ENVOY_LOG(debug, "resuming for next body chunk"); |
| coroutine_->resume(0, yield_callback_); | ||
| } | ||
|
|
||
| if (state_ == State::HttpCall || state_ == State::WaitForBody) { |
There was a problem hiding this comment.
It seems that we have to modify this global State for each new blocking feature we add.. can you comment on the tradeoffs here?
| // Uses 'key' (at index -2) and 'value' (at index -1). | ||
| const char* key = luaL_checkstring(state, -2); | ||
| const char* value = luaL_checkstring(state, -1); | ||
| headers->addCopy(LowerCaseString(key), value); |
There was a problem hiding this comment.
Memory resource use seems to be another place that multi-tenant Lua filters will fall flat..
| const char* value = luaL_checkstring(state, -1); | ||
| headers->addCopy(LowerCaseString(key), value); | ||
|
|
||
| // removes 'value'; keeps 'key' for next iteration. |
|
@htuch updated |
|
i really need this feature so i checked out your commit on OSX and built it via docker ( config.json {
"listeners": [
{
"address": "tcp://0.0.0.0:80",
"filters": [
{
"name": "http_connection_manager",
"config": {
"codec_type": "auto",
"stat_prefix": "ingress_http",
"route_config": {
"virtual_hosts": [
{
"name": "backend",
"domains": ["*"],
"routes": [
{
"timeout_ms": 0,
"prefix": "/service/1",
"cluster": "service1"
},
{
"timeout_ms": 0,
"prefix": "/service/2",
"cluster": "service2"
}
]
}
]
},
"filters": [
{
"name": "lua",
"config": {
"inline_code": "function envoy_on_response(response_handle)\nresponse_handle:headers():add(\"response_body_size\", response_handle:body():byteSize())\nend"
}
},
{
"name": "router",
"config": {}
}
]
}
}
]
}
], |
|
I also verified this by building on my mac with bazel build, these are the only lua files i see being compiled. got the same error |
|
@nmnellis fixed w/ latest commit. |
htuch
left a comment
There was a problem hiding this comment.
Time to ship it :) I've looked over most of the core code and it looks fine, and the tests look like they cover most of the corner cases, but I haven't audited them line-by-line. This is awesome, looking forward to seeing this stabilize and evolve. Feel free to fix nits and ship.
| namespace Lua { | ||
|
|
||
| /** | ||
| * Callbacks used by a a strem handler to access the filter. |
There was a problem hiding this comment.
Nit: stream (don't block shipping on this one ;) )
|
|
||
| /** | ||
| * @return a handle to the trailers or nil if there are no trailers. This call will cause the | ||
| * script to yield of Envoy does not yet know if there are trailers or not. |
|
|
||
| /** | ||
| * Log a message to the Envoy log. | ||
| * @param 1 (string): The log message. |
There was a problem hiding this comment.
One day we will render docs with Doxygen and some of this might end up looking might weird, but until that day comes, sure.
|
Ah, cool, I didn't know a 2.1 non-beta is in progress (not my area
clearly) Is there an ETA on that?
…On Wed, Nov 8, 2017 at 11:40 AM, Matt Klein ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In ci/build_container/build_recipes/luajit.sh
<#1859 (comment)>:
> @@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+VERSION=2.0.5
Yeah, I am aware of the situation. It's not completely abandoned. They are
actually working on shipping 2.1. Sure, I can upgrade to 2.1 and fix
whatever issues show up.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#1859 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ARYFvc0UnPF9yqSxKdGNyCrlXldg7t3Qks5s0dmDgaJpZM4P59BO>
.
|
|
Yep, that worked, thanks muchly!
…On Wed, Nov 8, 2017 at 12:31 PM, Matt Klein ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In ci/build_container/build_recipes/luajit.sh
<#1859 (comment)>:
> @@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+VERSION=2.0.5
@alyssawilk <https://github.com/alyssawilk> what error are you seeing
exactly? I kind of suspect the issue is you aren't compiling internally w/
5.2 compat options: https://github.com/envoyproxy/
envoy/blob/master/ci/build_container/build_recipes/luajit.sh#L35
You probably are going to have to build LuaJIT twice with different
defines. :(
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1859 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ARYFvesZo40MenklnGgy5Mpy3hzk3Ky0ks5s0eVggaJpZM4P59BO>
.
|
Signed-off-by: Shriram Rajagopalan <shriramr@vmware.com>
…roxy#1860) This reverts commit f47be07.

This commit adds an experimental HTTP LuaJIT filter. This is a very complex
feature and my goal with this massive pull request is to get general feedback
on the API, approach, etc. Once folks like the general idea I will break it up
into smaller pull requests that are easier to review.
For a very high level review, please start with the documentation, followed by
the integration tests. This will provide a good idea of what has been
implemented.
Note that the API surface here is small. I still suspect that this will cover 90%+
of what most people want to do in a script. The goal is not to completely
replicate the C++ filter API but to allow people that can't/won't write C++
filters some level of customizability.