springspring-boothazelcastevent-listener

Use EventListener causing circular dependency with repository


I am tring to use EntryExpiredListener to do an action using another service once cache entry expired, but I couldn't add the listener to the cache if it is calling a service, and the service is calling any repositry in the system

The dependencies of some of the beans in the application context form a cycle:

   filterChainExceptionHandler (field private org.springframework.web.servlet.HandlerExceptionResolver sb.practice.configs.security.filters.FilterChainExceptionHandler.resolver)
      ↓
   org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration
      ↓
   openEntityManagerInViewInterceptorConfigurer defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$JpaWebConfiguration.class]
      ↓
   openEntityManagerInViewInterceptor defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$JpaWebConfiguration.class]
┌─────┐
|  cacheManager defined in class path resource [org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.class]
↑     ↓
|  cacheService defined in file [D:\Dev\Locals\pt-spring-boot\target\classes\sb\practice\services\impl\CacheService.class]
↑     ↓
|  activeUserCacheService_Hazelcast defined in file [D:\Dev\Locals\pt-spring-boot\target\classes\sb\practice\services\impl\caches\ActiveUserCacheService_Hazelcast.class]
↑     ↓
|  masterRoleRepository defined in sb.practice.repositories.masters.MasterRoleRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration
↑     ↓
|  (inner bean)#28f48a7c
└─────┘

This is my CacheListener:

@Slf4j
public class CacheListener implements EntryExpiredListener<String, String>{
    
    public ActiveUserCacheService_Hazelcast activeUsersCache;

    @Override
    public void entryExpired(EntryEvent<String, String> event) {
        log.info("entryEvicted. Cache: {}. Value: {}. \nEvent: {} ", event.getName(), event.getOldValue(), event);
    }
}

CacheService:

@Service
@RequiredArgsConstructor
public class CacheService {

    
    @Value("${hazelcast.cache.cluster.nodes}")
    String[] clusteringNodes;
    
    private final ActiveUserCacheService_Hazelcast activeUserCacheService_Hazelcast;

    public <T> boolean containsKey(long key, String cacheName) {
        IMap<Long, T> map = hazelcastInstance().getMap(cacheName);
        return map.containsKey(key);
    }

    private static MapConfig mapConfig(String name, Integer timeToLive) {

        MapConfig mapConfig = new MapConfig(name);
        mapConfig.setTimeToLiveSeconds(timeToLive);
//
//      EntryListenerConfig entryListenerConfig = new EntryListenerConfig();
//      entryListenerConfig.setImplementation(new CacheListener());
//      mapConfig.addEntryListenerConfig(entryListenerConfig);

        final EvictionConfig evictionConfig = new EvictionConfig();
        evictionConfig.setEvictionPolicy(EvictionPolicy.LRU);
        evictionConfig.setMaxSizePolicy(MaxSizePolicy.PER_NODE);
        evictionConfig.setSize(500);

        mapConfig.setEvictionConfig(evictionConfig);
        return mapConfig;
    }

    @Bean
    HazelcastInstance hazelcastInstance() {

        Config config = new Config();
        CacheConstants.CACHES.forEach((name, timeToLive) -> config.addMapConfig(mapConfig(name, timeToLive)));

        NetworkConfig network = config.getNetworkConfig();
        network.setPort(5701).setPortCount(20);
        network.setPortAutoIncrement(true);

        JoinConfig join = network.getJoin();
        join.getMulticastConfig().setEnabled(false);
        join.getTcpIpConfig().setEnabled(true);

        for (String clusteringNode : clusteringNodes) {
            join.getTcpIpConfig().addMember(clusteringNode);
        }

        return Hazelcast.newHazelcastInstance(config);
    }

}

The service is only containing an injection to masterRoleRepository whick cuase the issue. Is there a way to use the EventListener as a component and call a service to do logic?

Thank you in advance!


Solution

  • If you have a config like this you can create the listener as bean

    @Configuration
    @EnableAsync
    public class HazelcastInstanceConfiguration {
    
      public static final String MAP_NAME = "myMap";
    
      @Bean
      public Config hazelcastConfig() {
        Config config = new Config();
    
        // Configure the map
        MapConfig mapConfig = config.getMapConfig(MAP_NAME);
        mapConfig.setTimeToLiveSeconds(30);
    
        EntryListenerConfig entryListenerConfig = new EntryListenerConfig();
        entryListenerConfig.setImplementation(cacheListener());
        mapConfig.addEntryListenerConfig(entryListenerConfig);
    
        return config;
      }
    
      @Bean
      CacheListener cacheListener() {
        return new CacheListener();
      }
    }
    

    In the listener inject the service with a setter

    @Slf4j
    @RequiredArgsConstructor
    public class CacheListener implements EntryExpiredListener<String, String> {
    
      private MyService myService;
    
      @Autowired
      public void setMyService(MyService myService) {
        this.myService = myService;
      }
    
      @Override
      public void entryExpired(EntryEvent<String, String> event) {
        log.info("entryEvicted. Cache: {}. Value: {}. \nEvent: {} ", 
          event.getName(), event.getOldValue(), event);
        myService.deleteFromRepository(event.getKey());
      }
    }
    

    Make the service async in order not to block the hazelcast thread

    @Service
    @Slf4j
    public class MyService {
    
      @Async
      public void deleteFromRepository(String key) {
        log.info("deleteFromRepository is called for key : {}", key);
      }
    }