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

io.leangen.graphql.spqr.spring.web.apollo.ApolloProtocolHandler Maven / Gradle / Ivy

The newest version!
package io.leangen.graphql.spqr.spring.web.apollo;

import graphql.ExecutionResult;
import graphql.GraphQL;
import io.leangen.graphql.spqr.spring.web.dto.ExecutorParams;
import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest;
import io.leangen.graphql.spqr.spring.web.dto.TransportType;
import io.leangen.graphql.spqr.spring.web.mvc.websocket.GraphQLWebSocketExecutor;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicReference;

import static io.leangen.graphql.spqr.spring.web.apollo.ApolloMessage.GQL_CONNECTION_INIT;
import static io.leangen.graphql.spqr.spring.web.apollo.ApolloMessage.GQL_CONNECTION_TERMINATE;
import static io.leangen.graphql.spqr.spring.web.apollo.ApolloMessage.GQL_START;
import static io.leangen.graphql.spqr.spring.web.apollo.ApolloMessage.GQL_STOP;

class ApolloProtocolHandler extends TextWebSocketHandler {

    private final GraphQL graphQL;
    private final GraphQLWebSocketExecutor executor;
    private final TaskScheduler taskScheduler;
    private final int keepAliveInterval;
    private final Map subscriptions = new ConcurrentHashMap<>();
    private final AtomicReference> keepAlive = new AtomicReference<>();

    private static final Logger log = LoggerFactory.getLogger(ApolloProtocolHandler.class);

    public ApolloProtocolHandler(GraphQL graphQL, GraphQLWebSocketExecutor executor,
                                 TaskScheduler taskScheduler, int keepAliveInterval) {
        this.graphQL = graphQL;
        this.executor = executor;
        this.taskScheduler = taskScheduler;
        this.keepAliveInterval = keepAliveInterval;
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        if (taskScheduler != null) {
            this.keepAlive.compareAndSet(null, taskScheduler.scheduleWithFixedDelay(keepAliveTask(session), Math.max(keepAliveInterval, 1000)));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        cancelAll();
        if (taskScheduler != null) {
            this.keepAlive.getAndUpdate(task -> {
                if (task != null) {
                    task.cancel(false);
                }
                return null;
            });
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        fatalError(session, exception);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        try {
            ApolloMessage apolloMessage;
            try {
                apolloMessage = ApolloMessages.from(message);
            } catch (IOException e) {
                session.sendMessage(ApolloMessages.connectionError());
                return;
            }
            switch (apolloMessage.getType()) {
                case GQL_CONNECTION_INIT:
                    session.sendMessage(ApolloMessages.connectionAck());
                    if (taskScheduler != null) {
                        session.sendMessage(ApolloMessages.keepAlive());
                    }
                    break;
                case GQL_START:
                    GraphQLRequest request = ((StartMessage) apolloMessage).getPayload();
                    ExecutorParams params = new ExecutorParams<>(request, session, TransportType.WEBSOCKET);
                    ExecutionResult result = executor.execute(graphQL, params);
                    if (result.getData() instanceof Publisher) {
                        handleSubscription(apolloMessage.getId(), result, session);
                    } else {
                        handleQueryOrMutation(apolloMessage.getId(), result, session);
                    }
                    break;
                case GQL_STOP:
                    Disposable toStop = subscriptions.get(apolloMessage.getId());
                    if (toStop != null) {
                        toStop.dispose();
                        subscriptions.remove(apolloMessage.getId(), toStop);
                    }
                    break;
                case GQL_CONNECTION_TERMINATE:
                    session.close();
                    cancelAll();
                    break;
            }
        } catch (Exception e) {
            fatalError(session, e);
        }
    }

    private void handleQueryOrMutation(String id, ExecutionResult result, WebSocketSession session) {
        try {
            session.sendMessage(ApolloMessages.data(id, result));
            session.sendMessage(ApolloMessages.complete(id));
        } catch (IOException e) {
            fatalError(session, e);
        }
    }

    private void handleSubscription(String id, ExecutionResult executionResult, WebSocketSession session) {
        Publisher events = executionResult.getData();

        Disposable subscription = Flux.from(events).subscribe(
                result -> onNext(result, id, session),
                error -> onError(error, id, session),
                () -> onComplete(id, session)
        );
        synchronized (subscriptions) {
            subscriptions.put(id, subscription);
        }
    }

    private void onNext(ExecutionResult result, String id, WebSocketSession session) {
        try {
            if (result.getErrors().isEmpty()) {
                session.sendMessage(ApolloMessages.data(id, result));
            } else {
                session.sendMessage(ApolloMessages.error(id, result.getErrors()));
            }
        } catch (IOException e) {
            fatalError(session, e);
        }
    }

    private void onError(Throwable error, String id, WebSocketSession session) {
        try {
            session.sendMessage(ApolloMessages.error(id, error));
            session.sendMessage(ApolloMessages.complete(id));
        } catch (IOException e) {
            fatalError(session, e);
        }
    }

    private void onComplete(String id, WebSocketSession session) {
        try {
            session.sendMessage(ApolloMessages.complete(id));
        } catch (IOException e) {
            fatalError(session, e);
        }
    }

    void cancelAll() {
        synchronized (subscriptions) {
            subscriptions.values().forEach(Disposable::dispose);
            subscriptions.clear();
        }
    }

    private void fatalError(WebSocketSession session, Throwable exception) {
        try {
            session.close(exception instanceof IOException ? CloseStatus.SESSION_NOT_RELIABLE : CloseStatus.SERVER_ERROR);
        } catch (Exception suppressed) {
            exception.addSuppressed(suppressed);
        }
        cancelAll();
        log.warn(String.format("WebSocket session %s (%s) closed due to an exception", session.getId(), session.getRemoteAddress()), exception);
    }

    private Runnable keepAliveTask(WebSocketSession session) {
        return () -> {
            try {
                if (session != null && session.isOpen()) {
                    session.sendMessage(ApolloMessages.keepAlive());
                }
            } catch (IOException exception) {
                fatalError(session, exception);
            }
        };
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy