Skip to content

Expand the lifecycle of the AggregationContext#94023

Merged
not-napoleon merged 20 commits intoelastic:mainfrom
not-napoleon:preallocated-breaker-lifetime-v2
Apr 20, 2023
Merged

Expand the lifecycle of the AggregationContext#94023
not-napoleon merged 20 commits intoelastic:mainfrom
not-napoleon:preallocated-breaker-lifetime-v2

Conversation

@not-napoleon
Copy link
Copy Markdown
Member

This PR replaces and supersedes #93594

Relates to #89437

One big lesson we learned from the first prototype of a dense memory aggregations format, in order for aggregation BigArrays to live into the serialization phase on the data nodes, we needed to not release the circuit breaker at the end of collection. The AggregationContext currently manages all of aggregations memory. Rather than change that, this PR extends the life cycle of the AggregationContext so it isn't closed until QuerySearchResult is closed, at which point we have serialized the aggregation information back to the coordinating node.

Importantly, this PR enables ref counting on QuerySearchResult, in what I think is a straightforward way. My earlier attempt, linked above, delegated that ref counting to a wrapped releasable, but ran into problems because the QuerySearchResult gets created before the things it needs to release. This iteration delegates directly to a ref counter, and maintains a list of items to release, which currently is just the aggregation context. That list can be appended to after creation, thus resolving the timing issue.

@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Feb 22, 2023
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytics-geo (Team:Analytics)

1 similar comment
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytics-geo (Team:Analytics)

readerContext.setRescoreDocIds(rescoreDocIds);
// Since we are returning the raw QuerySearchResult here, there's no opportunity for the object that will be holding that
// reference to incRef it before our try block closes and decRef's it when it closes the search context
context.queryResult().incRef();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm a little uncomfortable with this, as it makes a lot of assumptions about our caller. But I don't see a way around that, since we'll decRef when the try-with-resources closes, and if that drops us to 0 references (which it should), we'll release the agg context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's common, and totally reasonable to assume (require?) that the caller will take ownership of the returned value, and release it when no longer needed. I think it's not clear from the code that this is required, so it might be worth documenting it in this method's JavaDocs. I would recommend against the inline comments here however, stuff like this seems useful when you write it but tends to just become clutter over time.

I think the caller does behave as expected in this case, right? It looks like we pass it (pretty) directly to a ChannelActionListener which does decRef() once it no longer needs it. (aside: we really should document this)

There's definitely a risk of leaks with this kind of API tho, so it's important that a leak in this area would be caught by tests. Is that the case here? For instance, if it's using potentially-recycled pages then we should be using a CountingPageCacheRecycler (preferred) or MockPageCacheRecycler (relies on GC).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, the caller does the right thing and the tests do catch it if we don't. Honestly, the testing around this has been very good so far.

And you're right, I should just write javadoc for these methods. I hadn't because I've really only been focusing on this one aspect, which is not the main thrust of what's going on here at all, but they currently have none at all, so I'm sure I can do better than that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even thought this method is private, it makes sense to document the responsibility of the caller at the method level.

readerContext.setRescoreDocIds(rescoreDocIds);
// Since we are returning the raw QuerySearchResult here, there's no opportunity for the object that will be holding that
// reference to incRef it before our try block closes and decRef's it when it closes the search context
searchContext.queryResult().incRef();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As noted above, this is uncomfortable but I don't have a better solution.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ScrollQueryFetchSearcResult is a thin wrapper over QueryFetchSearchResult, so it seems reasonable to delegate ref counting to the wrapped reference.

@not-napoleon
Copy link
Copy Markdown
Member Author

There's a lot of places where things reach through SearchContext to modify QuerySearchResult, but don't actually acquire a reference to it. I'd like to refactor some of that to be more encapsulated, maybe in a follow up PR.

There's also the rather terrifying QuerySearchResult#readFromWithId method which essentially overwrites the QuerySearchResult in place. I'm not sure what to do about that.

Finally, QuerySearchResult has about five different constructors, including a null case, and exists in somewhat different states on the data nodes and the coordinating node. This work really only focuses on the data node side (because the AggregationContext that I'm trying to manage is data node only). I'm somewhat purposefully ignoring the coordinating node side of this, because I don't really know what changes I will need to make there yet.

@not-napoleon not-napoleon requested a review from jdconrad March 24, 2023 15:28
@not-napoleon not-napoleon requested a review from nik9000 March 24, 2023 15:28
@henningandersen henningandersen self-requested a review March 24, 2023 16:10
@not-napoleon
Copy link
Copy Markdown
Member Author

@elasticmachine run elasticsearch-ci/docs

@DaveCTurner
Copy link
Copy Markdown
Member

DaveCTurner commented Mar 27, 2023

There's also the rather terrifying QuerySearchResult#readFromWithId method which essentially overwrites the QuerySearchResult in place. I'm not sure what to do about that.

I think just before overwriting each releasable/refcounted field it should close/decRef the current value. Unless there's some (non-obvious-to-me) invariant which means that we're never meaningfully overwriting anything. But in that case we should be able to add assertions to that effect at least.

Copy link
Copy Markdown
Member

@martijnvg martijnvg left a comment

Choose a reason for hiding this comment

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

I left a few small questions/comments. In general this looks good to me.


private final RefCounted refCounted;

private List<Releasable> toRelease;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think toRelease can be final?

}

