org.apache.solr.update.CdcrUpdateLog Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.solr.update;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.update.processor.DistributedUpdateProcessor;
import org.apache.solr.update.processor.DistributingUpdateProcessorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An extension of the {@link org.apache.solr.update.UpdateLog} for the CDCR scenario.
* Compared to the original update log implementation, transaction logs are removed based on
* pointers instead of a fixed size limit. Pointers are created by the CDC replicators and
* correspond to replication checkpoints. If all pointers are ahead of a transaction log,
* this transaction log is removed.
* Given that the number of transaction logs can become considerable if some pointers are
* lagging behind, the {@link org.apache.solr.update.CdcrUpdateLog.CdcrLogReader} provides
* a {@link org.apache.solr.update.CdcrUpdateLog.CdcrLogReader#seek(long)} method to
* efficiently lookup a particular transaction log file given a version number.
*/
public class CdcrUpdateLog extends UpdateLog {
protected final Map logPointers = new ConcurrentHashMap<>();
/**
* A reader that will be used as toggle to turn on/off the buffering of tlogs
*/
private CdcrLogReader bufferToggle;
public static String LOG_FILENAME_PATTERN = "%s.%019d.%1d";
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private boolean debug = log.isDebugEnabled();
@Override
public void init(UpdateHandler uhandler, SolrCore core) {
// remove dangling readers
for (CdcrLogReader reader : logPointers.keySet()) {
reader.close();
}
logPointers.clear();
// init
super.init(uhandler, core);
}
@Override
public TransactionLog newTransactionLog(File tlogFile, Collection globalStrings, boolean openExisting) {
return new CdcrTransactionLog(tlogFile, globalStrings, openExisting);
}
@Override
protected void addOldLog(TransactionLog oldLog, boolean removeOld) {
if (oldLog == null) return;
numOldRecords += oldLog.numRecords();
int currRecords = numOldRecords;
if (oldLog != tlog && tlog != null) {
currRecords += tlog.numRecords();
}
while (removeOld && logs.size() > 0) {
TransactionLog log = logs.peekLast();
int nrec = log.numRecords();
// remove oldest log if we don't need it to keep at least numRecordsToKeep, or if
// we already have the limit of 10 log files.
if (currRecords - nrec >= numRecordsToKeep || logs.size() >= 10) {
// remove the oldest log if nobody points to it
if (!this.hasLogPointer(log)) {
currRecords -= nrec;
numOldRecords -= nrec;
TransactionLog last = logs.removeLast();
last.deleteOnClose = true;
last.close(); // it will be deleted if no longer in use
continue;
}
// we have one log with one pointer, we should stop removing logs
break;
}
break;
}
// Decref old log as we do not write to it anymore
// If the oldlog is uncapped, i.e., a write commit has to be performed
// during recovery, the output stream will be automatically re-open when
// TransaactionLog#incref will be called.
oldLog.deleteOnClose = false;
oldLog.decref();
// don't incref... we are taking ownership from the caller.
logs.addFirst(oldLog);
}
/**
* Checks if one of the log pointer is pointing to the given tlog.
*/
private boolean hasLogPointer(TransactionLog tlog) {
for (CdcrLogPointer pointer : logPointers.values()) {
// if we have a pointer that is not initialised, then do not remove the old tlogs
// as we have a log reader that didn't pick them up yet.
if (!pointer.isInitialised()) {
return true;
}
if (pointer.tlogFile == tlog.tlogFile) {
return true;
}
}
return false;
}
@Override
public long getLastLogId() {
if (id != -1) return id;
if (tlogFiles.length == 0) return -1;
String last = tlogFiles[tlogFiles.length - 1];
if (TLOG_NAME.length() + 1 > last.lastIndexOf('.')) {
// old tlog created by default UpdateLog impl
return Long.parseLong(last.substring(TLOG_NAME.length() + 1));
} else {
return Long.parseLong(last.substring(TLOG_NAME.length() + 1, last.lastIndexOf('.')));
}
}
@Override
public void add(AddUpdateCommand cmd, boolean clearCaches) {
// Ensure we create a new tlog file following our filename format,
// the variable tlog will be not null, and the ensureLog of the parent will be skipped
synchronized (this) {
if ((cmd.getFlags() & UpdateCommand.REPLAY) == 0) {
ensureLog(cmd.getVersion());
}
}
// Then delegate to parent method
super.add(cmd, clearCaches);
}
@Override
public void delete(DeleteUpdateCommand cmd) {
// Ensure we create a new tlog file following our filename format
// the variable tlog will be not null, and the ensureLog of the parent will be skipped
synchronized (this) {
if ((cmd.getFlags() & UpdateCommand.REPLAY) == 0) {
ensureLog(cmd.getVersion());
}
}
// Then delegate to parent method
super.delete(cmd);
}
@Override
public void deleteByQuery(DeleteUpdateCommand cmd) {
// Ensure we create a new tlog file following our filename format
// the variable tlog will be not null, and the ensureLog of the parent will be skipped
synchronized (this) {
if ((cmd.getFlags() & UpdateCommand.REPLAY) == 0) {
ensureLog(cmd.getVersion());
}
}
// Then delegate to parent method
super.deleteByQuery(cmd);
}
/**
* Creates a new {@link org.apache.solr.update.CdcrUpdateLog.CdcrLogReader}
* initialised with the current list of tlogs.
*/
public CdcrLogReader newLogReader() {
return new CdcrLogReader(new ArrayList(logs), tlog);
}
/**
* Enable the buffering of the tlogs. When buffering is activated, the update logs will not remove any
* old transaction log files.
*/
public void enableBuffer() {
if (bufferToggle == null) {
bufferToggle = this.newLogReader();
}
}
/**
* Disable the buffering of the tlogs.
*/
public void disableBuffer() {
if (bufferToggle != null) {
bufferToggle.close();
bufferToggle = null;
}
}
public CdcrLogReader getBufferToggle() {
return bufferToggle;
}
/**
* Is the update log buffering the tlogs ?
*/
public boolean isBuffering() {
return bufferToggle == null ? false : true;
}
protected void ensureLog(long startVersion) {
if (tlog == null) {
long absoluteVersion = Math.abs(startVersion); // version is negative for deletes
if (tlog == null) {
String newLogName = String.format(Locale.ROOT, LOG_FILENAME_PATTERN, TLOG_NAME, id, absoluteVersion);
tlog = new CdcrTransactionLog(new File(tlogDir, newLogName), globalStrings);
}
// push the new tlog to the opened readers
for (CdcrLogReader reader : logPointers.keySet()) {
reader.push(tlog);
}
}
}
/**
* expert: Reset the update log before initialisation. This is called by
* {@link org.apache.solr.handler.IndexFetcher#moveTlogFiles(File)} during a
* a Recovery operation in order to re-initialise the UpdateLog with a new set of tlog files.
* @see #initForRecovery(File, long)
*/
public BufferedUpdates resetForRecovery() {
synchronized (this) { // since we blocked updates in IndexFetcher, this synchronization shouldn't strictly be necessary.
// If we are buffering, we need to return the related information to the index fetcher
// for properly initialising the new update log - SOLR-8263
BufferedUpdates bufferedUpdates = new BufferedUpdates();
if (state == State.BUFFERING && tlog != null) {
bufferedUpdates.tlog = tlog.tlogFile; // file to keep
bufferedUpdates.offset = this.recoveryInfo.positionOfStart;
}
// Close readers
for (CdcrLogReader reader : logPointers.keySet()) {
reader.close();
}
logPointers.clear();
// Close and clear logs
doClose(prevTlog);
doClose(tlog);
for (TransactionLog log : logs) {
if (log == prevTlog || log == tlog) continue;
doClose(log);
}
logs.clear();
newestLogsOnStartup.clear();
tlog = prevTlog = null;
prevMapLog = prevMapLog2 = null;
map.clear();
if (prevMap != null) prevMap.clear();
if (prevMap2 != null) prevMap2.clear();
tlogFiles = null;
numOldRecords = 0;
oldDeletes.clear();
deleteByQueries.clear();
return bufferedUpdates;
}
}
public static class BufferedUpdates {
public File tlog;
public long offset;
}
/**
*
* expert: Initialise the update log with a tlog file containing buffered updates. This is called by
* {@link org.apache.solr.handler.IndexFetcher#moveTlogFiles(File)} during a Recovery operation.
* This is mainly a copy of the original {@link UpdateLog#init(UpdateHandler, SolrCore)} method, but modified
* to:
*
* - preserve the same {@link VersionInfo} instance in order to not "unblock" updates, since the
* {@link org.apache.solr.handler.IndexFetcher#moveTlogFiles(File)} acquired a write lock from this instance.
* - copy the buffered updates.
*
* @see #resetForRecovery()
*/
public void initForRecovery(File bufferedTlog, long offset) {
tlogFiles = getLogList(tlogDir);
id = getLastLogId() + 1; // add 1 since we will create a new log for the next update
if (debug) {
log.debug("UpdateHandler init: tlogDir=" + tlogDir + ", existing tlogs=" + Arrays.asList(tlogFiles) + ", next id=" + id);
}
TransactionLog oldLog = null;
for (String oldLogName : tlogFiles) {
File f = new File(tlogDir, oldLogName);
try {
oldLog = newTransactionLog(f, null, true);
addOldLog(oldLog, false); // don't remove old logs on startup since more than one may be uncapped.
} catch (Exception e) {
SolrException.log(log, "Failure to open existing log file (non fatal) " + f, e);
deleteFile(f);
}
}
// Record first two logs (oldest first) at startup for potential tlog recovery.
// It's possible that at abnormal close both "tlog" and "prevTlog" were uncapped.
for (TransactionLog ll : logs) {
newestLogsOnStartup.addFirst(ll);
if (newestLogsOnStartup.size() >= 2) break;
}
// TODO: these startingVersions assume that we successfully recover from all non-complete tlogs.
UpdateLog.RecentUpdates startingUpdates = getRecentUpdates();
long latestVersion = startingUpdates.getMaxRecentVersion();
try {
startingVersions = startingUpdates.getVersions(numRecordsToKeep);
// populate recent deletes list (since we can't get that info from the index)
for (int i=startingUpdates.deleteList.size()-1; i>=0; i--) {
DeleteUpdate du = startingUpdates.deleteList.get(i);
oldDeletes.put(new BytesRef(du.id), new LogPtr(-1,du.version));
}
// populate recent deleteByQuery commands
for (int i=startingUpdates.deleteByQueryList.size()-1; i>=0; i--) {
Update update = startingUpdates.deleteByQueryList.get(i);
List