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 4, 2024
1 parent 46b8c46 commit 0d14103
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 1 deletion.
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,6 +42,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

Expand All @@ -48,6 +54,9 @@ public class QueryHelper {
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 +107,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,6 +139,31 @@ public static Set<String> getRepPolicyNodePaths(final Session session,
return paths;
}

static boolean hasQueryIndexForACLs(final Session session) throws RepositoryException, JsonProcessingException, 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
// for lucene?
LOG.debug("Cost for rep:ACL query is estimated with {}", cost);
// minimum repo has at least some nodes to traverse
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* (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.RepositoryException;
import javax.jcr.Session;

import org.apache.jackrabbit.commons.JcrUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import com.fasterxml.jackson.core.JsonProcessingException;

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

@ExtendWith(OakRepository.class)
class QueryHelperIT {

@Test
void testHasQueryIndexForACLs(Session session) throws RepositoryException, JsonProcessingException, IOException {
// create more nodes than the threshold to have a realistic cost for traversal
createNodes(JcrUtils.getOrAddNode(session.getRootNode(), "tmp"), 256);
session.save();
// FIXME: the default oak repo does always have a node type index provider (NodeTypeIndexProvider) and an according index definition at "/oak:index/nodetype"
assertTrue(QueryHelper.hasQueryIndexForACLs(session));
}

private static void createNodes(Node rootNode, int numNodes) throws RepositoryException {
for (int n=0; n<numNodes; n++) {
JcrUtils.getOrAddNode(rootNode, "Node"+n);
}
}
}

0 comments on commit 0d14103

Please sign in to comment.