From b57d1f9ccac9ac4d33339b18a16ed42800f6532f Mon Sep 17 00:00:00 2001 From: Peter Smythe Date: Thu, 28 May 2026 17:03:19 +0200 Subject: [PATCH 1/2] Make seeder thread pool sizes configurable via system properties / env vars Allow GWC_SEEDER_CORE_POOL_SIZE and GWC_SEEDER_MAX_POOL_SIZE to be specified as Java system properties or OS environment variables, with system properties taking precedence. Falls back to the Spring XML constructor arguments (16/32) when not set or invalid. Invalid values (non-numeric, zero, negative) are logged as warnings and the defaults are used. If corePoolSize exceeds maxPoolSize after resolution, maxPoolSize is automatically adjusted upward to match. Includes unit tests and documentation updates. Closes #1429 --- .../en/user/source/production/index.rst | 37 +++++ .../config/GeoWebCacheConfiguration.java | 24 +++ .../config/ServerConfiguration.java | 19 +++ .../geowebcache/config/XMLConfiguration.java | 12 ++ .../seed/SeederThreadPoolExecutor.java | 84 +++++++++- .../core/src/main/resources/geowebcache.xml | 4 + .../org/geowebcache/config/geowebcache.xsd | 20 +++ .../seed/SeederThreadPoolExecutorTest.java | 157 ++++++++++++++++++ .../WEB-INF/geowebcache-core-context.xml | 4 +- 9 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 geowebcache/core/src/test/java/org/geowebcache/seed/SeederThreadPoolExecutorTest.java diff --git a/documentation/en/user/source/production/index.rst b/documentation/en/user/source/production/index.rst index 8d396d662..060fdfbaa 100644 --- a/documentation/en/user/source/production/index.rst +++ b/documentation/en/user/source/production/index.rst @@ -97,6 +97,43 @@ These applicaiton properties can be established by any of the following ways, in - As a System environment variable: `export GWC_SEED_ABORT_LIMIT=2000; ` (or for Tomcat, use the Tomcat's `CATALINA_OPTS` in Tomcat's `bin/catalina.sh` as this: `CATALINA_OPTS="GWC_SEED_ABORT_LIMIT=2000 GWC_SEED_RETRY_COUNT=2` +Seeder Thread Pool Size ++++++++++++++++++++++++ + +The seeder thread pool controls how many seeding threads can run concurrently. By default, the core pool size is ``16`` and the maximum pool size is ``32``. These can be configured in ``geowebcache.xml``: + +.. code-block:: xml + + + ... + 24 + 64 + ... + + +* ``seederCorePoolSize`` : the number of threads to keep in the pool, even if they are idle. Defaults to ``16``. +* ``seederMaxPoolSize`` : the maximum number of threads allowed in the pool. Defaults to ``32``. + +The configuration can be overridden at runtime using Java system properties or OS environment variables (useful for Docker and cloud deployments): + +* ``GWC_SEEDER_CORE_POOL_SIZE`` : overrides ``seederCorePoolSize`` from the XML configuration. +* ``GWC_SEEDER_MAX_POOL_SIZE`` : overrides ``seederMaxPoolSize`` from the XML configuration. + +These overrides are resolved in the following order of precedence: + +- As a Java system property: for example ``java -DGWC_SEEDER_CORE_POOL_SIZE=24 -DGWC_SEEDER_MAX_POOL_SIZE=64 ...`` +- As a System environment variable: ``export GWC_SEEDER_CORE_POOL_SIZE=24`` + +If the value is not set or is not a valid positive integer, the default is used. Invalid values are logged as warnings. + +If ``seederCorePoolSize`` (or its override) is set to a value greater than ``seederMaxPoolSize``, the maximum pool size will be automatically adjusted upward to match the core pool size. + +.. note:: + Unlike the seed failure tolerance settings above, the environment variable overrides are resolved directly by the Java code and do not support servlet context parameters in ``WEB-INF/web.xml``. Only Java system properties (``-D``) and OS environment variables are supported as overrides. + +Increasing these values is useful when seeding many layers in parallel, especially in combination with a larger HTTP connection pool to the backend WMS. + + Resource Allocation ------------------- diff --git a/geowebcache/core/src/main/java/org/geowebcache/config/GeoWebCacheConfiguration.java b/geowebcache/core/src/main/java/org/geowebcache/config/GeoWebCacheConfiguration.java index 1a4bcaf55..ff7487b52 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/config/GeoWebCacheConfiguration.java +++ b/geowebcache/core/src/main/java/org/geowebcache/config/GeoWebCacheConfiguration.java @@ -46,6 +46,10 @@ public class GeoWebCacheConfiguration { /* Default values */ private Integer backendTimeout; + private Integer seederCorePoolSize; + + private Integer seederMaxPoolSize; + private String lockProvider; private transient LockProvider lockProviderInstance; @@ -128,6 +132,26 @@ public void setBackendTimeout(Integer backendTimeout) { this.backendTimeout = backendTimeout; } + /** @see ServerConfiguration#getSeederCorePoolSize() */ + public Integer getSeederCorePoolSize() { + return seederCorePoolSize; + } + + /** @param seederCorePoolSize the core pool size for the seeder thread pool */ + public void setSeederCorePoolSize(Integer seederCorePoolSize) { + this.seederCorePoolSize = seederCorePoolSize; + } + + /** @see ServerConfiguration#getSeederMaxPoolSize() */ + public Integer getSeederMaxPoolSize() { + return seederMaxPoolSize; + } + + /** @param seederMaxPoolSize the maximum pool size for the seeder thread pool */ + public void setSeederMaxPoolSize(Integer seederMaxPoolSize) { + this.seederMaxPoolSize = seederMaxPoolSize; + } + /** @return the cacheBypassAllowed */ public Boolean getCacheBypassAllowed() { return cacheBypassAllowed; diff --git a/geowebcache/core/src/main/java/org/geowebcache/config/ServerConfiguration.java b/geowebcache/core/src/main/java/org/geowebcache/config/ServerConfiguration.java index 7503d152f..e9ae40543 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/config/ServerConfiguration.java +++ b/geowebcache/core/src/main/java/org/geowebcache/config/ServerConfiguration.java @@ -103,4 +103,23 @@ public interface ServerConfiguration extends BaseConfiguration { /** The version number should match the XSD namespace and the version of GWC */ String getVersion(); + + /** + * The core pool size for the seeder thread pool. This is the number of threads to keep in the pool, even if they + * are idle. + * + * @return the configured core pool size, or {@code null} if not set (defaults to 16) + */ + default Integer getSeederCorePoolSize() { + return null; + } + + /** + * The maximum pool size for the seeder thread pool. This is the maximum number of threads allowed in the pool. + * + * @return the configured max pool size, or {@code null} if not set (defaults to 32) + */ + default Integer getSeederMaxPoolSize() { + return null; + } } diff --git a/geowebcache/core/src/main/java/org/geowebcache/config/XMLConfiguration.java b/geowebcache/core/src/main/java/org/geowebcache/config/XMLConfiguration.java index 7c330fb78..381b78980 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/config/XMLConfiguration.java +++ b/geowebcache/core/src/main/java/org/geowebcache/config/XMLConfiguration.java @@ -1304,6 +1304,18 @@ public void setBackendTimeout(Integer backendTimeout) throws IOException { save(); } + /** @see ServerConfiguration#getSeederCorePoolSize() */ + @Override + public Integer getSeederCorePoolSize() { + return gwcConfig.getSeederCorePoolSize(); + } + + /** @see ServerConfiguration#getSeederMaxPoolSize() */ + @Override + public Integer getSeederMaxPoolSize() { + return gwcConfig.getSeederMaxPoolSize(); + } + /** @see ServerConfiguration#isCacheBypassAllowed() */ @Override public Boolean isCacheBypassAllowed() { diff --git a/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java b/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java index 508d97489..36942a55b 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java +++ b/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java @@ -28,8 +28,90 @@ public class SeederThreadPoolExecutor extends ThreadPoolExecutor implements Disp private static final ThreadFactory tf = new CustomizableThreadFactory("GWC Seeder Thread-"); + /** + * Environment variable / system property name for configuring the core pool size. Looked up from Java system + * properties first, then from OS environment variables. If neither is set or the value is not a valid positive + * integer, the {@code corePoolSize} constructor argument is used as the default. + */ + public static final String GWC_SEEDER_CORE_POOL_SIZE = "GWC_SEEDER_CORE_POOL_SIZE"; + + /** + * Environment variable / system property name for configuring the maximum pool size. Looked up from Java system + * properties first, then from OS environment variables. If neither is set or the value is not a valid positive + * integer, the {@code maxPoolSize} constructor argument is used as the default. + */ + public static final String GWC_SEEDER_MAX_POOL_SIZE = "GWC_SEEDER_MAX_POOL_SIZE"; + public SeederThreadPoolExecutor(int corePoolSize, int maxPoolSize) { - super(corePoolSize, maxPoolSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), tf); + this(resolveAndValidateSizes(corePoolSize, maxPoolSize)); + } + + private SeederThreadPoolExecutor(int[] sizes) { + super(sizes[0], sizes[1], 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), tf); + log.info("Seeder thread pool initialized with corePoolSize=" + + getCorePoolSize() + + ", maxPoolSize=" + + getMaximumPoolSize()); + } + + /** + * Resolves both pool sizes once and validates the core <= max constraint. Returns a two-element array [core, max]. + */ + private static int[] resolveAndValidateSizes(int defaultCore, int defaultMax) { + int core = resolvePoolSize(GWC_SEEDER_CORE_POOL_SIZE, defaultCore); + int max = resolvePoolSize(GWC_SEEDER_MAX_POOL_SIZE, defaultMax); + if (core > max) { + log.warning(GWC_SEEDER_CORE_POOL_SIZE + + " (" + + core + + ") is greater than " + + GWC_SEEDER_MAX_POOL_SIZE + + " (" + + max + + "), adjusting maxPoolSize to match corePoolSize"); + max = core; + } + return new int[] {core, max}; + } + + /** + * Resolves a pool size configuration value by looking up the given property name first as a Java system property, + * then as an OS environment variable. Falls back to the provided default if neither is set or the value is not a + * valid positive integer. + * + * @param propertyName the system property / environment variable name to look up + * @param defaultValue the fallback value if the property is not set or invalid + * @return the resolved pool size + */ + static int resolvePoolSize(String propertyName, int defaultValue) { + String value = System.getProperty(propertyName); + if (value == null || value.isBlank()) { + value = System.getenv(propertyName); + } + if (value != null && !value.isBlank()) { + try { + int parsed = Integer.parseInt(value.trim()); + if (parsed > 0) { + log.info("Using configured value for " + propertyName + "=" + parsed); + return parsed; + } else { + log.warning("Invalid value for " + + propertyName + + "=" + + value + + " (must be a positive integer), using default " + + defaultValue); + } + } catch (NumberFormatException e) { + log.warning("Invalid value for " + + propertyName + + "=" + + value + + " (not a valid integer), using default " + + defaultValue); + } + } + return defaultValue; } /** diff --git a/geowebcache/core/src/main/resources/geowebcache.xml b/geowebcache/core/src/main/resources/geowebcache.xml index 7804fb071..8b464be08 100644 --- a/geowebcache/core/src/main/resources/geowebcache.xml +++ b/geowebcache/core/src/main/resources/geowebcache.xml @@ -4,6 +4,10 @@ xsi:schemaLocation="http://geowebcache.org/schema/1.28.0 http://geowebcache.org/schema/1.28.0/geowebcache.xsd"> 1.8.0 120 + + + GeoWebCache GeoWebCache is an advanced tile cache for WMS servers. It supports a large variety of protocols and diff --git a/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd b/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd index bd9009b06..c73d04ea9 100644 --- a/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd +++ b/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd @@ -38,6 +38,26 @@ + + + + The number of threads to keep in the seeder thread pool, + even if they are idle. Defaults to 16. + Can be overridden by the GWC_SEEDER_CORE_POOL_SIZE + system property or environment variable. + + + + + + + The maximum number of threads allowed in the seeder + thread pool. Defaults to 32. + Can be overridden by the GWC_SEEDER_MAX_POOL_SIZE + system property or environment variable. + + + diff --git a/geowebcache/core/src/test/java/org/geowebcache/seed/SeederThreadPoolExecutorTest.java b/geowebcache/core/src/test/java/org/geowebcache/seed/SeederThreadPoolExecutorTest.java new file mode 100644 index 000000000..f06af5b16 --- /dev/null +++ b/geowebcache/core/src/test/java/org/geowebcache/seed/SeederThreadPoolExecutorTest.java @@ -0,0 +1,157 @@ +/** + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + *

