Skip to content

Commit 83fbf83

Browse files
committed
fix: rss failures with exponential blacklist
failed rss feed urls are marked as blacklisted upto 24 hours, if it's a dead url it notifies with error log for admin to remove it.
1 parent c03731a commit 83fbf83

1 file changed

Lines changed: 50 additions & 6 deletions

File tree

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

Lines changed: 50 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>
@@ -70,6 +72,9 @@
7072
*/
7173
public final class RSSHandlerRoutine implements Routine {
7274

75+
private record FailureState(int count, ZonedDateTime lastFailure) {
76+
}
77+
7378
private static final Logger logger = LoggerFactory.getLogger(RSSHandlerRoutine.class);
7479
private static final int MAX_CONTENTS = 1000;
7580
private static final ZonedDateTime ZONED_TIME_MIN =
@@ -84,6 +89,11 @@ public final class RSSHandlerRoutine implements Routine {
8489
private final int interval;
8590
private final Database database;
8691

92+
private final Cache<String, FailureState> circuitBreaker =
93+
Caffeine.newBuilder().expireAfterWrite(7, TimeUnit.DAYS).maximumSize(500).build();
94+
95+
private static final int DEAD_RSS_FEED_FAILURE_THRESHOLD = 15;
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.info("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 nextWait = (long) Math.min(Math.pow(2.0, newCount - 1.0), 24.0);
434+
435+
logger.warn(
436+
"RSS fetch failed for {} (Attempt #{}). Backing off for {} hours. Reason: {}",
437+
rssUrl, newCount, nextWait, e.getMessage(), e);
438+
406439
return List.of();
407440
}
408441
}
@@ -424,4 +457,15 @@ private static ZonedDateTime getZonedDateTime(@Nullable String date, String form
424457

425458
return ZonedDateTime.parse(date, DateTimeFormatter.ofPattern(format));
426459
}
460+
461+
private boolean isBackingOff(String url) {
462+
FailureState state = circuitBreaker.getIfPresent(url);
463+
if (state == null) {
464+
return false;
465+
}
466+
long waitHours = (long) Math.min(Math.pow(2.0, state.count() - 1.0), 24.0);
467+
ZonedDateTime retryAt = state.lastFailure().plusHours(waitHours);
468+
469+
return ZonedDateTime.now().isBefore(retryAt);
470+
}
427471
}

0 commit comments

Comments
 (0)