blob: 4c2b878e8e81aa75cd60938cf48881e25c4192e4 [file] [log] [blame]
/*
* 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.directory.server.core.shared.log;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.Adler32;
import java.util.zip.Checksum;
import org.apache.directory.server.core.api.log.InvalidLogException;
import org.apache.directory.server.core.api.log.LogAnchor;
import org.apache.directory.server.core.api.log.LogAnchorComparator;
import org.apache.directory.server.core.api.log.LogScanner;
import org.apache.directory.server.core.api.log.UserLogRecord;
import org.apache.directory.server.i18n.I18n;
/**
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
/* Package protected */class LogManager
{
/** Controlfile record size */
private final static int CONTROLFILE_RECORD_SIZE = 44;
/** Controlfile checksum size */
private final static int CONTROLFILE_CHECKSUM_SIZE = CONTROLFILE_RECORD_SIZE - 8 - 4;
/** Controlfile file magic number */
private final static int CONTROLFILE_MAGIC_NUMBER = 0xFF11FF11;
/** Controlfile log file number */
private final static long CONTROLFILE_LOG_FILE_NUMBER = -1;
/** Shadow Controlfile log file number */
private final static long CONTROLFILE_SHADOW_LOG_FILE_NUMBER = -2;
/** buffer used to do IO on controlfile */
private byte controlFileBuffer[] = new byte[CONTROLFILE_RECORD_SIZE];
/** ByteBuffer used to IO on checkpoint file */
private ByteBuffer controlFileMarker = ByteBuffer.wrap( controlFileBuffer );
/** Current checkpoint record in memory */
private ControlFileRecord controlFileRecord = new ControlFileRecord();
/** Min neeeded point in the log */
private LogAnchor minLogAnchor = new LogAnchor();
/** Protects minLogAchor */
private Lock minLogAnchorLock = new ReentrantLock();
/** Log file manager */
private LogFileManager logFileManager;
/** Log Anchor comparator */
private LogAnchorComparator anchorComparator = new LogAnchorComparator();
/** Current log file */
private long currentLogFileNumber;
/** Buffer used to read log file markers */
private byte markerBuffer[] = new byte[LogFileRecords.LOG_FILE_HEADER_SIZE];
/** ByteBuffer wrapper for the marker buffer */
private ByteBuffer markerHead = ByteBuffer.wrap( markerBuffer );
/** The Checksum used */
private Checksum checksum = new Adler32();
/** Max lsn in the log after recovery */
private long initialLsn = Long.MIN_VALUE ;
/**
* Creates a new instance of LogManager. It manages the control file and
*
* @param logFileManager The associated LogFileManager
*/
public LogManager( LogFileManager logFileManager )
{
this.logFileManager = logFileManager;
}
/**
*Initializes the log management:
* 1) Checks if control file exists and creates it if necessary. If it exists, it reads it and loads
* the latest checkpoint.
* 2) Starts from the latest checkpoint and scans forwards the logs to check for corrupted logs and
* determine the end of the log.
* This scan ends either when a properly ended log file is found or a partially written log record is found.
*
* @throws IOException If we had an issue accessing the log files
* @throws InvalidLogException If any of the user record is invalid
*/
public void initLogManager() throws IOException, InvalidLogException
{
LogAnchor scanPoint = new LogAnchor();
LogScanner scanner;
UserLogRecord logRecord;
LogFileManager.LogFileReader reader;
// Read and verify control file
boolean controlFileExists = true;
try
{
readControlFile();
}
catch ( FileNotFoundException e )
{
controlFileExists = false;
}
if ( controlFileExists )
{
boolean invalidLog = false;
// Set the min log anchor from the control file
minLogAnchor.resetLogAnchor( controlFileRecord.minNeededLogFile,
controlFileRecord.minNeededLogFileOffset, controlFileRecord.minNeededLSN );
scanPoint.resetLogAnchor( minLogAnchor );
logRecord = new UserLogRecord();
scanner = new DefaultLogScanner( scanPoint, logFileManager );
// Now, scan all the logged user records to check if they are all valid
try
{
while ( scanner.getNextRecord( logRecord ) )
{
// No need to do anything with the log record, just
// read all of them to check that they are all valid.
}
}
catch ( InvalidLogException e )
{
invalidLog = true;
}
finally
{
scanner.close();
}
initialLsn = logRecord.getLogAnchor().getLogLSN();
System.out.println(" Log manager inital lsn " + initialLsn);
long lastGoodLogFileNumber = scanner.getLastGoodFileNumber();
long lastGoodLogFileOffset = scanner.getLastGoodOffset();
currentLogFileNumber = lastGoodLogFileNumber;
if ( ( lastGoodLogFileNumber < LogAnchor.MIN_LOG_NUMBER ) ||
( lastGoodLogFileOffset < LogAnchor.MIN_LOG_OFFSET ) )
{
throw new InvalidLogException( I18n.err( I18n.ERR_750 ) );
}
scanPoint.resetLogAnchor( lastGoodLogFileNumber, lastGoodLogFileOffset,
LogAnchor.UNKNOWN_LSN );
if ( anchorComparator.compare( scanPoint, minLogAnchor ) < 0 )
{
throw new InvalidLogException( I18n.err( I18n.ERR_750 ) );
}
/*
* If invalid content at the end of file:
* if we are past the header of file, then
* truncate the file to the end of the last
* read log record, otherwise we read a partially
* written log file header, in this case reformat the log file.
* Also check next for the existence of next file to make
* sure we really read the last log file.
*/
if ( invalidLog )
{
// Check if next log file exists
reader = null;
try
{
reader = logFileManager.getReaderForLogFile( ( lastGoodLogFileNumber + 1 ) );
}
catch ( FileNotFoundException e )
{
// Fine, this is what we want
}
finally
{
if ( reader != null )
{
reader.close();
}
}
if ( reader != null )
{
throw new InvalidLogException( I18n.err( I18n.ERR_750 ) );
}
if ( lastGoodLogFileOffset >= LogFileRecords.LOG_FILE_HEADER_SIZE )
{
logFileManager.truncateLogFile( lastGoodLogFileNumber, lastGoodLogFileOffset );
}
else
{
// Reformat the existing log file
createNextLogFile( true );
}
}
return;
}
/*
* Control file does not exist. Either we are at the very beginning or
* maybe we crashed in the middle of creating the first log file.
* We should have the min log file at most with the file header formatted.
*/
reader = null;
boolean fileExists = false;
currentLogFileNumber = LogAnchor.MIN_LOG_NUMBER - 1;
try
{
reader = logFileManager.getReaderForLogFile( LogAnchor.MIN_LOG_NUMBER );
fileExists = true;
currentLogFileNumber++;
}
catch ( FileNotFoundException e )
{
// Fine, we will create the file
}
finally
{
if ( reader != null )
{
reader.close();
}
}
createNextLogFile( fileExists );
// Prepare the min log anchor and control file and write the control file
minLogAnchor.resetLogAnchor( LogAnchor.MIN_LOG_NUMBER, LogFileRecords.LOG_FILE_HEADER_SIZE,
LogAnchor.UNKNOWN_LSN );
writeControlFile();
}
/**
*
* @return return the max lsn in the log after recovery
*/
public long getInitialLsn()
{
return initialLsn;
}
/**
* Called by LogFlushManager to switch to the next file.
*
* Note:Currently we do a checkpoint and delete unnecessary log files when we switch to a new file. Some
* of this tasks can be delegated to a background thread later.
*
* @param currentWriter current log file used by the flush manager. Null if the flush manager is just starting up.
* @return new log file to be used.
* @throws IOException If we had an issue accessing the log files
* @throws InvalidLogException If any of the user record is invalid
*/
public LogFileManager.LogFileWriter switchToNextLogFile( LogFileManager.LogFileWriter currentWriter )
throws IOException, InvalidLogException
{
if ( currentWriter != null )
{
currentWriter.close();
writeControlFile();
createNextLogFile( false );
}
LogFileManager.LogFileWriter writer = logFileManager.getWriterForLogFile( currentLogFileNumber );
long currentOffset = writer.getLength();
if ( currentOffset > 0 )
{
writer.seek( currentOffset );
}
return writer;
}
/**
* Called when the logging subsystem is notified about the minimum position
* in the log files that is needed. Log manager uses this information to advance
* its checkpoint and delete unnecessary log files.
*
* @param newCheckPoint min needed log anchor
*/
public void advanceCheckPoint( LogAnchor newCheckPoint )
{
if ( newCheckPoint == null )
{
return;
}
minLogAnchorLock.lock();
if ( anchorComparator.compare( minLogAnchor, newCheckPoint ) < 0 )
{
minLogAnchor.resetLogAnchor( newCheckPoint );
}
try
{
writeControlFile();
}
catch ( IOException e )
{
// Ignore
}
minLogAnchorLock.unlock();
}
/**
*
* @return the current with the checkpoint log achor
*/
public LogAnchor getCheckPoint()
{
LogAnchor anchor = new LogAnchor();
minLogAnchorLock.lock();
anchor.resetLogAnchor( minLogAnchor );
minLogAnchorLock.unlock();
return anchor;
}
/**
* Writes the control file. To make partially written control files unlikely,
* data is first written to a shadow file and then moved(renamed) to the controlfile.
* Move of a file is atomic in POSIX systems, in GFS like file systems(in HDFS for example).
* On windows, it is not always atomic but atomic versions of rename operations started to
* appear in their recent file systems. <br/>
* The controlFile contains the following data :<br/>
* <ul>
* <li>minExistingLogFile : 8 bytes, the smallest file number in the Log</li>
* <li>minNeededLogFile : 8 bytes, the first valid file number in the Log</li>
* <li>minNeededLogFileOffset : the offset of the neededLogFile</li>
* <li>minNeededLSN : the LSN of the neededLogFile</li>
* <li>checksum : the header checksum, computed on the four previous values</li>
* <li>magicNumber : 4 bytes, the ControlFile magic number : 0xFF11FF11</li>
* </ul>
*
*
* @throws IOException If we can't write the control file
*/
private void writeControlFile() throws IOException
{
controlFileRecord.minNeededLogFile = minLogAnchor.getLogFileNumber();
controlFileRecord.minNeededLogFileOffset = minLogAnchor.getLogFileOffset();
controlFileRecord.minNeededLSN = minLogAnchor.getLogLSN();
if ( controlFileRecord.minNeededLogFile > controlFileRecord.minExistingLogFile )
{
deleteUnnecessaryLogFiles( controlFileRecord.minExistingLogFile, controlFileRecord.minNeededLogFile );
controlFileRecord.minExistingLogFile = controlFileRecord.minNeededLogFile;
}
// Create the control file record
controlFileMarker.rewind();
controlFileMarker.putLong( controlFileRecord.minExistingLogFile );
controlFileMarker.putLong( controlFileRecord.minNeededLogFile );
controlFileMarker.putLong( controlFileRecord.minNeededLogFileOffset );
controlFileMarker.putLong( controlFileRecord.minNeededLSN );
// Compute the checksum
checksum.reset();
checksum.update( controlFileMarker.array(), 0, CONTROLFILE_CHECKSUM_SIZE );
controlFileRecord.checksum = checksum.getValue();
controlFileMarker.putLong( controlFileRecord.checksum );
controlFileMarker.putInt( CONTROLFILE_MAGIC_NUMBER );
// Create the shadow file, and write the header into it
boolean shadowFileExists = logFileManager.createLogFile( CONTROLFILE_SHADOW_LOG_FILE_NUMBER );
if ( shadowFileExists )
{
logFileManager.truncateLogFile( CONTROLFILE_SHADOW_LOG_FILE_NUMBER, 0 );
}
LogFileManager.LogFileWriter controlFileWriter = logFileManager
.getWriterForLogFile( CONTROLFILE_SHADOW_LOG_FILE_NUMBER );
try
{
controlFileWriter.append( controlFileBuffer, 0, CONTROLFILE_RECORD_SIZE );
controlFileWriter.sync();
}
finally
{
controlFileWriter.close();
}
// Do the move now
logFileManager.rename( CONTROLFILE_SHADOW_LOG_FILE_NUMBER, CONTROLFILE_LOG_FILE_NUMBER );
}
/**
* Read and verifies the control file.
*
* @throws IOException If we can't read the control file
* @throws InvalidLogException If the control file is invalid
* @throws FileNotFoundException If the control file can't be found
*/
private void readControlFile() throws IOException, InvalidLogException, FileNotFoundException
{
LogFileManager.LogFileReader controlFileReader = logFileManager
.getReaderForLogFile( CONTROLFILE_LOG_FILE_NUMBER );
try
{
controlFileReader.read( controlFileBuffer, 0, CONTROLFILE_RECORD_SIZE );
}
catch ( EOFException e )
{
throw new InvalidLogException( I18n.err( I18n.ERR_750 ), e );
}
finally
{
controlFileReader.close();
}
controlFileMarker.rewind();
controlFileRecord.minExistingLogFile = controlFileMarker.getLong();
controlFileRecord.minNeededLogFile = controlFileMarker.getLong();
controlFileRecord.minNeededLogFileOffset = controlFileMarker.getLong();
controlFileRecord.minNeededLSN = controlFileMarker.getLong();
controlFileRecord.checksum = controlFileMarker.getLong();
int magicNumber = controlFileMarker.getInt();
checksum.reset();
checksum.update( controlFileMarker.array(), 0, CONTROLFILE_CHECKSUM_SIZE );
if ( ( controlFileRecord.minExistingLogFile < LogAnchor.MIN_LOG_NUMBER ) ||
( controlFileRecord.minNeededLogFile < LogAnchor.MIN_LOG_NUMBER ) ||
( controlFileRecord.minNeededLogFileOffset < LogAnchor.MIN_LOG_OFFSET ) ||
( controlFileRecord.minExistingLogFile > controlFileRecord.minNeededLogFile ) ||
( magicNumber != CONTROLFILE_MAGIC_NUMBER ) ||
( controlFileRecord.checksum != checksum.getValue() ) )
{
throw new InvalidLogException( I18n.err( I18n.ERR_750 ) );
}
}
/**
* Creates the next log file. If the log file already exists, then it is reformatted, that is,
* its size is truncated to zero and file header is written again.
*
* @param reformatExistingFile log file already exists and should be formatted. If false, log file should not exist.
* @throws IOException If we can't create the new log file
* @throws InvalidLogException If we already have a log file with the same number
*/
private void createNextLogFile( boolean reformatExistingFile ) throws IOException, InvalidLogException
{
LogFileManager.LogFileWriter writer = null;
long logFileNumber = currentLogFileNumber;
if ( reformatExistingFile == false )
{
logFileNumber++;
}
// Try to create the file.
boolean fileAlreadyExists = logFileManager.createLogFile( logFileNumber );
if ( reformatExistingFile != fileAlreadyExists )
{
// Didnt expect the file to be around
throw new InvalidLogException( I18n.err( I18n.ERR_750 ) );
}
if ( reformatExistingFile )
{
logFileManager.truncateLogFile( logFileNumber, LogAnchor.MIN_LOG_OFFSET );
}
writer = logFileManager.getWriterForLogFile( logFileNumber );
try
{
markerHead.rewind();
markerHead.putLong( logFileNumber );
markerHead.putInt( LogFileRecords.LOG_FILE_HEADER_MAGIC_NUMBER );
writer.append( markerBuffer, 0, LogFileRecords.LOG_FILE_HEADER_SIZE );
writer.sync();
}
finally
{
writer.close();
}
currentLogFileNumber = logFileNumber;
}
/**
* Purge the useless log files
*/
private void deleteUnnecessaryLogFiles( long startingLogFileNumber, long endingLogFileNumber )
{
for ( long logFileNumber = startingLogFileNumber; logFileNumber < endingLogFileNumber; logFileNumber++ )
{
// Do a best effort delete
logFileManager.deleteLogFile( logFileNumber );
}
}
/**
* Checkpoint record
*/
private class ControlFileRecord
{
/** The smallest existing log file number */
private long minExistingLogFile;
/** The log file number associated with a checkpoint */
private long minNeededLogFile;
/** The offset in the min needed log file */
private long minNeededLogFileOffset;
/** The LSN of the first user record in the min needed log file at the offset position */
private long minNeededLSN;
/** The control file checksum */
private long checksum;
}
}