![JAR search and dependency download from the Maven repository](/logo.png)
discord4j.gateway.DefaultGatewayClient Maven / Gradle / Ivy
/*
* This file is part of Discord4J.
*
* Discord4J is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Discord4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Discord4J. If not, see .
*/
package discord4j.gateway;
import discord4j.common.GitProperties;
import discord4j.common.LogUtil;
import discord4j.common.ResettableInterval;
import discord4j.common.close.CloseException;
import discord4j.common.close.CloseStatus;
import discord4j.common.close.DisconnectBehavior;
import discord4j.common.operator.RateLimitOperator;
import discord4j.common.retry.ReconnectContext;
import discord4j.common.retry.ReconnectOptions;
import discord4j.common.sinks.EmissionStrategy;
import discord4j.discordjson.json.gateway.*;
import discord4j.discordjson.possible.Possible;
import discord4j.gateway.json.GatewayPayload;
import discord4j.gateway.limiter.PayloadTransformer;
import discord4j.gateway.payload.PayloadReader;
import discord4j.gateway.payload.PayloadWriter;
import discord4j.gateway.retry.*;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.util.IllegalReferenceCountException;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.netty.ConnectionObserver;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.WebsocketClientSpec;
import reactor.util.Logger;
import reactor.util.Loggers;
import reactor.util.concurrent.Queues;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import reactor.util.retry.Retry;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import static discord4j.common.LogUtil.format;
import static io.netty.handler.codec.http.HttpHeaderNames.USER_AGENT;
import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST;
import static reactor.function.TupleUtils.consumer;
/**
* Represents a Discord WebSocket client, called Gateway, implementing its lifecycle.
*
* Keeps track of a single websocket session by wrapping an instance of {@link GatewayWebsocketHandler} each time a
* new WebSocket connection to Discord is made, therefore only one instance of this class is enough to
* handle the lifecycle of the Gateway operations, that could span multiple WebSocket sessions over time.
*
* Provides automatic reconnecting through a configurable retry policy, allows consumers to receive inbound events
* through {@link #dispatch()}, mapped payloads through {@link #receiver()} and allows a producer to
* submit events through {@link #sender()}.
*
* Provides sending raw {@link ByteBuf} payloads through {@link #sendBuffer(Publisher)} and receiving raw
* {@link ByteBuf} payloads mapped in-flight using a specified mapper using {@link #receiver(Function)}.
*/
public class DefaultGatewayClient implements GatewayClient {
private static final Logger log = Loggers.getLogger(DefaultGatewayClient.class);
private static final Logger senderLog = Loggers.getLogger("discord4j.gateway.protocol.sender");
private static final Logger receiverLog = Loggers.getLogger("discord4j.gateway.protocol.receiver");
// basic properties
private final GatewayReactorResources reactorResources;
private final PayloadReader payloadReader;
private final PayloadWriter payloadWriter;
private final ReconnectOptions reconnectOptions;
private final ReconnectContext reconnectContext;
private final IdentifyOptions identifyOptions;
private final String token;
private final GatewayObserver observer;
private final PayloadTransformer identifyLimiter;
private final ResettableInterval heartbeatEmitter;
private final int maxMissedHeartbeatAck;
private final boolean unpooled;
private final EmissionStrategy emissionStrategy;
private final Map, PayloadHandler>> handlerMap = new HashMap<>();
private final HttpClient httpClient;
/**
* Payloads coming from the websocket.
*/
private final Sinks.Many receiver;
/**
* Payloads that are being sent to the websocket.
*/
private final Sinks.Many sender;
/**
* Inbound gateway events from {@code receiver} that are pushed to consumers.
*/
private final Sinks.Many dispatch;
/**
* Outbound gateway events before they are pushed to {@code sender}.
*/
private final Sinks.Many> outbound;
/**
* A companion to {@code outbound} dedicated to collecting Gateway mandatory heartbeats.
*/
private final Sinks.Many> heartbeats;
/**
* Internal connection state changes are reflected as emissions to this sink.
*/
private final Sinks.Many state;
// Gateway session state tracking across multiple ws connections
private final AtomicInteger sequence = new AtomicInteger(0);
private final AtomicReference sessionId = new AtomicReference<>("");
private final AtomicReference resumeUrl = new AtomicReference<>();
private final AtomicLong lastSent = new AtomicLong(0);
private final AtomicLong lastAck = new AtomicLong(0);
private final AtomicInteger missedAck = new AtomicInteger(0);
private volatile long responseTime = 0;
// References that are changing each time a new ws connection is opened
private volatile Sinks.One disconnectNotifier;
private volatile GatewayWebsocketHandler sessionHandler;
private volatile ContextView currentContext;
/**
* Initializes a new GatewayClient.
*
* @param options the {@link GatewayOptions} to configure this client
*/
public DefaultGatewayClient(GatewayOptions options) {
this.token = Objects.requireNonNull(options.getToken());
this.reactorResources = Objects.requireNonNull(options.getReactorResources());
this.payloadReader = Objects.requireNonNull(options.getPayloadReader());
this.payloadWriter = Objects.requireNonNull(options.getPayloadWriter());
this.reconnectOptions = options.getReconnectOptions();
this.reconnectContext = new ReconnectContext(
this.reconnectOptions.getFirstBackoff(), this.reconnectOptions.getMaxBackoffInterval());
this.identifyOptions = Objects.requireNonNull(options.getIdentifyOptions());
this.observer = options.getInitialObserver();
this.identifyLimiter = Objects.requireNonNull(options.getIdentifyLimiter());
this.maxMissedHeartbeatAck = Math.max(0, options.getMaxMissedHeartbeatAck());
this.unpooled = options.isUnpooled();
this.emissionStrategy = options.getEmissionStrategy();
addHandler(Opcode.DISPATCH, this::handleDispatch);
addHandler(Opcode.HEARTBEAT, this::handleHeartbeat);
addHandler(Opcode.RECONNECT, this::handleReconnect);
addHandler(Opcode.INVALID_SESSION, this::handleInvalidSession);
addHandler(Opcode.HELLO, this::handleHello);
addHandler(Opcode.HEARTBEAT_ACK, this::handleHeartbeatAck);
this.httpClient = initHttpClient();
this.receiver = newEmitterSink();
this.sender = newEmitterSink();
this.dispatch = newEmitterSink();
this.outbound = newEmitterSink();
this.heartbeats = newEmitterSink();
this.heartbeatEmitter = new ResettableInterval(this.reactorResources.getTimerTaskScheduler());
SessionInfo resumeSession = this.identifyOptions.getResumeSession().orElse(null);
if (resumeSession != null) {
this.sequence.set(resumeSession.getSequence());
this.sessionId.set(resumeSession.getId());
this.state = Sinks.many().replay().latestOrDefault(GatewayConnection.State.START_RESUMING);
} else {
this.state = Sinks.many().replay().latestOrDefault(GatewayConnection.State.START_IDENTIFYING);
}
}
private void addHandler(Opcode op, PayloadHandler handler) {
handlerMap.put(op, handler);
}
private static Sinks.Many newEmitterSink() {
return Sinks.many().multicast().onBackpressureBuffer(Queues.SMALL_BUFFER_SIZE, false);
}
@Override
public Mono execute(String gatewayUrl) {
return Mono.deferContextual(
context -> {
currentContext = context;
disconnectNotifier = Sinks.one();
lastAck.set(0);
lastSent.set(0);
missedAck.set(0);
Sinks.Empty ping = Sinks.empty();
// Setup the sending logic from multiple sources into one merged Flux
Mono onConnected = state.asFlux().filter(s -> s == GatewayConnection.State.CONNECTED).next().then();
Flux heartbeatFlux = heartbeats.asFlux()
.flatMap(payload -> Flux.from(payloadWriter.write(payload)));
Flux identifyFlux = outbound.asFlux()
.filter(payload -> Opcode.IDENTIFY.equals(payload.getOp()))
.delayUntil(__ -> ping.asMono())
.flatMap(payload -> Flux.from(payloadWriter.write(payload)))
.transform(identifyLimiter);
Flux resumeFlux = outbound.asFlux()
.filter(payload -> Opcode.RESUME.equals(payload.getOp()))
.flatMap(payload -> Flux.from(payloadWriter.write(payload)));
Flux payloadFlux = outbound.asFlux()
.filter(DefaultGatewayClient::isNotStartupPayload)
.delayUntil(__ -> onConnected)
.flatMap(payload -> Flux.from(payloadWriter.write(payload)))
.transform(buf -> Flux.merge(buf, sender.asFlux()))
.transform(new RateLimitOperator<>(outboundLimiterCapacity(), Duration.ofSeconds(60),
reactorResources.getTimerTaskScheduler(),
reactorResources.getPayloadSenderScheduler()));
Flux outFlux = Flux.merge(heartbeatFlux, identifyFlux, resumeFlux, payloadFlux)
.doOnNext(buf -> logPayload(senderLog, context, buf))
.doOnDiscard(ByteBuf.class, DefaultGatewayClient::safeRelease);
sessionHandler = new GatewayWebsocketHandler(receiver, outFlux, context);
Mono readyHandler = dispatch.asFlux()
.filter(DefaultGatewayClient::isReadyOrResumed)
.zipWith(state.asFlux().next().repeat())
.doOnNext(consumer((event, currentState) -> {
ConnectionObserver.State observerState;
if (currentState == GatewayConnection.State.START_IDENTIFYING
|| currentState == GatewayConnection.State.START_RESUMING) {
log.info(format(context, "Connected to Gateway"));
emissionStrategy.emitNext(dispatch, GatewayStateChange.connected());
observerState = GatewayObserver.CONNECTED;
} else {
log.info(format(context, "Reconnected to Gateway"));
emissionStrategy.emitNext(dispatch,
GatewayStateChange.retrySucceeded(reconnectContext.getAttempts()));
observerState = GatewayObserver.RETRY_SUCCEEDED;
}
reconnectContext.reset();
state.emitNext(GatewayConnection.State.CONNECTED, FAIL_FAST);
notifyObserver(observerState);
}))
.then();
// Subscribe the receiver to process and transform the inbound payloads into Dispatch events
Mono receiverFuture = receiver.asFlux()
.map(buf -> unpooled ? buf : buf.retain())
.doOnNext(buf -> logPayload(receiverLog, context, buf))
.flatMap(payloadReader::read)
.doOnDiscard(ByteBuf.class, DefaultGatewayClient::safeRelease)
.doOnNext(payload -> {
if (Opcode.HEARTBEAT_ACK.equals(payload.getOp())) {
ping.emitEmpty(FAIL_FAST);
}
})
.map(this::updateSequence)
.flatMap(this::handlePayload)
.then();
// Subscribe the handler's outbound exchange with our outbound signals
// routing completion signals to close the gateway
Mono senderFuture = outbound.asFlux()
.doOnComplete(sessionHandler::close)
.doOnNext(payload -> {
if (Opcode.RECONNECT.equals(payload.getOp())) {
sessionHandler.error(
new GatewayException(context, "Reconnecting due to user action"));
}
})
.then();
// Create the heartbeat loop, and subscribe it using the sender sink
Mono heartbeatHandler = heartbeatEmitter.ticks()
.flatMap(t -> {
long now = System.nanoTime();
lastAck.compareAndSet(0, now);
long delay = now - lastAck.get();
if (lastSent.get() - lastAck.get() > 0) {
if (missedAck.incrementAndGet() > maxMissedHeartbeatAck) {
log.warn(format(context, "Missing heartbeat ACK for {} (tick: {}, seq: {})"),
Duration.ofNanos(delay), t, sequence.get());
sessionHandler.error(new GatewayException(context,
"Reconnecting due to zombie or failed connection"));
return Mono.empty();
}
}
log.debug(format(context, "Sending heartbeat {} after last ACK"),
Duration.ofNanos(delay));
lastSent.set(now);
return Mono.just(GatewayPayload.heartbeat(ImmutableHeartbeat.of(sequence.get())));
})
.doOnNext(tick -> emissionStrategy.emitNext(heartbeats, tick))
.then();
Mono httpFuture = httpClient
.websocket(WebsocketClientSpec.builder()
.maxFramePayloadLength(Integer.MAX_VALUE)
.build())
.uri(buildGatewayUrl(gatewayUrl))
.handle(sessionHandler::handle)
.contextWrite(LogUtil.clearContext())
.flatMap(t2 -> handleClose(t2.getT1(), t2.getT2()))
.then();
return Mono.zip(httpFuture, readyHandler, receiverFuture, senderFuture, heartbeatHandler)
.doOnError(t -> {
if (t instanceof ReconnectException) {
log.info(format(context, "{}"), t.getMessage());
} else if (t instanceof CloseException || t instanceof GatewayException) {
log.warn(format(context, "{}"), t.toString());
} else {
log.error(format(context, "Gateway client error"), t);
}
})
.doOnTerminate(heartbeatEmitter::stop)
.doOnCancel(() -> sessionHandler.close())
.then();
})
.contextWrite(ctx -> ctx.put(LogUtil.KEY_SHARD_ID, identifyOptions.getShardInfo().getIndex()))
.retryWhen(retryFactory())
.then(Mono.defer(() -> disconnectNotifier.asMono().then()))
.doOnSubscribe(s -> {
if (disconnectNotifier != null) {
throw new IllegalStateException("execute can only be subscribed once");
}
});
}
private String buildGatewayUrl(String identifyGatewayUrl) {
QueryStringDecoder query = new QueryStringDecoder(identifyGatewayUrl);
return Optional.ofNullable(resumeUrl.get())
.map(url -> url + '?' + query.rawQuery())
.orElse(identifyGatewayUrl);
}
private HttpClient initHttpClient() {
HttpClient client = reactorResources.getHttpClient()
.headers(headers -> headers.add(USER_AGENT, initUserAgent()));
if (observer == GatewayObserver.NOOP_LISTENER) {
// don't apply an observer if the feature is not used
return client;
} else {
return client.observe((connection, newState) -> notifyObserver(newState));
}
}
private String initUserAgent() {
final Properties properties = GitProperties.getProperties();
final String version = properties.getProperty(GitProperties.APPLICATION_VERSION, "3");
final String url = properties.getProperty(GitProperties.APPLICATION_URL, "https://discord4j.com");
return "DiscordBot(" + url + ", " + version + ")";
}
private void notifyObserver(ConnectionObserver.State state) {
observer.onStateChange(state, this);
}
private void logPayload(Logger logger, ContextView context, ByteBuf buf) {
if (logger.isTraceEnabled()) {
logger.trace(format(context, buf.toString(StandardCharsets.UTF_8)
.replaceAll("(\"token\": ?\")([A-Za-z0-9._-]*)(\")", "$1hunter2$3")));
}
}
private static boolean isNotStartupPayload(GatewayPayload> payload) {
return !Opcode.IDENTIFY.equals(payload.getOp()) && !Opcode.RESUME.equals(payload.getOp());
}
private static boolean isReadyOrResumed(Dispatch d) {
return Ready.class.isAssignableFrom(d.getClass()) || Resumed.class.isAssignableFrom(d.getClass());
}
private GatewayPayload> updateSequence(GatewayPayload> payload) {
if (payload.getSequence() != null) {
sequence.set(payload.getSequence());
notifyObserver(GatewayObserver.SEQUENCE);
}
return payload;
}
@SuppressWarnings("unchecked")
private Mono handlePayload(GatewayPayload payload) {
PayloadHandler handler = (PayloadHandler) handlerMap.get(payload.getOp());
if (handler == null) {
log.warn(format(currentContext, "Handler not found from: {}"), payload);
return Mono.empty();
}
return Mono.defer(() -> handler.handle(payload))
.checkpoint("Dispatch handled for OP " + payload.getOp().getRawOp() +
" seq " + payload.getSequence() + " type " + payload.getType());
}
private Mono handleDispatch(GatewayPayload payload) {
if (payload.getData() instanceof Ready) {
Ready ready = (Ready) payload.getData();
sessionId.set(ready.sessionId());
resumeUrl.set(ready.resumeGatewayUrl());
}
if (payload.getData() != null) {
emissionStrategy.emitNext(dispatch, payload.getData());
}
return Mono.empty();
}
private Mono handleHeartbeat(GatewayPayload payload) {
log.debug(format(currentContext, "Received heartbeat"));
emissionStrategy.emitNext(outbound, GatewayPayload.heartbeat(ImmutableHeartbeat.of(sequence.get())));
return Mono.empty();
}
private Mono handleReconnect(GatewayPayload> payload) {
sessionHandler.error(new ReconnectException(currentContext, "Reconnecting due to reconnect packet received"));
return Mono.empty();
}
private Mono handleInvalidSession(GatewayPayload payload) {
//noinspection ConstantConditions
if (payload.getData().resumable()) {
emissionStrategy.emitNext(outbound,
GatewayPayload.resume(ImmutableResume.of(token, sessionId.get(), sequence.get())));
} else {
resumeUrl.set(null);
sessionHandler.error(new InvalidSessionException(currentContext,
"Reconnecting due to non-resumable session invalidation"));
}
return Mono.empty();
}
private Mono handleHello(GatewayPayload payload) {
//noinspection ConstantConditions
Duration interval = Duration.ofMillis(payload.getData().heartbeatInterval());
heartbeatEmitter.start(Duration.ZERO, interval);
return state.asFlux()
.next()
.doOnNext(state -> {
if (state == GatewayConnection.State.START_RESUMING || state == GatewayConnection.State.RESUMING) {
doResume(payload);
} else {
doIdentify(payload);
}
})
.then();
}
private void doResume(GatewayPayload payload) {
log.debug(format(currentContext, "Resuming Gateway session from {}"), sequence.get());
emissionStrategy.emitNext(outbound,
GatewayPayload.resume(ImmutableResume.of(token, sessionId.get(), sequence.get())));
}
private void doIdentify(GatewayPayload payload) {
IdentifyProperties props = ImmutableIdentifyProperties.of(System.getProperty("os.name"), "Discord4J",
"Discord4J");
Identify identify = Identify.builder()
.token(token)
.intents(identifyOptions.getIntents().map(set -> Possible.of(set.getRawValue())).orElse(Possible.absent()))
.properties(props)
.compress(false)
.largeThreshold(identifyOptions.getLargeThreshold())
.shard(identifyOptions.getShardInfo().asArray())
.presence(identifyOptions.getInitialStatus().map(Possible::of).orElse(Possible.absent()))
.build();
log.debug(format(currentContext, "Identifying to Gateway"), sequence.get());
emissionStrategy.emitNext(outbound, GatewayPayload.identify(identify));
}
private Mono handleHeartbeatAck(GatewayPayload> context) {
responseTime = lastAck.updateAndGet(x -> System.nanoTime()) - lastSent.get();
missedAck.set(0);
log.debug(format(currentContext, "Heartbeat acknowledged after {}"), getResponseTime());
return Mono.empty();
}
private Retry retryFactory() {
return GatewayRetrySpec.create(reconnectOptions, reconnectContext)
.doBeforeRetry(retry -> {
state.emitNext(retry.nextState(), FAIL_FAST);
long attempt = retry.iteration();
Duration backoff = retry.nextBackoff();
log.debug(format(getContextFromException(retry.failure()),
"{} in {} (attempts: {})"), retry.nextState(), backoff, attempt);
if (retry.iteration() == 1) {
if (retry.nextState() == GatewayConnection.State.RESUMING) {
emissionStrategy.emitNext(dispatch, GatewayStateChange.retryStarted(backoff));
notifyObserver(GatewayObserver.RETRY_STARTED);
} else {
emissionStrategy.emitNext(dispatch, GatewayStateChange.retryStartedResume(backoff));
notifyObserver(GatewayObserver.RETRY_RESUME_STARTED);
}
} else {
emissionStrategy.emitNext(dispatch, GatewayStateChange.retryFailed(attempt - 1, backoff));
notifyObserver(GatewayObserver.RETRY_FAILED);
}
if (retry.nextState() == GatewayConnection.State.RECONNECTING) {
emissionStrategy.emitNext(dispatch, GatewayStateChange.sessionInvalidated());
}
});
}
private ContextView getContextFromException(Throwable t) {
if (t instanceof CloseException) {
return ((CloseException) t).getContext();
}
if (t instanceof GatewayException) {
return ((GatewayException) t).getContext();
}
return Context.empty();
}
private Mono handleClose(DisconnectBehavior sourceBehavior, CloseStatus closeStatus) {
return Mono.deferContextual(ctx -> {
DisconnectBehavior behavior;
if (GatewayRetrySpec.NON_RETRYABLE_STATUS_CODES.contains(closeStatus.getCode())) {
// non-retryable close codes are non-transient errors therefore stopping is the only choice
behavior = DisconnectBehavior.stop(sourceBehavior.getCause());
} else {
behavior = sourceBehavior;
}
log.debug(format(ctx, "Closing and {} with status {}"), behavior, closeStatus);
state.emitNext(GatewayConnection.State.DISCONNECTING, FAIL_FAST);
heartbeatEmitter.stop();
if (behavior.getAction() == DisconnectBehavior.Action.STOP_ABRUPTLY) {
emissionStrategy.emitNext(dispatch, GatewayStateChange.disconnectedResume());
notifyObserver(GatewayObserver.DISCONNECTED_RESUME);
} else if (behavior.getAction() == DisconnectBehavior.Action.STOP) {
emissionStrategy.emitNext(dispatch, GatewayStateChange.disconnected(sourceBehavior, closeStatus));
sequence.set(0);
sessionId.set("");
notifyObserver(GatewayObserver.DISCONNECTED);
}
switch (behavior.getAction()) {
case STOP_ABRUPTLY:
case STOP:
reconnectContext.clear();
responseTime = 0;
lastSent.set(0);
lastAck.set(0);
state.emitNext(GatewayConnection.State.DISCONNECTED, FAIL_FAST);
if (behavior.getCause() != null) {
return Mono.just(new CloseException(closeStatus, ctx, behavior.getCause()))
.flatMap(ex -> {
disconnectNotifier.emitError(ex, FAIL_FAST);
return Mono.error(ex);
});
}
return Mono.just(closeStatus)
.doOnNext(status -> disconnectNotifier.emitValue(closeStatus, FAIL_FAST));
case RETRY_ABRUPTLY:
case RETRY:
default:
return Mono.error(new CloseException(closeStatus, ctx, behavior.getCause()));
}
});
}
@Override
public Mono close(boolean allowResume) {
return Mono.defer(() -> {
if (sessionHandler == null || disconnectNotifier == null) {
return Mono.error(new IllegalStateException("Gateway client is not active!"));
}
if (allowResume) {
sessionHandler.close(DisconnectBehavior.stopAbruptly(null));
} else {
sessionHandler.close(DisconnectBehavior.stop(null));
}
return disconnectNotifier.asMono();
});
}
@Override
public Flux dispatch() {
return dispatch.asFlux();
}
@Override
public Flux> receiver() {
return receiver(payloadReader::read);
}
@Override
public Flux receiver(Function> mapper) {
return receiver.asFlux()
.map(ByteBuf::retainedDuplicate)
.doOnDiscard(ByteBuf.class, DefaultGatewayClient::safeRelease)
.flatMap(mapper);
}
private static void safeRelease(ByteBuf buf) {
if (buf.refCnt() > 0) {
try {
buf.release();
} catch (IllegalReferenceCountException e) {
if (log.isDebugEnabled()) {
log.debug("", e);
}
}
}
}
@Override
public Sinks.Many> sender() {
return outbound;
}
@Override
public Mono sendBuffer(Publisher publisher) {
return Flux.from(publisher).doOnNext(buf -> emissionStrategy.emitNext(sender, buf)).then();
}
@Override
public int getShardCount() {
return identifyOptions.getShardInfo().getCount();
}
@Override
public String getSessionId() {
return sessionId.get();
}
@Override
public int getSequence() {
return sequence.get();
}
@Override
public Flux stateEvents() {
return state.asFlux();
}
@Override
public Mono isConnected() {
return state.asFlux().next()
.filter(s -> s == GatewayConnection.State.CONNECTED)
.hasElement()
.defaultIfEmpty(false);
}
@Override
public Duration getResponseTime() {
return Duration.ofNanos(responseTime);
}
/**
* JVM property that allows modifying the number of outbound payloads permitted before activating the
* rate-limiter and delaying every following payload for 60 seconds. Default value: 115 permits
*/
private static final String OUTBOUND_CAPACITY_PROPERTY = "discord4j.gateway.outbound.capacity";
private int outboundLimiterCapacity() {
String capacityValue = System.getProperty(OUTBOUND_CAPACITY_PROPERTY);
if (capacityValue != null) {
try {
int capacity = Integer.parseInt(capacityValue);
log.info("Overriding default outbound limiter capacity: {}", capacity);
return capacity;
} catch (NumberFormatException e) {
log.warn("Invalid custom outbound limiter capacity: {}", capacityValue);
}
}
return 115;
}
}