Skip to content

Commit

Permalink
Improve detection of index for "rep:ACL" nodes
Browse files Browse the repository at this point in the history
Rely on EXPLAIN MEASURE and evaluate the estimated cost.

This closes #714
  • Loading branch information
kwin committed Jun 6, 2024
1 parent 46b8c46 commit 80c3c50
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import static biz.netcentric.cq.tools.actool.history.impl.PersistableInstallationLogger.msHumanReadable;

import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
Expand All @@ -29,6 +30,7 @@
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
Expand All @@ -40,13 +42,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class QueryHelper {
public static final Logger LOG = LoggerFactory.getLogger(QueryHelper.class);

private static final String ROOT_REP_POLICY_NODE = "/rep:policy";
private static final String ROOT_REPO_POLICY_NODE = "/" + Constants.REPO_POLICY_NODE;
private static final String HOME_REP_POLICY = "/home/rep:policy";
private static final String OAK_INDEX_PATH_REP_ACL = "/oak:index/repACL-custom-1";

/** every query cost below that threshold means a dedicated index exists, above that threshold means: fallback to traversal */
private static final double COST_THRESHOLD_FOR_QUERY_INDEX = 100d;

/** Method that returns a set containing all rep:policy nodes from repository excluding those contained in paths which are excluded from
* search
Expand Down Expand Up @@ -98,7 +105,12 @@ public static Set<String> getRepPolicyNodePaths(final Session session,
paths.add(HOME_REP_POLICY);
}

boolean indexForRepACLExists = session.nodeExists(OAK_INDEX_PATH_REP_ACL);
boolean indexForRepACLExists = false;
try {
indexForRepACLExists = hasQueryIndexForACLs(session);
} catch(IOException|RepositoryException e) {
LOG.warn("Cannot figure out if query index for rep:ACL nodes exist", e);
}
LOG.debug("Index for repACL exists: {}",indexForRepACLExists);
String queryForAClNodes = indexForRepACLExists ?
"SELECT * FROM [rep:ACL] WHERE ISDESCENDANTNODE([%s])" :
Expand All @@ -125,10 +137,32 @@ public static Set<String> getRepPolicyNodePaths(final Session session,
return paths;
}

static boolean hasQueryIndexForACLs(final Session session) throws RepositoryException, IOException {
Query query = session.getWorkspace().getQueryManager().createQuery("EXPLAIN MEASURE SELECT * FROM [rep:ACL] AS s WHERE ISDESCENDANTNODE([/])", Query.JCR_SQL2);
QueryResult queryResult = query.execute();
Row row = queryResult.getRows().nextRow();
// inspired by https://github.com/apache/jackrabbit-oak/blob/cc8adb42d89bc4625138a62ab074e7794a4d39ab/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java#L1092
String plan = row.getValue("plan").getString();
String costJson = plan.substring(plan.lastIndexOf('{'));

// use jackson for JSON parsing
ObjectMapper mapper = new ObjectMapper();

// read the json strings and convert it into JsonNode
JsonNode node = mapper.readTree(costJson);
double cost = node.get("s").asDouble(Double.MAX_VALUE);
// look at https://jackrabbit.apache.org/oak/docs/query/query-engine.html#cost-calculation for the threshold
// https://github.com/apache/jackrabbit-oak/blob/cc8adb42d89bc4625138a62ab074e7794a4d39ab/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java#L75

// for traversing cost = estimation of node count
// for property index = between 2 and 100
LOG.debug("Cost for rep:ACL query is estimated with {}", cost);
return cost <= COST_THRESHOLD_FOR_QUERY_INDEX;
}

/** Get Nodes with XPATH Query. */
public static Set<String> getNodePathsFromQuery(final Session session,
final String xpathQuery) throws InvalidQueryException,
RepositoryException {
final String xpathQuery) throws RepositoryException {
return getNodePathsFromQuery(session, xpathQuery, Query.XPATH);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.stream.Stream;

import javax.jcr.LoginException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
Expand Down Expand Up @@ -77,19 +79,39 @@ public class OakRepository implements BeforeAllCallback, BeforeEachCallback, Aft

private static final Logger LOGGER = LoggerFactory.getLogger(OakRepository.class);

/**
* Optionally uses a dedicated BlobStore with Oak, otherwise just in memory
*/
private final boolean useFileStore;

private final Consumer<Jcr> jcrInitCallback;

public OakRepository() {
this(true);
}

public OakRepository(boolean useFileStore) {
this(useFileStore, null);
}

public OakRepository(boolean useFileStore, Consumer<Jcr> jcrInitCallback) {
this.useFileStore = useFileStore;
this.jcrInitCallback = jcrInitCallback;
}

@Override
public void afterAll(ExtensionContext context) throws Exception {
shutdownRepository();
}

@Override
public void beforeAll(ExtensionContext context) throws Exception {
initRepository(true);
initRepository();
}

@Override
public void beforeEach(ExtensionContext context) throws Exception {
admin = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
admin = createAdminSession();
}

@Override
Expand All @@ -110,11 +132,11 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
}

/**
* @param useFileStore only evaluated for Oak. Optionally uses a dedicated BlobStore with Oak
* @param useFileStore only evaluated for Oak. O
* @throws RepositoryException
* @throws IOException
* @throws InvalidFileStoreVersionException */
private void initRepository(boolean useFileStore) throws RepositoryException, IOException, InvalidFileStoreVersionException {
private void initRepository() throws RepositoryException, IOException, InvalidFileStoreVersionException {
Jcr jcr;
if (useFileStore) {
BlobStore blobStore = createBlobStore();
Expand All @@ -128,7 +150,9 @@ private void initRepository(boolean useFileStore) throws RepositoryException, IO
// in-memory repo
jcr = new Jcr();
}

if (jcrInitCallback != null) {
jcrInitCallback.accept(jcr);
}
repository = jcr
.with(createSecurityProvider())
.withAtomicCounter()
Expand All @@ -151,8 +175,8 @@ private void shutdownRepository() throws IOException {
if (fileStore != null) {
fileStore.close();
fileStore = null;
deleteDirectory(DIR_OAK_REPO_HOME);
}
deleteDirectory(DIR_OAK_REPO_HOME);
}
repository = null;
}
Expand Down Expand Up @@ -206,4 +230,8 @@ public static ConfigurationParameters getSecurityConfigurationParameters() {
UserConfiguration.NAME, ConfigurationParameters.of(userProps),
AuthorizationConfiguration.NAME, ConfigurationParameters.of(authzProps));
}

public Session createAdminSession() throws RepositoryException {
return repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* (C) Copyright 2023 Cognizant Netcentric.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package biz.netcentric.cq.tools.actool.helper;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;

import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import biz.netcentric.cq.tools.actool.extensions.OakRepository;

class QueryHelperIT {

@RegisterExtension
static final OakRepository repository = new OakRepository(false);

@Test
void testHasQueryIndexForACLsWithoutIndex(Session session) throws RepositoryException, IOException {
// adjust nodetype index definition (at /oak:index/nodetype) to reflect what is configured in AEM
Node ntIndexDefNode = session.getNode("/oak:index/nodetype");
ntIndexDefNode.setProperty("declaringNodeTypes", new String[] { "oak:QueryIndexDefinition", "rep:User", "rep:Authorizable" }, PropertyType.NAME);
session.save();
assertFalse(QueryHelper.hasQueryIndexForACLs(session));
}

@Test
void testHasQueryIndexForACLsWithIndex(Session session) throws RepositoryException, IOException {
// adjust nodetype index definition (at /oak:index/nodetype) to include rep:ACL
// this is a different type than shipped with ACTool (Lucene), but lucene based index providers are hard to set up in an IT
Node ntIndexDefNode = session.getNode("/oak:index/nodetype");
ntIndexDefNode.setProperty("declaringNodeTypes", new String[] { "oak:QueryIndexDefinition", "rep:User", "rep:Authorizable", "rep:ACL" }, PropertyType.NAME);
session.save();
assertTrue(QueryHelper.hasQueryIndexForACLs(session));
}

}

0 comments on commit 80c3c50

Please sign in to comment.