Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Configuration Experience for Multiple CacheManagers #33282

Closed
Crain-32 opened this issue Jul 26, 2024 · 5 comments
Closed

Improve Configuration Experience for Multiple CacheManagers #33282

Crain-32 opened this issue Jul 26, 2024 · 5 comments
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@Crain-32
Copy link

The automatic configuration of a cache is amazing for the simple cases, however quickly becomes weird if you decide to use multiple CacheManager instances.

For example you might want to have a Distributed Cache using Redis, and a Local Caffeine or ConcurrentMap Cache for small items, you'd need the following configuration.

@Configuration
@EnableCaching
public CachingConfig implements CachingConfigurer, ApplicationContextAware {
    private ApplicationContext ac;
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
      // Implementing Redis Cache
    }
    @Bean
    public CacheManager caffeineCache(CacheProperties cacheProperties) {
    // Implementing Caffeine Cache
    }
    
    @Override
    @Bean
    public CacheResolver cacheResolver() {
        var redisCache = ac.getBean("redisCacheManager", CacheManager.class);
        var caffeineCache = ac.getBean("caffeineCache", CacheManager.class);
        // Implement Resolver
    }
    @Override
    public KeyGenerator keyGenerator() {
        return new SimpleKeyGenerator();
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ac= applicationContext;
    }
}

From a user perspective there are some parts that are a little verbose compared to other configurations. Highlights would be bringing in the ApplicationContext, but the class really doesn't care for it, implementing the KeyGenerator using the same default Spring does (per CachingConfigurer Docs), and then not overriding CachingConfigurer::cacheManager() for a mixture of reasons.

I personally I think Caching could use a more "Default unless Declared" approach, where we could reduce this down to the following.

@Configuration
@EnableCaching
public CachingConfig  {
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
     // Implementing Redis Cache
    }

    @Bean
    public CacheManager caffeineCache(CacheProperties cacheProperties) {
    // Implementing Caffeine Cache
    }
    
    @Bean
    public CacheResolver cacheResolver(
        @Qualifier("redisCacheManager") CacheManager redis,
        @Qualifier("caffeineCache") CacheManager caffeine
        ) {
        // Implement Resolver
    }
}

This removes the KeyGenerator, swaps ApplicationContext::getBean with Parameter Injection, and removes the CachingConfigurer in favor of global bean detection. What other adjustments would the Spring Boot Team be open to?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 26, 2024
@snicoll
Copy link
Member

snicoll commented Jul 27, 2024

The automatic configuration of a cache is amazing for the simple cases, however quickly becomes weird if you decide to use multiple CacheManager instances.

It's a common topic in Spring Boot where users enjoy the automated behavior for configuring the infrastructure but it doesn't expand to multiple instances of the same type, see spring-projects/spring-boot#15732.

From a user perspective there are some parts that are a little verbose compared to other configurations. Highlights would be bringing in the ApplicationContext, but the class really doesn't care for it, implementing the KeyGenerator using the same default Spring does (per CachingConfigurer Docs), and then not overriding CachingConfigurer::cacheManager() for a mixture of reasons.

There's a mix of things here. First of all, you don't have to override keyGenerator. Then a CacheResolver is all that's required to give a cache based on a name. From the pure perspective of the cache infrastructure, a cache resolver is all that's required. You can override cacheManager if you want but it will be superseded (per javadoc).

The problem with that stuff is that the infrastructure has to configure early, to be able to wrap things correctly. In your example above, the two cache managers don't need to be technically bean (albeit declaring them as such makes sure that callbacks are honored, typically on shutdown). Having any infrastructure declaring such types is a call for early initialization I am afraid.

I can see how the contract of the contributor makes it harder than it should.

I wonder if the following would work:

@Configuration
class CacheManagersConfig  {
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory, CacheProperties props) {
     // Implementing Redis Cache
    }

    @Bean
    public CacheManager caffeineCache(CacheProperties cacheProperties) {
    // Implementing Caffeine Cache
    }
}
@Configuration
@Import(CacheManagersConfig.class)
@EnableCaching
class CachingConfig implements CachingConfigurer {

