Skip to content

Commit

Permalink
[GEOS-8430] Cache small amount of features in memory to avoid repeate…
Browse files Browse the repository at this point in the history
…d data scans in GetFeature requests - some small improvements.

Small docs and code changes, added one test to check GetFeature still works when the caching machinery is disabled
  • Loading branch information
aaime committed Nov 25, 2017
1 parent f70f94a commit 349254f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 70 deletions.
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,152 +4,176 @@
*/ */
package org.geoserver.wfs; package org.geoserver.wfs;


import java.io.IOException; import org.geotools.data.DataUtilities;
import java.util.ArrayList;
import java.util.List;

import org.geotools.data.FeatureSource; import org.geotools.data.FeatureSource;
import org.geotools.data.Query; import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.FeatureIterator; import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.collection.DecoratingSimpleFeatureCollection; import org.geotools.feature.collection.DecoratingSimpleFeatureCollection;

import org.opengis.feature.Feature; import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.FeatureType;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;



/** /**
* A feature collection which caches a configurable (and small) amount of features. * A feature collection which caches a configurable (and small) amount of features to avoid
* * repeated reads against datastore that cannot optimize count in a count/read cycle typical
* of WFS requests (count as a FeatureCollection attribute, and then read for feature collection
* contents)
*
* @author Alvaro Huarte * @author Alvaro Huarte
*/ */
class FeatureSizeFeatureCollection extends DecoratingSimpleFeatureCollection { public class FeatureSizeFeatureCollection extends DecoratingSimpleFeatureCollection {


/**
* The default feature cache size
*/
public static final int DEFAULT_CACHE_SIZE = 16;

/** /**
* The original feature source. * The original feature source.
*/ */
protected FeatureSource<? extends FeatureType, ? extends Feature> featureSource; protected SimpleFeatureSource featureSource;

/** /**
* The feature cache to manage. * The feature cache to manage.
*/ */
protected List<SimpleFeature> featureCache; private List<SimpleFeature> featureCache;

/** /**
* The original query. * The original query.
*/ */
protected Query query; private Query query;

/** /**
* Defines the maximum number of cacheable features to avoid successive readings * Defines the maximum number of feature that will be cached in memory to avoid
* of the data source. * multiple data reads in case there is no fast {@link FeatureSource#getCount(Query)}
* <p> * implementation for the current query.
* With a minimum overload of memory, it takes advantage of a previous reading
* of features when the feature source does not support direct count of the
* collection managed (e.g. shapefile stores).
* </p>
* <p> * <p>
* It is very useful when the feature store needs to execute costly queries * Useful in particular for stores that do not have any way to perform a fast count
* and the filter returns empty or low-count feature results. * against a filtered query, like shapefiles
* In some contexts, WFS-GetFeature requests need to precalculate the size
* of the results, and (e.g. shapefile stores) the query is executed at
* least twice causing two readings of the data source.
* </p> * </p>
*/ */
static int FEATURE_CACHE_LIMIT = private static int FEATURE_CACHE_LIMIT =
Integer.valueOf(System.getProperty("org.geoserver.wfs.getfeature.cachelimit", "16")); Integer.valueOf(System.getProperty("org.geoserver.wfs.getfeature.cachelimit", String.valueOf(DEFAULT_CACHE_SIZE)));

/** /**
* Allows to programmatically set the maximum number of cacheable features. * Allows to programmatically set the maximum number of cacheable features.
*/ */
public static void setFeatureCacheLimit(int featureCacheLimit) { public static void setFeatureCacheLimit(int featureCacheLimit) {
FEATURE_CACHE_LIMIT = featureCacheLimit; FEATURE_CACHE_LIMIT = featureCacheLimit;
} }


public FeatureSizeFeatureCollection(SimpleFeatureCollection delegate, FeatureSource<? extends FeatureType, ? extends Feature> source, Query query) { public FeatureSizeFeatureCollection(SimpleFeatureCollection delegate, SimpleFeatureSource source, Query query) {
super(delegate); super(delegate);
this.featureSource = source; this.featureSource = source;
this.query = query; this.query = query;
} }


/**
* Wraps the {@link FeatureCollection} into {@link FeatureSizeFeatureCollection} in case the feature caching
* is enabled and the the features are simple ones
*
* @param features
* @param source
* @param gtQuery
* @return
*/
static FeatureCollection<? extends FeatureType, ? extends Feature> wrap(FeatureCollection<? extends FeatureType,
? extends Feature> features, FeatureSource<? extends FeatureType, ? extends Feature> source, Query
gtQuery) {
if (FEATURE_CACHE_LIMIT > 0 && features.getSchema() instanceof SimpleFeatureType) {
return new FeatureSizeFeatureCollection((SimpleFeatureCollection) features, DataUtilities.simple
(source), gtQuery);
} else {
return features;
}
}

class CachedWrappingFeatureIterator implements SimpleFeatureIterator { class CachedWrappingFeatureIterator implements SimpleFeatureIterator {

private List<SimpleFeature> featureCache; private List<SimpleFeature> featureCache;
private int featureIndex = 0; private int featureIndex = 0;

public CachedWrappingFeatureIterator(List<SimpleFeature> featureCache) { public CachedWrappingFeatureIterator(List<SimpleFeature> featureCache) {
this.featureCache = featureCache; this.featureCache = featureCache;
} }

@Override @Override
public boolean hasNext() { public boolean hasNext() {
return featureIndex < featureCache.size(); return featureIndex < featureCache.size();
} }

@Override @Override
public SimpleFeature next() { public SimpleFeature next() {
return featureCache.get(featureIndex++); return featureCache.get(featureIndex++);
} }

@Override @Override
public void close() { public void close() {
featureIndex = 0; featureIndex = 0;
} }
} }

@Override @Override
public SimpleFeatureIterator features() { public SimpleFeatureIterator features() {
if (featureCache != null) { if (featureCache != null) {
return new CachedWrappingFeatureIterator( featureCache ); return new CachedWrappingFeatureIterator(featureCache);
} }
return super.features(); return super.features();
} }

@Override @Override
public int size() { public int size() {
if (featureCache != null) { if (featureCache != null) {
return featureCache.size(); return featureCache.size();
} }
if (FEATURE_CACHE_LIMIT > 0) { if (FEATURE_CACHE_LIMIT > 0) {
FeatureIterator<? extends Feature> it = null;

try { try {
// try optimized method, will return -1 if there is no fast way to compute
int count = featureSource.getCount(query); int count = featureSource.getCount(query);


// zero is a legit value, cache and exit
if (count == 0) { if (count == 0) {
featureCache = new ArrayList<SimpleFeature>(); featureCache = new ArrayList<>();
return count; return count;
} }
// fast path, no need to cache
if (count > 0) { if (count > 0) {
return count; return count;
} }


// we have to iterate, save to cache to avoid later successive readings of data. // we have to iterate, save to cache to avoid reading data.
List<SimpleFeature> tempFeatureCache = new ArrayList<SimpleFeature>(); List<SimpleFeature> tempFeatureCache = new ArrayList<>();


// bean counting... // bean counting like ContentFeatureCollection would do, but with limited
it = featureSource.getFeatures(query).features(); // size feature caching in the mix
count = 0; try (SimpleFeatureIterator it = featureSource.getFeatures(query).features()) {
while (it.hasNext()) { count = 0;
SimpleFeature feature = (SimpleFeature) it.next(); while (it.hasNext()) {
if (tempFeatureCache.size() < FEATURE_CACHE_LIMIT) tempFeatureCache.add(feature); SimpleFeature feature = it.next();
count++; if (tempFeatureCache.size() < FEATURE_CACHE_LIMIT) {
} tempFeatureCache.add(feature);
if (count <= FEATURE_CACHE_LIMIT) { }
featureCache = tempFeatureCache; count++;
} else { }
tempFeatureCache.clear(); // if the count is below limit, keep the cache, otherwise clear it
if (count <= FEATURE_CACHE_LIMIT) {
featureCache = tempFeatureCache;
} else {
tempFeatureCache.clear();
}
return count;
} }
return count;

} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally {
if (it != null) {
it.close();
}
} }
} }
return super.size(); return super.size();
Expand Down
8 changes: 4 additions & 4 deletions src/wfs/src/main/java/org/geoserver/wfs/GetFeature.java
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.geotools.data.FeatureSource; import org.geotools.data.FeatureSource;
import org.geotools.data.Join; import org.geotools.data.Join;
import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.factory.Hints; import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollection;
import org.geotools.feature.NameImpl; import org.geotools.feature.NameImpl;
Expand Down Expand Up @@ -930,7 +931,8 @@ void encodeQueryAsKvp(Query q, StringBuilder typeNames, StringBuilder propertyNa
} }


/** /**
* Allows subclasses to poke with the feature collection extraction * Allows subclasses to poke with the feature collection extraction. The default behavior attempts to
* wrap the feature collectio into a {@link FeatureSizeFeatureCollection}.
* @param source * @param source
* @param gtQuery * @param gtQuery
* *
Expand All @@ -942,9 +944,7 @@ void encodeQueryAsKvp(Query q, StringBuilder typeNames, StringBuilder propertyNa
throws IOException { throws IOException {
FeatureCollection<? extends FeatureType, ? extends Feature> features = source.getFeatures(gtQuery); FeatureCollection<? extends FeatureType, ? extends Feature> features = source.getFeatures(gtQuery);


if (features.getSchema() instanceof SimpleFeatureType) { features = FeatureSizeFeatureCollection.wrap(features, source, gtQuery);
features = new FeatureSizeFeatureCollection((SimpleFeatureCollection)features, source, gtQuery);
}
return features; return features;
} }


Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,26 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs;

import org.geoserver.wfs.v2_0.GetFeatureTest;
import org.junit.AfterClass;
import org.junit.BeforeClass;

/**
* Test making sure GetFeature still works when the caching machinery is off (since most test datasets
* will actually be cached in memory)
*/
public class GetFeatureNoCachingTest extends GetFeatureTest {

@BeforeClass
public static void disableCaching() {
FeatureSizeFeatureCollection.setFeatureCacheLimit(0);
}

@AfterClass
public static void enableCaching() {
FeatureSizeFeatureCollection.setFeatureCacheLimit(FeatureSizeFeatureCollection.DEFAULT_CACHE_SIZE);
}
}

0 comments on commit 349254f

Please sign in to comment.