Skip to content

Commit e98b3ed

Browse files
committed
Enable recording where an interaction was acknowledged first
Activated after an exception is thrown, for minimal performance impact
1 parent 4d372fd commit e98b3ed

File tree

4 files changed

+73
-20
lines changed

4 files changed

+73
-20
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.dv8tion.jda.api.exceptions;
18+
19+
/**
20+
* Used to indicate where an interaction was first acknowledged at, for debugging purposes.
21+
*/
22+
public class FirstAckException extends RuntimeException
23+
{
24+
public FirstAckException()
25+
{
26+
super("This is where the interaction was first acknowledged at");
27+
}
28+
}

src/main/java/net/dv8tion/jda/internal/interactions/InteractionHookImpl.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,6 @@ public InteractionHookImpl(@Nonnull JDA api, @Nonnull String token)
7474
this.isReady = true;
7575
}
7676

77-
public boolean ack()
78-
{
79-
return interaction == null || interaction.ack();
80-
}
81-
8277
public boolean isAck()
8378
{
8479
return interaction == null || interaction.isAcknowledged();

src/main/java/net/dv8tion/jda/internal/interactions/InteractionImpl.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.dv8tion.jda.api.entities.channel.Channel;
2525
import net.dv8tion.jda.api.entities.channel.ChannelType;
2626
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
27+
import net.dv8tion.jda.api.exceptions.FirstAckException;
2728
import net.dv8tion.jda.api.interactions.DiscordLocale;
2829
import net.dv8tion.jda.api.interactions.IntegrationOwners;
2930
import net.dv8tion.jda.api.interactions.Interaction;
@@ -40,9 +41,17 @@
4041
import javax.annotation.Nonnull;
4142
import javax.annotation.Nullable;
4243
import java.util.List;
44+
import java.util.concurrent.ScheduledFuture;
45+
import java.util.concurrent.TimeUnit;
4346

4447
public class InteractionImpl implements Interaction
4548
{
49+
// Enables recording where the first ack is done,
50+
// to help with debugging when an interaction is acknowledged twice
51+
private static boolean recordAckTraces = false;
52+
private static ScheduledFuture<?> scheduledRecordDeactivation;
53+
private Throwable firstAckTrace = null;
54+
4655
protected final long id;
4756
protected final long channelId;
4857
protected final int type;
@@ -139,11 +148,41 @@ else if (guild instanceof DetachedGuildImpl)
139148
public synchronized void releaseHook(boolean success) {}
140149

141150
// Ensures that one cannot acknowledge an interaction twice
142-
public synchronized boolean ack()
151+
@Nullable
152+
public synchronized IllegalStateException tryAck()
143153
{
144-
boolean wasAck = isAck;
145-
this.isAck = true;
146-
return wasAck;
154+
// If not already acknowledged => no exception
155+
if (!isAck)
156+
{
157+
// Store where the first ack was made, so we can use show it on the 2nd ack
158+
if (recordAckTraces)
159+
firstAckTrace = new FirstAckException();
160+
isAck = true;
161+
return null;
162+
}
163+
164+
// Enable saving stack traces of acknowledgements.
165+
// On future acknowledgements, the stack trace of the first ack will be kept, and added to the second ack,
166+
// so the user doesn't have to figure out where the first ack was at.
167+
if (firstAckTrace == null)
168+
{
169+
recordAckTraces = true;
170+
171+
// Stop recording after 15 minutes if the issue was not reproduced
172+
if (scheduledRecordDeactivation != null)
173+
scheduledRecordDeactivation.cancel(false);
174+
scheduledRecordDeactivation = api.getGatewayPool().schedule(() ->
175+
{
176+
recordAckTraces = false;
177+
}, 15, TimeUnit.MINUTES);
178+
return new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once! Retry using this interaction for more details.");
179+
}
180+
else
181+
{
182+
recordAckTraces = false;
183+
scheduledRecordDeactivation.cancel(false);
184+
return new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once!", firstAckTrace);
185+
}
147186
}
148187

149188
@Override

src/main/java/net/dv8tion/jda/internal/requests/restaction/interactions/InteractionCallbackImpl.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,10 @@ public InteractionCallbackAction<T> closeResources()
7979
// Here we intercept calls to queue/submit/complete to prevent double ack/reply scenarios with a better error message than discord provides //
8080
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
8181

82-
// This is an exception factory method that only returns an exception if we would have to throw it or fail in another way.
83-
protected final IllegalStateException tryAck() // note that interaction.ack() is already synchronized so this is actually thread-safe!
84-
{
85-
// true => we already called this before => this will never succeed!
86-
return interaction.ack()
87-
? new IllegalStateException("This interaction has already been acknowledged or replied to. You can only reply or acknowledge an interaction once!")
88-
: null; // null indicates we were successful, no exception means we can't fail :)
89-
}
90-
9182
@Override
9283
public final void queue(Consumer<? super T> success, Consumer<? super Throwable> failure)
9384
{
94-
IllegalStateException exception = tryAck();
85+
IllegalStateException exception = interaction.tryAck();
9586
if (exception != null)
9687
{
9788
if (failure != null)
@@ -108,7 +99,7 @@ public final void queue(Consumer<? super T> success, Consumer<? super Throwable>
10899
@Override
109100
public final CompletableFuture<T> submit(boolean shouldQueue)
110101
{
111-
IllegalStateException exception = tryAck();
102+
IllegalStateException exception = interaction.tryAck();
112103
if (exception != null)
113104
{
114105
CompletableFuture<T> future = new CompletableFuture<>();

0 commit comments

Comments
 (0)