Skip to content

Doc: LocaleContextHolder with inheritable only propagates to newly created threads #36687

@heyarny

Description

@heyarny

I noticed that the Locales when trying to use them inside threads are not consistent when using LocaleContextHolder.setLocale(Locale.GERMAN, true);.
It seems like the thread pools use the language it once was sent and uses it for all other requests.
You can reporduce it when using a threadpool of 1.

This is the Test I have:

@SpringBootTest
class AsyncContextPropagationTest {

    private ThreadPoolTaskExecutor delegate;
    private TaskExecutor taskExecutor;

    @BeforeEach
    void setUpExecutor() {
        delegate = new ThreadPoolTaskExecutor();
        delegate.setCorePoolSize(1);
        delegate.setMaxPoolSize(100);
        delegate.setQueueCapacity(50);
        delegate.setThreadNamePrefix("test-async-");
        delegate.initialize();

        taskExecutor = new DelegatingSecurityContextAsyncTaskExecutor(delegate);
    }

    @AfterEach
    void tearDownExecutor() {
        if (delegate != null) {
            delegate.shutdown();
        }
    }

    @Test
    void localeIsVisibleInAsyncTask() {
        LocaleContextHolder.setLocale(Locale.GERMAN, true);
        CompletableFuture<Locale> futureGerman = CompletableFuture.supplyAsync(
            LocaleContextHolder::getLocale,
            taskExecutor
        );
        var futureGermanResult = futureGerman.join();

        LocaleContextHolder.setLocale(Locale.ENGLISH, true);
        CompletableFuture<Locale> futureEnglish = CompletableFuture.supplyAsync(
            LocaleContextHolder::getLocale,
            taskExecutor
        );

        var futureEnglishResult = futureEnglish.join();

        assertThat(futureGermanResult).isEqualTo(Locale.GERMAN);
        assertThat(futureEnglishResult).isEqualTo(Locale.ENGLISH);
    }
}

Right now the workaround is to use an explicit TaskDecorator for that case to copy the locale to all child threads.

public class ContextCopyingDecorator implements TaskDecorator {

    @NonNull
    @Override
    public Runnable decorate(@NonNull Runnable runnable) {
        var locale = LocaleContextHolder.getLocale();

        return () -> { // code runs inside executor thread and binds context
            try {
                if (locale != null) {
                    LocaleContextHolder.setLocale(locale);
                }
                runnable.run();
            } finally {
                // Nothing to do.
            }
        };
    }
}

Then set it via delegate.setTaskDecorator(new ContextCopyingDecorator()); and it works just fine.

Not sure if the inheritable on setLocale() is intended to work like that or its simply a bug.
At least I don't know why it would work like that.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: documentationA documentation task

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions