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}