All Downloads are FREE. Search and download functionalities are using the official Maven repository.

estonlabs.cxtl.common.stream.managed.ManagedWsSession Maven / Gradle / Ivy

There is a newer version: 1.4.14
Show newest version
package estonlabs.cxtl.common.stream.managed;

import estonlabs.cxtl.common.DateFormatUtils;
import estonlabs.cxtl.common.codec.Codec;
import estonlabs.cxtl.common.stream.pojo.PojoWsSession;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

/**
 * This is a Managed WsSession. A managed session handles:
 *  - pings/pongs
 *  - auto authentication
 *  - auto resubscribe on disconnect
 *  - can be configured to check for a stale feed and reconnect if no data has been sent within a window
 * @param 
 * @param 
 */
public class ManagedWsSession extends PojoWsSession {
    private static final Logger LOGGER = LoggerFactory.getLogger(ManagedWsSession.class);

    // Create a SimpleDateFormat object to format the date as a string
    private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private final AtomicInteger pingsSinceLastPong = new AtomicInteger(0);
    private final AtomicInteger failedReconnects = new AtomicInteger(0);
    private final AtomicBoolean isReconnecting = new AtomicBoolean(false);
    private final AtomicBoolean isOpen = new AtomicBoolean(true);
    private Runnable ping;
    private Supplier createPong;
    private Supplier createLogin;
    private final List recoveryMessages = new CopyOnWriteArrayList<>();
    private long nextReconnectMs = 0;
    private final String id;
    private long pingWindow;
    private boolean expectPingResponse;
    private long staleWindow =-1;
    private long nextPing =0;
    private long lastReceived=0;

    private final Map timerDisposables = new ConcurrentHashMap<>();

    public ManagedWsSession(String id, Codec codec, Class inboundType) {
        super(codec, inboundType);
        this.id = id;
    }

    /**
     * Setting this will mean that the feed disconnects, reconnects and recovers subscriptions after this duration has elapsed.
     */
    public void enableRestartFeedAfter(long restartFeedAfter) {
        setTimer("restartFeedAfter", restartFeedAfter, this::restartFeed);
    }

    public void enableStaleFeedCheck(long staleFeedWindow){
        this.staleWindow = staleFeedWindow;
        setTimer("staleWindow", staleWindow, this::checkForStaleFeed);
    }

    public void enablePing(long pingWindow, Runnable ping,boolean expectPingResponse){
        this.pingWindow = pingWindow;
        this.ping = ping;
        this.expectPingResponse = expectPingResponse;
        setTimer("pingWindow", pingWindow, this::ping);
    }

    public void enablePing(long pingWindow, Supplier createPing){
        enablePing(pingWindow, ()->{
            SEND ping = createPing.get();
            if(underlyingSession.isOpen()){
                send(ping);
            }else{
                LOGGER.debug("{} Skipping ping, the underlying session is not open",id);
            }
        },true);
    }

    @Override
    public void dispose() {
        close();
        timerDisposables.values().forEach(Disposable::dispose);
        timerDisposables.clear();
        super.dispose();
    }

    public void enablePong(Supplier createPong){
        this.createPong = createPong;
    }

    public void enableLogin(Supplier createLogin){
        this.createLogin = createLogin;
    }

    @Override
    protected void processToSend(SEND toSend) {
        if(toSend.getMessageType().isRecoverable()){
            recoveryMessages.add(toSend);
        }
    }

    @Override
    protected boolean processReceived(RECEIVE received) {
        lastReceived = System.currentTimeMillis();
        InboundMessage.MessageType messageType = received.getMessageType();
        if(messageType == InboundMessage.MessageType.PONG){
            pingsSinceLastPong.set(0);
            LOGGER.debug("Received pong {}, ping count is now {}", received, pingsSinceLastPong);
        }else if(messageType == InboundMessage.MessageType.PING){
            if(createPong == null){
                LOGGER.error("Received ping {}, but no pong supplier provided.", received);
            }else{
                sendDirectly(createPong.get());
            }
            return false;
        }
        return messageType == InboundMessage.MessageType.DATA;
    }

