AppConfig.java

package com.taxonomy;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.taxonomy.analysis.service.LlmService;

@Configuration
public class AppConfig {

    /**
     * Exposes the HTTP request factory as a bean so that {@code LlmService} can update
     * the read timeout at runtime when the {@code llm.timeout.seconds} preference changes.
     *
     * <p>The initial value is taken from the {@code taxonomy.llm.timeout-seconds} property
     * (default: 30 seconds). The value may be overridden at runtime via the Preferences API.
     */
    @Bean
    public SimpleClientHttpRequestFactory llmRequestFactory(
            @Value("${taxonomy.llm.timeout-seconds:30}") int timeoutSeconds) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(10_000);
        factory.setReadTimeout(timeoutSeconds * 1000);
        return factory;
    }

    @Bean
    public RestTemplate restTemplate(SimpleClientHttpRequestFactory llmRequestFactory) {
        return new RestTemplate(llmRequestFactory);
    }

    @Bean(destroyMethod = "shutdown")
    public ExecutorService analysisExecutor() {
        // Scale to available CPUs; cap at 4 to avoid over-threading on constrained hosts
        // (e.g. Render Free Tier has 1 CPU — uses 2 threads, which is still useful for I/O waits)
        int poolSize = Math.min(Runtime.getRuntime().availableProcessors() + 1, 4);
        return Executors.newFixedThreadPool(poolSize);
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("taxonomy-async-");
        executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}