Skip to content

Commit 1f3c011

Browse files
committed
[bugfix] When executing an XQuery that is stored in the database via the REST API only hold a lock on the source document whilst the query is compiled, then release it before executing the query
Closes eXist-db/exist#5916 Closes eXist-db/exist#4867 Closes eXist-db/exist#4022
1 parent 52faf52 commit 1f3c011

2 files changed

Lines changed: 78 additions & 43 deletions

File tree

exist-core/src/main/java/org/exist/http/RESTServer.java

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.exist.security.Subject;
7171
import org.exist.security.internal.RealmImpl;
7272
import org.exist.source.DBSource;
73+
import org.exist.source.DbStoreSource;
7374
import org.exist.source.Source;
7475
import org.exist.source.StringSource;
7576
import org.exist.source.URLSource;
@@ -578,7 +579,7 @@ public void doGet(final DBBroker broker, final Txn transaction, final HttpServle
578579
try {
579580
if (xquery_mime_type.equals(resource.getMediaType())) {
580581
// Execute the XQuery
581-
executeXQuery(broker, transaction, resource, request, response,
582+
executeXQuery(broker, transaction, lockedDocument, request, response,
582583
outputProperties, servletPath.toString(), pathInfo);
583584
} else if (xproc_mime_type.equals(resource.getMediaType())) {
584585
// Execute the XProc
@@ -745,7 +746,7 @@ public void doPost(final DBBroker broker, final Txn transaction, final HttpServl
745746
try {
746747
if (xquery_mime_type.equals(resource.getMediaType())) {
747748
// Execute the XQuery
748-
executeXQuery(broker, transaction, resource, request, response,
749+
executeXQuery(broker, transaction, lockedDocument, request, response,
749750
outputProperties, servletPath.toString(), pathInfo);
750751
} else {
751752
// Execute the XProc
@@ -1267,7 +1268,7 @@ private boolean checkForXQueryTarget(final DBBroker broker, final Txn transactio
12671268
final Properties outputProperties = new Properties(defaultOutputKeysProperties);
12681269
try {
12691270
// Execute the XQuery
1270-
executeXQuery(broker, transaction, resource, request, response,
1271+
executeXQuery(broker, transaction, lockedDocument, request, response,
12711272
outputProperties, servletPath.toString(), pathInfo);
12721273
} catch (final XPathException e) {
12731274
writeXPathExceptionHtml(response, HttpServletResponse.SC_BAD_REQUEST, UTF_8.name(), null, path.toString(), e);
@@ -1575,41 +1576,64 @@ private void declareExternalAndXQJVariables(final XQueryContext context,
15751576
/**
15761577
* Directly execute an XQuery stored as a binary document in the database.
15771578
*
1578-
* @throws PermissionDeniedException
1579+
* NOTE The document will be unlocked after query compilation (only if compilation succeeds).
1580+
*
1581+
* @param broker the database broker.
1582+
* @param transaction the database transaction.
1583+
* @param lockedQueryDocument the locked document holding the query to be executed.
1584+
* @param request the HTTP request.
1585+
* @param response the HTTP response.
1586+
* @param outputProperties any serialization properties.
1587+
* @param servletPath the path to the servlet.
1588+
* @param pathInfo the path.
1589+
*
1590+
* @throws XPathException if an error occurs during query compilation or execution.
1591+
* @throws BadRequestException if the request had a problem.
1592+
* @throws PermissionDeniedException if the calling user does not have sufficient permissions.
15791593
*/
1580-
private void executeXQuery(final DBBroker broker, final Txn transaction, final DocumentImpl resource,
1594+
private void executeXQuery(final DBBroker broker, final Txn transaction, final LockedDocument lockedQueryDocument,
15811595
final HttpServletRequest request, final HttpServletResponse response,
15821596
final Properties outputProperties, final String servletPath, final String pathInfo)
15831597
throws XPathException, BadRequestException, PermissionDeniedException {
15841598

1585-
final Source source = new DBSource(broker.getBrokerPool(), (BinaryDocument) resource, true);
1599+
final DocumentImpl resource = lockedQueryDocument.getDocument();
1600+
final DbStoreSource source = new DBSource(broker.getBrokerPool(), (BinaryDocument) resource, true);
15861601

15871602
final ConsumerE<XQueryContext, XPathException> setupXqueryContextPreCompilation = xqueryContext -> {
15881603
xqueryContext.setModuleLoadPath(XmldbURI.EMBEDDED_SERVER_URI.append(resource.getCollection().getURI()).toString());
15891604
xqueryContext.setStaticallyKnownDocuments(new XmldbURI[]{ resource.getCollection().getURI() });
15901605
};
15911606

1592-
final ConsumerE<XQueryContext, XPathException> setupXqueryContextPreExecution = xqueryContext -> {
1593-
final HttpRequestWrapper reqw = declareVariables(xqueryContext, null, request, response);
1594-
reqw.setServletPath(servletPath);
1595-
reqw.setPathInfo(pathInfo);
1596-
DebuggeeFactory.checkForDebugRequest(request, xqueryContext);
1597-
};
1607+
try {
1608+
// NOTE(AR) compilationResult will be cleaned up by the try-with-resources wrapped XQueryUtil.execute(...); at present between here and there no exceptions can occur
1609+
final XQueryUtil.CompilationResult compilationResult = XQueryUtil.compile(broker, source, false, true, setupXqueryContextPreCompilation);
15981610

1599-
final BiConsumerE<XQueryContext, XQueryUtil.QueryResult, XPathException> setupXqueryContextPostExecution = (xqueryContext, queryResult) -> {
1600-
// Pass last modified date to the HTTP response
1601-
HTTPUtils.addLastModifiedHeader(queryResult.result, xqueryContext);
1602-
};
1611+
// NOTE(AR) query is now compiled so we can release the LockedDocument eagerly
1612+
lockedQueryDocument.close();
16031613

1604-
try (final XQueryUtil.QueryResult queryResult = XQueryUtil.query(broker, source, true, null, outputProperties, setupXqueryContextPreCompilation, setupXqueryContextPreExecution, setupXqueryContextPostExecution)) {
16051614

1606-
// Special header to indicate whether the compiled query is returned from the cache
1607-
response.setHeader(XQUERY_CACHED_RESPONSE_HEADER, queryResult.compilationTime == XQueryUtil.CompilationResult.RETRIEVED_CACHED_COMPILED_QUERY ? "true" : "false");
1615+
final ConsumerE<XQueryContext, XPathException> setupXqueryContextPreExecution = xqueryContext -> {
1616+
final HttpRequestWrapper reqw = declareVariables(xqueryContext, null, request, response);
1617+
reqw.setServletPath(servletPath);
1618+
reqw.setPathInfo(pathInfo);
1619+
DebuggeeFactory.checkForDebugRequest(request, xqueryContext);
1620+
};
1621+
1622+
final BiConsumerE<XQueryContext, XQueryUtil.QueryResult, XPathException> setupXqueryContextPostExecution = (xqueryContext, queryResult) -> {
1623+
// Pass last modified date to the HTTP response
1624+
HTTPUtils.addLastModifiedHeader(queryResult.result, xqueryContext);
1625+
};
1626+
1627+
try (final XQueryUtil.QueryResult queryResult = XQueryUtil.execute(broker, compilationResult, null, outputProperties, setupXqueryContextPreExecution, setupXqueryContextPostExecution)) {
1628+
// Special header to indicate whether the compiled query is returned from the cache
1629+
response.setHeader(XQUERY_CACHED_RESPONSE_HEADER, queryResult.compilationTime == XQueryUtil.CompilationResult.RETRIEVED_CACHED_COMPILED_QUERY ? "true" : "false");
1630+
1631+
final boolean wrap = "yes".equals(outputProperties.getProperty("_wrap"));
1632+
writeResults(response, broker, transaction, queryResult, -1, 1, false, outputProperties, wrap);
1633+
}
16081634

1609-
final boolean wrap = "yes".equals(outputProperties.getProperty("_wrap"));
1610-
writeResults(response, broker, transaction, queryResult, -1, 1, false, outputProperties, wrap);
16111635
} catch (final IOException e) {
1612-
throw new BadRequestException("Failed to read query from " + resource.getURI(), e);
1636+
throw new BadRequestException("Failed to read query from: " + source.getDocumentPath(), e);
16131637
}
16141638
}
16151639

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

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ public void stopXQuery() {
224224
* We try and read the XQuery source document under those conditions, and the read should succeed.
225225
*/
226226
@Test
227-
public void readRunningXQuerySourceDocument() throws InterruptedException, ExecutionException, TimeoutException, XPathException {
227+
public void readRunningXQuerySourceDocument() throws InterruptedException, ExecutionException, TimeoutException {
228228
// Prepare a Callable that will try and read the XQuery's source document
229229
final Callable<String> readDocumentCallable = () -> {
230230

@@ -263,36 +263,47 @@ public void readRunningXQuerySourceDocument() throws InterruptedException, Execu
263263

264264
/**
265265
* When this test is run, the /db/running-xquery-test/latched-query.xq XQuery
266-
* 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.
267268
*
268-
* We try and replace the XQuery source document under those conditions, which should not be possible
269-
* 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.
270271
*/
271272
@Test
272-
public void replaceRunningXQuerySourceDocument() throws InterruptedException {
273+
public void replaceRunningXQuerySourceDocument() throws InterruptedException, ExecutionException, TimeoutException {
273274
// Prepare a Callable that will try and replace the XQuery's source document
274-
final Callable<Boolean> replaceDocumentCallable = () -> {
275+
final Callable<Integer> replaceDocumentCallable = () -> {
275276

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

278-
final HttpResponse storeResponse = executor.execute(
279-
Request
280-
.Put(docUri)
281-
.addHeader("Content-Type", MediaType.APPLICATION_XQUERY)
282-
.bodyByteArray(replacementQuery.getBytes(UTF_8))
283-
).returnResponse();
280+
final HttpResponse storeResponse = executor.execute(
281+
Request
282+
.Put(docUri)
283+
.addHeader("Content-Type", MediaType.APPLICATION_XQUERY)
284+
.bodyByteArray(replacementQuery.getBytes(UTF_8))
285+
).returnResponse();
284286

285-
// 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 storeResponse.getStatusLine().getStatusCode();
286288

287-
return SC_CREATED == storeResponse.getStatusLine().getStatusCode();
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+
}
288295
};
289296

290297
// Run the replaceDocumentCallable from a separate thread so that we don't block our test
291-
final Future<Boolean> replacedDocumentFuture = executorService.submit(replaceDocumentCallable);
292-
assertThrows(
293-
"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",
294-
TimeoutException.class, () ->
295-
replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
296-
);
298+
final Future<Integer> replacedDocumentFuture = executorService.submit(replaceDocumentCallable);
299+
final Integer replaceDocumentStatus = replacedDocumentFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
300+
assertEquals(SC_CREATED, replaceDocumentStatus.intValue());
301+
302+
// Check the results of the query
303+
final byte[] queryResult = queryResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
304+
assertNotNull(queryResult);
305+
final String queryResultStr = new String(queryResult, UTF_8);
306+
assertTrue(queryResultStr.startsWith("<elapsed-time exit-latch-zero=\"true\">PT"));
307+
assertTrue(queryResultStr.endsWith("</elapsed-time>"));
297308
}
298309
}

0 commit comments

Comments
 (0)