Skip to content

DefaultListableBeanFactory.getBean(Class) may throw NoSuchBeanDefinitionException when removeBeanDefinition is being called simultaneously to remove an unrelated bean #22263

@waded

Description

@waded

We're seeing sporadic NoSuchBeanDefinitionException exceptions out of ApplicationContext's getBean(Class) when in another thread DefaultListableBeanFactory.removeBeanDefinition(String) is being called, for unrelated/different beans. It appears to be a synchronization issue in DefaultListableBeanFactory's implementation (across use of fields beanDefinitionMap and beanDefinitionNames.) I have included a Spring Boot application that reproduces the issue. Having more beans seems to exacerbate the issue, I presume because it increases time to copy beanDefinitionNames in removeBeanDefinition.

We're seeing this with spring-beans-5.1.4 and 5.1.3, unknown about older versions than that.

Here's an unrealistic application that reproduces the condition, which we ran with spring-boot-starter-parent 2.1.2.RELEASE (spring-beans-5.1.4):

package com.example.demo;

import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
@EnableScheduling
@SpringBootApplication
public class DemoApplication implements ApplicationContextAware, SchedulingConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    private ApplicationContext context;

    public static class Volatile { }

    @Scheduled(fixedRate = 400)
    public void addAndRemove() {

        BeanDefinitionRegistry factory = (BeanDefinitionRegistry)context.getAutowireCapableBeanFactory();

        // Simulate add/remove of some beans in one background thread.
        // Using larger numbers here makes the exception increasingly easier to hit in get().
        for (int i = 0; i < 1000; i++) {
            String beanName = "volatile" + i;
            if (factory.containsBeanDefinition(beanName)) {
                factory.removeBeanDefinition(beanName);
            }
            factory.registerBeanDefinition(beanName, BeanDefinitionBuilder.genericBeanDefinition(Volatile.class).getBeanDefinition());
        }
    }

    public static class Stable { }

    @Bean
    public Stable stable()
    {
        return new Stable();
    }

    @Scheduled(fixedRate = 1)
    public void get() {
        try {
            // Here get a bean that is not the one(s) being added/removed. Expect to be able to get
            // it every time.
            context.getBean(Stable.class);

        } catch (NoSuchBeanDefinitionException e) {

            // Eventually NoSuchBeanDefinitionException occurs (the missing bean being one of the Volatile ones!)
            // In DefaultListableBeanFactory.removeBeanDefinition the map is modified, then it starts replacing
            // the list w/ new copy. Meanwhile in getBean it iterates across that list in doGetBeanNamesForType,
            // but then gets from the map. They're not in the map anymore.

            throw new RuntimeException("This is the problem", e);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        // It's necessary to have get() and addAndRemove running on separate threads
        threadPoolTaskScheduler.setPoolSize(2);
        threadPoolTaskScheduler.initialize();
        scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions