├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── build.yml │ ├── buildandtest.yml │ ├── release_stable.yml │ └── release_unstable.yml ├── .gitignore ├── Directory.Build.props ├── FasterKv.Cache.sln ├── README.md ├── benchmark └── FasterKvCache.Benchmark │ ├── FasterKvCache.Benchmark.csproj │ └── Program.cs ├── docs └── assets │ └── arch.png ├── sample └── FasterKvCache.Sample.ConsoleApp │ ├── FasterKvCache.Sample.ConsoleApp.csproj │ ├── ObjectFasterKvCache.cs │ ├── Program.cs │ └── TFasterKvCache.cs ├── src ├── FasterKv.Cache.Core │ ├── Abstractions │ │ ├── ClientSessionWrap.cs │ │ ├── IFasterKvCacheExtensionOptions.cs │ │ ├── IFasterKvCacheSerializer.cs │ │ ├── ISystemClock.cs │ │ ├── StoreFunctions.cs │ │ └── ValueWrapper.cs │ ├── Configurations │ │ ├── FasterKvCacheOptions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── FasterKv.Cache.Core.csproj │ ├── FasterKvCache.cs │ ├── FasterKvStore.TValue.cs │ ├── Guards.cs │ └── Serializers │ │ ├── FasterKvSerializer.TValue.cs │ │ ├── FasterKvSerializer.cs │ │ └── StringSerializer.cs ├── FasterKv.Cache.MessagePack │ ├── FasterKv.Cache.MessagePack.csproj │ ├── FasterKvCacheOptionsExtensions.cs │ ├── MessagePackFasterKvCacheSerializer.cs │ └── MessagePackFasterKvCacheSerializerExtensionOptions.cs └── FasterKv.Cache.SystemTextJson │ ├── FasterKv.Cache.SystemTextJson.csproj │ ├── FasterKvCacheOptionsExtensions.cs │ ├── SystemTextJsonFasterKvCacheSerializer.cs │ └── SystemTextJsonFasterKvCacheSerializerExtensionOptions.cs └── tests └── FasterKv.Cache.Core.Tests ├── DependencyInjection └── FasterKvCacheDITest.cs ├── FasterKv.Cache.Core.Tests.csproj ├── KvStore ├── DeleteFileOnClose │ ├── DeleteOnCloseTest.cs │ └── DeleteOnCloseTestObject.cs ├── FasterKvStoreObjectTest.GetOrAdd.cs ├── FasterKvStoreObjectTest.cs ├── FasterKvStoreTest.Expiry.cs ├── FasterKvStoreTest.GetOrAdd.cs └── FasterKvStoreTest.cs ├── MockSystemClock.cs ├── Serializers ├── FasterKvSerializer.Deserialize.Tests.cs ├── FasterKvSerializer.Serialize.Tests.cs ├── FasterKvSerializer.TValue.Deserialize.Tests.cs ├── FasterKvSerializer.TValue.Serialize.Tests.cs └── MessagePackTests.cs └── Usings.cs /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | [Description of the bug or feature] 4 | 5 | ### Steps to Reproduce 6 | 7 | 1. 8 | 2. 9 | 10 | ### Related code 11 | 12 | ``` 13 | insert short code snippets here 14 | ``` 15 | 16 | **Expected behavior:** [What you expected to happen] 17 | 18 | **Actual behavior:** [What actually happened] 19 | 20 | 21 | 22 | ## Specifications 23 | 24 | - .NET Version : 6.0.0 25 | - System : CentOS 7.2 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ dev, main, master, '**' ] 6 | pull_request: 7 | branches: [ dev, main, master ] 8 | 9 | jobs: 10 | 11 | windows: 12 | name: build on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ windows-latest ] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET SDK 6.0.x and 7.0.x 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: | 24 | 7.0.x 25 | 6.0.x 26 | 27 | - name: Show dotnet Version 28 | run: | 29 | dotnet --list-sdks 30 | dotnet --list-runtimes 31 | 32 | - name: Build with dotnet 33 | run: | 34 | dotnet build --configuration Release D:\a\FasterKvCache\FasterKvCache\FasterKv.Cache.sln -------------------------------------------------------------------------------- /.github/workflows/buildandtest.yml: -------------------------------------------------------------------------------- 1 | name: Build&Test 2 | 3 | on: 4 | push: 5 | branches: [ dev, main, master, '**' ] 6 | pull_request: 7 | branches: [ dev, main, master ] 8 | 9 | jobs: 10 | 11 | linux: 12 | name: build and test on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET SDK 6.0.x and 7.0.x 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: | 24 | 7.0.x 25 | 6.0.x 26 | 27 | - name: Show dotnet Version 28 | run: | 29 | dotnet --list-sdks 30 | dotnet --list-runtimes 31 | 32 | - name: Show docker info 33 | run: | 34 | docker ps -a 35 | 36 | - name: Build with dotnet 37 | run: | 38 | dotnet build --configuration Release /home/runner/work/FasterKvCache/FasterKvCache/FasterKv.Cache.sln 39 | 40 | - name: Run tests on net7.0 41 | run: | 42 | dotnet test --framework=net7.0 /home/runner/work/FasterKvCache/FasterKvCache/tests/FasterKv.Cache.Core.Tests/FasterKv.Cache.Core.Tests.csproj 43 | -------------------------------------------------------------------------------- /.github/workflows/release_stable.yml: -------------------------------------------------------------------------------- 1 | name: Release_Stable 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*-beta*" 7 | - "*.*.*-rc*" 8 | 9 | jobs: 10 | build_artifact: 11 | name: Build and upload artifact 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Setup .NET SDK 6.0.x and 7.0.x 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: | 20 | 7.0.x 21 | 6.0.x 22 | - name: Build with dotnet 23 | run: dotnet build --configuration Release /home/runner/work/FasterKvCache/FasterKvCache/FasterKv.Cache.sln 24 | - name: Pack with dotnet 25 | env: 26 | VERSION: ${{ github.ref_name }} 27 | run: dotnet pack /home/runner/work/FasterKvCache/FasterKvCache/FasterKv.Cache.sln --version-suffix $VERSION -o /home/runner/work/nugetpkgs -c Release --no-build 28 | - name: Upload artifact 29 | uses: actions/upload-artifact@v1 30 | with: 31 | name: nugetpkgs 32 | path: /home/runner/work/nugetpkgs 33 | 34 | release_nuget: 35 | name: Release to Nuget 36 | needs: build_artifact 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - name: Download build artifacts 41 | uses: actions/download-artifact@v1 42 | with: 43 | name: nugetpkgs 44 | - name: list nugetpkgs 45 | run: ls nugetpkgs 46 | - name: Release 47 | run: | 48 | for file in nugetpkgs/*.nupkg 49 | do 50 | dotnet nuget push $file -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate -s https://www.nuget.org/api/v2/package 51 | done 52 | -------------------------------------------------------------------------------- /.github/workflows/release_unstable.yml: -------------------------------------------------------------------------------- 1 | name: Release_Unstable 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | - "!*.*.*-beta*" 8 | - "!*.*.*-rc*" 9 | 10 | jobs: 11 | build_artifact: 12 | name: Build and upload artifact 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Setup .NET SDK 6.0.x and 7.0.x 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: | 21 | 7.0.x 22 | 6.0.x 23 | - name: Build with dotnet 24 | run: dotnet build --configuration Release /home/runner/work/FasterKvCache/FasterKvCache/FasterKv.Cache.sln 25 | - name: Pack with dotnet 26 | env: 27 | VERSION: ${{ github.ref_name }} 28 | run: dotnet pack /home/runner/work/FasterKvCache/FasterKvCache/FasterKv.Cache.sln --version-suffix $VERSION -o /home/runner/work/nugetpkgs -c Release --no-build 29 | - name: Upload artifact 30 | uses: actions/upload-artifact@v1 31 | with: 32 | name: nugetpkgs 33 | path: /home/runner/work/nugetpkgs 34 | 35 | release_nuget: 36 | name: Release to Nuget 37 | needs: build_artifact 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: Download build artifacts 42 | uses: actions/download-artifact@v1 43 | with: 44 | name: nugetpkgs 45 | - name: list nugetpkgs 46 | run: ls nugetpkgs 47 | - name: Release 48 | run: | 49 | for file in nugetpkgs/*.nupkg 50 | do 51 | dotnet nuget push $file -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate -s https://www.nuget.org/api/v2/package 52 | done -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | /.idea 352 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | 11 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /FasterKv.Cache.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKv.Cache.Core", "src\FasterKv.Cache.Core\FasterKv.Cache.Core.csproj", "{FFDB364D-A31F-44EB-AAED-2823F39E4D48}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{390A098B-4FD2-419A-A2F6-77B6D1B19BFB}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKv.Cache.Core.Tests", "tests\FasterKv.Cache.Core.Tests\FasterKv.Cache.Core.Tests.csproj", "{72445807-EB50-41D1-BEFE-1805B4AD0408}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FFC77C65-4B97-4712-9FA6-65055A0E3CE2}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKv.Cache.MessagePack", "src\FasterKv.Cache.MessagePack\FasterKv.Cache.MessagePack.csproj", "{B04D6116-8FE3-439B-B95A-8617A7777558}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKv.Cache.SystemTextJson", "src\FasterKv.Cache.SystemTextJson\FasterKv.Cache.SystemTextJson.csproj", "{F5940765-9AE9-44E9-8847-AE1693E6C92B}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Items", "Items", "{E26D3646-0BC7-43AA-97D0-FC51693BFBFD}" 16 | ProjectSection(SolutionItems) = preProject 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{A63977CD-B7C0-4963-857D-2DFCA8A28110}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKvCache.Sample.ConsoleApp", "sample\FasterKvCache.Sample.ConsoleApp\FasterKvCache.Sample.ConsoleApp.csproj", "{5D6CFE59-B3FB-418B-A833-29861CDCFA1D}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmark", "benchmark", "{814EFFB2-4634-47EC-82AF-0BC0D03C193E}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FasterKvCache.Benchmark", "benchmark\FasterKvCache.Benchmark\FasterKvCache.Benchmark.csproj", "{488C3EBF-043B-4F49-8295-727A89607ABD}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {FFDB364D-A31F-44EB-AAED-2823F39E4D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {FFDB364D-A31F-44EB-AAED-2823F39E4D48}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {FFDB364D-A31F-44EB-AAED-2823F39E4D48}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {FFDB364D-A31F-44EB-AAED-2823F39E4D48}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {72445807-EB50-41D1-BEFE-1805B4AD0408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {72445807-EB50-41D1-BEFE-1805B4AD0408}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {72445807-EB50-41D1-BEFE-1805B4AD0408}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {72445807-EB50-41D1-BEFE-1805B4AD0408}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {B04D6116-8FE3-439B-B95A-8617A7777558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {B04D6116-8FE3-439B-B95A-8617A7777558}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {B04D6116-8FE3-439B-B95A-8617A7777558}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {B04D6116-8FE3-439B-B95A-8617A7777558}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {F5940765-9AE9-44E9-8847-AE1693E6C92B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {F5940765-9AE9-44E9-8847-AE1693E6C92B}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {F5940765-9AE9-44E9-8847-AE1693E6C92B}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {F5940765-9AE9-44E9-8847-AE1693E6C92B}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {5D6CFE59-B3FB-418B-A833-29861CDCFA1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {5D6CFE59-B3FB-418B-A833-29861CDCFA1D}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {5D6CFE59-B3FB-418B-A833-29861CDCFA1D}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {5D6CFE59-B3FB-418B-A833-29861CDCFA1D}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {488C3EBF-043B-4F49-8295-727A89607ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {488C3EBF-043B-4F49-8295-727A89607ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {488C3EBF-043B-4F49-8295-727A89607ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {488C3EBF-043B-4F49-8295-727A89607ABD}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(NestedProjects) = preSolution 60 | {FFDB364D-A31F-44EB-AAED-2823F39E4D48} = {390A098B-4FD2-419A-A2F6-77B6D1B19BFB} 61 | {72445807-EB50-41D1-BEFE-1805B4AD0408} = {FFC77C65-4B97-4712-9FA6-65055A0E3CE2} 62 | {B04D6116-8FE3-439B-B95A-8617A7777558} = {390A098B-4FD2-419A-A2F6-77B6D1B19BFB} 63 | {F5940765-9AE9-44E9-8847-AE1693E6C92B} = {390A098B-4FD2-419A-A2F6-77B6D1B19BFB} 64 | {5D6CFE59-B3FB-418B-A833-29861CDCFA1D} = {A63977CD-B7C0-4963-857D-2DFCA8A28110} 65 | {488C3EBF-043B-4F49-8295-727A89607ABD} = {814EFFB2-4634-47EC-82AF-0BC0D03C193E} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FasterKv.Cache 2 | 3 | FasterKv.Cache是一个基于微软FasterKv封装的进程内混合缓存库(内存+磁盘)。FasterKv它可以承载大于机器内存的Key-Value数据库,并且有着远超其它内存+磁盘数据库的性能。不过使用起来比较繁琐,对新人不友好,于是FasterKv.Cache在它的基础上进行了一层封装,让我们能更简单的处理缓存。 4 | 5 | ![arch](docs/assets/arch.png) 6 | 7 | | 适用场景 | 不适用场景 | 原因 | 8 | | ------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------ | 9 | | 缓存数据量大,并且有降低内存使用的需求 | 数据量小,或者有钞能力 | 如果数据量小和有钞能力,直接使用内存缓存才是性能最好的。 | 10 | | 有明显的冷、热数据,数据能存储在内存中 | 没有冷热数据,完全随机访问 | 如果完全随机访问,意味着内存缓存将无效,每次读盘损耗会比较大 | 11 | | 对于缓存没有非常严格的延时要求,几百us无所谓 | 对于缓存有高要求,不能接受波动 | 如果对于缓存有非常高的要求,几百微秒延时都不能忍受,那解决方案还是内存缓存 | 12 | | 1.没有缓存非常大的数据。 2.有非常大的数据,但是经常访问,可以利用缓存。2.非常大的数据很少访问,对延时不敏感 | 有非常大的数据,且随机访问,并且延时非常敏感 | 如果有非常大的数据缓存,比如超过内存和ReadCache大小的,那么性能会变得比较差,想要解决它,只有钞能力。 | 13 | 14 | 笔者之前给EasyCaching提交了FasterKv的实现,但是由于有一些EasyCaching的高级功能在FasterKv上无法高性能的实现,所以单独创建了这个库,提供高性能和最基本的API实现;如果大家有使用EasyCaching那么一样可以直接使用EasyCaching.FasterKv。 15 | 16 | ## NuGet 软件包 17 | 18 | | 软件包名 | 版本 | 备注 | 19 | |-----------------------------------------------------------------------------------------------|-----------|-------------------------------------------------------------------------------------------| 20 | | [FasterKv.Cache.Core](https://www.nuget.org/packages/FasterKv.Cache.Core) | 1.0.2 | 缓存核心包,包含FasterKvCache主要的API | 21 | | [FasterKv.Cache.MessagePack](https://www.nuget.org/packages/FasterKv.Cache.MessagePack) | 1.0.2 | 基于MessagePack的磁盘序列化包,它具有着非常好的性能,但是需要注意它稍微有一点使用门槛,大家可以看它的文档。 | 22 | | [FasterKv.Cache.SystemTextJson](https://www.nuget.org/packages/FasterKv.Cache.SystemTextJson) | 1.0.2 | 基于System.Text.Json的磁盘序列化包,它是.NET平台上性能最好JSON序列化封装,但是比MessagePack差。不过它易用性非常好,无需对缓存实体进行单独配置。 | 23 | 24 | ## 使用 25 | 26 | ### 直接使用 27 | 28 | 我们可以直接通过`new FasterKvCache(...)`的方式使用它,目前它只支持基本的三种操作`Get`、`Set`、`Delete`。为了方便使用和性能的考虑,我们将FasterKvCache分为两种API风格,一种是通用对象风格,一种是泛型风格。 29 | 30 | * 通用对象:直接使用`new FasterKvCache(...)`创建,可以存放任意类型的Value。它底层使用`object`类型存储,所以内存缓冲内访问值类型对象会有装箱和拆箱的开销。 31 | * 泛型:需要使用`new FasterKvCache(...)`创建,只能存放`T`类型的Value。它底层使用`T`类型存储,所以内存缓冲内不会有任何开销。 32 | 33 | 当然如果内存缓冲不够,对应的Value被淘汰到磁盘上,那么同样都会有读磁盘、序列化和反序列化开销。 34 | 35 | ### 通用对象版本 36 | 37 | 代码如下所示,同一个cache实例可以添加任意类型: 38 | 39 | ```cs 40 | using FasterKv.Cache.Core; 41 | using FasterKv.Cache.Core.Configurations; 42 | using FasterKv.Cache.MessagePack; 43 | 44 | // create a FasterKvCache 45 | var cache = new FasterKv.Cache.Core.FasterKvCache("MyCache", 46 | new DefaultSystemClock(), 47 | new FasterKvCacheOptions(), 48 | new IFasterKvCacheSerializer[] 49 | { 50 | new MessagePackFasterKvCacheSerializer 51 | { 52 | Name = "MyCache" 53 | } 54 | }, 55 | null); 56 | 57 | var key = Guid.NewGuid().ToString("N"); 58 | 59 | // sync 60 | // set key and value with expiry time 61 | cache.Set(key, "my cache sync", TimeSpan.FromMinutes(5)); 62 | 63 | // get 64 | var result = cache.Get(key); 65 | Console.WriteLine(result); 66 | 67 | // get or add 68 | result = cache.GetOrAdd(key, () => "my cache sync", TimeSpan.FromMinutes(5)); 69 | Console.WriteLine(result); 70 | 71 | // delete 72 | cache.Delete(key); 73 | 74 | // async 75 | // set 76 | await cache.SetAsync(key, "my cache async"); 77 | 78 | // get 79 | result = await cache.GetAsync(key); 80 | Console.WriteLine(result); 81 | 82 | // get or add 83 | result = await cache.GetOrAddAsync(key, () => "my cache async"); 84 | Console.WriteLine(result); 85 | 86 | // delete 87 | await cache.DeleteAsync(key); 88 | 89 | // set other type object 90 | cache.Set(key, new DateTime(2022,2,22)); 91 | Console.WriteLine(cache.Get(key)); 92 | ``` 93 | 94 | 输出结果如下所示: 95 | 96 | ```sh 97 | my cache sync 98 | my cache async 99 | 2022/2/22 0:00:00 100 | ``` 101 | 102 | ### 泛型版本 103 | 104 | 泛型版本的话性能最好,但是它只允许添加一个类型,否则代码将编译不通过: 105 | 106 | ```cs 107 | // create a FasterKvCache 108 | // only set T type value 109 | var cache = new FasterKvCache("MyTCache", 110 | new DefaultSystemClock(), 111 | new FasterKvCacheOptions(), 112 | new IFasterKvCacheSerializer[] 113 | { 114 | new MessagePackFasterKvCacheSerializer 115 | { 116 | Name = "MyTCache" 117 | } 118 | }, 119 | null); 120 | ``` 121 | 122 | ### Microsoft.Extensions.DependencyInjection 123 | 124 | 当然,我们也可以直接使用依赖注入的方式使用它,用起来也非常简单。按照通用和泛型版本的区别,我们使用不同的扩展方法即可: 125 | 126 | ```cs 127 | var services = new ServiceCollection(); 128 | // use AddFasterKvCache 129 | services.AddFasterKvCache(options => 130 | { 131 | // use MessagePack serializer 132 | options.UseMessagePackSerializer(); 133 | }, "MyKvCache"); 134 | 135 | var provider = services.BuildServiceProvider(); 136 | 137 | // get instance do something 138 | var cache = provider.GetService(); 139 | ``` 140 | 141 | 泛型版本需要调用相应的`AddFasterKvCache`方法: 142 | 143 | ```cs 144 | var services = new ServiceCollection(); 145 | // use AddFasterKvCache 146 | services.AddFasterKvCache(options => 147 | { 148 | // use MessagePack serializer 149 | options.UseMessagePackSerializer(); 150 | }, "MyKvCache"); 151 | 152 | var provider = services.BuildServiceProvider(); 153 | 154 | // get instance do something 155 | var cache = provider.GetService>(); 156 | ``` 157 | 158 | ## 配置 159 | 160 | ### FasterKvCache构造函数 161 | 162 | ```cs 163 | public FasterKvCache( 164 | string name, // 如果存在多个Cache实例,定义一个名称可以隔离序列化等配置和磁盘文件 165 | ISystemClock systemClock, // 当前系统时钟,new DefaultSystemClock()即可 166 | FasterKvCacheOptions? options, // FasterKvCache的详细配置,详情见下文 167 | IEnumerable? serializers, // 序列化器,可以直接使用MessagePack或SystemTextJson序列化器 168 | ILoggerFactory? loggerFactory) // 日志工厂 用于记录FasterKv内部的一些日志信息 169 | ``` 170 | 171 | ### FasterKvCacheOptions 配置项 172 | 173 | 对于FasterKvCache,有着和FasterKv差不多的配置项,更详细的信息大家可以看[FasterKv-Settings](https://microsoft.github.io/FASTER/docs/fasterkv-basics/#fasterkvsettings),下方是FasterKvCache的配置: 174 | 175 | * IndexCount:FasterKv会维护一个hash索引池,IndexCount就是这个索引池的hash槽数量,一个槽为64bit。需要配置为2的次方。如1024(2的10次方)、 2048(2的11次方)、65536(2的16次方) 、131072(2的17次方)。**默认槽数量为131072,占用1024kb的内存。** 176 | * MemorySizeBit: FasterKv用来保存Log的内存字节数,配置为2的次方数。**默认为24,也就是2的24次方,使用16MB内存。** 177 | * PageSizeBit:FasterKv内存页的大小,配置为2的次方数。**默认为20,也就是2的20次方,每页大小为1MB内存。** 178 | * ReadCacheMemorySizeBit:FasterKv读缓存内存字节数,配置为2的次方数,缓存内的都是热点数据,最好设置为热点数据所占用的内存数量。**默认为20,也就是2的20次方,使用16MB内存。** 179 | * ReadCachePageSizeBit:FasterKv读缓存内存页的大小,配置为2的次方数。**默认为20,也就是2的20次方,每页大小为1MB内存。** 180 | * LogPath:FasterKv日志文件的目录,默认会创建两个日志文件,一个以`.log`结尾,一个以`obj.log`结尾,分别存放日志信息和Value序列化信息,如果开启了`DeleteFileOnClose`和`TryRecoverLatest`,也会创建一个`.checkpoint`来进行故障恢复,**注意,不要让不同的FasterKvCache使用相同的日志文件,会出现不可预料异常**。**默认为`{当前目录}/FasterKvCache/{进程Id}-HLog/{实例名称}.log`**。 181 | * SerializerName:Value序列化器名称,需要安装序列化Nuget包,如果没有单独指定`Name`的情况下,可以使用`MessagePack`和`SystemTextJson`。**默认无需指定**。 182 | * PreallocateFile: 是否预分配日志文件,如果开启,那么在创建日志文件的时候会预分配指定1GB大小的文件,如果有大量数据的话,预分配能提升性能。**默认为false**。 183 | * DeleteFileOnClose: 是否在关闭的时候删除日志文件,如果开启,那么在关闭的时候会删除日志文件,如果不开启,那么会保留日志文件,下次启动的时候会继续使用。**默认为true**。 184 | * TryRecoverLatest: 是否在启动的时候尝试恢复最新的日志文件,如果开启,那么在启动的时候会尝试恢复最新的日志文件,如果不开启,那么会重新开始,如果要使它生效,需关闭`DeleteFileOnClose`。**默认为false**。 185 | * ExpiryKeyScanInterval:由于FasterKv不支持过期删除功能,所以目前的实现是会定期扫描所有的key,将过期的key删除。这里配置的就是扫描间隔。**默认为5分钟**。 186 | * CustomStore:如果您不想使用自动生成的实例,那么可以自定义的FasterKv实例。**默认为null**。 187 | 188 | 所以FasterKvCache所占用的内存数量基本就是`(IndexCount*64)+(MemorySize)+ReadCacheMemorySize`,当然如果Key的数量过多,那么还有加上`OverflowBucketCount * 64`。 189 | 190 | ## 容量规划 191 | 192 | 从上面提到的内容大家可以知道,FasterKvCache所占用的内存**字节**基本就是`(IndexCount * 64)+(MemorySize) + ReadCacheMemorySize + (OverflowBucketCount * 64)`。磁盘的话就是保存了所有的数据+对象序列化的数据,由于不同的序列化协议有不同的大小,大家可以先进行测试。 193 | 194 | 内存数据存储到FasterKv存储引擎,每个key都会额外元数据信息,存储空间占用会有一定的放大,建议在磁盘空间选择上,留有适当余量,按实际存储需求的 1.2 - 1.5倍预估。 195 | 196 | 如果使用内存存储 100GB 的数据,总的访问QPS不到2W,其中80%的数据都很少访问到。那么可以使用 【32GB内存 + 128GB磁盘】 存储,节省了近 70GB 的内存存储,内存成本可以下降50%+。 197 | 198 | ## 性能 199 | 200 | 目前作者还没有时间将FasterKvCache和其它主流的缓存库进行比对,现在只对FasterKvCache、EasyCaching.FasterKv和EasyCaching.Sqlite做的比较。下面是FasterKVCache的配置,总内存占用约为**2MB**。 201 | 202 | ```cs 203 | services.AddFasterKvCache(options => 204 | { 205 | options.IndexCount = 1024; 206 | options.MemorySizeBit = 20; 207 | options.PageSizeBit = 20; 208 | options.ReadCacheMemorySizeBit = 20; 209 | options.ReadCachePageSizeBit = 20; 210 | // use MessagePack serializer 211 | options.UseMessagePackSerializer(); 212 | }, "MyKvCache"); 213 | ``` 214 | 215 | 由于作者笔记本性能不够,使用Sqlite无法在短期内完成100W、1W个Key的性能测试,所以我们在默认设置下将数据集大小设置为1000个Key,设置50%的热点Key。进行100%读、100%写和50%读写随机比较。 216 | 217 | 可以看到无论是读、写还是混合操作FasterKvCache都有着不俗的性能,在8个线程情况下,TPS达到了**惊人的1600w/s**。 218 | 219 | | Provider | Type | ThreadNum | Mean(us) | Error(us) | StdDev(us) | Gen0 | Gen1 | Allocated | 220 | | ------------- | ------ | --------- | ---------- | ---------- | ---------- | ------- | ------ | ---------- | 221 | | fasterKvCache | Read | 8 | 59.95 | 3.854 | 2.549 | 1.5259 | 7.02 | NULL | 222 | | fasterKvCache | Write | 8 | 63.67 | 1.032 | 0.683 | 0.7935 | 3.63 | NULL | 223 | | fasterKvCache | Random | 4 | 64.42 | 1.392 | 0.921 | 1.709 | 8.38 | NULL | 224 | | fasterKvCache | Read | 4 | 64.67 | 0.628 | 0.374 | 2.5635 | 11.77 | NULL | 225 | | fasterKvCache | Random | 8 | 64.80 | 3.639 | 2.166 | 1.0986 | 5.33 | NULL | 226 | | fasterKvCache | Write | 4 | 65.57 | 3.45 | 2.053 | 0.9766 | 4.93 | NULL | 227 | | fasterKv | Read | 8 | 92.15 | 10.678 | 7.063 | 5.7373 | - | 26.42 KB | 228 | | fasterKv | Write | 4 | 99.49 | 2 | 1.046 | 10.7422 | - | 49.84 KB | 229 | | fasterKv | Write | 8 | 108.50 | 5.228 | 3.111 | 5.6152 | - | 25.93 KB | 230 | | fasterKv | Read | 4 | 109.37 | 1.476 | 0.772 | 10.9863 | - | 50.82 KB | 231 | | fasterKv | Random | 8 | 119.94 | 14.175 | 9.376 | 5.7373 | - | 26.18 KB | 232 | | fasterKv | Random | 4 | 124.31 | 6.191 | 4.095 | 10.7422 | - | 50.34 KB | 233 | | fasterKvCache | Read | 1 | 207.77 | 3.307 | 1.73 | 9.2773 | 43.48 | NULL | 234 | | fasterKvCache | Random | 1 | 208.71 | 1.832 | 0.958 | 6.3477 | 29.8 | NULL | 235 | | fasterKvCache | Write | 1 | 211.26 | 1.557 | 1.03 | 3.418 | 16.13 | NULL | 236 | | fasterKv | Write | 1 | 378.60 | 17.755 | 11.744 | 42.4805 | - | 195.8 KB | 237 | | fasterKv | Read | 1 | 404.57 | 17.477 | 11.56 | 43.457 | - | 199.7 KB | 238 | | fasterKv | Random | 1 | 441.22 | 14.107 | 9.331 | 42.9688 | - | 197.75 KB | 239 | | sqlite | Read | 8 | 7450.11 | 260.279 | 172.158 | 54.6875 | 7.8125 | 357.78 KB | 240 | | sqlite | Read | 4 | 14309.94 | 289.113 | 172.047 | 109.375 | 15.625 | 718.9 KB | 241 | | sqlite | Read | 1 | 56973.53 | 1,774.35 | 1,173.62 | 400 | 100 | 2872.18 KB | 242 | | sqlite | Random | 8 | 475535.01 | 214,015.71 | 141,558.14 | - | - | 395.15 KB | 243 | | sqlite | Random | 4 | 1023524.87 | 97,993.19 | 64,816.43 | - | - | 762.46 KB | 244 | | sqlite | Write | 8 | 1153950.84 | 48,271.47 | 28,725.58 | - | - | 433.7 KB | 245 | | sqlite | Write | 4 | 2250382.93 | 110,262.72 | 72,931.96 | - | - | 867.7 KB | 246 | | sqlite | Write | 1 | 4200783.08 | 43,941.69 | 29,064.71 | - | - | 3462.89 KB | 247 | | sqlite | Random | 1 | 5383716.10 | 195,085.96 | 129,037.28 | - | - | 2692.09 KB | 248 | 249 | ## 其它 250 | 251 | 项目目前已经用于生产环境,如果遇到BUG,请及时反馈问题,将在第一时间解决。 252 | -------------------------------------------------------------------------------- /benchmark/FasterKvCache.Benchmark/FasterKvCache.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /benchmark/FasterKvCache.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | using BenchmarkDotNet.Order; 4 | using FasterKv.Cache.Core; 5 | using FasterKv.Cache.Core.Configurations; 6 | using FasterKv.Cache.MessagePack; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | BenchmarkDotNet.Running.BenchmarkRunner.Run(); 10 | 11 | public enum TestType 12 | { 13 | Read, 14 | Write, 15 | Random 16 | } 17 | 18 | [GcForce] 19 | [Orderer(SummaryOrderPolicy.FastestToSlowest)] 20 | [SimpleJob(RuntimeMoniker.Net60, launchCount: 1, warmupCount: 5, iterationCount: 10)] 21 | [MemoryDiagnoser] 22 | #nullable disable 23 | public class FasterKvBenchmark 24 | { 25 | private const long Count = 10000; 26 | private static readonly Random _random = new Random(1024); 27 | private FasterKvCache _provider; 28 | private static readonly TimeSpan _default = TimeSpan.FromSeconds(30); 29 | 30 | 31 | [Params(TestType.Read, TestType.Write, TestType.Random)] 32 | public TestType Type { get; set; } 33 | 34 | [Params(1,4,8)] 35 | public int ThreadCount { get; set; } 36 | 37 | private static readonly string[] HotKeys = Enumerable.Range(0, (int)(Count * 0.5)) 38 | .Select(i => $"cache_{_random.Next(0, (int) Count)}") 39 | .ToArray(); 40 | 41 | [GlobalSetup] 42 | public void Setup() 43 | { 44 | var services = new ServiceCollection(); 45 | services.AddFasterKvCache(options => 46 | { 47 | options.IndexCount = 1024; 48 | options.MemorySizeBit = 20; 49 | options.PageSizeBit = 20; 50 | options.ReadCacheMemorySizeBit = 20; 51 | options.ReadCachePageSizeBit = 20; 52 | // use MessagePack serializer 53 | options.UseMessagePackSerializer(); 54 | }, "MyKvCache"); 55 | IServiceProvider serviceProvider = services.BuildServiceProvider(); 56 | _provider = serviceProvider.GetService>()!; 57 | 58 | switch (Type) 59 | { 60 | case TestType.Write: 61 | break; 62 | case TestType.Read: 63 | case TestType.Random: 64 | for (int i = 0; i < Count; i++) 65 | { 66 | _provider!.Set($"cache_{i}", "cache", _default); 67 | } 68 | break; 69 | default: 70 | throw new ArgumentOutOfRangeException(); 71 | } 72 | } 73 | 74 | [Benchmark] 75 | public async Task Full() 76 | { 77 | var tasks = new Task[ThreadCount]; 78 | var threadOpCount = (int)(HotKeys.Length / ThreadCount); 79 | for (int i = 0; i < ThreadCount; i++) 80 | { 81 | int i1 = i; 82 | tasks[i] = Task.Run(() => 83 | { 84 | var j = i1 * threadOpCount; 85 | switch (Type) 86 | { 87 | case TestType.Read: 88 | for (; j < threadOpCount; j++) 89 | { 90 | _provider.Get(HotKeys[j]); 91 | } 92 | 93 | break; 94 | case TestType.Write: 95 | for (; j < threadOpCount; j++) 96 | { 97 | _provider.Set(HotKeys[j], "cache", _default); 98 | } 99 | 100 | break; 101 | case TestType.Random: 102 | for (; j < threadOpCount; j++) 103 | { 104 | if (j % 2 == 0) 105 | { 106 | _provider.Get(HotKeys[j]); 107 | } 108 | else 109 | { 110 | _provider.Set(HotKeys[j], "cache", _default); 111 | } 112 | } 113 | 114 | break; 115 | } 116 | }); 117 | } 118 | 119 | await Task.WhenAll(tasks); 120 | } 121 | 122 | 123 | [GlobalCleanup] 124 | public void Cleanup() 125 | { 126 | _provider.Dispose(); 127 | } 128 | } -------------------------------------------------------------------------------- /docs/assets/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InCerryGit/FasterKvCache/8bd8df49cc435dc29d62a1cf08ce6b3828f00a6f/docs/assets/arch.png -------------------------------------------------------------------------------- /sample/FasterKvCache.Sample.ConsoleApp/FasterKvCache.Sample.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/FasterKvCache.Sample.ConsoleApp/ObjectFasterKvCache.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using FasterKv.Cache.Core.Configurations; 4 | using FasterKv.Cache.MessagePack; 5 | 6 | namespace FasterKvCache.Sample.ConsoleApp; 7 | 8 | public class ObjectFasterKvCache 9 | { 10 | public async Task Run() 11 | { 12 | // create a FasterKvCache 13 | var cache = new FasterKv.Cache.Core.FasterKvCache("MyCache", 14 | new DefaultSystemClock(), 15 | new FasterKvCacheOptions(), 16 | new IFasterKvCacheSerializer[] 17 | { 18 | new MessagePackFasterKvCacheSerializer 19 | { 20 | Name = "MyCache" 21 | } 22 | }, 23 | null); 24 | 25 | var key = Guid.NewGuid().ToString("N"); 26 | 27 | // sync 28 | // set key and value with expiry time 29 | cache.Set(key, "my cache sync", TimeSpan.FromMinutes(5)); 30 | 31 | // get 32 | var result = cache.Get(key); 33 | Console.WriteLine(result); 34 | 35 | // delete 36 | cache.Delete(key); 37 | 38 | // async 39 | // set 40 | await cache.SetAsync(key, "my cache async"); 41 | 42 | // get 43 | result = await cache.GetAsync(key); 44 | Console.WriteLine(result); 45 | 46 | // delete 47 | await cache.DeleteAsync(key); 48 | 49 | // set other type object 50 | cache.Set(key, new DateTime(2022,2,22)); 51 | Console.WriteLine(cache.Get(key)); 52 | } 53 | } -------------------------------------------------------------------------------- /sample/FasterKvCache.Sample.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using FasterKvCache.Sample.ConsoleApp; 2 | 3 | _ = new ObjectFasterKvCache().Run(); 4 | _ = new TFasterKvCache().Run(); 5 | -------------------------------------------------------------------------------- /sample/FasterKvCache.Sample.ConsoleApp/TFasterKvCache.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using FasterKv.Cache.Core.Configurations; 4 | using FasterKv.Cache.MessagePack; 5 | 6 | namespace FasterKvCache.Sample.ConsoleApp; 7 | 8 | public class TFasterKvCache 9 | { 10 | public async Task Run() 11 | { 12 | // create a FasterKvCache 13 | var cache = new FasterKvCache("MyTCache", 14 | new DefaultSystemClock(), 15 | new FasterKvCacheOptions(), 16 | new IFasterKvCacheSerializer[] 17 | { 18 | new MessagePackFasterKvCacheSerializer 19 | { 20 | Name = "MyTCache" 21 | } 22 | }, 23 | null); 24 | 25 | var key = Guid.NewGuid().ToString("N"); 26 | 27 | // sync 28 | // set key and value with expiry time 29 | cache.Set(key, "my cache sync", TimeSpan.FromMinutes(5)); 30 | 31 | // get 32 | var result = cache.Get(key); 33 | Console.WriteLine(result); 34 | 35 | // delete 36 | cache.Delete(key); 37 | 38 | // async 39 | // set 40 | await cache.SetAsync(key, "my cache async"); 41 | 42 | 43 | // get 44 | result = await cache.GetAsync(key); 45 | Console.WriteLine(result); 46 | 47 | // delete 48 | await cache.DeleteAsync(key); 49 | } 50 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/ClientSessionWrap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using FASTER.core; 4 | 5 | namespace FasterKv.Cache.Core.Abstractions; 6 | 7 | internal sealed class ClientSessionWrap : IDisposable 8 | { 9 | public ClientSession, ValueWrapper, ValueWrapper, 10 | StoreContext>, StoreFunctions>> Session { get; } 11 | 12 | private readonly ConcurrentQueue, ValueWrapper, 13 | ValueWrapper, StoreContext>, StoreFunctions>>> 14 | _innerPool; 15 | 16 | public ClientSessionWrap( 17 | ClientSession, ValueWrapper, ValueWrapper, 18 | StoreContext>, StoreFunctions>> clientSession, 19 | ConcurrentQueue, ValueWrapper, ValueWrapper, 20 | StoreContext>, StoreFunctions>>> innerPool) 21 | { 22 | Session = clientSession; 23 | _innerPool = innerPool; 24 | } 25 | 26 | public void Dispose() 27 | { 28 | Session.CompletePending(true); 29 | _innerPool.Enqueue(Session); 30 | } 31 | } 32 | 33 | internal class ClientSessionWrap : IDisposable 34 | { 35 | public ClientSession, StoreFunctions> Session { get; } 37 | 38 | private readonly ConcurrentQueue, StoreFunctions>> 40 | _innerPool; 41 | 42 | public ClientSessionWrap( 43 | ClientSession, StoreFunctions> clientSession, 45 | ConcurrentQueue, StoreFunctions>> innerPool) 47 | { 48 | Session = clientSession; 49 | _innerPool = innerPool; 50 | } 51 | 52 | public void Dispose() 53 | { 54 | Session.CompletePending(true); 55 | _innerPool.Enqueue(Session); 56 | } 57 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/IFasterKvCacheExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace FasterKv.Cache.Core.Abstractions; 4 | 5 | /// 6 | /// FasterKvCache options extension. 7 | /// 8 | public interface IFasterKvCacheExtensionOptions 9 | { 10 | /// 11 | /// Adds the services. 12 | /// 13 | /// Services. 14 | void AddServices(IServiceCollection services, string name); 15 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/IFasterKvCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace FasterKv.Cache.Core; 4 | 5 | public interface IFasterKvCacheSerializer 6 | { 7 | string Name { get;} 8 | void Serialize(Stream stream, TValue data); 9 | TValue? Deserialize(byte[] bytes, int length); 10 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/ISystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FasterKv.Cache.Core.Abstractions; 4 | 5 | public interface ISystemClock 6 | { 7 | DateTimeOffset Now(); 8 | 9 | long NowUnixTimestamp(); 10 | } 11 | 12 | public sealed class DefaultSystemClock : ISystemClock 13 | { 14 | public DateTimeOffset Now() 15 | { 16 | return DateTimeOffset.Now; 17 | } 18 | 19 | public long NowUnixTimestamp() 20 | { 21 | return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/StoreFunctions.cs: -------------------------------------------------------------------------------- 1 | using FASTER.core; 2 | 3 | namespace FasterKv.Cache.Core.Abstractions; 4 | 5 | internal sealed class StoreContext 6 | { 7 | private Status _status; 8 | private TOutput? _output; 9 | 10 | internal void Populate(ref Status status, ref TOutput output) 11 | { 12 | _status = status; 13 | _output = output; 14 | } 15 | 16 | internal void FinalizeRead(out Status status, out TOutput output) 17 | { 18 | status = _status; 19 | output = _output!; 20 | } 21 | } 22 | 23 | internal sealed class StoreFunctions : SimpleFunctions> 24 | { 25 | public override void ReadCompletionCallback(ref TKey key, 26 | ref TOutput input, 27 | ref TOutput output, 28 | StoreContext? ctx, 29 | Status status, 30 | RecordMetadata recordMetadata) 31 | { 32 | ctx?.Populate(ref status, ref output); 33 | } 34 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Abstractions/ValueWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using FasterKv.Cache.Core.Serializers; 4 | 5 | namespace FasterKv.Cache.Core; 6 | 7 | internal struct ValueWrapper 8 | { 9 | public ValueWrapper() 10 | { 11 | 12 | } 13 | 14 | public ValueWrapper(T? data, long? expiryTime = null) 15 | { 16 | ExpiryTime = expiryTime; 17 | Data = data; 18 | } 19 | 20 | /// 21 | /// Inner Data 22 | /// 23 | internal T? Data { get; set; } 24 | 25 | /// 26 | /// Expiry Time 27 | /// 28 | public long? ExpiryTime { get; set; } 29 | 30 | /// 31 | /// HasExpired 32 | /// 33 | /// Now 34 | /// value has expired 35 | public bool HasExpired(long nowTimestamp) => nowTimestamp > ExpiryTime; 36 | 37 | /// 38 | /// Get FasterKvSerializerFlags 39 | /// 40 | /// 41 | /// 42 | internal FasterKvSerializerFlags GetFlags(long nowTimestamp) 43 | { 44 | var flags = FasterKvSerializerFlags.None; 45 | if (ExpiryTime is not null) 46 | { 47 | flags |= FasterKvSerializerFlags.HasExpiryTime; 48 | } 49 | 50 | // don't serializer expired value body 51 | if (Data is not null && HasExpired(nowTimestamp) == false) 52 | { 53 | flags |= FasterKvSerializerFlags.HasBody; 54 | } 55 | 56 | return flags; 57 | } 58 | } 59 | 60 | internal sealed class ValueWrapper 61 | { 62 | internal int DataByteLength = 0; 63 | internal object? Data; 64 | 65 | public ValueWrapper() 66 | { 67 | 68 | } 69 | 70 | public ValueWrapper(object? data, long? expiryTime = null) 71 | { 72 | ExpiryTime = expiryTime; 73 | Data = data; 74 | } 75 | 76 | /// 77 | /// Expiry Time 78 | /// 79 | public long? ExpiryTime { get; set; } 80 | 81 | /// 82 | /// DataBytes 83 | /// 84 | public byte[]? DataBytes { get; set; } 85 | 86 | /// 87 | /// HasExpired 88 | /// 89 | /// Now 90 | /// value has expired 91 | public bool HasExpired(long nowTimestamp) => nowTimestamp > ExpiryTime; 92 | 93 | /// 94 | /// Get FasterKvSerializerFlags 95 | /// 96 | /// 97 | /// 98 | internal FasterKvSerializerFlags GetFlags(long nowTimestamp) 99 | { 100 | var flags = FasterKvSerializerFlags.None; 101 | if (ExpiryTime is not null) 102 | { 103 | flags |= FasterKvSerializerFlags.HasExpiryTime; 104 | } 105 | 106 | // don't serializer expired value body 107 | if (Data is not null && HasExpired(nowTimestamp) == false) 108 | { 109 | flags |= FasterKvSerializerFlags.HasBody; 110 | } 111 | 112 | return flags; 113 | } 114 | 115 | /// 116 | /// Get TValue From Data or DataBytes 117 | /// 118 | /// 119 | /// 120 | /// 121 | public TValue? Get(IFasterKvCacheSerializer serializer) 122 | { 123 | if (DataBytes is not null) 124 | { 125 | Data = serializer.Deserialize(DataBytes, DataByteLength); 126 | var bytes = DataBytes; 127 | DataBytes = null; 128 | DataByteLength = 0; 129 | ArrayPool.Shared.Return(bytes); 130 | } 131 | 132 | return Data is null ? default : (TValue)Data; 133 | } 134 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Configurations/FasterKvCacheOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using FASTER.core; 5 | using FasterKv.Cache.Core.Abstractions; 6 | 7 | namespace FasterKv.Cache.Core.Configurations; 8 | 9 | /// 10 | /// FasterKvCacheOptions 11 | /// for details, see https://microsoft.github.io/FASTER/docs/fasterkv-basics/#fasterkvsettings 12 | /// 13 | public sealed class FasterKvCacheOptions 14 | { 15 | /// 16 | /// FasterKv index count, Must be power of 2 17 | /// 18 | /// For example: 1024(2^10) 2048(2^11) 65536(2^16) 131072(2^17) 19 | /// Each index is 64 bits. So this define 131072 keys. Used 1024Kb memory 20 | public long IndexCount { get; set; } = 131072; 21 | 22 | /// 23 | /// FasterKv used memory size 24 | /// 25 | /// Default: 16MB 26 | public int MemorySizeBit { get; set; } = 24; 27 | 28 | /// 29 | /// FasterKv page size 30 | /// 31 | /// Default: 1MB 32 | public int PageSizeBit { get; set; } = 20; 33 | 34 | /// 35 | /// FasterKv read cache used memory size 36 | /// 37 | /// Default: 16MB 38 | public int ReadCacheMemorySizeBit { get; set; } = 24; 39 | 40 | /// 41 | /// FasterKv read cache page size 42 | /// 43 | /// Default: 1MB 44 | public int ReadCachePageSizeBit { get; set; } = 20; 45 | 46 | /// 47 | /// FasterKv commit logs path 48 | /// 49 | /// Default: {CurrentDirectory}/FasterKvCache/{Environment.ProcessId}-HLog 50 | public string LogPath { get; set; } = 51 | #if NET6_0_OR_GREATER 52 | Path.Combine(Environment.CurrentDirectory, $"FasterKvCache/{Environment.ProcessId}-HLog"); 53 | #else 54 | Path.Combine(Environment.CurrentDirectory, 55 | $"FasterKvCache/{System.Diagnostics.Process.GetCurrentProcess().Id}-HLog"); 56 | #endif 57 | 58 | 59 | /// 60 | /// Serializer Name 61 | /// 62 | public string? SerializerName { get; set; } 63 | 64 | /// 65 | /// Preallocate file 66 | /// 67 | public bool PreallocateFile { get; set; } = false; 68 | 69 | /// 70 | /// Delete file on close 71 | /// 72 | public bool DeleteFileOnClose { get; set; } = true; 73 | 74 | /// 75 | /// Try recover latest 76 | /// 77 | public bool TryRecoverLatest { get; set; } = false; 78 | 79 | /// 80 | /// Expiry key scan thread interval 81 | /// 82 | /// Timed deletion of expired keys 83 | /// Zero or negative numbers are not scanned 84 | /// Default: 5min 85 | public TimeSpan ExpiryKeyScanInterval { get; set; } = TimeSpan.FromMinutes(5); 86 | 87 | /// 88 | /// Set Custom Store 89 | /// 90 | public FasterBase? CustomStore { get; set; } 91 | 92 | /// 93 | /// Gets the extensions. 94 | /// 95 | /// The extensions. 96 | internal IList Extensions { get; } = new List(); 97 | 98 | /// 99 | /// Registers the extension. 100 | /// 101 | /// Extension. 102 | public void RegisterExtension(IFasterKvCacheExtensionOptions extension) 103 | { 104 | extension.ArgumentNotNull(); 105 | 106 | Extensions.Add(extension); 107 | } 108 | 109 | internal LogSettings GetLogSettings(string? name) 110 | { 111 | name ??= ""; 112 | return new LogSettings 113 | { 114 | LogDevice = Devices.CreateLogDevice(Path.Combine(LogPath, name) + ".log", 115 | preallocateFile: PreallocateFile, 116 | deleteOnClose: DeleteFileOnClose), 117 | ObjectLogDevice = Devices.CreateLogDevice(Path.Combine(LogPath, name) + ".obj.log", 118 | preallocateFile: PreallocateFile, 119 | deleteOnClose: DeleteFileOnClose), 120 | PageSizeBits = PageSizeBit, 121 | MemorySizeBits = MemorySizeBit, 122 | ReadCacheSettings = new ReadCacheSettings 123 | { 124 | MemorySizeBits = ReadCacheMemorySizeBit, 125 | PageSizeBits = ReadCachePageSizeBit, 126 | } 127 | }; 128 | } 129 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Configurations/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace FasterKv.Cache.Core.Configurations; 9 | 10 | public static class ServiceCollectionExtensions 11 | { 12 | const string DefaultFasterKvCacheName = "FasterKvCache"; 13 | const string DefaultFasterKvCacheTValueName = "FasterKvCacheTValue"; 14 | 15 | /// 16 | /// Adds the FasterKvCache (specify the config via hard code). 17 | /// 18 | public static IServiceCollection AddFasterKvCache( 19 | this IServiceCollection services, 20 | Action setupAction, 21 | string name = DefaultFasterKvCacheName 22 | ) 23 | { 24 | services.ArgumentNotNull(); 25 | setupAction.ArgumentNotNull(); 26 | 27 | var option = new FasterKvCacheOptions(); 28 | setupAction(option); 29 | foreach (var extension in option.Extensions) 30 | { 31 | extension.AddServices(services, name); 32 | } 33 | 34 | services.Configure(name, x => 35 | { 36 | x.IndexCount = option.IndexCount; 37 | x.PageSizeBit = option.PageSizeBit; 38 | x.LogPath = option.LogPath; 39 | x.MemorySizeBit = option.MemorySizeBit; 40 | x.ExpiryKeyScanInterval = option.ExpiryKeyScanInterval; 41 | x.SerializerName = option.SerializerName; 42 | x.ReadCacheMemorySizeBit = option.ReadCacheMemorySizeBit; 43 | x.ReadCachePageSizeBit = option.ReadCachePageSizeBit; 44 | x.CustomStore = option.CustomStore; 45 | x.DeleteFileOnClose = option.DeleteFileOnClose; 46 | x.TryRecoverLatest = option.TryRecoverLatest; 47 | }); 48 | services.TryAddSingleton(); 49 | services.AddSingleton(provider => 50 | { 51 | var optionsMon = provider.GetRequiredService>(); 52 | var options = optionsMon.Get(name); 53 | var factory = provider.GetService(); 54 | var serializers = provider.GetServices(); 55 | var clock = provider.GetService(); 56 | return new FasterKvCache(name, clock!, options, serializers, factory); 57 | }); 58 | return services; 59 | } 60 | 61 | /// 62 | /// Adds the FasterKvCache (specify the config via hard code). 63 | /// 64 | public static IServiceCollection AddFasterKvCache( 65 | this IServiceCollection services, 66 | Action setupAction, 67 | string name = DefaultFasterKvCacheTValueName 68 | ) 69 | { 70 | services.ArgumentNotNull(); 71 | setupAction.ArgumentNotNull(); 72 | 73 | var option = new FasterKvCacheOptions(); 74 | setupAction(option); 75 | foreach (var extension in option.Extensions) 76 | { 77 | extension.AddServices(services, name); 78 | } 79 | 80 | services.Configure(name, x => 81 | { 82 | x.IndexCount = option.IndexCount; 83 | x.PageSizeBit = option.PageSizeBit; 84 | x.LogPath = option.LogPath; 85 | x.MemorySizeBit = option.MemorySizeBit; 86 | x.ExpiryKeyScanInterval = option.ExpiryKeyScanInterval; 87 | x.SerializerName = option.SerializerName; 88 | x.ReadCacheMemorySizeBit = option.ReadCacheMemorySizeBit; 89 | x.ReadCachePageSizeBit = option.ReadCachePageSizeBit; 90 | x.CustomStore = option.CustomStore; 91 | }); 92 | services.TryAddSingleton(); 93 | services.AddSingleton(provider => 94 | { 95 | var optionsMon = provider.GetRequiredService>(); 96 | var options = optionsMon.Get(name); 97 | var factory = provider.GetService(); 98 | var serializers = provider.GetServices(); 99 | var clock = provider.GetService(); 100 | return new FasterKvCache(name, clock!, options, serializers, factory); 101 | }); 102 | return services; 103 | } 104 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/FasterKv.Cache.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;netstandard2.0;net8.0;net9.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/FasterKvCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using FASTER.core; 9 | using FasterKv.Cache.Core.Abstractions; 10 | using FasterKv.Cache.Core.Configurations; 11 | using FasterKv.Cache.Core.Serializers; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace FasterKv.Cache.Core; 15 | 16 | public sealed class FasterKvCache : IDisposable 17 | { 18 | private readonly ILogger? _logger; 19 | private readonly ISystemClock _systemClock; 20 | 21 | private readonly string _name; 22 | private readonly FasterKvCacheOptions _options; 23 | private readonly FasterKV _fasterKv; 24 | private readonly LogSettings? _logSettings; 25 | 26 | private readonly ConcurrentQueue, StoreFunctions>> 28 | _sessionPool; 29 | 30 | private readonly CancellationTokenSource? _scanExpiryCancellationToken; 31 | private readonly IFasterKvCacheSerializer _valueSerializer; 32 | 33 | public FasterKvCache(string name, 34 | ISystemClock systemClock, 35 | FasterKvCacheOptions? options, 36 | IEnumerable? serializers, 37 | ILoggerFactory? loggerFactory) 38 | { 39 | _name = name; 40 | _systemClock = systemClock.ArgumentNotNull(); 41 | _options = options.ArgumentNotNull(); 42 | _logger = loggerFactory?.CreateLogger(); 43 | 44 | var serializerName = name.NotNullOrEmpty() ? name : options!.SerializerName; 45 | // ReSharper disable once PossibleMultipleEnumeration 46 | _valueSerializer = serializers.ArgumentNotNull() 47 | .FirstOrDefault(s => s.Name == serializerName) 48 | ?? throw new InvalidOperationException($"Not found {serializerName} serializer"); 49 | 50 | if (options!.CustomStore is null) 51 | { 52 | var serializer = new SerializerSettings 53 | { 54 | keySerializer = () => new StringSerializer(), 55 | valueSerializer = () => new FasterKvSerializer(_valueSerializer, _systemClock) 56 | }; 57 | 58 | _logSettings = options.GetLogSettings(name); 59 | _fasterKv = new FasterKV( 60 | options.IndexCount, 61 | _logSettings, 62 | checkpointSettings: new CheckpointSettings() 63 | { 64 | CheckpointDir = options.LogPath + ".checkpoint" 65 | }, 66 | serializerSettings: serializer, 67 | tryRecoverLatest: options.TryRecoverLatest 68 | ); 69 | } 70 | else 71 | { 72 | _fasterKv = (FasterKV) options.CustomStore; 73 | } 74 | 75 | _sessionPool = 76 | new ConcurrentQueue, StoreFunctions>>(); 78 | 79 | if (_options.ExpiryKeyScanInterval > TimeSpan.Zero) 80 | { 81 | _scanExpiryCancellationToken = new CancellationTokenSource(); 82 | Task.Run(ExpiryScanLoop); 83 | } 84 | } 85 | 86 | public TValue? Get(string key) 87 | { 88 | key.ArgumentNotNullOrEmpty(); 89 | 90 | using var scopeSession = GetSessionWrap(); 91 | var context = new StoreContext(); 92 | var result = scopeSession.Session.Read(key, context); 93 | if (result.status.IsPending) 94 | { 95 | scopeSession.Session.CompletePending(true); 96 | context.FinalizeRead(out result.status, out result.output); 97 | } 98 | 99 | if (result.output is null) 100 | { 101 | return default; 102 | } 103 | 104 | if (result.output.HasExpired(_systemClock.NowUnixTimestamp())) 105 | { 106 | Delete(key); 107 | return default; 108 | } 109 | 110 | return result.output.Get(_valueSerializer); 111 | } 112 | 113 | public TValue GetOrAdd(string key, Func factory) 114 | { 115 | key.ArgumentNotNullOrEmpty(); 116 | factory.ArgumentNotNull(); 117 | 118 | var result = Get(key); 119 | if (result is not null) 120 | return result; 121 | 122 | result = factory(key); 123 | Set(key, result); 124 | return result; 125 | } 126 | 127 | public TValue GetOrAdd(string key, Func factory, TimeSpan expiryTime) 128 | { 129 | key.ArgumentNotNullOrEmpty(); 130 | factory.ArgumentNotNull(); 131 | expiryTime.ArgumentNotNegativeOrZero(); 132 | 133 | var result = Get(key); 134 | if (result is not null) 135 | return result; 136 | 137 | result = factory(key); 138 | Set(key, result, expiryTime); 139 | return result; 140 | } 141 | 142 | public void Delete(string key) 143 | { 144 | using var scopeSession = GetSessionWrap(); 145 | scopeSession.Session.Delete(ref key); 146 | } 147 | 148 | public void Set(string key, TValue? value) 149 | { 150 | key.ArgumentNotNullOrEmpty(); 151 | 152 | using var sessionWrap = GetSessionWrap(); 153 | SetInternal(sessionWrap, key, value); 154 | } 155 | 156 | public void Set(string key, TValue value, TimeSpan expiryTime) 157 | { 158 | key.ArgumentNotNullOrEmpty(); 159 | expiryTime.ArgumentNotNegativeOrZero(); 160 | 161 | using var sessionWrap = GetSessionWrap(); 162 | SetInternal(sessionWrap, key, value, expiryTime); 163 | } 164 | 165 | public async Task GetAsync(string key, CancellationToken token = default) 166 | { 167 | key.ArgumentNotNullOrEmpty(); 168 | 169 | using var scopeSession = GetSessionWrap(); 170 | var result = (await scopeSession.Session.ReadAsync(ref key, token: token)).Complete(); 171 | 172 | if (result.output is null) 173 | { 174 | return default; 175 | } 176 | 177 | if (result.output.HasExpired(_systemClock.NowUnixTimestamp())) 178 | { 179 | await DeleteAsync(key, token); 180 | return default; 181 | } 182 | 183 | return result.output.Get(_valueSerializer); 184 | } 185 | 186 | public async Task GetOrAddAsync(string key, Func> factory, CancellationToken token = default) 187 | { 188 | factory.ArgumentNotNull(); 189 | 190 | var result = await GetAsync(key, token); 191 | if (result is not null) 192 | return result; 193 | 194 | result = await factory(key); 195 | await SetAsync(key, result, token); 196 | return result; 197 | } 198 | 199 | public async Task GetOrAddAsync(string key, Func> factory, TimeSpan expiryTime, CancellationToken token = default) 200 | { 201 | factory.ArgumentNotNull(); 202 | 203 | var result = await GetAsync(key, token); 204 | if (result is not null) 205 | return result; 206 | 207 | result = await factory(key); 208 | await SetAsync(key, result, expiryTime, token); 209 | return result; 210 | } 211 | 212 | public async Task DeleteAsync(string key, CancellationToken token = default) 213 | { 214 | key.ArgumentNotNull(); 215 | 216 | using var scopeSession = GetSessionWrap(); 217 | (await scopeSession.Session.DeleteAsync(ref key, token: token).ConfigureAwait(false)).Complete(); 218 | } 219 | 220 | public async Task SetAsync(string key, TValue? value, CancellationToken token = default) 221 | { 222 | key.ArgumentNotNullOrEmpty(); 223 | 224 | using var sessionWrap = GetSessionWrap(); 225 | await SetInternalAsync(sessionWrap, key, value, token); 226 | } 227 | 228 | public async Task SetAsync(string key, TValue? value, TimeSpan expiryTime, CancellationToken token = default) 229 | { 230 | key.ArgumentNotNullOrEmpty(); 231 | 232 | using var sessionWrap = GetSessionWrap(); 233 | await SetInternalAsync(sessionWrap, key, value, token, expiryTime); 234 | } 235 | 236 | private async Task SetInternalAsync(ClientSessionWrap sessionWrap, string key, TValue? value, 237 | CancellationToken cancellationToken, TimeSpan? expiryTime = null) 238 | { 239 | var wrapper = new ValueWrapper(value, 240 | expiryTime.HasValue 241 | ? _systemClock.Now() 242 | .Add(expiryTime.Value) 243 | .ToUnixTimeMilliseconds() 244 | : null); 245 | (await sessionWrap.Session.UpsertAsync(ref key, ref wrapper, token: cancellationToken) 246 | .ConfigureAwait(false)).Complete(); 247 | } 248 | 249 | private void SetInternal(ClientSessionWrap sessionWrap, string key, TValue? value, 250 | TimeSpan? expiryTime = null) 251 | { 252 | var wrapper = new ValueWrapper(value, 253 | expiryTime.HasValue 254 | ? _systemClock.Now() 255 | .Add(expiryTime.Value) 256 | .ToUnixTimeMilliseconds() 257 | : null); 258 | sessionWrap.Session.Upsert(ref key, ref wrapper); 259 | } 260 | 261 | /// 262 | /// Get ClientSession from pool 263 | /// 264 | /// 265 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 266 | private ClientSessionWrap GetSessionWrap() 267 | { 268 | if (_sessionPool.TryDequeue(out var session) == false) 269 | { 270 | session = _fasterKv.For(new StoreFunctions()) 271 | .NewSession>(); 272 | } 273 | 274 | return new ClientSessionWrap(session, _sessionPool); 275 | } 276 | 277 | private async Task ExpiryScanLoop() 278 | { 279 | var cancelToken = _scanExpiryCancellationToken!.Token; 280 | while (cancelToken.IsCancellationRequested == false) 281 | { 282 | try 283 | { 284 | await Task.Delay(_options.ExpiryKeyScanInterval, cancelToken); 285 | using var sessionWrap = GetSessionWrap(); 286 | using var iter = sessionWrap.Session.Iterate(); 287 | while (iter.GetNext(out _) && cancelToken.IsCancellationRequested == false) 288 | { 289 | var key = iter.GetKey(); 290 | var context = new StoreContext(); 291 | var result = sessionWrap.Session.Read(key, context); 292 | if (result.status.IsPending) 293 | { 294 | sessionWrap.Session.CompletePending(true); 295 | context.FinalizeRead(out result.status, out result.output); 296 | } 297 | 298 | if (result.status.Found && result.output.HasExpired(_systemClock.NowUnixTimestamp())) 299 | { 300 | sessionWrap.Session.Delete(key); 301 | } 302 | } 303 | } 304 | catch (Exception ex) 305 | { 306 | _logger?.LogWarning("Exception thrown in expiry scan loop:{Ex}", ex); 307 | } 308 | } 309 | } 310 | 311 | private void Dispose(bool _) 312 | { 313 | _scanExpiryCancellationToken?.Cancel(); 314 | foreach (var session in _sessionPool) 315 | { 316 | session.Dispose(); 317 | } 318 | 319 | if (_options.CustomStore != _fasterKv) 320 | { 321 | if (_options.DeleteFileOnClose == false) 322 | { 323 | _fasterKv.TakeFullCheckpointAsync(CheckpointType.FoldOver).AsTask().GetAwaiter().GetResult(); 324 | } 325 | _fasterKv.Dispose(); 326 | } 327 | 328 | _logSettings?.LogDevice.Dispose(); 329 | _logSettings?.ObjectLogDevice.Dispose(); 330 | } 331 | 332 | public void Dispose() 333 | { 334 | Dispose(true); 335 | GC.SuppressFinalize(this); 336 | } 337 | 338 | ~FasterKvCache() 339 | { 340 | Dispose(false); 341 | } 342 | 343 | internal ValueWrapper GetWithOutExpiry(string key) 344 | { 345 | var context = new StoreContext(); 346 | using var scopeSession = GetSessionWrap(); 347 | var result = scopeSession.Session.Read(key, context); 348 | if (result.status.IsPending) 349 | { 350 | scopeSession.Session.CompletePending(true); 351 | context.FinalizeRead(out result.status, out result.output); 352 | } 353 | 354 | return result.output; 355 | } 356 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/FasterKvStore.TValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FASTER.core; 8 | using FasterKv.Cache.Core.Abstractions; 9 | using FasterKv.Cache.Core.Configurations; 10 | using FasterKv.Cache.Core.Serializers; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace FasterKv.Cache.Core; 14 | 15 | public sealed class FasterKvCache : IDisposable 16 | { 17 | private readonly ILogger? _logger; 18 | private readonly ISystemClock _systemClock; 19 | 20 | private readonly string _name; 21 | private readonly FasterKvCacheOptions _options; 22 | private readonly FasterKV> _fasterKv; 23 | private readonly LogSettings? _logSettings; 24 | 25 | private readonly ConcurrentQueue, ValueWrapper, 26 | ValueWrapper, StoreContext>, StoreFunctions>>> 27 | _sessionPool; 28 | 29 | private readonly CancellationTokenSource? _scanExpiryCancellationToken; 30 | 31 | public FasterKvCache(string name, 32 | ISystemClock systemClock, 33 | FasterKvCacheOptions? options, 34 | IEnumerable? serializers, 35 | ILoggerFactory? loggerFactory) 36 | { 37 | _name = name; 38 | _systemClock = systemClock.ArgumentNotNull(); 39 | _options = options.ArgumentNotNull(); 40 | _logger = loggerFactory?.CreateLogger>(); 41 | 42 | if (options!.CustomStore is null) 43 | { 44 | var serializerName = name.NotNullOrEmpty() ? name : options.SerializerName; 45 | // ReSharper disable once PossibleMultipleEnumeration 46 | var valueSerializer = 47 | serializers.ArgumentNotNull().FirstOrDefault(s => s.Name == serializerName) 48 | ?? throw new InvalidOperationException($"Not found {serializerName} serializer"); 49 | 50 | var serializer = new SerializerSettings> 51 | { 52 | keySerializer = () => new StringSerializer(), 53 | valueSerializer = () => new FasterKvSerializer(valueSerializer, _systemClock) 54 | }; 55 | 56 | _logSettings = options.GetLogSettings(name); 57 | _fasterKv = new FasterKV>( 58 | options.IndexCount, 59 | _logSettings, 60 | checkpointSettings: new CheckpointSettings() 61 | { 62 | CheckpointDir = options.LogPath + ".checkpoint" 63 | }, 64 | serializerSettings: serializer, 65 | tryRecoverLatest: options.TryRecoverLatest 66 | ); 67 | } 68 | else 69 | { 70 | _fasterKv = (FasterKV>) options.CustomStore; 71 | } 72 | 73 | _sessionPool = 74 | new ConcurrentQueue, ValueWrapper, ValueWrapper, 75 | StoreContext>, StoreFunctions>>>(); 76 | 77 | if (_options.ExpiryKeyScanInterval > TimeSpan.Zero) 78 | { 79 | _scanExpiryCancellationToken = new CancellationTokenSource(); 80 | Task.Run(ExpiryScanLoop); 81 | } 82 | } 83 | 84 | public TValue? Get(string key) 85 | { 86 | key.ArgumentNotNullOrEmpty(); 87 | 88 | using var scopeSession = GetSessionWrap(); 89 | var context = new StoreContext>(); 90 | var result = scopeSession.Session.Read(key, context); 91 | if (result.status.IsPending) 92 | { 93 | scopeSession.Session.CompletePending(true); 94 | context.FinalizeRead(out result.status, out result.output); 95 | } 96 | 97 | if (result.output.HasExpired(_systemClock.NowUnixTimestamp())) 98 | { 99 | Delete(key); 100 | return default; 101 | } 102 | 103 | return result.output.Data; 104 | } 105 | 106 | public TValue GetOrAdd(string key, Func factory) 107 | { 108 | factory.ArgumentNotNull(); 109 | 110 | var value = Get(key); 111 | if (value is not null) 112 | return value; 113 | 114 | value = factory(key); 115 | Set(key, value); 116 | return value; 117 | } 118 | 119 | public TValue GetOrAdd(string key, Func factory, TimeSpan expiryTime) 120 | { 121 | factory.ArgumentNotNull(); 122 | 123 | var value = Get(key); 124 | if (value is not null) 125 | return value; 126 | 127 | value = factory(key); 128 | Set(key, value, expiryTime); 129 | return value; 130 | } 131 | 132 | public void Delete(string key) 133 | { 134 | key.ArgumentNotNull(); 135 | 136 | using var scopeSession = GetSessionWrap(); 137 | scopeSession.Session.Delete(ref key); 138 | } 139 | 140 | 141 | 142 | public void Set(string key, TValue? value) 143 | { 144 | key.ArgumentNotNullOrEmpty(); 145 | 146 | using var sessionWrap = GetSessionWrap(); 147 | SetInternal(sessionWrap, key, value); 148 | } 149 | 150 | 151 | 152 | public void Set(string key, TValue value, TimeSpan expiryTime) 153 | { 154 | key.ArgumentNotNullOrEmpty(); 155 | expiryTime.ArgumentNotNegativeOrZero(); 156 | 157 | using var sessionWrap = GetSessionWrap(); 158 | SetInternal(sessionWrap, key, value, expiryTime); 159 | } 160 | 161 | public async Task GetAsync(string key, CancellationToken token = default) 162 | { 163 | key.ArgumentNotNullOrEmpty(); 164 | 165 | using var scopeSession = GetSessionWrap(); 166 | var result = (await scopeSession.Session.ReadAsync(ref key, token: token)).Complete(); 167 | 168 | if (result.output.HasExpired(_systemClock.NowUnixTimestamp())) 169 | { 170 | await DeleteAsync(key, token); 171 | return default; 172 | } 173 | 174 | return result.output.Data; 175 | } 176 | 177 | public async Task GetOrAddAsync(string key, Func> factory, CancellationToken token = default) 178 | { 179 | factory.ArgumentNotNull(); 180 | 181 | var value = await GetAsync(key, token); 182 | if (value is not null) 183 | return value; 184 | 185 | value = await factory(key); 186 | await SetAsync(key, value, token); 187 | return value; 188 | } 189 | 190 | public async Task GetOrAddAsync(string key, Func> factory, TimeSpan expiryTime, CancellationToken token = default) 191 | { 192 | factory.ArgumentNotNull(); 193 | 194 | var value = await GetAsync(key, token); 195 | if (value is not null) 196 | return value; 197 | 198 | value = await factory(key); 199 | await SetAsync(key, value, expiryTime, token); 200 | return value; 201 | } 202 | 203 | public async Task DeleteAsync(string key, CancellationToken token = default) 204 | { 205 | key.ArgumentNotNull(); 206 | 207 | using var scopeSession = GetSessionWrap(); 208 | (await scopeSession.Session.DeleteAsync(ref key, token: token).ConfigureAwait(false)).Complete(); 209 | } 210 | 211 | public async Task SetAsync(string key, TValue? value, CancellationToken token = default) 212 | { 213 | key.ArgumentNotNullOrEmpty(); 214 | 215 | using var sessionWrap = GetSessionWrap(); 216 | await SetInternalAsync(sessionWrap, key, value, token); 217 | } 218 | 219 | public async Task SetAsync(string key, TValue? value, TimeSpan expiryTime, CancellationToken token = default) 220 | { 221 | key.ArgumentNotNullOrEmpty(); 222 | 223 | using var sessionWrap = GetSessionWrap(); 224 | await SetInternalAsync(sessionWrap, key, value, token, expiryTime); 225 | } 226 | 227 | 228 | private void SetInternal(ClientSessionWrap sessionWrap, string key, TValue? value, 229 | TimeSpan? expiryTime = null) 230 | { 231 | var wrapper = new ValueWrapper(value, 232 | expiryTime.HasValue ? _systemClock.Now().Add(expiryTime.Value).ToUnixTimeMilliseconds() : null); 233 | sessionWrap.Session.Upsert(ref key, ref wrapper); 234 | } 235 | 236 | private async Task SetInternalAsync(ClientSessionWrap sessionWrap, string key, TValue? value, 237 | CancellationToken cancellationToken, TimeSpan? expiryTime = null) 238 | { 239 | var wrapper = new ValueWrapper(value, 240 | expiryTime.HasValue ? _systemClock.Now().Add(expiryTime.Value).ToUnixTimeMilliseconds() : null); 241 | (await sessionWrap.Session.UpsertAsync(ref key, ref wrapper, token: cancellationToken) 242 | .ConfigureAwait(false)).Complete(); 243 | } 244 | 245 | /// 246 | /// Get ClientSession from pool 247 | /// 248 | /// 249 | private ClientSessionWrap GetSessionWrap() 250 | { 251 | if (_sessionPool.TryDequeue(out var session) == false) 252 | { 253 | session = _fasterKv.For(new StoreFunctions>()) 254 | .NewSession>>(); 255 | } 256 | 257 | return new ClientSessionWrap(session, _sessionPool); 258 | } 259 | 260 | private async Task ExpiryScanLoop() 261 | { 262 | var cancelToken = _scanExpiryCancellationToken!.Token; 263 | while (cancelToken.IsCancellationRequested == false) 264 | { 265 | try 266 | { 267 | await Task.Delay(_options.ExpiryKeyScanInterval, cancelToken); 268 | using var sessionWrap = GetSessionWrap(); 269 | using var iter = sessionWrap.Session.Iterate(); 270 | while (iter.GetNext(out _) && cancelToken.IsCancellationRequested == false) 271 | { 272 | var key = iter.GetKey(); 273 | var context = new StoreContext>(); 274 | var result = sessionWrap.Session.Read(key, context); 275 | if (result.status.IsPending) 276 | { 277 | sessionWrap.Session.CompletePending(true); 278 | context.FinalizeRead(out result.status, out result.output); 279 | } 280 | 281 | if (result.status.Found && result.output.HasExpired(_systemClock.NowUnixTimestamp())) 282 | { 283 | sessionWrap.Session.Delete(key); 284 | } 285 | } 286 | } 287 | catch (Exception ex) 288 | { 289 | _logger?.LogWarning("Exception thrown in expiry scan loop:{Ex}", ex); 290 | } 291 | } 292 | } 293 | 294 | private void Dispose(bool _) 295 | { 296 | _scanExpiryCancellationToken?.Cancel(); 297 | foreach (var session in _sessionPool) 298 | { 299 | session.Dispose(); 300 | } 301 | 302 | if (_options.CustomStore != _fasterKv) 303 | { 304 | if (_options.DeleteFileOnClose == false) 305 | { 306 | _fasterKv.TakeFullCheckpointAsync(CheckpointType.FoldOver).AsTask().GetAwaiter().GetResult(); 307 | } 308 | _fasterKv.Dispose(); 309 | } 310 | 311 | _logSettings?.LogDevice.Dispose(); 312 | _logSettings?.ObjectLogDevice.Dispose(); 313 | } 314 | 315 | public void Dispose() 316 | { 317 | Dispose(true); 318 | GC.SuppressFinalize(this); 319 | } 320 | 321 | ~FasterKvCache() 322 | { 323 | Dispose(false); 324 | } 325 | 326 | internal ValueWrapper GetWithOutExpiry(string key) 327 | { 328 | var context = new StoreContext>(); 329 | using var scopeSession = GetSessionWrap(); 330 | var result = scopeSession.Session.Read(key, context); 331 | if (result.status.IsPending) 332 | { 333 | scopeSession.Session.CompletePending(true); 334 | context.FinalizeRead(out result.status, out result.output); 335 | } 336 | 337 | return result.output; 338 | } 339 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Guards.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace FasterKv.Cache.Core; 6 | 7 | public static class Guards 8 | { 9 | /// 10 | /// ArgumentNotNull 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static T ArgumentNotNull( 19 | [NotNull] 20 | this T? obj, 21 | [CallerArgumentExpression(nameof(obj))] 22 | string? name = null) 23 | => obj ?? throw new ArgumentNullException(name); 24 | 25 | /// 26 | /// ArgumentNotNull 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static string ArgumentNotNullOrEmpty( 35 | [NotNull] 36 | this string? obj, 37 | [CallerArgumentExpression(nameof(obj))] 38 | string? name = null) 39 | => obj.IsNullOrEmpty() ? throw new ArgumentNullException(name) : obj!; 40 | 41 | /// 42 | /// IsNullOrEmpty 43 | /// 44 | /// 45 | /// 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static bool IsNullOrEmpty( 48 | [NotNullWhen(false)] 49 | this string? str) 50 | => string.IsNullOrEmpty(str); 51 | 52 | /// 53 | /// NotNullOrEmpty 54 | /// 55 | /// 56 | /// 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public static bool NotNullOrEmpty( 59 | [NotNullWhen(true)] 60 | this string? str) 61 | => !string.IsNullOrEmpty(str); 62 | 63 | /// 64 | /// Argument Not Negative Or Zero 65 | /// 66 | /// 67 | /// 68 | /// 69 | /// 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | public static TimeSpan ArgumentNotNegativeOrZero(this TimeSpan timeSpan, [CallerArgumentExpression(nameof(timeSpan))]string? name = null) => 72 | timeSpan > TimeSpan.Zero ? timeSpan : throw new ArgumentOutOfRangeException(name); 73 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Serializers/FasterKvSerializer.TValue.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using FASTER.core; 3 | using FasterKv.Cache.Core.Abstractions; 4 | 5 | namespace FasterKv.Cache.Core.Serializers; 6 | 7 | internal sealed class FasterKvSerializer : BinaryObjectSerializer> 8 | { 9 | private readonly ISystemClock _systemClock; 10 | private readonly IFasterKvCacheSerializer _serializer; 11 | 12 | public FasterKvSerializer(IFasterKvCacheSerializer serializer, ISystemClock systemClock) 13 | { 14 | _serializer = serializer.ArgumentNotNull(); 15 | _systemClock = systemClock.ArgumentNotNull(); 16 | } 17 | 18 | public override void Deserialize(out ValueWrapper obj) 19 | { 20 | obj = new ValueWrapper(); 21 | var flags = (FasterKvSerializerFlags)reader.ReadByte(); 22 | if ((flags & FasterKvSerializerFlags.HasExpiryTime) == FasterKvSerializerFlags.HasExpiryTime) 23 | { 24 | obj.ExpiryTime = reader.ReadInt64(); 25 | } 26 | 27 | if ((flags & FasterKvSerializerFlags.HasBody) == FasterKvSerializerFlags.HasBody) 28 | { 29 | var dataLength = reader.ReadInt32(); 30 | if (obj.HasExpired(_systemClock.NowUnixTimestamp())) 31 | { 32 | reader.BaseStream.Position += dataLength; 33 | } 34 | else 35 | { 36 | var buffer = ArrayPool.Shared.Rent(dataLength); 37 | try 38 | { 39 | _ = reader.Read(buffer, 0, dataLength); 40 | obj.Data = _serializer.Deserialize(buffer, dataLength); 41 | } 42 | finally 43 | { 44 | ArrayPool.Shared.Return(buffer); 45 | } 46 | } 47 | } 48 | 49 | } 50 | 51 | public override void Serialize(ref ValueWrapper obj) 52 | { 53 | var flags = obj.GetFlags(_systemClock.NowUnixTimestamp()); 54 | writer.Write((byte)flags); 55 | if ((flags & FasterKvSerializerFlags.HasExpiryTime) == FasterKvSerializerFlags.HasExpiryTime) 56 | { 57 | writer.Write(obj.ExpiryTime!.Value); 58 | } 59 | 60 | if ((flags & FasterKvSerializerFlags.HasBody) == FasterKvSerializerFlags.HasBody) 61 | { 62 | var beforePos = writer.BaseStream.Position; 63 | var dataPos = writer.BaseStream.Position = writer.BaseStream.Position += sizeof(int); 64 | _serializer.Serialize(writer.BaseStream, obj.Data); 65 | var afterPos = writer.BaseStream.Position; 66 | 67 | var length = (int)(afterPos - dataPos); 68 | writer.BaseStream.Position = beforePos; 69 | 70 | writer.Write(length); 71 | writer.BaseStream.Position = afterPos; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Serializers/FasterKvSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using FASTER.core; 4 | using FasterKv.Cache.Core.Abstractions; 5 | 6 | namespace FasterKv.Cache.Core.Serializers; 7 | 8 | internal sealed class FasterKvSerializer : BinaryObjectSerializer 9 | { 10 | private readonly ISystemClock _systemClock; 11 | private readonly IFasterKvCacheSerializer _serializer; 12 | 13 | public FasterKvSerializer(IFasterKvCacheSerializer serializer, ISystemClock systemClock) 14 | { 15 | _systemClock = systemClock.ArgumentNotNull(); 16 | _serializer = serializer.ArgumentNotNull(); 17 | } 18 | 19 | public override void Deserialize(out ValueWrapper obj) 20 | { 21 | obj = new ValueWrapper(); 22 | var flags = (FasterKvSerializerFlags)reader.ReadByte(); 23 | if ((flags & FasterKvSerializerFlags.HasExpiryTime) == FasterKvSerializerFlags.HasExpiryTime) 24 | { 25 | obj.ExpiryTime = reader.ReadInt64(); 26 | } 27 | 28 | if ((flags & FasterKvSerializerFlags.HasBody) == FasterKvSerializerFlags.HasBody) 29 | { 30 | obj.DataByteLength = reader.ReadInt32(); 31 | if (obj.HasExpired(_systemClock.NowUnixTimestamp())) 32 | { 33 | reader.BaseStream.Position += obj.DataByteLength; 34 | obj.DataByteLength = 0; 35 | } 36 | else 37 | { 38 | obj.DataBytes = ArrayPool.Shared.Rent(obj.DataByteLength); 39 | _ = reader.Read(obj.DataBytes, 0, obj.DataByteLength); 40 | } 41 | } 42 | } 43 | 44 | public override void Serialize(ref ValueWrapper obj) 45 | { 46 | var flags = obj.GetFlags(_systemClock.NowUnixTimestamp()); 47 | writer.Write((byte)flags); 48 | if ((flags & FasterKvSerializerFlags.HasExpiryTime) == FasterKvSerializerFlags.HasExpiryTime) 49 | { 50 | writer.Write(obj.ExpiryTime!.Value); 51 | } 52 | 53 | if ((flags & FasterKvSerializerFlags.HasBody) == FasterKvSerializerFlags.HasBody) 54 | { 55 | var beforePos = writer.BaseStream.Position; 56 | var dataPos = writer.BaseStream.Position = writer.BaseStream.Position += sizeof(int); 57 | _serializer.Serialize(writer.BaseStream, obj.Data); 58 | var afterPos = writer.BaseStream.Position; 59 | 60 | var length = (int)(afterPos - dataPos); 61 | writer.BaseStream.Position = beforePos; 62 | 63 | writer.Write(length); 64 | writer.BaseStream.Position = afterPos; 65 | } 66 | } 67 | } 68 | 69 | [Flags] 70 | internal enum FasterKvSerializerFlags : byte 71 | { 72 | None = 0, 73 | HasExpiryTime = 1 << 0, 74 | HasBody = 1 << 1 75 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.Core/Serializers/StringSerializer.cs: -------------------------------------------------------------------------------- 1 | using FASTER.core; 2 | 3 | namespace FasterKv.Cache.Core.Serializers; 4 | 5 | internal sealed class StringSerializer : BinaryObjectSerializer 6 | { 7 | public override void Deserialize(out string obj) 8 | { 9 | obj = reader.ReadString(); 10 | } 11 | 12 | public override void Serialize(ref string obj) 13 | { 14 | writer.Write(obj); 15 | } 16 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.MessagePack/FasterKv.Cache.MessagePack.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/FasterKv.Cache.MessagePack/FasterKvCacheOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Configurations; 3 | 4 | namespace FasterKv.Cache.MessagePack; 5 | 6 | public static class FasterKvCacheOptionsExtensions 7 | { 8 | /// 9 | /// Adds the FasterKv Cache Message Pack Serializer(specify the config via hard code). 10 | /// 11 | public static FasterKvCacheOptions UseMessagePackSerializer( 12 | this FasterKvCacheOptions options 13 | ) 14 | { 15 | options.ArgumentNotNull(); 16 | 17 | options.RegisterExtension(new MessagePackFasterKvCacheSerializerExtensionOptions()); 18 | return options; 19 | } 20 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.MessagePack/MessagePackFasterKvCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO; 3 | using FasterKv.Cache.Core; 4 | using MessagePack; 5 | 6 | namespace FasterKv.Cache.MessagePack; 7 | 8 | public sealed class MessagePackFasterKvCacheSerializer : IFasterKvCacheSerializer 9 | { 10 | public string Name { get; set; } = "MessagePack"; 11 | 12 | public void Serialize(Stream stream, TValue data) 13 | { 14 | MessagePackSerializer.Serialize(stream, data); 15 | } 16 | 17 | public TValue? Deserialize(byte[] serializerData, int length) 18 | { 19 | return MessagePackSerializer.Deserialize(new ReadOnlySequence(serializerData, 0, length)); 20 | } 21 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.MessagePack/MessagePackFasterKvCacheSerializerExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FasterKv.Cache.MessagePack; 6 | 7 | public sealed class MessagePackFasterKvCacheSerializerExtensionOptions : IFasterKvCacheExtensionOptions 8 | { 9 | public void AddServices(IServiceCollection services, string name) 10 | { 11 | services.ArgumentNotNull(); 12 | name.ArgumentNotNullOrEmpty(); 13 | 14 | services.AddSingleton(_ => new MessagePackFasterKvCacheSerializer {Name = name}); 15 | } 16 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.SystemTextJson/FasterKv.Cache.SystemTextJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FasterKv.Cache.SystemTextJson/FasterKvCacheOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Configurations; 3 | 4 | namespace FasterKv.Cache.SystemTextJson; 5 | 6 | public static class FasterKvCacheOptionsExtensions 7 | { 8 | /// 9 | /// Adds the FasterKv Cache System.Text.Json Serializer(specify the config via hard code). 10 | /// 11 | public static FasterKvCacheOptions UseSystemTextJsonSerializer( 12 | this FasterKvCacheOptions options 13 | ) 14 | { 15 | options.ArgumentNotNull(); 16 | 17 | options.RegisterExtension(new SystemTextJsonFasterKvCacheSerializerExtensionOptions()); 18 | return options; 19 | } 20 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.SystemTextJson/SystemTextJsonFasterKvCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using FasterKv.Cache.Core; 4 | 5 | namespace FasterKv.Cache.SystemTextJson; 6 | 7 | public sealed class SystemTextJsonFasterKvCacheSerializer : IFasterKvCacheSerializer 8 | { 9 | public string Name { get; set; } = "SystemTextJson"; 10 | 11 | public void Serialize(Stream stream, TValue data) 12 | { 13 | System.Text.Json.JsonSerializer.Serialize(stream, data); 14 | } 15 | 16 | public TValue? Deserialize(byte[] serializerData, int length) 17 | { 18 | return System.Text.Json.JsonSerializer.Deserialize(new Span(serializerData,0, length)); 19 | } 20 | } -------------------------------------------------------------------------------- /src/FasterKv.Cache.SystemTextJson/SystemTextJsonFasterKvCacheSerializerExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FasterKv.Cache.SystemTextJson; 6 | 7 | public sealed class SystemTextJsonFasterKvCacheSerializerExtensionOptions : IFasterKvCacheExtensionOptions 8 | { 9 | public void AddServices(IServiceCollection services, string name) 10 | { 11 | services.ArgumentNotNull(); 12 | name.ArgumentNotNullOrEmpty(); 13 | 14 | services.AddSingleton(_ => new SystemTextJsonFasterKvCacheSerializer{Name = name}); 15 | } 16 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/DependencyInjection/FasterKvCacheDITest.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Configurations; 2 | using FasterKv.Cache.Core.Tests.KvStore; 3 | using FasterKv.Cache.MessagePack; 4 | using FasterKv.Cache.SystemTextJson; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FasterKv.Cache.Core.Tests.DependencyInjection; 8 | 9 | public class FasterKvCacheDiTest 10 | { 11 | [Fact] 12 | public void Use_MessagePackSerializer_Create_FasterKvCache_Should_Success() 13 | { 14 | var services = new ServiceCollection(); 15 | services.AddFasterKvCache(options => 16 | { 17 | // use MessagePack serializer 18 | options.UseMessagePackSerializer(); 19 | }, "MyKvCache"); 20 | var provider = services.BuildServiceProvider(); 21 | 22 | var cache = provider.GetService(); 23 | Assert.NotNull(cache); 24 | 25 | cache!.Set("abc", "abc"); 26 | var result = cache.Get("abc"); 27 | Assert.Equal("abc", result); 28 | } 29 | 30 | [Fact] 31 | public void Use_MessagePackSerializer_Create_FasterKvCacheTValue_Should_Success() 32 | { 33 | var services = new ServiceCollection(); 34 | services.AddFasterKvCache(options => { options.UseMessagePackSerializer(); }, "MyKvCache"); 35 | var provider = services.BuildServiceProvider(); 36 | 37 | var cache = provider.GetService>(); 38 | Assert.NotNull(cache); 39 | 40 | var data = new Data 41 | { 42 | One = "1024", 43 | Two = 1024 44 | }; 45 | cache!.Set("abc", data); 46 | var result = cache.Get("abc"); 47 | Assert.Equal(data, result); 48 | } 49 | 50 | [Fact] 51 | public void Use_SystemTextJson_Create_FasterKvCache_Should_Success() 52 | { 53 | var services = new ServiceCollection(); 54 | services.AddFasterKvCache(options => { options.UseSystemTextJsonSerializer(); }, "MyKvCache"); 55 | var provider = services.BuildServiceProvider(); 56 | 57 | var cache = provider.GetService(); 58 | Assert.NotNull(cache); 59 | 60 | cache!.Set("abc", "abc"); 61 | var result = cache.Get("abc"); 62 | Assert.Equal("abc", result); 63 | } 64 | 65 | [Fact] 66 | public void Use_SystemTextJson_Create_FasterKvCacheTValue_Should_Success() 67 | { 68 | var services = new ServiceCollection(); 69 | services.AddFasterKvCache(options => { options.UseSystemTextJsonSerializer(); }, "MyKvCache"); 70 | var provider = services.BuildServiceProvider(); 71 | 72 | var cache = provider.GetService>(); 73 | Assert.NotNull(cache); 74 | 75 | var data = new Data 76 | { 77 | One = "1024", 78 | Two = 1024 79 | }; 80 | cache!.Set("abc", data); 81 | var result = cache.Get("abc"); 82 | Assert.Equal(data, result); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/FasterKv.Cache.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | true 9 | 11 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/DeleteFileOnClose/DeleteOnCloseTest.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | 5 | namespace FasterKv.Cache.Core.Tests.KvStore.DeleteFileOnClose; 6 | 7 | public class DeleteOnCloseTest 8 | { 9 | private string GetPath() 10 | { 11 | var guid = Guid.NewGuid().ToString("N"); 12 | return $"./unit-test/faster-kv-store-delete-on-close-test/{guid}/log"; 13 | } 14 | 15 | [Fact] 16 | public void Should_Not_Delete_On_Close() 17 | { 18 | var path = GetPath(); 19 | var fasterKv = new FasterKvCache(null!, 20 | new DefaultSystemClock(), 21 | new FasterKvCacheOptions 22 | { 23 | SerializerName = "MessagePack", 24 | ExpiryKeyScanInterval = TimeSpan.Zero, 25 | IndexCount = 16384, 26 | MemorySizeBit = 10, 27 | PageSizeBit = 10, 28 | ReadCacheMemorySizeBit = 10, 29 | ReadCachePageSizeBit = 10, 30 | PreallocateFile = false, 31 | DeleteFileOnClose = false, 32 | LogPath = path 33 | }, 34 | new IFasterKvCacheSerializer[] 35 | { 36 | new MessagePackFasterKvCacheSerializer 37 | { 38 | Name = "MessagePack" 39 | } 40 | }, 41 | null); 42 | 43 | fasterKv.Set("key", "value"); 44 | 45 | Assert.Equal("value", fasterKv.Get("key")); 46 | 47 | fasterKv.Dispose(); 48 | 49 | Assert.True(File.Exists($"{path}.log.0")); 50 | Assert.True(File.Exists($"{path}.obj.log.0")); 51 | 52 | Cleanup(path); 53 | } 54 | 55 | [Fact] 56 | public void Should_Restore_The_Data() 57 | { 58 | var path = GetPath(); 59 | var fasterKv = new FasterKvCache(null!, 60 | new DefaultSystemClock(), 61 | new FasterKvCacheOptions 62 | { 63 | SerializerName = "MessagePack", 64 | ExpiryKeyScanInterval = TimeSpan.Zero, 65 | IndexCount = 16384, 66 | MemorySizeBit = 10, 67 | PageSizeBit = 10, 68 | ReadCacheMemorySizeBit = 10, 69 | ReadCachePageSizeBit = 10, 70 | PreallocateFile = false, 71 | DeleteFileOnClose = false, 72 | LogPath = path 73 | }, 74 | new IFasterKvCacheSerializer[] 75 | { 76 | new MessagePackFasterKvCacheSerializer 77 | { 78 | Name = "MessagePack" 79 | } 80 | }, 81 | null); 82 | 83 | for (int i = 0; i < 100; i++) 84 | { 85 | fasterKv.Set($"key{i}", $"value{i}"); 86 | } 87 | 88 | Assert.Equal("value0", fasterKv.Get("key0")); 89 | 90 | fasterKv.Dispose(); 91 | 92 | Assert.True(File.Exists($"{path}.log.0")); 93 | Assert.True(File.Exists($"{path}.obj.log.0")); 94 | 95 | fasterKv = new FasterKvCache(null!, 96 | new DefaultSystemClock(), 97 | new FasterKvCacheOptions 98 | { 99 | SerializerName = "MessagePack", 100 | ExpiryKeyScanInterval = TimeSpan.Zero, 101 | IndexCount = 16384, 102 | MemorySizeBit = 10, 103 | PageSizeBit = 10, 104 | ReadCacheMemorySizeBit = 10, 105 | ReadCachePageSizeBit = 10, 106 | PreallocateFile = false, 107 | DeleteFileOnClose = false, 108 | TryRecoverLatest = true, 109 | LogPath = path 110 | }, 111 | new IFasterKvCacheSerializer[] 112 | { 113 | new MessagePackFasterKvCacheSerializer 114 | { 115 | Name = "MessagePack" 116 | } 117 | }, 118 | null); 119 | 120 | for (int i = 0; i < 100; i++) 121 | { 122 | Assert.Equal($"value{i}", fasterKv.Get($"key{i}")); 123 | } 124 | 125 | fasterKv.Dispose(); 126 | 127 | Cleanup(path); 128 | } 129 | 130 | private static void Cleanup(string path) 131 | { 132 | var dir = Path.GetDirectoryName(path); 133 | if (Directory.Exists(dir)) 134 | { 135 | Directory.Delete(dir, true); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/DeleteFileOnClose/DeleteOnCloseTestObject.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | 5 | namespace FasterKv.Cache.Core.Tests.KvStore.DeleteFileOnClose; 6 | 7 | public class DeleteOnCloseTestObject 8 | { 9 | private string GetPath() 10 | { 11 | var guid = Guid.NewGuid().ToString("N"); 12 | return $"./unit-test/faster-kv-store-delete-on-close-object-test/{guid}/log"; 13 | } 14 | 15 | [Fact] 16 | public void Should_Not_Delete_On_Close() 17 | { 18 | var path = GetPath(); 19 | var fasterKv = new FasterKvCache(null!, 20 | new DefaultSystemClock(), 21 | new FasterKvCacheOptions 22 | { 23 | SerializerName = "MessagePack", 24 | ExpiryKeyScanInterval = TimeSpan.Zero, 25 | IndexCount = 16384, 26 | MemorySizeBit = 10, 27 | PageSizeBit = 10, 28 | ReadCacheMemorySizeBit = 10, 29 | ReadCachePageSizeBit = 10, 30 | PreallocateFile = false, 31 | DeleteFileOnClose = false, 32 | LogPath = path 33 | }, 34 | new IFasterKvCacheSerializer[] 35 | { 36 | new MessagePackFasterKvCacheSerializer 37 | { 38 | Name = "MessagePack" 39 | } 40 | }, 41 | null); 42 | 43 | fasterKv.Set("key", "value"); 44 | 45 | Assert.Equal("value", fasterKv.Get("key")); 46 | 47 | fasterKv.Dispose(); 48 | 49 | Assert.True(File.Exists($"{path}.log.0")); 50 | Assert.True(File.Exists($"{path}.obj.log.0")); 51 | 52 | Cleanup(path); 53 | } 54 | 55 | [Fact] 56 | public void Should_Restore_The_Data() 57 | { 58 | var path = GetPath(); 59 | var fasterKv = new FasterKvCache(null!, 60 | new DefaultSystemClock(), 61 | new FasterKvCacheOptions 62 | { 63 | SerializerName = "MessagePack", 64 | ExpiryKeyScanInterval = TimeSpan.Zero, 65 | IndexCount = 16384, 66 | MemorySizeBit = 10, 67 | PageSizeBit = 10, 68 | ReadCacheMemorySizeBit = 10, 69 | ReadCachePageSizeBit = 10, 70 | PreallocateFile = false, 71 | DeleteFileOnClose = false, 72 | LogPath = path 73 | }, 74 | new IFasterKvCacheSerializer[] 75 | { 76 | new MessagePackFasterKvCacheSerializer 77 | { 78 | Name = "MessagePack" 79 | } 80 | }, 81 | null); 82 | 83 | for (int i = 0; i < 100; i++) 84 | { 85 | fasterKv.Set($"key{i}", $"value{i}"); 86 | } 87 | 88 | Assert.Equal("value0", fasterKv.Get("key0")); 89 | 90 | fasterKv.Dispose(); 91 | 92 | Assert.True(File.Exists($"{path}.log.0")); 93 | Assert.True(File.Exists($"{path}.obj.log.0")); 94 | 95 | fasterKv = new FasterKvCache(null!, 96 | new DefaultSystemClock(), 97 | new FasterKvCacheOptions 98 | { 99 | SerializerName = "MessagePack", 100 | ExpiryKeyScanInterval = TimeSpan.Zero, 101 | IndexCount = 16384, 102 | MemorySizeBit = 10, 103 | PageSizeBit = 10, 104 | ReadCacheMemorySizeBit = 10, 105 | ReadCachePageSizeBit = 10, 106 | PreallocateFile = false, 107 | DeleteFileOnClose = false, 108 | TryRecoverLatest = true, 109 | LogPath = path 110 | }, 111 | new IFasterKvCacheSerializer[] 112 | { 113 | new MessagePackFasterKvCacheSerializer 114 | { 115 | Name = "MessagePack" 116 | } 117 | }, 118 | null); 119 | 120 | for (int i = 0; i < 100; i++) 121 | { 122 | Assert.Equal($"value{i}", fasterKv.Get($"key{i}")); 123 | } 124 | 125 | fasterKv.Dispose(); 126 | 127 | Cleanup(path); 128 | } 129 | 130 | private static void Cleanup(string path) 131 | { 132 | var dir = Path.GetDirectoryName(path); 133 | if (Directory.Exists(dir)) 134 | { 135 | Directory.Delete(dir, true); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreObjectTest.GetOrAdd.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | 5 | namespace FasterKv.Cache.Core.Tests.KvStore; 6 | 7 | public class FasterKvStoreObjectTestGetOrAdd 8 | { 9 | private static FasterKvCache CreateKvStore(string guid, ISystemClock? systemClock = null) 10 | { 11 | return new FasterKvCache(null!, 12 | systemClock ?? new DefaultSystemClock(), 13 | new FasterKvCacheOptions 14 | { 15 | IndexCount = 16384, 16 | MemorySizeBit = 10, 17 | PageSizeBit = 10, 18 | ReadCacheMemorySizeBit = 10, 19 | ReadCachePageSizeBit = 10, 20 | SerializerName = "MessagePack", 21 | ExpiryKeyScanInterval = TimeSpan.FromSeconds(1), 22 | LogPath = $"./unit-test/{guid}" 23 | }, 24 | new IFasterKvCacheSerializer[] 25 | { 26 | new MessagePackFasterKvCacheSerializer 27 | { 28 | Name = "MessagePack" 29 | } 30 | }, 31 | null); 32 | } 33 | 34 | [Fact] 35 | public void GetOrAdd_Should_Return_Existing_Value() 36 | { 37 | 38 | var guid = Guid.NewGuid().ToString("N"); 39 | using var fasterKv = CreateKvStore(guid); 40 | 41 | var data = new Data 42 | { 43 | One = "one", 44 | Two = 2 45 | }; 46 | fasterKv.Set(guid, data); 47 | 48 | var result = fasterKv.GetOrAdd(guid, _ => new Data 49 | { 50 | One = "two", 51 | Two = 3 52 | }); 53 | 54 | Assert.Equal(data, result); 55 | } 56 | 57 | [Fact] 58 | public void GetOrAdd_Should_Return_New_Value() 59 | { 60 | var guid = Guid.NewGuid().ToString("N"); 61 | using var fasterKv = CreateKvStore(guid); 62 | 63 | var result = fasterKv.GetOrAdd(guid, (_) => new Data 64 | { 65 | One = "two", 66 | Two = 3 67 | }); 68 | 69 | Assert.Equal("two", result.One); 70 | Assert.Equal(3, result.Two); 71 | } 72 | 73 | [Fact] 74 | public void GetOrAdd_Should_Return_NewValue_When_Expired() 75 | { 76 | var guid = Guid.NewGuid().ToString("N"); 77 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 78 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 79 | 80 | var data = new Data 81 | { 82 | One = "one", 83 | Two = 2 84 | }; 85 | fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); 86 | 87 | mockSystemClock.AddSeconds(2); 88 | 89 | var result = fasterKv.GetOrAdd(guid, _ => new Data 90 | { 91 | One = "two", 92 | Two = 3 93 | }); 94 | 95 | Assert.Equal("two", result.One); 96 | Assert.Equal(3, result.Two); 97 | } 98 | 99 | [Fact] 100 | public void GetOrAdd_Should_Return_NewValue_When_Expired_And_Refresh() 101 | { 102 | var guid = Guid.NewGuid().ToString("N"); 103 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 104 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 105 | 106 | var result = fasterKv.GetOrAdd(guid, _ => new Data() 107 | { 108 | One = "one", 109 | Two = 2 110 | }, TimeSpan.FromSeconds(1)); 111 | 112 | Assert.Equal("one", result.One); 113 | Assert.Equal(2, result.Two); 114 | 115 | mockSystemClock.AddSeconds(2); 116 | 117 | result = fasterKv.GetOrAdd(guid, _ => new Data 118 | { 119 | One = "two", 120 | Two = 3 121 | }, TimeSpan.FromSeconds(1)); 122 | 123 | Assert.Equal("two", result.One); 124 | Assert.Equal(3, result.Two); 125 | } 126 | 127 | // below test GetOrAddAsync 128 | 129 | [Fact] 130 | public async Task GetOrAddAsync_Should_Return_Existing_Value() 131 | { 132 | 133 | var guid = Guid.NewGuid().ToString("N"); 134 | using var fasterKv = CreateKvStore(guid); 135 | 136 | var data = new Data 137 | { 138 | One = "one", 139 | Two = 2 140 | }; 141 | fasterKv.Set(guid, data); 142 | 143 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 144 | { 145 | One = "two", 146 | Two = 3 147 | })); 148 | 149 | Assert.Equal(data, result); 150 | } 151 | 152 | [Fact] 153 | public async Task GetOrAddAsync_Should_Return_New_Value() 154 | { 155 | var guid = Guid.NewGuid().ToString("N"); 156 | using var fasterKv = CreateKvStore(guid); 157 | 158 | var result = await fasterKv.GetOrAddAsync(guid, (_) => Task.FromResult(new Data 159 | { 160 | One = "two", 161 | Two = 3 162 | })); 163 | 164 | Assert.Equal("two", result.One); 165 | Assert.Equal(3, result.Two); 166 | } 167 | 168 | [Fact] 169 | public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired() 170 | { 171 | var guid = Guid.NewGuid().ToString("N"); 172 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 173 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 174 | 175 | var data = new Data 176 | { 177 | One = "one", 178 | Two = 2 179 | }; 180 | fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); 181 | 182 | mockSystemClock.AddSeconds(2); 183 | 184 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 185 | { 186 | One = "two", 187 | Two = 3 188 | })); 189 | 190 | Assert.Equal("two", result.One); 191 | Assert.Equal(3, result.Two); 192 | } 193 | 194 | [Fact] 195 | public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired_And_Refresh() 196 | { 197 | var guid = Guid.NewGuid().ToString("N"); 198 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 199 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 200 | 201 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data() 202 | { 203 | One = "one", 204 | Two = 2 205 | }), TimeSpan.FromSeconds(1)); 206 | 207 | Assert.Equal("one", result.One); 208 | Assert.Equal(2, result.Two); 209 | 210 | mockSystemClock.AddSeconds(2); 211 | 212 | result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 213 | { 214 | One = "two", 215 | Two = 3 216 | }), TimeSpan.FromSeconds(1)); 217 | 218 | Assert.Equal("two", result.One); 219 | Assert.Equal(3, result.Two); 220 | } 221 | } 222 | 223 | -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreObjectTest.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | using FasterKv.Cache.SystemTextJson; 5 | 6 | namespace FasterKv.Cache.Core.Tests.KvStore; 7 | 8 | public class FasterKvStoreObjectTest : IDisposable 9 | { 10 | private readonly FasterKvCache _fasterKv; 11 | 12 | private readonly Data _data = new() 13 | { 14 | One = "one", 15 | Two = 2 16 | }; 17 | 18 | public FasterKvStoreObjectTest() 19 | { 20 | _fasterKv = CreateKvStore(); 21 | } 22 | 23 | private static FasterKvCache CreateKvStore() 24 | { 25 | return new FasterKvCache(null!, 26 | new DefaultSystemClock(), 27 | new FasterKvCacheOptions 28 | { 29 | SerializerName = "MessagePack", 30 | ExpiryKeyScanInterval = TimeSpan.Zero, 31 | IndexCount = 16384, 32 | MemorySizeBit = 10, 33 | PageSizeBit = 10, 34 | ReadCacheMemorySizeBit = 10, 35 | ReadCachePageSizeBit = 10, 36 | LogPath = "./unit-test/faster-kv-store-object-test" 37 | }, 38 | new IFasterKvCacheSerializer[] 39 | { 40 | new MessagePackFasterKvCacheSerializer 41 | { 42 | Name = "MessagePack" 43 | }, 44 | new SystemTextJsonFasterKvCacheSerializer 45 | { 46 | Name = "SystemTextJson" 47 | } 48 | }, 49 | null); 50 | } 51 | 52 | [Fact] 53 | public void Set_Null_Value_Should_Get_Null_Value() 54 | { 55 | var guid = Guid.NewGuid().ToString("N"); 56 | _fasterKv.Set(guid, null); 57 | 58 | var result = _fasterKv.Get(guid); 59 | Assert.Null(result); 60 | } 61 | 62 | [Fact] 63 | public void Set_Key_Should_Success() 64 | { 65 | var guid = Guid.NewGuid().ToString("N"); 66 | _fasterKv.Set(guid, _data); 67 | 68 | var result = _fasterKv.Get(guid); 69 | 70 | Assert.Equal(_data, result); 71 | } 72 | 73 | [Fact] 74 | public void Set_Key_With_ExpiryTime_Should_Success() 75 | { 76 | var guid = Guid.NewGuid().ToString("N"); 77 | _fasterKv.Set(guid, _data, TimeSpan.FromMinutes(1)); 78 | 79 | var result = _fasterKv.Get(guid); 80 | 81 | Assert.Equal(_data, result); 82 | } 83 | 84 | 85 | [Fact] 86 | public void Get_Not_Exist_Key_Should_Return_Null() 87 | { 88 | var guid = Guid.NewGuid().ToString("N"); 89 | var result = _fasterKv.Get(guid); 90 | Assert.Null(result); 91 | } 92 | 93 | [Fact] 94 | public void Delete_Key_Should_Success() 95 | { 96 | var guid = Guid.NewGuid().ToString("N"); 97 | _fasterKv.Set(guid, _data); 98 | _fasterKv.Delete(guid); 99 | 100 | var result = _fasterKv.Get(guid); 101 | Assert.Null(result); 102 | } 103 | 104 | [Fact] 105 | public async Task SetAsync_Null_Value_Should_Get_Null_Value() 106 | { 107 | var guid = Guid.NewGuid().ToString("N"); 108 | await _fasterKv.SetAsync(guid, null); 109 | 110 | var result = await _fasterKv.GetAsync(guid); 111 | Assert.Null(result); 112 | } 113 | 114 | [Fact] 115 | public async Task SetAsync_Key_Should_Success() 116 | { 117 | var guid = Guid.NewGuid().ToString("N"); 118 | await _fasterKv.SetAsync(guid, _data); 119 | 120 | var result = await _fasterKv.GetAsync(guid); 121 | 122 | Assert.Equal(_data, result); 123 | } 124 | 125 | [Fact] 126 | public async Task SetAsync_Key_With_ExpiryTime_Should_Success() 127 | { 128 | var guid = Guid.NewGuid().ToString("N"); 129 | await _fasterKv.SetAsync(guid, _data, TimeSpan.FromMinutes(1)); 130 | 131 | var result = await _fasterKv.GetAsync(guid); 132 | 133 | Assert.Equal(_data, result); 134 | } 135 | 136 | 137 | [Fact] 138 | public async Task GetAsync_Not_Exist_Key_Should_Return_Null() 139 | { 140 | var guid = Guid.NewGuid().ToString("N"); 141 | var result = await _fasterKv.GetAsync(guid); 142 | Assert.Null(result); 143 | } 144 | 145 | [Fact] 146 | public async Task DeleteAsync_Key_Should_Success() 147 | { 148 | var guid = Guid.NewGuid().ToString("N"); 149 | await _fasterKv.SetAsync(guid, _data); 150 | await _fasterKv.DeleteAsync(guid); 151 | 152 | var result = await _fasterKv.GetAsync(guid); 153 | Assert.Null(result); 154 | } 155 | 156 | [Fact] 157 | public void Set_Big_DataSize_Should_Success() 158 | { 159 | int nums = 1000; 160 | for (int i = 0; i < nums; i++) 161 | { 162 | _fasterKv.Set($"big_data_{i}", new Data 163 | { 164 | One = i.ToString(), 165 | Two = i 166 | }); 167 | } 168 | 169 | for (int i = 0; i < nums; i++) 170 | { 171 | var value = _fasterKv.Get($"big_data_{i}"); 172 | Assert.NotNull(value); 173 | Assert.Equal(i.ToString(), value!.One); 174 | Assert.Equal(i, value.Two); 175 | } 176 | } 177 | 178 | [Fact] 179 | public void Set_Big_DataSize_With_ExpiryTime_Should_Success() 180 | { 181 | int nums = 1000; 182 | for (int i = 0; i < nums; i++) 183 | { 184 | _fasterKv.Set($"big_data_{i}", new Data 185 | { 186 | One = i.ToString(), 187 | Two = i 188 | }, TimeSpan.FromMinutes(5)); 189 | } 190 | 191 | for (int i = 0; i < nums; i++) 192 | { 193 | var value = _fasterKv.Get($"big_data_{i}"); 194 | Assert.NotNull(value); 195 | Assert.Equal(i.ToString(), value!.One); 196 | Assert.Equal(i, value.Two); 197 | } 198 | } 199 | 200 | [Fact] 201 | public void Set_Big_DataSize_And_Repeat_Reading_Should_Success() 202 | { 203 | int nums = 1000; 204 | for (int i = 0; i < nums; i++) 205 | { 206 | _fasterKv.Set($"big_data_{i}", new Data 207 | { 208 | One = i.ToString(), 209 | Two = i 210 | }); 211 | } 212 | 213 | var value = _fasterKv.Get($"big_data_{0}"); 214 | Assert.NotNull(value); 215 | Assert.Equal(0.ToString(), value!.One); 216 | Assert.Equal(0, value.Two); 217 | 218 | 219 | value = _fasterKv.Get($"big_data_{0}"); 220 | Assert.NotNull(value); 221 | Assert.Equal(0.ToString(), value!.One); 222 | Assert.Equal(0, value.Two); 223 | } 224 | 225 | [Fact] 226 | public void Set_Big_Value_Should_Success() 227 | { 228 | // 8MB Value 229 | var bigValues = Enumerable.Range(0, 8 * 1024 * 1024).Select(i => (byte) i).ToArray(); 230 | int nums = 200; 231 | for (int i = 0; i < nums; i++) 232 | { 233 | _fasterKv.Set($"big_value_{i}", bigValues); 234 | } 235 | 236 | for (int i = 0; i < nums; i++) 237 | { 238 | var result = _fasterKv.Get($"big_value_{i}"); 239 | 240 | Assert.NotNull(result); 241 | Assert.True(bigValues.SequenceEqual(result!)); 242 | } 243 | } 244 | 245 | [Fact] 246 | public void Set_Big_DataSize_With_Expired_Should_Return_Null() 247 | { 248 | int nums = 1000; 249 | for (int i = 0; i < nums; i++) 250 | { 251 | _fasterKv.Set($"Set_Big_DataSize_With_Expired_Should_Return_Null_{i}", new Data 252 | { 253 | One = i.ToString(), 254 | Two = i 255 | }, TimeSpan.FromSeconds(1)); 256 | } 257 | 258 | Thread.Sleep(1000); 259 | 260 | for (int i = 0; i < nums; i++) 261 | { 262 | var value = _fasterKv.Get($"Set_Big_DataSize_With_Expired_Should_Return_Null_{i}"); 263 | Assert.Null(value); 264 | } 265 | } 266 | 267 | [Fact] 268 | public void Set_Big_DataSize_With_Random_Expired_Should_Success() 269 | { 270 | int nums = 1000; 271 | for (int i = 0; i < nums; i++) 272 | { 273 | _fasterKv.Set($"Set_Big_DataSize_With_Random_Expired_Should_Success_{i}", new Data 274 | { 275 | One = i.ToString(), 276 | Two = i 277 | }, i % 2 == 0 ? TimeSpan.FromSeconds(1) : TimeSpan.FromMinutes(1)); 278 | } 279 | 280 | Thread.Sleep(1000); 281 | 282 | for (int i = 0; i < nums; i++) 283 | { 284 | var value = _fasterKv.Get($"Set_Big_DataSize_With_Random_Expired_Should_Success_{i}"); 285 | if (i % 2 == 0) 286 | { 287 | Assert.Null(value); 288 | } 289 | else 290 | { 291 | Assert.NotNull(value); 292 | Assert.NotNull(value); 293 | Assert.Equal(i.ToString(), value!.One); 294 | Assert.Equal(i, value.Two); 295 | } 296 | } 297 | } 298 | 299 | public void Dispose() 300 | { 301 | _fasterKv.Dispose(); 302 | } 303 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.Expiry.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | 5 | namespace FasterKv.Cache.Core.Tests.KvStore; 6 | 7 | public class FasterKvStoreTestExpiry 8 | { 9 | private FasterKvCache _fasterKv; 10 | 11 | private readonly Data _data = new() 12 | { 13 | One = "one", 14 | Two = 2 15 | }; 16 | 17 | public FasterKvStoreTestExpiry() 18 | { 19 | _fasterKv = CreateKvStore(); 20 | } 21 | 22 | private static FasterKvCache CreateKvStore() 23 | { 24 | return new FasterKvCache(null!, 25 | new DefaultSystemClock(), 26 | new FasterKvCacheOptions 27 | { 28 | IndexCount = 16384, 29 | MemorySizeBit = 10, 30 | PageSizeBit = 10, 31 | ReadCacheMemorySizeBit = 10, 32 | ReadCachePageSizeBit = 10, 33 | SerializerName = "MessagePack", 34 | ExpiryKeyScanInterval = TimeSpan.FromSeconds(1), 35 | LogPath = "./unit-test/faster-kv-store-expiry-test" 36 | }, 37 | new IFasterKvCacheSerializer[] 38 | { 39 | new MessagePackFasterKvCacheSerializer 40 | { 41 | Name = "MessagePack" 42 | } 43 | }, 44 | null); 45 | } 46 | 47 | [Fact] 48 | public async Task Set_Key_With_Expired_Should_Return_Null() 49 | { 50 | var guid = Guid.NewGuid().ToString("N"); 51 | _fasterKv.Set(guid, _data, TimeSpan.FromSeconds(1)); 52 | 53 | await Task.Delay(2000); 54 | var result = _fasterKv.Get(guid); 55 | 56 | Assert.Null(result); 57 | } 58 | 59 | [Fact] 60 | public async Task ExpiryScanLoop_Should_Delete_Expiry_Key() 61 | { 62 | var guid = Guid.NewGuid().ToString("N"); 63 | _fasterKv.Set(guid, _data, TimeSpan.FromSeconds(1)); 64 | var result = _fasterKv.Get(guid); 65 | Assert.Equal(_data, result); 66 | 67 | await Task.Delay(3000); 68 | var wrapper = _fasterKv.GetWithOutExpiry(guid); 69 | 70 | Assert.Null(wrapper.Data); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.GetOrAdd.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | 5 | namespace FasterKv.Cache.Core.Tests.KvStore; 6 | 7 | public class FasterKvStoreTestGetOrAdd 8 | { 9 | private static FasterKvCache CreateKvStore(string guid, ISystemClock? systemClock = null) 10 | { 11 | return new FasterKvCache(null!, 12 | systemClock ?? new DefaultSystemClock(), 13 | new FasterKvCacheOptions 14 | { 15 | IndexCount = 16384, 16 | MemorySizeBit = 10, 17 | PageSizeBit = 10, 18 | ReadCacheMemorySizeBit = 10, 19 | ReadCachePageSizeBit = 10, 20 | SerializerName = "MessagePack", 21 | ExpiryKeyScanInterval = TimeSpan.FromSeconds(1), 22 | LogPath = $"./unit-test/{guid}" 23 | }, 24 | new IFasterKvCacheSerializer[] 25 | { 26 | new MessagePackFasterKvCacheSerializer 27 | { 28 | Name = "MessagePack" 29 | } 30 | }, 31 | null); 32 | } 33 | 34 | [Fact] 35 | public void GetOrAdd_Should_Return_Existing_Value() 36 | { 37 | 38 | var guid = Guid.NewGuid().ToString("N"); 39 | using var fasterKv = CreateKvStore(guid); 40 | 41 | var data = new Data 42 | { 43 | One = "one", 44 | Two = 2 45 | }; 46 | fasterKv.Set(guid, data); 47 | 48 | var result = fasterKv.GetOrAdd(guid, (_) => new Data 49 | { 50 | One = "two", 51 | Two = 3 52 | }); 53 | 54 | Assert.Equal(data, result); 55 | } 56 | 57 | [Fact] 58 | public void GetOrAdd_Should_Return_New_Value() 59 | { 60 | var guid = Guid.NewGuid().ToString("N"); 61 | using var fasterKv = CreateKvStore(guid); 62 | 63 | var result = fasterKv.GetOrAdd(guid, (_) => new Data 64 | { 65 | One = "two", 66 | Two = 3 67 | }); 68 | 69 | Assert.Equal("two", result.One); 70 | Assert.Equal(3, result.Two); 71 | } 72 | 73 | [Fact] 74 | public void GetOrAdd_Should_Return_NewValue_When_Expired() 75 | { 76 | var guid = Guid.NewGuid().ToString("N"); 77 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 78 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 79 | 80 | var data = new Data 81 | { 82 | One = "one", 83 | Two = 2 84 | }; 85 | fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); 86 | 87 | mockSystemClock.AddSeconds(2); 88 | 89 | var result = fasterKv.GetOrAdd(guid, _ => new Data 90 | { 91 | One = "two", 92 | Two = 3 93 | }); 94 | 95 | Assert.Equal("two", result.One); 96 | Assert.Equal(3, result.Two); 97 | } 98 | 99 | [Fact] 100 | public void GetOrAdd_Should_Return_NewValue_When_Expired_And_Refresh() 101 | { 102 | var guid = Guid.NewGuid().ToString("N"); 103 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 104 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 105 | 106 | var result = fasterKv.GetOrAdd(guid, _ => new Data() 107 | { 108 | One = "one", 109 | Two = 2 110 | }, TimeSpan.FromSeconds(1)); 111 | 112 | Assert.Equal("one", result.One); 113 | Assert.Equal(2, result.Two); 114 | 115 | mockSystemClock.AddSeconds(2); 116 | 117 | result = fasterKv.GetOrAdd(guid, _ => new Data 118 | { 119 | One = "two", 120 | Two = 3 121 | }, TimeSpan.FromSeconds(1)); 122 | 123 | Assert.Equal("two", result.One); 124 | Assert.Equal(3, result.Two); 125 | } 126 | 127 | // below test GetOrAddAsync 128 | 129 | [Fact] 130 | public async Task GetOrAddAsync_Should_Return_Existing_Value() 131 | { 132 | 133 | var guid = Guid.NewGuid().ToString("N"); 134 | using var fasterKv = CreateKvStore(guid); 135 | 136 | var data = new Data 137 | { 138 | One = "one", 139 | Two = 2 140 | }; 141 | fasterKv.Set(guid, data); 142 | 143 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 144 | { 145 | One = "two", 146 | Two = 3 147 | })); 148 | 149 | Assert.Equal(data, result); 150 | } 151 | 152 | [Fact] 153 | public async Task GetOrAddAsync_Should_Return_New_Value() 154 | { 155 | var guid = Guid.NewGuid().ToString("N"); 156 | using var fasterKv = CreateKvStore(guid); 157 | 158 | var result = await fasterKv.GetOrAddAsync(guid, (_) => Task.FromResult(new Data 159 | { 160 | One = "two", 161 | Two = 3 162 | })); 163 | 164 | Assert.Equal("two", result.One); 165 | Assert.Equal(3, result.Two); 166 | } 167 | 168 | [Fact] 169 | public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired() 170 | { 171 | var guid = Guid.NewGuid().ToString("N"); 172 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 173 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 174 | 175 | var data = new Data 176 | { 177 | One = "one", 178 | Two = 2 179 | }; 180 | fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); 181 | 182 | mockSystemClock.AddSeconds(2); 183 | 184 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 185 | { 186 | One = "two", 187 | Two = 3 188 | })); 189 | 190 | Assert.Equal("two", result.One); 191 | Assert.Equal(3, result.Two); 192 | } 193 | 194 | [Fact] 195 | public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired_And_Refresh() 196 | { 197 | var guid = Guid.NewGuid().ToString("N"); 198 | var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); 199 | using var fasterKv = CreateKvStore(guid, mockSystemClock); 200 | 201 | var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data() 202 | { 203 | One = "one", 204 | Two = 2 205 | }), TimeSpan.FromSeconds(1)); 206 | 207 | Assert.Equal("one", result.One); 208 | Assert.Equal(2, result.Two); 209 | 210 | mockSystemClock.AddSeconds(2); 211 | 212 | result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data 213 | { 214 | One = "two", 215 | Two = 3 216 | }), TimeSpan.FromSeconds(1)); 217 | 218 | Assert.Equal("two", result.One); 219 | Assert.Equal(3, result.Two); 220 | } 221 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Configurations; 3 | using FasterKv.Cache.MessagePack; 4 | using FasterKv.Cache.SystemTextJson; 5 | using MessagePack; 6 | 7 | namespace FasterKv.Cache.Core.Tests.KvStore; 8 | 9 | public class FasterKvStoreTest : IDisposable 10 | { 11 | private readonly FasterKvCache _fasterKv; 12 | 13 | private readonly Data _data = new() 14 | { 15 | One = "one", 16 | Two = 2 17 | }; 18 | 19 | public FasterKvStoreTest() 20 | { 21 | _fasterKv = CreateKvStore(); 22 | } 23 | 24 | private static FasterKvCache CreateKvStore() 25 | { 26 | return new FasterKvCache(null!, 27 | new DefaultSystemClock(), 28 | new FasterKvCacheOptions 29 | { 30 | SerializerName = "MessagePack", 31 | ExpiryKeyScanInterval = TimeSpan.Zero, 32 | IndexCount = 16384, 33 | MemorySizeBit = 10, 34 | PageSizeBit = 10, 35 | ReadCacheMemorySizeBit = 10, 36 | ReadCachePageSizeBit = 10, 37 | LogPath = "./unit-test/faster-kv-store-test" 38 | }, 39 | new IFasterKvCacheSerializer[] 40 | { 41 | new MessagePackFasterKvCacheSerializer 42 | { 43 | Name = "MessagePack" 44 | }, 45 | new SystemTextJsonFasterKvCacheSerializer 46 | { 47 | Name = "SystemTextJson" 48 | } 49 | }, 50 | null); 51 | } 52 | 53 | [Fact] 54 | public void Set_Null_Value_Should_Get_Null_Value() 55 | { 56 | var guid = Guid.NewGuid().ToString("N"); 57 | _fasterKv.Set(guid, null); 58 | 59 | var result = _fasterKv.Get(guid); 60 | Assert.Null(result); 61 | } 62 | 63 | [Fact] 64 | public void Set_Key_Should_Success() 65 | { 66 | var guid = Guid.NewGuid().ToString("N"); 67 | _fasterKv.Set(guid, _data); 68 | 69 | var result = _fasterKv.Get(guid); 70 | 71 | Assert.Equal(_data, result); 72 | } 73 | 74 | [Fact] 75 | public void Set_Key_With_ExpiryTime_Should_Success() 76 | { 77 | var guid = Guid.NewGuid().ToString("N"); 78 | _fasterKv.Set(guid, _data, TimeSpan.FromMinutes(1)); 79 | 80 | var result = _fasterKv.Get(guid); 81 | 82 | Assert.Equal(_data, result); 83 | } 84 | 85 | 86 | [Fact] 87 | public void Get_Not_Exist_Key_Should_Return_Null() 88 | { 89 | var guid = Guid.NewGuid().ToString("N"); 90 | var result = _fasterKv.Get(guid); 91 | Assert.Null(result); 92 | } 93 | 94 | [Fact] 95 | public void Delete_Key_Should_Success() 96 | { 97 | var guid = Guid.NewGuid().ToString("N"); 98 | _fasterKv.Set(guid, _data); 99 | _fasterKv.Delete(guid); 100 | 101 | var result = _fasterKv.Get(guid); 102 | Assert.Null(result); 103 | } 104 | 105 | 106 | [Fact] 107 | public async Task SetAsync_Null_Value_Should_Get_Null_Value() 108 | { 109 | var guid = Guid.NewGuid().ToString("N"); 110 | await _fasterKv.SetAsync(guid, null); 111 | 112 | var result = await _fasterKv.GetAsync(guid); 113 | Assert.Null(result); 114 | } 115 | 116 | [Fact] 117 | public async Task SetAsync_Key_Should_Success() 118 | { 119 | var guid = Guid.NewGuid().ToString("N"); 120 | await _fasterKv.SetAsync(guid, _data); 121 | 122 | var result = await _fasterKv.GetAsync(guid); 123 | 124 | Assert.Equal(_data, result); 125 | } 126 | 127 | [Fact] 128 | public async Task SetAsync_Key_With_ExpiryTime_Should_Success() 129 | { 130 | var guid = Guid.NewGuid().ToString("N"); 131 | await _fasterKv.SetAsync(guid, _data, TimeSpan.FromMinutes(1)); 132 | 133 | var result = await _fasterKv.GetAsync(guid); 134 | 135 | Assert.Equal(_data, result); 136 | } 137 | 138 | 139 | [Fact] 140 | public async Task GetAsync_Not_Exist_Key_Should_Return_Null() 141 | { 142 | var guid = Guid.NewGuid().ToString("N"); 143 | var result = await _fasterKv.GetAsync(guid); 144 | Assert.Null(result); 145 | } 146 | 147 | [Fact] 148 | public async Task DeleteAsync_Key_Should_Success() 149 | { 150 | var guid = Guid.NewGuid().ToString("N"); 151 | await _fasterKv.SetAsync(guid, _data); 152 | await _fasterKv.DeleteAsync(guid); 153 | 154 | var result = await _fasterKv.GetAsync(guid); 155 | Assert.Null(result); 156 | } 157 | 158 | [Fact] 159 | public void Set_Big_DataSize_Should_Success() 160 | { 161 | int nums = 10000; 162 | for (int i = 0; i < nums; i++) 163 | { 164 | _fasterKv.Set($"big_data_{i}", new Data 165 | { 166 | One = i.ToString(), 167 | Two = i 168 | }); 169 | } 170 | 171 | for (int i = 0; i < nums; i++) 172 | { 173 | var value = _fasterKv.Get($"big_data_{i}"); 174 | Assert.NotNull(value); 175 | Assert.Equal(i.ToString(), value!.One); 176 | Assert.Equal(i, value.Two); 177 | } 178 | } 179 | 180 | [Fact] 181 | public async Task SetAsync_Big_DataSize_Should_Success() 182 | { 183 | int nums = 10000; 184 | for (int i = 0; i < nums; i++) 185 | { 186 | await _fasterKv.SetAsync($"big_data_{i}", new Data 187 | { 188 | One = i.ToString(), 189 | Two = i 190 | }); 191 | } 192 | 193 | for (int i = 0; i < nums; i++) 194 | { 195 | var value = await _fasterKv.GetAsync($"big_data_{i}"); 196 | Assert.NotNull(value); 197 | Assert.Equal(i.ToString(), value!.One); 198 | Assert.Equal(i, value.Two); 199 | } 200 | } 201 | 202 | [Fact] 203 | public void Set_Big_DataSize_With_ExpiryTime_Should_Success() 204 | { 205 | int nums = 1000; 206 | for (int i = 0; i < nums; i++) 207 | { 208 | _fasterKv.Set($"big_data_{i}", new Data 209 | { 210 | One = i.ToString(), 211 | Two = i 212 | }, TimeSpan.FromMinutes(5)); 213 | } 214 | 215 | for (int i = 0; i < nums; i++) 216 | { 217 | var value = _fasterKv.Get($"big_data_{i}"); 218 | Assert.NotNull(value); 219 | Assert.Equal(i.ToString(), value!.One); 220 | Assert.Equal(i, value.Two); 221 | } 222 | } 223 | 224 | [Fact] 225 | public void Set_Big_DataSize_And_Repeat_Reading_Should_Success() 226 | { 227 | int nums = 1000; 228 | for (int i = 0; i < nums; i++) 229 | { 230 | _fasterKv.Set($"big_value_{i}", new Data 231 | { 232 | One = i.ToString(), 233 | Two = i 234 | }); 235 | } 236 | 237 | var value = _fasterKv.Get($"big_value_{0}"); 238 | Assert.NotNull(value); 239 | Assert.Equal(0.ToString(), value!.One); 240 | Assert.Equal(0, value.Two); 241 | 242 | 243 | value = _fasterKv.Get($"big_value_{0}"); 244 | Assert.NotNull(value); 245 | Assert.Equal(0.ToString(), value!.One); 246 | Assert.Equal(0, value.Two); 247 | } 248 | 249 | [Fact] 250 | public void Set_Big_Value_Should_Success() 251 | { 252 | // 4MB value 253 | var bigValues = Enumerable.Range(0, 4 * 1024 * 1024).Select(i => (byte) i).ToArray(); 254 | 255 | int nums = 200; 256 | for (int i = 0; i < nums; i++) 257 | { 258 | _fasterKv.Set($"big_value_{i}", new Data 259 | { 260 | One = i.ToString(), 261 | Two = i, 262 | Three = bigValues 263 | }); 264 | } 265 | 266 | for (int i = 0; i < nums; i++) 267 | { 268 | var result = _fasterKv.Get($"big_value_{i}"); 269 | 270 | Assert.NotNull(result?.Three); 271 | Assert.Equal(i.ToString(), result!.One); 272 | Assert.Equal(i, result.Two); 273 | Assert.True(bigValues.SequenceEqual(result.Three!)); 274 | } 275 | } 276 | 277 | 278 | [Fact] 279 | public void Set_Big_DataSize_With_Expired_Should_Return_Null() 280 | { 281 | int nums = 1000; 282 | for (int i = 0; i < nums; i++) 283 | { 284 | _fasterKv.Set($"Set_Big_DataSize_With_Expired_Should_Return_Null_{i}", new Data 285 | { 286 | One = i.ToString(), 287 | Two = i 288 | }, TimeSpan.FromSeconds(1)); 289 | } 290 | 291 | Thread.Sleep(1000); 292 | 293 | for (int i = 0; i < nums; i++) 294 | { 295 | var value = _fasterKv.Get($"Set_Big_DataSize_With_Expired_Should_Return_Null_{i}"); 296 | Assert.Null(value); 297 | } 298 | } 299 | 300 | [Fact] 301 | public void Set_Big_DataSize_With_Random_Expired_Should_Success() 302 | { 303 | int nums = 1000; 304 | for (int i = 0; i < nums; i++) 305 | { 306 | _fasterKv.Set($"Set_Big_DataSize_With_Random_Expired_Should_Success_{i}", new Data 307 | { 308 | One = i.ToString(), 309 | Two = i 310 | }, i % 2 == 0 ? TimeSpan.FromSeconds(1) : TimeSpan.FromMinutes(1)); 311 | } 312 | 313 | Thread.Sleep(1000); 314 | 315 | for (int i = 0; i < nums; i++) 316 | { 317 | var value = _fasterKv.Get($"Set_Big_DataSize_With_Random_Expired_Should_Success_{i}"); 318 | if (i % 2 == 0) 319 | { 320 | Assert.Null(value); 321 | } 322 | else 323 | { 324 | Assert.NotNull(value); 325 | Assert.NotNull(value); 326 | Assert.Equal(i.ToString(), value!.One); 327 | Assert.Equal(i, value.Two); 328 | } 329 | } 330 | } 331 | 332 | public void Dispose() 333 | { 334 | _fasterKv.Dispose(); 335 | } 336 | } 337 | 338 | [MessagePackObject] 339 | public class Data 340 | { 341 | [Key(0)] public string? One { get; set; } 342 | 343 | [Key(1)] public long Two { get; set; } 344 | 345 | [Key(2)] public byte[]? Three { get; set; } 346 | 347 | public override bool Equals(object? obj) 348 | { 349 | return base.Equals(obj); 350 | } 351 | 352 | protected bool Equals(Data other) 353 | { 354 | return One == other.One && Two == other.Two; 355 | } 356 | 357 | public override int GetHashCode() 358 | { 359 | return HashCode.Combine(One, Two); 360 | } 361 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/MockSystemClock.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | 3 | namespace FasterKv.Cache.Core.Tests; 4 | 5 | public class MockSystemClock : ISystemClock 6 | { 7 | private DateTimeOffset _now; 8 | 9 | public MockSystemClock(DateTimeOffset now) 10 | { 11 | _now = now; 12 | } 13 | 14 | public DateTimeOffset Now() 15 | { 16 | return _now; 17 | } 18 | 19 | public long NowUnixTimestamp() 20 | { 21 | return _now.ToUnixTimeMilliseconds(); 22 | } 23 | 24 | public void AddSeconds(int seconds) 25 | { 26 | _now = _now.AddSeconds(seconds); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Serializers/FasterKvSerializer.Deserialize.Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using FasterKv.Cache.Core.Abstractions; 3 | using FasterKv.Cache.Core.Serializers; 4 | using Moq; 5 | 6 | namespace FasterKv.Cache.Core.Tests.Serializers; 7 | 8 | public class FasterKvSerializerDeserializeTests 9 | { 10 | private unsafe Span ToSpan(ref T value) 11 | { 12 | return new Span(Unsafe.AsPointer(ref value), Unsafe.SizeOf()); 13 | } 14 | 15 | [Fact] 16 | public void Expired_Value_Should_Only_DeSerialize_ExpiryTime() 17 | { 18 | var mockKvCache = new Mock(); 19 | 20 | var mockClock = new Mock(); 21 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 22 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 23 | 24 | long timeStamp = 1020304; 25 | // | flag | timestamp | 26 | // | 1B | 8B | 27 | using var ms = new MemoryStream(); 28 | ms.WriteByte((byte)FasterKvSerializerFlags.HasExpiryTime); 29 | ms.Write(ToSpan(ref timeStamp)); 30 | 31 | ms.Position = 0; 32 | ser.BeginDeserialize(ms); 33 | 34 | ser.Deserialize(out var valueWrapper); 35 | 36 | Assert.Equal(0, valueWrapper.DataByteLength); 37 | Assert.Equal(timeStamp, valueWrapper.ExpiryTime); 38 | Assert.Null(valueWrapper.DataBytes); 39 | } 40 | 41 | [Fact] 42 | public void NotExpiry_Value_Should_Deserialize_All_Member() 43 | { 44 | var mockKvCache = new Mock(); 45 | var mockClock = new Mock(); 46 | 47 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 48 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 49 | 50 | using var ms = new MemoryStream(); 51 | 52 | // | flag | timestamp | data length | serialize length| 53 | // | 1B | 8B | 4B | xxB | 54 | ms.WriteByte((byte)(FasterKvSerializerFlags.HasExpiryTime | FasterKvSerializerFlags.HasBody)); 55 | 56 | long timeStamp = 1020304; 57 | ms.Write(ToSpan(ref timeStamp)); 58 | 59 | ReadOnlySpan data = "hello world"u8; 60 | int dataLength = data.Length; 61 | ms.Write(ToSpan(ref dataLength)); 62 | ms.Write(data); 63 | 64 | ms.Position = 0; 65 | ser.BeginDeserialize(ms); 66 | 67 | ser.Deserialize(out var wrapper); 68 | 69 | Assert.Equal(timeStamp, wrapper.ExpiryTime); 70 | Assert.Equal(dataLength, wrapper.DataByteLength); 71 | for (int i = 0; i < wrapper.DataByteLength; i++) 72 | { 73 | Assert.Equal(data[i], wrapper.DataBytes![i]); 74 | } 75 | } 76 | 77 | [Fact] 78 | public void Not_Value_And_Not_ExpiryTime_Should_Only_Deserialize_Flag() 79 | { 80 | var mockKvCache = new Mock(); 81 | 82 | var mockClock = new Mock(); 83 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 84 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 85 | 86 | // | flag | 87 | // | 1B | 88 | using var ms = new MemoryStream(); 89 | ms.WriteByte((byte)FasterKvSerializerFlags.None); 90 | ms.Position = 0; 91 | 92 | ser.BeginDeserialize(ms); 93 | ser.Deserialize(out var obj); 94 | 95 | Assert.Null(obj.ExpiryTime); 96 | Assert.Null(obj.DataBytes); 97 | Assert.Null(obj.Data); 98 | Assert.Equal(0, obj.DataByteLength); 99 | } 100 | 101 | [Fact] 102 | public void Not_ExpiryTime_Should_Deserialize_Flag_DataLength_Value() 103 | { 104 | var mockKvCache = new Mock(); 105 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())); 106 | 107 | var mockClock = new Mock(); 108 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 109 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 110 | 111 | // | flag | data length | serialize length | 112 | // | 1B | 4B | xxB | 113 | using var ms = new MemoryStream(); 114 | ser.BeginDeserialize(ms); 115 | 116 | ReadOnlySpan data = "hello world"u8; 117 | ms.WriteByte((byte)FasterKvSerializerFlags.HasBody); 118 | var dataLength = data.Length; 119 | ms.Write(ToSpan(ref dataLength)); 120 | ms.Write(data); 121 | 122 | ms.Position = 0; 123 | ser.BeginDeserialize(ms); 124 | 125 | ser.Deserialize(out var valueWrapper); 126 | 127 | Assert.Equal(dataLength, valueWrapper.DataByteLength); 128 | for (int i = 0; i < valueWrapper.DataByteLength; i++) 129 | { 130 | Assert.Equal(data[i], valueWrapper.DataBytes![i]); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Serializers/FasterKvSerializer.Serialize.Tests.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Serializers; 3 | using Moq; 4 | 5 | namespace FasterKv.Cache.Core.Tests.Serializers; 6 | 7 | public class FasterKvSerializerSerializeTests 8 | { 9 | [Fact] 10 | public void Expired_Value_Should_Only_Serialize_ExpiryTime() 11 | { 12 | var mockKvCache = new Mock(); 13 | 14 | var mockClock = new Mock(); 15 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 16 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 17 | 18 | using var ms = new MemoryStream(); 19 | ser.BeginSerialize(ms); 20 | 21 | var wrapper = new ValueWrapper("", 80); 22 | ser.Serialize(ref wrapper); 23 | 24 | // | flag | timestamp | 25 | // | 1B | 8B | 26 | Assert.Equal(1 + 8, ms.Position); 27 | } 28 | 29 | [Fact] 30 | public void NotExpiry_Value_Should_Serialize_All_Member() 31 | { 32 | int serializeLength = 10; 33 | 34 | var mockKvCache = new Mock(); 35 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 36 | (stream, data) => 37 | { 38 | stream.Position += serializeLength; 39 | }); 40 | 41 | var mockClock = new Mock(); 42 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 43 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 44 | 45 | using var ms = new MemoryStream(); 46 | ser.BeginSerialize(ms); 47 | 48 | var wrapper = new ValueWrapper("", 110); 49 | ser.Serialize(ref wrapper); 50 | 51 | // | flag | timestamp | data length | serialize length| 52 | // | 1B | 8B | 4B | 8B | 53 | Assert.Equal(1 + 8 + 4 + serializeLength, ms.Position); 54 | } 55 | 56 | [Fact] 57 | public void Not_Value_And_Not_ExpiryTime_Should_Only_Serialize_Flag() 58 | { 59 | int serializeLength = 10; 60 | 61 | var mockKvCache = new Mock(); 62 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 63 | (stream, data) => 64 | { 65 | stream.Position += serializeLength; 66 | }); 67 | 68 | var mockClock = new Mock(); 69 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 70 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 71 | 72 | using var ms = new MemoryStream(); 73 | ser.BeginSerialize(ms); 74 | 75 | var wrapper = new ValueWrapper(null, null); 76 | ser.Serialize(ref wrapper); 77 | 78 | // | flag | 79 | // | 1B | 80 | Assert.Equal(1 , ms.Position); 81 | } 82 | 83 | [Fact] 84 | public void Not_ExpiryTime_Should_Serialize_Flag_DataLength_Value() 85 | { 86 | int serializeLength = 10; 87 | 88 | var mockKvCache = new Mock(); 89 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 90 | (stream, data) => 91 | { 92 | stream.Position += serializeLength; 93 | }); 94 | 95 | var mockClock = new Mock(); 96 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 97 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 98 | 99 | using var ms = new MemoryStream(); 100 | ser.BeginSerialize(ms); 101 | 102 | var wrapper = new ValueWrapper("", null); 103 | ser.Serialize(ref wrapper); 104 | 105 | // | flag | data length | serialize length | 106 | // | 1B | 4B | 10B | 107 | Assert.Equal(1 + 4 + 10, ms.Position); 108 | } 109 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Serializers/FasterKvSerializer.TValue.Deserialize.Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | using FasterKv.Cache.Core.Abstractions; 4 | using FasterKv.Cache.Core.Serializers; 5 | using Moq; 6 | 7 | namespace FasterKv.Cache.Core.Tests.Serializers; 8 | 9 | public class FasterKvSerializerTValueDeserializeTests 10 | { 11 | private unsafe Span ToSpan(ref T value) 12 | { 13 | return new Span(Unsafe.AsPointer(ref value), Unsafe.SizeOf()); 14 | } 15 | 16 | [Fact] 17 | public void Expired_Value_Should_Only_DeSerialize_ExpiryTime() 18 | { 19 | var mockKvCache = new Mock(); 20 | 21 | var mockClock = new Mock(); 22 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 23 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 24 | 25 | long timeStamp = 1020304; 26 | // | flag | timestamp | 27 | // | 1B | 8B | 28 | using var ms = new MemoryStream(); 29 | ms.WriteByte((byte)FasterKvSerializerFlags.HasExpiryTime); 30 | ms.Write(ToSpan(ref timeStamp)); 31 | 32 | ms.Position = 0; 33 | ser.BeginDeserialize(ms); 34 | 35 | ser.Deserialize(out var valueWrapper); 36 | 37 | Assert.Equal(timeStamp, valueWrapper.ExpiryTime); 38 | Assert.Null(valueWrapper.Data); 39 | } 40 | 41 | [Fact] 42 | public void NotExpiry_Value_Should_Deserialize_All_Member() 43 | { 44 | var mockKvCache = new Mock(); 45 | mockKvCache.Setup(i => i.Deserialize(It.IsAny(), It.IsAny())) 46 | .Returns((bytes, length) => Encoding.UTF8.GetString(bytes, 0, length)); 47 | var mockClock = new Mock(); 48 | 49 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 50 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 51 | 52 | using var ms = new MemoryStream(); 53 | 54 | // | flag | timestamp | data length | serialize length| 55 | // | 1B | 8B | 4B | xxB | 56 | ms.WriteByte((byte)(FasterKvSerializerFlags.HasExpiryTime | FasterKvSerializerFlags.HasBody)); 57 | 58 | long timeStamp = 1020304; 59 | ms.Write(ToSpan(ref timeStamp)); 60 | 61 | ReadOnlySpan data = "hello world"u8; 62 | int dataLength = data.Length; 63 | ms.Write(ToSpan(ref dataLength)); 64 | ms.Write(data); 65 | 66 | ms.Position = 0; 67 | ser.BeginDeserialize(ms); 68 | 69 | ser.Deserialize(out var wrapper); 70 | 71 | Assert.Equal(timeStamp, wrapper.ExpiryTime); 72 | Assert.Equal("hello world", wrapper.Data); 73 | } 74 | 75 | [Fact] 76 | public void Not_Value_And_Not_ExpiryTime_Should_Only_Deserialize_Flag() 77 | { 78 | var mockKvCache = new Mock(); 79 | 80 | var mockClock = new Mock(); 81 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 82 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 83 | 84 | // | flag | 85 | // | 1B | 86 | using var ms = new MemoryStream(); 87 | ms.WriteByte((byte)FasterKvSerializerFlags.None); 88 | ms.Position = 0; 89 | 90 | ser.BeginDeserialize(ms); 91 | ser.Deserialize(out var obj); 92 | 93 | Assert.Null(obj.ExpiryTime); 94 | Assert.Null(obj.Data); 95 | } 96 | 97 | [Fact] 98 | public void Not_ExpiryTime_Should_Deserialize_Flag_DataLength_Value() 99 | { 100 | var mockKvCache = new Mock(); 101 | mockKvCache.Setup(i => i.Deserialize(It.IsAny(), It.IsAny())) 102 | .Returns((bytes, length) => Encoding.UTF8.GetString(bytes, 0, length)); 103 | 104 | var mockClock = new Mock(); 105 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 106 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 107 | 108 | // | flag | data length | serialize length | 109 | // | 1B | 4B | xxB | 110 | using var ms = new MemoryStream(); 111 | ser.BeginDeserialize(ms); 112 | 113 | ReadOnlySpan data = "hello world"u8; 114 | ms.WriteByte((byte)FasterKvSerializerFlags.HasBody); 115 | var dataLength = data.Length; 116 | ms.Write(ToSpan(ref dataLength)); 117 | ms.Write(data); 118 | 119 | ms.Position = 0; 120 | ser.BeginDeserialize(ms); 121 | 122 | ser.Deserialize(out var valueWrapper); 123 | 124 | Assert.Equal("hello world", valueWrapper.Data); 125 | } 126 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Serializers/FasterKvSerializer.TValue.Serialize.Tests.cs: -------------------------------------------------------------------------------- 1 | using FasterKv.Cache.Core.Abstractions; 2 | using FasterKv.Cache.Core.Serializers; 3 | using Moq; 4 | 5 | namespace FasterKv.Cache.Core.Tests.Serializers; 6 | 7 | public class FasterKvSerializerTValueSerializeTests 8 | { 9 | [Fact] 10 | public void Expired_Value_Should_Only_Serialize_ExpiryTime() 11 | { 12 | var mockKvCache = new Mock(); 13 | 14 | var mockClock = new Mock(); 15 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 16 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 17 | 18 | using var ms = new MemoryStream(); 19 | ser.BeginSerialize(ms); 20 | 21 | var wrapper = new ValueWrapper("", 80); 22 | ser.Serialize(ref wrapper); 23 | 24 | // | flag | timestamp | 25 | // | 1B | 8B | 26 | Assert.Equal(1 + 8, ms.Position); 27 | } 28 | 29 | [Fact] 30 | public void NotExpiry_Value_Should_Serialize_All_Member() 31 | { 32 | int serializeLength = 10; 33 | 34 | var mockKvCache = new Mock(); 35 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 36 | (stream, data) => 37 | { 38 | stream.Position += serializeLength; 39 | }); 40 | 41 | var mockClock = new Mock(); 42 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 43 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 44 | 45 | using var ms = new MemoryStream(); 46 | ser.BeginSerialize(ms); 47 | 48 | var wrapper = new ValueWrapper("", 110); 49 | ser.Serialize(ref wrapper); 50 | 51 | // | flag | timestamp | data length | serialize length| 52 | // | 1B | 8B | 4B | 8B | 53 | Assert.Equal(1 + 8 + 4 + serializeLength, ms.Position); 54 | } 55 | 56 | [Fact] 57 | public void Not_Value_And_Not_ExpiryTime_Should_Only_Serialize_Flag() 58 | { 59 | int serializeLength = 10; 60 | 61 | var mockKvCache = new Mock(); 62 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 63 | (stream, data) => 64 | { 65 | stream.Position += serializeLength; 66 | }); 67 | 68 | var mockClock = new Mock(); 69 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 70 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 71 | 72 | using var ms = new MemoryStream(); 73 | ser.BeginSerialize(ms); 74 | 75 | var wrapper = new ValueWrapper(null, null); 76 | ser.Serialize(ref wrapper); 77 | 78 | // | flag | 79 | // | 1B | 80 | Assert.Equal(1 , ms.Position); 81 | } 82 | 83 | [Fact] 84 | public void Not_ExpiryTime_Should_Serialize_Flag_DataLength_Value() 85 | { 86 | int serializeLength = 10; 87 | 88 | var mockKvCache = new Mock(); 89 | mockKvCache.Setup(i => i.Serialize(It.IsAny(), It.IsAny())).Callback( 90 | (stream, data) => 91 | { 92 | stream.Position += serializeLength; 93 | }); 94 | 95 | var mockClock = new Mock(); 96 | mockClock.Setup(i => i.NowUnixTimestamp()).Returns(100); 97 | var ser = new FasterKvSerializer(mockKvCache.Object, mockClock.Object); 98 | 99 | using var ms = new MemoryStream(); 100 | ser.BeginSerialize(ms); 101 | 102 | var wrapper = new ValueWrapper("", null); 103 | ser.Serialize(ref wrapper); 104 | 105 | // | flag | data length | serialize length | 106 | // | 1B | 4B | 10B | 107 | Assert.Equal(1 + 4 + 10, ms.Position); 108 | } 109 | } -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Serializers/MessagePackTests.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InCerryGit/FasterKvCache/8bd8df49cc435dc29d62a1cf08ce6b3828f00a6f/tests/FasterKv.Cache.Core.Tests/Serializers/MessagePackTests.cs -------------------------------------------------------------------------------- /tests/FasterKv.Cache.Core.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; --------------------------------------------------------------------------------