diff --git a/src/main/java/io/ebean/config/ServerConfig.java b/src/main/java/io/ebean/config/ServerConfig.java index f2d41b0ea..e5784b14c 100644 --- a/src/main/java/io/ebean/config/ServerConfig.java +++ b/src/main/java/io/ebean/config/ServerConfig.java @@ -435,6 +435,11 @@ public class ServerConfig { private String jodaLocalTimeMode; + /** + * Time to live for query plans - defaults to 5 minutes. + */ + private int queryPlanTTLSeconds = 60 * 5; + /** * Set to true to globally disable L2 caching (typically for performance testing). */ @@ -2595,6 +2600,7 @@ public class ServerConfig { dbTypeConfig.setGeometrySRID(srid); } + queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds); slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis); docStoreOnly = p.getBoolean("docStoreOnly", docStoreOnly); disableL2Cache = p.getBoolean("disableL2Cache", disableL2Cache); @@ -2817,6 +2823,20 @@ public class ServerConfig { this.disableL2Cache = disableL2Cache; } + /** + * Return the query plan time to live. + */ + public int getQueryPlanTTLSeconds() { + return queryPlanTTLSeconds; + } + + /** + * Set the query plan time to live. + */ + public void setQueryPlanTTLSeconds(int queryPlanTTLSeconds) { + this.queryPlanTTLSeconds = queryPlanTTLSeconds; + } + /** * Run the DB migration against the DataSource. */ diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 81d681208..9215b8883 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -151,6 +151,7 @@ public class InternalConfiguration { this.beanDescriptorManager = new BeanDescriptorManager(this); Map asOfTableMapping = beanDescriptorManager.deploy(); Map draftTableMap = beanDescriptorManager.getDraftTableMap(); + beanDescriptorManager.scheduleBackgroundTrim(); this.dataTimeZone = initDataTimeZone(); this.binder = getBinder(typeManager, databasePlatform, dataTimeZone); diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 3469bb0c5..17cd7fc45 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -89,6 +89,7 @@ import java.sql.Types; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -1530,6 +1531,24 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { } } + /** + * Trim query plans not used since the passed in epoch time. + */ + public List trimQueryPlans(long unusedSince) { + + List list = new ArrayList<>(); + + Iterator it = queryPlanCache.values().iterator(); + while (it.hasNext()) { + CQueryPlan queryPlan = it.next(); + if (queryPlan.getLastQueryTime() < unusedSince) { + it.remove(); + list.add(queryPlan); + } + } + return list; + } + /** * Execute the postLoad if a BeanPostLoad exists for this bean. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 72d29e20d..81df08c31 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -49,6 +49,7 @@ import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.properties.BeanPropertiesReader; import io.ebeaninternal.server.properties.BeanPropertyAccess; import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess; +import io.ebeaninternal.server.query.CQueryPlan; import io.ebeaninternal.xmlmapping.XmlMappingReader; import io.ebeaninternal.xmlmapping.model.XmAliasMapping; import io.ebeaninternal.xmlmapping.model.XmColumnMapping; @@ -80,6 +81,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -190,6 +192,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { */ private final Map draftTableMap = new HashMap<>(); + private final int queryPlanTTLSeconds; + /** * Create for a given database dbConfig. */ @@ -207,6 +211,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.multiValueBind = config.getMultiValueBind(); this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm(), multiValueBind); this.eagerFetchLobs = serverConfig.isEagerFetchLobs(); + this.queryPlanTTLSeconds = serverConfig.getQueryPlanTTLSeconds(); this.asOfViewSuffix = getAsOfViewSuffix(databasePlatform, serverConfig); String versionsBetweenSuffix = getVersionsBetweenSuffix(databasePlatform, serverConfig); @@ -236,6 +241,25 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.changeLogRegister = config.changeLogRegister(bootupClasses.getChangeLogRegister()); } + /** + * Run periodic trim of query plans. + */ + public void scheduleBackgroundTrim() { + backgroundExecutor.executePeriodically(this::trimQueryPlans, 30L, TimeUnit.SECONDS); + } + + private void trimQueryPlans() { + long lastUsed = System.currentTimeMillis() - (queryPlanTTLSeconds * 1000L); + for (BeanDescriptor descriptor : immutableDescriptorList) { + if (!descriptor.isEmbedded()) { + List trimmedPlans = descriptor.trimQueryPlans(lastUsed); + if (!trimmedPlans.isEmpty()) { + logger.trace("trimmed {} query plans for type:{}", trimmedPlans.size(), descriptor.getName()); + } + } + } + } + /** * Return the AsOfViewSuffix based on the DbHistorySupport. */ diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index 42cb13d81..5c2b84fb5 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -252,7 +252,7 @@ class CQueryBuilder { // skip building the SqlTree and Sql string predicates.prepare(false); String sql = queryPlan.getSql(); - return new CQueryRowCount(request, predicates, sql); + return new CQueryRowCount(queryPlan, request, predicates, sql); } predicates.prepare(true); @@ -283,7 +283,7 @@ class CQueryBuilder { queryPlan = new CQueryPlan(request, sql, sqlTree, false, s.isIncludesRowNumberColumn(), predicates.getLogWhereSql()); request.putQueryPlan(queryPlan); - return new CQueryRowCount(request, predicates, sql); + return new CQueryRowCount(queryPlan, request, predicates, sql); } /** diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java b/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java index c418086d1..82b1b0cd7 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java @@ -16,6 +16,8 @@ import java.sql.SQLException; */ class CQueryRowCount { + private final CQueryPlan queryPlan; + /** * The overall find request wrapper object. */ @@ -54,7 +56,8 @@ class CQueryRowCount { /** * Create the Sql select based on the request. */ - CQueryRowCount(OrmQueryRequest request, CQueryPredicates predicates, String sql) { + CQueryRowCount(CQueryPlan queryPlan, OrmQueryRequest request, CQueryPredicates predicates, String sql) { + this.queryPlan = queryPlan; this.request = request; this.query = request.getQuery(); this.sql = sql; @@ -116,6 +119,7 @@ class CQueryRowCount { rowCount = rset.getInt(1); executionTimeMicros = (System.nanoTime() - startNano) / 1000L; + queryPlan.executionTime(rowCount, executionTimeMicros, query.getParentNode()); request.slowQueryCheck(executionTimeMicros, rowCount); return rowCount; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index c032393b1..9dfce8911 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -88,7 +88,7 @@ - +