Skip to content

Commit 089b8ec

Browse files
committed
Resolve merge conflicts
Signed-off-by: Chris Sdogkos <[email protected]>
2 parents 5d5ae05 + 5bc588e commit 089b8ec

File tree

7 files changed

+308
-8
lines changed

7 files changed

+308
-8
lines changed

CLA.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,6 @@ IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 5. AND SECTION 6. CANNO
115115

116116
### Us
117117

118-
Name: Daniel Tischner (aka Zabuzard, acting on behalf of Together Java)
119-
120118
Organization: https://github.com/Together-Java
121119

122120
Contact: https://discord.com/invite/XXFUXzK
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.togetherjava.tjbot.features;
2+
3+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
4+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
5+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
6+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
7+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
8+
9+
import java.util.regex.Pattern;
10+
11+
/**
12+
* Receives incoming Discord guild events from voice channels matching a given pattern.
13+
* <p>
14+
* All voice receivers have to implement this interface. For convenience, there is a
15+
* {@link VoiceReceiverAdapter} available that implemented most methods already. A new receiver can
16+
* then be registered by adding it to {@link Features}.
17+
* <p>
18+
* <p>
19+
* After registration, the system will notify a receiver whenever a new event was sent or an
20+
* existing event was updated in any channel matching the {@link #getChannelNamePattern()} the bot
21+
* is added to.
22+
*/
23+
public interface VoiceReceiver extends Feature {
24+
/**
25+
* Retrieves the pattern matching the names of channels of which this receiver is interested in
26+
* receiving events from. Called by the core system once during the startup in order to register
27+
* the receiver accordingly.
28+
* <p>
29+
* Changes on the pattern returned by this method afterwards will not be picked up.
30+
*
31+
* @return the pattern matching the names of relevant channels
32+
*/
33+
Pattern getChannelNamePattern();
34+
35+
/**
36+
* Triggered by the core system whenever a member joined, left or moved voice channels.
37+
*
38+
* @param event the event that triggered this
39+
*/
40+
void onVoiceUpdate(GuildVoiceUpdateEvent event);
41+
42+
/**
43+
* Triggered by the core system whenever a member toggled their camera in a voice channel.
44+
*
45+
* @param event the event that triggered this
46+
*/
47+
void onVideoToggle(GuildVoiceVideoEvent event);
48+
49+
/**
50+
* Triggered by the core system whenever a member started or stopped a stream.
51+
*
52+
* @param event the event that triggered this
53+
*/
54+
void onStreamToggle(GuildVoiceStreamEvent event);
55+
56+
/**
57+
* Triggered by the core system whenever a member toggled their mute status.
58+
*
59+
* @param event the event that triggered this
60+
*/
61+
void onMuteToggle(GuildVoiceMuteEvent event);
62+
63+
/**
64+
* Triggered by the core system whenever a member toggled their deafened status.
65+
*
66+
* @param event the event that triggered this
67+
*/
68+
void onDeafenToggle(GuildVoiceDeafenEvent event);
69+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.togetherjava.tjbot.features;
2+
3+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceDeafenEvent;
4+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMuteEvent;
5+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceStreamEvent;
6+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
7+
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceVideoEvent;
8+
9+
import java.util.regex.Pattern;
10+
11+
public class VoiceReceiverAdapter implements VoiceReceiver {
12+
13+
private final Pattern channelNamePattern;
14+
15+
protected VoiceReceiverAdapter() {
16+
this(Pattern.compile(".*"));
17+
}
18+
19+
protected VoiceReceiverAdapter(Pattern channelNamePattern) {
20+
this.channelNamePattern = channelNamePattern;
21+
}
22+
23+
@Override
24+
public Pattern getChannelNamePattern() {
25+
return channelNamePattern;
26+
}
27+
28+
@Override
29+
public void onVoiceUpdate(GuildVoiceUpdateEvent event) {
30+
// Adapter does not react by default, subclasses may change this behavior
31+
}
32+
33+
@Override
34+
public void onVideoToggle(GuildVoiceVideoEvent event) {
35+
// Adapter does not react by default, subclasses may change this behavior
36+
}
37+
38+
@Override
39+
public void onStreamToggle(GuildVoiceStreamEvent event) {
40+
// Adapter does not react by default, subclasses may change this behavior
41+
}
42+
43+
@Override
44+
public void onMuteToggle(GuildVoiceMuteEvent event) {
45+
// Adapter does not react by default, subclasses may change this behavior
46+
}
47+
48+
@Override
49+
public void onDeafenToggle(GuildVoiceDeafenEvent event) {
50+
// Adapter does not react by default, subclasses may change this behavior
51+
}
52+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.togetherjava.tjbot.features.rss;
2+
3+
import java.time.ZonedDateTime;
4+
5+
record FailureState(int count, ZonedDateTime lastFailure) {
6+
}

application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.apptasticsoftware.rssreader.Item;
44
import com.apptasticsoftware.rssreader.RssReader;
5+
import com.github.benmanes.caffeine.cache.Cache;
6+
import com.github.benmanes.caffeine.cache.Caffeine;
57
import net.dv8tion.jda.api.EmbedBuilder;
68
import net.dv8tion.jda.api.JDA;
79
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
@@ -48,7 +50,7 @@
4850
* <p>
4951
* To include a new RSS feed, simply define an {@link RSSFeed} entry in the {@code "rssFeeds"} array
5052
* within the configuration file, adhering to the format shown below:
51-
*
53+
*
5254
* <pre>
5355
* {@code
5456
* {
@@ -58,7 +60,7 @@
5860
* }
5961
* }
6062
* </pre>
61-
*
63+
* <p>
6264
* Where:
6365
* <ul>
6466
* <li>{@code url} represents the URL of the RSS feed.</li>
@@ -84,6 +86,14 @@ public final class RSSHandlerRoutine implements Routine {
8486
private final int interval;
8587
private final Database database;
8688

89+
private final Cache<String, FailureState> circuitBreaker =
90+
Caffeine.newBuilder().expireAfterWrite(7, TimeUnit.DAYS).maximumSize(500).build();
91+
92+
private static final int DEAD_RSS_FEED_FAILURE_THRESHOLD = 15;
93+
private static final double BACKOFF_BASE = 2.0;
94+
private static final double BACKOFF_EXPONENT_OFFSET = 1.0;
95+
private static final double MAX_BACKOFF_HOURS = 24.0;
96+
8797
/**
8898
* Constructs an RSSHandlerRoutine with the provided configuration and database.
8999
*
@@ -117,7 +127,14 @@ public Schedule createSchedule() {
117127

118128
@Override
119129
public void runRoutine(@Nonnull JDA jda) {
120-
this.config.feeds().forEach(feed -> sendRSS(jda, feed));
130+
this.config.feeds().forEach(feed -> {
131+
if (isBackingOff(feed.url())) {
132+
logger.debug("Skipping RSS feed (Backing off): {}", feed.url());
133+
return;
134+
}
135+
136+
sendRSS(jda, feed);
137+
});
121138
}
122139

123140
/**
@@ -257,7 +274,6 @@ private void postItem(List<TextChannel> textChannels, Item rssItem, RSSFeed feed
257274
* @param rssFeedRecord the record representing the RSS feed, can be null if not found in the
258275
* database
259276
* @param lastPostedDate the last posted date to be updated
260-
*
261277
* @throws DateTimeParseException if the date cannot be parsed
262278
*/
263279
private void updateLastDateToDatabase(RSSFeed feedConfig, @Nullable RssFeedRecord rssFeedRecord,
@@ -400,9 +416,26 @@ private MessageCreateData constructMessage(Item item, RSSFeed feedConfig) {
400416
*/
401417
private List<Item> fetchRSSItemsFromURL(String rssUrl) {
402418
try {
403-
return rssReader.read(rssUrl).toList();
419+
List<Item> items = rssReader.read(rssUrl).toList();
420+
circuitBreaker.invalidate(rssUrl);
421+
return items;
404422
} catch (IOException e) {
405-
logger.error("Could not fetch RSS from URL ({})", rssUrl, e);
423+
FailureState oldState = circuitBreaker.getIfPresent(rssUrl);
424+
int newCount = (oldState == null) ? 1 : oldState.count() + 1;
425+
426+
if (newCount >= DEAD_RSS_FEED_FAILURE_THRESHOLD) {
427+
logger.error(
428+
"Possibly dead RSS feed URL: {} - Failed {} times. Please remove it from config.",
429+
rssUrl, newCount);
430+
}
431+
circuitBreaker.put(rssUrl, new FailureState(newCount, ZonedDateTime.now()));
432+
433+
long blacklistedHours = calculateWaitHours(newCount);
434+
435+
logger.warn(
436+
"RSS fetch failed for {} (Attempt #{}). Backing off for {} hours. Reason: {}",
437+
rssUrl, newCount, blacklistedHours, e.getMessage(), e);
438+
406439
return List.of();
407440
}
408441
}
@@ -424,4 +457,21 @@ private static ZonedDateTime getZonedDateTime(@Nullable String date, String form
424457

425458
return ZonedDateTime.parse(date, DateTimeFormatter.ofPattern(format));
426459
}
460+
461+
private long calculateWaitHours(int failureCount) {
462+
return (long) Math.min(Math.pow(BACKOFF_BASE, failureCount - BACKOFF_EXPONENT_OFFSET),
463+
MAX_BACKOFF_HOURS);
464+
}
465+
466+
private boolean isBackingOff(String url) {
467+
FailureState state = circuitBreaker.getIfPresent(url);
468+
if (state == null) {
469+
return false;
470+
}
471+
472+
long waitHours = calculateWaitHours(state.count());
473+
ZonedDateTime retryAt = state.lastFailure().plusHours(waitHours);
474+
475+
return ZonedDateTime.now().isBefore(retryAt);
476+
}
427477
}

0 commit comments

Comments
 (0)