    private final ObjectProvider<CacheManager> cacheManagers;

    CachingConfig(ObjectProvider<CacheManager> cacheManagers) { ... }

    @Override
    @Bean
    public CacheResolver cacheResolver() {
        // Implement Resolver
    }
}

(note I've used ObjectProvider to be concise but you can do the qualifier injection as well).

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Jul 27, 2024
@Crain-32
Copy link
Author

First of all, you don't have to override keyGenerator

The @EnableCaching Javadoc could use a minor update then.

@EnableCaching will configure Spring's SimpleKeyGenerator for this purpose, but when implementing CachingConfigurer, a key generator must be provided explicitly.

Threw me off, even though the next line does state

Return null or new SimpleKeyGenerator() from this method if no customization is necessary

Returning null feels incorrect when stating a key generator must be provided explicitly. Maybe the function can be changed to the following?

public interface CachingConfigurer {

    @Nullable
    default KeyGenerator keyGenerator() { return new SimpleKeyGenerator(); }
    
}

Although now that conflicts with the @Bean Requirement.

Going back to the CacheResolver and some thoughts on it,

From the pure perspective of the cache infrastructure, a cache resolver is all that's required

This is reminding me a bit of the following Spring Security Issue. Although a CacheResolver is all that is required, the current documentation points towards using CacheManager, and a CacheManager is really a CacheResolver that only uses the Operation's Cache Name to resolve, along with a helper function.

That approach makes sense with this section's note in Spring Boot Docs,

Similarly to key and keyGenerator, the cacheManager and cacheResolver parameters are mutually exclusive.

Maybe this adjust should be made instead?

public interface CacheManager {

      Collection<? extends Cache> resolveCaches(CacheOperationInvovationContext<?> context);
      
      Collection<String> getCacheNames();
}

Then using the ConcurrentMapCacheManager as an example.

public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
      @Override
      public Collection<? extends Cache> resolveCaches(CacheOperationInvovationContext<?> context) {
            Collection<String> cacheNames = context.getOperation().getCacheNames();
            // Unsure if the empty collection check should be in here or the caller
            return cacheNames.stream().map(this::getCache).filter(Objects::nonNull).toList();
      }
      
      @Nullable
      private Cache getCache(String name) {
            Cache cache = this.cacheMap.get(name);
            if (cache == null && this.dynamic) {
                  cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache);
            }
            return cache;
      }
}

This feels easier to work with, since there is a single contract to work with. Power users could still used named CacheManager instances as well for special handling.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jul 27, 2024
@snicoll snicoll transferred this issue from spring-projects/spring-boot Jul 27, 2024
@snicoll
Copy link
Member

snicoll commented Jul 28, 2024

Can we focus on the issue as reported please? I'll have a look to the Javadoc and see what needs to be updated but I think I've provided a fairly good route for what you claim is a problem. Please give that a try and report if that works for you.

@snicoll snicoll added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jul 28, 2024
@Crain-32
Copy link
Author

Sorry about that. To make things worse I also realized while writing the reply this was Spring Framework's Repository, not Spring Boot's 🤦🏻‍♂️. Since the behavior falls into the Cache Auto Configuration, not the Cache Configuration, I'm going to review the CacheAutoConfiguration some more and move the CacheResolver bean only working in a CachingConfigurer implementation to Boot, as that likely stems from there. I'll pop back up in with another issue once I feel like I really understand the CacheResolver vs CacheManager

Your solution does work, thank you.

@snicoll snicoll closed this as not planned Won't fix, can't repro, duplicate, stale Jul 29, 2024
@snicoll snicoll added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged or decided on labels Jul 29, 2024
@snicoll
Copy link
Member

snicoll commented Jul 29, 2024

Javadoc has been polished in #33288. The requirement mismatch for KeyGenerator is because that part of the doc was written before we could use default methods in interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

3 participants