/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.f1x.v1;
import org.f1x.api.FixInitiatorSettings;
import org.f1x.api.FixVersion;
import org.f1x.api.session.SessionID;
import org.f1x.api.session.SessionStatus;
import org.f1x.io.InputChannel;
import org.f1x.io.OutputChannel;
import org.f1x.v1.schedule.SessionTimes;
import java.net.ConnectException;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicReference;
/**
* FIX Communicator that initiates outbound FIX connections
*/
public class FixSessionInitiator extends FixSocketCommunicator {
private final SessionID sessionID;
private final String host;
private final int port;
private final AtomicReference<Thread> initiatorThread = new AtomicReference<>();
public FixSessionInitiator(String host, int port, FixVersion fixVersion, SessionID sessionID) {
this(host, port, fixVersion, sessionID, new FixInitiatorSettings());
}
public FixSessionInitiator(String host, int port, FixVersion fixVersion, SessionID sessionID, FixInitiatorSettings settings) {
super(fixVersion, settings);
this.host = host;
this.port = port;
this.sessionID = sessionID;
}
@Override
public FixInitiatorSettings getSettings() {
return (FixInitiatorSettings) super.getSettings();
}
@Override
public SessionID getSessionID() {
return sessionID;
}
@Override
public void run() {
if ( ! initiatorThread.compareAndSet(null, Thread.currentThread()))
throw new IllegalStateException("Another thread already using this initiator");
try {
init();
try {
work();
} finally {
destroy();
}
} finally {
// active = true; //TODO: Can we delay it till the moment we need to reuse
initiatorThread.set(null);
LOGGER.info().append("Terminating FIX Initiator thread").commit();
}
}
protected void work() {
boolean needPause = false;
try {
while ( ! closeInProgress) {
try {
startSession(needPause);
needPause = !processInboundMessages();
} finally {
endSession();
}
}
} catch (Throwable e) {
if (!(e instanceof InterruptedException))
LOGGER.warn().append("Error in initiator loop (ignoring)").append(e).commit();
}
}
@Override
protected void connect(InputChannel in, OutputChannel out) {
assert Thread.currentThread() == initiatorThread.get();
super.connect(in, out);
}
protected void startSession(boolean needPause) throws InterruptedException {
boolean newFixSession = waitForSessionStart();
connect(needPause);
if (newFixSession) {
sessionState.resetNextSeqNums();
messageStore.clean();
}
logon(getSettings().isResetSequenceNumbersOnEachLogon());
scheduleSessionMonitoring();
}
private boolean waitForSessionStart() throws InterruptedException {
boolean newFixSession = false;
if (schedule != null) {
long now = timeSource.currentTimeMillis();
SessionTimes sessionTimes = schedule.getCurrentSessionTimes(now);
final long sessionStart = sessionTimes.getStart();
long timeToWaitUntilNextSession = sessionStart - now;
if (timeToWaitUntilNextSession > 0) {
LOGGER.info().append("Waiting ").append(timeToWaitUntilNextSession/1000).append(" seconds until next FIX Session").commit();
timeSource.sleep(sessionStart - now);
now = sessionStart;
}
final long lastConnectionTime = sessionState.getLastConnectionTimestamp();
if (lastConnectionTime < sessionStart) {
newFixSession = true;
}
final long sessionEnd = sessionTimes.getEnd();
scheduleSessionEnd(sessionEnd - now);
}
return newFixSession;
}
protected void endSession() {
getSessionState().flush();
unscheduleSessionEnd();
unscheduleSessionMonitoring();
}
/**
* Connects to FIX counter-party (in several attempts if necessary)
*/
private void connect(boolean needPause) throws InterruptedException {
if (needPause) {
LOGGER.warn().append("FIX: will reconnect after short pause...").commit();
timeSource.sleep(getSettings().getErrorRecoveryInterval());
}
assertSessionStatus(SessionStatus.Disconnected);
while ( ! closeInProgress && getSessionStatus() == SessionStatus.Disconnected) {
try {
LOGGER.info().append("Connecting...").commit();
connect(new Socket(host, port));
} catch (ConnectException e) {
LOGGER.info().append("Server ").append(host).append(':').append(port).append(" is unreachable, will retry later").commit();
timeSource.sleep(getSettings().getConnectInterval());
} catch (Throwable e) {
LOGGER.warn().append("Error connecting to server ").append(host).append(':').append(port).append(", will retry later").append(e).commit();
timeSource.sleep(getSettings().getConnectInterval());
}
}
assert getSessionStatus() == SessionStatus.SocketConnected; // TODO: may be false when active is set to false from close method
}
/**
* Initiates a LOGON procedure
* @param newFixSession true at the beginning of new session (will cause sequence number reset)
*/
private void logon(boolean newFixSession) {
LOGGER.info().append("Initiating FIX Logon").commit();
try {
assertSessionStatus(SessionStatus.SocketConnected);
sendLogon(newFixSession || getSettings().isResetSequenceNumbersOnEachLogon());
setSessionStatus(SessionStatus.InitiatedLogon);
} catch (Throwable e) {
LOGGER.warn().append("Error sending LOGON request, dropping connection").append(e).commit();
disconnect("LOGON error");
}
}
@Override
public void close() {
super.close();
Thread initiatorThread = this.initiatorThread.get();
if (initiatorThread != null)
initiatorThread.interrupt();
}
}