com.signalfx.signalflow.ServerSentEventsTransport Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of signalfx-codahale Show documentation
Show all versions of signalfx-codahale Show documentation
Dropwizard Codahale metrics plugin for signalfx
The newest version!
/*
* 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 = 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