This is an automated email from the ASF dual-hosted git repository. jooger pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push: new 97ac8dcfa78 IGNITE-26155 Expire of cache of SQL plans (#6467) 97ac8dcfa78 is described below commit 97ac8dcfa78a54f5ee46882beb42d6d2bfb2399b Author: Max Zhuravkov <shh...@gmail.com> AuthorDate: Wed Aug 27 11:15:06 2025 +0300 IGNITE-26155 Expire of cache of SQL plans (#6467) --- .../configuration/ignite-snapshot.bin | Bin 5503 -> 5511 bytes .../SqlPlannerDistributedConfigurationSchema.java | 7 +++ .../sql/engine/prepare/PrepareServiceImpl.java | 5 +- .../sql/engine/util/cache/CacheFactory.java | 14 ++++++ .../engine/util/cache/CaffeineCacheFactory.java | 10 ++++ .../sql/engine/exec/ExecutionServiceImplTest.java | 9 ++++ .../sql/engine/framework/TestBuilders.java | 2 + .../sql/engine/planner/PlannerTimeoutTest.java | 13 ++++- .../sql/engine/prepare/PrepareServiceImplTest.java | 54 ++++++++++++++++++++- .../sql/engine/util/EmptyCacheFactory.java | 6 +++ .../sql/metrics/PlanningCacheMetricsTest.java | 2 +- 11 files changed, 117 insertions(+), 5 deletions(-) diff --git a/modules/runner/src/test/resources/compatibility/configuration/ignite-snapshot.bin b/modules/runner/src/test/resources/compatibility/configuration/ignite-snapshot.bin index 7e4e530f8d2..503adbf67df 100644 Binary files a/modules/runner/src/test/resources/compatibility/configuration/ignite-snapshot.bin and b/modules/runner/src/test/resources/compatibility/configuration/ignite-snapshot.bin differ diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/configuration/distributed/SqlPlannerDistributedConfigurationSchema.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/configuration/distributed/SqlPlannerDistributedConfigurationSchema.java index 80d31c7a24e..b37ea213d23 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/configuration/distributed/SqlPlannerDistributedConfigurationSchema.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/configuration/distributed/SqlPlannerDistributedConfigurationSchema.java @@ -38,4 +38,11 @@ public class SqlPlannerDistributedConfigurationSchema { @Value(hasDefault = true) @Range(min = 0) public final int estimatedNumberOfQueries = 1024; + + /** + * The number of seconds after which a query plan is removed from the query plan cache if it is not used. + */ + @Value(hasDefault = true) + @Range(min = 0) + public final int planCacheExpiresAfterSeconds = 30 * 60; } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java index 3d2c3ae4622..16a7cfcce28 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java @@ -26,6 +26,7 @@ import static org.apache.ignite.internal.sql.engine.util.Commons.fastQueryOptimi import static org.apache.ignite.internal.thread.ThreadOperation.NOTHING_ALLOWED; import static org.apache.ignite.lang.ErrorGroups.Sql.EXECUTION_CANCELLED_ERR; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -164,6 +165,7 @@ public class PrepareServiceImpl implements PrepareService { cacheFactory, ddlSqlToCommandConverter, clusterCfg.planner().maxPlanningTimeMillis().value(), + clusterCfg.planner().planCacheExpiresAfterSeconds().value(), nodeCfg.planner().threadCount().value(), metricManager, schemaManager @@ -188,6 +190,7 @@ public class PrepareServiceImpl implements PrepareService { DdlSqlToCommandConverter ddlConverter, long plannerTimeout, int plannerThreadCount, + int planExpirySeconds, MetricManager metricManager, SqlSchemaManager schemaManager ) { @@ -199,7 +202,7 @@ public class PrepareServiceImpl implements PrepareService { this.schemaManager = schemaManager; sqlPlanCacheMetricSource = new SqlPlanCacheMetricSource(); - cache = cacheFactory.create(cacheSize, sqlPlanCacheMetricSource); + cache = cacheFactory.create(cacheSize, sqlPlanCacheMetricSource, Duration.ofSeconds(planExpirySeconds)); } /** {@inheritDoc} */ diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CacheFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CacheFactory.java index 4d4c35df5bc..d1e28046b0a 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CacheFactory.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CacheFactory.java @@ -17,6 +17,8 @@ package org.apache.ignite.internal.sql.engine.util.cache; +import java.time.Duration; + /** * Factory that creates a cache. */ @@ -41,4 +43,16 @@ public interface CacheFactory { * @param <V> Type of the value object. */ <K, V> Cache<K, V> create(int size, StatsCounter statCounter); + + /** + * Creates a cache of the required size, controls whether statistics should be collected, and specifies entry expiration time. + * + * @param size Desired size of the cache. + * @param statCounter Cache statistic accumulator. + * @param expireAfterAccess Duration to use for expiration. + * @return An instance of the cache. + * @param <K> Type of the key object. + * @param <V> Type of the value object. + */ + <K, V> Cache<K, V> create(int size, StatsCounter statCounter, Duration expireAfterAccess); } diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CaffeineCacheFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CaffeineCacheFactory.java index 4b4c01824cb..55e9e8b0767 100644 --- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CaffeineCacheFactory.java +++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/cache/CaffeineCacheFactory.java @@ -20,6 +20,7 @@ package org.apache.ignite.internal.sql.engine.util.cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.stats.CacheStats; +import java.time.Duration; import java.util.concurrent.Executor; import java.util.function.BiFunction; import java.util.function.Function; @@ -54,6 +55,11 @@ public class CaffeineCacheFactory implements CacheFactory { /** {@inheritDoc} */ @Override public <K, V> Cache<K, V> create(int size, StatsCounter statCounter) { + return create(size, statCounter, null); + } + + @Override + public <K, V> Cache<K, V> create(int size, StatsCounter statCounter, Duration expireAfterAccess) { Caffeine<Object, Object> builder = Caffeine.newBuilder() .maximumSize(size); @@ -65,6 +71,10 @@ public class CaffeineCacheFactory implements CacheFactory { builder.recordStats(() -> new CaffeineStatsCounterAdapter(statCounter)); } + if (expireAfterAccess != null) { + builder.expireAfterAccess(expireAfterAccess); + } + return new CaffeineCacheToCacheAdapter<>(builder.build()); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java index 9b907e3ff18..6a23a9bace5 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java @@ -48,6 +48,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -187,6 +188,8 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { public static final int PLANNING_THREAD_COUNT = 2; + public static final int PLAN_EXPIRATION_SECONDS = Integer.MAX_VALUE; + /** Timeout in ms for stopping execution service. */ private static final long SHUTDOWN_TIMEOUT = 5_000; @@ -256,6 +259,7 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { converter, PLANNING_TIMEOUT, PLANNING_THREAD_COUNT, + PLAN_EXPIRATION_SECONDS, metricManager, new PredefinedSchemaManager(schema) ); @@ -1465,6 +1469,11 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest { throw new UnsupportedOperationException(); } + @Override + public <K, V> Cache<K, V> create(int size, StatsCounter statCounter, Duration expireAfterAccess) { + throw new UnsupportedOperationException(); + } + private static class BlockOnComputeCache<K, V> extends EmptyCacheFactory.EmptyCache<K, V> { private final CountDownLatch waitLatch; diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java index ed64c9f9720..85c5a6e50c5 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java @@ -21,6 +21,7 @@ import static java.util.UUID.randomUUID; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.apache.ignite.internal.lang.IgniteStringFormatter.format; import static org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImplTest.PLANNING_THREAD_COUNT; +import static org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImplTest.PLAN_EXPIRATION_SECONDS; import static org.apache.ignite.internal.testframework.IgniteTestUtils.await; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty; @@ -752,6 +753,7 @@ public class TestBuilders { new DdlSqlToCommandConverter(storageProfiles -> completedFuture(null)), planningTimeout, PLANNING_THREAD_COUNT, + PLAN_EXPIRATION_SECONDS, new NoOpMetricManager(), schemaManager ); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTimeoutTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTimeoutTest.java index 7b1713b8a86..94f20b76755 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTimeoutTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/PlannerTimeoutTest.java @@ -67,8 +67,17 @@ public class PlannerTimeoutTest extends AbstractPlannerTest { IgniteSchema schema = createSchema(createTestTable("T1")); SqlOperationContext ctx = operationContext(); - PrepareService prepareService = new PrepareServiceImpl("test", 0, - CaffeineCacheFactory.INSTANCE, null, plannerTimeout, 1, new MetricManagerImpl(), new PredefinedSchemaManager(schema)); + PrepareService prepareService = new PrepareServiceImpl( + "test", + 0, + CaffeineCacheFactory.INSTANCE, + null, + plannerTimeout, + 1, + Integer.MAX_VALUE, + new MetricManagerImpl(), + new PredefinedSchemaManager(schema) + ); prepareService.start(); try { ParserService parserService = new ParserServiceImpl(); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java index f5875d3845a..b6a7b9038fc 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java @@ -34,12 +34,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import java.time.Duration; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.ignite.internal.hlc.HybridClockImpl; @@ -69,6 +71,7 @@ import org.apache.ignite.lang.ErrorGroups.Sql; import org.apache.ignite.sql.ColumnMetadata; import org.apache.ignite.sql.ColumnType; import org.apache.ignite.sql.SqlException; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -318,6 +321,11 @@ public class PrepareServiceImplTest extends BaseIgniteAbstractTest { public <K, V> Cache<K, V> create(int size, StatsCounter statCounter) { return (Cache<K, V>) cache; } + + @Override + public <K, V> Cache<K, V> create(int size, StatsCounter statCounter, Duration expireAfterAccess) { + return (Cache<K, V>) cache; + } }; PrepareServiceImpl service = createPlannerService(schema, cacheFactory, 100); @@ -367,6 +375,41 @@ public class PrepareServiceImplTest extends BaseIgniteAbstractTest { )); } + @Test + public void planCacheExpiry() { + IgniteTable table = TestBuilders.table() + .name("T") + .addColumn("C", NativeTypes.INT32) + .distribution(IgniteDistributions.single()) + .build(); + + IgniteSchema schema = new IgniteSchema("TEST", 0, List.of(table)); + + Awaitility.await().timeout(30, TimeUnit.SECONDS).untilAsserted(() -> { + int expireSeconds = 2; + PrepareService service = createPlannerService(schema, CaffeineCacheFactory.INSTANCE, Integer.MAX_VALUE, 2); + + String query = "SELECT * FROM test.t WHERE c = 1"; + QueryPlan p0 = await(service.prepareAsync(parse(query), operationContext().build())); + + // Expires if not used + TimeUnit.SECONDS.sleep(expireSeconds * 2); + QueryPlan p2 = await(service.prepareAsync(parse(query), operationContext().build())); + assertNotSame(p0, p2); + + // Returns the previous plan + TimeUnit.MILLISECONDS.sleep(500); + + QueryPlan p3 = await(service.prepareAsync(parse(query), operationContext().build())); + assertSame(p2, p3); + + // Eventually expires + TimeUnit.SECONDS.sleep(expireSeconds * 2); + QueryPlan p4 = await(service.prepareAsync(parse(query), operationContext().build())); + assertNotSame(p4, p3); + }); + } + private static Stream<Arguments> parameterTypes() { int noScale = ColumnMetadata.UNDEFINED_SCALE; int noPrecision = ColumnMetadata.UNDEFINED_PRECISION; @@ -429,8 +472,17 @@ public class PrepareServiceImplTest extends BaseIgniteAbstractTest { } private static PrepareServiceImpl createPlannerService(IgniteSchema schemas, CacheFactory cacheFactory, int timeoutMillis) { + return createPlannerService(schemas, cacheFactory, timeoutMillis, Integer.MAX_VALUE); + } + + private static PrepareServiceImpl createPlannerService( + IgniteSchema schemas, + CacheFactory cacheFactory, + int timeoutMillis, + int planExpireSeconds + ) { PrepareServiceImpl service = new PrepareServiceImpl("test", 1000, cacheFactory, - mock(DdlSqlToCommandConverter.class), timeoutMillis, 2, mock(MetricManagerImpl.class), + mock(DdlSqlToCommandConverter.class), timeoutMillis, 2, planExpireSeconds, mock(MetricManagerImpl.class), new PredefinedSchemaManager(schemas)); createdServices.add(service); diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/EmptyCacheFactory.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/EmptyCacheFactory.java index afa7efdb535..b2e00a9f74f 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/EmptyCacheFactory.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/EmptyCacheFactory.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.sql.engine.util; +import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -50,6 +51,11 @@ public class EmptyCacheFactory implements CacheFactory { return create(size); } + @Override + public <K, V> Cache<K, V> create(int size, StatsCounter statCounter, Duration expireAfterAccess) { + return create(size); + } + /** A cache that keeps no object. */ public static class EmptyCache<K, V> implements Cache<K, V> { @Override diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java index ec92e77b180..5cb1bd0d4b2 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java @@ -62,7 +62,7 @@ public class PlanningCacheMetricsTest extends AbstractPlannerTest { IgniteSchema schema = createSchema(table); PrepareService prepareService = new PrepareServiceImpl( - "test", 2, cacheFactory, null, 15_000L, 2, metricManager, new PredefinedSchemaManager(schema) + "test", 2, cacheFactory, null, 15_000L, 2, Integer.MAX_VALUE, metricManager, new PredefinedSchemaManager(schema) ); prepareService.start();