public void addReleasable(Releasable releasable) {
toRelease.add(releasable);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we sure toRelease is always assigned when this method is invoked?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Currently? I'm pretty sure, but it's tricky. Right now, this class has basically three "modes" - data node, coordinating node, and null. The data node constructor path correctly initializes toRelease, but neither of the other to do. Coordinating mode could initialize it, and the associated ref counting, but we don't do anything with it there yet. The null case probably shouldn't? I'm not entirely clear on why we need that path, but it's definitely used. Seemed safer to throw (NPE) if we tried to add a releasable in a path we weren't expecting it, but I'm open to discussion if you'd rather do something else.

messageFields.put("elasticsearch.slowlog.took_millis", TimeUnit.NANOSECONDS.toMillis(tookInNanos));
if (context.queryResult().getTotalHits() != null) {
messageFields.put("elasticsearch.slowlog.total_hits", context.queryResult().getTotalHits());
if (context.getTotalHits() != null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just double checking, but the change to this file doesn't seem to be related with the core of this PR?

}
}

public void releaseAggregationContext() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this method can have a private visibility? Since it is only called from line 104?

readerContext.setRescoreDocIds(rescoreDocIds);
// Since we are returning the raw QuerySearchResult here, there's no opportunity for the object that will be holding that
// reference to incRef it before our try block closes and decRef's it when it closes the search context
context.queryResult().incRef();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even thought this method is private, it makes sense to document the responsibility of the caller at the method level.

@not-napoleon
Copy link
Copy Markdown
Member Author

@elasticmachine update branch

@not-napoleon
Copy link
Copy Markdown
Member Author

That test failure reproduced in main. I've opened #95386 to investigate, but I don't think it needs to block this PR. I'm going to re-run it to get a different random seed.

@not-napoleon
Copy link
Copy Markdown
Member Author

@elasticmachine run elasticsearch-ci/part-1

@not-napoleon not-napoleon merged commit cb04885 into elastic:main Apr 20, 2023
@not-napoleon not-napoleon deleted the preallocated-breaker-lifetime-v2 branch April 20, 2023 13:23
hendrikmuhs pushed a commit that referenced this pull request May 5, 2023
…95860)

Refactorings done in #94023. improved memory management but also made it possible to run into a race condition in DelegatingCircuitBreaker. This change makes DelegatingCircuitBreaker thread-safe.

This change is a quick-fix with low risk. Eventually DelegatingCircuitBreaker should be removed. Work on that is underway: #89437. Under that circumstances the change is reasonable to me. The added locking overhead shouldn't be a problem, re-allocations happen rarely and there is just the disconnect case where 2 threads potentially access the structure at the same time.

fixes #95681
hendrikmuhs pushed a commit to hendrikmuhs/elasticsearch that referenced this pull request May 5, 2023
…lastic#95860)

Refactorings done in elastic#94023. improved memory management but also made it possible to run into a race condition in DelegatingCircuitBreaker. This change makes DelegatingCircuitBreaker thread-safe.

This change is a quick-fix with low risk. Eventually DelegatingCircuitBreaker should be removed. Work on that is underway: elastic#89437. Under that circumstances the change is reasonable to me. The added locking overhead shouldn't be a problem, re-allocations happen rarely and there is just the disconnect case where 2 threads potentially access the structure at the same time.

fixes elastic#95681
elasticsearchmachine pushed a commit that referenced this pull request May 5, 2023
…95860) (#95879)

Refactorings done in #94023. improved memory management but also made it possible to run into a race condition in DelegatingCircuitBreaker. This change makes DelegatingCircuitBreaker thread-safe.

This change is a quick-fix with low risk. Eventually DelegatingCircuitBreaker should be removed. Work on that is underway: #89437. Under that circumstances the change is reasonable to me. The added locking overhead shouldn't be a problem, re-allocations happen rarely and there is just the disconnect case where 2 threads potentially access the structure at the same time.

fixes #95681
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/Aggregations Aggregations >refactoring Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.8.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants