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

com.signalfx.signalflow.ServerSentEventsTransport Maven / Gradle / Ivy

/*
 * Copyright (C) 2016 SignalFx, Inc. All rights reserved.
 */
package com.signalfx.signalflow;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;

import com.signalfx.shaded.apache.http.HttpEntity;
import com.signalfx.shaded.apache.http.NameValuePair;
import com.signalfx.shaded.apache.http.StatusLine;
import com.signalfx.shaded.apache.http.client.config.RequestConfig;
import com.signalfx.shaded.apache.http.client.methods.CloseableHttpResponse;
import com.signalfx.shaded.apache.http.client.methods.HttpPost;
import com.signalfx.shaded.apache.http.client.utils.URIBuilder;
import com.signalfx.shaded.apache.http.entity.StringEntity;
import com.signalfx.shaded.apache.http.impl.conn.BasicHttpClientConnectionManager;
import com.signalfx.shaded.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.signalfx.connection.AbstractHttpReceiverConnection;
import com.signalfx.endpoint.SignalFxEndpoint;

/**
 * Server-Sent Events transport.
 *
 * Implements a transport to the SignalFlow API that uses simple HTTP requests and reads Server-Sent
 * Events streams back from SignalFx. One connection per SignalFlow computation is required when
 * using this transport. This is a good transport for single, ad-hoc computations. For most use
 * cases though, the WebSocket-based transport is more efficient and has lower latency.
 *
 * @author dgriff
 */
public class ServerSentEventsTransport implements SignalFlowTransport {

    protected static final Logger log = LoggerFactory.getLogger(ServerSentEventsTransport.class);
    public static final Integer DEFAULT_TIMEOUT = 1000;
    public static final Integer DEFAULT_MAX_RETRIES = 3;

    protected final String token;
    protected final SignalFxEndpoint endpoint;
    protected final String path;
    protected Integer timeout = DEFAULT_TIMEOUT;
    protected Integer maxRetries = DEFAULT_MAX_RETRIES;

    protected ServerSentEventsTransport(final String token, final SignalFxEndpoint endpoint,
                                        final int apiVersion, final Integer timeout) {
        this(token, endpoint, apiVersion, timeout, DEFAULT_MAX_RETRIES);
    }

    protected ServerSentEventsTransport(final String token, final SignalFxEndpoint endpoint,
                                        final int apiVersion, final Integer timeout, final Integer maxRetries) {
        this.token = token;
        this.endpoint = endpoint;
        this.path = "/v" + apiVersion + "/signalflow";
        this.timeout = timeout;
        this.maxRetries = maxRetries;
    }

