Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.quotemedia.streamer.client.impl.StreamImpl Maven / Gradle / Ivy
Go to download
Java streaming client that provides easy-to-use client APIs to connect and subscribe to QuoteMedia's market data streaming services. https://quotemedia.com/
package com.quotemedia.streamer.client.impl;
import com.google.common.collect.Lists;
import com.quotemedia.datacache.messaging.GenericDataMessage;
import com.quotemedia.integration.Response;
import com.quotemedia.qitch.UIntSequence;
import com.quotemedia.streamer.client.*;
import com.quotemedia.streamer.client.auth.AuthClient;
import com.quotemedia.streamer.client.auth.impl.HttpPostJson;
import com.quotemedia.streamer.client.auth.json.ObjectMapperFactory;
import com.quotemedia.streamer.client.factory.AtmoFactory;
import com.quotemedia.streamer.client.factory.AuthHeader;
import com.quotemedia.streamer.client.util.Datatypes;
import com.quotemedia.streamer.messages.MimeTypes;
import com.quotemedia.streamer.messages.control.*;
import com.quotemedia.streamer.messages.market.DataMessage;
import com.quotemedia.streamer.messages.market.impl.SymbolInfoImpl;
import com.quotemedia.streamer.messages.qmci.QmciMessage;
import com.quotemedia.streamer.messages.smessage.LongSequence;
import com.quotemedia.streamer.messages.smessage.SMessage;
import com.quotemedia.streamer.messages.smessage.UShortId;
import org.apache.commons.lang.StringUtils;
import org.atmosphere.wasync.Event;
import org.atmosphere.wasync.Function;
import org.atmosphere.wasync.Socket;
import org.atmosphere.wasync.impl.AtmosphereRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Preconditions.checkNotNull;
public final class StreamImpl implements Stream {
private static final Logger log = LoggerFactory.getLogger(StreamImpl.class);
private static final String TRACKING_ID_HEADERNAME = "X-Atmosphere-tracking-id";
private final Authenticate auth;
private final AtmoFactory atmo;
private final PayloadDecoderProvider decoders;
private final PayloadEncoderProvider encoders;
private final QmciMessageMapper qmci;
private final StreamStats stats;
private final SymbolLocator symbols;
private final RequestResponseTracker tracker;
private SubscribeMessageTracker subscribeMessageTracker;
private HttpPostJson http;
private String connectionId;
private StreamCfg streamCfg;
public StreamImpl(final AuthClient auth,
final AtmoFactory atmo,
final PayloadDecoderProvider decoders,
final PayloadEncoderProvider encoders,
final QmciMessageMapper qmci,
final StreamStats stats,
final SymbolLocator symbols
) {
this.auth = new Authenticate(checkNotNull(auth));
this.atmo = checkNotNull(atmo);
this.decoders = checkNotNull(decoders);
this.encoders = checkNotNull(encoders);
this.qmci = checkNotNull(qmci);
this.stats = checkNotNull(stats);
this.symbols = checkNotNull(symbols);
this.tracker = new RequestResponseTracker();
this.subscribeMessageTracker = new SubscribeMessageTracker();
this.http = new HttpPostJson(new ObjectMapperFactory().create());
}
/**
* May be used to monitor stream statistics.
* The returned instance is shared and 'live' reflecting any updates instantly.
*
* @return the statistics.
*/
public final StreamStats stats() {
return this.stats;
}
private AtomicReference> onmessage = new AtomicReference<>();
@Override
public final void onmessage(final OnMessage> callback) {
this.onmessage.set(callback);
}
private AtomicReference onReconnect = new AtomicReference<>();
@Override
public final void onReconnect(final OnReconnect callback){
this.onReconnect.set(callback);
}
private AtomicReference onerror = new AtomicReference();
@Override
public final void onerror(final OnError callback) {
this.onerror.set(callback);
}
private AtomicReference> onctrlmessage = new AtomicReference<>();
@Override
public final void onctrlmessage(final OnMessage> callback) {
onctrlmessage.set(callback);
}
private final AtomicReference _state = new AtomicReference<>(STATE.NEW);
@Override
public final STATE state() {
return this._state.get();
}
private void state(final STATE val) {
final STATE oldval = this._state.getAndSet(val);
log.debug("State change {} -> {}", oldval, val);
}
private final AtomicBoolean _doReconnect = new AtomicBoolean();
private boolean doReconnect() {
return this._doReconnect.get();
}
private void doReconnect(boolean val) {
this._doReconnect.set(val);
}
private Socket socket;
private final AtomicReference> connectresponse
= new AtomicReference<>();
private final AtomicReference >reconnectresponse
= new AtomicReference<>();
volatile private String format = StreamCfg.FORMAT_DEFAULT;
volatile private OnMarketDataMessage ondatamessage;
volatile private PayloadDecoder decoder;
volatile private PayloadEncoder encoder;
volatile private FlowControl flow;
volatile private MessageProcessor messageProcessor;
private int maxEntitlementsPerSubscription;
volatile private AuthHeader authheader;
private int maxReconnectAttempts;
private boolean checkServerOnReconnect;
// synchronous
@Override
public final ConnectResponse open(final StreamCfg cfg) throws StreamException {
synchronized (this._state) {
this.checkopen();
this.state(STATE.OPENING);
}
this.streamCfg = cfg;
this.format = cfg.format();
this.checkServerOnReconnect = cfg.isCheckServerOnReconnect();
this.maxReconnectAttempts = cfg.getReopenMaxAttempts();
if (maxReconnectAttempts > StreamCfg.MAX_PERMITTED_ATTEMPTS){
throw new StreamException("Max permitted attempts for reconnection is " + StreamCfg.MAX_PERMITTED_ATTEMPTS
+ ". Please reduce the amount. Amount entered: " + maxReconnectAttempts);
}
if (!MimeTypes.QITCH.equals(this.format)) {
this.ondatamessage = new OnMarketDataMessage() {
@Override
public void on(final long timestamp, final Object msg) {
assert msg instanceof GenericDataMessage || msg instanceof QmciMessage;
firedatamessage(timestamp, qmci.map(msg));
}
};
} else {
this.ondatamessage = new OnMarketDataMessage() {
private final LocatorCodeInjector injector = new LocatorCodeInjector(symbols);
@Override
public void on(final long timestamp, final Object msg) {
assert msg instanceof DataMessage;
// Handle locator code symbol mapping
if (msg instanceof SymbolInfoImpl) {
final SymbolInfoImpl info = (SymbolInfoImpl) msg;
symbols.add(info.getLocateCode(), info.getSymbol());
} else {
this.injector.inject(msg);
}
firedatamessage(timestamp, (DataMessage) msg);
}
};
}
this.decoder = this.decoders.get(this.format);
this.encoder = this.encoders.get(this.format);
if (!MimeTypes.QITCH.equals(this.format)) {
this.flow = new JsonFlowControl();
} else {
this.flow = new QitchFlowControl();
}
final FutureValue futureresponse = new FutureValue<>();
this.connectresponse.set(futureresponse);
ConnectResponse response;
try {
if (cfg.checkversion()) {
String versionEndpoint = cfg.uri().replace("/stream/", "/version/");
String httpsEndpoint = versionEndpoint;
if (versionEndpoint.contains("https:")) {
httpsEndpoint = versionEndpoint.replace("http:", "https:");
}
this.http.get(new URL(httpsEndpoint), null, HttpPostJson.REQUEST_METHOD_GET, Response.class);
}
} catch (Exception e) {
log.warn("Error making server version call Reason {}: {}", e.getClass().getSimpleName(), e.getMessage());
}
try {
this.authheader = this.auth.authenticate(cfg.auth());
this.socket = this.newsocket(cfg);
this.opensocket(cfg, authheader, null);
// Applying timeout at this level unifies transports.
if (cfg.opentimeout_s() == null) {
response = futureresponse.get();
} else {
response = futureresponse.get(cfg.opentimeout_s(), TimeUnit.SECONDS);
}
if (ResponseCodes.OK_CODE == response.getCode()) {
initializeMessageProcessor(cfg);
}
} catch (final Exception e) {
synchronized (this._state){
state(STATE.BROKEN);
}
futureresponse.fail(e);
throw new StreamException(e);
}
maxEntitlementsPerSubscription = response.getMaxEntitlementsPerSubscription();
return response;
}
private ConnectResponse reopen() throws StreamException {
synchronized (this._state) {
this.checkReopen();
this.state(STATE.REOPENING);
}
final FutureValue connectFutureResponse = new FutureValue<>();
this.connectresponse.set(connectFutureResponse);
ConnectResponse connectResponse;
OnReconnect onReconnect = this.onReconnect.get();
try {
if (this.socket == null || this.connectionId == null) {
log.error("Unable to reopen connection. Socket or connection id is null");
this.doReconnect(false);
this.closeimpl();
StreamException streamException = new StreamException("Unable to reopen connection. Socket or connection id is null");
if (onReconnect != null) {
onReconnect.error(streamException);
}
throw streamException;
}
this.opensocket(this.streamCfg, this.authheader, connectionId);
connectResponse = (this.streamCfg.reopentimeout_ms() == null) ? connectFutureResponse.get() : connectFutureResponse.get(this.streamCfg.reopentimeout_ms(), TimeUnit.SECONDS);
if (ResponseCodes.OK_CODE == connectResponse.getCode()) {
initializeMessageProcessor(this.streamCfg);
if (onReconnect != null) {
onReconnect.onConnectionSuccess(connectResponse);
}
} else {
if (onReconnect != null) {
onReconnect.error(new StreamException("Connection response returned" + connectResponse.toString()));
}
}
} catch (final Exception e) {
connectFutureResponse.fail(e);
if (onReconnect != null) {
onReconnect.error(e);
}
throw new StreamException(e);
}
maxEntitlementsPerSubscription = connectResponse.getMaxEntitlementsPerSubscription();
return connectResponse;
}
/**
* Needs to check if the current state of the stream is either broken or closed
* */
private void checkReopen(){
final STATE state = this.state();
if (!(STATE.CLOSED.equals(state) || STATE.BROKEN.equals(state))) {
throw new IllegalStateException(String.format("Cannot trigger re-open for stream in state '%s'", state));
}
}
private void initializeMessageProcessor(StreamCfg cfg) {
if (cfg.isSynchronousMessageProcessing()) {
this.messageProcessor = new SynchronousMessageProcessor();
} else {
this.messageProcessor = new AsynchronousMessageProcessor(cfg.queueCapacity());
}
this.messageProcessor.start(ondatamessage);
}
private void checkopen() {
final STATE s = this.state();
if (!(STATE.NEW.equals(s) || STATE.CLOSED.equals(s) || STATE.REOPENING.equals(s))) {
throw new IllegalStateException(String.format("Cannot open stream in state '%s'.", s));
}
}
volatile private AtmosphereRequest request;
private void opensocket(final StreamCfg cfg, final AuthHeader authheader, String connectionId) throws IOException, TimeoutException {
if (this.socket == null) {
return;
}
this.request = this.atmo.newrequest(cfg, authheader, connectionId);
try {
if (cfg.opentimeout_s() == null) {
this.socket.open(this.request);
} else {
/**
* As of wasync 1.3.2 open only throws a {@link java.util.concurrent.TimeoutException} for transport
* {@link org.atmosphere.wasync.transport.WebSocketTransport}.
*
* For {@link org.atmosphere.wasync.transport.StreamTransport} and derived implementations NO
* {@link java.util.concurrent.TimeoutException} will be thrown.
* Instead after {@link org.atmosphere.wasync.OptionsBuilder#waitBeforeUnlocking} open will return and
* the connection is considered open.
*/
this.socket.open(this.request, cfg.opentimeout_s(), TimeUnit.SECONDS);
}
this.connectionId = request.queryString().get(TRACKING_ID_HEADERNAME).get(0);
} catch (final IOException e) {
if (e.getCause() instanceof TimeoutException) {
throw (TimeoutException) e.getCause();
}
throw e;
}
}
private Socket newsocket(final StreamCfg cfg) {
final Socket s = this.atmo.newsocket(cfg);
s.on(Event.OPEN, new Function() {
@Override
public final void on(final Object o) {
onSocketOpen();
}
});
s.on(Event.MESSAGE, new OnSocketMessage(this.decoder));
s.on(Event.CLOSE, new Function() {
@Override
public void on(Object o) {
onSocketClose();
}
});
s.on(Event.REOPENED, new Function() {
@Override
public final void on(Object o) {
onSocketReopen();
}
});
s.on(Event.ERROR, new Function() {
@Override
public final void on(final Throwable t) {
onSocketError(t);
}
});
return s;
}
// handle socket open event
private void onSocketOpen() {
// This doesn't trigger open, ConnectResponse message does.
log.debug("Atmosphere socket open.");
}
// Flag to check if the connection is successfully reconnected
private boolean isReconnected = false;
// This flag is mainly to check if we should stop reconnecting regardless, i.e 401 returned from server. if this is true at some point, it won't be set back to false
private boolean stopReconnection = false;
// handle socket close event
private void onSocketClose() {
OnError on = this.onerror.get();
if (messageProcessor != null) {
messageProcessor.stop();
}
final STATE state = this._state.get();
// logic is needed to unify WEBSOCKET and STREAMING
if (STATE.CLOSING.equals(state)) {
this.closeimpl();
} else { // unexpected close
synchronized (this._state){
this.state(STATE.BROKEN);
}
if (this.streamCfg.isReconnectActive() && !doReconnect()) {
this.doReconnect(true);
}
if (on != null) {
on.error(new StreamException(new ConnectException("Unexpected connection close.")));
}
}
if (doReconnect() && !stopReconnection) {
try {
this.tryReopen();
} catch (StreamException streamException) {
log.error("There was an error while reconnecting. {}", streamException.toString());
if (on != null) {
on.error(new StreamException("Error while trying re-connection. Reason: " + streamException.getMessage()));
}
}
}
}
protected void tryReopen() throws StreamException {
//TODO: Look a better way to check for all of this since this is a bit hacky
if (!isServerUp()) {
log.debug("Unable to reconnect, server is down");
throw new StreamException("Unable to reconnect, server is down");
}
if (isReconnected) {
log.debug("Connection state is {}, won't try reconnection", this.state());
throw new StreamException(String.format("Connection state is {}, won't try reconnection", this.state()));
}
if(!doReconnect() || stopReconnection){
log.debug("The reconnect is not active and won't try to reconnect");
throw new StreamException("The reconnect is not active and won't try to reconnect");
}
startReconnection();
}
private void startReconnection() throws StreamException {
if (maxReconnectAttempts <= 0) {
throw new StreamException("Max reconnect attempts reached and won't try to reconnect");
}
else {
try {
int attempt = 1;
while (!stopReconnection && doReconnect() && !isReconnected && attempt <= maxReconnectAttempts) {
ConnectResponse connectResponse = this.reopen();
if (connectResponse.getCode() == ResponseCodes.OK_CODE && this.state() == STATE.OPEN) {
isReconnected = true;
maxReconnectAttempts--;
this.doReconnect(false);
break;
}
else if(connectResponse.getCode() == ResponseCodes.UNAUTHORIZED_CODE) {
log.warn("Unauthorized response. Won't try to reconnect");
stopReconnection = true; // need to stop right here since there is no point of continuing trying to reconnect if 401 was sent
}
else {
log.warn("Reconnection attempt: {} failed. Reason: {}", attempt, connectResponse.getReason());
}
attempt++;
}
} catch (StreamException e) {
log.error("Exception was thrown while reconnecting. Reason: {}", e.getMessage());
doReconnect(false);
this.closeimpl();
throw e;
}
}
}
@Override
public void performReconnect() throws StreamException {
//Check if reconnect is active
if (!streamCfg.isReconnectActive()) {
log.debug("Reconnect flag is set to {}. Won't try to reconnect", streamCfg.isReconnectActive());
throw new StreamException(String.format("Reconnect flag is set to {}",streamCfg.isReconnectActive()));
}
this.doReconnect(true);
try {
this.tryReopen();
} catch (Exception exception) {
OnReconnect onReconnect = this.onReconnect.get();
if(onReconnect != null){
onReconnect.error(new StreamException(exception));
}
}
}
protected boolean isServerUp(){
if (!this.checkServerOnReconnect) {
return true;
}
String pingEndpoint = this.streamCfg.uri().replace("/stream/", "/ping/");
String httpsEndpoint = pingEndpoint;
httpsEndpoint = pingEndpoint.replace("http:", "https:");
try {
this.http.get(new URL(httpsEndpoint), null, HttpPostJson.REQUEST_METHOD_GET, Response.class);
return true;
} catch (IOException e) {
log.warn("Unable to get ping response from server. Reason {}: {}", e.getClass(), e.getMessage());
this.doReconnect(false);
stopReconnection = true;
}
return false;
}
// handle socket reopen event
private void onSocketReopen() {
//this is handled by the onReconnectResponse method
}
// handle socket error event
private void onSocketError(final Throwable t) {
OnError on;
synchronized (this._state){
this.state(STATE.BROKEN);
on = this.onerror.get();
}
if (on != null) {
on.error(new StreamException(t));
}
}
@Override
public final void subscribe(final Subscription sub, final OnResponse callback) {
List subscriptionRequests = prepareRequests(sub, Action.SUBSCRIBE);
synchronized (this._state){
if(subscriptionRequests.size() == 1) {
sendRequest(subscriptionRequests.get(0), callback, null);
} else {
Integer subscribeCombineMsgId = this.tracker.getID();
for (SubscribeMessage subscriptionRequest : subscriptionRequests) {
sendRequest(subscriptionRequest, callback, subscribeCombineMsgId);
}
}
}
}
@Override
public final void unsubscribe(final Subscription sub, final OnResponse callback) {
List subscriptionRequests = prepareRequests(sub, Action.UNSUBSCRIBE);
synchronized (this._state){
for (SubscribeMessage subscriptionRequest : subscriptionRequests) {
sendRequest(subscriptionRequest, callback, null);
}
}
}
private List prepareRequests(Subscription sub, Action action) {
int currentApproximateNumberOfEntitlements = 0;
int lastAddedSymbolIndex = -1;
int numberOfSymbols = sub.symbols().length;
boolean isOrderbookSubscription = Arrays.asList(sub.types()).contains(Datatype.ORDERBOOK);
List requests = Lists.newArrayList();
for (int i = 0; i < numberOfSymbols; i++) {
currentApproximateNumberOfEntitlements = getUpdatedNumberOfEntitlements(sub.types().length,
currentApproximateNumberOfEntitlements, sub.symbols()[i], isOrderbookSubscription);
if (currentApproximateNumberOfEntitlements >= maxEntitlementsPerSubscription || i == numberOfSymbols - 1) {
requests.add(buildRequest(Arrays.copyOfRange(sub.symbols(), lastAddedSymbolIndex + 1, i + 1), sub, action));
lastAddedSymbolIndex = i;
currentApproximateNumberOfEntitlements = 0;
}
}
return requests;
}
private SubscribeMessage buildRequest(String[] symbols, Subscription sub, Action action) {
final SubscribeMessage msg = new SubscribeMessage();
msg.setAction(action);
msg.setSymbols(symbols);
msg.setTypes(Datatypes.map(sub.types()));
msg.setSkipHeavyInitialLoad(sub.skipHeavyInitialLoad());
msg.setMimetype(this.format);
msg.setConflation(sub.conflation());
msg.setIntervalPeriod(sub.intervalPeriod());
return msg;
}
private void sendRequest(final SubscribeMessage msg, final OnResponse extends BaseResponse> callback, final Integer subscribeCombinedMsgId) {
this.checkfire();
this.fire(msg, callback, subscribeCombinedMsgId);
}
@Override
public void subscribeExchange(final ExchangeSubscription sub, final OnResponse callback) {
ExchangeSubscribeMessage exchangeSubscribeMessage = new ExchangeSubscribeMessage();
exchangeSubscribeMessage.setExchange(sub.getExchange());
exchangeSubscribeMessage.setMimetype(this.format);
exchangeSubscribeMessage.setConflation(sub.getConflation());
exchangeSubscribeMessage.setAction(Action.SUBSCRIBE);
this.checkfire();
this.fire(exchangeSubscribeMessage, callback, null);
}
@Override
public void unsubscribeExchange(ExchangeSubscription request, OnResponse callback) {
ExchangeSubscribeMessage exchangeSubscribeMessage = new ExchangeSubscribeMessage();
exchangeSubscribeMessage.setExchange(request.getExchange());
exchangeSubscribeMessage.setMimetype(this.format);
exchangeSubscribeMessage.setConflation(request.getConflation());
exchangeSubscribeMessage.setAction(Action.UNSUBSCRIBE);
this.checkfire();
this.fire(exchangeSubscribeMessage, callback, null);
}
@Override
public void subUnsubAlert(AlertSubscription request, OnResponse callback) {
SubUnsubAlertMessage subUnsubAlertMessage = new SubUnsubAlertMessage();
subUnsubAlertMessage.setOperation(request.getOperation());
subUnsubAlertMessage.setMimetype(this.format);
this.checkfire();
this.fire(subUnsubAlertMessage, callback, null);
}
@Override
public void subscribeCorporateEvent(CorpEventSubscription request, OnResponse callback) {
CorpEventSubscribeMessage corpEventSubscribeMessage = new CorpEventSubscribeMessage();
corpEventSubscribeMessage.setSymbols(request.getSymbols());
corpEventSubscribeMessage.setAllCorpEvents(request.isAllCorpEvents());
corpEventSubscribeMessage.setAction(Action.SUBSCRIBE);
corpEventSubscribeMessage.setCorpEventTypes(request.getTypes());
corpEventSubscribeMessage.setMimeType(this.format);
this.checkfire();
this.fire(corpEventSubscribeMessage, callback, null);
}
@Override
public void unsubscribeCorporateEvent(CorpEventSubscription request, OnResponse callback) {
CorpEventSubscribeMessage corpEventSubscribeMessage = new CorpEventSubscribeMessage();
corpEventSubscribeMessage.setSymbols(request.getSymbols());
corpEventSubscribeMessage.setAllCorpEvents(request.isAllCorpEvents());
corpEventSubscribeMessage.setAction(Action.UNSUBSCRIBE);
corpEventSubscribeMessage.setCorpEventTypes(request.getTypes());
corpEventSubscribeMessage.setMimeType(this.format);
this.checkfire();
this.fire(corpEventSubscribeMessage, callback, null);
}
//TODO add subscribe news client interface when java client is ready
// public void subscribeNews(NewsSubscription request, OnResponse callback){
// NewsSubscribeMessage newsSubscribeMessage = new NewsSubscribeMessage();
// newsSubscribeMessage.setNewsFilters(request.getNewsFilters());
// newsSubscribeMessage.setSkipHeavyInitialLoad(request.skipHeavyInitialLoad());
// this.checkfire();
// this.fire(newsSubscribeMessage, callback);
// }
@Override
public void getSessionStats(OnResponse callback) {
this.checkfire();
final StatsMessage msg = new StatsMessage();
this.fire(msg, callback, null);
}
private boolean isConsolidatedSymbol(String symbol) {
return StringUtils.endsWith(symbol, CONSOLIDATED_SYMBOL_SUFFIX);
}
private int getUpdatedNumberOfEntitlements(int numberOfSubscriptionTypes, int currentApproximateNumberOfEntitlements, String symbol, boolean isSubscribeToOrderbook) {
int result = currentApproximateNumberOfEntitlements;
if (isSubscribeToOrderbook && isConsolidatedSymbol(symbol)) {
result += CONSOLIDATED_SYMBOL_ENTITLEMENTS_COEFFICIENT * numberOfSubscriptionTypes;
} else {
result += numberOfSubscriptionTypes;
}
return result;
}
private void checkfire() {
final STATE s = this.state();
if (!STATE.OPEN.equals(s)) {
throw new IllegalStateException(String.format("Cannot fire message over stream in state '%s'.", s));
}
}
private void fire(final CtrlMessage sub, final OnResponse extends BaseResponse> callback, final Integer subscribeCombinedMsgId) {
Integer id;
if (callback != null) {
id = this.tracker.add(callback);
} else {
id = UShortId.NULL_VALUE;
}
try {
if (sub instanceof BaseRequest) {
((BaseRequest) sub).setId(id);
if (sub instanceof SubscribeMessage && ((SubscribeMessage) sub).getAction().equals(Action.SUBSCRIBE)) {
if(subscribeCombinedMsgId != null && subscribeCombinedMsgId.intValue() > 0) {
this.subscribeMessageTracker.add(id);
this.subscribeMessageTracker.addGroupTrackingMap(subscribeCombinedMsgId, id);
}
}
}
final MessageImpl msg = this.encoder.encode(sub);
msg.sequencenumber(this.flow.next());
msg.id(id);
this.socketfire(msg);
} catch (final IOException e) {
this.tracker.remove(id);
this.subscribeMessageTracker.remove(id);
this.subscribeMessageTracker.removeGroupTrackingMap(subscribeCombinedMsgId, id);
// exception handling through callback
if (callback != null) {
callback.error(new StreamException(e));
}
}
}
private void socketfire(final Object msg) throws IOException {
this.socket.fire(msg);
this.stats.incmsgout();
}
private final AtomicReference onclose = new AtomicReference();
@Override
public final void close(final OnClose callback) {
synchronized (this._state){
if (!this.canclose()) {
return;
}
this.onclose.set(callback);
state(STATE.CLOSING);
}
boolean closesync = true;
if (this.socket != null) {
final Socket.STATUS socketstatus = this.socket.status();
// socket in state CLOSE won't trigger async close event
// socket in state ERROR might or might not trigger async close event
closesync = Socket.STATUS.CLOSE.equals(socketstatus) || Socket.STATUS.ERROR.equals(socketstatus);
this.socket.close();
}
if (closesync) {
// close sync in case async CLOSE event won't be triggered
this.closeimpl();
}
}
private boolean canclose() {
final STATE s = this.state();
return !(STATE.CLOSED.equals(s) || STATE.CLOSING.equals(s));
}
private void closeimpl() {
OnClose on;
synchronized (_state){
on = onclose.getAndSet(null);
state(STATE.CLOSED);
isReconnected = false; //Add it here since this impl is also called during onError, we need to reset the value since user is no longer connected
}
if (on != null) {
on.closed();
}
}
/**
* Handle socket message event.
*/
private final class OnSocketMessage implements Function {
final PayloadDecoder decoder;
public OnSocketMessage(final PayloadDecoder decoder) {
this.decoder = decoder;
}
@Override
public final void on(final SMessage msg) {
if (msg instanceof MessageGroupImpl) {
List messages = ((MessageGroupImpl) msg).getMessages();
if (messages == null || messages.isEmpty()) {
return;
}
for (final SMessage message : messages) {
flowcontrol(message);
this.handlepayload(message);
}
} else if (msg.type() == MessageBlockImpl.TYPEID) {
flowcontrol(msg); // Sequencing happens on block level
final Collection messages = ((MessageBlockImpl) msg).messages();
if (messages == null || messages.isEmpty()) {
return;
}
for (final SMessage message : messages) {
this.handlepayload(message);
}
} else {
flowcontrol(msg);
this.handlepayload(msg);
}
}
private void handlepayload(final SMessage msg) {
Object payload;
try {
payload = this.decoder.decode(msg);
} catch (final IOException e) {
payload = null;
log.warn("Problem decoding message. Reason {}: {}", e.getClass().getSimpleName(), e.getMessage());
log.debug("Problem decoding message.", e);
}
if (payload == null) {
return;
}
StreamImpl.this.onMessage(msg.timestamp(), payload);
}
}
private void flowcontrol(final SMessage msg) {
final FlowMessage update = this.flow.track(msg);
if (update == null) {
return;
}
log.debug("Sending update {}.", update);
this.fire(update, null, null);
}
private void onMessage(final long timestamp, final Object msg) {
if (!(msg instanceof CtrlMessage)) {
this.messageProcessor.process(timestamp, msg);
} else {
this.stats.incctrlmsgin();
this.stats.incctrlmsglag_ms(System.currentTimeMillis() - timestamp);
this.onCtrlMessage((CtrlMessage) msg);
}
}
private void firedatamessage(final long timestamp, final DataMessage msg) {
log.trace("Received MarketData Message {}", msg);
this.stats.incdatamsgin();
this.stats.incdatamsglag_ms(System.currentTimeMillis() - timestamp);
final OnMessage on = this.onmessage.get();
if (on != null) {
on.message(msg);
}
}
private void firectrlmessage(final CtrlMessage msg) {
final OnMessage on = this.onctrlmessage.get();
if (on != null) {
on.message(msg);
}
}
private void onCtrlMessage(final CtrlMessage msg) {
log.debug("Received control message. {}", msg);
if (msg instanceof Heartbeat) {
return; // do not forward
}
if(msg instanceof ReconnectResponse){
this.onReconnectResponse((ReconnectResponse) msg);
}
else if (msg instanceof ConnectResponse) {
this.onConnectResponse((ConnectResponse) msg);
}
else if (msg instanceof SubscribeResponse) {
this.onSubscribeResponse((SubscribeResponse) msg);
} else if (msg instanceof UnsubscribeResponse) {
this.onUnsubscribeResponse((UnsubscribeResponse) msg);
} else if (msg instanceof ConnectionClose) {
this.onConnectionClose((ConnectionClose) msg);
} else if (msg instanceof SlowConnection) {
this.onSlowConnectionMessage((SlowConnection) msg);
} else if (msg instanceof StatsResponse) {
this.onStatsResponse((StatsResponse) msg);
} else if (msg instanceof InitialDataSent) {
this.onInitialDataSent((InitialDataSent) msg);
} else if (msg instanceof ResubscribeMessage) {
this.onResubscribeMessage((ResubscribeMessage) msg);
} else if (msg instanceof ExchangeSubscribeResponse) {
this.onExchangeSubscribeResponse((ExchangeSubscribeResponse) msg);
} else if (msg instanceof ExchangeUnsubscribeResponse) {
this.onExchangeUnsubscribeResponse((ExchangeUnsubscribeResponse) msg);
} else if (msg instanceof AlertsSubUnsubResponse) {
this.onAlertSubUnsubResponse((AlertsSubUnsubResponse) msg);
} else if (msg instanceof NewsSubscribeResponse) {
this.onNewsSubscribeResponse((NewsSubscribeResponse) msg);
}else if (msg instanceof CorpEventResponse){
this.onCorpEventResponse((CorpEventResponse) msg);
}else if (msg instanceof MissedDataSent) {
this.onMissedDataSent((MissedDataSent) msg);
} else if (msg != null) {
log.warn("Received unsupported control message. {}", msg);
}
}
private void onExchangeSubscribeResponse(ExchangeSubscribeResponse msg) {
OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
private void onExchangeUnsubscribeResponse(ExchangeUnsubscribeResponse msg) {
OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
private void onAlertSubUnsubResponse(AlertsSubUnsubResponse msg) {
OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
private void onNewsSubscribeResponse(NewsSubscribeResponse msg) {
OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
private void onCorpEventResponse(CorpEventResponse msg) {
OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
volatile private String serverversion = null;
private void onConnectResponse(final ConnectResponse msg) {
this.serverversion = msg.getVersion();
synchronized (this._state){
final FutureValue response = this.connectresponse.getAndSet(null);
if (STATE.OPENING.equals(this._state.get()) || STATE.REOPENING.equals(this._state.get())) {
this.state(STATE.OPEN);
this.flow.checkinterval(msg.getFlowControlCheckInterval());
response.complete(msg);
}
}
}
private void onReconnectResponse(ReconnectResponse msg){
this.serverversion = msg.getVersion();
OnReconnect callback;
synchronized (this._state){
final FutureValue response = this.reconnectresponse.getAndSet(null);
callback = this.onReconnect.get();
if (STATE.REOPENING.equals(this._state.get())) {
this.state(STATE.OPEN);
this.flow.checkinterval(msg.getFlowControlCheckInterval());
//Add the stats here
response.complete(msg);
firectrlmessage(msg);
}
if (callback != null) {
callback.onReconnectResponse(msg);
}
}
if(msg.getCode() == 450){
this.doReconnect(false);
this.stopReconnection = true;
log.warn("A slow connection response was sent by the server. Another reopen won't be attempted");
}
log.debug("Received reconnect response for id: {} ", msg.getRequestId());
}
private void onSubscribeResponse(final SubscribeResponse msg) {
log.debug("Received subscribe response for request {}", msg.getRequestId());
final OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
log.debug("Callback is not null");
if(this.subscribeMessageTracker.pending.contains(msg.getRequestId())) {
SubscribeResponse response = this.subscribeMessageTracker.combineResponse(msg.getRequestId(), msg);
if (msg.getCode() != ResponseCodes.OK_CODE) {
callback.response(msg);
}
if(response != null) {
callback.response(response);
}
} else {
callback.response(msg);
}
}
}
private void onUnsubscribeResponse(final UnsubscribeResponse msg) {
final OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
private void onStatsResponse(final StatsResponse msg) {
final OnResponse callback = (OnResponse) this.tracker.remove(msg.getRequestId());
if (callback != null) {
callback.response(msg);
}
}
/**
* Last message before close the connection.
*
* @param msg
*/
private void onConnectionClose(final ConnectionClose msg) {
firectrlmessage(msg);
}
/**
* Messages informing that client is lagging behind.
*
* @param msg
*/
private void onSlowConnectionMessage(final SlowConnection msg) {
firectrlmessage(msg);
}
/**
* Messages informing that Initial data for subscription was sent.
*
* @param msg
*/
private void onInitialDataSent(final InitialDataSent msg) {
firectrlmessage(msg);
}
/**
* Messages informing that Resubscription has occur after a datasource reset.
*
* @param msg
*/
private void onResubscribeMessage(final ResubscribeMessage msg) {
firectrlmessage(msg);
}
/**
* Messages informing that Missed data for reconnection subscription was sent.
*
* @param msg
*/
private void onMissedDataSent(final MissedDataSent msg) {
firectrlmessage(msg);
OnReconnect callback = this.onReconnect.get();
if (callback != null) {
callback.onMissedData(msg);
}
}
/**
*
*/
private static final class RequestResponseTracker {
private final UShortId ids;
private final Map> pending;
public RequestResponseTracker() {
this.ids = new UShortId();
this.pending = new HashMap<>();
}
public final Integer add(final OnResponse> r) {
synchronized (this.pending){
final int id = this.ids.next();
this.pending.put(id, r);
return id;
}
}
public final OnResponse> remove(final Integer id) {
synchronized (this.pending){
return this.pending.remove(id);
}
}
public final Integer getID() {
return this.ids.next();
}
}
private static final class SubscribeMessageTracker {
private final List pending;
private final Map> groupTrackingMap;
private final Map responseTrackingMap;
public SubscribeMessageTracker() {
this.pending = new ArrayList<>();
this.groupTrackingMap = new HashMap<>();
this.responseTrackingMap = new HashMap<>();
}
public final void add(final Integer id) {
synchronized (this.pending){
this.pending.add(id);
}
}
public final void remove(Integer id) {
synchronized (this.pending){
this.pending.remove(id);
}
}
public final void addGroupTrackingMap(Integer groupId, Integer id) {
synchronized (this.groupTrackingMap) {
if (!this.groupTrackingMap.containsKey(groupId)) {
this.groupTrackingMap.put(groupId, new ArrayList<>(Arrays.asList(id)));
} else {
this.groupTrackingMap.get(groupId).add(id);
}
}
}
public final void removeGroupTrackingMap(Integer groupId, Integer id) {
synchronized (this.groupTrackingMap) {
if(this.groupTrackingMap.get(groupId).size() == 1) {
this.groupTrackingMap.remove(groupId);
} else {
for (int i = 0; i < this.groupTrackingMap.get(groupId).size(); i++) {
if(this.groupTrackingMap.get(groupId).get(i) == id) {
this.groupTrackingMap.get(groupId).remove(i);
}
}
}
}
}
public final void addResponseTrackingMap(Integer groupId, SubscribeResponse msg) {
synchronized (this.responseTrackingMap) {
if(!this.responseTrackingMap.containsKey(groupId)) {
SubscribeResponse response = new SubscribeResponse();
response.setEntitlements(new ArrayList());
response.setInvalidSymbols(new ArrayList());
response.setRejectedSymbols(new ArrayList());
response.setMessageFilters(new ArrayList());
this.responseTrackingMap.put(groupId, response);
}
this.responseTrackingMap.get(groupId).setCode(ResponseCodes.OK_CODE);
this.responseTrackingMap.get(groupId).getEntitlements().addAll(msg.getEntitlements());
this.responseTrackingMap.get(groupId).getInvalidSymbols().addAll(msg.getInvalidSymbols());
this.responseTrackingMap.get(groupId).getRejectedSymbols().addAll(msg.getRejectedSymbols());
this.responseTrackingMap.get(groupId).getMessageFilters().addAll(msg.getMessageFilters());
this.responseTrackingMap.get(groupId).setReason(msg.getReason());
this.responseTrackingMap.get(groupId).setRequestId(msg.getRequestId());
}
}
public final void removeResponseTrackingMap(Integer groupId) {
synchronized (this.responseTrackingMap) {
this.responseTrackingMap.remove(groupId);
}
}
/**
* Time complexity O(n^2)
*
* pending = [2,3,5,6]
* groupTrackingMap = {1 -> [2,3], 4 -> [5,6]}
* responseTrackingMap = {}
*
* Using Bubble sort for finding the trackingID from groupTrackingMap and then add to responseTrackingMap
* Once all responses are received it will return the response message back to client
*
* @param id
* @param msg
* @return SubscribeResponse
*/
public final SubscribeResponse combineResponse(Integer id, SubscribeResponse msg) {
synchronized (this.groupTrackingMap) {
if(msg != null) {
log.debug("Combining response");
for (Map.Entry> set : this.groupTrackingMap.entrySet()) {
for (Integer item : set.getValue()) {
if (item.intValue() == id) {
if (msg.getCode() == ResponseCodes.OK_CODE) {
addResponseTrackingMap(set.getKey(), msg);
}
remove(id);
removeGroupTrackingMap(set.getKey(), id);
if (this.groupTrackingMap.containsKey(set.getKey())) {
return null;
} else {
SubscribeResponse result = this.responseTrackingMap.get(set.getKey());
removeResponseTrackingMap(set.getKey());
return result;
}
}
}
}
}
return null;
}
}
}
/**
*
*/
private abstract static class FlowControl {
public abstract long next();
volatile Integer checkinterval;
public void checkinterval(final Integer val) {
this.checkinterval = val;
}
public FlowMessage track(final SMessage msg) {
if (this.checkinterval == null) {
return null;
}
FlowMessage update = null;
if ((msg.sequencenumber() % this.checkinterval) == 0) {
update = new FlowMessage();
update.setSequence(msg.sequencenumber());
}
return update;
}
}
/**
*
*/
private static class JsonFlowControl extends FlowControl {
private final LongSequence sequence;
public JsonFlowControl() {
this.sequence = new LongSequence();
}
@Override
public long next() {
return this.sequence.next();
}
}
/**
*
*/
private static class QitchFlowControl extends FlowControl {
private final UIntSequence sequence;
public QitchFlowControl() {
this.sequence = new UIntSequence();
}
@Override
public long next() {
return this.sequence.next();
}
}
}