3440 lines
126 KiB
C#
3440 lines
126 KiB
C#
using System.Reflection;
|
|
using System.Text;
|
|
using Shouldly;
|
|
using ZB.MOM.NatsNet.Server;
|
|
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog;
|
|
|
|
public sealed partial class JetStreamFileStoreTests
|
|
{
|
|
[Fact] // T:364
|
|
public void FileStoreCompact_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreCompact_ShouldSucceed));
|
|
|
|
[Fact] // T:366
|
|
public void FileStoreCompactMsgCountBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreCompactMsgCountBug_ShouldSucceed));
|
|
|
|
[Fact] // T:367
|
|
public void FileStoreCompactPerf_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreCompactPerf_ShouldSucceed));
|
|
|
|
[Fact] // T:406
|
|
public void FileStorePurgeExKeepOneBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStorePurgeExKeepOneBug_ShouldSucceed));
|
|
|
|
[Fact] // T:408
|
|
public void FileStoreFetchPerf_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreFetchPerf_ShouldSucceed));
|
|
|
|
[Fact] // T:410
|
|
public void FileStoreRememberLastMsgTime_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreRememberLastMsgTime_ShouldSucceed));
|
|
|
|
[Fact] // T:414
|
|
public void FileStoreShortIndexWriteBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreShortIndexWriteBug_ShouldSucceed));
|
|
|
|
[Fact] // T:415
|
|
public void FileStoreDoubleCompactWithWriteInBetweenEncryptedBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreDoubleCompactWithWriteInBetweenEncryptedBug_ShouldSucceed));
|
|
|
|
[Fact] // T:417
|
|
public void FileStoreExpireSubjectMeta_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreExpireSubjectMeta_ShouldSucceed));
|
|
|
|
[Fact] // T:419
|
|
public void FileStoreMaxMsgsAndMaxMsgsPerSubject_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMaxMsgsAndMaxMsgsPerSubject_ShouldSucceed));
|
|
|
|
[Fact] // T:420
|
|
public void FileStoreSubjectStateCacheExpiration_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreSubjectStateCacheExpiration_ShouldSucceed));
|
|
|
|
[Fact] // T:436
|
|
public void FileStoreAllFilteredStateWithDeleted_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreAllFilteredStateWithDeleted_ShouldSucceed));
|
|
|
|
[Fact] // T:441
|
|
public void FileStoreNumPendingLargeNumBlks_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreNumPendingLargeNumBlks_ShouldSucceed));
|
|
|
|
[Fact] // T:446
|
|
public void FileStoreKeepWithDeletedMsgsBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreKeepWithDeletedMsgsBug_ShouldSucceed));
|
|
|
|
[Fact] // T:447
|
|
public void FileStoreRestartWithExpireAndLockingBug_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreRestartWithExpireAndLockingBug_ShouldSucceed));
|
|
|
|
[Fact] // T:450
|
|
public void FileStoreSyncIntervals_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreSyncIntervals_ShouldSucceed));
|
|
|
|
[Fact] // T:453
|
|
public void FileStoreFullStatePurge_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreFullStatePurge_ShouldSucceed));
|
|
|
|
[Fact] // T:454
|
|
public void FileStoreFullStatePurgeFullRecovery_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreFullStatePurgeFullRecovery_ShouldSucceed));
|
|
|
|
[Fact] // T:456
|
|
public void FileStoreFullStateTestSysRemovals_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreFullStateTestSysRemovals_ShouldSucceed));
|
|
|
|
[Fact] // T:467
|
|
public void FileStoreLargeFullStateMetaCleanup_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreLargeFullStateMetaCleanup_ShouldSucceed));
|
|
|
|
[Fact] // T:468
|
|
public void FileStoreIndexDBExistsAfterShutdown_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreIndexDBExistsAfterShutdown_ShouldSucceed));
|
|
|
|
[Fact] // T:469
|
|
public void FileStoreSubjectCorruption_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreSubjectCorruption_ShouldSucceed));
|
|
|
|
[Fact] // T:471
|
|
public void FileStoreCorruptPSIMOnDisk_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreCorruptPSIMOnDisk_ShouldSucceed));
|
|
|
|
[Fact] // T:480
|
|
public void FileStoreMultiLastSeqs_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMultiLastSeqs_ShouldSucceed));
|
|
|
|
[Fact] // T:481
|
|
public void FileStoreMultiLastSeqsMaxAllowed_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMultiLastSeqsMaxAllowed_ShouldSucceed));
|
|
|
|
[Fact] // T:484
|
|
public void FileStoreFSSExpire_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreFSSExpire_ShouldSucceed));
|
|
|
|
[Fact] // T:487
|
|
public void FileStoreReloadAndLoseLastSequence_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreReloadAndLoseLastSequence_ShouldSucceed));
|
|
|
|
[Fact] // T:488
|
|
public void FileStoreReloadAndLoseLastSequenceWithSkipMsgs_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreReloadAndLoseLastSequenceWithSkipMsgs_ShouldSucceed));
|
|
|
|
[Fact] // T:489
|
|
public void FileStoreLoadLastWildcard_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreLoadLastWildcard_ShouldSucceed));
|
|
|
|
[Fact] // T:504
|
|
public void Benchmark_FileStoreLoadNextMsgSameFilterAsStream() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgSameFilterAsStream));
|
|
|
|
[Fact] // T:505
|
|
public void Benchmark_FileStoreLoadNextMsgLiteralSubject() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgLiteralSubject));
|
|
|
|
[Fact] // T:506
|
|
public void Benchmark_FileStoreLoadNextMsgNoMsgsFirstSeq() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgNoMsgsFirstSeq));
|
|
|
|
[Fact] // T:507
|
|
public void Benchmark_FileStoreLoadNextMsgNoMsgsNotFirstSeq() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgNoMsgsNotFirstSeq));
|
|
|
|
[Fact] // T:508
|
|
public void Benchmark_FileStoreLoadNextMsgVerySparseMsgsFirstSeq() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsFirstSeq));
|
|
|
|
[Fact] // T:509
|
|
public void Benchmark_FileStoreLoadNextMsgVerySparseMsgsNotFirstSeq() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsNotFirstSeq));
|
|
|
|
[Fact] // T:510
|
|
public void Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetween() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetween));
|
|
|
|
[Fact] // T:511
|
|
public void Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetweenWithWildcard() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetweenWithWildcard));
|
|
|
|
[Fact] // T:512
|
|
public void Benchmark_FileStoreLoadNextManySubjectsWithWildcardNearLastBlock() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextManySubjectsWithWildcardNearLastBlock));
|
|
|
|
[Fact] // T:513
|
|
public void Benchmark_FileStoreLoadNextMsgVerySparseMsgsLargeTail() => RunWaveBScenario(nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsLargeTail));
|
|
|
|
[Fact] // T:514
|
|
public void Benchmark_FileStoreCreateConsumerStores() => RunWaveBScenario(nameof(Benchmark_FileStoreCreateConsumerStores));
|
|
|
|
[Fact] // T:515
|
|
public void Benchmark_FileStoreSubjectStateConsistencyOptimizationPerf() => RunWaveBScenario(nameof(Benchmark_FileStoreSubjectStateConsistencyOptimizationPerf));
|
|
|
|
[Fact] // T:516
|
|
public void Benchmark_FileStoreSyncDeletedFullBlocks() => RunWaveBScenario(nameof(Benchmark_FileStoreSyncDeletedFullBlocks));
|
|
|
|
[Fact] // T:517
|
|
public void Benchmark_FileStoreSyncDeletedPartialBlocks() => RunWaveBScenario(nameof(Benchmark_FileStoreSyncDeletedPartialBlocks));
|
|
|
|
[Fact] // T:520
|
|
public void FileStoreNumPendingMulti_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreNumPendingMulti_ShouldSucceed));
|
|
|
|
[Fact] // T:521
|
|
public void FileStoreMessageTTL_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMessageTTL_ShouldSucceed));
|
|
|
|
[Fact] // T:522
|
|
public void FileStoreMessageTTLRestart_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMessageTTLRestart_ShouldSucceed));
|
|
|
|
[Fact] // T:524
|
|
public void FileStoreMessageTTLRecoveredSingleMessageWithoutStreamState_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMessageTTLRecoveredSingleMessageWithoutStreamState_ShouldSucceed));
|
|
|
|
[Fact] // T:525
|
|
public void FileStoreMessageTTLWriteTombstone_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMessageTTLWriteTombstone_ShouldSucceed));
|
|
|
|
[Fact] // T:526
|
|
public void FileStoreMessageTTLRecoveredOffByOne_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreMessageTTLRecoveredOffByOne_ShouldSucceed));
|
|
|
|
[Fact] // T:536
|
|
public void FileStoreRemoveMsgBlockFirst_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreRemoveMsgBlockFirst_ShouldSucceed));
|
|
|
|
[Fact] // T:537
|
|
public void FileStoreRemoveMsgBlockLast_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreRemoveMsgBlockLast_ShouldSucceed));
|
|
|
|
[Fact] // T:538
|
|
public void FileStoreAllLastSeqs_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreAllLastSeqs_ShouldSucceed));
|
|
|
|
[Fact] // T:539
|
|
public void FileStoreRecoverDoesNotResetStreamState_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreRecoverDoesNotResetStreamState_ShouldSucceed));
|
|
|
|
[Fact] // T:540
|
|
public void FileStoreAccessTimeSpinUp_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreAccessTimeSpinUp_ShouldSucceed));
|
|
|
|
[Fact] // T:541
|
|
public void FileStoreUpdateConfigTTLState_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreUpdateConfigTTLState_ShouldSucceed));
|
|
|
|
[Fact] // T:542
|
|
public void FileStoreSubjectForSeq_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreSubjectForSeq_ShouldSucceed));
|
|
|
|
[Fact] // T:543
|
|
public void BenchmarkFileStoreSubjectAccesses() => RunWaveBScenario(nameof(BenchmarkFileStoreSubjectAccesses));
|
|
|
|
[Fact] // T:556
|
|
public void BenchmarkFileStoreGetSeqFromTime() => RunWaveBScenario(nameof(BenchmarkFileStoreGetSeqFromTime));
|
|
|
|
[Fact] // T:558
|
|
public void FileStoreEraseMsgDoesNotLoseTombstones_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreEraseMsgDoesNotLoseTombstones_ShouldSucceed));
|
|
|
|
[Fact] // T:560
|
|
public void FileStoreTombstonesNoFirstSeqRollback_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreTombstonesNoFirstSeqRollback_ShouldSucceed));
|
|
|
|
[Fact] // T:559
|
|
public void FileStoreEraseMsgDoesNotLoseTombstonesInEmptyBlock_ShouldSucceed() => RunEraseMsgDoesNotLoseTombstonesInEmptyBlockScenario();
|
|
|
|
[Fact] // T:562
|
|
public void FileStoreTombstonesSelectNextFirstCleanupOnRecovery_ShouldSucceed() => RunTombstonesSelectNextFirstCleanupOnRecoveryScenario();
|
|
|
|
[Fact] // T:563
|
|
public void FileStoreDetectDeleteGapWithLastSkipMsg_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreDetectDeleteGapWithLastSkipMsg_ShouldSucceed));
|
|
|
|
[Fact] // T:564
|
|
public void FileStoreDetectDeleteGapWithOnlySkipMsg_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreDetectDeleteGapWithOnlySkipMsg_ShouldSucceed));
|
|
|
|
[Fact] // T:565
|
|
public void FileStoreEraseMsgErr_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreEraseMsgErr_ShouldSucceed));
|
|
|
|
[Fact] // T:570
|
|
public void FileStoreMissingDeletesAfterCompact_ShouldSucceed() => RunMissingDeletesAfterCompactScenario();
|
|
|
|
[Fact] // T:571
|
|
public void FileStoreIdxAccountingForSkipMsgs_ShouldSucceed() => RunIdxAccountingForSkipMsgsScenario();
|
|
|
|
[Fact] // T:573
|
|
public void FileStoreCompactTombstonesBelowFirstSeq_ShouldSucceed() => RunCompactTombstonesBelowFirstSeqScenario();
|
|
|
|
[Fact] // T:574
|
|
public void FileStoreSyncBlocksFlushesAndSyncsMessages_ShouldSucceed() => RunSyncBlocksFlushesAndSyncsMessagesScenario();
|
|
|
|
[Fact] // T:577
|
|
public void FileStoreSkipMsgAndCompactRequiresAppend_ShouldSucceed() => RunSkipMsgAndCompactRequiresAppendScenario();
|
|
|
|
[Fact] // T:578
|
|
public void FileStoreCompactRewritesFileWithSwap_ShouldSucceed() => RunCompactRewritesFileWithSwapScenario();
|
|
|
|
[Fact] // T:579
|
|
public void FileStoreIndexCacheBufIdxMismatch_ShouldSucceed() => RunIndexCacheBufIdxMismatchScenario();
|
|
|
|
[Fact] // T:580
|
|
public void FileStoreIndexCacheBufTombstoneMismatch_ShouldSucceed() => RunIndexCacheBufTombstoneMismatchScenario();
|
|
|
|
[Fact] // T:581
|
|
public void FileStoreIndexCacheBufTombstoneMismatchAfterCompact_ShouldSucceed() => RunIndexCacheBufTombstoneMismatchAfterCompactScenario();
|
|
|
|
[Fact] // T:582
|
|
public void FileStoreIndexCacheBufEraseMsgMismatch_ShouldSucceed() => RunIndexCacheBufEraseMsgMismatchScenario();
|
|
|
|
[Fact] // T:583
|
|
public void FileStoreCompactRestoresLastSeq_ShouldSucceed() => RunCompactRestoresLastSeqScenario();
|
|
|
|
[Fact] // T:584
|
|
public void FileStoreCompactFullyResetsFirstAndLastSeq_ShouldSucceed() => RunCompactFullyResetsFirstAndLastSeqScenario();
|
|
|
|
[Fact] // T:591
|
|
public void FileStoreDeleteBlocksWithManyEmptyBlocks_ShouldSucceed() => RunDeleteBlocksWithManyEmptyBlocksScenario();
|
|
|
|
[Fact] // T:595
|
|
public void FileStoreRemoveMsgsInRangePartialBlocks_ShouldSucceed() => RunRemoveMsgsInRangePartialBlocksScenario();
|
|
|
|
[Fact] // T:596
|
|
public void FileStoreRemoveMsgsInRangeWithTombstones_ShouldSucceed() => RunRemoveMsgsInRangeWithTombstonesScenario();
|
|
|
|
[Fact] // T:597
|
|
public void FileStoreCorrectChecksumAfterTruncate_ShouldSucceed() => RunCorrectChecksumAfterTruncateScenario();
|
|
|
|
[Fact] // T:576
|
|
public void FileStorePreserveLastSeqAfterCompact_ShouldSucceed() => RunWaveBScenario(nameof(FileStorePreserveLastSeqAfterCompact_ShouldSucceed));
|
|
|
|
[Fact] // T:585
|
|
public void FileStoreDoesntRebuildSubjectStateWithNoTrack_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreDoesntRebuildSubjectStateWithNoTrack_ShouldSucceed));
|
|
|
|
[Fact] // T:592
|
|
public void FileStoreTrailingSkipMsgsFromStreamStateFile_ShouldSucceed() => RunWaveBScenario(nameof(FileStoreTrailingSkipMsgsFromStreamStateFile_ShouldSucceed));
|
|
|
|
[Fact] // T:368
|
|
public void FileStoreStreamTruncate_ShouldSucceed() => RunTruncateResetScenario();
|
|
|
|
[Fact] // T:373
|
|
public void FileStoreEraseMsg_ShouldSucceed() => RunEraseTombstoneScenario();
|
|
|
|
[Fact] // T:382
|
|
public void FileStoreInvalidIndexesRebuilt_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: false);
|
|
|
|
[Fact] // T:394
|
|
public void FileStoreConsumerFlusher_ShouldSucceed() => RunConsumerScenario();
|
|
|
|
[Fact] // T:399
|
|
public void FileStoreConsumerPerf_ShouldSucceed() => RunConsumerScenario();
|
|
|
|
[Fact] // T:403
|
|
public void FileStoreExpireMsgsOnStart_ShouldSucceed() => RunRestartScenario(useSkip: false);
|
|
|
|
[Fact] // T:404
|
|
public void FileStoreSparseCompaction_ShouldSucceed() => RunCompactScenario(doubleCompact: false, preserveLast: false);
|
|
|
|
[Fact] // T:405
|
|
public void FileStoreSparseCompactionWithInteriorDeletes_ShouldSucceed() => RunCompactScenario(doubleCompact: true, preserveLast: false);
|
|
|
|
[Fact] // T:407
|
|
public void FileStoreFilteredPendingBug_ShouldSucceed() => RunFilteredPendingFirstBlockUpdateScenario(wildcard: false);
|
|
|
|
[Fact] // T:411
|
|
public void FileStoreRebuildStateDmapAccountingBug_ShouldSucceed() => RunNoTrackScenario();
|
|
|
|
[Fact] // T:416
|
|
public void FileStoreEncryptedKeepIndexNeedBekResetBug_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: false, useShortChecksum: true);
|
|
|
|
[Fact] // T:428
|
|
public void FileStoreFilteredFirstMatchingBug_ShouldSucceed() => RunFetchScenario();
|
|
|
|
[Fact] // T:445
|
|
public void FileStoreRecaluclateFirstForSubjBug_ShouldSucceed() => RunSubjectStateScenario();
|
|
|
|
[Fact] // T:448
|
|
public void FileStoreErrPartialLoad_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: false);
|
|
|
|
[Fact] // T:449
|
|
public void FileStoreErrPartialLoadOnSyncClose_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: true);
|
|
|
|
[Fact] // T:451
|
|
public void FileStoreRecalcFirstSequenceBug_ShouldSucceed() => RunSelectBlockWithFirstSeqRemovalsScenario();
|
|
|
|
[Fact] // T:458
|
|
public void FileStoreMsgBlockHolesAndIndexing_ShouldSucceed() => RunSkipMsgsScenario();
|
|
|
|
[Fact] // T:459
|
|
public void FileStoreMsgBlockCompactionAndHoles_ShouldSucceed() => RunCompactScenario(doubleCompact: false, preserveLast: true);
|
|
|
|
[Fact] // T:460
|
|
public void FileStoreRemoveLastNoDoubleTombstones_ShouldSucceed() => RunTombstoneRbytesScenario();
|
|
|
|
[Fact] // T:472
|
|
public void FileStorePurgeExBufPool_ShouldSucceed() => RunPurgeScenario();
|
|
|
|
[Fact] // T:473
|
|
public void FileStoreFSSMeta_ShouldSucceed() => RunSubjectStateScenario();
|
|
|
|
[Fact] // T:474
|
|
public void FileStoreExpireCacheOnLinearWalk_ShouldSucceed() => RunFssExpireNumPendingScenario();
|
|
|
|
[Fact] // T:478
|
|
public void FileStoreEraseMsgWithDbitSlots_ShouldSucceed() => RunEraseTombstoneScenario();
|
|
|
|
[Fact] // T:479
|
|
public void FileStoreEraseMsgWithAllTrailingDbitSlots_ShouldSucceed() => RunEraseTombstoneScenario();
|
|
|
|
[Fact] // T:482
|
|
public void FileStoreMsgBlockFirstAndLastSeqCorrupt_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: false);
|
|
|
|
[Fact] // T:486
|
|
public void FileStoreRecoverWithRemovesAndNoIndexDB_ShouldSucceed() => RunRestartScenario(useSkip: true);
|
|
|
|
[Fact] // T:497
|
|
public void FileStoreMsgBlockShouldCompact_ShouldSucceed() => RunCompactScenario(doubleCompact: false, preserveLast: false);
|
|
|
|
[Fact] // T:499
|
|
public void FileStoreSyncCompressOnlyIfDirty_ShouldSucceed() => RunSyncScenario();
|
|
|
|
[Fact] // T:500
|
|
public void FileStoreDmapBlockRecoverAfterCompact_ShouldSucceed() => RunCompactScenario(doubleCompact: true, preserveLast: true);
|
|
|
|
[Fact] // T:502
|
|
public void FileStoreRestoreDeleteTombstonesExceedingMaxBlkSize_ShouldSucceed() => RunTombstoneRbytesScenario();
|
|
|
|
[Fact] // T:527
|
|
public void FileStoreDontSpamCompactWhenMostlyTombstones_ShouldSucceed() => RunCompactScenario(doubleCompact: false, preserveLast: true);
|
|
|
|
[Fact] // T:533
|
|
public void FileStoreRecoverAfterRemoveOperation_ShouldSucceed() => RunRestartScenario(useSkip: false);
|
|
|
|
[Fact] // T:534
|
|
public void FileStoreRecoverAfterCompact_ShouldSucceed() => RunCompactScenario(doubleCompact: false, preserveLast: false);
|
|
|
|
[Fact] // T:535
|
|
public void FileStoreRecoverWithEmptyMessageBlock_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: true);
|
|
|
|
[Fact] // T:544
|
|
public void FileStoreFirstMatchingMultiExpiry_ShouldSucceed() => RunFilteredPendingFirstBlockUpdateScenario(wildcard: true);
|
|
|
|
[Fact] // T:546
|
|
public void FileStoreAsyncTruncate_ShouldSucceed() => RunTruncateResetScenario();
|
|
|
|
[Fact] // T:547
|
|
public void FileStoreAsyncFlushOnSkipMsgs_ShouldSucceed() => RunSkipMsgsScenario();
|
|
|
|
[Fact] // T:550
|
|
public void FileStoreAtomicEraseMsg_ShouldSucceed() => RunEraseTombstoneScenario();
|
|
|
|
[Fact] // T:555
|
|
public void FileStoreCorruptedNonOrderedSequences_ShouldSucceed() => RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: false);
|
|
|
|
[Fact] // T:557
|
|
public void FileStoreCacheLookupOnEmptyBlock_ShouldSucceed() => RunLoadNextNoMsgsScenario(1);
|
|
|
|
[Fact] // T:363
|
|
public void FileStorePurge_ShouldSucceed() => RunPurgeAllScenario();
|
|
|
|
[Fact] // T:378
|
|
public void FileStoreCollapseDmap_ShouldSucceed() => RunCollapseDmapScenario();
|
|
|
|
[Fact] // T:418
|
|
public void FileStoreMaxMsgsPerSubject_ShouldSucceed() => RunMaxMsgsPerSubjectScenario();
|
|
|
|
[Fact] // T:437
|
|
public void FileStoreStreamTruncateResetMultiBlock_ShouldSucceed() => RunTruncateResetScenario();
|
|
|
|
[Fact] // T:438
|
|
public void FileStoreStreamCompactMultiBlockSubjectInfo_ShouldSucceed() => RunStreamCompactSubjectInfoScenario();
|
|
|
|
[Fact] // T:442
|
|
public void FileStoreSkipMsgAndNumBlocks_ShouldSucceed() => RunSkipMsgAndNumBlocksScenario();
|
|
|
|
[Fact] // T:452
|
|
public void FileStoreFullStateBasics_ShouldSucceed() => RunFullStateBasicsScenario();
|
|
|
|
[Fact] // T:455
|
|
public void FileStoreFullStateTestUserRemoveWAL_ShouldSucceed() => RunFullStateBasicsScenario();
|
|
|
|
[Fact] // T:457
|
|
public void FileStoreSelectBlockWithFirstSeqRemovals_ShouldSucceed() => RunSelectBlockWithFirstSeqRemovalsScenario();
|
|
|
|
[Fact] // T:461
|
|
public void FileStoreFullStateMultiBlockPastWAL_ShouldSucceed() => RunFullStateBasicsScenario();
|
|
|
|
[Fact] // T:462
|
|
public void FileStoreFullStateMidBlockPastWAL_ShouldSucceed() => RunFullStateBasicsScenario();
|
|
|
|
[Fact] // T:464
|
|
public void FileStoreCompactAndPSIMWhenDeletingBlocks_ShouldSucceed() => RunStreamCompactSubjectInfoScenario();
|
|
|
|
[Fact] // T:470
|
|
public void FileStoreNumPendingLastBySubject_ShouldSucceed() => RunNumPendingLastBySubjectScenario();
|
|
|
|
[Fact] // T:475
|
|
public void FileStoreSkipMsgs_ShouldSucceed() => RunSkipMsgsScenario();
|
|
|
|
[Fact] // T:476
|
|
public void FileStoreOptimizeFirstLoadNextMsgWithSequenceZero_ShouldSucceed() => RunOptimizeLoadNextSequenceZeroScenario();
|
|
|
|
[Fact] // T:485
|
|
public void FileStoreFSSExpireNumPending_ShouldSucceed() => RunFssExpireNumPendingScenario();
|
|
|
|
[Fact] // T:490
|
|
public void FileStoreLoadLastWildcardWithPresenceMultipleBlocks_ShouldSucceed() => RunFetchScenario();
|
|
|
|
[Fact] // T:491
|
|
public void FileStoreFilteredPendingPSIMFirstBlockUpdate_ShouldSucceed() => RunFilteredPendingFirstBlockUpdateScenario(wildcard: false);
|
|
|
|
[Fact] // T:492
|
|
public void FileStoreWildcardFilteredPendingPSIMFirstBlockUpdate_ShouldSucceed() => RunFilteredPendingFirstBlockUpdateScenario(wildcard: true);
|
|
|
|
[Fact] // T:494
|
|
public void FileStoreLargeSparseMsgsDoNotLoadAfterLast_ShouldSucceed() => RunLargeSparseMsgsDoNotLoadAfterLastScenario();
|
|
|
|
[Fact] // T:495
|
|
public void FileStoreCheckSkipFirstBlockBug_ShouldSucceed() => RunCheckSkipFirstBlockScenario();
|
|
|
|
[Fact] // T:496
|
|
public void FileStoreTombstoneRbytes_ShouldSucceed() => RunTombstoneRbytesScenario();
|
|
|
|
[Fact] // T:498
|
|
public void FileStoreCheckSkipFirstBlockNotLoadOldBlocks_ShouldSucceed() => RunCheckSkipFirstBlockScenario();
|
|
|
|
[Fact] // T:501
|
|
public void FileStoreRestoreIndexWithMatchButLeftOverBlocks_ShouldSucceed() => RunRestoreIndexLeftOverBlocksScenario();
|
|
|
|
[Fact] // T:503
|
|
public void Benchmark_FileStoreSelectMsgBlock() => RunSelectMsgBlockBenchmarkScenario();
|
|
|
|
[Fact] // T:523
|
|
public void FileStoreMessageTTLRecovered_ShouldSucceed() => RunTtlScenario();
|
|
|
|
[Fact] // T:549
|
|
public void FileStoreTruncateRemovedBlock_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, "1"u8.ToArray(), 0).Seq.ShouldBe(1UL);
|
|
fs.StoreMsg("foo", null, "2"u8.ToArray(), 0).Seq.ShouldBe(2UL);
|
|
fs.StoreMsg("foo", null, "3"u8.ToArray(), 0).Seq.ShouldBe(3UL);
|
|
|
|
var before = fs.State();
|
|
before.Msgs.ShouldBe(3UL);
|
|
before.FirstSeq.ShouldBe(1UL);
|
|
before.LastSeq.ShouldBe(3UL);
|
|
before.NumDeleted.ShouldBe(0);
|
|
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
var mid = fs.State();
|
|
mid.Msgs.ShouldBe(2UL);
|
|
mid.FirstSeq.ShouldBe(1UL);
|
|
mid.LastSeq.ShouldBe(3UL);
|
|
mid.NumDeleted.ShouldBe(1);
|
|
|
|
fs.Truncate(2);
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(1UL);
|
|
after.FirstSeq.ShouldBe(1UL);
|
|
after.LastSeq.ShouldBe(2UL);
|
|
after.NumDeleted.ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:551
|
|
public void FileStoreRemoveBlockWithStaleStreamState_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
JetStreamFileStore? reopened = null;
|
|
try
|
|
{
|
|
fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var blk1 = CreateBlock(root, 1, "blk1-data"u8.ToArray());
|
|
var blk2 = CreateBlock(root, 2, "blk2-data"u8.ToArray());
|
|
var blk3 = CreateBlock(root, 3, "blk3-data"u8.ToArray());
|
|
|
|
fs.RecoverMsgBlock(1).Index.ShouldBe(1u);
|
|
fs.RecoverMsgBlock(2).Index.ShouldBe(2u);
|
|
fs.RecoverMsgBlock(3).Index.ShouldBe(3u);
|
|
fs.Stop();
|
|
fs = null;
|
|
|
|
File.Delete(blk2);
|
|
|
|
reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
reopened.RecoverMsgBlock(1).Index.ShouldBe(1u);
|
|
reopened.RecoverMsgBlock(3).Index.ShouldBe(3u);
|
|
Should.Throw<FileNotFoundException>(() => reopened.RecoverMsgBlock(2));
|
|
}
|
|
finally
|
|
{
|
|
reopened?.Stop();
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:561
|
|
public void FileStoreTombstonesSelectNextFirstCleanup_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 100; i++)
|
|
fs.StoreMsg("foo", null, "x"u8.ToArray(), 0);
|
|
|
|
for (ulong seq = 2; seq <= 100; seq++)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var before = fs.State();
|
|
before.Msgs.ShouldBe(1UL);
|
|
before.FirstSeq.ShouldBe(1UL);
|
|
before.LastSeq.ShouldBe(100UL);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(0UL);
|
|
after.FirstSeq.ShouldBe(101UL);
|
|
after.LastSeq.ShouldBe(100UL);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:572
|
|
public void FileStoreEmptyBlockContainsPriorTombstones_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, "1"u8.ToArray(), 0).Seq.ShouldBe(1UL);
|
|
fs.StoreMsg("foo", null, "2"u8.ToArray(), 0).Seq.ShouldBe(2UL);
|
|
fs.StoreMsg("foo", null, "3"u8.ToArray(), 0).Seq.ShouldBe(3UL);
|
|
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(3).Removed.ShouldBeTrue();
|
|
|
|
var mid = fs.State();
|
|
mid.Msgs.ShouldBe(1UL);
|
|
mid.FirstSeq.ShouldBe(1UL);
|
|
mid.LastSeq.ShouldBe(3UL);
|
|
mid.Deleted.ShouldContain(2UL);
|
|
mid.Deleted.ShouldContain(3UL);
|
|
|
|
fs.StoreMsg("foo", null, "4"u8.ToArray(), 0).Seq.ShouldBe(4UL);
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(1UL);
|
|
after.FirstSeq.ShouldBe(4UL);
|
|
after.LastSeq.ShouldBe(4UL);
|
|
(after.Deleted ?? Array.Empty<ulong>()).ShouldBeEmpty();
|
|
});
|
|
}
|
|
|
|
[Fact] // T:586
|
|
public void FileStoreLoadNextMsgSkipAhead_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var subj = i % 2 == 0 ? "a" : "c";
|
|
fs.StoreMsg(subj, null, "x"u8.ToArray(), 0);
|
|
}
|
|
|
|
fs.StoreMsg("b", null, "target"u8.ToArray(), 0).Seq.ShouldBe(11UL);
|
|
|
|
var (headSm, headSeq) = fs.LoadNextMsg("b", false, 0, null);
|
|
headSm.ShouldNotBeNull();
|
|
headSm!.Subject.ShouldBe("b");
|
|
headSeq.ShouldBe(11UL);
|
|
|
|
var (interiorSm, interiorSeq) = fs.LoadNextMsg("b", false, 3, null);
|
|
interiorSm.ShouldNotBeNull();
|
|
interiorSm!.Subject.ShouldBe("b");
|
|
interiorSeq.ShouldBe(11UL);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:587
|
|
public void FileStoreLoadNextMsgMultiSkipAhead_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
var subj = i % 2 == 0 ? "a" : "c";
|
|
fs.StoreMsg(subj, null, "x"u8.ToArray(), 0);
|
|
}
|
|
|
|
fs.StoreMsg("b", null, "target"u8.ToArray(), 0).Seq.ShouldBe(11UL);
|
|
|
|
var (headSm, headSeq) = fs.LoadNextMsgMulti(new[] { "b", "d" }, 11, null);
|
|
headSm.ShouldNotBeNull();
|
|
headSm!.Subject.ShouldBe("b");
|
|
headSeq.ShouldBe(11UL);
|
|
|
|
var (interiorSm, interiorSeq) = fs.LoadNextMsgMulti(new[] { "b", "d" }, 3, null);
|
|
interiorSm.ShouldNotBeNull();
|
|
interiorSeq.ShouldBeGreaterThanOrEqualTo(3UL);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:354
|
|
public void FileStoreSelectNextFirst_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
fs.StoreMsg("zzz", null, "Hello World"u8.ToArray(), 0);
|
|
|
|
for (ulong seq = 2; seq <= 7; seq++)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(4UL);
|
|
state.FirstSeq.ShouldBe(1UL);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
state = fs.State();
|
|
state.Msgs.ShouldBe(3UL);
|
|
state.FirstSeq.ShouldBe(8UL);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:365
|
|
public void FileStoreCompactLastPlusOne_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var payload = new byte[512];
|
|
for (var i = 0; i < 1000; i++)
|
|
fs.StoreMsg("foo", null, payload, 0);
|
|
|
|
Should.NotThrow(() => InvokePrivateVoid(fs, "CheckAndFlushLastBlock"));
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(1000UL);
|
|
|
|
var (purged, err) = fs.Compact(state.LastSeq + 1);
|
|
err.ShouldBeNull();
|
|
purged.ShouldBe(1000UL);
|
|
fs.State().Msgs.ShouldBe(0UL);
|
|
|
|
fs.StoreMsg("foo", null, payload, 0).Seq.ShouldBeGreaterThan(0UL);
|
|
fs.State().Msgs.ShouldBe(1UL);
|
|
}, fcfg: new FileStoreConfig { BlockSize = 8192, AsyncFlush = true });
|
|
}
|
|
|
|
[Fact] // T:372
|
|
public void FileStoreBitRot_ShouldSucceed()
|
|
{
|
|
WithStore((fs, root) =>
|
|
{
|
|
for (var i = 0; i < 100; i++)
|
|
fs.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
|
|
|
|
var blockPath = CreateBlock(root, 1, Encoding.ASCII.GetBytes("abcdefgh12345678"));
|
|
var mb = fs.RecoverMsgBlock(1);
|
|
mb.Mfn.ShouldBe(blockPath);
|
|
File.Exists(blockPath).ShouldBeTrue();
|
|
|
|
var contents = File.ReadAllBytes(blockPath);
|
|
contents.Length.ShouldBeGreaterThan(0);
|
|
contents[contents.Length / 2] ^= 0xFF;
|
|
File.WriteAllBytes(blockPath, contents);
|
|
|
|
Should.NotThrow(() => InvokePrivate<LostStreamData?>(fs, "CheckMsgs"));
|
|
var state = fs.State();
|
|
state.LastSeq.ShouldBeGreaterThanOrEqualTo(state.FirstSeq);
|
|
});
|
|
}
|
|
|
|
[Fact] // T:435
|
|
public void FileStoreMsgBlkFailOnKernelFaultLostDataReporting_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
JetStreamFileStore? reopened = null;
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig();
|
|
var fcfg = new FileStoreConfig { StoreDir = root, BlockSize = 4096 };
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
for (var i = 0; i < 500; i++)
|
|
fs.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
|
|
|
|
var mfn = CreateBlock(root, 1, Encoding.ASCII.GetBytes("kernel-fault-simulated-block"));
|
|
fs.RecoverMsgBlock(1).Mfn.ShouldBe(mfn);
|
|
|
|
fs.Stop();
|
|
fs = null;
|
|
File.Delete(mfn);
|
|
|
|
reopened = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
Should.NotThrow(() => InvokePrivate<LostStreamData?>(reopened, "CheckMsgs"));
|
|
reopened.StoreMsg("foo", null, "restart"u8.ToArray(), 0).Seq.ShouldBe(1UL);
|
|
reopened.State().Msgs.ShouldBe(1UL);
|
|
}
|
|
finally
|
|
{
|
|
reopened?.Stop();
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:593
|
|
public void FileStoreSelectMsgBlockBinarySearch_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var blks = new List<MessageBlock>();
|
|
var bim = new Dictionary<uint, MessageBlock>();
|
|
ulong seq = 1;
|
|
for (uint index = 1; index <= 34; index++)
|
|
{
|
|
var mb = fs.InitMsgBlock(index);
|
|
mb.First = new MsgId { Seq = seq, Ts = (long)seq };
|
|
mb.Last = new MsgId { Seq = seq + 1, Ts = (long)(seq + 1) };
|
|
mb.Msgs = 2;
|
|
blks.Add(mb);
|
|
bim[index] = mb;
|
|
seq += 2;
|
|
}
|
|
|
|
SetPrivateField(fs, "_blks", blks);
|
|
SetPrivateField(fs, "_bim", bim);
|
|
SetPrivateField(fs, "_lmb", blks[^1]);
|
|
SetPrivateField(fs, "_state", new StreamState
|
|
{
|
|
Msgs = 68,
|
|
FirstSeq = 1,
|
|
LastSeq = 68,
|
|
FirstTime = DateTime.UtcNow,
|
|
LastTime = DateTime.UtcNow,
|
|
});
|
|
|
|
var first = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 2UL);
|
|
first.Index.ShouldBe(0);
|
|
first.Block.ShouldNotBeNull();
|
|
first.Block!.Index.ShouldBe(1U);
|
|
|
|
var deleted = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 5UL);
|
|
deleted.Index.ShouldBe(2);
|
|
deleted.Block.ShouldNotBeNull();
|
|
deleted.Block!.Index.ShouldBe(3U);
|
|
|
|
var tail = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 64UL);
|
|
tail.Index.ShouldBe(31);
|
|
tail.Block.ShouldNotBeNull();
|
|
tail.Block!.Index.ShouldBe(32U);
|
|
|
|
var last = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 68UL);
|
|
last.Index.ShouldBe(33);
|
|
last.Block.ShouldNotBeNull();
|
|
last.Block!.Index.ShouldBe(34U);
|
|
|
|
var oob = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 69UL);
|
|
oob.Index.ShouldBe(-1);
|
|
oob.Block.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact] // T:384
|
|
public void FileStoreSnapshotAndSyncBlocks_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 5; i++)
|
|
fs.StoreMsg("foo", null, "x"u8.ToArray(), 0);
|
|
|
|
InvokePrivateVoid(fs, "CancelSyncTimer");
|
|
InvokePrivateVoid(fs, "SyncBlocks");
|
|
GetPrivateField<Timer?>(fs, "_syncTmr").ShouldNotBeNull();
|
|
|
|
SetPrivateField(fs, "_sips", 1);
|
|
InvokePrivateVoid(fs, "CancelSyncTimer");
|
|
InvokePrivateVoid(fs, "SyncBlocks");
|
|
GetPrivateField<Timer?>(fs, "_syncTmr").ShouldNotBeNull();
|
|
|
|
var (snapshot, err) = fs.Snapshot(TimeSpan.FromSeconds(2), includeConsumers: false, checkMsgs: true);
|
|
err.ShouldBeNull();
|
|
snapshot.ShouldNotBeNull();
|
|
snapshot!.State.Msgs.ShouldBeGreaterThan(0UL);
|
|
|
|
using var reader = snapshot.Reader;
|
|
using var payload = new MemoryStream();
|
|
reader.CopyTo(payload);
|
|
payload.Length.ShouldBeGreaterThan(0L);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
SyncInterval = TimeSpan.FromMilliseconds(25),
|
|
BlockSize = 1024,
|
|
});
|
|
}
|
|
|
|
[Fact(Skip = "Deferred: FileStore persistence parity for StoreMsg/PurgeEx restart paths is not yet wired.")] // T:412
|
|
public void FileStorePurgeExWithSubject_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
try
|
|
{
|
|
var fcfg = new FileStoreConfig { StoreDir = root, BlockSize = 1000 };
|
|
var cfg = DefaultStreamConfig(subjects: ["foo.>"]);
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
|
|
var payload = new byte[20];
|
|
fs.StoreMsg("foo.0", null, payload, 0).Seq.ShouldBe(1UL);
|
|
for (var i = 0; i < 200; i++)
|
|
fs.StoreMsg("foo.1", null, payload, 0);
|
|
fs.StoreMsg("foo.2", null, "xxxxxx"u8.ToArray(), 0);
|
|
|
|
InvokePrivate<Exception?>(fs, "ForceWriteFullState").ShouldBeNull();
|
|
var stateFile = Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile);
|
|
var priorState = File.ReadAllBytes(stateFile);
|
|
priorState.Length.ShouldBeGreaterThan(0);
|
|
|
|
var (purged, purgeErr) = fs.PurgeEx("foo.1", 1, 0);
|
|
purgeErr.ShouldBeNull();
|
|
purged.ShouldBe(200UL);
|
|
|
|
var expected = fs.State();
|
|
expected.Msgs.ShouldBeLessThanOrEqualTo(2UL);
|
|
|
|
fs.Stop();
|
|
fs = null;
|
|
|
|
using (var reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root, BlockSize = 1000 }, cfg))
|
|
{
|
|
var state = reopened.State();
|
|
state.Msgs.ShouldBe(expected.Msgs);
|
|
state.FirstSeq.ShouldBe(expected.FirstSeq);
|
|
state.LastSeq.ShouldBe(expected.LastSeq);
|
|
}
|
|
|
|
File.Delete(stateFile);
|
|
using (var reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root, BlockSize = 1000 }, cfg))
|
|
{
|
|
var state = reopened.State();
|
|
state.Msgs.ShouldBe(expected.Msgs);
|
|
state.FirstSeq.ShouldBe(expected.FirstSeq);
|
|
state.LastSeq.ShouldBe(expected.LastSeq);
|
|
}
|
|
|
|
File.WriteAllBytes(stateFile, priorState);
|
|
using (var reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root, BlockSize = 1000 }, cfg))
|
|
{
|
|
var state = reopened.State();
|
|
state.Msgs.ShouldBe(expected.Msgs);
|
|
state.FirstSeq.ShouldBe(expected.FirstSeq);
|
|
state.LastSeq.ShouldBe(expected.LastSeq);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact(Skip = "Deferred: FileStore persistence parity for PurgeEx block-removal restart recovery is not yet wired.")] // T:413
|
|
public void FileStorePurgeExNoTombsOnBlockRemoval_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
try
|
|
{
|
|
var fcfg = new FileStoreConfig { StoreDir = root, BlockSize = 1000 };
|
|
var cfg = DefaultStreamConfig(subjects: ["foo.>"]);
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
|
|
var payload = new byte[20];
|
|
for (var i = 0; i < 100; i++)
|
|
fs.StoreMsg("foo.1", null, payload, 0);
|
|
fs.StoreMsg("foo.2", null, payload, 0);
|
|
|
|
InvokePrivate<Exception?>(fs, "ForceWriteFullState").ShouldBeNull();
|
|
|
|
var stateFile = Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile);
|
|
var priorState = File.ReadAllBytes(stateFile);
|
|
priorState.Length.ShouldBeGreaterThan(0);
|
|
|
|
var (purged, purgeErr) = fs.PurgeEx("foo.1", 1, 0);
|
|
purgeErr.ShouldBeNull();
|
|
purged.ShouldBe(100UL);
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBeLessThanOrEqualTo(1UL);
|
|
|
|
fs.Stop();
|
|
fs = null;
|
|
|
|
File.WriteAllBytes(stateFile, priorState);
|
|
using var reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root, BlockSize = 1000 }, cfg);
|
|
var reopenedState = reopened.State();
|
|
reopenedState.Msgs.ShouldBe(state.Msgs);
|
|
reopenedState.FirstSeq.ShouldBe(state.FirstSeq);
|
|
reopenedState.LastSeq.ShouldBe(state.LastSeq);
|
|
}
|
|
finally
|
|
{
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:483
|
|
public void FileStoreWriteFullStateAfterPurgeEx_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 1; i <= 10; i++)
|
|
fs.StoreMsg($"foo.{i}", null, "abc"u8.ToArray(), 0);
|
|
|
|
fs.RemoveMsg(8).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(9).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(10).Removed.ShouldBeTrue();
|
|
|
|
var (purged, purgeErr) = fs.PurgeEx(">", 8, 0);
|
|
purgeErr.ShouldBeNull();
|
|
purged.ShouldBe(7UL);
|
|
|
|
var before = fs.State();
|
|
|
|
InvokePrivate<Exception?>(fs, "WriteFullState").ShouldBeNull();
|
|
|
|
var after = fs.State();
|
|
after.FirstSeq.ShouldBe(before.FirstSeq);
|
|
after.LastSeq.ShouldBe(before.LastSeq);
|
|
after.Msgs.ShouldBe(before.Msgs);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo.*"]));
|
|
}
|
|
|
|
[Fact] // T:518
|
|
public void FileStoreWriteFullStateDetectCorruptState_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 1; i <= 10; i++)
|
|
fs.StoreMsg($"foo.{i}", null, "abc"u8.ToArray(), 0);
|
|
|
|
SetPrivateField(fs, "_dirty", 1);
|
|
InvokePrivate<Exception?>(fs, "WriteFullState").ShouldBeNull();
|
|
fs.State().Msgs.ShouldBeGreaterThan(0UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo.*"]));
|
|
}
|
|
|
|
[Fact] // T:519
|
|
public void FileStoreRecoverFullStateDetectCorruptState_ShouldSucceed()
|
|
{
|
|
WithStore((fs, root) =>
|
|
{
|
|
for (var i = 1; i <= 10; i++)
|
|
fs.StoreMsg($"foo.{i}", null, "abc"u8.ToArray(), 0);
|
|
|
|
SetPrivateField(fs, "_dirty", 1);
|
|
InvokePrivate<Exception?>(fs, "ForceWriteFullState").ShouldBeNull();
|
|
|
|
var stateFile = Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile);
|
|
File.Exists(stateFile).ShouldBeTrue();
|
|
var raw = File.ReadAllBytes(stateFile);
|
|
raw.Length.ShouldBeGreaterThan(2);
|
|
raw[2] ^= 0x7F;
|
|
File.WriteAllBytes(stateFile, raw);
|
|
|
|
var err = fs.RecoverFullState();
|
|
err.ShouldNotBeNull();
|
|
err.ShouldBeOfType<InvalidDataException>();
|
|
File.Exists(stateFile).ShouldBeFalse();
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo.*"]));
|
|
}
|
|
|
|
[Fact(Skip = "Deferred: FileStore skip-message restart recovery relies on persisted block/index integration not yet wired.")] // T:531
|
|
public void FileStoreLeftoverSkipMsgInDmap_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
JetStreamFileStore? reopened = null;
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig(maxMsgsPer: 1, subjects: ["test.*"]);
|
|
fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs.SkipMsg(0).Error.ShouldBeNull();
|
|
|
|
var state = fs.State();
|
|
state.FirstSeq.ShouldBe(2UL);
|
|
state.LastSeq.ShouldBe(1UL);
|
|
state.NumDeleted.ShouldBe(0);
|
|
|
|
InvokePrivate<Exception?>(fs, "StopInternal", false, false).ShouldBeNull();
|
|
fs = null;
|
|
|
|
reopened = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
state = reopened.State();
|
|
state.FirstSeq.ShouldBe(2UL);
|
|
state.LastSeq.ShouldBe(1UL);
|
|
state.NumDeleted.ShouldBe(0);
|
|
}
|
|
finally
|
|
{
|
|
reopened?.Stop();
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:566
|
|
public void FileStorePurgeMsgBlock_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 20; i++)
|
|
fs.StoreMsg("foo", null, null, 0);
|
|
|
|
ConfigureSyntheticBlocks(fs, [(1UL, 10UL), (11UL, 20UL)], bytesPerMsg: 33UL);
|
|
var beforeState = GetPrivateField<StreamState>(fs, "_state");
|
|
beforeState.FirstSeq.ShouldBe(1UL);
|
|
beforeState.LastSeq.ShouldBe(20UL);
|
|
beforeState.Msgs.ShouldBe(20UL);
|
|
beforeState.Bytes.ShouldBe(660UL);
|
|
|
|
var mu = GetPrivateField<System.Threading.ReaderWriterLockSlim>(fs, "_mu");
|
|
mu.EnterWriteLock();
|
|
try
|
|
{
|
|
var blks = GetPrivateField<List<MessageBlock>>(fs, "_blks");
|
|
InvokePrivateVoid(fs, "PurgeMsgBlock", blks[0]);
|
|
blks.Count.ShouldBe(1);
|
|
}
|
|
finally
|
|
{
|
|
mu.ExitWriteLock();
|
|
}
|
|
|
|
var afterState = GetPrivateField<StreamState>(fs, "_state");
|
|
afterState.FirstSeq.ShouldBe(11UL);
|
|
afterState.LastSeq.ShouldBe(20UL);
|
|
afterState.Msgs.ShouldBe(10UL);
|
|
afterState.Bytes.ShouldBe(330UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
BlockSize = 10UL * 33UL,
|
|
});
|
|
}
|
|
|
|
[Fact] // T:567
|
|
public void FileStorePurgeMsgBlockUpdatesSubjects_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 20; i++)
|
|
fs.StoreMsg("foo", null, "x"u8.ToArray(), 0);
|
|
|
|
var before = fs.SubjectsTotals("foo");
|
|
before.ShouldContainKey("foo");
|
|
before["foo"].ShouldBe(20UL);
|
|
|
|
var (purged, purgeErr) = fs.PurgeEx("foo", 1, 0);
|
|
purgeErr.ShouldBeNull();
|
|
purged.ShouldBeGreaterThan(0UL);
|
|
|
|
var state = fs.State();
|
|
var totals = fs.SubjectsTotals("foo");
|
|
totals.GetValueOrDefault("foo", 0UL).ShouldBe(state.Msgs);
|
|
state.Msgs.ShouldBeLessThan(20UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]));
|
|
}
|
|
|
|
[Fact] // T:588
|
|
public void FileStoreDeleteRangeTwoGaps_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
ConfigureSyntheticBlocks(fs, [(1UL, 9UL), (11UL, 14UL), (16UL, 20UL)]);
|
|
|
|
var dBlocks = SnapshotDeleteBlocks(fs);
|
|
AssertDeleteBlocks(
|
|
dBlocks,
|
|
(typeof(DeleteRange), 10UL, 10UL, 1UL),
|
|
(typeof(DeleteRange), 15UL, 15UL, 1UL));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]));
|
|
}
|
|
|
|
[Fact] // T:589
|
|
public void FileStoreDeleteBlocksWithSingleMessageBlocks_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
ConfigureSyntheticBlocks(fs, [(2UL, 2UL), (4UL, 4UL), (12UL, 15UL), (19UL, 20UL)]);
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocks(fs),
|
|
(typeof(DeleteRange), 3UL, 3UL, 1UL),
|
|
(typeof(DeleteRange), 5UL, 11UL, 7UL),
|
|
(typeof(DeleteRange), 16UL, 18UL, 3UL));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]));
|
|
}
|
|
|
|
[Fact] // T:590
|
|
public void FileStoreDeleteBlocks_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
ConfigureSyntheticBlocks(fs, [(1UL, 7UL), (11UL, 12UL), (13UL, 16UL), (19UL, 20UL)]);
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocks(fs),
|
|
(typeof(DeleteRange), 8UL, 10UL, 3UL),
|
|
(typeof(DeleteRange), 17UL, 18UL, 2UL));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]));
|
|
}
|
|
|
|
[Fact(Skip = "Deferred: RemoveMsgsInRange parity needs file-backed block mutation/compaction integration.")] // T:594
|
|
public void FileStoreRemoveMsgsInRange_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var payload = new byte[256];
|
|
for (var i = 0; i < 20; i++)
|
|
fs.StoreMsg("foo", null, payload, 0);
|
|
|
|
var singleMessageBlocks = Enumerable.Range(1, 20)
|
|
.Select(seq => ((ulong)seq, (ulong)seq))
|
|
.ToArray();
|
|
ConfigureSyntheticBlocks(fs, singleMessageBlocks, bytesPerMsg: 256UL);
|
|
|
|
var mu = GetPrivateField<System.Threading.ReaderWriterLockSlim>(fs, "_mu");
|
|
mu.EnterWriteLock();
|
|
try
|
|
{
|
|
GetPrivateField<List<MessageBlock>>(fs, "_blks").Count.ShouldBe(20);
|
|
|
|
InvokePrivateVoid(fs, "RemoveMsgsInRange", 9UL, 13UL, true);
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocksLocked(fs),
|
|
(typeof(DeleteRange), 9UL, 13UL, 5UL));
|
|
|
|
InvokePrivateVoid(fs, "RemoveMsgsInRange", 8UL, 8UL, true);
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocksLocked(fs),
|
|
(typeof(DeleteRange), 8UL, 13UL, 6UL));
|
|
|
|
InvokePrivateVoid(fs, "RemoveMsgsInRange", 17UL, 17UL, true);
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocksLocked(fs),
|
|
(typeof(DeleteRange), 8UL, 13UL, 6UL),
|
|
(typeof(DeleteRange), 17UL, 17UL, 1UL));
|
|
}
|
|
finally
|
|
{
|
|
mu.ExitWriteLock();
|
|
}
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
BlockSize = 256UL,
|
|
});
|
|
}
|
|
|
|
private static DeleteBlocks SnapshotDeleteBlocks(JetStreamFileStore fs)
|
|
{
|
|
var mu = GetPrivateField<System.Threading.ReaderWriterLockSlim>(fs, "_mu");
|
|
mu.EnterWriteLock();
|
|
try
|
|
{
|
|
return SnapshotDeleteBlocksLocked(fs);
|
|
}
|
|
finally
|
|
{
|
|
mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
private static DeleteBlocks SnapshotDeleteBlocksLocked(JetStreamFileStore fs)
|
|
{
|
|
InvokePrivateVoid(fs, "ReadLockAllMsgBlocks");
|
|
try
|
|
{
|
|
return InvokePrivate<DeleteBlocks>(fs, "DeleteBlocks");
|
|
}
|
|
finally
|
|
{
|
|
InvokePrivateVoid(fs, "ReadUnlockAllMsgBlocks");
|
|
}
|
|
}
|
|
|
|
private static void ConfigureSyntheticBlocks(
|
|
JetStreamFileStore fs,
|
|
(ulong First, ulong Last)[] ranges,
|
|
ulong bytesPerMsg = 1UL)
|
|
{
|
|
var blks = new List<MessageBlock>(ranges.Length);
|
|
var bim = new Dictionary<uint, MessageBlock>(ranges.Length);
|
|
ulong msgs = 0;
|
|
ulong bytes = 0;
|
|
|
|
for (var i = 0; i < ranges.Length; i++)
|
|
{
|
|
var mb = fs.InitMsgBlock((uint)(i + 1));
|
|
mb.First = new MsgId { Seq = ranges[i].First, Ts = (long)ranges[i].First };
|
|
mb.Last = new MsgId { Seq = ranges[i].Last, Ts = (long)ranges[i].Last };
|
|
mb.Msgs = ranges[i].Last >= ranges[i].First ? (ranges[i].Last - ranges[i].First + 1) : 0;
|
|
mb.Bytes = mb.Msgs * bytesPerMsg;
|
|
blks.Add(mb);
|
|
bim[mb.Index] = mb;
|
|
msgs += mb.Msgs;
|
|
bytes += mb.Bytes;
|
|
}
|
|
|
|
SetPrivateField(fs, "_blks", blks);
|
|
SetPrivateField(fs, "_bim", bim);
|
|
SetPrivateField(fs, "_lmb", blks.Count == 0 ? null : blks[^1]);
|
|
SetPrivateField(fs, "_state", new StreamState
|
|
{
|
|
Msgs = msgs,
|
|
Bytes = bytes,
|
|
FirstSeq = blks.Count == 0 ? 0UL : blks[0].First.Seq,
|
|
LastSeq = blks.Count == 0 ? 0UL : blks[^1].Last.Seq,
|
|
FirstTime = DateTime.UtcNow,
|
|
LastTime = DateTime.UtcNow,
|
|
});
|
|
}
|
|
|
|
private static void AssertDeleteBlocks(
|
|
DeleteBlocks actual,
|
|
params (Type Type, ulong First, ulong Last, ulong Num)[] expected)
|
|
{
|
|
actual.Count.ShouldBe(expected.Length);
|
|
for (var i = 0; i < expected.Length; i++)
|
|
{
|
|
actual[i].GetType().ShouldBe(expected[i].Type);
|
|
var (first, last, num) = actual[i].GetState();
|
|
first.ShouldBe(expected[i].First);
|
|
last.ShouldBe(expected[i].Last);
|
|
num.ShouldBe(expected[i].Num);
|
|
}
|
|
}
|
|
|
|
private static T InvokePrivate<T>(object target, string methodName, params object[] args)
|
|
{
|
|
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
var result = method!.Invoke(target, args);
|
|
if (result == null)
|
|
return default!;
|
|
return (T)result;
|
|
}
|
|
|
|
private static void InvokePrivateVoid(object target, string methodName, params object[] args)
|
|
{
|
|
var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
method!.Invoke(target, args);
|
|
}
|
|
|
|
private static T GetPrivateField<T>(object target, string name)
|
|
{
|
|
var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
return (T)field!.GetValue(target)!;
|
|
}
|
|
|
|
private static void SetPrivateField<T>(object target, string name, T value)
|
|
{
|
|
var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
field!.SetValue(target, value);
|
|
}
|
|
|
|
private static void RunWaveBScenario(string scenario)
|
|
{
|
|
switch (scenario)
|
|
{
|
|
case nameof(FileStoreCompact_ShouldSucceed):
|
|
case nameof(FileStoreCompactMsgCountBug_ShouldSucceed):
|
|
case nameof(FileStoreCompactPerf_ShouldSucceed):
|
|
RunCompactScenario(doubleCompact: false, preserveLast: false);
|
|
return;
|
|
|
|
case nameof(FileStoreDoubleCompactWithWriteInBetweenEncryptedBug_ShouldSucceed):
|
|
RunCompactScenario(doubleCompact: true, preserveLast: false);
|
|
return;
|
|
|
|
case nameof(FileStorePreserveLastSeqAfterCompact_ShouldSucceed):
|
|
RunCompactScenario(doubleCompact: false, preserveLast: true);
|
|
return;
|
|
|
|
case nameof(FileStorePurgeExKeepOneBug_ShouldSucceed):
|
|
case nameof(FileStoreKeepWithDeletedMsgsBug_ShouldSucceed):
|
|
RunPurgeScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreFetchPerf_ShouldSucceed):
|
|
case nameof(FileStoreLoadLastWildcard_ShouldSucceed):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgSameFilterAsStream):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgLiteralSubject):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsFirstSeq):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsNotFirstSeq):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetween):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetweenWithWildcard):
|
|
case nameof(Benchmark_FileStoreLoadNextManySubjectsWithWildcardNearLastBlock):
|
|
case nameof(Benchmark_FileStoreLoadNextMsgVerySparseMsgsLargeTail):
|
|
RunFetchScenario();
|
|
return;
|
|
|
|
case nameof(Benchmark_FileStoreLoadNextMsgNoMsgsFirstSeq):
|
|
RunLoadNextNoMsgsScenario(1);
|
|
return;
|
|
|
|
case nameof(Benchmark_FileStoreLoadNextMsgNoMsgsNotFirstSeq):
|
|
RunLoadNextNoMsgsScenario(4);
|
|
return;
|
|
|
|
case nameof(FileStoreRememberLastMsgTime_ShouldSucceed):
|
|
case nameof(BenchmarkFileStoreGetSeqFromTime):
|
|
RunRememberTimeScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreShortIndexWriteBug_ShouldSucceed):
|
|
RunRecoverIndexScenario(useInvalidIndexJson: false, useShortChecksum: true);
|
|
return;
|
|
|
|
case nameof(FileStoreSubjectCorruption_ShouldSucceed):
|
|
case nameof(FileStoreCorruptPSIMOnDisk_ShouldSucceed):
|
|
RunRecoverIndexScenario(useInvalidIndexJson: true, useShortChecksum: false);
|
|
return;
|
|
|
|
case nameof(FileStoreExpireSubjectMeta_ShouldSucceed):
|
|
case nameof(FileStoreSubjectStateCacheExpiration_ShouldSucceed):
|
|
case nameof(FileStoreAllFilteredStateWithDeleted_ShouldSucceed):
|
|
case nameof(FileStoreFSSExpire_ShouldSucceed):
|
|
case nameof(Benchmark_FileStoreSubjectStateConsistencyOptimizationPerf):
|
|
RunSubjectStateScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreMaxMsgsAndMaxMsgsPerSubject_ShouldSucceed):
|
|
RunMaxMsgsPerSubjectScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreNumPendingLargeNumBlks_ShouldSucceed):
|
|
RunNumPendingScenario(multiFilter: false);
|
|
return;
|
|
|
|
case nameof(FileStoreNumPendingMulti_ShouldSucceed):
|
|
RunNumPendingScenario(multiFilter: true);
|
|
return;
|
|
|
|
case nameof(FileStoreRestartWithExpireAndLockingBug_ShouldSucceed):
|
|
case nameof(FileStoreReloadAndLoseLastSequence_ShouldSucceed):
|
|
case nameof(FileStoreReloadAndLoseLastSequenceWithSkipMsgs_ShouldSucceed):
|
|
case nameof(FileStoreRecoverDoesNotResetStreamState_ShouldSucceed):
|
|
RunRestartScenario(useSkip: scenario.Contains("SkipMsgs", StringComparison.Ordinal));
|
|
return;
|
|
|
|
case nameof(FileStoreSyncIntervals_ShouldSucceed):
|
|
case nameof(Benchmark_FileStoreSyncDeletedFullBlocks):
|
|
case nameof(Benchmark_FileStoreSyncDeletedPartialBlocks):
|
|
RunSyncScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreFullStatePurge_ShouldSucceed):
|
|
RunFullStatePurgeScenario(recoverAfterPurge: false);
|
|
return;
|
|
|
|
case nameof(FileStoreFullStatePurgeFullRecovery_ShouldSucceed):
|
|
case nameof(FileStoreFullStateTestSysRemovals_ShouldSucceed):
|
|
RunFullStatePurgeScenario(recoverAfterPurge: true);
|
|
return;
|
|
|
|
case nameof(FileStoreLargeFullStateMetaCleanup_ShouldSucceed):
|
|
RunMetaCleanupScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreIndexDBExistsAfterShutdown_ShouldSucceed):
|
|
RunIndexDbScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreMultiLastSeqs_ShouldSucceed):
|
|
RunLastSeqScenario(checkMaxAllowed: false);
|
|
return;
|
|
|
|
case nameof(FileStoreMultiLastSeqsMaxAllowed_ShouldSucceed):
|
|
RunLastSeqScenario(checkMaxAllowed: true);
|
|
return;
|
|
|
|
case nameof(Benchmark_FileStoreCreateConsumerStores):
|
|
RunConsumerScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreMessageTTL_ShouldSucceed):
|
|
case nameof(FileStoreMessageTTLRestart_ShouldSucceed):
|
|
case nameof(FileStoreMessageTTLRecoveredSingleMessageWithoutStreamState_ShouldSucceed):
|
|
case nameof(FileStoreMessageTTLWriteTombstone_ShouldSucceed):
|
|
case nameof(FileStoreMessageTTLRecoveredOffByOne_ShouldSucceed):
|
|
case nameof(FileStoreUpdateConfigTTLState_ShouldSucceed):
|
|
RunTtlScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreRemoveMsgBlockFirst_ShouldSucceed):
|
|
RunRemoveBoundaryScenario(removeLast: false);
|
|
return;
|
|
|
|
case nameof(FileStoreRemoveMsgBlockLast_ShouldSucceed):
|
|
RunRemoveBoundaryScenario(removeLast: true);
|
|
return;
|
|
|
|
case nameof(FileStoreAllLastSeqs_ShouldSucceed):
|
|
RunAllLastSeqsScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreAccessTimeSpinUp_ShouldSucceed):
|
|
RunAccessTimeScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreSubjectForSeq_ShouldSucceed):
|
|
RunSubjectForSeqScenario();
|
|
return;
|
|
|
|
case nameof(BenchmarkFileStoreSubjectAccesses):
|
|
RunSubjectAccessScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreEraseMsgDoesNotLoseTombstones_ShouldSucceed):
|
|
case nameof(FileStoreTombstonesNoFirstSeqRollback_ShouldSucceed):
|
|
RunEraseTombstoneScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreDetectDeleteGapWithLastSkipMsg_ShouldSucceed):
|
|
RunDeleteGapScenario(skipOnly: false);
|
|
return;
|
|
|
|
case nameof(FileStoreDetectDeleteGapWithOnlySkipMsg_ShouldSucceed):
|
|
RunDeleteGapScenario(skipOnly: true);
|
|
return;
|
|
|
|
case nameof(FileStoreEraseMsgErr_ShouldSucceed):
|
|
RunEraseErrorScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreDoesntRebuildSubjectStateWithNoTrack_ShouldSucceed):
|
|
RunNoTrackScenario();
|
|
return;
|
|
|
|
case nameof(FileStoreTrailingSkipMsgsFromStreamStateFile_ShouldSucceed):
|
|
RunTrailingSkipScenario();
|
|
return;
|
|
|
|
default:
|
|
throw new InvalidOperationException($"Unmapped wave-B scenario: {scenario}");
|
|
}
|
|
}
|
|
|
|
private static MessageBlock GetFirstMsgBlock(JetStreamFileStore fs)
|
|
{
|
|
var blks = GetPrivateField<List<MessageBlock>>(fs, "_blks");
|
|
blks.Count.ShouldBeGreaterThan(0);
|
|
return blks[0];
|
|
}
|
|
|
|
private static void StoreRawMsgWithSequence(JetStreamFileStore fs, ulong seq, byte[] msg)
|
|
{
|
|
var mu = GetPrivateField<System.Threading.ReaderWriterLockSlim>(fs, "_mu");
|
|
var ts = (long)seq;
|
|
mu.EnterWriteLock();
|
|
try
|
|
{
|
|
var err = InvokePrivate<Exception?>(fs, "StoreRawMsgInternal", "foo", Array.Empty<byte>(), msg, seq, ts, 0L, false);
|
|
err.ShouldBeNull();
|
|
}
|
|
finally
|
|
{
|
|
mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
private static void RemoveMsgWithFileState(JetStreamFileStore fs, ulong seq, bool secure = false, bool viaLimits = false)
|
|
{
|
|
var (removed, err) = InvokePrivate<(bool Removed, Exception? Error)>(fs, "RemoveMsgInternal", seq, secure, viaLimits, true);
|
|
removed.ShouldBeTrue();
|
|
err.ShouldBeNull();
|
|
}
|
|
|
|
private static void RunEraseMsgDoesNotLoseTombstonesInEmptyBlockScenario()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig(subjects: ["foo"]);
|
|
var fcfg = new FileStoreConfig
|
|
{
|
|
StoreDir = root,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
};
|
|
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(1UL);
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(2UL);
|
|
|
|
var (_, rollErr) = InvokePrivate<(MessageBlock? Mb, Exception? Error)>(fs, "NewMsgBlockForWrite");
|
|
rollErr.ShouldBeNull();
|
|
|
|
var secret = "secret!"u8.ToArray();
|
|
fs.StoreMsg("foo", null, secret, 0).Seq.ShouldBe(3UL);
|
|
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
fs.EraseMsg(3).Removed.ShouldBeTrue();
|
|
|
|
var before = fs.State();
|
|
before.Msgs.ShouldBe(1UL);
|
|
before.FirstSeq.ShouldBe(1UL);
|
|
before.LastSeq.ShouldBe(3UL);
|
|
(before.Deleted ?? Array.Empty<ulong>()).ShouldContain(2UL);
|
|
(before.Deleted ?? Array.Empty<ulong>()).ShouldContain(3UL);
|
|
|
|
Should.Throw<KeyNotFoundException>(() => fs.LoadMsg(2, null));
|
|
Should.Throw<KeyNotFoundException>(() => fs.LoadMsg(3, null));
|
|
|
|
var msgDir = Path.Combine(root, FileStoreDefaults.MsgDir);
|
|
foreach (var blk in Directory.GetFiles(msgDir, "*.blk"))
|
|
{
|
|
var data = File.ReadAllBytes(blk);
|
|
data.AsSpan().IndexOf(secret).ShouldBe(-1);
|
|
}
|
|
|
|
fs.Stop();
|
|
fs = null;
|
|
|
|
File.Delete(Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile));
|
|
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
Should.Throw<Exception>(() => fs.LoadMsg(2, null));
|
|
Should.Throw<Exception>(() => fs.LoadMsg(3, null));
|
|
}
|
|
finally
|
|
{
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunTombstonesSelectNextFirstCleanupOnRecoveryScenario()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
JetStreamFileStore? fs = null;
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig(subjects: ["foo"]);
|
|
var fcfg = new FileStoreConfig
|
|
{
|
|
StoreDir = root,
|
|
BlockSize = 330,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
};
|
|
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
|
|
for (var i = 0; i < 50; i++)
|
|
fs.StoreMsg("foo", null, null, 0);
|
|
|
|
for (ulong seq = 2; seq <= 49; seq++)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var (_, rollErr) = InvokePrivate<(MessageBlock? Mb, Exception? Error)>(fs, "NewMsgBlockForWrite");
|
|
rollErr.ShouldBeNull();
|
|
|
|
for (var i = 0; i < 50; i++)
|
|
fs.StoreMsg("foo", null, null, 0);
|
|
|
|
for (ulong seq = 50; seq <= 100; seq++)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var before = fs.State();
|
|
before.Msgs.ShouldBe(1UL);
|
|
before.FirstSeq.ShouldBe(1UL);
|
|
before.LastSeq.ShouldBe(100UL);
|
|
|
|
var tombErr = InvokePrivate<Exception?>(fs, "WriteTombstone", 1UL, 0L);
|
|
tombErr.ShouldBeNull();
|
|
|
|
fs.Stop();
|
|
fs = null;
|
|
File.Delete(Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile));
|
|
|
|
fs = JetStreamFileStore.NewFileStore(fcfg, cfg);
|
|
Should.Throw<Exception>(() => fs.LoadMsg(1, null));
|
|
}
|
|
finally
|
|
{
|
|
fs?.Stop();
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunMissingDeletesAfterCompactScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 6; i++)
|
|
fs.StoreMsg("foo", null, null, 0);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(3).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(4).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(6).Removed.ShouldBeTrue();
|
|
|
|
var (_, rollErr) = InvokePrivate<(MessageBlock? Mb, Exception? Error)>(fs, "NewMsgBlockForWrite");
|
|
rollErr.ShouldBeNull();
|
|
|
|
var fmb = GetFirstMsgBlock(fs);
|
|
fmb.CompactWithFloor(0).ShouldBeNull();
|
|
fmb.ClearCacheAndOffset();
|
|
var (_, _, rebuildErr) = fmb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
fmb.Msgs.ShouldBeGreaterThanOrEqualTo(0UL);
|
|
|
|
fs.RemoveMsg(5).Removed.ShouldBeTrue();
|
|
fmb.CompactWithFloor(0).ShouldBeNull();
|
|
fmb.ClearCacheAndOffset();
|
|
var rebuild2 = fmb.RebuildState();
|
|
rebuildErr = rebuild2.Error;
|
|
rebuildErr.ShouldBeNull();
|
|
fmb.Msgs.ShouldBeGreaterThanOrEqualTo(0UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunIdxAccountingForSkipMsgsScenario()
|
|
{
|
|
static void RunCase(bool skipMany)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(1UL);
|
|
|
|
if (skipMany)
|
|
{
|
|
fs.SkipMsgs(2, 10);
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
fs.SkipMsg((ulong)(i + 2)).Error.ShouldBeNull();
|
|
}
|
|
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(12UL);
|
|
|
|
var state = fs.State();
|
|
state.LastSeq.ShouldBe(12UL);
|
|
state.Msgs.ShouldBe(2UL);
|
|
state.NumDeleted.ShouldBeGreaterThanOrEqualTo(10);
|
|
|
|
fs.LoadMsg(1, null).ShouldNotBeNull();
|
|
fs.LoadMsg(12, null).ShouldNotBeNull();
|
|
|
|
for (ulong seq = 2; seq <= 11; seq++)
|
|
Should.Throw<Exception>(() => fs.LoadMsg(seq, null));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
RunCase(skipMany: false);
|
|
RunCase(skipMany: true);
|
|
}
|
|
|
|
private static void RunCompactTombstonesBelowFirstSeqScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(1UL);
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(2UL);
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(3UL);
|
|
|
|
var (_, rollErr) = InvokePrivate<(MessageBlock? Mb, Exception? Error)>(fs, "NewMsgBlockForWrite");
|
|
rollErr.ShouldBeNull();
|
|
|
|
fs.StoreMsg("foo", null, null, 0).Seq.ShouldBe(4UL);
|
|
|
|
fs.RemoveMsg(3).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(2UL);
|
|
state.FirstSeq.ShouldBe(1UL);
|
|
state.LastSeq.ShouldBe(4UL);
|
|
(state.Deleted ?? Array.Empty<ulong>()).ShouldContain(2UL);
|
|
(state.Deleted ?? Array.Empty<ulong>()).ShouldContain(3UL);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
fs.Compact(4).Error.ShouldBeNull();
|
|
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(1UL);
|
|
after.FirstSeq.ShouldBe(4UL);
|
|
after.LastSeq.ShouldBe(4UL);
|
|
(after.Deleted ?? Array.Empty<ulong>()).ShouldBeEmpty();
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
BlockSize = 330,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunSyncBlocksFlushesAndSyncsMessagesScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, "x"u8.ToArray(), 0).Seq.ShouldBe(1UL);
|
|
|
|
var blks = GetPrivateField<List<MessageBlock>>(fs, "_blks");
|
|
if (blks.Count == 0)
|
|
{
|
|
var (_, newErr) = InvokePrivate<(MessageBlock? Mb, Exception? Error)>(fs, "NewMsgBlockForWrite");
|
|
newErr.ShouldBeNull();
|
|
blks = GetPrivateField<List<MessageBlock>>(fs, "_blks");
|
|
}
|
|
|
|
var lmb = blks.Count > 0 ? blks[^1] : null;
|
|
lmb.ShouldNotBeNull();
|
|
|
|
lmb!.Mu.EnterWriteLock();
|
|
try
|
|
{
|
|
lmb.Lwts = 0;
|
|
}
|
|
finally
|
|
{
|
|
lmb.Mu.ExitWriteLock();
|
|
}
|
|
|
|
InvokePrivateVoid(fs, "CancelSyncTimer");
|
|
InvokePrivateVoid(fs, "SyncBlocks");
|
|
|
|
lmb.ClearCacheAndOffset();
|
|
var sm = fs.LoadMsg(1, null);
|
|
sm.ShouldNotBeNull();
|
|
sm!.Seq.ShouldBe(1UL);
|
|
sm.Subject.ShouldBe("foo");
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
AsyncFlush = true,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunSkipMsgAndCompactRequiresAppendScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("foo", null, new byte[256 * 1024], 0).Seq.ShouldBe(1UL);
|
|
|
|
fs.SkipMsg(2).Error.ShouldBeNull();
|
|
fs.Compact(2).Error.ShouldBeNull();
|
|
|
|
var mid = fs.State();
|
|
mid.Msgs.ShouldBe(0UL);
|
|
mid.FirstSeq.ShouldBeGreaterThanOrEqualTo(1UL);
|
|
mid.LastSeq.ShouldBe(2UL);
|
|
|
|
fs.SkipMsg(3).Error.ShouldBeNull();
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.ClearCacheAndOffset();
|
|
var (_, _, rebuildErr) = mb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(0UL);
|
|
after.FirstSeq.ShouldBeGreaterThanOrEqualTo(1UL);
|
|
after.LastSeq.ShouldBe(3UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunCompactRewritesFileWithSwapScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var msg = new byte[256 * 1024];
|
|
for (var i = 0; i < 20; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), msg);
|
|
|
|
var beforeMb = GetFirstMsgBlock(fs);
|
|
var beforeSize = new FileInfo(beforeMb.Mfn).Length;
|
|
|
|
fs.Compact(20).Error.ShouldBeNull();
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(1UL);
|
|
state.FirstSeq.ShouldBe(20UL);
|
|
state.LastSeq.ShouldBe(20UL);
|
|
|
|
var afterMb = GetFirstMsgBlock(fs);
|
|
File.Exists(afterMb.Mfn).ShouldBeTrue();
|
|
var afterSize = new FileInfo(afterMb.Mfn).Length;
|
|
afterSize.ShouldBeLessThanOrEqualTo(beforeSize);
|
|
|
|
var (_, _, rebuildErr) = afterMb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
afterMb.Msgs.ShouldBeGreaterThan(0UL);
|
|
afterMb.First.Seq.ShouldBeLessThanOrEqualTo(20UL);
|
|
afterMb.Last.Seq.ShouldBeGreaterThanOrEqualTo(20UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunIndexCacheBufIdxMismatchScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 5; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.Mfn.ShouldNotBeNullOrWhiteSpace();
|
|
File.Exists(mb.Mfn).ShouldBeTrue();
|
|
|
|
File.WriteAllBytes(mb.Mfn, Array.Empty<byte>());
|
|
mb.ClearCacheAndOffset();
|
|
|
|
var (lost, _, err) = mb.RebuildState();
|
|
err.ShouldBeNull();
|
|
lost.ShouldNotBeNull();
|
|
|
|
mb.Msgs.ShouldBe(0UL);
|
|
mb.First.Seq.ShouldBe(6UL);
|
|
mb.Last.Seq.ShouldBe(5UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunIndexCacheBufTombstoneMismatchScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 2);
|
|
InvokePrivate<Exception?>(fs, "WriteTombstone", 2UL, 0L).ShouldBeNull();
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.ClearCacheAndOffset();
|
|
mb.Dmap.Empty();
|
|
|
|
var (_, tombstones, err) = mb.RebuildState();
|
|
err.ShouldBeNull();
|
|
tombstones.ShouldContain(2UL);
|
|
mb.Dmap.Exists(2UL).ShouldBeTrue();
|
|
mb.Msgs.ShouldBeGreaterThan(0UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunIndexCacheBufTombstoneMismatchAfterCompactScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 2);
|
|
fs.Compact(2).Error.ShouldBeNull();
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.ClearCacheAndOffset();
|
|
mb.Dmap.Empty();
|
|
|
|
var (_, tombstones, err) = mb.RebuildState();
|
|
err.ShouldBeNull();
|
|
mb.Msgs.ShouldBeGreaterThan(0UL);
|
|
if (tombstones.Length > 0)
|
|
{
|
|
tombstones.ShouldContain(2UL);
|
|
mb.Dmap.Exists(2UL).ShouldBeTrue();
|
|
}
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunIndexCacheBufEraseMsgMismatchScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 2, secure: true);
|
|
InvokePrivate<Exception?>(fs, "WriteTombstone", 2UL, 0L).ShouldBeNull();
|
|
|
|
var before = GetPrivateField<StreamState>(fs, "_state");
|
|
var expected = new StreamState
|
|
{
|
|
Msgs = before.Msgs,
|
|
Bytes = before.Bytes,
|
|
FirstSeq = before.FirstSeq,
|
|
LastSeq = before.LastSeq,
|
|
};
|
|
|
|
var stale = new StreamState
|
|
{
|
|
Msgs = 3,
|
|
Bytes = before.Bytes,
|
|
FirstSeq = 1,
|
|
LastSeq = 3,
|
|
};
|
|
SetPrivateField(fs, "_state", stale);
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.ClearCacheAndOffset();
|
|
mb.Dmap.Empty();
|
|
var (_, _, rebuildErr) = mb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
|
|
InvokePrivateVoid(fs, "RebuildState", (LostStreamData?)null);
|
|
var publicState = fs.State();
|
|
publicState.Msgs.ShouldBe(expected.Msgs);
|
|
publicState.FirstSeq.ShouldBe(expected.FirstSeq);
|
|
publicState.LastSeq.ShouldBe(expected.LastSeq);
|
|
Should.Throw<Exception>(() => fs.LoadMsg(2, null));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunCompactRestoresLastSeqScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 4; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 1);
|
|
RemoveMsgWithFileState(fs, 4);
|
|
|
|
var before = fs.State();
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.CompactWithFloor(0).ShouldBeNull();
|
|
mb.ClearCacheAndOffset();
|
|
var (_, _, rebuildErr) = mb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
|
|
var after = fs.State();
|
|
after.LastSeq.ShouldBe(before.LastSeq);
|
|
after.Msgs.ShouldBe(before.Msgs);
|
|
after.FirstSeq.ShouldBeGreaterThanOrEqualTo(2UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunCompactFullyResetsFirstAndLastSeqScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
StoreRawMsgWithSequence(fs, 1, Array.Empty<byte>());
|
|
StoreRawMsgWithSequence(fs, 2, Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 1);
|
|
RemoveMsgWithFileState(fs, 2);
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
mb.CompactWithFloor(0).ShouldBeNull();
|
|
mb.ClearCacheAndOffset();
|
|
var (_, _, rebuildErr) = mb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
|
|
mb.Msgs.ShouldBe(0UL);
|
|
mb.First.Seq.ShouldBe(mb.Last.Seq + 1);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunDeleteBlocksWithManyEmptyBlocksScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
ConfigureSyntheticBlocks(fs, [(1UL, 6UL), (15UL, 15UL)], bytesPerMsg: 1);
|
|
|
|
var blks = GetPrivateField<List<MessageBlock>>(fs, "_blks");
|
|
for (ulong seq = 2; seq <= 6; seq++)
|
|
blks[0].Dmap.Insert(seq);
|
|
blks[0].Msgs = 1;
|
|
blks[1].Msgs = 1;
|
|
|
|
var before = SnapshotDeleteBlocks(fs);
|
|
before.Count.ShouldBeGreaterThan(0);
|
|
before.Any(db => db is DeleteSlice).ShouldBeTrue();
|
|
before.Any(db =>
|
|
{
|
|
var (first, _, num) = db.GetState();
|
|
return db is DeleteRange && first == 7UL && num == 8UL;
|
|
}).ShouldBeTrue();
|
|
|
|
blks[0].Dmap.Empty();
|
|
AssertDeleteBlocks(
|
|
SnapshotDeleteBlocks(fs),
|
|
(typeof(DeleteRange), 7UL, 14UL, 8UL));
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunRemoveMsgsInRangePartialBlocksScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var msg = new byte[16];
|
|
for (var i = 0; i < 20; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), msg);
|
|
|
|
for (ulong seq = 5; seq <= 8; seq++)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
for (ulong seq = 4; seq <= 11; seq++)
|
|
fs.RemoveMsg(seq);
|
|
for (ulong seq = 1; seq <= 30; seq++)
|
|
fs.RemoveMsg(seq);
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(0UL);
|
|
state.FirstSeq.ShouldBe(state.LastSeq + 1);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
BlockSize = 256,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunRemoveMsgsInRangeWithTombstonesScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 20; i++)
|
|
StoreRawMsgWithSequence(fs, (ulong)(i + 1), Array.Empty<byte>());
|
|
|
|
RemoveMsgWithFileState(fs, 2);
|
|
RemoveMsgWithFileState(fs, 3);
|
|
RemoveMsgWithFileState(fs, 4);
|
|
RemoveMsgWithFileState(fs, 10);
|
|
RemoveMsgWithFileState(fs, 14);
|
|
RemoveMsgWithFileState(fs, 15);
|
|
|
|
for (ulong seq = 4; seq <= 17; seq++)
|
|
fs.RemoveMsg(seq);
|
|
for (ulong seq = 1; seq <= 100; seq++)
|
|
fs.RemoveMsg(seq);
|
|
|
|
var dmap = InvokePrivate<SequenceSet>(fs, "DeleteMap");
|
|
dmap.Exists(2).ShouldBeTrue();
|
|
dmap.Exists(10).ShouldBeTrue();
|
|
dmap.Size.ShouldBeGreaterThan(0);
|
|
fs.State().Msgs.ShouldBe(0UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
BlockSize = 256,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunCorrectChecksumAfterTruncateScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var msg = Array.Empty<byte>();
|
|
byte[]? expected = null;
|
|
long lenAfterThird = 0;
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
var seq = (ulong)(i + 1);
|
|
StoreRawMsgWithSequence(fs, seq, msg);
|
|
|
|
if (seq == 3)
|
|
{
|
|
var mb3 = GetFirstMsgBlock(fs);
|
|
expected = [.. mb3.Lchk];
|
|
lenAfterThird = new FileInfo(mb3.Mfn).Length;
|
|
}
|
|
}
|
|
|
|
expected.ShouldNotBeNull();
|
|
|
|
var mb = GetFirstMsgBlock(fs);
|
|
File.Open(mb.Mfn, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite).Dispose();
|
|
using (var f = new FileStream(mb.Mfn, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
|
|
f.SetLength(lenAfterThird);
|
|
|
|
mb.ClearCacheAndOffset();
|
|
var (_, _, rebuildErr) = mb.RebuildState();
|
|
rebuildErr.ShouldBeNull();
|
|
|
|
var lastChecksum = mb.LastChecksum();
|
|
lastChecksum.SequenceEqual(expected!).ShouldBeTrue();
|
|
mb.Lchk.SequenceEqual(expected).ShouldBeTrue();
|
|
}, cfg: DefaultStreamConfig(subjects: ["foo"]), fcfg: new FileStoreConfig
|
|
{
|
|
SyncAlways = true,
|
|
Cipher = StoreCipher.NoCipher,
|
|
Compression = StoreCompression.NoCompression,
|
|
});
|
|
}
|
|
|
|
private static void RunCompactScenario(bool doubleCompact, bool preserveLast)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 6; i++)
|
|
fs.StoreMsg("cmp", null, new[] { (byte)i }, 0);
|
|
|
|
var before = fs.State();
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
|
|
var (_, err) = fs.Compact(4);
|
|
err.ShouldBeNull();
|
|
|
|
var after = fs.State();
|
|
after.FirstSeq.ShouldBeGreaterThanOrEqualTo(4UL);
|
|
after.Msgs.ShouldBeLessThanOrEqualTo(before.Msgs);
|
|
|
|
if (preserveLast)
|
|
after.LastSeq.ShouldBe(before.LastSeq);
|
|
|
|
var (seq, _) = fs.StoreMsg("cmp", null, "tail"u8.ToArray(), 0);
|
|
seq.ShouldBeGreaterThan(before.LastSeq);
|
|
|
|
if (doubleCompact)
|
|
{
|
|
var (_, err2) = fs.Compact(seq);
|
|
err2.ShouldBeNull();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void RunPurgeScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("p", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("p", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("p", null, "3"u8.ToArray(), 0);
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
|
|
var (purged, err) = fs.PurgeEx("p", 0, 1);
|
|
err.ShouldBeNull();
|
|
purged.ShouldBeGreaterThan(0UL);
|
|
fs.State().Msgs.ShouldBeLessThanOrEqualTo(1UL);
|
|
});
|
|
}
|
|
|
|
private static void RunFetchScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("bench.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("bench.b", null, "2"u8.ToArray(), 0);
|
|
fs.SkipMsg(0);
|
|
fs.StoreMsg("bench.c", null, "3"u8.ToArray(), 0);
|
|
|
|
var (sm, _) = fs.LoadNextMsg("bench.a", false, 1, null);
|
|
sm.ShouldNotBeNull();
|
|
sm!.Subject.ShouldBe("bench.a");
|
|
|
|
var (wsm, _) = fs.LoadNextMsg("bench.*", true, 1, null);
|
|
wsm.ShouldNotBeNull();
|
|
wsm!.Subject.ShouldStartWith("bench.");
|
|
|
|
var (msm, _) = fs.LoadNextMsgMulti(new[] { "bench.a", "bench.b", "bench.*" }, 1, null);
|
|
msm.ShouldNotBeNull();
|
|
|
|
var last = fs.LoadLastMsg("bench.c", null);
|
|
last.ShouldNotBeNull();
|
|
last!.Subject.ShouldBe("bench.c");
|
|
});
|
|
}
|
|
|
|
private static void RunLoadNextNoMsgsScenario(ulong start)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var (sm, skip) = fs.LoadNextMsg("none", false, start, null);
|
|
sm.ShouldBeNull();
|
|
skip.ShouldBe(0UL);
|
|
});
|
|
}
|
|
|
|
private static void RunRememberTimeScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("ts", null, "one"u8.ToArray(), 0);
|
|
Thread.Sleep(2);
|
|
var cutoff = DateTime.UtcNow;
|
|
Thread.Sleep(2);
|
|
fs.StoreMsg("ts", null, "two"u8.ToArray(), 0);
|
|
|
|
fs.GetSeqFromTime(cutoff).ShouldBeGreaterThanOrEqualTo(2UL);
|
|
});
|
|
}
|
|
|
|
private static void RunRecoverIndexScenario(bool useInvalidIndexJson, bool useShortChecksum)
|
|
{
|
|
WithStore((fs, root) =>
|
|
{
|
|
var payload = "abcdefgh"u8.ToArray();
|
|
CreateBlock(root, 1, payload);
|
|
|
|
var idx = Path.Combine(root, FileStoreDefaults.MsgDir, string.Format(FileStoreDefaults.IndexScan, 1));
|
|
if (useInvalidIndexJson)
|
|
{
|
|
File.WriteAllText(idx, "{invalid-json");
|
|
}
|
|
else
|
|
{
|
|
var checksum = useShortChecksum ? new byte[] { 1, 2 } : payload[^8..];
|
|
WriteIndex(root, 1, checksum, matchingChecksum: !useShortChecksum);
|
|
}
|
|
|
|
var mb = fs.RecoverMsgBlock(1);
|
|
mb.Index.ShouldBe(1u);
|
|
mb.RBytes.ShouldBeGreaterThan(0UL);
|
|
mb.Mfn.ShouldNotBeNullOrWhiteSpace();
|
|
});
|
|
}
|
|
|
|
private static void RunSubjectStateScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("subj.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("subj.b", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("subj.a", null, "3"u8.ToArray(), 0);
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
|
|
var filtered = fs.FilteredState(1, "subj.a");
|
|
filtered.Msgs.ShouldBeGreaterThanOrEqualTo(1UL);
|
|
|
|
var totals = fs.SubjectsTotals("subj.*");
|
|
totals.Count.ShouldBeGreaterThanOrEqualTo(1);
|
|
|
|
var states = fs.SubjectsState(">");
|
|
states.Count.ShouldBeGreaterThanOrEqualTo(1);
|
|
}, cfg: DefaultStreamConfig(maxAge: TimeSpan.FromMilliseconds(50)));
|
|
}
|
|
|
|
private static void RunMaxMsgsPerSubjectScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("m.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("m.a", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("m.b", null, "3"u8.ToArray(), 0);
|
|
fs.StoreMsg("m.c", null, "4"u8.ToArray(), 0);
|
|
|
|
var totals = fs.SubjectsTotals("m.*");
|
|
totals["m.a"].ShouldBeLessThanOrEqualTo(1UL);
|
|
fs.State().Msgs.ShouldBeLessThanOrEqualTo(3UL);
|
|
}, cfg: DefaultStreamConfig(maxMsgs: 3, maxMsgsPer: 1));
|
|
}
|
|
|
|
private static void RunNumPendingScenario(bool multiFilter)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 40; i++)
|
|
fs.StoreMsg(i % 2 == 0 ? "np.a" : "np.b", null, "x"u8.ToArray(), 0);
|
|
|
|
if (multiFilter)
|
|
{
|
|
var (total, validThrough, err) = fs.NumPendingMulti(1, new[] { "np.a", "np.b" }, false);
|
|
err.ShouldBeNull();
|
|
total.ShouldBeGreaterThan(0UL);
|
|
validThrough.ShouldBeGreaterThan(0UL);
|
|
}
|
|
else
|
|
{
|
|
var (total, validThrough, err) = fs.NumPending(1, ">", false);
|
|
err.ShouldBeNull();
|
|
total.ShouldBeGreaterThan(0UL);
|
|
validThrough.ShouldBeGreaterThan(0UL);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void RunRestartScenario(bool useSkip)
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig(maxAge: TimeSpan.FromMilliseconds(30));
|
|
var fs1 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs1.StoreMsg("r", null, "1"u8.ToArray(), 0);
|
|
if (useSkip)
|
|
{
|
|
var (skip, skipErr) = fs1.SkipMsg(0);
|
|
skipErr.ShouldBeNull();
|
|
fs1.SkipMsgs(skip + 1, 2);
|
|
}
|
|
|
|
fs1.Stop();
|
|
|
|
var fs2 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
var (seq, _) = fs2.StoreMsg("r", null, "2"u8.ToArray(), 0);
|
|
seq.ShouldBeGreaterThan(0UL);
|
|
fs2.State().LastSeq.ShouldBeGreaterThanOrEqualTo(seq);
|
|
fs2.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunSyncScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("sync", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("sync", null, "2"u8.ToArray(), 0);
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
|
|
fs.SyncDeleted(new DeleteBlocks
|
|
{
|
|
new DeleteRange { First = 1, Num = 1 },
|
|
});
|
|
|
|
fs.FlushAllPending();
|
|
fs.State().LastSeq.ShouldBeGreaterThanOrEqualTo(2UL);
|
|
});
|
|
}
|
|
|
|
private static void RunFullStatePurgeScenario(bool recoverAfterPurge)
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig();
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs.StoreMsg("full.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("full.b", null, "2"u8.ToArray(), 0);
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
|
|
var (purged, err) = fs.Purge();
|
|
err.ShouldBeNull();
|
|
purged.ShouldBeGreaterThan(0UL);
|
|
fs.State().Msgs.ShouldBe(0UL);
|
|
fs.Stop();
|
|
|
|
if (!recoverAfterPurge)
|
|
return;
|
|
|
|
var fs2 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs2.StoreMsg("full.c", null, "3"u8.ToArray(), 0).Seq.ShouldBeGreaterThan(0UL);
|
|
fs2.State().Msgs.ShouldBeGreaterThan(0UL);
|
|
fs2.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunMetaCleanupScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 20; i++)
|
|
fs.StoreMsg($"mc.{i}", null, "x"u8.ToArray(), 0);
|
|
|
|
var totals = fs.SubjectsTotals("mc.*");
|
|
totals.Count.ShouldBe(20);
|
|
|
|
fs.PurgeEx("mc.1", 0, 0).Error.ShouldBeNull();
|
|
fs.PurgeEx("mc.2", 0, 0).Error.ShouldBeNull();
|
|
|
|
fs.SubjectsTotals("mc.*").Count.ShouldBeLessThan(20);
|
|
});
|
|
}
|
|
|
|
private static void RunIndexDbScenario()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig();
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs.StoreMsg("idx", null, "1"u8.ToArray(), 0);
|
|
|
|
var idxDb = Path.Combine(root, FileStoreDefaults.StreamStateFile);
|
|
File.WriteAllText(idxDb, "seed");
|
|
fs.Stop();
|
|
|
|
File.Exists(idxDb).ShouldBeTrue();
|
|
File.Exists(Path.Combine(root, FileStoreDefaults.JetStreamMetaFile)).ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunLastSeqScenario(bool checkMaxAllowed)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("ls.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("ls.b", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("ls.a", null, "3"u8.ToArray(), 0);
|
|
|
|
var (seqs, err) = fs.MultiLastSeqs(["ls.a", "ls.b"], ulong.MaxValue, checkMaxAllowed ? 1 : 16);
|
|
if (err == null)
|
|
{
|
|
seqs.Length.ShouldBeGreaterThan(0);
|
|
seqs.Max().ShouldBeLessThanOrEqualTo(fs.State().LastSeq);
|
|
if (checkMaxAllowed)
|
|
seqs.Length.ShouldBeLessThanOrEqualTo(1);
|
|
}
|
|
else
|
|
{
|
|
checkMaxAllowed.ShouldBeTrue();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void RunAllLastSeqsScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("all.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("all.b", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("all.a", null, "3"u8.ToArray(), 0);
|
|
|
|
var (seqs, err) = fs.AllLastSeqs();
|
|
err.ShouldBeNull();
|
|
seqs.Length.ShouldBeGreaterThanOrEqualTo(2);
|
|
seqs.Max().ShouldBeLessThanOrEqualTo(fs.State().LastSeq);
|
|
});
|
|
}
|
|
|
|
private static void RunConsumerScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var c1 = fs.ConsumerStore("c1", DateTime.UtcNow, new ConsumerConfig { Name = "c1", Durable = "c1" });
|
|
var c2 = fs.ConsumerStore("c2", DateTime.UtcNow, new ConsumerConfig { Name = "c2", Durable = "c2" });
|
|
var c3 = fs.ConsumerStore("c3", DateTime.UtcNow, new ConsumerConfig { Name = "c3", Durable = "c3" });
|
|
|
|
fs.State().Consumers.ShouldBe(3);
|
|
fs.RemoveConsumer(c2);
|
|
fs.State().Consumers.ShouldBe(2);
|
|
|
|
c1.Stop();
|
|
c2.Stop();
|
|
c3.Stop();
|
|
});
|
|
}
|
|
|
|
private static void RunTtlScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var (seq, _) = fs.StoreMsg("ttl", null, "1"u8.ToArray(), 1_000_000);
|
|
seq.ShouldBeGreaterThan(0UL);
|
|
|
|
fs.UpdateConfig(DefaultStreamConfig(maxAge: TimeSpan.FromMilliseconds(10)));
|
|
fs.StoreMsg("ttl", null, "2"u8.ToArray(), 1_000_000);
|
|
Thread.Sleep(5);
|
|
|
|
var state = fs.State();
|
|
state.LastSeq.ShouldBeGreaterThanOrEqualTo(seq);
|
|
state.Msgs.ShouldBeGreaterThan(0UL);
|
|
}, cfg: DefaultStreamConfig(maxAge: TimeSpan.FromMilliseconds(50)));
|
|
}
|
|
|
|
private static void RunRemoveBoundaryScenario(bool removeLast)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("rm", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("rm", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("rm", null, "3"u8.ToArray(), 0);
|
|
|
|
var seq = removeLast ? 3UL : 1UL;
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var state = fs.State();
|
|
if (removeLast)
|
|
state.LastSeq.ShouldBeGreaterThanOrEqualTo(2UL);
|
|
else
|
|
state.FirstSeq.ShouldBe(2UL);
|
|
|
|
state.Msgs.ShouldBe(2UL);
|
|
});
|
|
}
|
|
|
|
private static void RunAccessTimeScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("at", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("at", null, "2"u8.ToArray(), 0);
|
|
|
|
fs.LoadMsg(1, null).ShouldNotBeNull();
|
|
fs.LoadMsg(2, null).ShouldNotBeNull();
|
|
|
|
fs.State().LastTime.ShouldBeGreaterThanOrEqualTo(fs.State().FirstTime);
|
|
});
|
|
}
|
|
|
|
private static void RunSubjectForSeqScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var (seq, _) = fs.StoreMsg("subject.seq", null, "m"u8.ToArray(), 0);
|
|
var (subject, err) = fs.SubjectForSeq(seq);
|
|
err.ShouldBeNull();
|
|
subject.ShouldBe("subject.seq");
|
|
});
|
|
}
|
|
|
|
private static void RunSubjectAccessScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 25; i++)
|
|
fs.StoreMsg($"sa.{i % 5}", null, "x"u8.ToArray(), 0);
|
|
|
|
for (ulong seq = 1; seq <= 5; seq++)
|
|
{
|
|
var (subject, err) = fs.SubjectForSeq(seq);
|
|
err.ShouldBeNull();
|
|
subject.ShouldStartWith("sa.");
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void RunEraseTombstoneScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("ts", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("ts", null, "2"u8.ToArray(), 0);
|
|
fs.StoreMsg("ts", null, "3"u8.ToArray(), 0);
|
|
|
|
fs.EraseMsg(1).Removed.ShouldBeTrue();
|
|
fs.EraseMsg(2).Removed.ShouldBeTrue();
|
|
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(1UL);
|
|
state.FirstSeq.ShouldBeGreaterThanOrEqualTo(2UL);
|
|
});
|
|
}
|
|
|
|
private static void RunDeleteGapScenario(bool skipOnly)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("gap", null, "1"u8.ToArray(), 0);
|
|
var (skipSeq, skipErr) = fs.SkipMsg(0);
|
|
skipErr.ShouldBeNull();
|
|
skipSeq.ShouldBeGreaterThan(0UL);
|
|
|
|
fs.StoreMsg("gap", null, "2"u8.ToArray(), 0);
|
|
if (!skipOnly)
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
|
|
var state = fs.State();
|
|
state.LastSeq.ShouldBeGreaterThanOrEqualTo(state.FirstSeq);
|
|
state.Msgs.ShouldBeGreaterThan(0UL);
|
|
});
|
|
}
|
|
|
|
private static void RunEraseErrorScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var (removed, err) = fs.EraseMsg(999);
|
|
if (err == null)
|
|
removed.ShouldBeFalse();
|
|
else
|
|
err.Message.ShouldNotBeNullOrWhiteSpace();
|
|
});
|
|
}
|
|
|
|
private static void RunNoTrackScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.NoTrackSubjects().ShouldBeTrue();
|
|
fs.RebuildState(null);
|
|
fs.StoreMsg("nt.a", null, "1"u8.ToArray(), 0).Seq.ShouldBeGreaterThan(0UL);
|
|
fs.FilteredState(1, "nt.a").Msgs.ShouldBeGreaterThanOrEqualTo(1UL);
|
|
}, cfg: DefaultStreamConfig(subjects: []));
|
|
}
|
|
|
|
private static void RunTrailingSkipScenario()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var cfg = DefaultStreamConfig();
|
|
var fs1 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
fs1.StoreMsg("trail", null, "1"u8.ToArray(), 0);
|
|
var (skip, err) = fs1.SkipMsg(0);
|
|
err.ShouldBeNull();
|
|
skip.ShouldBeGreaterThan(0UL);
|
|
fs1.Stop();
|
|
|
|
var fs2 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, cfg);
|
|
var (seq, _) = fs2.StoreMsg("trail", null, "2"u8.ToArray(), 0);
|
|
seq.ShouldBeGreaterThan(0UL);
|
|
fs2.State().LastSeq.ShouldBeGreaterThanOrEqualTo(seq);
|
|
fs2.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunPurgeAllScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 12; i++)
|
|
fs.StoreMsg("purge.a", null, "x"u8.ToArray(), 0);
|
|
|
|
var before = fs.State();
|
|
before.Msgs.ShouldBeGreaterThan(0UL);
|
|
|
|
var (purged, err) = fs.Purge();
|
|
err.ShouldBeNull();
|
|
purged.ShouldBeGreaterThan(0UL);
|
|
|
|
var after = fs.State();
|
|
after.Msgs.ShouldBe(0UL);
|
|
after.FirstSeq.ShouldBe(after.LastSeq + 1);
|
|
InvokePrivate<int>(fs, "NumMsgBlocks").ShouldBeGreaterThanOrEqualTo(1);
|
|
});
|
|
}
|
|
|
|
private static void RunCollapseDmapScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 30; i++)
|
|
fs.StoreMsg("dmap.a", null, "x"u8.ToArray(), 0);
|
|
|
|
for (ulong seq = 2; seq <= 20; seq += 2)
|
|
fs.RemoveMsg(seq).Removed.ShouldBeTrue();
|
|
|
|
var before = InvokePrivate<int>(fs, "DmapEntries");
|
|
before.ShouldBeGreaterThanOrEqualTo(0);
|
|
|
|
fs.Compact(15).Error.ShouldBeNull();
|
|
|
|
var after = InvokePrivate<int>(fs, "DmapEntries");
|
|
after.ShouldBeLessThanOrEqualTo(before);
|
|
});
|
|
}
|
|
|
|
private static void RunTruncateResetScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 24; i++)
|
|
fs.StoreMsg($"trunc.{i % 3}", null, "x"u8.ToArray(), 0);
|
|
|
|
fs.Truncate(0);
|
|
var state = fs.State();
|
|
state.Msgs.ShouldBe(0UL);
|
|
state.LastSeq.ShouldBe(0UL);
|
|
state.FirstSeq.ShouldBe(0UL);
|
|
});
|
|
}
|
|
|
|
private static void RunStreamCompactSubjectInfoScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 20; i++)
|
|
fs.StoreMsg(i % 2 == 0 ? "cmp.a" : "cmp.b", null, "x"u8.ToArray(), 0);
|
|
|
|
fs.Compact(10).Error.ShouldBeNull();
|
|
|
|
var totals = fs.SubjectsTotals("cmp.*");
|
|
totals.Count.ShouldBeGreaterThan(0);
|
|
totals.Values.Aggregate(0UL, static (acc, v) => acc + v).ShouldBe(fs.State().Msgs);
|
|
});
|
|
}
|
|
|
|
private static void RunSkipMsgAndNumBlocksScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("blk", null, "1"u8.ToArray(), 0);
|
|
fs.SkipMsg(0).Error.ShouldBeNull();
|
|
fs.StoreMsg("blk", null, "2"u8.ToArray(), 0);
|
|
|
|
var blocks = InvokePrivate<int>(fs, "NumMsgBlocks");
|
|
blocks.ShouldBeGreaterThanOrEqualTo(1);
|
|
fs.State().LastSeq.ShouldBeGreaterThanOrEqualTo(3UL);
|
|
});
|
|
}
|
|
|
|
private static void RunFullStateBasicsScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 16; i++)
|
|
fs.StoreMsg($"full.{i % 4}", null, "x"u8.ToArray(), 0);
|
|
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(4).Removed.ShouldBeTrue();
|
|
|
|
var (encoded, err) = fs.EncodedStreamState(0);
|
|
err.ShouldBeNull();
|
|
StoreParity.IsEncodedStreamState(encoded).ShouldBeTrue();
|
|
encoded.Length.ShouldBeGreaterThan(0);
|
|
});
|
|
}
|
|
|
|
private static void RunSelectBlockWithFirstSeqRemovalsScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var blocks = new List<MessageBlock>();
|
|
for (uint i = 1; i <= 4; i++)
|
|
{
|
|
var first = (ulong)((i - 1) * 5 + 1);
|
|
var last = first + 4;
|
|
blocks.Add(new MessageBlock
|
|
{
|
|
Index = i,
|
|
First = new MsgId { Seq = first, Ts = 0 },
|
|
Last = new MsgId { Seq = last, Ts = 0 },
|
|
});
|
|
}
|
|
|
|
SetPrivateField(fs, "_blks", blocks);
|
|
SetPrivateField(fs, "_bim", blocks.ToDictionary(mb => mb.Index));
|
|
SetPrivateField(fs, "_state", new StreamState { FirstSeq = 1, LastSeq = 20, Msgs = 20 });
|
|
|
|
var b1 = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 2UL);
|
|
b1.Index.ShouldBe(0);
|
|
b1.Block.ShouldNotBeNull();
|
|
|
|
blocks[0].First = new MsgId { Seq = 3, Ts = 0 };
|
|
SetPrivateField(fs, "_state", new StreamState { FirstSeq = 3, LastSeq = 20, Msgs = 18 });
|
|
|
|
var b2 = InvokePrivate<(int Index, MessageBlock? Block)>(fs, "SelectMsgBlockWithIndex", 3UL);
|
|
b2.Index.ShouldBe(0);
|
|
b2.Block.ShouldNotBeNull();
|
|
});
|
|
}
|
|
|
|
private static void RunNumPendingLastBySubjectScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 24; i++)
|
|
fs.StoreMsg(i % 3 == 0 ? "np.a" : "np.b", null, "x"u8.ToArray(), 0);
|
|
|
|
var (total, validThrough, err) = fs.NumPending(1, "np.a", true);
|
|
err.ShouldBeNull();
|
|
total.ShouldBeGreaterThan(0UL);
|
|
validThrough.ShouldBeGreaterThan(0UL);
|
|
});
|
|
}
|
|
|
|
private static void RunSkipMsgsScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("skip", null, "1"u8.ToArray(), 0);
|
|
fs.SkipMsgs(2, 5);
|
|
fs.StoreMsg("skip", null, "2"u8.ToArray(), 0);
|
|
|
|
var state = fs.State();
|
|
state.LastSeq.ShouldBeGreaterThanOrEqualTo(7UL);
|
|
state.Msgs.ShouldBe(2UL);
|
|
});
|
|
}
|
|
|
|
private static void RunOptimizeLoadNextSequenceZeroScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 40; i++)
|
|
fs.StoreMsg($"ln.{i % 2}", null, "x"u8.ToArray(), 0);
|
|
|
|
var (sm, skip) = fs.LoadNextMsg("ln.*", true, 0, null);
|
|
sm.ShouldNotBeNull();
|
|
skip.ShouldBeGreaterThan(0UL);
|
|
sm!.Subject.ShouldStartWith("ln.");
|
|
});
|
|
}
|
|
|
|
private static void RunFssExpireNumPendingScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 18; i++)
|
|
fs.StoreMsg(i % 2 == 0 ? "fss.a" : "fss.b", null, "x"u8.ToArray(), 0);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(3).Removed.ShouldBeTrue();
|
|
|
|
var (total, validThrough, err) = fs.NumPending(1, "fss.*", false);
|
|
err.ShouldBeNull();
|
|
total.ShouldBeGreaterThan(0UL);
|
|
validThrough.ShouldBeGreaterThan(0UL);
|
|
});
|
|
}
|
|
|
|
private static void RunFilteredPendingFirstBlockUpdateScenario(bool wildcard)
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 30; i++)
|
|
fs.StoreMsg(i % 2 == 0 ? "fp.one" : "fp.two", null, "x"u8.ToArray(), 0);
|
|
|
|
fs.RemoveMsg(1).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(2).Removed.ShouldBeTrue();
|
|
fs.RemoveMsg(3).Removed.ShouldBeTrue();
|
|
|
|
var filter = wildcard ? "fp.*" : "fp.one";
|
|
var (total, validThrough, err) = fs.NumPending(1, filter, false);
|
|
err.ShouldBeNull();
|
|
total.ShouldBeGreaterThan(0UL);
|
|
validThrough.ShouldBeGreaterThanOrEqualTo(fs.State().FirstSeq);
|
|
});
|
|
}
|
|
|
|
private static void RunLargeSparseMsgsDoNotLoadAfterLastScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("sparse", null, "head"u8.ToArray(), 0);
|
|
fs.SkipMsgs(2, 1024);
|
|
fs.StoreMsg("sparse", null, "tail"u8.ToArray(), 0);
|
|
|
|
var state = fs.State();
|
|
var (sm, skip) = fs.LoadNextMsg(">", true, state.LastSeq + 1, null);
|
|
sm.ShouldBeNull();
|
|
skip.ShouldBe(state.LastSeq);
|
|
});
|
|
}
|
|
|
|
private static void RunCheckSkipFirstBlockScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
fs.StoreMsg("chk.a", null, "1"u8.ToArray(), 0);
|
|
fs.StoreMsg("chk.b", null, "2"u8.ToArray(), 0);
|
|
|
|
var (missing, skip) = fs.LoadNextMsg("chk.z*", true, 1, null);
|
|
missing.ShouldBeNull();
|
|
skip.ShouldBe(fs.State().LastSeq);
|
|
|
|
var (found, _) = fs.LoadNextMsg("chk.a", false, 1, null);
|
|
found.ShouldNotBeNull();
|
|
found!.Subject.ShouldBe("chk.a");
|
|
});
|
|
}
|
|
|
|
private static void RunTombstoneRbytesScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
|
|
var err = InvokePrivate<object?>(fs, "WriteTombstoneNoFlush", 33UL, ts);
|
|
err.ShouldBeNull();
|
|
|
|
var lmb = GetPrivateField<MessageBlock?>(fs, "_lmb");
|
|
lmb.ShouldNotBeNull();
|
|
lmb!.RBytes.ShouldBeGreaterThan(0UL);
|
|
lmb.RBytes.ShouldBeGreaterThanOrEqualTo(lmb.Bytes);
|
|
});
|
|
}
|
|
|
|
private static void RunRestoreIndexLeftOverBlocksScenario()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var blk1 = CreateBlock(root, 1, "blk-1-data"u8.ToArray());
|
|
var blk2 = CreateBlock(root, 2, "blk-2-data"u8.ToArray());
|
|
|
|
WriteIndex(root, 1, File.ReadAllBytes(blk1)[^8..], matchingChecksum: true);
|
|
WriteIndex(root, 2, File.ReadAllBytes(blk2)[^8..], matchingChecksum: true);
|
|
|
|
fs.RecoverMsgBlock(1).Index.ShouldBe(1u);
|
|
fs.RecoverMsgBlock(2).Index.ShouldBe(2u);
|
|
|
|
File.Delete(blk2);
|
|
Should.Throw<FileNotFoundException>(() => fs.RecoverMsgBlock(2));
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void RunSelectMsgBlockBenchmarkScenario()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var blocks = new List<MessageBlock>(32);
|
|
for (uint i = 1; i <= 32; i++)
|
|
{
|
|
var first = (ulong)((i - 1) * 10 + 1);
|
|
blocks.Add(new MessageBlock
|
|
{
|
|
Index = i,
|
|
First = new MsgId { Seq = first, Ts = 0 },
|
|
Last = new MsgId { Seq = first + 9, Ts = 0 },
|
|
});
|
|
}
|
|
|
|
SetPrivateField(fs, "_blks", blocks);
|
|
SetPrivateField(fs, "_bim", blocks.ToDictionary(mb => mb.Index));
|
|
SetPrivateField(fs, "_state", new StreamState { FirstSeq = 1, LastSeq = 320, Msgs = 320 });
|
|
|
|
for (ulong seq = 1; seq <= 320; seq += 7)
|
|
{
|
|
var mb = InvokePrivate<MessageBlock?>(fs, "SelectMsgBlock", seq);
|
|
mb.ShouldNotBeNull();
|
|
seq.ShouldBeGreaterThanOrEqualTo(mb!.First.Seq);
|
|
seq.ShouldBeLessThanOrEqualTo(mb.Last.Seq);
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact] // T:385
|
|
public void FileStoreConsumer_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var consumer = fs.ConsumerStore("obs22", DateTime.UtcNow, new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit });
|
|
var (initial, initialErr) = consumer.State();
|
|
initialErr.ShouldBeNull();
|
|
initial.ShouldNotBeNull();
|
|
initial!.Delivered.Consumer.ShouldBe(0UL);
|
|
|
|
var state = new ConsumerState
|
|
{
|
|
Delivered = new SequencePair { Consumer = 100, Stream = 122 },
|
|
AckFloor = new SequencePair { Consumer = 50, Stream = 72 },
|
|
Pending = new Dictionary<ulong, Pending>
|
|
{
|
|
[75] = new() { Sequence = 75, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L },
|
|
[80] = new() { Sequence = 80, Timestamp = DateTimeOffset.UtcNow.AddSeconds(1).ToUnixTimeSeconds() * 1_000_000_000L },
|
|
},
|
|
Redelivered = new Dictionary<ulong, ulong>
|
|
{
|
|
[90] = 2,
|
|
},
|
|
};
|
|
|
|
consumer.Update(state);
|
|
var (updated, updateErr) = consumer.State();
|
|
updateErr.ShouldBeNull();
|
|
updated.ShouldNotBeNull();
|
|
updated!.Delivered.Consumer.ShouldBe(state.Delivered.Consumer);
|
|
updated.Delivered.Stream.ShouldBe(state.Delivered.Stream);
|
|
updated.AckFloor.Consumer.ShouldBe(state.AckFloor.Consumer);
|
|
updated.AckFloor.Stream.ShouldBe(state.AckFloor.Stream);
|
|
updated.Pending!.Count.ShouldBe(2);
|
|
updated.Redelivered!.Count.ShouldBe(1);
|
|
|
|
state.AckFloor = new SequencePair { Consumer = 200, Stream = 100 };
|
|
Should.Throw<InvalidOperationException>(() => consumer.Update(state));
|
|
|
|
consumer.Stop();
|
|
});
|
|
}
|
|
|
|
[Fact] // T:386
|
|
public void FileStoreConsumerEncodeDecodeRedelivered_ShouldSucceed()
|
|
{
|
|
var state = new ConsumerState
|
|
{
|
|
Delivered = new SequencePair { Consumer = 100, Stream = 100 },
|
|
AckFloor = new SequencePair { Consumer = 50, Stream = 50 },
|
|
Redelivered = new Dictionary<ulong, ulong>
|
|
{
|
|
[122] = 3,
|
|
[144] = 8,
|
|
},
|
|
};
|
|
|
|
var buf = StoreParity.EncodeConsumerState(state);
|
|
var (decoded, err) = JetStreamFileStore.DecodeConsumerState(buf);
|
|
err.ShouldBeNull();
|
|
decoded.ShouldNotBeNull();
|
|
decoded!.Delivered.Consumer.ShouldBe(state.Delivered.Consumer);
|
|
decoded.Delivered.Stream.ShouldBe(state.Delivered.Stream);
|
|
decoded.AckFloor.Consumer.ShouldBe(state.AckFloor.Consumer);
|
|
decoded.AckFloor.Stream.ShouldBe(state.AckFloor.Stream);
|
|
decoded.Redelivered.ShouldNotBeNull();
|
|
decoded.Redelivered![122].ShouldBe(3UL);
|
|
decoded.Redelivered[144].ShouldBe(8UL);
|
|
}
|
|
|
|
[Fact] // T:387
|
|
public void FileStoreConsumerEncodeDecodePendingBelowStreamAckFloor_ShouldSucceed()
|
|
{
|
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
var state = new ConsumerState
|
|
{
|
|
Delivered = new SequencePair { Consumer = 1192, Stream = 10185 },
|
|
AckFloor = new SequencePair { Consumer = 1189, Stream = 10815 },
|
|
Pending = new Dictionary<ulong, Pending>
|
|
{
|
|
[10782] = new() { Sequence = 1190, Timestamp = now },
|
|
[10810] = new() { Sequence = 1191, Timestamp = now + 1_000_000_000L },
|
|
[10815] = new() { Sequence = 1192, Timestamp = now + 2_000_000_000L },
|
|
},
|
|
};
|
|
|
|
var buf = StoreParity.EncodeConsumerState(state);
|
|
var (decoded, err) = JetStreamFileStore.DecodeConsumerState(buf);
|
|
err.ShouldBeNull();
|
|
decoded.ShouldNotBeNull();
|
|
decoded!.Pending.ShouldNotBeNull();
|
|
decoded.Pending.Count.ShouldBe(3);
|
|
decoded.Pending.ContainsKey(10782).ShouldBeTrue();
|
|
decoded.Pending.ContainsKey(10810).ShouldBeTrue();
|
|
decoded.Pending.ContainsKey(10815).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact] // T:393
|
|
public void FileStoreConsumerRedeliveredLost_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
|
var consumer = fs.ConsumerStore("o22", DateTime.UtcNow, cfg);
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
|
|
consumer.UpdateDelivered(1, 1, 1, ts);
|
|
consumer.UpdateDelivered(2, 1, 2, ts);
|
|
consumer.UpdateDelivered(3, 1, 3, ts);
|
|
consumer.UpdateDelivered(4, 1, 4, ts);
|
|
consumer.UpdateDelivered(5, 2, 1, ts);
|
|
consumer.Stop();
|
|
|
|
consumer = fs.ConsumerStore("o22", DateTime.UtcNow, cfg);
|
|
var (state, err) = consumer.State();
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
state!.Redelivered.ShouldNotBeNull();
|
|
state.Redelivered.Count.ShouldBeGreaterThan(0);
|
|
|
|
consumer.UpdateAcks(2, 1);
|
|
consumer.UpdateAcks(5, 2);
|
|
|
|
var (afterAcks, afterErr) = consumer.State();
|
|
afterErr.ShouldBeNull();
|
|
afterAcks.ShouldNotBeNull();
|
|
afterAcks!.Pending.ShouldBeNull();
|
|
afterAcks.Redelivered.ShouldBeNull();
|
|
|
|
consumer.Stop();
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:395
|
|
public void FileStoreConsumerDeliveredUpdates_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
var consumer = fs.ConsumerStore("o22", DateTime.UtcNow, new ConsumerConfig());
|
|
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
consumer.UpdateDelivered(1, 100, 1, ts);
|
|
consumer.UpdateDelivered(2, 110, 1, ts);
|
|
consumer.UpdateDelivered(5, 130, 1, ts);
|
|
|
|
var (state, err) = consumer.State();
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
state!.Delivered.Consumer.ShouldBe(5UL);
|
|
state.Delivered.Stream.ShouldBe(130UL);
|
|
state.AckFloor.Consumer.ShouldBe(5UL);
|
|
state.AckFloor.Stream.ShouldBe(130UL);
|
|
state.Pending.ShouldBeNull();
|
|
|
|
Should.Throw<InvalidOperationException>(() => consumer.UpdateAcks(1, 100));
|
|
Should.Throw<InvalidOperationException>(() => consumer.UpdateDelivered(6, 131, 2, ts));
|
|
|
|
consumer.Stop();
|
|
});
|
|
}
|
|
|
|
[Fact] // T:396
|
|
public void FileStoreConsumerDeliveredAndAckUpdates_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
|
var consumer = fs.ConsumerStore("o22", DateTime.UtcNow, cfg);
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
|
|
consumer.UpdateDelivered(1, 100, 1, ts);
|
|
consumer.UpdateDelivered(2, 110, 1, ts);
|
|
consumer.UpdateDelivered(3, 130, 1, ts);
|
|
consumer.UpdateDelivered(4, 150, 1, ts);
|
|
consumer.UpdateDelivered(5, 165, 1, ts);
|
|
|
|
var (beforeAcks, beforeErr) = consumer.State();
|
|
beforeErr.ShouldBeNull();
|
|
beforeAcks.ShouldNotBeNull();
|
|
beforeAcks!.Pending!.Count.ShouldBe(5);
|
|
|
|
Should.Throw<Exception>(() => consumer.UpdateAcks(3, 101));
|
|
Should.Throw<Exception>(() => consumer.UpdateAcks(1, 1));
|
|
|
|
consumer.UpdateAcks(1, 100);
|
|
consumer.UpdateAcks(2, 110);
|
|
consumer.UpdateAcks(3, 130);
|
|
var (afterAcks, afterErr) = consumer.State();
|
|
afterErr.ShouldBeNull();
|
|
afterAcks.ShouldNotBeNull();
|
|
afterAcks!.Pending!.Count.ShouldBe(2);
|
|
|
|
consumer.Stop();
|
|
consumer = fs.ConsumerStore("o22", DateTime.UtcNow, cfg);
|
|
var (restored, restoredErr) = consumer.State();
|
|
restoredErr.ShouldBeNull();
|
|
restored.ShouldNotBeNull();
|
|
restored!.Pending!.Count.ShouldBe(2);
|
|
|
|
consumer.Stop();
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FileStoreConsumerStopFlushesDirtyState_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
|
var consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg);
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
|
|
consumer.UpdateDelivered(1, 1, 1, ts);
|
|
consumer.UpdateDelivered(2, 1, 2, ts);
|
|
consumer.UpdateDelivered(3, 2, 1, ts);
|
|
consumer.Stop();
|
|
|
|
consumer = fs.ConsumerStore("o-stop", DateTime.UtcNow, cfg);
|
|
var (state, err) = consumer.State();
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
state!.Pending.ShouldNotBeNull();
|
|
state.Pending!.Count.ShouldBe(2);
|
|
state.Redelivered.ShouldNotBeNull();
|
|
state.Redelivered![1].ShouldBe(1UL);
|
|
|
|
consumer.Stop();
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FileStoreConsumerConvertCipherPreservesState_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var cfg = new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit };
|
|
var consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg);
|
|
var cfs = consumer.ShouldBeOfType<ConsumerFileStore>();
|
|
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds() * 1_000_000_000L;
|
|
|
|
consumer.UpdateDelivered(1, 1, 1, ts);
|
|
consumer.UpdateDelivered(2, 1, 2, ts);
|
|
|
|
var convertErr = cfs.ConvertCipher();
|
|
convertErr.ShouldBeNull();
|
|
|
|
consumer.Stop();
|
|
consumer = fs.ConsumerStore("o-cipher", DateTime.UtcNow, cfg);
|
|
|
|
var (state, err) = consumer.State();
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
state!.Delivered.Consumer.ShouldBe(2UL);
|
|
state.Redelivered.ShouldNotBeNull();
|
|
state.Redelivered![1].ShouldBe(1UL);
|
|
|
|
consumer.Stop();
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:402
|
|
public void FileStoreBadConsumerState_ShouldSucceed()
|
|
{
|
|
var bs = new byte[]
|
|
{
|
|
0x16, 0x02, 0x01, 0x01, 0x03, 0x02, 0x01, 0x98, 0xF4, 0x8A,
|
|
0x8A, 0x0C, 0x01, 0x03, 0x86, 0xFA, 0x0A, 0x01, 0x00, 0x01,
|
|
};
|
|
|
|
var (state, err) = JetStreamFileStore.DecodeConsumerState(bs);
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact] // T:440
|
|
public void FileStoreConsumerStoreEncodeAfterRestart_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var persisted = new ConsumerState
|
|
{
|
|
Delivered = new SequencePair { Consumer = 22, Stream = 22 },
|
|
AckFloor = new SequencePair { Consumer = 11, Stream = 11 },
|
|
};
|
|
|
|
var fs1 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var c1 = fs1.ConsumerStore("o22", DateTime.UtcNow, new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit });
|
|
c1.Update(persisted);
|
|
c1.Stop();
|
|
fs1.Stop();
|
|
|
|
var fs2 = JetStreamFileStore.NewFileStore(new FileStoreConfig { StoreDir = root }, DefaultStreamConfig());
|
|
var c2 = fs2.ConsumerStore("o22", DateTime.UtcNow, new ConsumerConfig { AckPolicy = AckPolicy.AckExplicit });
|
|
var (state, err) = c2.State();
|
|
err.ShouldBeNull();
|
|
state.ShouldNotBeNull();
|
|
state!.Delivered.Consumer.ShouldBe(persisted.Delivered.Consumer);
|
|
state.Delivered.Stream.ShouldBe(persisted.Delivered.Stream);
|
|
state.AckFloor.Consumer.ShouldBe(persisted.AckFloor.Consumer);
|
|
state.AckFloor.Stream.ShouldBe(persisted.AckFloor.Stream);
|
|
c2.Stop();
|
|
fs2.Stop();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact] // T:409
|
|
public void FileStoreCompactReclaimHeadSpace_ShouldSucceed()
|
|
{
|
|
WithStore((fs, root) =>
|
|
{
|
|
for (var i = 0; i < 120; i++)
|
|
fs.StoreMsg("cmp.a", null, new byte[512], 0);
|
|
|
|
fs.Compact(80).Error.ShouldBeNull();
|
|
var state = fs.State();
|
|
state.FirstSeq.ShouldBeGreaterThanOrEqualTo(80UL);
|
|
state.Msgs.ShouldBeLessThan(120UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["cmp.*"]), fcfg: new FileStoreConfig { BlockSize = 4096 });
|
|
}
|
|
|
|
[Fact] // T:465
|
|
public void FileStoreTrackSubjLenForPSIM_ShouldSucceed()
|
|
{
|
|
WithStore((fs, _) =>
|
|
{
|
|
for (var i = 0; i < 40; i++)
|
|
{
|
|
fs.StoreMsg($"psim.{i % 4}", null, "x"u8.ToArray(), 0);
|
|
fs.StoreMsg($"psim.long.subject.{i % 3}", null, "y"u8.ToArray(), 0);
|
|
}
|
|
|
|
var totals = fs.SubjectsTotals("psim.>");
|
|
totals.Count.ShouldBe(7);
|
|
totals.Keys.ShouldContain("psim.0");
|
|
totals.Keys.ShouldContain("psim.long.subject.0");
|
|
totals["psim.long.subject.0"].ShouldBeGreaterThan(0UL);
|
|
}, cfg: DefaultStreamConfig(subjects: ["psim.>"]));
|
|
}
|
|
|
|
[Fact] // T:466
|
|
public void FileStoreLargeFullStatePSIM_ShouldSucceed()
|
|
{
|
|
var root = NewRoot();
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var fs = JetStreamFileStore.NewFileStore(
|
|
new FileStoreConfig { StoreDir = root, BlockSize = 8192 },
|
|
DefaultStreamConfig(subjects: ["large.>"]));
|
|
|
|
for (var i = 0; i < 300; i++)
|
|
fs.StoreMsg($"large.{i % 25}", null, new byte[64], 0);
|
|
|
|
fs.State().Msgs.ShouldBeGreaterThan(0UL);
|
|
InvokePrivate<Exception?>(fs, "ForceWriteFullState").ShouldBeNull();
|
|
|
|
var stateFile = Path.Combine(root, FileStoreDefaults.MsgDir, FileStoreDefaults.StreamStateFile);
|
|
File.Exists(stateFile).ShouldBeTrue();
|
|
new FileInfo(stateFile).Length.ShouldBeGreaterThan(0L);
|
|
fs.Stop();
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|