Skip to content

Commit

Permalink
Fixed log rotation on Windows
Browse files Browse the repository at this point in the history
Synchronizes the operations on logs to make sure that we can safely
close the file stream before performing rotation.

Previously we did not close the stream which prevented log rotation
from ever working on Windows.
  • Loading branch information
spacecowboy committed Feb 22, 2017
1 parent ad67157 commit e0ea709
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 40 deletions.
Expand Up @@ -75,6 +75,7 @@ public void rotationError( @SuppressWarnings("unused") Exception e, @SuppressWar
private final int maxArchives; private final int maxArchives;
private final RotationListener rotationListener; private final RotationListener rotationListener;
private final Executor rotationExecutor; private final Executor rotationExecutor;
private final OutputStream streamWrapper;
private final AtomicBoolean closed = new AtomicBoolean( false ); private final AtomicBoolean closed = new AtomicBoolean( false );
private final AtomicBoolean rotating = new AtomicBoolean( false ); private final AtomicBoolean rotating = new AtomicBoolean( false );
private final AtomicLong earliestRotationTimeRef = new AtomicLong( 0 ); private final AtomicLong earliestRotationTimeRef = new AtomicLong( 0 );
Expand Down Expand Up @@ -129,10 +130,49 @@ public RotatingFileOutputStreamSupplier( FileSystemAbstraction fileSystem, File
this.rotationListener = rotationListener; this.rotationListener = rotationListener;
this.rotationExecutor = rotationExecutor; this.rotationExecutor = rotationExecutor;
this.outRef.set( openOutputFile() ); this.outRef.set( openOutputFile() );
// Wrap the actual reference to prevent race conditions during log rotation
this.streamWrapper = new OutputStream()
{
@Override
public void write( int i ) throws IOException
{
synchronized ( outRef )
{
outRef.get().write( i );
}
}

@Override
public void write( byte[] bytes ) throws IOException
{
synchronized ( outRef )
{
outRef.get().write( bytes );
}
}

@Override
public void write( byte[] bytes, int off, int len ) throws IOException
{
synchronized ( outRef )
{
outRef.get().write( bytes, off, len );
}
}

@Override
public void flush() throws IOException
{
synchronized ( outRef )
{
outRef.get().flush();
}
}
};
} }


/** /**
* @return An stream outputting to the latest output file * @return A stream outputting to the latest output file
*/ */
@Override @Override
public OutputStream get() public OutputStream get()
Expand All @@ -144,13 +184,13 @@ public OutputStream get()
rotate(); rotate();
} }
} }
return outRef.get(); return this.streamWrapper;
} }


