spring-bootcluster-computingwartraefiktomcat9

Spring boot Tomcat session replication with Traefik


I'm trying to setup session replication using Spring boot with Traefik. I've found how it can be achieved with Tomcat and its server.xml file in the following link: Tomcat session replication in docker swarm.

My whole app looks like the one in this repository https://github.com/trajano/tomcat-docker-swarm (compose-file, JSP page etc).

What is different is that I'm using following dockerfile:

FROM tomcat:10-jdk17-openjdk-slim

WORKDIR /app

COPY build/libs/demo-0.0.1-SNAPSHOT.war app.war

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/app/app.war"]

However, I'm not able to get it working using spring boot with embedded Tomcat. I've also found this post which shows how to set it up with embedded tomcat: How to setup Tomcat Session Replication with Spring Boot embedded tomcat, but it uses StaticMembershipInterceptor and I was unable to get it working using CloudMembershipService.

My current configuration class looks following:

@Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCust() {
        return factory -> {
            factory.addContextCustomizers(tomcatContextCutomizer());
        };
    }
    private TomcatContextCustomizer tomcatContextCutomizer() {
        return context -> {

            DeltaManager deltaManager = new DeltaManager();
            deltaManager.setNotifyListenersOnReplication(true);
            deltaManager.setExpireSessionsOnShutdown(false);

            context.setManager(deltaManager);

            SimpleTcpCluster simpleTcpCluster = new SimpleTcpCluster();

            simpleTcpCluster.registerManager(deltaManager);

            GroupChannel channel = new GroupChannel();

            NioReceiver receiver = new NioReceiver();
            channel.setChannelReceiver(receiver);

            ReplicationTransmitter sender = new ReplicationTransmitter();
            sender.setTransport(new PooledParallelSender());

            channel.setChannelSender(sender);

            channel.addInterceptor(new TcpPingInterceptor());
            channel.addInterceptor(new TcpFailureDetector());
            channel.addInterceptor(new MessageDispatchInterceptor());


            simpleTcpCluster.addValve(new ReplicationValve());
            simpleTcpCluster.addValve(new JvmRouteBinderValve());
            simpleTcpCluster.addClusterListener(new ClusterSessionListener());

            CloudMembershipService clms = new CloudMembershipService();

            channel.setUtilityExecutor(new ScheduledThreadPoolExecutor(10));

            clms.setChannel(channel);
            channel.setMembershipService(clms);

            MembershipProvider mp = new DNSMembershipProvider();
            clms.setMembershipProvider(mp);

            mp.setMembershipService(channel.getMembershipService());

            simpleTcpCluster.setChannel(channel);

            Host host = (Host)context.getParent();
            host.setCluster(simpleTcpCluster);
        };
    }

The result of this code is that I'm able to see that the containers see each other because of the log message:

CloudMembershipProvider  : Member added: org.apache.catalina.tribes.membership.MemberImpl[tcp://1.2.3.4:0,1.2.3.4,0, alive=-1, securePort=-1, UDP Port=-1, id={1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 }, payload={}, command={}, domain={}]`.<br><br>
But the session never get's replicated. Some logs implying what may be happening are: <br>
`DeltaManager     : Manager []: skipping state transfer. No members active in cluster group. <br>

or
ReplicationValve : Cluster is standalone: reset Session Request Delta at context [].

Using context.setDistributable(true) didn't help.

I'm using Spring boot 2.7.18 and respective Tomcat version 9.0.83.


Solution

  • I found the issue. I will keep the question for anyone who wants to have out of the box configuration for session replication.

    You shouldn't create new MembershipProvider, instead you need to specify name of the provider.

    The adjusted config is:

      DeltaManager deltaManager = new DeltaManager();
      deltaManager.setNotifyListenersOnReplication(true);
      deltaManager.setExpireSessionsOnShutdown(false);
      context.setManager(deltaManager);
    
      SimpleTcpCluster simpleTcpCluster = new SimpleTcpCluster();
    
      simpleTcpCluster.registerManager(deltaManager);
    
      GroupChannel channel = new GroupChannel();
    
      NioReceiver receiver = new NioReceiver();
      channel.setChannelReceiver(receiver);
    
      ReplicationTransmitter sender = new ReplicationTransmitter();
      sender.setTransport(new PooledParallelSender());
    
      channel.setChannelSender(sender);
    
      channel.addInterceptor(new TcpPingInterceptor());
      channel.addInterceptor(new TcpFailureDetector());
      channel.addInterceptor(new MessageDispatchInterceptor());
    
    
      simpleTcpCluster.addValve(new ReplicationValve());
      simpleTcpCluster.addValve(new JvmRouteBinderValve());
      simpleTcpCluster.addClusterListener(new ClusterSessionListener());
    
      CloudMembershipService clms = new CloudMembershipService();
    
      channel.setUtilityExecutor(new ScheduledThreadPoolExecutor(10));
    
      clms.setChannel(channel);
      channel.setMembershipService(clms);
    
      clms.setMembershipProviderClassName(DNSMembershipProvider.class.getName());
    
    
      simpleTcpCluster.setChannel(channel);
    
      Engine engine = (Engine)context.getParent().getParent();
      engine.setCluster(simpleTcpCluster);
    };
    
      clms.setMembershipProviderClassName(DNSMembershipProvider.class.getName());
    
    
      simpleTcpCluster.setChannel(channel);
    
      Engine engine = (Engine)context.getParent().getParent();
      engine.setCluster(simpleTcpCluster);