/*
* 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;
import static com.eightkdata.mongowp.bson.utils.DefaultBsonValues.EMPTY_DOC;
import static com.eightkdata.mongowp.bson.utils.DefaultBsonValues.newLong;
import com.eightkdata.mongowp.ErrorCode;
import com.eightkdata.mongowp.OpTime;
import com.eightkdata.mongowp.Status;
import com.eightkdata.mongowp.bson.BsonDateTime;
import com.eightkdata.mongowp.bson.BsonDocument;
import com.eightkdata.mongowp.bson.utils.DefaultBsonValues;
import com.eightkdata.mongowp.bson.utils.TimestampToDateTime;
import com.eightkdata.mongowp.exceptions.MongoException;
import com.eightkdata.mongowp.server.api.Request;
import com.eightkdata.mongowp.server.api.oplog.OplogOperation;
import com.eightkdata.mongowp.server.api.tools.Empty;
import com.eightkdata.mongowp.utils.BsonDocumentBuilder;
import com.eightkdata.mongowp.utils.BsonReaderTool;
import com.google.common.base.Preconditions;
import com.torodb.core.annotations.TorodbIdleService;
import com.torodb.core.exceptions.user.UserException;
import com.torodb.core.retrier.Retrier;
import com.torodb.core.retrier.Retrier.Hint;
import com.torodb.core.retrier.RetrierAbortException;
import com.torodb.core.retrier.RetrierGiveUpException;
import com.torodb.core.services.IdleTorodbService;
import com.torodb.mongodb.annotations.Locked;
import com.torodb.mongodb.commands.signatures.general.DeleteCommand;
import com.torodb.mongodb.commands.signatures.general.DeleteCommand.DeleteArgument;
import com.torodb.mongodb.commands.signatures.general.DeleteCommand.DeleteStatement;
import com.torodb.mongodb.commands.signatures.general.FindCommand;
import com.torodb.mongodb.commands.signatures.general.FindCommand.FindArgument;
import com.torodb.mongodb.commands.signatures.general.FindCommand.FindResult;
import com.torodb.mongodb.commands.signatures.general.InsertCommand;
import com.torodb.mongodb.commands.signatures.general.InsertCommand.InsertArgument;
import com.torodb.mongodb.commands.signatures.general.InsertCommand.InsertResult;
import com.torodb.mongodb.core.MongodConnection;
import com.torodb.mongodb.core.MongodServer;
import com.torodb.mongodb.core.ReadOnlyMongodTransaction;
import com.torodb.mongodb.core.WriteMongodTransaction;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.Closeable;
import java.util.Iterator;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class OplogManager extends IdleTorodbService {
private static final Logger LOGGER = LogManager.getLogger(OplogManager.class);
private static final String KEY = "lastAppliedOplogEntry";
private static final BsonDocument DOC_QUERY = EMPTY_DOC;
private static final String OPLOG_DB = "torodb";
private static final String OPLOG_COL = "oplog.replication";
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private long lastAppliedHash;
private OpTime lastAppliedOpTime;
private final MongodConnection connection;
private final Retrier retrier;
private final ReplMetrics metrics;
@Inject
public OplogManager(@TorodbIdleService ThreadFactory threadFactory,
MongodServer mongodServer, Retrier retrier, ReplMetrics metrics) {
super(threadFactory);
this.connection = mongodServer.openConnection();
this.retrier = retrier;
this.metrics = metrics;
}
public ReadOplogTransaction createReadTransaction() {
Preconditions.checkState(isRunning(), "The service is not running");
return new ReadOplogTransaction(lock.readLock());
}
public WriteOplogTransaction createWriteTransaction() {
Preconditions.checkState(isRunning(), "The service is not running");
return new WriteOplogTransaction(lock.writeLock());
}
private void notifyLastAppliedOpTimeChange() {
metrics.getLastOpTimeApplied().setValue(lastAppliedOpTime.toString());
}
@Override
protected void startUp() throws Exception {
LOGGER.debug("Starting OplogManager");
Lock mutex = lock.writeLock();
mutex.lock();
try {
loadState();
} finally {
mutex.unlock();
}
LOGGER.debug("Started OplogManager");
}
@Override
protected void shutDown() throws Exception {
LOGGER.debug("Stopping OplogManager");
connection.close();
}
@Locked(exclusive = true)
private void storeState(long hash, OpTime opTime) throws OplogManagerPersistException {
Preconditions.checkState(isRunning(), "The service is not running");
try {
retrier.retry(() -> {
try (WriteMongodTransaction transaction = connection.openWriteTransaction()) {
Status<Long> deleteResult = transaction.execute(
new Request(OPLOG_DB, null, true, null),
DeleteCommand.INSTANCE,
new DeleteArgument.Builder(OPLOG_COL)
.addStatement(new DeleteStatement(DOC_QUERY, false))
.build()
);
if (!deleteResult.isOk()) {
throw new RetrierAbortException(new MongoException(deleteResult));
}
//TODO: This should be stored as timestamp once TORODB-189 is resolved
long optimeAsLong = opTime.toOldBson().getMillisFromUnix();
Status<InsertResult> insertResult = transaction.execute(
new Request(OPLOG_DB, null, true, null),
InsertCommand.INSTANCE,
new InsertArgument.Builder(OPLOG_COL)
.addDocument(
new BsonDocumentBuilder()
.appendUnsafe(KEY, new BsonDocumentBuilder()
.appendUnsafe("hash", newLong(hash))
.appendUnsafe("optime_i", DefaultBsonValues.newLong(optimeAsLong))
.appendUnsafe("optime_t", newLong(opTime.getTerm()))
.build()
).build()
).build()
);
if (insertResult.isOk() && insertResult.getResult().getN() != 1) {
throw new RetrierAbortException(new MongoException(ErrorCode.OPERATION_FAILED,
"More than one element inserted"));
}
if (!insertResult.isOk()) {
throw new RetrierAbortException(new MongoException(insertResult));
}
transaction.commit();
return Empty.getInstance();
} catch (UserException ex) {
throw new RetrierAbortException(ex);
}
}, Hint.INFREQUENT_ROLLBACK);
} catch (RetrierGiveUpException ex) {
throw new OplogManagerPersistException(ex);
}
}
@Locked(exclusive = true)
private void loadState() throws OplogManagerPersistException {
try {
retrier.retry(() -> {
try (ReadOnlyMongodTransaction transaction = connection.openReadOnlyTransaction()) {
Status<FindResult> status = transaction.execute(
new Request(OPLOG_DB, null, true, null),
FindCommand.INSTANCE,
new FindArgument.Builder()
.setCollection(OPLOG_COL)
.setSlaveOk(true)
.build()
);
if (!status.isOk()) {
throw new RetrierAbortException(new MongoException(status));
}
Iterator<BsonDocument> batch = status.getResult().getCursor().getFirstBatch();
if (!batch.hasNext()) {
lastAppliedHash = 0;
lastAppliedOpTime = OpTime.EPOCH;
} else {
BsonDocument doc = batch.next();
BsonDocument subDoc = BsonReaderTool.getDocument(doc, KEY);
lastAppliedHash = BsonReaderTool.getLong(subDoc, "hash");
long optimeAsLong = BsonReaderTool.getLong(subDoc, "optime_i");
BsonDateTime optimeAsDateTime = DefaultBsonValues.newDateTime(optimeAsLong);
lastAppliedOpTime = new OpTime(
TimestampToDateTime.toTimestamp(optimeAsDateTime, DefaultBsonValues::newTimestamp),
BsonReaderTool.getLong(subDoc, "optime_t")
);
}
notifyLastAppliedOpTimeChange();
return Empty.getInstance();
}
}, Hint.INFREQUENT_ROLLBACK);
} catch (RetrierGiveUpException ex) {
throw new OplogManagerPersistException(ex);
}
}
public static class OplogManagerPersistException extends Exception {
private static final long serialVersionUID = -2352073393613989057L;
public OplogManagerPersistException(String message) {
super(message);
}
public OplogManagerPersistException(String message, Throwable cause) {
super(message, cause);
}
public OplogManagerPersistException(Throwable cause) {
super(cause);
}
}
@NotThreadSafe
public class ReadOplogTransaction implements Closeable {
private final Lock readLock;
private boolean closed;
private ReadOplogTransaction(Lock readLock) {
this.readLock = readLock;
readLock.lock();
closed = false;
}
public long getLastAppliedHash() {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
return lastAppliedHash;
}
@Nonnull
public OpTime getLastAppliedOptime() {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
if (lastAppliedOpTime == null) {
throw new AssertionError("lastAppliedOpTime should not be null");
}
return lastAppliedOpTime;
}
@Override
public void close() {
if (!closed) {
closed = true;
readLock.unlock();
}
}
}
@NotThreadSafe
public class WriteOplogTransaction implements Closeable {
private final Lock writeLock;
private boolean closed = false;
public WriteOplogTransaction(Lock writeLock) {
this.writeLock = writeLock;
writeLock.lock();
closed = false;
}
public long getLastAppliedHash() {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
return lastAppliedHash;
}
public OpTime getLastAppliedOptime() {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
return lastAppliedOpTime;
}
public void addOperation(@Nonnull OplogOperation op) throws OplogManagerPersistException {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
storeState(op.getHash(), op.getOpTime());
lastAppliedHash = op.getHash();
lastAppliedOpTime = op.getOpTime();
notifyLastAppliedOpTimeChange();
}
public void forceNewValue(long newHash, OpTime newOptime) throws OplogManagerPersistException {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
storeState(newHash, newOptime);
OplogManager.this.lastAppliedHash = newHash;
OplogManager.this.lastAppliedOpTime = newOptime;
notifyLastAppliedOpTimeChange();
}
@Override
public void close() {
if (!closed) {
closed = true;
writeLock.unlock();
}
}
/**
* Deletes all information on the current oplog and reset all its variables (like
* lastAppliedHash or lastAppliedOptime).
*/
void truncate() throws OplogManagerPersistException {
if (closed) {
throw new IllegalStateException("Transaction closed");
}
storeState(0, OpTime.EPOCH);
lastAppliedHash = 0;
lastAppliedOpTime = OpTime.EPOCH;
notifyLastAppliedOpTimeChange();
}
}
}