Skip to content

Commit 80f8d20

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 abbbdbc commit 80f8d20

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
@@ -182,7 +182,7 @@ public EXistResourceSet query(final XMLResource res, final String query, final S
182182
}
183183

184184
private EXistResourceSet doQuery(final DBBroker broker, final Txn transaction, final String query, final XmldbURI[] docs, final Sequence contextSet, final String sortExpr) throws XMLDBException {
185-
final Either<XPathException, CompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
185+
final Either<XPathException, LocalCompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
186186
if(maybeExpr.isLeft()) {
187187
final XPathException e = maybeExpr.left().get();
188188
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
@@ -269,6 +269,9 @@ public EXistResourceSet execute(final Source source) throws XMLDBException {
269269
@Override
270270
public EXistResourceSet executeStoredQuery(final String uri) throws XMLDBException {
271271
return withDb((broker, transaction) -> {
272+
273+
final Either<XPathException, LocalCompiledExpression> maybeExpr;
274+
272275
try (final LockedDocument lockedResource = broker.getXMLResource(new XmldbURI(uri), LockMode.READ_LOCK)) {
273276
if (lockedResource == null) {
274277
throw new XMLDBException(ErrorCodes.INVALID_URI, "No stored XQuery exists at: " + uri);
@@ -279,8 +282,15 @@ public EXistResourceSet executeStoredQuery(final String uri) throws XMLDBExcepti
279282
}
280283

281284
final Source dbSource = new DBSource(broker.getBrokerPool(), (BinaryDocument) resource, false);
285+
maybeExpr = compileAndCheck(broker, transaction, dbSource);
286+
}
282287

283-
return execute(broker, transaction, dbSource);
288+
// NOTE(AR) query is now compiled so we can release the LockedDocument eagerly above
289+
if (maybeExpr.isLeft()) {
290+
final XPathException e = maybeExpr.left().get();
291+
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
292+
} else {
293+
return execute(broker, transaction, null, null, maybeExpr.right().get(), null);
284294
}
285295
});
286296
}
@@ -341,7 +351,7 @@ private EXistResourceSet execute(final DBBroker broker, final Txn transaction, f
341351
@Override
342352
public CompiledExpression compile(final String query) throws XMLDBException {
343353
return withDb((broker, transaction) -> {
344-
final Either<XPathException, CompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
354+
final Either<XPathException, LocalCompiledExpression> maybeExpr = compileAndCheck(broker, transaction, query);
345355
if(maybeExpr.isLeft()) {
346356
final XPathException e = maybeExpr.left().get();
347357
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
@@ -353,18 +363,20 @@ public CompiledExpression compile(final String query) throws XMLDBException {
353363

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

364-
private Either<XPathException, CompiledExpression> compileAndCheck(final DBBroker broker, final Txn transaction, final String query) throws XMLDBException {
365-
374+
private Either<XPathException, LocalCompiledExpression> compileAndCheck(final DBBroker broker, final Txn transaction, final String query) throws XMLDBException {
366375
final Source source = new StringSource(query);
376+
return compileAndCheck(broker, transaction, source);
377+
}
367378

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

370382
try {
@@ -373,7 +385,7 @@ private Either<XPathException, CompiledExpression> compileAndCheck(final DBBroke
373385
LOG.debug("Compilation took {}ms", compilationResult.compilationTime);
374386
}
375387

376-
final CompiledExpression compiledExpression = new LocalCompiledExpression(compilationResult);
388+
final LocalCompiledExpression compiledExpression = new LocalCompiledExpression(compilationResult);
377389
return Either.Right(compiledExpression);
378390
} catch (final PermissionDeniedException e) {
379391
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}.
@@ -2159,22 +2161,31 @@ public Map<String, Object> executeT(final String pathToQuery, final Map<String,
21592161

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

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

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

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

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

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

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

7978
/**
@@ -264,36 +263,47 @@ public void readRunningXQuerySourceDocument() throws InterruptedException, Execu
264263

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

277-
final String replacementQuery = "<replaced/>";
277+
try {
278+
final String replacementQuery = "<replaced/>";
278279

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

286-
// 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
287+
return true;
287288

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

291297
// Run the replaceDocumentCallable from a separate thread so that we don't block our test
292298
final Future<Boolean> replacedDocumentFuture = executorService.submit(replaceDocumentCallable);
293-
assertThrows(
294-
"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",
295-
TimeoutException.class, () ->
296-
replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
297-
);
299+
final Boolean replaceDocumentStatus = replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
300+
assertTrue(replaceDocumentStatus);
301+
302+
// Check the results of the query
303+
// Check the results of the query
304+
final String queryResult = queryResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
305+
assertNotNull(queryResult);
306+
assertTrue(queryResult.startsWith("<elapsed-time exit-latch-zero=\"true\">PT"));
307+
assertTrue(queryResult.endsWith("</elapsed-time>"));
298308
}
299309
}

0 commit comments

Comments
 (0)