@Override @Override
public void close() throws IOException public void close() throws IOException
{ {
synchronized (outRef) synchronized ( outRef )
{ {
closed.set( true ); closed.set( true );
for ( WeakReference<OutputStream> archivedStream : archivedStreams ) for ( WeakReference<OutputStream> archivedStream : archivedStreams )
Expand Down Expand Up @@ -190,40 +230,58 @@ private void rotate()
return; return;
} }


Runnable runnable = () -> { Runnable runnable = () ->
OutputStream newStream; {
try synchronized ( outRef )
{ {
if ( fileSystem.fileExists( outputFile ) ) OutputStream oldStream = outRef.get();

// Must close file prior to doing any operations on it or else it won't work on Windows
try
{ {
shiftArchivedOutputFiles(); oldStream.flush();
fileSystem.renameFile( outputFile, archivedOutputFile( 1 ) ); oldStream.close();
} }
newStream = openOutputFile(); catch ( Exception e )
} {
catch ( Exception e ) rotationListener.rotationError( e, outRef.get() );
{ rotating.set( false );
rotationListener.rotationError( e, outRef.get() ); return;
rotating.set( false ); }
return;
} OutputStream newStream;
OutputStream oldStream = outRef.get(); try
rotationListener.outputFileCreated( newStream, oldStream ); {
synchronized ( outRef ) if ( fileSystem.fileExists( outputFile ) )
{ {
shiftArchivedOutputFiles();
fileSystem.renameFile( outputFile, archivedOutputFile( 1 ) );
}
newStream = openOutputFile();
}
catch ( Exception e )
{
rotationListener.rotationError( e, outRef.get() );
rotating.set( false );
return;
}

rotationListener.outputFileCreated( newStream, oldStream );

if ( !closed.get() ) if ( !closed.get() )
{ {
outRef.set( newStream ); outRef.set( newStream );
removeCollectedReferences( archivedStreams ); removeCollectedReferences( archivedStreams );
archivedStreams.add( new WeakReference<>( oldStream ) ); archivedStreams.add( new WeakReference<>( oldStream ) );
} }

if ( rotationDelay > 0 )
{
earliestRotationTimeRef.set( currentTimeSupplier.getAsLong() + rotationDelay );
}
rotationListener.rotationCompleted( newStream, oldStream );
rotating.set( false );
} }
if ( rotationDelay > 0 )
{
earliestRotationTimeRef.set( currentTimeSupplier.getAsLong() + rotationDelay );
}
rotationListener.rotationCompleted( newStream, oldStream );
rotating.set( false );
}; };


try try
Expand Down
Expand Up @@ -43,20 +43,25 @@
import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.io.fs.FileSystemAbstraction;
import org.neo4j.kernel.configuration.Settings; import org.neo4j.kernel.configuration.Settings;
import org.neo4j.logging.AssertableLogProvider; import org.neo4j.logging.AssertableLogProvider;
import org.neo4j.test.DefaultFileSystemRule;
import org.neo4j.test.EphemeralFileSystemRule; import org.neo4j.test.EphemeralFileSystemRule;
import org.neo4j.test.TargetDirectory; import org.neo4j.test.TargetDirectory;
import org.neo4j.test.TestGraphDatabaseFactory; import org.neo4j.test.TestGraphDatabaseFactory;


import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.neo4j.function.Predicates.await;


public class QueryLoggerIT public class QueryLoggerIT
{ {


// It is imperitive that this test executes using a real filesystem; otherwise rotation failures will not be
// detected on Windows.
@Rule @Rule
public final EphemeralFileSystemRule fileSystem = new EphemeralFileSystemRule(); public final DefaultFileSystemRule fileSystem = new DefaultFileSystemRule();
@Rule @Rule
public final TargetDirectory.TestDirectory testDirectory = TargetDirectory.testDirForTest( getClass() ); public final TargetDirectory.TestDirectory testDirectory = TargetDirectory.testDirForTest( getClass() );
private AssertableLogProvider inMemoryLog; private AssertableLogProvider inMemoryLog;
Expand Down Expand Up @@ -150,33 +155,40 @@ public void disabledQueryLogging() throws IOException


executeQueryAndShutdown( database ); executeQueryAndShutdown( database );


List<String> lines = readAllLines( logFilename ); assertFalse( "Should not have any queries logged since query log is disabled", logFilename.exists() );
assertTrue( "Should not have any queries logged since query log is disabled", lines.isEmpty() );
} }


@Test @Test
public void queryLogRotation() throws Exception public void queryLogRotation() throws Exception
{ {
final File logsDirectory = new File( testDirectory.graphDbDir(), "logs" ); final File logsDirectory = new File( testDirectory.graphDbDir(), "logs" );
final File logFilename = new File( logsDirectory, "query.log" ); final File logFilename = new File( logsDirectory, "query.log" );
final File shiftedLogFilename = new File( logsDirectory, "query.log.1" ); final File shiftedLogFilename1 = new File( logsDirectory, "query.log.1" );
final File shiftedLogFilename2 = new File( logsDirectory, "query.log.2" );
GraphDatabaseService database = databaseBuilder.setConfig( GraphDatabaseSettings.log_queries, Settings.TRUE ) GraphDatabaseService database = databaseBuilder.setConfig( GraphDatabaseSettings.log_queries, Settings.TRUE )
.setConfig( GraphDatabaseSettings.logs_directory, logsDirectory.getPath() ) .setConfig( GraphDatabaseSettings.logs_directory, logsDirectory.getPath() )
.setConfig( GraphDatabaseSettings.log_queries_rotation_threshold, "1" ) .setConfig( GraphDatabaseSettings.log_queries_rotation_threshold, "1" )
.newGraphDatabase(); .newGraphDatabase();


database.execute( QUERY );
database.execute( QUERY ); // Logging is done asynchronously, and it turns out it's really hard to make it all work the same on Linux
// wait for file rotation to finish // and on Windows, so just write maaaaany times to make sure we rotate several times.
Thread.sleep( TimeUnit.SECONDS.toMillis( 1 ) );
// execute one more slow query which should go to new log file for ( int i = 0; i < 100; i++ )
database.execute( QUERY ); {
database.execute( QUERY );
}

database.shutdown(); database.shutdown();


List<String> lines = readAllLines( logFilename ); List<String> lines = readAllLines( logFilename );
assertEquals( 1, lines.size() ); assertTrue( "Expected log file to have at least one log entry", lines.size() >= 1 );
List<String> shiftedLines = readAllLines( shiftedLogFilename );
assertEquals( 2, shiftedLines.size() ); lines = readAllLines( shiftedLogFilename1 );
assertTrue( "Expected shifted log file to have at least one log entry", lines.size() >= 1 );

lines = readAllLines( shiftedLogFilename2 );
assertTrue( "Expected second shifted log file to have at least one log entry", lines.size() >= 1 );
} }


private void executeQueryAndShutdown( GraphDatabaseService database ) private void executeQueryAndShutdown( GraphDatabaseService database )
Expand Down

0 comments on commit e0ea709

Please sign in to comment.