Skip to content

Commit 1a12d25

Browse files
committed
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
1 parent 5d1c9e5 commit 1a12d25

3 files changed

Lines changed: 281 additions & 1 deletion

File tree

documentation/en/user/source/production/index.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ These applicaiton properties can be established by any of the following ways, in
9797
- As a System environment variable: `export GWC_SEED_ABORT_LIMIT=2000; <your usual command to run GWC here>` (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`
9898

9999

100+
Seeder Thread Pool Size
101+
+++++++++++++++++++++++
102+
103+
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 using the following properties:
104+
105+
* ``GWC_SEEDER_CORE_POOL_SIZE`` : the number of threads to keep in the pool, even if they are idle. Defaults to ``16``.
106+
* ``GWC_SEEDER_MAX_POOL_SIZE`` : the maximum number of threads allowed in the pool. Defaults to ``32``.
107+
108+
These properties are resolved at startup in the following order of precedence:
109+
110+
- As a Java system property: for example ``java -DGWC_SEEDER_CORE_POOL_SIZE=24 -DGWC_SEEDER_MAX_POOL_SIZE=64 ...``
111+
- As a System environment variable: ``export GWC_SEEDER_CORE_POOL_SIZE=24``
112+
113+
If the value is not set or is not a valid positive integer, the default is used. Invalid values are logged as warnings.
114+
115+
If ``GWC_SEEDER_CORE_POOL_SIZE`` is set to a value greater than ``GWC_SEEDER_MAX_POOL_SIZE``, the maximum pool size will be automatically adjusted upward to match the core pool size.
116+
117+
.. note::
118+
Unlike the seed failure tolerance settings above, these properties 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.
119+
120+
Increasing these values is useful when seeding many layers in parallel, especially in combination with a larger HTTP connection pool to the backend WMS.
121+
122+
100123
Resource Allocation
101124
-------------------
102125

geowebcache/core/src/main/java/org/geowebcache/seed/SeederThreadPoolExecutor.java

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,108 @@ public class SeederThreadPoolExecutor extends ThreadPoolExecutor implements Disp
2828

2929
private static final ThreadFactory tf = new CustomizableThreadFactory("GWC Seeder Thread-");
3030

31+
/**
32+
* Environment variable / system property name for configuring the core pool size. Looked up from Java system
33+
* properties first, then from OS environment variables. If neither is set or the value is not a valid positive
34+
* integer, the {@code corePoolSize} constructor argument is used as the default.
35+
*/
36+
public static final String GWC_SEEDER_CORE_POOL_SIZE = "GWC_SEEDER_CORE_POOL_SIZE";
37+
38+
/**
39+
* Environment variable / system property name for configuring the maximum pool size. Looked up from Java system
40+
* properties first, then from OS environment variables. If neither is set or the value is not a valid positive
41+
* integer, the {@code maxPoolSize} constructor argument is used as the default.
42+
*/
43+
public static final String GWC_SEEDER_MAX_POOL_SIZE = "GWC_SEEDER_MAX_POOL_SIZE";
44+
45+
/**
46+
* Holds the resolved pool sizes computed once during construction. This avoids resolving each property multiple
47+
* times (which would cause duplicate log messages) while working within the constraint that {@code super()} must be
48+
* the first statement in the constructor.
49+
*/
50+
private static final ThreadLocal<int[]> RESOLVED_SIZES = new ThreadLocal<>();
51+
3152
public SeederThreadPoolExecutor(int corePoolSize, int maxPoolSize) {
32-
super(corePoolSize, maxPoolSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), tf);
53+
super(
54+
resolveAndCacheSizes(corePoolSize, maxPoolSize)[0],
55+
getCachedMaxPoolSize(),
56+
60,
57+
TimeUnit.SECONDS,
58+
new LinkedBlockingQueue<>(),
59+
tf);
60+
RESOLVED_SIZES.remove();
61+
log.info("Seeder thread pool initialized with corePoolSize="
62+
+ getCorePoolSize()
63+
+ ", maxPoolSize="
64+
+ getMaximumPoolSize());
65+
}
66+
67+
/**
68+
* Resolves both pool sizes once, validates the core <= max constraint, caches the result in a ThreadLocal for the
69+
* second argument to {@code super()}, and returns the array.
70+
*/
71+
private static int[] resolveAndCacheSizes(int defaultCore, int defaultMax) {
72+
int core = resolvePoolSize(GWC_SEEDER_CORE_POOL_SIZE, defaultCore);
73+
int max = resolvePoolSize(GWC_SEEDER_MAX_POOL_SIZE, defaultMax);
74+
if (core > max) {
75+
log.warning(GWC_SEEDER_CORE_POOL_SIZE
76+
+ " ("
77+
+ core
78+
+ ") is greater than "
79+
+ GWC_SEEDER_MAX_POOL_SIZE
80+
+ " ("
81+
+ max
82+
+ "), adjusting maxPoolSize to match corePoolSize");
83+
max = core;
84+
}
85+
int[] sizes = {core, max};
86+
RESOLVED_SIZES.set(sizes);
87+
return sizes;
88+
}
89+
90+
/** Retrieves the cached max pool size computed by {@link #resolveAndCacheSizes}. */
91+
private static int getCachedMaxPoolSize() {
92+
return RESOLVED_SIZES.get()[1];
93+
}
94+
95+
/**
96+
* Resolves a pool size configuration value by looking up the given property name first as a Java system property,
97+
* then as an OS environment variable. Falls back to the provided default if neither is set or the value is not a
98+
* valid positive integer.
99+
*
100+
* @param propertyName the system property / environment variable name to look up
101+
* @param defaultValue the fallback value if the property is not set or invalid
102+
* @return the resolved pool size
103+
*/
104+
static int resolvePoolSize(String propertyName, int defaultValue) {
105+
String value = System.getProperty(propertyName);
106+
if (value == null || value.isBlank()) {
107+
value = System.getenv(propertyName);
108+
}
109+
if (value != null && !value.isBlank()) {
110+
try {
111+
int parsed = Integer.parseInt(value.trim());
112+
if (parsed > 0) {
113+
log.info("Using configured value for " + propertyName + "=" + parsed);
114+
return parsed;
115+
} else {
116+
log.warning("Invalid value for "
117+
+ propertyName
118+
+ "="
119+
+ value
120+
+ " (must be a positive integer), using default "
121+
+ defaultValue);
122+
}
123+
} catch (NumberFormatException e) {
124+
log.warning("Invalid value for "
125+
+ propertyName
126+
+ "="
127+
+ value
128+
+ " (not a valid integer), using default "
129+
+ defaultValue);
130+
}
131+
}
132+
return defaultValue;
33133
}
34134

35135
/**
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*/
12+
package org.geowebcache.seed;
13+
14+
import static org.junit.Assert.assertEquals;
15+
16+
import org.geowebcache.util.PropertyRule;
17+
import org.junit.Rule;
18+
import org.junit.Test;
19+
20+
public class SeederThreadPoolExecutorTest {
21+
22+
@Rule
23+
public PropertyRule corePoolSizeProp = PropertyRule.system(SeederThreadPoolExecutor.GWC_SEEDER_CORE_POOL_SIZE);
24+
25+
@Rule
26+
public PropertyRule maxPoolSizeProp = PropertyRule.system(SeederThreadPoolExecutor.GWC_SEEDER_MAX_POOL_SIZE);
27+
28+
@Test
29+
public void testDefaultValues() {
30+
// No system property set, should use the constructor-provided defaults
31+
corePoolSizeProp.setValue(null);
32+
maxPoolSizeProp.setValue(null);
33+
34+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
35+
try {
36+
assertEquals(16, executor.getCorePoolSize());
37+
assertEquals(32, executor.getMaximumPoolSize());
38+
} finally {
39+
executor.shutdownNow();
40+
}
41+
}
42+
43+
@Test
44+
public void testSystemPropertyOverride() {
45+
corePoolSizeProp.setValue("24");
46+
maxPoolSizeProp.setValue("64");
47+
48+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
49+
try {
50+
assertEquals(24, executor.getCorePoolSize());
51+
assertEquals(64, executor.getMaximumPoolSize());
52+
} finally {
53+
executor.shutdownNow();
54+
}
55+
}
56+
57+
@Test
58+
public void testInvalidValueFallsBackToDefault() {
59+
corePoolSizeProp.setValue("invalid");
60+
maxPoolSizeProp.setValue("notanumber");
61+
62+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
63+
try {
64+
assertEquals(16, executor.getCorePoolSize());
65+
assertEquals(32, executor.getMaximumPoolSize());
66+
} finally {
67+
executor.shutdownNow();
68+
}
69+
}
70+
71+
@Test
72+
public void testNegativeValueFallsBackToDefault() {
73+
corePoolSizeProp.setValue("-5");
74+
maxPoolSizeProp.setValue("0");
75+
76+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
77+
try {
78+
assertEquals(16, executor.getCorePoolSize());
79+
assertEquals(32, executor.getMaximumPoolSize());
80+
} finally {
81+
executor.shutdownNow();
82+
}
83+
}
84+
85+
@Test
86+
public void testPartialOverride() {
87+
// Only override core pool size, leave max at default
88+
corePoolSizeProp.setValue("20");
89+
maxPoolSizeProp.setValue(null);
90+
91+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
92+
try {
93+
assertEquals(20, executor.getCorePoolSize());
94+
assertEquals(32, executor.getMaximumPoolSize());
95+
} finally {
96+
executor.shutdownNow();
97+
}
98+
}
99+
100+
@Test
101+
public void testWhitespaceValueFallsBackToDefault() {
102+
corePoolSizeProp.setValue(" ");
103+
maxPoolSizeProp.setValue("");
104+
105+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
106+
try {
107+
assertEquals(16, executor.getCorePoolSize());
108+
assertEquals(32, executor.getMaximumPoolSize());
109+
} finally {
110+
executor.shutdownNow();
111+
}
112+
}
113+
114+
@Test
115+
public void testValueWithWhitespaceIsTrimmed() {
116+
corePoolSizeProp.setValue(" 24 ");
117+
maxPoolSizeProp.setValue(" 48 ");
118+
119+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
120+
try {
121+
assertEquals(24, executor.getCorePoolSize());
122+
assertEquals(48, executor.getMaximumPoolSize());
123+
} finally {
124+
executor.shutdownNow();
125+
}
126+
}
127+
128+
@Test
129+
public void testCorePoolSizeExceedsMaxPoolSizeAdjustsMax() {
130+
// Set core higher than max — max should be adjusted upward to match core
131+
corePoolSizeProp.setValue("64");
132+
maxPoolSizeProp.setValue("32");
133+
134+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
135+
try {
136+
assertEquals(64, executor.getCorePoolSize());
137+
assertEquals(64, executor.getMaximumPoolSize());
138+
} finally {
139+
executor.shutdownNow();
140+
}
141+
}
142+
143+
@Test
144+
public void testCorePoolSizeOverrideExceedsDefaultMax() {
145+
// Only override core to be higher than the default max — max should adjust
146+
corePoolSizeProp.setValue("48");
147+
maxPoolSizeProp.setValue(null);
148+
149+
SeederThreadPoolExecutor executor = new SeederThreadPoolExecutor(16, 32);
150+
try {
151+
assertEquals(48, executor.getCorePoolSize());
152+
assertEquals(48, executor.getMaximumPoolSize());
153+
} finally {
154+
executor.shutdownNow();
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)