Skip to content

Commit 6bb2434

Browse files
authored
Limit possibility of file corruption (#58)
* Fix indentation on test that catches a possible recursion loop * Minimize chance of file corruption * Bugfix version bump
1 parent ed9ece4 commit 6bb2434

File tree

4 files changed

+38
-42
lines changed

4 files changed

+38
-42
lines changed

CacheAdvance.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'CacheAdvance'
3-
s.version = '1.2.1'
3+
s.version = '1.2.2'
44
s.license = 'Apache License, Version 2.0'
55
s.summary = 'A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite.'
66
s.homepage = 'https://github.com/dfed/CacheAdvance'

Sources/CacheAdvance/CacheAdvance.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,18 @@ public final class CacheAdvance<T: Codable> {
112112

113113
let cacheHasSpaceForNewMessageBeforeEndOfFile = writer.offsetInFile + bytesNeededToStoreMessage <= header.maximumBytes
114114
if header.overwritesOldMessages {
115-
let truncateAtOffset: UInt64?
116-
if cacheHasSpaceForNewMessageBeforeEndOfFile {
117-
// We have room for this message. No need to truncate.
118-
truncateAtOffset = nil
119-
} else {
115+
if !cacheHasSpaceForNewMessageBeforeEndOfFile {
120116
// This message can't be written without exceeding our maximum file length.
121117
// We'll need to start writing the file from the beginning of the file.
122118

123119
// Trim the file to the current writer position to remove soon-to-be-abandoned data from the file.
124-
truncateAtOffset = writer.offsetInFile
120+
try writer.truncate(at: writer.offsetInFile)
121+
122+
// Update the offsetInFileAtEndOfNewestMessage in our header and reader such that we will ignore the now-deleted data in the file.
123+
// If the application crashes between writing this header and writing the next message data, we'll have only lost the messages we were about to delete.
124+
// If `writer.offsetInFile == FileHeader.expectedEndOfHeaderInFile`, then crashing before we update the header would lead to this now-empty file being viewed as corrupted.
125+
try header.updateOffsetInFileAtEndOfNewestMessage(to: writer.offsetInFile)
126+
reader.offsetInFileAtEndOfNewestMessage = writer.offsetInFile
125127

126128
// Set the offset back to the beginning of the file.
127129
try writer.seek(to: FileHeader.expectedEndOfHeaderInFile)
@@ -144,12 +146,6 @@ public final class CacheAdvance<T: Codable> {
144146
// If the application crashes between writing the header and writing the message data, we'll have lost the messages between the previous offsetInFileOfOldestMessage and the new offsetInFileOfOldestMessage.
145147
try header.updateOffsetInFileOfOldestMessage(to: offsetInFileOfOldestMessage)
146148

147-
// Truncate the file if it needs truncation before we write the next message, and after we update our header.
148-
// If the application crashes between truncating this message data and writing the next message, our file will still be consistent.
149-
if let truncateAtOffset = truncateAtOffset {
150-
try writer.truncate(at: truncateAtOffset)
151-
}
152-
153149
// Let the reader know where the oldest message begins.
154150
reader.offsetInFileOfOldestMessage = offsetInFileOfOldestMessage
155151

Sources/CacheAdvance/CacheReader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ final class CacheReader {
6363
guard !previousReadWasEmpty else {
6464
// If the previous read was also empty, then the file has been corrupted.
6565
// Two empty reads in a row means that offsetInFileAtEndOfNewestMessage is incorrect.
66-
// This inconsistency is likely due to a crash occurring during a message write.
67-
// This issue will not occur in a current version of the library, but an earlier version was capable of creating this corruption.
66+
// This inconsistency is likely due to a crash occurring during a message write, between truncating the file and updating the header values to reflect that the file was truncated.
67+
// This file is actually empty, despite what the header tells us.
6868
throw CacheAdvanceError.fileCorrupted
6969
}
7070
// We know the next message is at the end of the file header. Let's seek to it.

Tests/CacheAdvanceTests/CacheAdvanceTests.swift

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,34 @@ final class CacheAdvanceTests: XCTestCase {
9696
XCTAssertEqual(messages, [])
9797
}
9898

99-
func test_messages_throwsFileCorruptedWhenOffsetInFileAtEndOfNewsetMessageOutOfSync() throws {
100-
let randomHighValue: UInt64 = 10_1000
101-
let header = try CacheHeaderHandle(
102-
forReadingFrom: testFileLocation,
103-
maximumBytes: randomHighValue,
104-
overwritesOldMessages: true)
105-
let cache = CacheAdvance<TestableMessage>(
106-
fileURL: testFileLocation,
107-
writer: try FileHandle(forWritingTo: testFileLocation),
108-
reader: try CacheReader(forReadingFrom: testFileLocation),
109-
header: try CacheHeaderHandle(
99+
func test_messages_throwsFileCorruptedWhenOffsetInFileAtEndOfNewsetMessageOutOfSync() throws {
100+
let randomHighValue: UInt64 = 10_1000
101+
let header = try CacheHeaderHandle(
110102
forReadingFrom: testFileLocation,
111-
maximumBytes: header.maximumBytes,
112-
overwritesOldMessages: header.overwritesOldMessages),
113-
decoder: JSONDecoder(),
114-
encoder: JSONEncoder())
115-
116-
// Make sure the header data is persisted before we read it as part of the `messages()` call below.
117-
try header.synchronizeHeaderData()
118-
// Our file is empty. Make the file corrupted by setting the offset at end of newest message to be further in the file.
119-
// This should never happen, but past versions of this repo could lead to a file having this kind of inconsistency if a crash occurred at the wrong time.
120-
try header.updateOffsetInFileAtEndOfNewestMessage(
121-
to: FileHeader.expectedEndOfHeaderInFile + 1)
122-
123-
XCTAssertThrowsError(try cache.messages()) {
124-
XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted)
125-
}
126-
}
103+
maximumBytes: randomHighValue,
104+
overwritesOldMessages: true)
105+
let cache = CacheAdvance<TestableMessage>(
106+
fileURL: testFileLocation,
107+
writer: try FileHandle(forWritingTo: testFileLocation),
108+
reader: try CacheReader(forReadingFrom: testFileLocation),
109+
header: try CacheHeaderHandle(
110+
forReadingFrom: testFileLocation,
111+
maximumBytes: header.maximumBytes,
112+
overwritesOldMessages: header.overwritesOldMessages),
113+
decoder: JSONDecoder(),
114+
encoder: JSONEncoder())
115+
116+
// Make sure the header data is persisted before we read it as part of the `messages()` call below.
117+
try header.synchronizeHeaderData()
118+
// Our file is empty. Make the file corrupted by setting the offset at end of newest message to be further in the file.
119+
// This should never happen, but past versions of this repo could lead to a file having this kind of inconsistency if a crash occurred at the wrong time.
120+
try header.updateOffsetInFileAtEndOfNewestMessage(
121+
to: FileHeader.expectedEndOfHeaderInFile + 1)
122+
123+
XCTAssertThrowsError(try cache.messages()) {
124+
XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted)
125+
}
126+
}
127127

128128
func test_isWritable_returnsTrueWhenStaticHeaderMetadataMatches() throws {
129129
let originalCache = try createCache(overwritesOldMessages: false)

0 commit comments

Comments
 (0)