Skip to content

Commit

Permalink
Implement a timeout for unavailable guilds (#1469)
Browse files Browse the repository at this point in the history
Add GuildTimeoutEvent and some docs
  • Loading branch information
MinnDevelopment committed Mar 8, 2021
1 parent c7178cf commit 769601e
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 75 deletions.
3 changes: 2 additions & 1 deletion src/main/java/net/dv8tion/jda/api/events/ReadyEvent.java
Expand Up @@ -37,7 +37,8 @@ public ReadyEvent(@Nonnull JDA api, long responseNumber)
{
super(api, responseNumber);
this.availableGuilds = (int) getJDA().getGuildCache().size();
this.unavailableGuilds = ((JDAImpl) getJDA()).getGuildSetupController().getSetupNodes(GuildSetupController.Status.UNAVAILABLE).size();
GuildSetupController setupController = ((JDAImpl) getJDA()).getGuildSetupController();
this.unavailableGuilds = setupController.getSetupNodes(GuildSetupController.Status.UNAVAILABLE).size() + setupController.getUnavailableGuilds().size();
}

/**
Expand Down
Expand Up @@ -18,6 +18,7 @@

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.ReadyEvent;

import javax.annotation.Nonnull;

Expand All @@ -29,6 +30,12 @@
* setup and full reconnects (indicated by {@link net.dv8tion.jda.api.events.ReconnectedEvent ReconnectedEvent}).
*
* <p>Can be used to initialize any services that depend on this guild.
*
* <p>When a guild fails to ready up due to Discord outages you will not receive this event.
* Guilds that fail to ready up will either timeout or get marked as unavailable.
* <br>You can use {@link ReadyEvent#getGuildUnavailableCount()} and {@link JDA#getUnavailableGuilds()} to check for unavailable guilds.
* {@link GuildTimeoutEvent} will be fired for guilds that don't ready up and also don't get marked as unavailable by Discord.
* Guilds that timeout will be marked as unavailable by the timeout event, they will <b>not</b> fire a {@link GuildUnavailableEvent} as that event is only indicating that a guild becomes unavailable <b>after</b> ready happened.
*/
public class GuildReadyEvent extends GenericGuildEvent
{
Expand Down
@@ -0,0 +1,67 @@
/*
* Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
*
* 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 net.dv8tion.jda.api.events.guild;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.events.Event;
import net.dv8tion.jda.api.events.ReadyEvent;

import javax.annotation.Nonnull;

/**
* Indicates that a guild failed to ready up and timed out.
* <br>Usually this event will be fired right before a {@link net.dv8tion.jda.api.events.ReadyEvent ReadyEvent}.
*
* <p>This will mark the guild as <b>unavailable</b> and it will not be usable when JDA becomes ready.
* You can check all unavailable guilds with {@link ReadyEvent#getGuildUnavailableCount()} and {@link JDA#getUnavailableGuilds()}.
*
* <h2>Developer Note</h2>
*
* <p>Discord may also explicitly mark guilds as unavailable during the setup, in which case this event will not fire.
* It is recommended to check for unavailable guilds in the ready event explicitly to avoid any ambiguity.
*/
public class GuildTimeoutEvent extends Event
{
private final long guildId;

public GuildTimeoutEvent(@Nonnull JDA api, long guildId)
{
super(api);
this.guildId = guildId;
}

/**
* The guild id for the timed out guild
*
* @return The guild id
*/
public long getGuildIdLong()
{
return guildId;
}

/**
* The guild id for the timed out guild
*
* @return The guild id
*/
@Nonnull
public String getGuildId()
{
return Long.toUnsignedString(guildId);
}
}
Expand Up @@ -24,6 +24,7 @@
/**
* Indicates that you joined a {@link net.dv8tion.jda.api.entities.Guild Guild} that is not yet available.
* <b>This does not extend {@link net.dv8tion.jda.api.events.guild.GenericGuildEvent GenericGuildEvent}</b>
* <br>This will be followed by a {@link GuildAvailableEvent} once the guild becomes available again.
*
* <p>Can be used to retrieve id of new unavailable Guild.
*/
Expand Down
Expand Up @@ -268,6 +268,7 @@ public void onPrivateChannelDelete(@Nonnull PrivateChannelDeleteEvent event) {}

//Guild Events
public void onGuildReady(@Nonnull GuildReadyEvent event) {}
public void onGuildTimeout(@Nonnull GuildTimeoutEvent event) {}
public void onGuildJoin(@Nonnull GuildJoinEvent event) {}
public void onGuildLeave(@Nonnull GuildLeaveEvent event) {}
public void onGuildAvailable(@Nonnull GuildAvailableEvent event) {}
Expand Down
Expand Up @@ -16,14 +16,12 @@

package net.dv8tion.jda.internal.handle;

import gnu.trove.iterator.TLongLongIterator;
import gnu.trove.iterator.TLongObjectIterator;
import gnu.trove.map.TLongLongMap;
import gnu.trove.map.TLongObjectMap;
import gnu.trove.map.hash.TLongLongHashMap;
import gnu.trove.map.hash.TLongObjectHashMap;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import net.dv8tion.jda.api.events.guild.GuildTimeoutEvent;
import net.dv8tion.jda.api.events.guild.UnavailableGuildLeaveEvent;
import net.dv8tion.jda.api.utils.MiscUtil;
import net.dv8tion.jda.api.utils.data.DataArray;
Expand All @@ -35,21 +33,24 @@
import org.slf4j.Logger;

import javax.annotation.Nullable;
import java.util.*;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@SuppressWarnings("WeakerAccess")
public class GuildSetupController
{
protected static final int CHUNK_TIMEOUT = 10000;
protected static final Logger log = JDALogger.getLog(GuildSetupController.class);

private static final long timeoutDuration = 75; // seconds
private static final int timeoutThreshold = 60; // Half of 120 rate limit

private final JDAImpl api;
private final TLongObjectMap<GuildSetupNode> setupNodes = new TLongObjectHashMap<>();
private final TLongSet chunkingGuilds = new TLongHashSet();
private final TLongLongMap pendingChunks = new TLongLongHashMap();
private final TLongSet unavailableGuilds = new TLongHashSet();

// TODO: Rewrite this incompleteCount system to just rely on the state of each node
Expand Down Expand Up @@ -91,7 +92,7 @@ void remove(long id)
unavailableGuilds.remove(id);
setupNodes.remove(id);
chunkingGuilds.remove(id);
synchronized (pendingChunks) { pendingChunks.remove(id); }
checkReady();
}

public void ready(long id)
Expand All @@ -107,21 +108,24 @@ private void checkReady()
WebSocketClient client = getJDA().getClient();
// If no guilds are marked as incomplete we can fire a ready
if (incompleteCount < 1 && !client.isReady())
{
if (timeoutHandle != null)
timeoutHandle.cancel(false);
timeoutHandle = null;
client.ready();
else // otherwise see if we can chunk any guilds
tryChunking();
}
else if (incompleteCount <= timeoutThreshold)
{
startTimeout(); // try to timeout the other guilds
}
}

public boolean setIncompleteCount(int count)
{
log.debug("Setting incomplete count to {}", count);
this.incompleteCount = count;
boolean ready = count == 0;
if (ready)
getJDA().getClient().ready();
else
startTimeout();
return !ready;
log.debug("Setting incomplete count to {}", incompleteCount);
checkReady();
return count != 0;
}

public void onReady(long id, DataObject obj)
Expand Down Expand Up @@ -191,7 +195,6 @@ public boolean onDelete(long id, DataObject obj)
{
// Allow other guilds to start chunking
chunkingGuilds.remove(id);
synchronized (pendingChunks) { pendingChunks.remove(id); }
incompleteCount--;
}
}
Expand All @@ -218,10 +221,6 @@ public void onMemberChunk(long id, DataObject chunk)
int index = chunk.getInt("chunk_index");
int count = chunk.getInt("chunk_count");
log.debug("Received member chunk for guild id: {} size: {} index: {}/{}", id, members.length(), index, count);
synchronized (pendingChunks)
{
pendingChunks.remove(id);
}
GuildSetupNode node = setupNodes.get(id);
if (node != null)
node.handleMemberChunk(MemberChunkManager.isLastChunk(chunk), members);
Expand Down Expand Up @@ -286,16 +285,13 @@ public void clearCache()
unavailableGuilds.clear();
incompleteCount = 0;
close();
synchronized (pendingChunks)
{
pendingChunks.clear();
}
}

