package com.constellio.data.dao.services.recovery;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.codehaus.plexus.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.constellio.data.dao.dto.records.TransactionResponseDTO;
import com.constellio.data.dao.services.bigVault.solr.BigVaultException.CouldNotExecuteQuery;
import com.constellio.data.dao.services.bigVault.solr.BigVaultServer;
import com.constellio.data.dao.services.bigVault.solr.BigVaultServerTransaction;
import com.constellio.data.dao.services.bigVault.solr.listeners.BigVaultServerAddEditListener;
import com.constellio.data.dao.services.bigVault.solr.listeners.BigVaultServerQueryListener;
import com.constellio.data.dao.services.factories.DataLayerFactory;
import com.constellio.data.dao.services.transactionLog.SecondTransactionLogManager;
import com.constellio.data.io.services.facades.IOServices;
import com.constellio.data.utils.BatchBuilderIterator;
import com.constellio.data.utils.ImpossibleRuntimeException;
import com.constellio.data.utils.LazyIterator;
public class TransactionLogRecoveryManager implements RecoveryService, BigVaultServerAddEditListener,
BigVaultServerQueryListener {
private final static Logger LOGGER = LoggerFactory.getLogger(TransactionLogRecoveryManager.class);
private static final String RECOVERY_WORK_DIR = TransactionLogRecoveryManager.class.getName() + "recoveryWorkDir";
final DataLayerFactory dataLayerFactory;
File recoveryWorkDir, recoveryFile;
RecoveryTransactionReadWriteServices readWriteServices;
private final IOServices ioServices;
private boolean inRollbackMode;
Set<String> loadedRecordsIds, fullyLoadedRecordsIds, newRecordsIds, deletedRecordsIds, updatedRecordsIds;
public TransactionLogRecoveryManager(DataLayerFactory dataLayerFactory) {
this.dataLayerFactory = dataLayerFactory;
ioServices = this.dataLayerFactory.getIOServicesFactory().newIOServices();
}
@Override
public void startRollbackMode() {
if (!inRollbackMode) {
LOGGER.info("Rollback mode started");
realStartRollback();
}
}
void realStartRollback() {
loadedRecordsIds = new HashSet<>();
fullyLoadedRecordsIds = new HashSet<>();
newRecordsIds = new HashSet<>();
deletedRecordsIds = new HashSet<>();
updatedRecordsIds = new HashSet<>();
createRecoveryFile();
inRollbackMode = true;
SecondTransactionLogManager transactionLogManager = dataLayerFactory
.getSecondTransactionLogManager();
transactionLogManager.setAutomaticRegroupAndMoveInVaultEnabled(false);
transactionLogManager.regroupAndMoveInVault();
dataLayerFactory.getRecordsVaultServer().registerListener(this);
}
private void createRecoveryFile() {
recoveryWorkDir = ioServices.newTemporaryFolder(RECOVERY_WORK_DIR);
this.recoveryFile = new File(recoveryWorkDir, "rollbackLogs");
readWriteServices = new RecoveryTransactionReadWriteServices(ioServices, dataLayerFactory.getDataLayerConfiguration());
}
@Override
public void stopRollbackMode() {
if (inRollbackMode) {
LOGGER.info("Rollback mode stopped");
realStopRollback();
}
}
void realStopRollback() {
dataLayerFactory.getRecordsVaultServer().unregisterListener(this);
deleteRecoveryFile();
inRollbackMode = false;
SecondTransactionLogManager transactionLogManager = dataLayerFactory
.getSecondTransactionLogManager();
transactionLogManager.regroupAndMoveInVault();
transactionLogManager.setAutomaticRegroupAndMoveInVaultEnabled(true);
}
private void deleteRecoveryFile() {
ioServices.deleteQuietly(recoveryWorkDir);
}
@Override
public boolean isInRollbackMode() {
return inRollbackMode;
}
public void disableRollbackModeDuringSolrRestore() {
stopRollbackMode();
}
@Override
public void rollback(Throwable t) {
if (inRollbackMode) {
LOGGER.info("Rolling back");
realRollback(t);
}
}
void realRollback(Throwable t) {
dataLayerFactory.getRecordsVaultServer().unregisterListener(this);
recover();
deleteRecoveryFile();
SecondTransactionLogManager transactionLogManager = dataLayerFactory
.getSecondTransactionLogManager();
transactionLogManager.deleteUnregroupedLog();
transactionLogManager.setAutomaticRegroupAndMoveInVaultEnabled(true);
inRollbackMode = false;
}
private void recover() {
BigVaultServer bigVaultServer = dataLayerFactory.getRecordsVaultServer();
SolrClient server = bigVaultServer.getNestedSolrServer();
this.deletedRecordsIds.removeAll(this.newRecordsIds);
this.updatedRecordsIds.removeAll(this.newRecordsIds);
removeNewRecords(server);
Set<String> alteredDocuments = new HashSet<>(this.deletedRecordsIds);
alteredDocuments.addAll(this.updatedRecordsIds);
restore(server, alteredDocuments);
try {
bigVaultServer.softCommit();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SolrServerException e) {
throw new RuntimeException(e);
}
}
private void restore(SolrClient server, final Set<String> alteredRecordsIds) {
if (alteredRecordsIds == null || alteredRecordsIds.isEmpty()) {
return;
}
final Iterator<BigVaultServerTransaction> transactionsIterator = this.readWriteServices
.newOperationsIterator(recoveryFile);
Iterator<List<SolrInputDocument>> docsToRecoverIterator = new LazyIterator<List<SolrInputDocument>>() {
@Override
protected List<SolrInputDocument> getNextOrNull() {
if (transactionsIterator.hasNext()) {
List<SolrInputDocument> currentAlteredDocuments = new ArrayList<>();
BigVaultServerTransaction currentTransaction = transactionsIterator.next();
for (SolrInputDocument newDocument : currentTransaction.getNewDocuments()) {
String currentDocumentId = (String) newDocument.getFieldValue("id");
if (alteredRecordsIds.contains(currentDocumentId)) {
currentAlteredDocuments.add(newDocument);
}
}
for (SolrInputDocument document : currentTransaction.getUpdatedDocuments()) {
String currentDocumentId = (String) document.getFieldValue("id");
if (alteredRecordsIds.contains(currentDocumentId)) {
currentAlteredDocuments.add(document);
}
}
return currentAlteredDocuments;
} else {
return null;
}
}
};
int batchSize = 1000;
Iterator<List<SolrInputDocument>> iterator = BatchBuilderIterator.forListIterator(docsToRecoverIterator, batchSize);
while (iterator.hasNext()) {
try {
server.add(iterator.next());
} catch (SolrServerException | HttpSolrClient.RemoteSolrException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void removeNewRecords(SolrClient server) {
if (this.newRecordsIds.isEmpty()) {
return;
}
int batchSize = 1000;
Iterator<List<String>> iterator = new BatchBuilderIterator(this.newRecordsIds.iterator(), batchSize);
while (iterator.hasNext()) {
try {
server.deleteById(iterator.next());
} catch (SolrServerException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void afterAdd(BigVaultServerTransaction transaction, TransactionResponseDTO responseDTO) {
//Nothing to do even if transaction did not succeed
}
@Override
public void beforeAdd(BigVaultServerTransaction transaction) {
if (transaction.getDeletedQueries() != null && !transaction.getDeletedQueries().isEmpty()) {
if (!transaction.isInTestRollbackMode()) {
throw new ImpossibleRuntimeException("Delete by query not supported in recovery mode");
}
}
handleAddUpdateFullDocuments(transaction.getNewDocuments());
handleUpdatedPartialDocuments(transaction.getUpdatedDocuments());
handleDeletedDocuments(transaction.getDeletedRecords());
}
private boolean isTestMode() {
return dataLayerFactory.getDataLayerConfiguration().isInRollbackTestMode();
}
void handleDeletedDocuments(List<String> deletedRecords) {
if (deletedRecords == null || deletedRecords.isEmpty()) {
return;
}
Set<String> deletedRecordsIds = new HashSet<>();
for (String deletedRecordId : deletedRecords) {
deletedRecordsIds.add(deletedRecordId);
}
ensureRecordLoaded(deletedRecordsIds);
this.deletedRecordsIds.addAll(deletedRecords);
}
void ensureRecordLoaded(Set<String> recordsIds) {
provokeRecordsLoad(recordsIds);
if (!this.fullyLoadedRecordsIds.containsAll(recordsIds)) {
throw new RuntimeException("Records not loaded after their load request : " +
StringUtils.join(CollectionUtils.subtract(recordsIds, this.fullyLoadedRecordsIds), ", "));
}
}
private void provokeRecordsLoad(Set<String> recordsIds) {
//do not reload
Set<String> recordsToLoadIds = new HashSet<>(recordsIds);
recordsToLoadIds.removeAll(this.fullyLoadedRecordsIds);
if (recordsToLoadIds.isEmpty()) {
return;
}
//query solr to load non loaded
ModifiableSolrParams solrParams = new ModifiableSolrParams();
//field:(value1 OR value2 OR value3)
solrParams.set("rows", "999999999");
solrParams.set("q", "id:(" + StringUtils.join(recordsToLoadIds, " OR ") + ")");
try {
dataLayerFactory.getRecordsVaultServer().query(solrParams);
} catch (CouldNotExecuteQuery e) {
throw new RuntimeException(e);
}
}
void handleUpdatedPartialDocuments(List<SolrInputDocument> updatedDocuments) {
handleUpdatedDocuments(updatedDocuments, true);
}
void handleUpdatedFullDocuments(List<SolrInputDocument> updatedDocuments) {
handleUpdatedDocuments(updatedDocuments, false);
}
void handleUpdatedDocuments(List<SolrInputDocument> updatedDocuments, boolean partialDocument) {
if (updatedDocuments == null || updatedDocuments.isEmpty()) {
return;
}
Set<String> updatedDocumentsIds = new HashSet<>();
for (SolrInputDocument document : updatedDocuments) {
String id = (String) document.getFieldValue("id");
updatedDocumentsIds.add(id);
}
if (partialDocument) {
ensureRecordLoaded(updatedDocumentsIds);
} else {
List<Object> updatedDocumentsAsObjects = new ArrayList<>();
updatedDocumentsAsObjects.addAll(updatedDocuments);
appendLoadedRecordsFile(updatedDocumentsAsObjects);
}
this.updatedRecordsIds.addAll(updatedDocumentsIds);
}
void handleAddUpdateFullDocuments(List<SolrInputDocument> addUpdateFullDocuments) {
if (addUpdateFullDocuments == null || addUpdateFullDocuments.isEmpty()) {
return;
}
Set<String> possiblyNewDocumentsIds = new HashSet<>();
Set<String> addUpdateFullDocumentsIds = new HashSet<>();
for (SolrInputDocument document : addUpdateFullDocuments) {
String id = (String) document.getFieldValue("id");
addUpdateFullDocumentsIds.add(id);
if (!this.loadedRecordsIds.contains(id)) {
possiblyNewDocumentsIds.add(id);
}
}
Collection<String> updatedFullDocuments;
if (!possiblyNewDocumentsIds.isEmpty()) {
Set<String> newDocuments = getOnlyNewDocuments(possiblyNewDocumentsIds);
this.newRecordsIds.addAll(newDocuments);
updatedFullDocuments = CollectionUtils.removeAll(addUpdateFullDocumentsIds, newDocuments);
} else {
updatedFullDocuments = addUpdateFullDocumentsIds;
}
handleUpdatedFullDocuments(getDocumentsHavingIds(addUpdateFullDocuments, updatedFullDocuments));
}
//TODO test me
private List<SolrInputDocument> getDocumentsHavingIds(List<SolrInputDocument> solrInputDocuments,
final Collection<String> ids) {
List<SolrInputDocument> returnList = new ArrayList<>();
for (SolrInputDocument document : solrInputDocuments) {
String id = (String) document.getFieldValue("id");
if (ids.contains(id)) {
returnList.add(document);
}
}
return returnList;
}
//TODO test me
Set<String> getOnlyNewDocuments(Set<String> possiblyNewDocumentsIds) {
BigVaultServer bigVaultServer = dataLayerFactory.getRecordsVaultServer();
SolrClient server = bigVaultServer.getNestedSolrServer();
//field:(value1 OR value2 OR value3)
Set<String> newDocuments = new HashSet<>();
for (String id : possiblyNewDocumentsIds) {
ModifiableSolrParams solrParams = new ModifiableSolrParams();
solrParams.set("q", "id:" + id);
solrParams.set("fl", "id");
QueryResponse response = null;
try {
response = server.query(solrParams);
} catch (IOException | SolrServerException e) {
throw new RuntimeException(e);
}
SolrDocumentList result = response.getResults();
if (result.getNumFound() == 0) {
newDocuments.add(id);
}
}
return newDocuments;
}
private Set<String> getIdsNotInResult(Set<String> possiblyNewDocumentsIds, SolrDocumentList result) {
Set<String> returnSet = new HashSet<>();
for (SolrDocument document : result) {
String id = (String) document.get("id");
if (!possiblyNewDocumentsIds.contains(id)) {
returnSet.add(id);
}
}
return returnSet;
}
@Override
public void onQuery(SolrParams params, QueryResponse response) {
boolean fullSearch = isFullSearch(params);
SolrDocumentList results = response.getResults();
List<Object> documentsToSave = new ArrayList<>();
List<String> loadedDocuments = new ArrayList<>();
for (SolrDocument document : results) {
String currentId = (String) document.get("id");
if (!this.fullyLoadedRecordsIds.contains(currentId)) {
documentsToSave.add(document);
loadedDocuments.add(currentId);
}
}
if (fullSearch) {
appendLoadedRecordsFile(documentsToSave);
}
this.loadedRecordsIds.addAll(loadedDocuments);
}
//TODO test me
boolean isFullSearch(SolrParams params) {
return StringUtils.isBlank(params.get("fl"));
}
private void appendLoadedRecordsFile(List<Object> documentsToSave) {
List<Object> notAlreadySavedDocuments = new ArrayList<>();
List<String> notAlreadyLoadedDocumentsIds = new ArrayList<>();
for (Object document : documentsToSave) {
String id = getDocumentId(document);
if (!this.fullyLoadedRecordsIds.contains(id)) {
notAlreadySavedDocuments.add(document);
notAlreadyLoadedDocumentsIds.add(id);
}
}
if (notAlreadySavedDocuments.isEmpty()) {
return;
}
this.fullyLoadedRecordsIds.addAll(notAlreadyLoadedDocumentsIds);
String transaction = this.readWriteServices.toLogEntry(notAlreadySavedDocuments);
try {
FileUtils.fileAppend(this.recoveryFile.getPath(), transaction);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String getDocumentId(Object document) {
if (document instanceof SolrDocument) {
return (String) ((SolrDocument) document).getFieldValue("id");
} else if (document instanceof SolrInputDocument) {
return (String) ((SolrInputDocument) document).getFieldValue("id");
} else {
throw new ImpossibleRuntimeException(
"Expecting solr document or solr input document : " + document.getClass().getName());
}
}
@Override
public String getListenerUniqueId() {
return "recoveryListener";
}
public void close() {
stopRollbackMode();
deleteRecoveryFile();
}
}