/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.mongodb.repl.oplogreplier; import com.eightkdata.mongowp.OpTime; import com.eightkdata.mongowp.Status; import com.eightkdata.mongowp.exceptions.MongoException; import com.eightkdata.mongowp.server.api.oplog.OplogOperation; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.assistedinject.Assisted; import com.torodb.core.annotations.TorodbIdleService; import com.torodb.core.services.IdleTorodbService; import com.torodb.core.supervision.SupervisorDecision; import com.torodb.mongodb.core.MongodServer; import com.torodb.mongodb.repl.OplogManager; import com.torodb.mongodb.repl.OplogManager.OplogManagerPersistException; import com.torodb.mongodb.repl.oplogreplier.fetcher.ContinuousOplogFetcher.ContinuousOplogFetcherFactory; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.Collections; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; /** * The old {@link OplogApplierService} which uses a very simmilar algorithm to the one that MongoDB * uses. */ @ThreadSafe public class SequentialOplogApplierService extends IdleTorodbService implements OplogApplierService { /** * The maximum capacity of the {@linkplain #fetchQueue}. */ private static final int BUFFER_CAPACITY = 1024; private static final Logger LOGGER = LogManager.getLogger(SequentialOplogApplierService.class); private final ReentrantLock mutex = new ReentrantLock(); /** * A queue used to store fetched oplogs to be applied on this node. */ private final MyQueue fetchQueue; private final Callback callback; private final OplogManager oplogManager; private final OplogOperationApplier oplogOpApplier; private final MongodServer server; private final Condition allApplied; private final ThreadFactory threadFactory; private final Executor executor; private final ContinuousOplogFetcherFactory oplogFetcherFactory; private boolean paused; private boolean pauseRequested; private boolean fetcherIsPaused; private final Condition fetcherPausedCond; private final Condition fetcherCanContinueCond; private ReplSyncFetcher fetcherService; private ReplSyncApplier applierService; @Inject SequentialOplogApplierService( @TorodbIdleService ThreadFactory threadFactory, @Assisted Callback callback, OplogManager oplogManager, OplogOperationApplier oplogOpApplier, MongodServer server, ContinuousOplogFetcherFactory oplogFetcherFactory) { super(threadFactory); this.callback = callback; this.fetchQueue = new MyQueue(); this.oplogManager = oplogManager; this.oplogOpApplier = oplogOpApplier; this.server = server; this.allApplied = mutex.newCondition(); this.fetcherPausedCond = mutex.newCondition(); this.fetcherCanContinueCond = mutex.newCondition(); this.threadFactory = threadFactory; final ThreadFactory utilityThreadFactory = new ThreadFactoryBuilder() .setThreadFactory(threadFactory) .setNameFormat("repl-secondary-util-%d") .build(); this.executor = (Runnable command) -> { utilityThreadFactory.newThread(command).start(); }; this.oplogFetcherFactory = oplogFetcherFactory; } @Override protected void startUp() { callback.waitUntilStartPermision(); LOGGER.info("Starting SECONDARY service"); paused = false; fetcherIsPaused = false; pauseRequested = false; long lastAppliedHash; OpTime lastAppliedOptime; try (OplogManager.ReadOplogTransaction oplogReadTrans = oplogManager.createReadTransaction()) { lastAppliedHash = oplogReadTrans.getLastAppliedHash(); lastAppliedOptime = oplogReadTrans.getLastAppliedOptime(); } fetcherService = new ReplSyncFetcher( threadFactory, new FetcherView(), oplogFetcherFactory.createFetcher(lastAppliedHash, lastAppliedOptime) ); fetcherService.startAsync(); applierService = new ReplSyncApplier( threadFactory, oplogOpApplier, server, oplogManager, new ApplierView() ); applierService.startAsync(); fetcherService.awaitRunning(); applierService.awaitRunning(); LOGGER.info("Started SECONDARY service"); } @Override protected void shutDown() { fetcherService.stopAsync(); applierService.stopAsync(); fetcherService.awaitTerminated(); applierService.awaitTerminated(); } public boolean isPaused() { return paused; } private final class FetcherView implements ReplSyncFetcher.SyncServiceView { @Override public void deliver(OplogOperation oplogOp) throws InterruptedException { fetchQueue.addLast(oplogOp); } @Override public void rollback(RollbackReplicationException ex) { executor.execute(() -> { callback.rollback(SequentialOplogApplierService.this, ex); }); } @Override @SuppressFBWarnings(value = {"WA_AWAIT_NOT_IN_LOOP"}, justification = "This class seem deprecated. We just ignore the warning even if it is correct") public void awaitUntilUnpaused() throws InterruptedException { mutex.lock(); try { fetcherIsPaused = true; fetcherPausedCond.signalAll(); fetcherCanContinueCond.await(); } finally { mutex.unlock(); } } @Override public boolean shouldPause() { return pauseRequested; } @Override public void awaitUntilAllFetchedAreApplied() { mutex.lock(); try { while (!fetchQueue.isEmpty()) { allApplied.awaitUninterruptibly(); } } finally { mutex.unlock(); } } @Override public void fetchFinished() { } @Override public void fetchAborted(final Throwable ex) { executor.execute(() -> { callback.onError(SequentialOplogApplierService.this, ex); }); } } private final class ApplierView implements ReplSyncApplier.SyncServiceView { @Override public List<OplogOperation> takeOps() throws InterruptedException { //TODO: Improve this class to be able to return more than one action per call! //To do that, some changes must be done to avoid concurrency problems while //the fetcher service is working OplogOperation first = fetchQueue.getFirst(); return Collections.singletonList(first); } @Override public void markAsApplied(OplogOperation oplogOperation) { fetchQueue.removeLast(oplogOperation); } @Override public boolean failedToApply(OplogOperation oplogOperation, Status<?> status) { executor.execute(() -> { LOGGER.error("Secondary state failed to apply an operation: {}", status); callback.onError(SequentialOplogApplierService.this, new MongoException(status)); }); return false; } @Override public boolean failedToApply(OplogOperation oplogOperation, final Throwable t) { executor.execute(() -> { LOGGER.error("Secondary state failed to apply an operation", t); callback.onError(SequentialOplogApplierService.this, t); }); return false; } @Override public boolean failedToApply(OplogOperation oplogOperation, final OplogManagerPersistException t) { executor.execute(() -> { LOGGER.error("Secondary state failed to apply an operation", t); callback.onError(SequentialOplogApplierService.this, t); }); return false; } @Override public SupervisorDecision onError(Object supervised, Throwable t) { executor.execute(() -> { LOGGER.error("Secondary state failed", t); callback.onError(SequentialOplogApplierService.this, t); }); return SupervisorDecision.STOP; } } /** * A simplification of a {@link ArrayBlockingQueue} that use the same lock as the container class. */ private class MyQueue { private final OplogOperation[] buffer = new OplogOperation[BUFFER_CAPACITY]; private final Condition notEmpty = mutex.newCondition(); private final Condition notFull = mutex.newCondition(); private int iFirst = 0; private int iLast = 0; private int count = 0; /** * Circularly increment i. */ final int inc(int i) { return (++i == BUFFER_CAPACITY) ? 0 : i; } /** * Circularly decrement i. */ final int dec(int i) { return ((i == 0) ? BUFFER_CAPACITY : i) - 1; } private boolean isEmpty() { return count == 0; } private void addLast(OplogOperation op) throws InterruptedException { if (op == null) { throw new NullPointerException(); } final OplogOperation[] items = this.buffer; final ReentrantLock mutex = SequentialOplogApplierService.this.mutex; mutex.lockInterruptibly(); try { try { while (count == BUFFER_CAPACITY) { notFull.await(); } } catch (InterruptedException ex) { notFull.signal(); throw ex; } items[iLast] = op; iLast = inc(iLast); ++count; notEmpty.signal(); } finally { mutex.unlock(); } } private OplogOperation getFirst() throws InterruptedException { final OplogOperation[] items = this.buffer; final ReentrantLock mutex = SequentialOplogApplierService.this.mutex; mutex.lock(); try { while (isEmpty()) { notEmpty.await(); } return items[iFirst]; } finally { mutex.unlock(); } } private void removeLast(OplogOperation sign) { final OplogOperation[] items = this.buffer; final ReentrantLock mutex = SequentialOplogApplierService.this.mutex; mutex.lock(); try { if (count == 0) { throw new IllegalStateException("The queue is empty"); } OplogOperation first = items[iFirst]; if (first != sign) { throw new IllegalArgumentException("There given operation " + "sign is not the same as the first element to " + "read"); } items[iFirst] = null; iFirst = inc(iFirst); --count; notFull.signal(); } finally { mutex.unlock(); } } } }