// Copyright 2017 JanusGraph Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.janusgraph.graphdb.log; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.cache.*; import com.google.common.collect.*; import org.janusgraph.core.RelationType; import org.janusgraph.core.JanusGraphElement; import org.janusgraph.core.JanusGraphException; import org.janusgraph.core.JanusGraphTransaction; import org.janusgraph.core.log.TransactionRecovery; import org.janusgraph.diskstorage.*; import org.janusgraph.diskstorage.indexing.IndexEntry; import org.janusgraph.diskstorage.indexing.IndexTransaction; import org.janusgraph.diskstorage.log.*; import org.janusgraph.diskstorage.util.BackendOperation; import org.janusgraph.diskstorage.util.time.TimestampProvider; import org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration; import org.janusgraph.graphdb.database.StandardJanusGraph; import org.janusgraph.graphdb.database.log.LogTxMeta; import org.janusgraph.graphdb.database.log.LogTxStatus; import org.janusgraph.graphdb.database.log.TransactionLogHeader; import org.janusgraph.graphdb.database.serialize.Serializer; import org.janusgraph.graphdb.internal.ElementCategory; import org.janusgraph.graphdb.internal.InternalRelation; import org.janusgraph.graphdb.internal.InternalRelationType; import org.janusgraph.graphdb.relations.RelationIdentifier; import org.janusgraph.graphdb.transaction.StandardJanusGraphTx; import org.janusgraph.graphdb.types.IndexType; import org.janusgraph.graphdb.types.MixedIndexType; import org.janusgraph.graphdb.types.SchemaSource; import org.janusgraph.graphdb.types.indextype.IndexTypeWrapper; import org.janusgraph.graphdb.types.vertices.JanusGraphSchemaVertex; import org.janusgraph.util.system.BackgroundThread; import org.apache.commons.lang.builder.HashCodeBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * * @author Matthias Broecheler (me@matthiasb.com) */ public class StandardTransactionLogProcessor implements TransactionRecovery { private static final Logger logger = LoggerFactory.getLogger(StandardTransactionLogProcessor.class); private static final Duration CLEAN_SLEEP_TIME = Duration.ofSeconds(5); private static final Duration MIN_TX_LENGTH = Duration.ofSeconds(5); private final StandardJanusGraph graph; private final Serializer serializer; private final TimestampProvider times; private final Log txLog; private final Duration persistenceTime; private final Duration readTime = Duration.ofSeconds(1); private final AtomicLong txCounter = new AtomicLong(0); private final BackgroundCleaner cleaner; private final boolean verboseLogging; private final AtomicLong successTxCounter = new AtomicLong(0); private final AtomicLong failureTxCounter = new AtomicLong(0); private final Cache<StandardTransactionId,TxEntry> txCache; public StandardTransactionLogProcessor(StandardJanusGraph graph, Instant startTime) { Preconditions.checkArgument(graph != null && graph.isOpen()); Preconditions.checkArgument(startTime!=null); Preconditions.checkArgument(graph.getConfiguration().hasLogTransactions(),"Transaction logging must be enabled for recovery to work"); Duration maxTxLength = graph.getConfiguration().getMaxCommitTime(); if (maxTxLength.compareTo(MIN_TX_LENGTH)<0) maxTxLength= MIN_TX_LENGTH; Preconditions.checkArgument(maxTxLength != null && !maxTxLength.isZero(), "Max transaction time cannot be 0"); this.graph = graph; this.serializer = graph.getDataSerializer(); this.times = graph.getConfiguration().getTimestampProvider(); this.txLog = graph.getBackend().getSystemTxLog(); this.persistenceTime = graph.getConfiguration().getMaxWriteTime(); this.verboseLogging = graph.getConfiguration().getConfiguration().get(GraphDatabaseConfiguration.VERBOSE_TX_RECOVERY); this.txCache = CacheBuilder.newBuilder() .concurrencyLevel(2) .initialCapacity(100) .expireAfterWrite(maxTxLength.toNanos(), TimeUnit.NANOSECONDS) .removalListener(new RemovalListener<StandardTransactionId, TxEntry>() { @Override public void onRemoval(RemovalNotification<StandardTransactionId, TxEntry> notification) { RemovalCause cause = notification.getCause(); Preconditions.checkArgument(cause == RemovalCause.EXPIRED, "Unexpected removal cause [%s] for transaction [%s]", cause, notification.getKey()); TxEntry entry = notification.getValue(); if (entry.status == LogTxStatus.SECONDARY_FAILURE || entry.status == LogTxStatus.PRIMARY_SUCCESS) { failureTxCounter.incrementAndGet(); fixSecondaryFailure(notification.getKey(), entry); } else { successTxCounter.incrementAndGet(); } } }) .build(); ReadMarker start = ReadMarker.fromTime(startTime); this.txLog.registerReader(start,new TxLogMessageReader()); cleaner = new BackgroundCleaner(); cleaner.start(); } public long[] getStatistics() { return new long[]{successTxCounter.get(),failureTxCounter.get()}; } public synchronized void shutdown() throws JanusGraphException { cleaner.close(CLEAN_SLEEP_TIME); } private void logRecoveryMsg(String message, Object... args) { if (logger.isInfoEnabled() || verboseLogging) { String msg = String.format(message,args); logger.info(msg); if (verboseLogging) System.out.println(msg); } } private void fixSecondaryFailure(final StandardTransactionId txId, final TxEntry entry) { logRecoveryMsg("Attempting to repair partially failed transaction [%s]",txId); if (entry.entry==null) { logRecoveryMsg("Trying to repair expired or unpersisted transaction [%s] (Ignore in startup)",txId); return; } boolean userLogFailure = true; boolean secIndexFailure = true; final Predicate<String> isFailedIndex; final TransactionLogHeader.Entry commitEntry = entry.entry; final TransactionLogHeader.SecondaryFailures secFail = entry.failures; if (secFail!=null) { userLogFailure = secFail.userLogFailure; secIndexFailure = !secFail.failedIndexes.isEmpty(); isFailedIndex = new Predicate<String>() { @Override public boolean apply(@Nullable String s) { return secFail.failedIndexes.contains(s); } }; } else { isFailedIndex = Predicates.alwaysTrue(); } // I) Restore external indexes if (secIndexFailure) { //1) Collect all elements (vertices and relations) and the indexes for which they need to be restored final SetMultimap<String,IndexRestore> indexRestores = HashMultimap.create(); BackendOperation.execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { StandardJanusGraphTx tx = (StandardJanusGraphTx) graph.newTransaction(); try { for (TransactionLogHeader.Modification modification : commitEntry.getContentAsModifications(serializer)) { InternalRelation rel = ModificationDeserializer.parseRelation(modification,tx); //Collect affected vertex indexes for (MixedIndexType index : getMixedIndexes(rel.getType())) { if (index.getElement()==ElementCategory.VERTEX && isFailedIndex.apply(index.getBackingIndexName())) { assert rel.isProperty(); indexRestores.put(index.getBackingIndexName(), new IndexRestore(rel.getVertex(0).longId(),ElementCategory.VERTEX,getIndexId(index))); } } //See if relation itself is affected for (RelationType relType : rel.getPropertyKeysDirect()) { for (MixedIndexType index : getMixedIndexes(relType)) { if (index.getElement().isInstance(rel) && isFailedIndex.apply(index.getBackingIndexName())) { assert rel.id() instanceof RelationIdentifier; indexRestores.put(index.getBackingIndexName(), new IndexRestore(rel.id(),ElementCategory.getByClazz(rel.getClass()),getIndexId(index))); } } } } } finally { if (tx.isOpen()) tx.rollback(); } return true; } },readTime); //2) Restore elements per backing index for (final String indexName : indexRestores.keySet()) { final StandardJanusGraphTx tx = (StandardJanusGraphTx) graph.newTransaction(); try { BackendTransaction btx = tx.getTxHandle(); final IndexTransaction indexTx = btx.getIndexTransaction(indexName); BackendOperation.execute(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Map<String,Map<String,List<IndexEntry>>> restoredDocs = Maps.newHashMap(); for (IndexRestore restore : indexRestores.get(indexName)) { JanusGraphSchemaVertex indexV = (JanusGraphSchemaVertex)tx.getVertex(restore.indexId); MixedIndexType index = (MixedIndexType)indexV.asIndexType(); JanusGraphElement element = restore.retrieve(tx); if (element!=null) { graph.getIndexSerializer().reindexElement(element,index,restoredDocs); } else { //Element is deleted graph.getIndexSerializer().removeElement(restore.elementId,index,restoredDocs); } } indexTx.restore(restoredDocs); indexTx.commit(); return true; } @Override public String toString() { return "IndexMutation"; } }, persistenceTime); } finally { if (tx.isOpen()) tx.rollback(); } } } // II) Restore log messages final String logTxIdentifier = (String)commitEntry.getMetadata().get(LogTxMeta.LOG_ID); if (userLogFailure && logTxIdentifier!=null) { TransactionLogHeader txHeader = new TransactionLogHeader(txCounter.incrementAndGet(),times.getTime(), times); final StaticBuffer userLogContent = txHeader.serializeUserLog(serializer,commitEntry,txId); BackendOperation.execute(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { final Log userLog = graph.getBackend().getUserLog(logTxIdentifier); Future<Message> env = userLog.add(userLogContent); if (env.isDone()) { env.get(); } return true; } },persistenceTime); } } private static class IndexRestore { private final Object elementId; private final long indexId; private final ElementCategory elementCategory; private IndexRestore(Object elementId, ElementCategory category, long indexId) { this.elementId = elementId; this.indexId = indexId; this.elementCategory = category; } public JanusGraphElement retrieve(JanusGraphTransaction tx) { return elementCategory.retrieve(elementId,tx); } @Override public int hashCode() { return new HashCodeBuilder().append(elementId).append(indexId).toHashCode(); } @Override public boolean equals(Object other) { if (this==other) return true; else if (other==null || !getClass().isInstance(other)) return false; IndexRestore r = (IndexRestore)other; return r.elementId.equals(elementId) && indexId==r.indexId; } } private static long getIndexId(IndexType index) { SchemaSource base = ((IndexTypeWrapper)index).getSchemaBase(); assert base instanceof JanusGraphSchemaVertex; return base.longId(); } private static Iterable<MixedIndexType> getMixedIndexes(RelationType type) { if (!type.isPropertyKey()) return Collections.EMPTY_LIST; return Iterables.filter(Iterables.filter(((InternalRelationType)type).getKeyIndexes(),MIXED_INDEX_FILTER),MixedIndexType.class); } private static final Predicate<IndexType> MIXED_INDEX_FILTER = new Predicate<IndexType>() { @Override public boolean apply(@Nullable IndexType indexType) { return indexType.isMixedIndex(); } }; private class TxLogMessageReader implements MessageReader { private final Callable<TxEntry> entryFactory = new Callable<TxEntry>() { @Override public TxEntry call() throws Exception { return new TxEntry(); } }; @Override public void read(Message message) { ReadBuffer content = message.getContent().asReadBuffer(); String senderId = message.getSenderId(); TransactionLogHeader.Entry txentry = TransactionLogHeader.parse(content,serializer,times); TransactionLogHeader txheader = txentry.getHeader(); StandardTransactionId transactionId = new StandardTransactionId(senderId,txheader.getId(), txheader.getTimestamp()); TxEntry entry; try { entry = txCache.get(transactionId,entryFactory); } catch (ExecutionException e) { throw new AssertionError("Unexpected exception",e); } entry.update(txentry); } } private class TxEntry { LogTxStatus status; TransactionLogHeader.Entry entry; TransactionLogHeader.SecondaryFailures failures; synchronized void update(TransactionLogHeader.Entry e) { switch (e.getStatus()) { case PRECOMMIT: entry = e; if (status==null) status=LogTxStatus.PRECOMMIT; break; case PRIMARY_SUCCESS: if (status==null || status==LogTxStatus.PRECOMMIT) status=LogTxStatus.PRIMARY_SUCCESS; break; case COMPLETE_SUCCESS: if (status==null || status==LogTxStatus.PRECOMMIT) status=LogTxStatus.COMPLETE_SUCCESS; break; case SECONDARY_SUCCESS: status=LogTxStatus.SECONDARY_SUCCESS; break; case SECONDARY_FAILURE: status=LogTxStatus.SECONDARY_FAILURE; failures=e.getContentAsSecondaryFailures(serializer); break; default: throw new AssertionError("Unexpected status: " + e.getStatus()); } } } private class BackgroundCleaner extends BackgroundThread { private Instant lastInvocation = null; public BackgroundCleaner() { super("TxLogProcessorCleanup", false); } @Override protected void waitCondition() throws InterruptedException { if (lastInvocation!=null) times.sleepPast(lastInvocation.plus(CLEAN_SLEEP_TIME)); } @Override protected void action() { lastInvocation = times.getTime(); txCache.cleanUp(); } @Override protected void cleanup() { txCache.cleanUp(); } } }