    @Override
    public void close(){
        recoveryMessages.clear();
        isOpen.set(false);
        super.close();
        LOGGER.debug("{} Closed session session={}, isOpen={}",id, underlyingSession, isOpen.get());
    }

    public void restart() {
        //just calling close on the parent should trigger recovery
        super.close();
    }

    private void checkForStaleFeed() {
        long now = System.currentTimeMillis();
        long lastAcceptableMs = lastReceived + staleWindow;
        if(lastReceived>0 && lastAcceptableMs < now){

            LOGGER.error(" {} STALE FEED!!!  Will restart the feed: now={} vs last= {}",id,
                    DateFormatUtils.toTime(now),
                    DateFormatUtils.toTime(lastReceived));
            restartFeed();
        }
    }

    private void restartFeed() {
        try {
            LOGGER.debug("{} Restarting feed",id);
            restartConnection();
        }catch (Exception e){
            LOGGER.error("Error restarting feed {}, will try once more",id, e);
            restartConnection();
        }
    }

    private void ping() {
        if(underlyingSession == null || !isOpen.get()){
            LOGGER.debug("{} Skipping ping, the underlying session is not initiated yet or the session is closed, session={}, isOpen={}",id, underlyingSession, isOpen.get());
            return;
        }

        if(expectPingResponse){
            if(!underlyingSession.isOpen() || pingsSinceLastPong.get()>2){
                restartConnection();
            }else if(pingsSinceLastPong.get() >0){
                LOGGER.warn("{} missed {} pings.",id,pingsSinceLastPong.get());
            }
            pingsSinceLastPong.incrementAndGet();
        }

        long now = System.currentTimeMillis();
        if(now < nextPing){
            LOGGER.debug("{} Skipping ping, not time to ping yet",id);
            return;
        }


        try{
            ping.run();
        }catch (Exception e){
            LOGGER.error("Error sending ping, will try to reconnect.",e);
            restartConnection();
        }
        nextPing = now+pingWindow;
    }



    @Override
    @SneakyThrows
    public void processOpened() {
        LOGGER.debug("{} Session is open", id);
        failedReconnects.set(0);
        pingsSinceLastPong.set(0);
        nextReconnectMs = 0;
        isOpen.set(true);
        if(createLogin != null){
            LOGGER.debug("{} Logging in", id);
            doSendDirectly(createLogin.get());
            LOGGER.debug("{} Waiting for  response", id);
            Thread.sleep(1000);
        }
        this.underlyingListener.processOpened();
        recoverSubscriptions();
    }

    private void recoverSubscriptions() {
        for(SEND s: recoveryMessages){
            LOGGER.debug("Recovering subscription {}",s);
            doSendDirectly(s);
        }
    }

    private void doSendDirectly(SEND s) {
        sendDirectly(s);
    }

    @Override
    public void processError(Throwable t) {
        super.processError(t);
        restartConnection();
    }

    private void restartConnection() {
        long now = System.currentTimeMillis();
        if(now>= nextReconnectMs && !isReconnecting.getAndSet(true)){
            try{
                LOGGER.debug("{} Reconnecting!", id);
                try {
                    LOGGER.debug("{} Closing", id);
                    underlyingSession.close();
                    Thread.sleep(1000);
                } catch (Exception e) {
                    LOGGER.error("{}: Error closing the session, will ignore and continue",id,e);
                }

                try {
                    LOGGER.debug("{} Connecting", id);
                    underlyingSession.connect();
                    Thread.sleep(1000);
                } catch (Exception e) {
                    int failed = failedReconnects.incrementAndGet();
                    now = System.currentTimeMillis();
                    long ratio = failedReconnects.get() / 3;
                    nextReconnectMs = now + (ratio *5000);

                    LOGGER.error("{}: Failed to reconnect {} times, will try again at {}",id,failed,format.format(nextReconnectMs), e);
                }
            }finally {
                isReconnecting.set(false);
            }
        }else{
            LOGGER.debug("{} Not ready to reconnect", id);
        }
    }

    public void setTimer(String key, long period, Runnable runnable) {
        Disposable d = timerDisposables.remove(key);
        if(d!=null) {
            d.dispose();
        }

        if(period >0){
            timerDisposables.put(key, PeriodicTimer.INSTANCE.register(period,runnable));
        }
    }
}