public async Task KeysAndValues_SupportsFixedLengthStructs() { // Arrange var dummyOptions = new MixedStorageKVStoreOptions() { LogDirectory = _fixture.TempDirectory, LogFileNamePrefix = nameof(KeysAndValues_SupportsFixedLengthStructs), PageSizeBits = 12, MemorySizeBits = 13 // Limit to 8KB so we're testing both in-memory and disk-based operations }; var dummyStructInstance = new DummyFixedLengthStruct() { // Populate with dummy values DummyByte = byte.MaxValue, DummyShort = short.MaxValue, DummyLong = long.MaxValue }; int numRecords = 10000; using var testSubject = new MixedStorageKVStore <DummyFixedLengthStruct, DummyFixedLengthStruct>(dummyOptions); // Act and assert // Insert Parallel.For(0, numRecords, key => { DummyFixedLengthStruct localDummyStructInstance = dummyStructInstance; localDummyStructInstance.DummyInt = key; testSubject.Upsert(localDummyStructInstance, localDummyStructInstance); }); // Read List <Task <(Status, DummyFixedLengthStruct)> > readTasks = new(); for (int key = 0; key < numRecords; key++) { DummyFixedLengthStruct localDummyStructInstance = dummyStructInstance; localDummyStructInstance.DummyInt = key; readTasks.Add(ReadAsync(localDummyStructInstance, testSubject)); } await Task.WhenAll(readTasks).ConfigureAwait(false); // Verify for (int key = 0; key < numRecords; key++) { (Status status, DummyFixedLengthStruct result) = readTasks[key].Result; Assert.Equal(Status.OK, status); DummyFixedLengthStruct localDummyStructInstance = dummyStructInstance; localDummyStructInstance.DummyInt = key; Assert.Equal(localDummyStructInstance, result); } ; }
public async Task KeysAndValues_SupportsPrimitives() { // Arrange var dummyOptions = new MixedStorageKVStoreOptions() { LogDirectory = _fixture.TempDirectory, LogFileNamePrefix = nameof(KeysAndValues_SupportsPrimitives), PageSizeBits = 12, MemorySizeBits = 13 // Limit to 8KB so we're testing both in-memory and disk-based operations }; const int dummyValue = 12345; const int numRecords = 10000; using var testSubject = new MixedStorageKVStore <int, int>(dummyOptions); // Act and assert Parallel.For(0, numRecords, key => testSubject.Upsert(key, dummyValue)); await ReadAndVerifyValuesAsync(numRecords, testSubject, Status.OK, dummyValue).ConfigureAwait(false); }
public async Task KeysAndValues_SupportsObjects() { // Arrange var dummyOptions = new MixedStorageKVStoreOptions() { LogDirectory = _fixture.TempDirectory, LogFileNamePrefix = nameof(KeysAndValues_SupportsObjects), PageSizeBits = 12, MemorySizeBits = 13 // Limit to 8KB so we're testing both in-memory and disk-based operations }; int numRecords = 10000; using var testSubject = new MixedStorageKVStore <string, string>(dummyOptions); // Act and assert // Insert Parallel.For(0, numRecords, key => { string keyAsString = key.ToString(); testSubject.Upsert(keyAsString, keyAsString); }); // Read List <Task <(Status, string?)> > readTasks = new(); for (int key = 0; key < numRecords; key++) { readTasks.Add(ReadAsync(key.ToString(), testSubject)); } await Task.WhenAll(readTasks).ConfigureAwait(false); // Verify Parallel.For(0, numRecords, key => { (Status status, string?result) = readTasks[key].Result; Assert.Equal(Status.OK, status); Assert.Equal(key.ToString(), result); }); }
public async Task UpsertReadAsyncDelete_AreThreadSafe() { // Arrange var dummyOptions = new MixedStorageKVStoreOptions() { LogDirectory = _fixture.TempDirectory, LogFileNamePrefix = nameof(UpsertReadAsyncDelete_AreThreadSafe), PageSizeBits = 12, MemorySizeBits = 13 // Limit to 8KB so we're testing both in-memory and disk-based operations }; DummyClass dummyClassInstance = CreatePopulatedDummyClassInstance(); int numRecords = 10000; //using var testSubject = new ObjLogMixedStorageKVStore<int, DummyClass>(dummyOptions); //using var testSubject = new MemoryMixedStorageKVStore<int, DummyClass>(dummyOptions); using var testSubject = new MixedStorageKVStore <int, DummyClass>(dummyOptions); // Act and assert // Insert Parallel.For(0, numRecords, key => testSubject.Upsert(key, dummyClassInstance)); // Read await ReadAndVerifyValuesAsync(numRecords, testSubject, Status.OK, dummyClassInstance).ConfigureAwait(false); // Update dummyClassInstance.DummyInt = 20; dummyClassInstance.DummyString = "anotherDummyString"; Parallel.For(0, numRecords, key => testSubject.Upsert(key, dummyClassInstance)); // Verify updates await ReadAndVerifyValuesAsync(numRecords, testSubject, Status.OK, dummyClassInstance).ConfigureAwait(false); // Delete Parallel.For(0, numRecords, key => testSubject.Delete(key)); // Verify deletes await ReadAndVerifyValuesAsync(numRecords, testSubject, Status.NOTFOUND, null).ConfigureAwait(false); }
public async Task LogCompaction_SkippedIfSafeReadOnlyRegionIsLessThanThreshold() { // Arrange var resultStringBuilder = new StringBuilder(); ILogger <MixedStorageKVStore <int, DummyClass> > dummyLogger = CreateLogger <int, DummyClass>(resultStringBuilder, LogLevel.Trace); var logSettings = new LogSettings { LogDevice = Devices.CreateLogDevice(Path.Combine(_fixture.TempDirectory, $"{nameof(LogCompaction_SkippedIfSafeReadOnlyRegionIsLessThanThreshold)}.log"), deleteOnClose: true), PageSizeBits = 12, MemorySizeBits = 13 }; DummyClass dummyClassInstance = CreatePopulatedDummyClassInstance(); int dummyThreshold = 100_000; var dummyOptions = new MixedStorageKVStoreOptions() { TimeBetweenLogCompactionsMS = 1, InitialLogCompactionThresholdBytes = 100_000 }; int expectedResultMinLength = string.Format(Strings.LogTrace_SkippingLogCompaction, 0, dummyThreshold).Length; // Act using (var testSubject = new MixedStorageKVStore <int, DummyClass>(dummyOptions, dummyLogger)) // Start log compaction { while (resultStringBuilder.Length <= expectedResultMinLength) { await Task.Delay(10).ConfigureAwait(false); } } // Assert string result = resultStringBuilder.ToString(); int numLines = result.Split("\n", StringSplitOptions.RemoveEmptyEntries).Length; string regexPattern = string.Format(Strings.LogTrace_SkippingLogCompaction, "0", dummyThreshold); // MixedStorageKVStore is empty Assert.Equal(numLines, Regex.Matches(result, regexPattern).Count); }
public void LogFiles_DeletedOnClose() { // Arrange string directory = Path.Combine(_fixture.TempDirectory, nameof(LogFiles_DeletedOnClose)); // Use a separate directory so the test is never affected by other tests var dummyOptions = new MixedStorageKVStoreOptions() { LogDirectory = directory, LogFileNamePrefix = nameof(LogFiles_DeletedOnClose), PageSizeBits = 9, // Minimum MemorySizeBits = 10 // Minimum }; DummyClass dummyClassInstance = CreatePopulatedDummyClassInstance(); int numRecords = 50; // Just enough to make sure log files are created. Segment size isn't exceeded (only 1 of each log file). var testSubject = new MixedStorageKVStore <int, DummyClass>(dummyOptions); Parallel.For(0, numRecords, key => testSubject.Upsert(key, dummyClassInstance)); // Creates log Assert.Single(Directory.EnumerateFiles(directory, $"{nameof(LogFiles_DeletedOnClose)}*")); // Log and object log // Act testSubject.Dispose(); // Assert Assert.Empty(Directory.EnumerateFiles(directory)); // Logs deleted }
public async Task LogCompaction_OccursIfSafeReadOnlyRegionIsLargerThanThreshold() { // Arrange var resultStringBuilder = new StringBuilder(); ILogger <MixedStorageKVStore <int, DummyClass> > dummyLogger = CreateLogger <int, DummyClass>(resultStringBuilder, LogLevel.Trace); var logSettings = new LogSettings { LogDevice = Devices.CreateLogDevice(Path.Combine(_fixture.TempDirectory, $"{nameof(LogCompaction_OccursIfSafeReadOnlyRegionIsLargerThanThreshold)}.log"), deleteOnClose: true), PageSizeBits = 12, MemorySizeBits = 13 }; DummyClass dummyClassInstance = CreatePopulatedDummyClassInstance(); var dummyOptions = new MixedStorageKVStoreOptions() { TimeBetweenLogCompactionsMS = 1, InitialLogCompactionThresholdBytes = 80_000 }; // Create and populate faster KV store before passing it to MixedStorageKVStore, at which point the compaction loop starts. // For quicker tests, use thread local sessions. FasterKV <SpanByte, SpanByte>?dummyFasterKVStore = null; ThreadLocal <ClientSession <SpanByte, SpanByte, SpanByte, SpanByteAndMemory, Empty, SpanByteFunctions <Empty> > >?dummyThreadLocalSession = null; try { dummyFasterKVStore = new FasterKV <SpanByte, SpanByte>(1L << 20, logSettings); FasterKV <SpanByte, SpanByte> .ClientSessionBuilder <SpanByte, SpanByteAndMemory, Empty> dummyClientSessionBuilder = dummyFasterKVStore.For(new SpanByteFunctions <Empty>()); dummyThreadLocalSession = new(() => dummyClientSessionBuilder.NewSession <SpanByteFunctions <Empty> >(), true); MessagePackSerializerOptions dummyMessagePackSerializerOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4BlockArray); // Record size estimate: // - dummyClassInstance serialized and compressed = ~73 bytes // - int key serialized and compressed = ~3 bytes // - key length metadata = 4 bytes // - value length metadata = 4 bytes // - record header = 8 bytes // Total = ~92 bytes // // n * 92 - 8192 > InitialLogCompactionThresholdBytes // Insert byte[] dummyValueBytes = MessagePackSerializer.Serialize(dummyClassInstance, dummyMessagePackSerializerOptions); Parallel.For(0, 500, key => { byte[] dummyKeyBytes = MessagePackSerializer.Serialize(key, dummyMessagePackSerializerOptions); unsafe { // Upsert fixed(byte *keyPointer = dummyKeyBytes) fixed(byte *valuePointer = dummyValueBytes) { var keySpanByte = SpanByte.FromPointer(keyPointer, dummyKeyBytes.Length); var objSpanByte = SpanByte.FromPointer(valuePointer, dummyValueBytes.Length); dummyThreadLocalSession.Value.Upsert(ref keySpanByte, ref objSpanByte); } } }); // Update so compaction does something. Can't update in insert loop or we'll get a bunch of in-place updates. dummyClassInstance.DummyInt++; dummyValueBytes = MessagePackSerializer.Serialize(dummyClassInstance, dummyMessagePackSerializerOptions); Parallel.For(0, 500, key => { byte[] dummyKeyBytes = MessagePackSerializer.Serialize(key, dummyMessagePackSerializerOptions); unsafe { // Upsert fixed(byte *keyPointer = dummyKeyBytes) fixed(byte *valuePointer = dummyValueBytes) { var keySpanByte = SpanByte.FromPointer(keyPointer, dummyKeyBytes.Length); var objSpanByte = SpanByte.FromPointer(valuePointer, dummyValueBytes.Length); dummyThreadLocalSession.Value.Upsert(ref keySpanByte, ref objSpanByte); } } }); } finally { if (dummyThreadLocalSession != null) { foreach (ClientSession <SpanByte, SpanByte, SpanByte, SpanByteAndMemory, Empty, SpanByteFunctions <Empty> > session in dummyThreadLocalSession.Values) { session.Dispose(); // Faster synchronously completes all pending operations, so we should not get exceptions if we're in the middle of log compaction } } } // We compact 20% of the safe-readonly region of the log. Since we inserted then updated, compaction here means removal. // 90048 * 0.8 = 72038, ~72000 (missing 38 bytes likely has to do with the fact that we can't remove only part of a record). string expectedResultStart = $"{LogLevel.Trace}: {string.Format(Strings.LogTrace_LogCompacted, 90048, 72000, 1)}"; int expectedResultStartLength = expectedResultStart.Length; // Act using (var testSubject = new MixedStorageKVStore <int, DummyClass>(dummyOptions, dummyLogger, dummyFasterKVStore)) // Start log compaction { while (resultStringBuilder.Length <= expectedResultStartLength) { await Task.Delay(10).ConfigureAwait(false); } } // Assert Assert.StartsWith(expectedResultStart, resultStringBuilder.ToString()); // If compaction runs more than once, should be skipped after first compaction (< threshold behaviour verified in previous test) }