FeedReaderService.java

package net.sasasin.sreader.service.job;

import java.util.Objects;
import java.util.Optional;
import net.sasasin.sreader.config.FeedReaderProperties;
import net.sasasin.sreader.domain.FeedUrl;
import net.sasasin.sreader.repository.FeedUrlRepository;
import net.sasasin.sreader.service.extraction.FullTextExtractionBatchResult;
import net.sasasin.sreader.service.extraction.FullTextExtractionService;
import net.sasasin.sreader.service.feed.FeedRegistrationService;
import net.sasasin.sreader.service.feed.ingestion.FeedEntryImportService;
import net.sasasin.sreader.service.feed.ingestion.FeedImportJobSummary;
import net.sasasin.sreader.service.feed.ingestion.FeedImportResult;
import net.sasasin.sreader.service.feed.ingestion.FeedImportSummary;
import net.sasasin.sreader.service.outcome.BatchStopReason;
import net.sasasin.sreader.service.outcome.OperationFailure;
import net.sasasin.sreader.service.outcome.OutcomePreconditions;
import net.sasasin.sreader.service.text.ContentTextFileExportService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class FeedReaderService {

  private static final Logger logger = LoggerFactory.getLogger(FeedReaderService.class);

  private final FeedReaderProperties properties;
  private final FeedRegistrationService feedRegistrationService;
  private final FeedUrlRepository feedUrlRepository;
  private final FeedEntryImportService feedEntryImportService;
  private final FullTextExtractionService fullTextExtractionService;
  private final ContentTextFileExportService contentTextFileExportService;

  public FeedReaderService(
      FeedReaderProperties properties,
      FeedRegistrationService feedRegistrationService,
      FeedUrlRepository feedUrlRepository,
      FeedEntryImportService feedEntryImportService,
      FullTextExtractionService fullTextExtractionService,
      ContentTextFileExportService contentTextFileExportService) {
    this.properties = properties;
    this.feedRegistrationService = feedRegistrationService;
    this.feedUrlRepository = feedUrlRepository;
    this.feedEntryImportService = feedEntryImportService;
    this.fullTextExtractionService = fullTextExtractionService;
    this.contentTextFileExportService = contentTextFileExportService;
  }

  public JobResult runOnce() {
    int seeded = feedRegistrationService.registerFeedUrls(properties.seedFeedUrls());

    int completedFeeds = 0;
    int failedFeeds = 0;
    FeedImportSummary entrySummary = FeedImportSummary.empty();
    Optional<BatchStopReason> feedStopReason = Optional.empty();

    for (FeedUrl feedUrl : feedUrlRepository.findActiveForReading()) {
      if (Thread.currentThread().isInterrupted()) {
        feedStopReason = Optional.of(BatchStopReason.INTERRUPTED);
        break;
      }
      FeedImportResult importResult = feedEntryImportService.importEntries(feedUrl);
      entrySummary = entrySummary.plus(importResult.summary());
      switch (importResult) {
        case FeedImportResult.Completed ignored -> completedFeeds++;
        case FeedImportResult.Failed failed -> {
          failedFeeds++;
          OperationFailure failure = failed.failure();
          if (failure.cause().isPresent()) {
            logger.error(
                "Failed to import feed {} stage={} kind={} message={}",
                feedUrl.url(),
                failure.stage(),
                failure.kind(),
                failure.message(),
                failure.cause().get());
          } else {
            logger.error(
                "Failed to import feed {} stage={} kind={} message={}",
                feedUrl.url(),
                failure.stage(),
                failure.kind(),
                failure.message());
          }
          if (failure.interrupted()) {
            feedStopReason = Optional.of(BatchStopReason.INTERRUPTED);
            FeedImportJobSummary feedImport =
                new FeedImportJobSummary(completedFeeds, failedFeeds, entrySummary, feedStopReason);
            JobResult partial =
                new JobResult(
                    seeded,
                    feedImport,
                    FullTextExtractionBatchResult.empty(),
                    0,
                    Optional.of(JobStopReason.INTERRUPTED));
            logger.warn("Feed reader job interrupted during feed import: {}", partial);
            return partial;
          }
        }
      }
    }

    FeedImportJobSummary feedImport =
        new FeedImportJobSummary(completedFeeds, failedFeeds, entrySummary, feedStopReason);
    if (feedStopReason.isPresent()) {
      JobResult partial =
          new JobResult(
              seeded,
              feedImport,
              FullTextExtractionBatchResult.empty(),
              0,
              Optional.of(JobStopReason.INTERRUPTED));
      logger.warn("Feed reader job interrupted after feed import loop: {}", partial);
      return partial;
    }

    FullTextExtractionBatchResult fullTextExtraction =
        fullTextExtractionService.extractPending(100);
    if (fullTextExtraction.stopReason().isPresent()) {
      JobResult partial =
          new JobResult(
              seeded, feedImport, fullTextExtraction, 0, Optional.of(JobStopReason.INTERRUPTED));
      logger.warn("Feed reader job interrupted during full text extraction: {}", partial);
      return partial;
    }

    int textFilesExported =
        contentTextFileExportService.exportPending(properties.textExport().batchSize());
    JobResult result =
        new JobResult(seeded, feedImport, fullTextExtraction, textFilesExported, Optional.empty());
    logger.info("Feed reader job finished: {}", result);
    return result;
  }

  public record JobResult(
      int feedUrlsInserted,
      FeedImportJobSummary feedImport,
      FullTextExtractionBatchResult fullTextExtraction,
      int textFilesExported,
      Optional<JobStopReason> stopReason) {

    public JobResult {
      feedUrlsInserted =
          OutcomePreconditions.requireNonNegative("feedUrlsInserted", feedUrlsInserted);
      Objects.requireNonNull(feedImport, "feedImport must not be null");
      Objects.requireNonNull(fullTextExtraction, "fullTextExtraction must not be null");
      textFilesExported =
          OutcomePreconditions.requireNonNegative("textFilesExported", textFilesExported);
      stopReason = Objects.requireNonNull(stopReason, "stopReason must not be null");
    }
  }
}