Skip to content

Commit a347343

Browse files
authored
GEOMESA-3452 Accumulo - Use number of tablets scanned for query planning (#3287)
* `stats` cost evaluation for Accumulo now uses tablets scanned instead of cached stat estimates * Query interceptors moved to a scan-time check instead of plan time
1 parent 1c0555a commit a347343

File tree

38 files changed

+560
-378
lines changed

38 files changed

+560
-378
lines changed

docs/user/upgrade.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ The following dependencies have been upgraded:
9393
* scala 2.13 ``2.13.12`` -> ``2.13.16``
9494
* spark ``3.5.5`` -> ``3.5.7``
9595

96+
StrategyDecider API Update
97+
--------------------------
98+
99+
The ``org.locationtech.geomesa.index.planning.StrategyDecider`` API has been changed slightly to provide more context for
100+
implementations to use. The old API method has been deprecated and will be removed in a future version.
101+
96102
Removed Modules
97103
---------------
98104

@@ -125,6 +131,10 @@ Deprecated Classes
125131
* ``org.locationtech.geomesa.utils.stats.Cardinality`` - replaced with ``org.locationtech.geomesa.utils.index.Cardinality``
126132
* ``org.locationtech.geomesa.utils.stats.IndexCoverage`` - replaced with ``org.locationtech.geomesa.utils.index.IndexCoverage``
127133

134+
Deprecated Methods
135+
------------------
136+
137+
* ``org.locationtech.geomesa.index.planning.StrategyDecider.selectFilterPlan``
128138

129139
Internal API Changes
130140
--------------------

geomesa-accumulo/geomesa-accumulo-datastore/src/main/scala/org/locationtech/geomesa/accumulo/data/AccumuloDataStore.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import org.locationtech.geomesa.accumulo.data.stats._
2525
import org.locationtech.geomesa.accumulo.index._
2626
import org.locationtech.geomesa.accumulo.iterators.{AgeOffIterator, DtgAgeOffIterator, ProjectVersionIterator, VisibilityIterator}
2727
import org.locationtech.geomesa.filter.FilterHelper
28-
import org.locationtech.geomesa.index.api.{FilterStrategy, GeoMesaFeatureIndex}
28+
import org.locationtech.geomesa.index.api.{FilterStrategy, GeoMesaFeatureIndex, QueryStrategy}
2929
import org.locationtech.geomesa.index.geotools.GeoMesaDataStore
3030
import org.locationtech.geomesa.index.index.attribute.AttributeIndex
3131
import org.locationtech.geomesa.index.index.id.IdIndex
@@ -421,7 +421,9 @@ class AccumuloDataStore(val connector: AccumuloClient, override val config: Accu
421421
val queryPlans = getQueryPlan(query)
422422

423423
if (queryPlans.isEmpty) {
424-
EmptyPlan(FilterStrategy(fallbackIndex, None, Some(Filter.EXCLUDE), temporal = false, Float.PositiveInfinity))
424+
val filter =
425+
FilterStrategy(fallbackIndex, None, Some(Filter.EXCLUDE), temporal = false, Float.PositiveInfinity, query.getHints)
426+
EmptyPlan(QueryStrategy(filter, Seq.empty, Seq.empty, Seq.empty, filter.filter, None))
425427
} else {
426428
val qps =
427429
if (queryPlans.lengthCompare(1) == 0) { queryPlans } else {

geomesa-accumulo/geomesa-accumulo-datastore/src/main/scala/org/locationtech/geomesa/accumulo/data/AccumuloIndexAdapter.scala

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
package org.locationtech.geomesa.accumulo.data
1010

11+
import com.github.benmanes.caffeine.cache.{CacheLoader, Caffeine}
1112
import com.typesafe.scalalogging.LazyLogging
1213
import org.apache.accumulo.core.data.{Key, Range, Value}
1314
import org.apache.accumulo.core.file.keyfunctor.RowFunctor
1415
import org.apache.hadoop.io.Text
1516
import org.geotools.api.feature.simple.{SimpleFeature, SimpleFeatureType}
17+
import org.locationtech.geomesa.accumulo.AccumuloProperties.TableProperties.TableCacheExpiry
1618
import org.locationtech.geomesa.accumulo.data.AccumuloIndexAdapter._
1719
import org.locationtech.geomesa.accumulo.data.AccumuloQueryPlan.{BatchScanPlan, EmptyPlan}
1820
import org.locationtech.geomesa.accumulo.data.writer.tx.AccumuloAtomicIndexWriter
@@ -35,6 +37,7 @@ import org.locationtech.geomesa.index.index.z2.{Z2Index, Z2IndexValues}
3537
import org.locationtech.geomesa.index.index.z3.{Z3Index, Z3IndexValues}
3638
import org.locationtech.geomesa.index.iterators.StatsScan
3739
import org.locationtech.geomesa.index.planning.LocalQueryRunner.LocalTransformReducer
40+
import org.locationtech.geomesa.index.utils.Explainer
3841
import org.locationtech.geomesa.security.SecurityUtils
3942
import org.locationtech.geomesa.utils.concurrent.CachedThreadPool
4043
import org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.{Configs, InternalConfigs}
@@ -58,6 +61,13 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
5861

5962
private val tableOps = ds.connector.tableOperations()
6063

64+
private val tableSizeCache =
65+
Caffeine.newBuilder().expireAfterWrite(TableCacheExpiry.toJavaDuration.get).build[String, Integer](
66+
new CacheLoader[String, Integer]() {
67+
override def load(table: String): Integer = tableOps.listSplits(table).size() + 1
68+
}
69+
)
70+
6171
// noinspection ScalaDeprecation
6272
override def createTable(
6373
index: GeoMesaFeatureIndex[_, _],
@@ -153,21 +163,12 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
153163
override def createQueryPlan(strategy: QueryStrategy): AccumuloQueryPlan = {
154164
import org.locationtech.geomesa.index.conf.QueryHints.RichHints
155165

156-
val QueryStrategy(filter, byteRanges, _, _, ecql, hints, _) = strategy
157-
val index = filter.index
158-
// index api defines empty start/end for open-ended range - in accumulo, it's indicated with null
159-
// index api defines start row inclusive, end row exclusive
160-
val ranges = byteRanges.map {
161-
case BoundedByteRange(start, end) =>
162-
val startKey = if (start.length == 0) { null } else { new Key(new Text(start)) }
163-
val endKey = if (end.length == 0) { null } else { new Key(new Text(end)) }
164-
new Range(startKey, true, endKey, false)
165-
166-
case SingleRowByteRange(row) =>
167-
new Range(new Text(row))
168-
}
166+
val index = strategy.index
167+
val ecql = strategy.ecql
168+
val hints = strategy.hints
169+
val ranges = strategy.ranges.map(toAccumuloRange)
169170
val numThreads = if (index.name == IdIndex.name) { ds.config.queries.recordThreads } else { ds.config.queries.threads }
170-
val tables = index.getTablesForQuery(filter.filter)
171+
val tables = index.getTablesForQuery(strategy.filter.filter)
171172
val (colFamily, schema) = {
172173
val (cf, s) = groups.group(index.sft, hints.getTransformDefinition, ecql)
173174
(Some(new Text(ColumnFamilyMapper(index)(cf))), s)
@@ -180,10 +181,10 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
180181

181182
index match {
182183
case i: AttributeJoinIndex =>
183-
AccumuloJoinIndexAdapter.createQueryPlan(ds, i, filter, tables, ranges, colFamily, schema, ecql, hints, numThreads)
184+
AccumuloJoinIndexAdapter.createQueryPlan(ds, i, strategy, tables, ranges, colFamily, schema, ecql, hints, numThreads)
184185

185186
case _ =>
186-
val (iter, eToF, reduce) = if (strategy.hints.isBinQuery) {
187+
val (iter, eToF, reduce) = if (hints.isBinQuery) {
187188
if (ds.config.remote.bin) {
188189
val iter = BinAggregatingIterator.configure(schema, index, ecql, hints)
189190
(Seq(iter), new AccumuloBinResultsToFeatures(), None)
@@ -195,9 +196,9 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
195196
}
196197
(fti, resultsToFeatures, localReducer)
197198
}
198-
} else if (strategy.hints.isArrowQuery) {
199+
} else if (hints.isArrowQuery) {
199200
if (ds.config.remote.arrow) {
200-
val (iter, reduce) = ArrowIterator.configure(schema, index, ds.stats, filter.filter, ecql, hints)
201+
val (iter, reduce) = ArrowIterator.configure(schema, index, ds.stats, strategy.filter.filter, ecql, hints)
201202
(Seq(iter), new AccumuloArrowResultsToFeatures(), Some(reduce))
202203
} else {
203204
if (hints.isSkipReduce) {
@@ -207,7 +208,7 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
207208
}
208209
(fti, resultsToFeatures, localReducer)
209210
}
210-
} else if (strategy.hints.isDensityQuery) {
211+
} else if (hints.isDensityQuery) {
211212
if (ds.config.remote.density) {
212213
val iter = DensityIterator.configure(schema, index, ecql, hints)
213214
(Seq(iter), new AccumuloDensityResultsToFeatures(), None)
@@ -219,7 +220,7 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
219220
}
220221
(fti, resultsToFeatures, localReducer)
221222
}
222-
} else if (strategy.hints.isStatsQuery) {
223+
} else if (hints.isStatsQuery) {
223224
if (ds.config.remote.stats) {
224225
val iter = StatsIterator.configure(schema, index, ecql, hints)
225226
val reduce = Some(StatsScan.StatsReducer(schema, hints))
@@ -236,7 +237,7 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
236237
(fti, resultsToFeatures, None)
237238
}
238239

239-
if (ranges.isEmpty) { EmptyPlan(strategy.filter, reduce) } else {
240+
if (ranges.isEmpty) { EmptyPlan(strategy, reduce) } else {
240241
// configure additional iterators based on the index
241242
// TODO pull this out to be SPI loaded so that new indices can be added seamlessly
242243
val indexIter = if (index.name == Z3Index.name) {
@@ -272,7 +273,7 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
272273
val max = hints.getMaxFeatures
273274
val project = hints.getProjection
274275

275-
BatchScanPlan(filter, tables, ranges, iters, colFamily, eToF, reduce, sort, max, project, numThreads)
276+
BatchScanPlan(strategy, tables, ranges, iters, colFamily, eToF, reduce, sort, max, project, numThreads)
276277
}
277278
}
278279
}
@@ -290,12 +291,56 @@ class AccumuloIndexAdapter(ds: AccumuloDataStore)
290291
case (true, true) => new AccumuloAtomicIndexWriter(ds, sft, indices, wrapper, partition) with RequiredVisibilityWriter
291292
}
292293
}
294+
295+
override def getStrategyCost(strategy: FilterStrategy, explain: Explainer): Option[Long] = {
296+
explain.pushLevel(s"Calculating cost for ${strategy.index.identifier}")
297+
val start = System.currentTimeMillis()
298+
try {
299+
val tables = strategy.index.getTablesForQuery(strategy.filter)
300+
if (tables.isEmpty) {
301+
return Some(0L)
302+
}
303+
val ranges = strategy.getQueryStrategy(explain).ranges.map(toAccumuloRange).asJava
304+
val cost =
305+
tables.foldLeft(0d) { case (sum, table) =>
306+
val numTablets = tableSizeCache.get(table)
307+
val tabletsScanned = tableOps.locate(table, ranges).groupByTablet().size()
308+
explain(s"Strategy hits $tabletsScanned/$numTablets tablets for table $table")
309+
val cost = 100 * (tabletsScanned.toDouble / numTablets)
310+
sum + cost
311+
}
312+
Some(cost.toLong)
313+
} finally {
314+
explain(s"Cost calculations took ${System.currentTimeMillis() - start}ms").popLevel()
315+
}
316+
}
293317
}
294318

295319
object AccumuloIndexAdapter {
296320

297321
val ZIterPriority = 23
298322

323+
/**
324+
* Converts a generic index-api range into an Accumulo range
325+
*
326+
* @param range range
327+
* @return
328+
*/
329+
private def toAccumuloRange(range: ByteRange): Range = range match {
330+
case BoundedByteRange(lower, upper) =>
331+
// index api defines empty start/end for open-ended range - in accumulo, it's indicated with null
332+
val start = if (lower.length == 0) { null } else { new Key(new Text(lower)) }
333+
val end = if (upper.length == 0) { null } else { new Key(new Text(upper)) }
334+
// index api defines start row inclusive, end row exclusive
335+
new Range(start, true, end, false)
336+
337+
case SingleRowByteRange(row) =>
338+
new Range(new Text(row))
339+
340+
case _ =>
341+
throw new IllegalArgumentException(s"Unexpected range type $range")
342+
}
343+
299344
/**
300345
* Accumulo entries to features
301346
*

geomesa-accumulo/geomesa-accumulo-datastore/src/main/scala/org/locationtech/geomesa/accumulo/data/AccumuloJoinIndexAdapter.scala

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ object AccumuloJoinIndexAdapter {
6262
def createQueryPlan(
6363
ds: AccumuloDataStore,
6464
index: AttributeJoinIndex,
65-
filter: FilterStrategy,
65+
strategy: QueryStrategy,
6666
tables: Seq[String],
6767
ranges: Seq[org.apache.accumulo.core.data.Range],
6868
colFamily: Option[Text],
@@ -85,7 +85,7 @@ object AccumuloJoinIndexAdapter {
8585
val sort = hints.getSortFields
8686
val max = hints.getMaxFeatures
8787
val project = hints.getProjection
88-
BatchScanPlan(filter, tables, ranges, iterators, colFamily, kvsToFeatures, reduce, sort, max, project, numThreads)
88+
BatchScanPlan(strategy, tables, ranges, iterators, colFamily, kvsToFeatures, reduce, sort, max, project, numThreads)
8989
}
9090

9191
// used when remote processing is disabled
@@ -115,13 +115,13 @@ object AccumuloJoinIndexAdapter {
115115
}
116116
} else {
117117
// have to do a join against the record table
118-
createJoinPlan(ds, index, filter, tables, ranges, colFamily, ecql, hints)
118+
createJoinPlan(ds, index, strategy, tables, ranges, colFamily, ecql, hints)
119119
}
120120
} else if (hints.isArrowQuery) {
121121
// check to see if we can execute against the index values
122122
if (index.canUseIndexSchema(ecql, transform)) {
123123
if (ds.config.remote.arrow) {
124-
val (arrowIter, reduce) = ArrowIterator.configure(indexSft, index, ds.stats, filter.filter, ecql, hints)
124+
val (arrowIter, reduce) = ArrowIterator.configure(indexSft, index, ds.stats, strategy.filter.filter, ecql, hints)
125125
plan(Seq(arrowIter), new AccumuloArrowResultsToFeatures(), Some(reduce))
126126
} else {
127127
localPlan()
@@ -147,7 +147,7 @@ object AccumuloJoinIndexAdapter {
147147
}
148148
} else {
149149
// have to do a join against the record table
150-
createJoinPlan(ds, index, filter, tables, ranges, colFamily, ecql, hints)
150+
createJoinPlan(ds, index, strategy, tables, ranges, colFamily, ecql, hints)
151151
}
152152
} else if (hints.isDensityQuery) {
153153
// check to see if we can execute against the index values
@@ -183,7 +183,7 @@ object AccumuloJoinIndexAdapter {
183183
}
184184
} else {
185185
// have to do a join against the record table
186-
createJoinPlan(ds, index, filter, tables, ranges, colFamily, ecql, hints)
186+
createJoinPlan(ds, index, strategy, tables, ranges, colFamily, ecql, hints)
187187
}
188188
} else if (hints.isStatsQuery) {
189189
// check to see if we can execute against the index values
@@ -197,7 +197,7 @@ object AccumuloJoinIndexAdapter {
197197
}
198198
} else {
199199
// have to do a join against the record table
200-
createJoinPlan(ds, index, filter, tables, ranges, colFamily, ecql, hints)
200+
createJoinPlan(ds, index, strategy, tables, ranges, colFamily, ecql, hints)
201201
}
202202
} else if (index.canUseIndexSchema(ecql, transform)) {
203203
// we can use the index value
@@ -222,10 +222,10 @@ object AccumuloJoinIndexAdapter {
222222
plan(iters, toFeatures, None)
223223
} else {
224224
// have to do a join against the record table
225-
createJoinPlan(ds, index, filter, tables, ranges, colFamily, ecql, hints)
225+
createJoinPlan(ds, index, strategy, tables, ranges, colFamily, ecql, hints)
226226
}
227227

228-
if (ranges.nonEmpty) { qp } else { EmptyPlan(qp.filter, qp.reducer) }
228+
if (ranges.nonEmpty) { qp } else { EmptyPlan(strategy, qp.reducer) }
229229
}
230230

231231
/**
@@ -235,7 +235,7 @@ object AccumuloJoinIndexAdapter {
235235
private def createJoinPlan(
236236
ds: AccumuloDataStore,
237237
index: AttributeJoinIndex,
238-
filter: FilterStrategy,
238+
strategy: QueryStrategy,
239239
tables: Seq[String],
240240
ranges: Seq[org.apache.accumulo.core.data.Range],
241241
colFamily: Option[Text],
@@ -280,7 +280,7 @@ object AccumuloJoinIndexAdapter {
280280
hints.put(QueryHints.Internal.RETURN_SFT, resultSft)
281281
}
282282

283-
val recordTables = recordIndex.getTablesForQuery(filter.filter)
283+
val recordTables = recordIndex.getTablesForQuery(strategy.filter.filter)
284284
val recordThreads = ds.config.queries.recordThreads
285285

286286
// function to join the attribute index scan results to the record table
@@ -294,13 +294,13 @@ object AccumuloJoinIndexAdapter {
294294
}
295295
}
296296

297-
val joinQuery = BatchScanPlan(filter, recordTables, Seq.empty, recordIterators, recordColFamily, toFeatures,
297+
val joinQuery = BatchScanPlan(strategy, recordTables, Seq.empty, recordIterators, recordColFamily, toFeatures,
298298
Some(reducer), hints.getSortFields, hints.getMaxFeatures, hints.getProjection, recordThreads)
299299

300300
val attributeIters = visibilityIter(index) ++
301301
FilterTransformIterator.configure(index.indexSft, index, stFilter, None, hints.getSampling).toSeq
302302

303-
JoinPlan(filter, tables, ranges, attributeIters, colFamily, recordThreads, joinFunction, joinQuery)
303+
JoinPlan(strategy, tables, ranges, attributeIters, colFamily, recordThreads, joinFunction, joinQuery)
304304
}
305305

306306
private def visibilityIter(index: AttributeJoinIndex): Seq[IteratorSetting] = {

geomesa-accumulo/geomesa-accumulo-datastore/src/main/scala/org/locationtech/geomesa/accumulo/data/AccumuloQueryPlan.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import org.apache.accumulo.core.security.Authorizations
1515
import org.apache.hadoop.io.Text
1616
import org.locationtech.geomesa.accumulo.util.BatchMultiScanner
1717
import org.locationtech.geomesa.index.api.QueryPlan.{FeatureReducer, ResultsToFeatures}
18-
import org.locationtech.geomesa.index.api.{FilterStrategy, QueryPlan}
18+
import org.locationtech.geomesa.index.api.{QueryPlan, QueryStrategy}
1919
import org.locationtech.geomesa.index.utils.Explainer
2020
import org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems
2121
import org.locationtech.geomesa.index.utils.ThreadManagement.{LowLevelScanner, ManagedScan, Timeout}
@@ -79,7 +79,7 @@ object AccumuloQueryPlan extends LazyLogging {
7979
Key.toPrintableString(k.getRow.getBytes, 0, k.getRow.getLength, k.getRow.getLength)
8080

8181
// plan that will not actually scan anything
82-
case class EmptyPlan(filter: FilterStrategy, reducer: Option[FeatureReducer] = None) extends AccumuloQueryPlan {
82+
case class EmptyPlan(strategy: QueryStrategy, reducer: Option[FeatureReducer] = None) extends AccumuloQueryPlan {
8383
override val tables: Seq[String] = Seq.empty
8484
override val iterators: Seq[IteratorSetting] = Seq.empty
8585
override val ranges: Seq[org.apache.accumulo.core.data.Range] = Seq.empty
@@ -94,7 +94,7 @@ object AccumuloQueryPlan extends LazyLogging {
9494

9595
// batch scan plan
9696
case class BatchScanPlan(
97-
filter: FilterStrategy,
97+
strategy: QueryStrategy,
9898
tables: Seq[String],
9999
ranges: Seq[org.apache.accumulo.core.data.Range],
100100
iterators: Seq[IteratorSetting],
@@ -108,6 +108,8 @@ object AccumuloQueryPlan extends LazyLogging {
108108
) extends AccumuloQueryPlan {
109109

110110
override def scan(ds: AccumuloDataStore): CloseableIterator[Entry[Key, Value]] = {
111+
// query guard hook - also handles full table scan checks
112+
strategy.runGuards(ds)
111113
// convert the relative timeout to an absolute timeout up front
112114
val timeout = ds.config.queries.timeout.map(Timeout.apply)
113115
// note: calculate authorizations up front so that multi-threading doesn't mess up auth providers
@@ -123,7 +125,7 @@ object AccumuloQueryPlan extends LazyLogging {
123125
* @param timeout absolute stop time, as sys time
124126
* @return
125127
*/
126-
def scan(
128+
private[accumulo] def scan(
127129
connector: AccumuloClient,
128130
auths: Authorizations,
129131
partitionParallelScans: Boolean,
@@ -162,7 +164,7 @@ object AccumuloQueryPlan extends LazyLogging {
162164

163165
// join on multiple tables - requires multiple scans
164166
case class JoinPlan(
165-
filter: FilterStrategy,
167+
strategy: QueryStrategy,
166168
tables: Seq[String],
167169
ranges: Seq[org.apache.accumulo.core.data.Range],
168170
iterators: Seq[IteratorSetting],
@@ -180,6 +182,8 @@ object AccumuloQueryPlan extends LazyLogging {
180182
override def projection: Option[QueryReferenceSystems] = joinQuery.projection
181183

182184
override def scan(ds: AccumuloDataStore): CloseableIterator[Entry[Key, Value]] = {
185+
// query guard hook - also handles full table scan checks
186+
strategy.runGuards(ds)
183187
// convert the relative timeout to an absolute timeout up front
184188
val timeout = ds.config.queries.timeout.map(Timeout.apply)
185189
// calculate authorizations up front so that multi-threading doesn't mess up auth providers

0 commit comments

Comments
 (0)