
info.bitrich.xchangestream.binance.BinanceUserDataChannel Maven / Gradle / Ivy
The newest version!
package info.bitrich.xchangestream.binance;
import static java.util.concurrent.TimeUnit.SECONDS;
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.knowm.xchange.binance.BinanceAuthenticated;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binance user data streams must be established by first requesting a unique "listen key" via
* authenticated REST API, which is then used to create an obscured WS URI (rather than
* authenticating the web socket). This class handles the initial request for a listen key, but also
* the 30-minute keepalive REST calls necessary to keep the socket open. It also allows for the
* possibility that extended downtime might cause the listen key to expire without being able to
* renew it. In this case, a new listen key is requested and a caller can be alerted via
* asynchronous callback to re-establish the socket with the new listen key.
*
* @author Graham Crockford
*/
class BinanceUserDataChannel implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(BinanceUserDataChannel.class);
private final BinanceAuthenticated binance;
private final String apiKey;
private final Runnable onApiCall;
private final Disposable keepAlive;
private String listenKey;
private Consumer onChangeListenKey;
/**
* Creates the channel, establishing a listen key (immediately available from {@link
* #getListenKey()}) and starting timers to ensure the channel is kept alive.
*
* @param binance Access to binance services.
* @param apiKey The API key.
* @param onApiCall A callback to perform prior to any service calls.
*/
BinanceUserDataChannel(BinanceAuthenticated binance, String apiKey, Runnable onApiCall) {
this.binance = binance;
this.apiKey = apiKey;
this.onApiCall = onApiCall;
openChannel();
// Send a keepalive every 30 minutes as recommended by Binance
this.keepAlive = Observable.interval(30, TimeUnit.MINUTES).subscribe(x -> keepAlive());
}
/**
* Set this callback to get notified if the current listen key is revoked.
*
* @param onChangeListenKey The callback.
*/
void onChangeListenKey(Consumer onChangeListenKey) {
this.onChangeListenKey = onChangeListenKey;
}
private void keepAlive() {
if (listenKey == null) return;
try {
LOG.debug("Keeping user data channel alive");
onApiCall.run();
binance.keepAliveUserDataStream(apiKey, listenKey);
LOG.debug("User data channel keepalive sent successfully");
} catch (Exception e) {
LOG.error("User data channel keepalive failed.", e);
this.listenKey = null;
reconnect();
}
}
private void reconnect() {
try {
openChannel();
if (onChangeListenKey != null) {
onChangeListenKey.accept(this.listenKey);
}
} catch (Exception e) {
LOG.error("Failed to reconnect. Will retry in 15 seconds.", e);
Observable.timer(15, SECONDS).subscribe(x -> reconnect());
}
}
private void openChannel() {
try {
LOG.debug("Opening new user data channel");
onApiCall.run();
this.listenKey = binance.startUserDataStream(apiKey).getListenKey();
LOG.debug("Opened new user data channel");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @return The current listen key.
* @throws NoActiveChannelException If no listen key is currently available.
*/
String getListenKey() throws NoActiveChannelException {
if (listenKey == null) throw new NoActiveChannelException();
return listenKey;
}
@Override
public void close() {
keepAlive.dispose();
}
/**
* Thrown on calls to {@link BinanceUserDataChannel#getListenKey()} if no channel is currently
* open.
*
* @author Graham Crockford
*/
static final class NoActiveChannelException extends Exception {
private static final long serialVersionUID = -8161003286845820286L;
NoActiveChannelException() {
super();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy