GH-3837: Stop SERVICE materialization when query cancellation is observed - #4223
goutamadwant wants to merge 2 commits into
Conversation
Check the outer cancellation signal before execution and during result materialization. Avoid the extra response read on aborted close while preserving detached results and SERVICE SILENT behavior. Add cancellation and compatibility regressions. This partial improvement does not interrupt an already-blocked network read or resolve the entire reported Fuseki stall.
rvesse
left a comment
There was a problem hiding this comment.
Thanks for the contribution, this does look to improve the situation for this bug
Couple of comments about potentially simplifying the logic
| for (;;) { | ||
| checkCancelled(cancelSignal, qExec); | ||
| boolean hasNext = rowSet.hasNext(); | ||
| checkCancelled(cancelSignal, qExec); |
There was a problem hiding this comment.
I am always a little wary of doing checks like this twice inside a loop although I can understand the reasoning (checking both before and after the network read which could block), might it make more sense to make this a while loop conditional on the cancel signal? Only other change that would necessitate would be a final cancellation check post-loop as hasNext could be false leading to break out of the loop before the next cancellation check
For the case when no cancel signal is available it might also be simpler to preserve existing code path from deleted lines then the actual cancellation check doesn't need to make a null check as well
There was a problem hiding this comment.
Thanks @rvesse. restoredc the original materialization path when no cancel signal is available and moved the cancellation guards into a while condition. The final check also catches cancellation when hasNext() returns false. Kept the guards around the potentially blocking read and added coverage for cancellation at the end of results and successful results with and without a signal. let me know if this looks good
Preserve the original materialization path when no cancellation signal is available. Use a guarded while loop otherwise, with a final check that also handles cancellation when the result iterator is exhausted. Cover cancellation at the end of results and successful detachment with and without a signal. Use an import in the execution test suite. The existing limitation for already-blocked network reads is unchanged. Validation: full ARQ reactor and scoped HTTP/Fuseki integration tests pass. UI tooling/tests, Javadocs and RAT were skipped in the integration build.
|
Hi, My feeling is that it would be better to defer materialization into its own iterator. As a sketch: // Service.java
QueryExec qExec = ...; // remove try-with-resources block
// Build and access the rowSet here because
// some tests expect Service.exec to raise a QueryExceptionHTTP immediately.
// If the tests could be relaxed then this preflight check could be removed.
RowSet rowSet;
try {
rowSet = qExec.select();
rowSet.hasNext(); // Will "hang" waiting for connection and the first result.
} catch (Throwable t) {
try {
if (t instanceof HttpException ex) {
throw QueryExceptionHTTP.rewrap(ex);
}
throw t;
} finally {
qExec.close();
}
}
QueryIterator qIter = new QueryIterMaterializeQueryExec(execCxt, qExec, rowSet);
if (requiresRemapping)
qIter = QueryIter.map(qIter, varMapping);// QueryIterMaterializeQueryExec.java
/** QueryIter backed by a QueryExec. The RowSet is materialized on the first call to next(). */
public class QueryIterMaterializeQueryExec
extends QueryIter
{
protected final QueryExec queryExec;
protected boolean queryExecClosed = false;
protected RowSet baseRowSet;
protected RowSet materializeRowSet;
/**
* Constructor.
*
* @param execCxt ExecutionContext. May be null, however cancellation signals are passed via contexts.
* @param queryExec QueryIter. The backing query execution. Must not be null.
* @param baseRowSet The backing row set. Must not be null.
*/
public QueryIterMaterializeQueryExec(ExecutionContext execCxt, QueryExec queryExec, RowSet baseRowSet) {
super(execCxt);
this.queryExec = Objects.requireNonNull(queryExec);
this.baseRowSet = Objects.requireNonNull(baseRowSet);
}
@Override
protected boolean hasNextBinding() {
try {
if (materializeRowSet == null) {
return baseRowSet.hasNext();
}
return materializeRowSet.hasNext();
} catch (HttpException ex) {
throw QueryExceptionHTTP.rewrap(ex);
}
}
@Override
protected Binding moveToNextBinding() {
try {
if (materializeRowSet == null) {
materializeRowSet = baseRowSet.materialize();
queryExecClosed = true;
queryExec.close(); // Close immediately to possibly return a HTTP connection to its pool.
}
return materializeRowSet.next();
} catch (HttpException ex) {
throw QueryExceptionHTTP.rewrap(ex);
}
}
@Override
protected void closeIterator() {
if (!queryExecClosed) {
queryExec.close();
queryExecClosed = true; // Actually not needed because base class prevents calling closeIterator twice.
}
}
@Override
protected void requestCancel() {
queryExec.abort();
}
} |
Refs #3837
Pull request Description:
Check the outer query's cancellation signal before executing a SERVICE request and while materializing its results. When cancellation is observed, abort the HTTP query and close its response without starting another potentially blocking read.
Successful responses remain fully materialized, preserving SERVICE SILENT error handling and variable remapping. The existing HTTP timeout configuration is unchanged.
Tests cover cancellation before a request, during result consumption, after a blocked read returns, and at the end of results; repeated abort/close; detached results with and without a cancellation signal; SILENT behavior; variable remapping; and timeout propagation.
Validation: all eleven cancellation regression cases, the full ARQ reactor, 97 HTTP integration tests, and 12 Fuseki-main service access tests passed. The integration build skipped UI tooling/tests, Javadocs, and RAT; this is not a full-project verification claim.
This does not interrupt a network read already in progress or address all the Fuseki thread and connection exhaustion factors discussed in #3837. A local execution of the reported workload still stalled after client timeouts. This is a partial cancellation improvement, not a fix for the entire incident, and is not intended to close the issue.
By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the Contributor's Agreement.
See the Apache Jena "Contributing" guide.