MvcAsyncConfiguration.java

/*
 * Copyright (c) 2007-2026 MetaSolutions AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.entrystore.rest.springboot.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

/**
 * Configuration properties for the bounded MVC async-dispatch thread pool. Consumed by
 * {@code MvcConfiguration.configureAsyncSupport}, which uses these values to construct the
 * {@code ThreadPoolTaskExecutor} that Spring MVC uses for {@code StreamingResponseBody} and
 * {@code DeferredResult} dispatch (replacing Spring's default unbounded
 * {@code SimpleAsyncTaskExecutor}). Overflow beyond {@code maxPoolSize + queueCapacity} is
 * rejected via {@code AbortPolicy} and mapped to HTTP 503 by {@code AppExceptionHandler}.
 *
 * <p>Sizing rationale for {@code queueCapacity}: with {@code corePoolSize=8} and SPARQL
 * queries up to ~10 s, a request at queue position N waits ~N × 10 / 8 s before executing —
 * past position ~12 the wait exceeds {@code defaultTimeoutMs} and the request is doomed to
 * time out while still occupying a slot. Keeping the queue small lets overflow fail fast
 * (503) so the client can back off, instead of accepting work it cannot complete. The
 * default {@code 16} is therefore the authoritative documented-good value; an operator
 * deploying with a custom {@code application.yaml} that omits the {@code mvc.async} block
 * still gets the same behaviour.
 */
@ConfigurationProperties(prefix = "mvc.async")
public record MvcAsyncConfiguration(
		@DefaultValue("8") int corePoolSize,
		@DefaultValue("32") int maxPoolSize,
		@DefaultValue("16") int queueCapacity,
		@DefaultValue("15000") long defaultTimeoutMs
) {
	public MvcAsyncConfiguration {
		if (corePoolSize < 0) {
			throw new IllegalArgumentException(
					"mvc.async.core-pool-size must be >= 0, got " + corePoolSize);
		}
		if (maxPoolSize < 1 || maxPoolSize < corePoolSize) {
			throw new IllegalArgumentException(
					"mvc.async.max-pool-size must be >= max(1, core-pool-size), got " + maxPoolSize);
		}
		if (queueCapacity < 0) {
			throw new IllegalArgumentException(
					"mvc.async.queue-capacity must be >= 0, got " + queueCapacity);
		}
		if (defaultTimeoutMs <= 0) {
			throw new IllegalArgumentException(
					"mvc.async.default-timeout-ms must be > 0, got " + defaultTimeoutMs);
		}
	}
}