001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.persistence.ocfl.impl;
007
008import static org.slf4j.LoggerFactory.getLogger;
009
010import java.time.Duration;
011import java.time.Instant;
012import java.util.ArrayList;
013import java.util.Iterator;
014import java.util.List;
015import java.util.concurrent.TimeUnit;
016import java.util.concurrent.atomic.AtomicInteger;
017import java.util.stream.Stream;
018
019import org.fcrepo.common.db.DbTransactionExecutor;
020import org.fcrepo.config.OcflPropsConfig;
021import org.fcrepo.kernel.api.Transaction;
022import org.fcrepo.kernel.api.TransactionManager;
023
024import org.slf4j.Logger;
025
026/**
027 * Class to coordinate the index rebuilding tasks.
028 * @author whikloj
029 * @since 6.0.0
030 */
031public class ReindexManager {
032
033    private static final Logger LOGGER = getLogger(ReindexManager.class);
034
035    private static final long REPORTING_INTERVAL_SECS = 300;
036
037    private final List<ReindexWorker> workers;
038
039    private final Iterator<String> ocflIter;
040
041    private final Stream<String> ocflStream;
042
043    private final AtomicInteger completedCount;
044
045    private final AtomicInteger errorCount;
046
047    private final AtomicInteger skipCount;
048
049    private final ReindexService reindexService;
050
051    private final long batchSize;
052
053    private final boolean failOnError;
054
055    private TransactionManager txManager;
056
057    private DbTransactionExecutor dbTransactionExecutor;
058
059    private Transaction transaction = null;
060
061    /**
062     * Basic constructor
063     * @param ids stream of ocfl ids.
064     * @param reindexService the reindexing service.
065     * @param config OCFL property config object.
066     * @param manager the transaction manager object.
067     * @param dbTransactionExecutor manages db transactions
068     */
069    public ReindexManager(final Stream<String> ids,
070                          final ReindexService reindexService,
071                          final OcflPropsConfig config,
072                          final TransactionManager manager,
073                          final DbTransactionExecutor dbTransactionExecutor) {
074        this.ocflStream = ids;
075        this.ocflIter = ocflStream.iterator();
076        this.reindexService = reindexService;
077        this.batchSize = config.getReindexBatchSize();
078        this.failOnError = config.isReindexFailOnError();
079        txManager = manager;
080        this.dbTransactionExecutor = dbTransactionExecutor;
081        workers = new ArrayList<>();
082        completedCount = new AtomicInteger(0);
083        errorCount = new AtomicInteger(0);
084        skipCount = new AtomicInteger(0);
085
086        final var workerCount = config.getReindexingThreads();
087
088        if (workerCount < 1) {
089            throw new IllegalStateException(String.format("Reindexing requires at least 1 thread. Found: %s",
090                    workerCount));
091        }
092
093        for (var i = 0; i < workerCount; i += 1) {
094            workers.add(new ReindexWorker("ReindexWorker-" + i, this,
095                    this.reindexService, txManager, this.dbTransactionExecutor, this.failOnError));
096        }
097    }
098
099    /**
100     * Start reindexing.
101     * @throws InterruptedException on an indexing error in a thread.
102     */
103    public void start() throws InterruptedException {
104        final var reporter = startReporter();
105        try {
106            workers.forEach(ReindexWorker::start);
107            for (final var worker : workers) {
108                worker.join();
109            }
110            if (!failOnError || errorCount.get() == 0) {
111                indexMembership();
112            } else {
113                LOGGER.error("Reindex did not complete successfully");
114            }
115        } catch (final Exception e) {
116            LOGGER.error("Error while rebuilding index", e);
117            stop();
118            throw e;
119        } finally {
120            reporter.interrupt();
121        }
122    }
123
124    /**
125     * Stop all threads.
126     */
127    public void stop() {
128        LOGGER.debug("Stop worker threads");
129        workers.forEach(ReindexWorker::stopThread);
130    }
131
132    /**
133     * Return a batch of OCFL ids to reindex.
134     * @return list of OCFL ids.
135     */
136    public synchronized List<String> getIds() {
137        int counter = 0;
138        final List<String> ids = new ArrayList<>((int) batchSize);
139        while (ocflIter.hasNext() && counter < batchSize) {
140            ids.add(ocflIter.next());
141            counter += 1;
142        }
143        return ids;
144    }
145
146    /**
147     * Update the master list of reindexing states.
148     * @param batchSuccessful how many items were completed successfully in the last batch.
149     * @param batchErrors how many items had an error in the last batch.
150     * @param batchSkipped how many items were skipped in the last batch.
151     */
152    public void updateComplete(final int batchSuccessful, final int batchErrors, final int batchSkipped) {
153        completedCount.addAndGet(batchSuccessful);
154        errorCount.addAndGet(batchErrors);
155        skipCount.addAndGet(batchSkipped);
156    }
157
158    /**
159     * @return the count of items that completed successfully.
160     */
161    public int getCompletedCount() {
162        return completedCount.get();
163    }
164
165    /**
166     * @return the count of items that had errors.
167     */
168    public int getErrorCount() {
169        return errorCount.get();
170    }
171
172    /**
173     * @return the count of items skipped in this run.
174     */
175    public int getSkippedCount() {
176        return skipCount.get();
177    }
178
179    /**
180     * Index the membership relationships
181     */
182    private void indexMembership() {
183        final var tx = transaction();
184        LOGGER.info("Starting membership indexing");
185        reindexService.indexMembership(tx);
186        tx.commit();
187        LOGGER.debug("Completed membership indexing");
188    }
189
190    /**
191     * Close stream.
192     */
193    public void shutdown() {
194        ocflStream.close();
195    }
196
197    private Thread startReporter() {
198        final var reporter = new Thread(() -> {
199            final var startTime = Instant.now();
200            try {
201                while (true) {
202                    TimeUnit.SECONDS.sleep(REPORTING_INTERVAL_SECS);
203                    final var complete = completedCount.get();
204                    final var errored = errorCount.get();
205                    final var skipped = skipCount.get();
206                    final var now = Instant.now();
207                    final var duration = Duration.between(startTime, now);
208                    LOGGER.info("Index rebuild progress: Complete: {}; Errored: {}; Skipped: {}, Time: {}; Rate: {}/s",
209                            complete, errored, skipped, getDurationMessage(duration),
210                            (complete + errored) / duration.getSeconds());
211                }
212            } catch (final InterruptedException e) {
213                // processing has completed exit normally
214            }
215        });
216
217        reporter.start();
218        return reporter;
219    }
220
221    private String getDurationMessage(final Duration duration) {
222        String message = String.format("%d secs", duration.toSecondsPart());
223        if (duration.getSeconds() > 60) {
224            message = String.format("%d mins, ", duration.toMinutesPart()) + message;
225        }
226        if (duration.getSeconds() > 3600) {
227            message = String.format("%d hours, ", duration.getSeconds() / 3600) + message;
228        }
229        return message;
230    }
231
232    private Transaction transaction() {
233        if (transaction == null) {
234            transaction = txManager.create();
235            transaction.setShortLived(true);
236        }
237        return transaction;
238    }
239}