public void close()
{
if (timeoutHandle != null)
timeoutHandle.cancel(false);
timeoutHandle = null;
}

public boolean containsMember(long userId, @Nullable GuildSetupNode excludedNode)
Expand Down Expand Up @@ -356,21 +352,6 @@ void sendChunkRequest(Object obj)
{
log.debug("Sending chunking requests for {} guilds", obj instanceof DataArray ? ((DataArray) obj).length() : 1);

long timeout = System.currentTimeMillis() + CHUNK_TIMEOUT;
synchronized (pendingChunks)
{
if (obj instanceof DataArray)
{
DataArray arr = (DataArray) obj;
for (Object o : arr)
pendingChunks.put((long) o, timeout);
}
else
{
pendingChunks.put((long) obj, timeout);
}
}

getJDA().getClient().sendChunkRequest(
DataObject.empty()
.put("guild_id", obj)
Expand All @@ -390,7 +371,11 @@ private void tryChunking()

private void startTimeout()
{
timeoutHandle = getJDA().getGatewayPool().scheduleAtFixedRate(new ChunkTimeout(), CHUNK_TIMEOUT, CHUNK_TIMEOUT, TimeUnit.MILLISECONDS);
if (timeoutHandle != null || incompleteCount < 1) // We don't need to start a timeout for 0 guilds
return;

log.debug("Starting {} second timeout for {} guilds", timeoutDuration, incompleteCount);
timeoutHandle = getJDA().getGatewayPool().schedule(this::onTimeout, timeoutDuration, TimeUnit.SECONDS);
}

public void onUnavailable(long id)
Expand All @@ -399,6 +384,25 @@ public void onUnavailable(long id)
log.debug("Guild with id {} is now marked unavailable. Total: {}", id, unavailableGuilds.size());
}

public void onTimeout()
{
if (incompleteCount < 1)
return;
log.warn("Automatically marking {} guilds as unavailable due to timeout!", incompleteCount);
TLongObjectIterator<GuildSetupNode> iterator = setupNodes.iterator();
while (iterator.hasNext())
{
iterator.advance();
GuildSetupNode node = iterator.value();
iterator.remove();
unavailableGuilds.add(node.getIdLong());
// Inform users that the guild timed out
getJDA().handleEvent(new GuildTimeoutEvent(getJDA(), node.getIdLong()));
}
incompleteCount = 0;
checkReady();
}

public enum Status
{
INIT,
Expand All @@ -414,37 +418,4 @@ public interface StatusListener
{
void onStatusChange(long guildId, Status oldStatus, Status newStatus);
}

private class ChunkTimeout implements Runnable
{
@Override
public void run()
{
if (pendingChunks.isEmpty())
return;
synchronized (pendingChunks)
{
TLongLongIterator it = pendingChunks.iterator();
List<DataArray> requests = new LinkedList<>();
DataArray arr = DataArray.empty();
while (it.hasNext())
{
// key=guild_id, value=timeout
it.advance();
if (System.currentTimeMillis() <= it.value())
continue;
arr.add(it.key());

if (arr.length() == 50)
{
requests.add(arr);
arr = DataArray.empty();
}
}
if (arr.length() > 0)
requests.add(arr);
requests.forEach(GuildSetupController.this::sendChunkRequest);
}
}
}
}

0 comments on commit 769601e

Please sign in to comment.