Skip to content

Conversation

@original-brownbear
Copy link
Contributor

@original-brownbear original-brownbear commented Mar 2, 2025

We ahve some incredibly deep callstacks in security that seem to visibly raise context switch costs, make profiling more complicated and generally make the code rather hard to follow.
Also, deeply nested listener chains tend to make logic consume more heap because we almost inevitably continue holding on to things we no longer need as we go down the stack.
Since the methods adjusted here mostly return a result synchronously on the hot path, we can both save overhead (we can do a couple follow-ups to that effect) and make things a little easier to follow by using promises as returns in place of consuming callbacks.

A good example of the effects of this change would be this piece of search request REST handling that flattens out enormously.

Before:
image

After:
image

We ahve some incredibly deep callstacks in security that seem to visibly
raise context switch costs, make profiling more complicated and
generally make the code rather hard to follow.
Since the methods adjusted here return a result synchronously we can
both save overhead and make things a little easier to follow by using
promises as returns in place of consuming callbacks.
@elasticsearchmachine elasticsearchmachine added the Team:Security Meta label for security team label Mar 2, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-security (Team:Security)

Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

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

Should we do something similar to flatten out RequestFilterChain#proceed rather than using recursion there too?

Comment on lines 657 to 661
try {
res.rawResult();
} catch (Exception e) {
listener.onFailure(e);
return;
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 rather add something like this to SubscribableListener over exposing rawResult():

    public boolean completeIfFailed(ActionListener<?> listener) {
        final Object currentState = state;
        if (currentState instanceof FailureResult result) {
            listener.onFailure(result.exception());
            return true;
        } else {
            return false;
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went for just an isSuccess now like other solutions have. There isn't much of a reason to be tricky for the failure path and this way we have a fast solution for the hot success path. Let me know what you think :) Combining this approach with the get-results on listenable futures would allow neat simplifications + speedups in some places I believe (by avoiding allocating new listeners).

@original-brownbear
Copy link
Contributor Author

Should we do something similar to flatten out RequestFilterChain#proceed rather than using recursion there too?

I think we should yea, gotta be a little careful maybe because of thread-context though. That kind of thing is made it impossible to go further with this kind of change in security for the time being when I tried because any thread-local context that only exists around the onResponse is lost if the listener hasn't been added before.

Comment on lines +272 to +273
public final boolean isSuccess() {
return state instanceof SuccessResult;
Copy link
Contributor

Choose a reason for hiding this comment

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

This bit LGTM, I'll leave the rest to the security team

@slobodanadamovic slobodanadamovic self-requested a review March 4, 2025 11:42
Copy link
Contributor

@slobodanadamovic slobodanadamovic left a comment

Choose a reason for hiding this comment

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

The changes LGTM 👍 Nice job!

(I left a non-blocking question and comment regarding adjusting the example plugin)

* @return a listener to be notified of the authorization result
*/
void authorizeIndexAction(
SubscribableListener<IndexAuthorizationResult> authorizeIndexAction(
Copy link
Contributor

@slobodanadamovic slobodanadamovic Mar 4, 2025

Choose a reason for hiding this comment

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

The change to this interface will cause compilation errors of CustomAuthorizationEngine. Can you adjust the security example plugin as well?

while (requestInterceptorIterator.hasNext()) {
var res = requestInterceptorIterator.next().intercept(requestInfo, authorizationEngine, authorizationInfo);
if (res.isSuccess() == false) {
res.addListener(new DelegatingActionListener<>(listener) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What would you think if we changed the request interceptors to be fully sync and not return a promise?

public interface RequestInterceptor {

    void intercept(RequestInfo requestInfo, AuthorizationEngine authorizationEngine, AuthorizationInfo authorizationInfo)
        throws ElasticsearchSecurityException;
}

then runRequestInterceptors would look like this

private void runRequestInterceptors(
    RequestInfo requestInfo,
    AuthorizationInfo authorizationInfo,
    AuthorizationEngine authorizationEngine,
    ActionListener<Void> listener
) {
    for (RequestInterceptor requestInterceptor : requestInterceptors) {
        try {
            requestInterceptor.intercept(requestInfo, authorizationEngine, authorizationInfo);
        } catch (Exception e) {
            listener.onFailure(e);
            return;
        }
    }
    listener.onResponse(null);
}

@original-brownbear original-brownbear added the auto-backport Automatically create backport pull requests when merged label Mar 5, 2025
@original-brownbear
Copy link
Contributor Author

Thanks you two!

I fixed up the plugin. + I'm all on board with a sync API here, but is that possible looking at the code it might fork today?

@original-brownbear original-brownbear merged commit b1c75d1 into elastic:main Mar 5, 2025
22 checks passed
@original-brownbear original-brownbear deleted the stack-savings-security branch March 5, 2025 11:08
@elasticsearchmachine
Copy link
Collaborator

💔 Backport failed

Status Branch Result
8.x Commit could not be cherrypicked due to conflicts

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 123812

georgewallace pushed a commit to georgewallace/elasticsearch that referenced this pull request Mar 11, 2025
…tic#123812)

We have some incredibly deep callstacks in security that seem to visibly
raise context switch costs, make profiling more complicated and
generally make the code rather hard to follow.
Since the methods adjusted here return a result synchronously we can
both save overhead and make things a little easier to follow by using
promises as returns in place of consuming callbacks.
@slobodanadamovic
Copy link
Contributor

  • I'm all on board with a sync API here, but is that possible looking at the code it might fork today?

You are right. 🤦

ywangd added a commit to ywangd/elasticsearch that referenced this pull request Mar 23, 2025
ywangd added a commit that referenced this pull request Mar 24, 2025
omricohenn pushed a commit to omricohenn/elasticsearch that referenced this pull request Mar 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-backport Automatically create backport pull requests when merged backport pending >non-issue :Security/Security Security issues without another label Team:Security Meta label for security team v8.19.0 v9.1.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants