Skip to content

Commit 6be595d

Browse files
committed
Attempt to fix #242
1 parent 8f7d060 commit 6be595d

File tree

2 files changed

+86
-40
lines changed

2 files changed

+86
-40
lines changed

src/DotNext.Tests/Net/Cluster/Consensus/Raft/MemoryBasedStateMachineTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,34 @@ public static async Task Overwrite(long? maxLogEntrySize)
333333
}
334334
}
335335

336+
[Fact]
337+
public static async Task OverwriteUnsealed()
338+
{
339+
var entry1 = new TestLogEntry("SET A = 0 SET B=1 SET C=2 SET D=3 SET E=4 SET F=5") { Term = 42L };
340+
var entry2 = new TestLogEntry("SET Y = 1") { Term = 43L };
341+
Func<IReadOnlyList<IRaftLogEntry>, long?, CancellationToken, ValueTask<Missing>> checker;
342+
var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
343+
using (var state = new PersistentStateWithoutSnapshot(dir, RecordsPerPartition, new() { UseCaching = false }))
344+
{
345+
Equal(1L, await state.AppendAsync(entry1));
346+
await state.AppendAsync(entry2, 1L);
347+
}
348+
349+
//read again
350+
using (var state = new PersistentStateWithoutSnapshot(dir, RecordsPerPartition, new() { UseCaching = false }))
351+
{
352+
checker = async (entries, snapshotIndex, token) =>
353+
{
354+
Null(snapshotIndex);
355+
Single(entries);
356+
False(entries[0].IsSnapshot);
357+
Equal(entry2.Content, await entries[0].ToStringAsync(Encoding.UTF8));
358+
return Missing.Value;
359+
};
360+
await state.As<IRaftLog>().ReadAsync(new LogEntryConsumer(checker), 1L, CancellationToken.None);
361+
}
362+
}
363+
336364
[Obsolete]
337365
[Fact]
338366
public static async Task LegacyOverwrite()

src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/PersistentState.Partition.cs

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -505,11 +505,12 @@ internal override void Initialize()
505505
if (RandomAccess.Read(Handle, header.Span, fileOffset: 0L) < HeaderSize)
506506
{
507507
header.Span.Clear();
508+
writer.FilePosition = HeaderSize;
508509
}
509510
else if (IsSealed)
510511
{
511512
// partition is completed, read table
512-
fileOffset = RandomAccess.GetLength(Handle);
513+
writer.FilePosition = fileOffset = RandomAccess.GetLength(Handle);
513514

514515
if (fileOffset < footer.Length + HeaderSize)
515516
throw new IntegrityException(ExceptionMessages.InvalidPartitionFormat);
@@ -533,7 +534,7 @@ internal override void Initialize()
533534
fileOffset = HeaderSize;
534535
}
535536

536-
for (Span<byte> metadataBuffer = stackalloc byte[LogEntryMetadata.Size], metadataTable = footer.Span; ; footerOffset += LogEntryMetadata.Size)
537+
for (Span<byte> metadataBuffer = stackalloc byte[LogEntryMetadata.Size], metadataTable = footer.Span; footerOffset < footer.Length; footerOffset += LogEntryMetadata.Size)
537538
{
538539
var count = RandomAccess.Read(Handle, metadataBuffer, fileOffset);
539540
if (count < LogEntryMetadata.Size)
@@ -543,6 +544,7 @@ internal override void Initialize()
543544
if (fileOffset <= 0L)
544545
break;
545546

547+
writer.FilePosition = fileOffset;
546548
metadataBuffer.CopyTo(metadataTable.Slice(footerOffset, LogEntryMetadata.Size));
547549
}
548550
}
@@ -562,6 +564,7 @@ private long GetWriteAddress(int index)
562564
protected override async ValueTask PersistAsync<TEntry>(TEntry entry, int index, CancellationToken token)
563565
{
564566
var writeAddress = GetWriteAddress(index);
567+
await UnsealIfNeededAsync(writeAddress, token).ConfigureAwait(false);
565568

566569
LogEntryMetadata metadata;
567570
Memory<byte> metadataBuffer;
@@ -601,6 +604,8 @@ protected override async ValueTask WriteThroughAsync(CachedLogEntry entry, int i
601604
Debug.Assert(writer.HasBufferedData is false);
602605

603606
var writeAddress = GetWriteAddress(index);
607+
await UnsealIfNeededAsync(writeAddress, token).ConfigureAwait(false);
608+
604609
var startPos = writeAddress + LogEntryMetadata.Size;
605610
var metadata = LogEntryMetadata.Create(entry, startPos, entry.Length);
606611
var metadataBuffer = GetMetadataBuffer(index);
@@ -621,12 +626,59 @@ protected override void OnCached(in CachedLogEntry cachedEntry, int index)
621626
metadata.Format(GetMetadataBuffer(index).Span);
622627
}
623628

629+
private ValueTask UnsealIfNeededAsync(long truncatePosition, CancellationToken token)
630+
{
631+
ValueTask task;
632+
if (IsSealed)
633+
{
634+
task = UnsealAsync(truncatePosition, token);
635+
}
636+
else if (token.IsCancellationRequested)
637+
{
638+
task = ValueTask.FromCanceled(token);
639+
}
640+
else if (truncatePosition < writer.FilePosition)
641+
{
642+
task = new();
643+
try
644+
{
645+
// The caller is trying to rewrite the log entry.
646+
// For a correctness of Initialize() method for unsealed partitions, we
647+
// need to adjust file size. This is expensive syscall which can lead to file fragmentation.
648+
// However, this is acceptable because rare.
649+
RandomAccess.SetLength(Handle, truncatePosition);
650+
}
651+
catch (Exception e)
652+
{
653+
task = ValueTask.FromException(e);
654+
}
655+
}
656+
else
657+
{
658+
task = new();
659+
}
660+
661+
return task;
662+
}
663+
664+
private async ValueTask UnsealAsync(long truncatePosition, CancellationToken token)
665+
{
666+
// This is expensive operation in terms of I/O. However, it is needed only when
667+
// the consumer decided to rewrite the existing log entry, which is rare.
668+
IsSealed = false;
669+
await WriteHeaderAsync(token).ConfigureAwait(false);
670+
RandomAccess.FlushToDisk(Handle);
671+
672+
// destroy all entries in the tail of partition
673+
RandomAccess.SetLength(Handle, truncatePosition);
674+
}
675+
624676
public override ValueTask FlushAsync(CancellationToken token = default)
625677
{
626-
return runningIndex == LastIndex
678+
return IsSealed
679+
? ValueTask.CompletedTask
680+
: runningIndex == LastIndex
627681
? FlushAndSealAsync(token)
628-
: IsSealed
629-
? FlushAndUnsealAsync(token)
630682
: base.FlushAsync(token);
631683
}
632684

@@ -651,41 +703,7 @@ private async ValueTask FlushAndSealAsync(CancellationToken token)
651703
IsSealed = true;
652704
await WriteHeaderAsync(token).ConfigureAwait(false);
653705

654-
writer.FlushToDisk();
655-
}
656-
657-
private async ValueTask FlushAndUnsealAsync(CancellationToken token)
658-
{
659-
await FlushAndEraseNextEntryAsync(token).ConfigureAwait(false);
660-
661-
IsSealed = false;
662-
await WriteHeaderAsync(token).ConfigureAwait(false);
663-
664-
writer.FlushToDisk();
665-
}
666-
667-
private ValueTask FlushAndEraseNextEntryAsync(CancellationToken token)
668-
{
669-
ValueTask task;
670-
671-
// write the rest of the entry,
672-
// then cleanup next entry header to indicate that the current entry is the last entry
673-
if (!writer.HasBufferedData)
674-
{
675-
task = RandomAccess.WriteAsync(Handle, EmptyMetadata, writer.FilePosition, token);
676-
}
677-
else if (writer.Buffer is { Length: >= LogEntryMetadata.Size } emptyMetadataStub)
678-
{
679-
emptyMetadataStub.Span.Slice(0, LogEntryMetadata.Size).Clear();
680-
writer.Produce(LogEntryMetadata.Size);
681-
task = writer.WriteAsync(token);
682-
}
683-
else
684-
{
685-
task = writer.WriteAsync(EmptyMetadata, token);
686-
}
687-
688-
return task;
706+
RandomAccess.FlushToDisk(Handle);
689707
}
690708

691709
private ValueTask WriteHeaderAsync(CancellationToken token)

0 commit comments

Comments
 (0)