Skip to content

Commit 06c5608

Browse files
committed
[bugfix] When executing an XQuery that is stored in the database via the XML:DB and RPC APIs only hold a lock on the source document whilst the query is compiled, then release it before executing the query
Closes eXist-db/exist#4022
1 parent 2413fba commit 06c5608

3 files changed

Lines changed: 71 additions & 38 deletions

File tree

exist-core/src/main/java/org/exist/xmldb/LocalXPathQueryService.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public EXistResourceSet query(final XMLResource res, final String query, final S
185185
}
186186

187187
private EXistResourceSet doQuery(final DBBroker broker, final Txn transaction, final String query, final XmldbURI[] docs, final Sequence contextSet, final String sortExpr) throws XMLDBException {
188-
final Either<XPathException, CompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
188+
final Either<XPathException, LocalCompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
189189
if(maybeExpr.isLeft()) {
190190
final XPathException e = maybeExpr.left().get();
191191
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
@@ -272,6 +272,9 @@ public EXistResourceSet execute(final Source source) throws XMLDBException {
272272
@Override
273273
public EXistResourceSet executeStoredQuery(final String uri) throws XMLDBException {
274274
return withDb((broker, transaction) -> {
275+
276+
final Either<XPathException, LocalCompiledExpression> maybeExpr;
277+
275278
try (final LockedDocument lockedResource = broker.getXMLResource(new XmldbURI(uri), LockMode.READ_LOCK)) {
276279
if (lockedResource == null) {
277280
throw new XMLDBException(ErrorCodes.INVALID_URI, "No stored XQuery exists at: " + uri);
@@ -282,8 +285,15 @@ public EXistResourceSet executeStoredQuery(final String uri) throws XMLDBExcepti
282285
}
283286

284287
final Source dbSource = new DBSource(broker.getBrokerPool(), (BinaryDocument) resource, false);
288+
maybeExpr = compileAndCheck(broker, transaction, dbSource);
289+
}
285290

286-
return execute(broker, transaction, dbSource);
291+
// NOTE(AR) query is now compiled so we can release the LockedDocument eagerly above
292+
if (maybeExpr.isLeft()) {
293+
final XPathException e = maybeExpr.left().get();
294+
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
295+
} else {
296+
return execute(broker, transaction, null, null, maybeExpr.right().get(), null);
287297
}
288298
});
289299
}
@@ -344,7 +354,7 @@ private EXistResourceSet execute(final DBBroker broker, final Txn transaction, f
344354
@Override
345355
public CompiledExpression compile(final String query) throws XMLDBException {
346356
return withDb((broker, transaction) -> {
347-
final Either<XPathException, CompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
357+
final Either<XPathException, LocalCompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
348358
if(maybeExpr.isLeft()) {
349359
final XPathException e = maybeExpr.left().get();
350360
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
@@ -356,18 +366,20 @@ public CompiledExpression compile(final String query) throws XMLDBException {
356366

357367
@Override
358368
public CompiledExpression compileAndCheck(final String query) throws XMLDBException, XPathException {
359-
final Either<XPathException, CompiledExpression> result = withDb((broker, transaction) -> compileAndCheck(broker, transaction, query));
369+
final Either<XPathException, LocalCompiledExpression> result = withDb((broker, transaction) -> compileAndCheck(broker, transaction, query));
360370
if(result.isLeft()) {
361371
throw result.left().get();
362372
} else {
363373
return result.right().get();
364374
}
365375
}
366376

367-
private Either<XPathException, CompiledExpression> compileAndCheck(final DBBroker broker, final Txn transaction, final String query) throws XMLDBException {
368-
377+
private Either<XPathException, LocalCompiledExpression> compileAndCheck(final DBBroker broker, final Txn transaction, final String query) throws XMLDBException {
369378
final Source source = new StringSource(query);
379+
return compileAndCheck(broker, transaction, source);
380+
}
370381

382+
private Either<XPathException, LocalCompiledExpression> compileAndCheck(final DBBroker broker, final Txn transaction, final Source source) throws XMLDBException {
371383
final ConsumerE<XQueryContext, XPathException> preCompilationContext = xqueryContext -> setupContext(source, xqueryContext);
372384

373385
try {
@@ -376,7 +388,7 @@ private Either<XPathException, CompiledExpression> compileAndCheck(final DBBroke
376388
LOG.debug("Compilation took {}ms", compilationResult.compilationTime);
377389
}
378390

379-
final CompiledExpression compiledExpression = new LocalCompiledExpression(compilationResult);
391+
final LocalCompiledExpression compiledExpression = new LocalCompiledExpression(compilationResult);
380392
return Either.Right(compiledExpression);
381393
} catch (final PermissionDeniedException e) {
382394
throw new XMLDBException(ErrorCodes.PERMISSION_DENIED, e.getMessage(), e);

exist-core/src/main/java/org/exist/xmlrpc/RpcConnection.java

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@
151151
import static org.exist.xmldb.EXistXPathQueryService.BEGIN_PROTECTED_MAX_LOCKING_RETRIES;
152152
import static java.nio.file.StandardOpenOption.*;
153153

154+
// TODO(AR) this class leaks resources from the XQueryPool and XQueryContext not being cleaned up correctly, switch to XQueryUtil for compilation/execution
155+
154156
/**
155157
* This class implements the actual methods defined by
156158
* {@link org.exist.xmlrpc.RpcAPI}.
@@ -2157,22 +2159,31 @@ public Map<String, Object> executeT(final String pathToQuery, final Map<String,
21572159

21582160
final Optional<String> sortBy = Optional.ofNullable(parameters.get(RpcAPI.SORT_EXPR)).map(Object::toString);
21592161

2160-
return this.<Map<String, Object>>readDocument(XmldbURI.createInternal(pathToQuery)).apply((document, broker, transaction) -> {
2161-
final BinaryDocument xquery = (BinaryDocument) document;
2162-
if (xquery.getResourceType() != DocumentImpl.BINARY_FILE) {
2163-
throw new EXistException("Document " + pathToQuery + " is not a binary resource");
2164-
}
2162+
return withDb((broker, transaction) -> {
21652163

2166-
if (!xquery.getPermissions().validate(user, Permission.READ | Permission.EXECUTE)) {
2167-
throw new PermissionDeniedException("Insufficient privileges to access resource");
2168-
}
2164+
final CompiledXQuery compiledXquery = this.<CompiledXQuery>readDocument(broker, transaction, XmldbURI.createInternal(pathToQuery)).apply((document, broker1, transaction1) -> {
2165+
if (document.getResourceType() != DocumentImpl.BINARY_FILE) {
2166+
throw new EXistException("Document " + pathToQuery + " is not a binary resource");
2167+
}
21692168

2170-
final Source source = new DBSource(broker.getBrokerPool(), xquery, true);
2169+
if (!document.getPermissions().validate(user, Permission.READ | Permission.EXECUTE)) {
2170+
throw new PermissionDeniedException("Insufficient privileges to access resource");
2171+
}
21712172

2173+
final Source source = new DBSource(broker1.getBrokerPool(), (BinaryDocument) document, true);
2174+
try {
2175+
return this.<CompiledXQuery>compileQuery(broker1, transaction1, source, parameters).apply(cx -> (CompiledXQuery) cx);
2176+
} catch (final XPathException e) {
2177+
throw new EXistException(e);
2178+
}
2179+
});
2180+
2181+
// NOTE(AR) query is now compiled so we can release the readDocument lock eagerly above
21722182
try {
2173-
final Map<String, Object> rpcResponse = this.<Map<String, Object>>compileQuery(broker, transaction, source, parameters)
2174-
.apply(compiledQuery -> queryResultToTypedRpcResponse(startTime, getXdmSerializationOptions(compiledQuery.getContext()), doQuery(broker, compiledQuery, null, parameters), sortBy));
2175-
return rpcResponse;
2183+
final QueryResult queryResult = doQuery(broker, compiledXquery, null, parameters);
2184+
2185+
final Properties xdmSerializationOptions = getXdmSerializationOptions(compiledXquery.getContext());
2186+
return queryResultToTypedRpcResponse(startTime, xdmSerializationOptions, queryResult, sortBy);
21762187
} catch (final XPathException e) {
21772188
throw new EXistException(e);
21782189
}

exist-core/src/test/java/org/exist/xquery/RunningXQueryXmlDbTest.java

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
import static org.exist.util.PropertiesBuilder.propertiesBuilder;
7575
import static org.junit.Assert.assertEquals;
7676
import static org.junit.Assert.assertNotNull;
77-
import static org.junit.Assert.assertThrows;
7877
import static org.junit.Assert.assertTrue;
7978

8079
/**
@@ -265,36 +264,47 @@ public void readRunningXQuerySourceDocument() throws InterruptedException, Execu
265264

266265
/**
267266
* When this test is run, the /db/running-xquery-test/latched-query.xq XQuery
268-
* is already executing (see {@link #runXQuery()}) in a separate thread that holds a READ_LOCK on its Source document.
267+
* is already executing (see {@link #runXQuery()}) in a separate thread,
268+
* that should have released the READ_LOCK on its Source document after query compilation.
269269
*
270-
* We try and replace the XQuery source document under those conditions, which should not be possible
271-
* as writing to the document would require a WRITE_LOCK, but a READ_LOCK is already held.
270+
* We try and replace the XQuery source document under those conditions, which should be possible
271+
* as no lock is held on the source document after compilation.
272272
*/
273273
@Test
274-
public void replaceRunningXQuerySourceDocument() {
274+
public void replaceRunningXQuerySourceDocument() throws ExecutionException, InterruptedException, TimeoutException {
275275
// Prepare a Callable that will try and replace the XQuery's source document
276276
final Callable<Boolean> replaceDocumentCallable = () -> {
277277

278-
final String replacementQuery = "<replaced/>";
278+
try {
279+
final String replacementQuery = "<replaced/>";
279280

280-
try (final Collection testCollection = DatabaseManager.getCollection(getBaseUri() + TEST_COLLECTION_URI.getCollectionPath(), TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD);
281-
final Resource queryResource = testCollection.getResource(LATCHED_QUERY_URI.getCollectionPath())) {
282-
((EXistResource) queryResource).setMediaType(MediaType.APPLICATION_XQUERY);
283-
queryResource.setContent(replacementQuery.getBytes(UTF_8));
284-
testCollection.storeResource(queryResource);
285-
}
281+
try (final Collection testCollection = DatabaseManager.getCollection(getBaseUri() + TEST_COLLECTION_URI.getCollectionPath(), TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD);
282+
final Resource queryResource = testCollection.getResource(LATCHED_QUERY_URI.getCollectionPath())) {
283+
((EXistResource) queryResource).setMediaType(MediaType.APPLICATION_XQUERY);
284+
queryResource.setContent(replacementQuery.getBytes(UTF_8));
285+
testCollection.storeResource(queryResource);
286+
}
286287

287-
// NOTE(AR) It should not have been possible to get to this point as we should not be able to acquire a WRITE_LOCK lock on the document (above) when calling broker.storeDocument, as the query thread holds a READ_LOCK on the document
288+
return true;
288289

289-
return true;
290+
} finally {
291+
// Instruct the running XQuery to finish
292+
@Nullable final CountDownLatch exitLatch = LatchWrappersSingleton.INSTANCE.queryExitLatchRef.get();
293+
assertNotNull("exitLatch should not be null at this point", exitLatch);
294+
exitLatch.countDown();
295+
}
290296
};
291297

292298
// Run the replaceDocumentCallable from a separate thread so that we don't block our test
293299
final Future<Boolean> replacedDocumentFuture = executorService.submit(replaceDocumentCallable);
294-
assertThrows(
295-
"We should not have been able to replace the document as that requires this thread to obtain a WRITE_LOCK on the document, but the query thread already holds a READ_LOCK on the document",
296-
TimeoutException.class, () ->
297-
replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
298-
);
300+
final Boolean replaceDocumentStatus = replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
301+
assertTrue(replaceDocumentStatus);
302+
303+
// Check the results of the query
304+
// Check the results of the query
305+
final String queryResult = queryResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
306+
assertNotNull(queryResult);
307+
assertTrue(queryResult.startsWith("<elapsed-time exit-latch-zero=\"true\">PT"));
308+
assertTrue(queryResult.endsWith("</elapsed-time>"));
299309
}
300310
}

0 commit comments

Comments
 (0)