    @Override
    public Channel attach(String handle, final Map parameters) {
        if (log.isDebugEnabled()) {
            log.debug("attach: [ {} ] with parameters: {}", handle, parameters);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);

            response = connection.post(this.token, this.path + "/" + handle + "/attach", parameters,
                    null);

            return new TransportChannel(connection, response);
        } catch (Exception ex) {
            close(response);
            close(connection);
            throw new SignalFlowException("failed to create transport channel for attach", ex);
        }
    }

    @Override
    public Channel execute(String program, final Map parameters)
            throws SignalFlowException {
        if (log.isDebugEnabled()) {
            log.debug("execute: [ {} ] with parameters: {}", program, parameters);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);

            response = connection.post(this.token, this.path + "/execute", parameters, program);

            return new TransportChannel(connection, response);
        } catch (IOException ioex) {
            close(response);
            close(connection);
            throw new SignalFlowException("failed to create transport channel for execute", ioex);
        }
    }

    @Override
    public Channel preflight(String program, final Map parameters)
            throws SignalFlowException {
        if (log.isDebugEnabled()) {
            log.debug("preflight: [ {} ] with parameters: {}", program, parameters);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);

            response = connection.post(this.token, this.path + "/preflight", parameters, program);

            return new TransportChannel(connection, response);
        } catch (IOException ioex) {
            close(response);
            close(connection);
            throw new SignalFlowException("failed to create transport channel for execute", ioex);
        }
    }
    @Override
    public void start(String program, final Map parameters) {
        if (log.isDebugEnabled()) {
            log.debug("start: [ {} ] with parameters: {}", program, parameters);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);
            response = connection.post(this.token, this.path + "/start", parameters, program);
        } catch (Exception ex) {
            throw new SignalFlowException("failed to start program - " + program, ex);
        } finally {
            close(response);
            close(connection);
        }
    }

    @Override
    public void stop(String handle, final Map parameters) {
        if (log.isDebugEnabled()) {
            log.debug("stop: [ {} ] with parameters: {}", handle, parameters);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);
            response = connection.post(this.token, this.path + "/" + handle + "/stop", parameters,
                    null);
        } catch (Exception ex) {
            throw new SignalFlowException("failed to stop program - " + handle, ex);
        } finally {
            close(response);
            close(connection);
        }
    }

    @Override
    public void keepalive(String handle) {
        if (log.isDebugEnabled()) {
            log.debug("keepalive: [ {} ]", handle);
        }

        TransportConnection connection = null;
        CloseableHttpResponse response = null;
        try {
            connection = new TransportConnection(this.endpoint, timeout, maxRetries);
            response = connection.post(this.token, this.path + "/" + handle + "/keepalive", null,
                    null);
        } catch (Exception ex) {
            throw new SignalFlowException("failed to set keepalive for program - " + handle, ex);
        } finally {
            close(response);
            close(connection);
        }
    }

    @Override
    public void close(int code, String reason) {
        // nothing to close (separate connections are used and closed by the channel using it)
    }

    private void close(CloseableHttpResponse response) {
        try {
            if (response != null) {
                response.close();
            }
        } catch (IOException ioex) {
            log.error("error closing response", ioex);
        }
    }

    private void close(TransportConnection connection) {
        try {
            if (connection != null) {
                connection.close();
            }
        } catch (IOException ioex) {
            log.error("error closing transport connection", ioex);
        }
    }

    /**
     * Builder of SSE Transport Instance
     */
    public static class TransportBuilder {

        private String token;
        private String protocol = "https";
        private String host = SignalFlowTransport.DEFAULT_HOST;
        private int port = 443;
        private int timeout = 1;
        private int version = 2;

        public TransportBuilder(String token) {
            this.token = token;
        }

        public TransportBuilder setProtocol(String protocol) {
            this.protocol = protocol;
            return this;
        }

        public TransportBuilder setHost(String host) {
            this.host = host;
            return this;
        }

        public TransportBuilder setPort(int port) {
            this.port = port;
            return this;
        }

        public TransportBuilder setTimeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public TransportBuilder setAPIVersion(int version) {
            this.version = version;
            return this;
        }

        public ServerSentEventsTransport build() {
            SignalFxEndpoint endpoint = new SignalFxEndpoint(this.protocol, this.host, this.port);
            ServerSentEventsTransport transport = new ServerSentEventsTransport(this.token,
                    endpoint, this.version, this.timeout * 1000);
            return transport;
        }
    }

    /**
     * SSE Transport Connection
     */
    public static class TransportConnection extends AbstractHttpReceiverConnection {

        protected static final Logger log = LoggerFactory.getLogger(TransportConnection.class);
        public static final int DEFAULT_TIMEOUT_MS = 1000;
        public static final int DEFAULT_MAX_RETRIES = 3;
        protected final RequestConfig transportRequestConfig;

        public TransportConnection(SignalFxEndpoint endpoint) {
            this(endpoint, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES);
        }

        public TransportConnection(SignalFxEndpoint endpoint, int timeoutMs, int maxRetries) {
            super(endpoint, timeoutMs, maxRetries, new BasicHttpClientConnectionManager());

            this.transportRequestConfig = RequestConfig.custom().setSocketTimeout(0)
                    .setConnectionRequestTimeout(this.requestConfig.getConnectionRequestTimeout())
                    .setConnectTimeout(this.requestConfig.getConnectTimeout())
                    .setProxy(this.requestConfig.getProxy()).build();

            log.debug("constructed request config: {}", this.transportRequestConfig.toString());
        }

        public CloseableHttpResponse post(String token, String path,
                                          final Map parameters, String body)
                throws SignalFlowException {
            HttpPost httpPost = null;
            try {
                List params = new ArrayList();
                if (parameters != null) {
                    for (Map.Entry entry : parameters.entrySet()) {
                        params.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                    }
                }

                URIBuilder uriBuilder = new URIBuilder(String.format("%s%s", host.toURI(), path));
                uriBuilder.addParameters(params);

                httpPost = new HttpPost(uriBuilder.build());
                httpPost.setConfig(transportRequestConfig);
                httpPost.setHeader("X-SF-TOKEN", token);
                httpPost.setHeader("User-Agent", USER_AGENT);
                httpPost.setHeader("Content-Type", "text/plain");
                if (body != null) {
                    HttpEntity httpEntity = new StringEntity(body);
                    httpPost.setEntity(httpEntity);
                }

                if (log.isDebugEnabled()) {
                    log.debug(httpPost.toString());
                }

                CloseableHttpResponse response = client.execute(httpPost);

                StatusLine statusLine = response.getStatusLine();
                int statuscode = statusLine.getStatusCode();
                if ((statuscode < 200) || (statuscode >= 300)) {

                    try {
                        response.close();
                    } catch (IOException ex) {
                        log.error("failed to close response", ex);
                    }

                    String errorMessage = statusLine.getStatusCode() + ": failed post [ " + httpPost
                            + " ] reason: " + statusLine.getReasonPhrase();
                    throw new SignalFlowException(statusLine.getStatusCode(), errorMessage);
                }

                return response;
            } catch (IOException ex) {
                throw new SignalFlowException("failed communication. " + ex.getMessage(), ex);
            } catch (URISyntaxException ex) {
                throw new SignalFlowException("invalid uri. " + ex.getMessage(), ex);
            }
        }

        public void close() throws IOException {
            client.close();
        }
    }

    /**
     * Computation channel fed from a Server-Sent Events stream.
     */
    public static class TransportChannel extends Channel {

        protected static final Logger log = LoggerFactory.getLogger(TransportChannel.class);

        private TransportConnection connection;
        private CloseableHttpResponse response;
        private HttpEntity responseHttpEntity;
        private TransportEventStreamParser streamParser;

        public TransportChannel(final TransportConnection connection,
                                final CloseableHttpResponse response)
                throws IOException {
            super();
            this.connection = connection;
            this.response = response;
            this.responseHttpEntity = response.getEntity();
            this.streamParser = new TransportEventStreamParser(
                    this.responseHttpEntity.getContent());
            this.iterator = this.streamParser;

            log.debug("constructed {} of type {}", this, this.getClass().getName());
        }

        @Override
        public void close() {
            super.close();

            try {
                this.response.close();
            } catch (IOException ex) {
                log.error("failed to close response", ex);
            }

            try {
                this.connection.close();
            } catch (IOException ex) {
                log.error("failed to close connection", ex);
            }

            this.streamParser.close();
        }
    }

    public static class TransportEventStreamParser implements Iterator, Closeable {

        protected static final Logger log = LoggerFactory
                .getLogger(TransportEventStreamParser.class);

        private static final String EVENT = "event";
        private static final String ID = "id";
        private static final String DATA = "data";
        private static final String RETRY = "retry";
        private static final String DEFAULT_EVENT = "message";
        private static final String EMPTY_STRING = "";
        private static final Pattern DIGITS_ONLY = Pattern.compile("^[\\d]+$");

        private BufferedReader eventStreamReader;
        private boolean endOfStreamReached = false;

        private int reconnectionTimeoutMs = 1000; // default is 1 second
        private StreamMessage nextMessage;
        private String lastEventId;
        private String eventNameBuffer = DEFAULT_EVENT;
        private StringBuilder dataBuffer = new StringBuilder();

        public TransportEventStreamParser(final InputStream eventStream)
                throws UnsupportedEncodingException {
            this.eventStreamReader = new BufferedReader(
                    new InputStreamReader(eventStream, "UTF-8"));
        }

        public String getLastEventId() {
            return this.lastEventId;
        }

        public int getReconnectionTimeoutMs() {
            return this.reconnectionTimeoutMs;
        }

        @Override
        public boolean hasNext() {
            while ((endOfStreamReached == false) && (eventStreamReader != null)
                    && (nextMessage == null)) {
                parseNext();
            }

            return nextMessage != null;
        }

        @Override
        public StreamMessage next() {
            while ((endOfStreamReached == false) && (eventStreamReader != null)
                    && (nextMessage == null)) {
                parseNext();
            }

            if (nextMessage != null) {
                StreamMessage message = this.nextMessage;

                // important to set next message to null here as that variable stores the next
                // message (if one exists) which is checked by next and hasNext methods. and we just
                // popped the last message off so it should be null now.
                this.nextMessage = null;

                return message;
            } else {
                throw new NoSuchElementException("no more stream messages");
            }
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("remove from stream not supported");
        }

        @Override
        public void close() {
            if (this.eventStreamReader != null) {
                try {
                    this.eventStreamReader.close();
                    this.eventStreamReader = null;
                } catch (IOException ex) {
                    log.error("failed to close event stream", ex);
                }
            }
        }

        private void parseNext() {
            if (eventStreamReader != null) {
                try {
                    long startTime = System.currentTimeMillis();
                    dataBuffer.setLength(0);

                    String line;
                    while ((line = eventStreamReader.readLine()) != null) {
                        int colonIndex;
                        if (line.trim().isEmpty()) {
                            // message ready for dispatch
                            break;
                        } else if (line.startsWith(":")) {
                            // ignore the line
                        } else if ((colonIndex = line.indexOf(":")) != -1) {
                            String field = line.substring(0, colonIndex);
                            String value = line.substring(colonIndex + 1).replaceFirst(" ",
                                    EMPTY_STRING);
                            processField(field, value);
                        } else {
                            processField(line.trim(), EMPTY_STRING);
                        }
                    }

                    if (line == null) {
                        // end of stream reached
                        endOfStreamReached = true;
                        close();
                    }

                    if (dataBuffer.length() > 0) {
                        String data = dataBuffer.toString();
                        if (data.endsWith("\n")) {
                            data = data.substring(0, data.length() - 1);
                        }

                        nextMessage = new StreamMessage(eventNameBuffer, lastEventId, data);

                    } else {
                        log.debug(eventNameBuffer.toString());
                        eventNameBuffer = EMPTY_STRING;
                        nextMessage = null;
                    }

                    log.debug("total stream message read/parse time (ms): {}",
                            (System.currentTimeMillis() - startTime));

                } catch (IOException ex) {
                    log.error("failed to parse next stream event", ex);
                    throw new SignalFlowException("failed to parse next stream event", ex);
                }
            } else {
                nextMessage = null;
            }
        }

        private void processField(String field, String value) {
            if (DATA.equals(field)) {
                dataBuffer.append(value).append("\n");
            } else if (ID.equals(field)) {
                lastEventId = value;
            } else if (EVENT.equals(field)) {
                eventNameBuffer = value;
            } else if (RETRY.equals(field)) {
                if (DIGITS_ONLY.matcher(value).matches()) {
                    // set event stream's reconnection time to integer value
                    reconnectionTimeoutMs = Integer.parseInt(value);
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy