-
Notifications
You must be signed in to change notification settings - Fork 427
OAK-12348: Property and node type indexes support costPerEntry/costPerExecution overrides #3076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -28,13 +28,15 @@ | |||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
|
|
||||
| import org.apache.jackrabbit.oak.api.PropertyState; | ||||
| import org.apache.jackrabbit.oak.api.PropertyValue; | ||||
| import org.apache.jackrabbit.oak.api.Type; | ||||
| import org.apache.jackrabbit.oak.commons.PathUtils; | ||||
| import org.apache.jackrabbit.oak.commons.collections.IterableUtils; | ||||
| import org.apache.jackrabbit.oak.plugins.index.IndexConstants; | ||||
| import org.apache.jackrabbit.oak.plugins.index.IndexUtils; | ||||
| import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy; | ||||
| import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider; | ||||
| import org.apache.jackrabbit.oak.spi.mount.Mounts; | ||||
|
|
@@ -75,6 +77,24 @@ public class PropertyIndexLookup { | |||
| */ | ||||
| static final int MAX_COST = 100; | ||||
|
|
||||
| /** | ||||
| * Feature toggle name for the configurable costPerEntry/costPerExecution | ||||
| * cost formula (OAK-12348). | ||||
| */ | ||||
| public static final String FT_OAK_12348 = "FT_OAK-12348"; | ||||
|
|
||||
| /** | ||||
| * When {@code true} (the default), {@link #getCost} reads {@code costPerEntry}/ | ||||
| * {@code costPerExecution} from the index definition ({@link #getCostConfigurable}). | ||||
| * When {@code false}, {@link #getCost} uses the original hardcoded formula | ||||
| * ({@link #getCostLegacy}) unconditionally, ignoring those properties even if | ||||
| * set. Enabled by default: the new formula reproduces the legacy one exactly | ||||
| * whenever {@code costPerEntry}/{@code costPerExecution} are absent, so this is | ||||
| * a behavior-preserving default for anyone not using the new properties -- the | ||||
| * toggle exists as an escape hatch, not as an opt-in gate. | ||||
| */ | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to AI: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java:137 has an early return, that get's around this PR. However, in my opinion it should not cause any issues, as it only happens for cost == 2.0 which is rare, and anyways a very fast case.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But there seem to be things no longer correct in PropertyIndex.java like getMinimumCost() Line 223 in a3d05a4
Probably worth checking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch :) |
||||
| public static final AtomicBoolean FT_OAK_12348_ENABLE = new AtomicBoolean(true); | ||||
|
bhabegger marked this conversation as resolved.
|
||||
|
|
||||
| private final NodeState root; | ||||
|
|
||||
| private final MountInfoProvider mountInfoProvider; | ||||
|
|
@@ -135,7 +155,22 @@ Set<IndexStoreStrategy> getStrategies(NodeState definition) { | |||
| definition, INDEX_CONTENT_NODE_NAME); | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Dispatches to {@link #getCostConfigurable} or {@link #getCostLegacy} | ||||
| * depending on {@link #FT_OAK_12348_ENABLE}. | ||||
| */ | ||||
| public double getCost(Filter filter, String propertyName, PropertyValue value) { | ||||
| return FT_OAK_12348_ENABLE.get() | ||||
| ? getCostConfigurable(filter, propertyName, value) | ||||
| : getCostLegacy(filter, propertyName, value); | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Original cost formula: {@code COST_OVERHEAD + entryCount}. Ignores | ||||
| * {@code costPerEntry}/{@code costPerExecution} even if set on the index | ||||
| * definition. | ||||
| */ | ||||
| public double getCostLegacy(Filter filter, String propertyName, PropertyValue value) { | ||||
| NodeState indexMeta = getIndexNode(root, propertyName, filter); | ||||
| if (indexMeta == null) { | ||||
| return Double.POSITIVE_INFINITY; | ||||
|
|
@@ -149,6 +184,31 @@ public double getCost(Filter filter, String propertyName, PropertyValue value) { | |||
| return cost; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * {@code cost = costPerExecution + costPerEntry * entryCount}, both | ||||
| * optionally configured on the index definition (OAK-12348). Defaults | ||||
| * ({@code costPerEntry=1.0}, {@code costPerExecution=COST_OVERHEAD}) | ||||
| * reproduce {@link #getCostLegacy} exactly. | ||||
| */ | ||||
| public double getCostConfigurable(Filter filter, String propertyName, PropertyValue value) { | ||||
| NodeState indexMeta = getIndexNode(root, propertyName, filter); | ||||
| if (indexMeta == null) { | ||||
| return Double.POSITIVE_INFINITY; | ||||
| } | ||||
| Set<IndexStoreStrategy> strategies = getStrategies(indexMeta); | ||||
| if (strategies.isEmpty()) { | ||||
| return MAX_COST; | ||||
| } | ||||
| ValuePattern pattern = new ValuePattern(indexMeta); | ||||
| double entryCount = 0; | ||||
| for (IndexStoreStrategy s : strategies) { | ||||
| entryCount += s.count(filter, root, indexMeta, encode(value, pattern), MAX_COST); | ||||
| } | ||||
| double costPerEntry = IndexUtils.getOptionalValue(indexMeta, IndexConstants.COST_PER_ENTRY, 1.0); | ||||
| double costPerExecution = IndexUtils.getOptionalValue(indexMeta, IndexConstants.COST_PER_EXECUTION, COST_OVERHEAD); | ||||
| return costPerExecution + costPerEntry * entryCount; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Get the node with the index definition for the given property, if there | ||||
| * is an applicable index with data. | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,13 @@ public class PropertyIndexPlan { | |
|
|
||
| private boolean matchesNodeTypes; | ||
|
|
||
| private final double cost; | ||
| /** | ||
| * The number of matching entries for the best-matching property found by | ||
| * the constructor's search loop, or {@code Double.POSITIVE_INFINITY} if | ||
| * none matched. {@link #getCostLegacy} and {@link #getCostConfigurable} | ||
| * both derive the final cost from this count. | ||
| */ | ||
| private final double bestCount; | ||
|
|
||
| private final Set<String> values; | ||
|
|
||
|
|
@@ -113,7 +119,7 @@ public class PropertyIndexPlan { | |
|
|
||
| ValuePattern valuePattern = new ValuePattern(definition); | ||
|
|
||
| double bestCost = Double.POSITIVE_INFINITY; | ||
| double bestCount = Double.POSITIVE_INFINITY; | ||
| Set<String> bestValues = emptySet(); | ||
| int bestDepth = 1; | ||
|
|
||
|
|
@@ -168,22 +174,22 @@ public class PropertyIndexPlan { | |
| } | ||
| } | ||
| values = PropertyIndexUtil.encode(values); | ||
| double cost = strategies.isEmpty() ? MAX_COST : 0; | ||
| double count = strategies.isEmpty() ? MAX_COST : 0; | ||
| for (IndexStoreStrategy strategy : strategies) { | ||
| cost += strategy.count(filter, root, definition, | ||
| count += strategy.count(filter, root, definition, | ||
| values, MAX_COST); | ||
| } | ||
| if (unique && cost <= 1) { | ||
| if (unique && count <= 1) { | ||
| // for unique index, for the normal case | ||
| // (that is, for a regular lookup) | ||
| // no further reads are needed | ||
| cost = 0; | ||
| count = 0; | ||
| } | ||
| if (cost < bestCost) { | ||
| if (count < bestCount) { | ||
| bestDepth = depth; | ||
| bestValues = values; | ||
| bestCost = cost; | ||
| if (bestCost == 0) { | ||
| bestCount = count; | ||
| if (bestCount == 0) { | ||
| // shortcut: not possible to top this | ||
| break; | ||
| } | ||
|
|
@@ -194,15 +200,45 @@ public class PropertyIndexPlan { | |
|
|
||
| this.depth = bestDepth; | ||
| this.values = bestValues; | ||
| this.cost = COST_OVERHEAD + bestCost; | ||
| this.bestCount = bestCount; | ||
| } | ||
|
|
||
| String getName() { | ||
| return name; | ||
| } | ||
|
|
||
| /** | ||
| * Dispatches to {@link #getCostConfigurable} or {@link #getCostLegacy} | ||
| * depending on {@link PropertyIndexLookup#FT_OAK_12348_ENABLE}, evaluated | ||
| * once per plan (a new plan is built whenever the filter changes, so a | ||
| * toggle flip is picked up on the next query, not on this cached plan). | ||
| */ | ||
| double getCost() { | ||
| return cost; | ||
| return PropertyIndexLookup.FT_OAK_12348_ENABLE.get() ? getCostConfigurable() : getCostLegacy(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Computed each time we call getCost() (called multiple times in createPlan), but probably works best with toggle this way. |
||
| } | ||
|
|
||
| /** | ||
| * Original cost formula: {@code COST_OVERHEAD + bestCount}. Ignores | ||
| * {@code costPerEntry}/{@code costPerExecution} even if set on the index | ||
| * definition. | ||
| */ | ||
| double getCostLegacy() { | ||
| return bestCount == Double.POSITIVE_INFINITY ? Double.POSITIVE_INFINITY : COST_OVERHEAD + bestCount; | ||
| } | ||
|
|
||
| /** | ||
| * {@code cost = costPerExecution + costPerEntry * bestCount}, both | ||
| * optionally configured on the index definition (OAK-12348). Defaults | ||
| * ({@code costPerEntry=1.0}, {@code costPerExecution=COST_OVERHEAD}) | ||
| * reproduce {@link #getCostLegacy} exactly. | ||
| */ | ||
| double getCostConfigurable() { | ||
| if (bestCount == Double.POSITIVE_INFINITY) { | ||
| return Double.POSITIVE_INFINITY; | ||
| } | ||
| double costPerEntry = IndexUtils.getOptionalValue(definition, IndexConstants.COST_PER_ENTRY, 1.0); | ||
| double costPerExecution = IndexUtils.getOptionalValue(definition, IndexConstants.COST_PER_EXECUTION, COST_OVERHEAD); | ||
| return costPerExecution + costPerEntry * bestCount; | ||
| } | ||
|
|
||
| Cursor execute() { | ||
|
|
@@ -260,7 +296,7 @@ public String toString() { | |
| } | ||
| } | ||
| buffer.append("\n"); | ||
| buffer.append(" estimatedCost: ").append(cost).append("\n"); | ||
| buffer.append(" estimatedCost: ").append(getCost()).append("\n"); | ||
| return buffer.toString(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this toggle usable? Where is it wired into the whiteboard mechanism?
In my opinion this kind of wiring is missing:
jackrabbit-oak/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
Line 68 in a3d05a4
jackrabbit-oak/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
Line 588 in a3d05a4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed the whiteboard registration was missed.