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>
7072 */
7173public 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