├── .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 | 
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