You should have received a copy of the GNU Lesser General Public License along with this program. If not, see + * . + */ +package org.geowebcache.seed; + +import static org.junit.Assert.assertEquals; + +import org.geowebcache.util.PropertyRule; +import org.junit.Rule; +import org.junit.Test; + +public class SeederThreadPoolExecutorTest { + + @Rule + public PropertyRule corePoolSizeProp = PropertyRule.system(SeederThreadPoolExecutor.GWC_SEEDER_CORE_POOL_SIZE); + + @Rule + public PropertyRule maxPoolSizeProp = PropertyRule.system(SeederThreadPoolExecutor.GWC_SEEDER_MAX_POOL_SIZE); + + @Test + public void testDefaultValues() { + // No system property set, should use the constructor-provided defaults + corePoolSizeProp.setValue(null); + maxPoolSizeProp.setValue(null); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(16, executor.getCorePoolSize()); + assertEquals(32, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testSystemPropertyOverride() { + corePoolSizeProp.setValue("24"); + maxPoolSizeProp.setValue("64"); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(24, executor.getCorePoolSize()); + assertEquals(64, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testInvalidValueFallsBackToDefault() { + corePoolSizeProp.setValue("invalid"); + maxPoolSizeProp.setValue("notanumber"); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(16, executor.getCorePoolSize()); + assertEquals(32, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testNegativeValueFallsBackToDefault() { + corePoolSizeProp.setValue("-5"); + maxPoolSizeProp.setValue("0"); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(16, executor.getCorePoolSize()); + assertEquals(32, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testPartialOverride() { + // Only override core pool size, leave max at default + corePoolSizeProp.setValue("20"); + maxPoolSizeProp.setValue(null); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(20, executor.getCorePoolSize()); + assertEquals(32, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testWhitespaceValueFallsBackToDefault() { + corePoolSizeProp.setValue(" "); + maxPoolSizeProp.setValue(""); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(16, executor.getCorePoolSize()); + assertEquals(32, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testValueWithWhitespaceIsTrimmed() { + corePoolSizeProp.setValue(" 24 "); + maxPoolSizeProp.setValue(" 48 "); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(24, executor.getCorePoolSize()); + assertEquals(48, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testCorePoolSizeExceedsMaxPoolSizeAdjustsMax() { + // Set core higher than max — max should be adjusted upward to match core + corePoolSizeProp.setValue("64"); + maxPoolSizeProp.setValue("32"); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(64, executor.getCorePoolSize()); + assertEquals(64, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testCorePoolSizeOverrideExceedsDefaultMax() { + // Only override core to be higher than the default max — max should adjust + corePoolSizeProp.setValue("48"); + maxPoolSizeProp.setValue(null); + + SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32); + try { + assertEquals(48, executor.getCorePoolSize()); + assertEquals(48, executor.getMaximumPoolSize()); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-core-context.xml b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-core-context.xml index 43ac8eaf2..54843de9a 100644 --- a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-core-context.xml +++ b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-core-context.xml @@ -180,8 +180,8 @@ - - + + From 8e7c6245d99c0b79d823f1b1a614a55599fee6c7 Mon Sep 17 00:00:00 2001 From: Peter Smythe Date: Thu, 28 May 2026 19:38:53 +0200 Subject: [PATCH 2/2] Improve logging message Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../org/geowebcache/seed/SeederThreadPoolExecutor.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java b/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java index 36942a55b..cb3882b14 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java +++ b/geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java @@ -61,12 +61,9 @@ private static int[] resolveAndValidateSizes(int defaultCore, int defaultMax) { int core = resolvePoolSize(GWC_SEEDER_CORE_POOL_SIZE, defaultCore); int max = resolvePoolSize(GWC_SEEDER_MAX_POOL_SIZE, defaultMax); if (core > max) { - log.warning(GWC_SEEDER_CORE_POOL_SIZE - + " (" + log.warning("Configured corePoolSize (" + core - + ") is greater than " - + GWC_SEEDER_MAX_POOL_SIZE - + " (" + + ") is greater than maxPoolSize (" + max + "), adjusting maxPoolSize to match corePoolSize"); max = core;