This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
The following commit(s) were added to refs/heads/master by this push:
new a0270a0 SLING-12384: ResourceResolver: paged query for sling:alias
will miss entries when 'first(sling:alias)' is empty (#119)
a0270a0 is described below
commit a0270a0cff7d9d892906293cd13fd8f0b7d8f9be
Author: Julian Reschke <[email protected]>
AuthorDate: Tue Jul 23 16:36:04 2024 +0200
SLING-12384: ResourceResolver: paged query for sling:alias will miss
entries when 'first(sling:alias)' is empty (#119)
* SLING-12384: ResourceResolver: paged query for sling:alias will miss
entries where 'first(sling:alias)' is empty
* SLING-12384: adapt test case
* SLING-12384: rewrite pageing logic so that no resources are skipped on
page boundaries, add more diagnostics
* SLING-12384: rewrite pageing logic so that no resources are skipped on
page boundaries, add more diagnostics - cleanup
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: empty search result
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: broken query
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: simple query
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: paged query skipping resource with empty key
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: broken paging
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: detect broken sort order
* SLING-12384: include tests from SLING-12387, add detection of sort issue
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: sonar mangling
* SLING-12387: ResourceResolver: improve test coverage for
PagedQueryIterator: align test with SLING-12384
* SLING-12384: fix page number in debug log
* SLING-12384: report stats about last page as well (ack: Yannick Poireault)
* SLING-12384: make the paging warning a WARN log message
* SLING-12384: test the warning message
* SLING-12384: test cleanup
* SLING-12384: cleanup
* SLING-12384: avoid huge literal array init
* SLING-12384: doPageStats -> updatePageStats
* SLING-12384: fix string format for iterator warning message
* SLING-12384: MapEntriesTest - adjust string const for VP query
---
.../resourceresolver/impl/mapping/MapEntries.java | 85 +++++++++++++++++++---
.../impl/mapping/MapEntriesTest.java | 4 +-
.../impl/mapping/PagedQueryIteratorTest.java | 54 +++++++++-----
3 files changed, 112 insertions(+), 31 deletions(-)
diff --git
a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
index f9fa751..792ec7b 100644
---
a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
+++
b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
@@ -358,6 +358,7 @@ public class MapEntries implements
long initElapsed = System.nanoTime() - initStart;
long resourcesPerSecond = (vanityResourcesOnStartup.get() *
TimeUnit.SECONDS.toNanos(1) / (initElapsed == 0 ? 1 : initElapsed));
+
log.info(
"vanity path initialization - completed, processed {}
resources with sling:vanityPath properties in {}ms (~{} resource/s)",
vanityResourcesOnStartup.get(),
TimeUnit.NANOSECONDS.toMillis(initElapsed), resourcesPerSecond);
@@ -1160,7 +1161,7 @@ public class MapEntries implements
Iterator<Resource> it;
try {
- final String queryStringWithSort = baseQueryString + " AND
FIRST([sling:alias]) > '%s' ORDER BY FIRST([sling:alias])";
+ final String queryStringWithSort = baseQueryString + " AND
FIRST([sling:alias]) >= '%s' ORDER BY FIRST([sling:alias])";
it = new PagedQueryIterator("alias", "sling:alias", resolver,
queryStringWithSort, 2000);
} catch (QuerySyntaxException ex) {
log.debug("sort with first() not supported, falling back to base
query", ex);
@@ -1179,8 +1180,20 @@ public class MapEntries implements
}
long processElapsed = System.nanoTime() - processStart;
long resourcePerSecond = (count * TimeUnit.SECONDS.toNanos(1) /
(processElapsed == 0 ? 1 : processElapsed));
- log.info("alias initialization - completed, processed {} resources
with sling:alias properties in {}ms (~{} resource/s)",
- count, TimeUnit.NANOSECONDS.toMillis(processElapsed),
resourcePerSecond);
+
+ String diagnostics = "";
+ if (it instanceof PagedQueryIterator) {
+ PagedQueryIterator pit = (PagedQueryIterator)it;
+
+ if (!pit.getWarning().isEmpty()) {
+ log.warn(pit.getWarning());
+ }
+
+ diagnostics = pit.getStatistics();
+ }
+
+ log.info("alias initialization - completed, processed {} resources
with sling:alias properties in {}ms (~{} resource/s){}",
+ count, TimeUnit.NANOSECONDS.toMillis(processElapsed),
resourcePerSecond, diagnostics);
this.aliasResourcesOnStartup.set(count);
@@ -1316,17 +1329,22 @@ public class MapEntries implements
private String subject;
private String propertyName;
private String query;
- private String lastValue = "";
+ private String lastKey = "";
+ private String lastValue = null;
private Iterator<Resource> it;
private int count = 0;
private int page = 0;
private int pageSize;
private Resource next = null;
private String[] defaultValue = new String[0];
+ private int largestPage = 0;
+ private String largestKeyValue = "";
+ private int largestKeyCount = 0;
+ private int currentKeyCount = 0;
/**
* @param subject name of the query, will be used only for logging
- * @param propertyName name of multivalued string property to query on
+ * @param propertyName name of multivalued string property to query on
(used for diagnostics)
* @param resolver resource resolver
* @param query query string in SQL2 syntax
* @param pageSize page size (start a new query after page size is
exceeded)
@@ -1342,7 +1360,7 @@ public class MapEntries implements
private void nextPage() {
count = 0;
- String tquery = String.format(query, queryLiteral(lastValue));
+ String tquery = String.format(query, queryLiteral(lastKey));
log.debug("start {} query (page {}): {}", subject, page, tquery);
long queryStart = System.nanoTime();
this.it = resolver.findResources(tquery, "JCR-SQL2");
@@ -1355,20 +1373,41 @@ public class MapEntries implements
Resource resource = it.next();
count += 1;
final String[] values = resource.getValueMap().get(propertyName,
defaultValue);
+
if (values.length > 0) {
String value = values[0];
- if (value.compareTo(lastValue) < 0) {
+ if (value.compareTo(lastKey) < 0) {
String message = String.format("unexpected query result in
page %d, %s of '%s' despite querying for > '%s'",
+ (page - 1), propertyName, value, lastKey);
+ log.error(message);
+ throw new RuntimeException(message);
+ }
+ if (lastValue != null && value.compareTo(lastValue) < 0) {
+ String message = String.format("unexpected query result in
page %d, property name '%s', got '%s', last value was '%s'",
(page - 1), propertyName, value, lastValue);
log.error(message);
throw new RuntimeException(message);
}
+
+ // keep information about large key counts
+ if (value.equals(lastValue)) {
+ currentKeyCount += 1;
+ } else {
+ if (currentKeyCount > largestKeyCount) {
+ largestKeyCount = currentKeyCount + 1;
+ largestKeyValue = lastValue;
+ }
+ currentKeyCount = 0;
+ }
+
// start next page?
if (count > pageSize && !value.equals(lastValue)) {
- log.debug("read {} query (page {}); {} entries", subject,
page, count);
- lastValue = value;
+ updatePageStats();
+ lastKey = value;
nextPage();
+ return getNext();
}
+ lastValue = value;
}
return resource;
@@ -1380,6 +1419,11 @@ public class MapEntries implements
try {
next = getNext();
} catch (NoSuchElementException ex) {
+ if (currentKeyCount > largestKeyCount) {
+ largestKeyCount = currentKeyCount + 1;
+ largestKeyValue = lastValue;
+ }
+ updatePageStats();
// there are no more
next = null;
}
@@ -1393,6 +1437,27 @@ public class MapEntries implements
next = null;
return result;
}
+
+ private void updatePageStats() {
+ largestPage = Math.max(largestPage, count - 1);
+ log.debug("read {} query (page {}); {} entries, last key was: {},
largest page so far: {}", subject, page - 1,
+ count, lastKey, largestPage);
+ }
+
+ public @NotNull String getStatistics() {
+ return String.format(" (max. page size: %d, number of pages: %d)",
largestPage, page);
+ }
+
+ public @NotNull String getWarning() {
+ int warnAt = pageSize * 10;
+ if (largestKeyCount > warnAt) {
+ return String.format(
+ "Largest number of aliases with the same 'first'
selector exceeds expectation of %d (value '%s' appears %d times)",
+ warnAt, largestKeyValue, largestKeyCount);
+ } else {
+ return "";
+ }
+ }
}
/**
@@ -1408,7 +1473,7 @@ public class MapEntries implements
boolean supportsSort = true;
Iterator<Resource> it;
try {
- final String queryStringWithSort = baseQueryString + " AND
FIRST([sling:vanityPath]) > '%s' ORDER BY FIRST([sling:vanityPath])";
+ final String queryStringWithSort = baseQueryString + " AND
FIRST([sling:vanityPath]) >= '%s' ORDER BY FIRST([sling:vanityPath])";
it = new PagedQueryIterator("vanity path", PROP_VANITY_PATH,
resolver, queryStringWithSort, 2000);
} catch (QuerySyntaxException ex) {
log.debug("sort with first() not supported, falling back to base
query", ex);
diff --git
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
index b82baaf..47917e7 100644
---
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
+++
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
@@ -2279,7 +2279,7 @@ public class MapEntriesTest extends
AbstractMappingMapEntriesTest {
@Override
public Iterator<Resource> answer(InvocationOnMock invocation)
throws Throwable {
String query =
StringUtils.trim((String)invocation.getArguments()[0]);
- assertEquals("SELECT [sling:alias] FROM [nt:base] WHERE NOT
isdescendantnode('/jcr:system') AND [sling:alias] IS NOT NULL AND
FIRST([sling:alias]) > '' ORDER BY FIRST([sling:alias])", query);
+ assertEquals("SELECT [sling:alias] FROM [nt:base] WHERE NOT
isdescendantnode('/jcr:system') AND [sling:alias] IS NOT NULL AND
FIRST([sling:alias]) >= '' ORDER BY FIRST([sling:alias])", query);
return Collections.<Resource> emptySet().iterator();
}
});
@@ -2299,7 +2299,7 @@ public class MapEntriesTest extends
AbstractMappingMapEntriesTest {
// utilities for testing vanity path queries
- private static String VPQSTART = "SELECT [sling:vanityPath],
[sling:redirect], [sling:redirectStatus] FROM [nt:base] WHERE NOT
isdescendantnode('/jcr:system') AND [sling:vanityPath] IS NOT NULL AND
FIRST([sling:vanityPath]) > '";
+ private static String VPQSTART = "SELECT [sling:vanityPath],
[sling:redirect], [sling:redirectStatus] FROM [nt:base] WHERE NOT
isdescendantnode('/jcr:system') AND [sling:vanityPath] IS NOT NULL AND
FIRST([sling:vanityPath]) >= '";
private static String VPQEND = "' ORDER BY FIRST([sling:vanityPath])";
private boolean matchesPagedQuery(String query) {
diff --git
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java
index 4bd41d0..7e23e1f 100644
---
a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java
+++
b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIteratorTest.java
@@ -40,8 +40,8 @@ import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.resourceresolver.impl.ResourceResolverMetrics;
+import
org.apache.sling.resourceresolver.impl.mapping.MapEntries.PagedQueryIterator;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
@@ -86,14 +86,14 @@ public class PagedQueryIteratorTest extends
AbstractMappingMapEntriesTest {
String[] expected = new String[] { "a", "b", "c" };
Collection<Resource> expectedResources = toResourceList(expected);
when(resourceResolver.findResources(eq("simple"),
eq("JCR-SQL2"))).thenReturn(expectedResources.iterator());
- Iterator<Resource> it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver, "simple", 2000);
+ PagedQueryIterator it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver, "simple", 2000);
for (String key : expected) {
assertEquals(key, getFirstValueOf(it.next(), PROPNAME));
}
assertFalse(it.hasNext());
+ assertEquals("", it.getWarning());
}
- @Ignore("SLING-12384: detection of incorrect sort order fails")
@Test(expected = RuntimeException.class)
public void testSimpleWrongOrder() {
String[] expected = new String[] { "a", "b", "d", "c" };
@@ -106,7 +106,6 @@ public class PagedQueryIteratorTest extends
AbstractMappingMapEntriesTest {
}
}
- @Ignore("SLING-12384: resources with empty keys lost")
@Test
public void testPagedWithEmpty() {
String[] expected = new String[] { "", "a", "b", "c", "d" };
@@ -114,15 +113,29 @@ public class PagedQueryIteratorTest extends
AbstractMappingMapEntriesTest {
Collection<Resource> expectedFilteredResources = filter("",
expectedResources);
when(resourceResolver.findResources(eq("testPagedWithEmpty ''"),
eq("JCR-SQL2")))
.thenReturn(expectedFilteredResources.iterator());
- Iterator<Resource> it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver, "testPagedWithEmpty '%s'",
+ PagedQueryIterator it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver, "testPagedWithEmpty '%s'",
2000);
- for (String key : expected) {
- assertEquals(key, getFirstValueOf(it.next(), PROPNAME));
- }
- assertFalse(it.hasNext());
+ checkResult(it, expected);
+ assertEquals("", it.getWarning());
+ }
+
+ @Test
+ public void testPagedLargePage() {
+ final int cnt = 140;
+ final int pageSize = 5;
+ String[] expected = new String[cnt];
+ Arrays.fill(expected,"a");
+ Collection<Resource> expectedResources = toResourceList(expected);
+ Collection<Resource> expectedFilteredResources = filter("",
expectedResources);
+ when(resourceResolver.findResources(eq("testPagedLargePage ''"),
eq("JCR-SQL2")))
+ .thenReturn(expectedFilteredResources.iterator());
+ PagedQueryIterator it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver, "testPagedLargePage '%s'",
+ pageSize);
+ checkResult(it, expected);
+ assertEquals("Largest number of aliases with the same 'first' selector
exceeds expectation of " + pageSize * 10
+ + " (value 'a' appears " + cnt + " times)", it.getWarning());
}
- @Ignore("SLING-12384: broken paging")
@Test
public void testPagedResourcesOnPageBoundaryLost() {
String[] expected = new String[] { "a", "a", "a", "a", "a", "a", "b",
"c", "d" };
@@ -144,12 +157,8 @@ public class PagedQueryIteratorTest extends
AbstractMappingMapEntriesTest {
.thenReturn(expectedFilteredResourcesD.iterator());
Iterator<Resource> it = mapEntries.new PagedQueryIterator("alias",
PROPNAME, resourceResolver,
"testPagedResourcesOnPageBoundaryLost '%s'", 5);
- int pos = 0;
- for (String key : expected) {
- assertEquals("expects " + key + " at position " + pos, key,
getFirstValueOf(it.next(), PROPNAME));
- pos += 1;
- }
- assertFalse(it.hasNext());
+
+ checkResult(it, expected);
}
private static Collection<Resource> toResourceList(String... keys) {
@@ -165,13 +174,20 @@ public class PagedQueryIteratorTest extends
AbstractMappingMapEntriesTest {
}
private static Collection<Resource> filter(String key,
Collection<Resource> input) {
- // this emulates the ">" condition used by PagedQueryIterator prior to
- // resolution of SLING-12384
- Predicate<Resource> filter = r -> getFirstValueOf(r,
PROPNAME).compareTo(key) > 0;
+ Predicate<Resource> filter = r -> getFirstValueOf(r,
PROPNAME).compareTo(key) >= 0;
return input.stream().filter(filter).collect(Collectors.toList());
}
private static String getFirstValueOf(Resource r, String propname) {
return r.getValueMap().get(propname, new String[0])[0];
}
+
+ private static void checkResult(Iterator<Resource> it, String...expected )
{
+ int pos = 0;
+ for (String key : expected) {
+ assertEquals("expects " + key + " at position " + pos, key,
getFirstValueOf(it.next(), PROPNAME));
+ pos += 1;
+ }
+ assertFalse(it.hasNext());
+ }
}