22
33import com .apptasticsoftware .rssreader .Item ;
44import com .apptasticsoftware .rssreader .RssReader ;
5+ import com .github .benmanes .caffeine .cache .Cache ;
6+ import com .github .benmanes .caffeine .cache .Caffeine ;
57import net .dv8tion .jda .api .EmbedBuilder ;
68import net .dv8tion .jda .api .JDA ;
79import net .dv8tion .jda .api .entities .channel .concrete .TextChannel ;
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 * {
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,20 @@ 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+ long waitHours = calculateWaitHours (state .count ());
472+ ZonedDateTime retryAt = state .lastFailure ().plusHours (waitHours );
473+
474+ return ZonedDateTime .now ().isBefore (retryAt );
475+ }
427476}
0 commit comments