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

org.restheart.mongodb.handlers.changestreams.GetChangeStreamHandler Maven / Gradle / Ivy

There is a newer version: 8.1.5
Show newest version
/*-
 * ========================LICENSE_START=================================
 * restheart-mongodb
 * %%
 * Copyright (C) 2014 - 2024 SoftInstigate
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 * =========================LICENSE_END==================================
 */
package org.restheart.mongodb.handlers.changestreams;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.bson.BsonDocument;
import org.bson.json.JsonMode;
import org.restheart.exchange.InvalidMetadataException;
import org.restheart.exchange.MongoRequest;
import org.restheart.exchange.MongoResponse;
import org.restheart.exchange.QueryNotFoundException;
import org.restheart.exchange.QueryVariableNotBoundException;
import org.restheart.handlers.PipelinedHandler;
import org.restheart.mongodb.utils.StagesInterpolator;
import org.restheart.mongodb.utils.StagesInterpolator.STAGE_OPERATOR;
import org.restheart.mongodb.utils.VarsInterpolator.VAR_OPERATOR;
import org.restheart.utils.HttpStatus;
import org.restheart.utils.ThreadsUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;

/**
 *
 * @author Andrea Di Cesare {@literal }
 * @author Omar Trasatti {@literal }
 */
public class GetChangeStreamHandler extends PipelinedHandler {
    private final String CONNECTION_HEADER_KEY = "connection";
    private final String CONNECTION_HEADER_VALUE = "upgrade";
    private final String UPGRADE_HEADER_KEY = "upgrade";
    private final String UPGRADE_HEADER_VALUE = "websocket";

    private static final Logger LOGGER = LoggerFactory.getLogger(GetChangeStreamHandler.class);
    private static final HttpHandler WEBSOCKET_HANDLER = Handlers.websocket((exchange, channel) -> {
        var csKey = new ChangeStreamWorkerKey(exchange);
        var csw$ = ChangeStreamWorkers.getInstance().get(csKey);

        if (csw$.isPresent()) {
            var csw = csw$.get();
            var wss = new WebSocketSession(channel, csw);
            csw.websocketSessions().add(wss);
            LOGGER.debug("New Change Stream WebSocket session, sessionkey={} for changeStreamKey={}", wss.getId(), csKey);
        } else {
            LOGGER.error("Cannot find Change Stream Worker changeStreamKey={}", csKey);
            try {
                channel.close();
            } catch (IOException e) {
            }
        }
    });

    public static final AttachmentKey AVARS_ATTACHMENT_KEY = AttachmentKey.create(BsonDocument.class);
    public static final AttachmentKey JSON_MODE_ATTACHMENT_KEY = AttachmentKey.create(JsonMode.class);

    @Override
    public void handleRequest(HttpServerExchange exchange) throws Exception {
        var request = MongoRequest.of(exchange);
        var response = MongoResponse.of(exchange);

        if (request.isInError()) {
            next(exchange);
            return;
        }

        try {
            if (isWebSocketHandshakeRequest(exchange)) {
                exchange.putAttachment(JSON_MODE_ATTACHMENT_KEY, request.getJsonMode());
                exchange.putAttachment(AVARS_ATTACHMENT_KEY, request.getAggregationVars());

                initChangeStreamWorker(exchange);

                WEBSOCKET_HANDLER.handleRequest(exchange);
            } else {
                response.setInError(HttpStatus.SC_BAD_REQUEST, "Change Stream requires WebSocket, no 'Upgrade' or 'Connection' request header found");

                next(exchange);
            }
        } catch (QueryNotFoundException ex) {
            response.setInError(HttpStatus.SC_NOT_FOUND, "Change Stream does not exist");

            LOGGER.debug("Requested Change Stream {} does not exist", request.getUnmappedRequestUri());

            next(exchange);
        } catch (QueryVariableNotBoundException ex) {
            response.setInError(HttpStatus.SC_BAD_REQUEST, ex.getMessage());

            LOGGER.warn("Cannot open change stream, "
                    + "the request does not specify the required variables "
                    + "in the avars query paramter: {}",
                    ex.getMessage());

            next(exchange);
        } catch (IllegalStateException ise) {
            if (ise.getMessage() != null && ise.getMessage().contains("transport does not support HTTP upgrade")) {
                var error = "Cannot open change stream: the AJP listener does not support WebSocket";

                LOGGER.warn(error);

                response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, error);
            }
        } catch (Throwable t) {
            LOGGER.error("Error handling the Change Stream request", t);
            response.setInError(HttpStatus.SC_INTERNAL_SERVER_ERROR, t.getMessage());
        }
    }

    private boolean isWebSocketHandshakeRequest(HttpServerExchange exchange) {
        var chVals = exchange.getRequestHeaders().get(CONNECTION_HEADER_KEY);

        var uhVals = exchange.getRequestHeaders().get(UPGRADE_HEADER_KEY);

        return chVals != null && uhVals != null &&
            Arrays.stream(chVals.toArray()).anyMatch(val -> val.toLowerCase().contains(CONNECTION_HEADER_VALUE)) &&
            Arrays.stream(uhVals.toArray()).anyMatch(val -> val.toLowerCase().contains(UPGRADE_HEADER_VALUE));
    }

    private List getResolvedStagesAsList(MongoRequest request) throws InvalidMetadataException, QueryVariableNotBoundException, QueryNotFoundException {
        var changesStreamOperation = request.getChangeStreamOperation();

        var streams = ChangeStreamOperation.getFromJson(request.getCollectionProps());

        var _query = streams
            .stream()
            .filter(q -> q.getUri().equals(changesStreamOperation))
            .findFirst();

        if (!_query.isPresent()) {
            throw new QueryNotFoundException("Stream " + request.getUnmappedRequestUri() + "  does not exist");
        }

        var pipeline = _query.get();

        var resolvedStages = StagesInterpolator.interpolate(VAR_OPERATOR.$var, STAGE_OPERATOR.$ifvar, pipeline.getStages(), request.getAggregationVars());
        return resolvedStages;
    }

    /**
     * Initiate a `ChangeStreamWorker` thread to monitor change streams and relay updates to WebSocket clients.
     *
     * @param exchange
     *
     * @throws QueryVariableNotBoundException
     * @throws QueryNotFoundException
     * @throws InvalidMetadataException
     */
    private synchronized void initChangeStreamWorker(HttpServerExchange exchange) throws QueryVariableNotBoundException, QueryNotFoundException, InvalidMetadataException {
        var csKey = new ChangeStreamWorkerKey(exchange);
        var request = MongoRequest.of(exchange);

        var resolvedStages = getResolvedStagesAsList(request);

        var existingChangeSreamWorker$ = ChangeStreamWorkers.getInstance().get(csKey);

        if (existingChangeSreamWorker$.isEmpty()) {
            var changeStreamWorker = (new ChangeStreamWorker(csKey,
                resolvedStages,
                request.getDBName(),
                request.getCollectionName()));

            ChangeStreamWorkers.getInstance().put(changeStreamWorker);

            ThreadsUtils.virtualThreadsExecutor().execute(changeStreamWorker);

            LOGGER.debug("Started Change Stream Worker, {}", csKey);
        } else {
            LOGGER.debug("Change Stream Worker already exists, {}", csKey);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy