├── .editorconfig ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── Akka.Persistence.MongoDb.sln ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE.md ├── NuGet.Config ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build-system ├── README.md ├── azure-pipeline.template.yaml ├── linux-pr-validation.yaml ├── windows-pr-validation.yaml └── windows-release.yaml ├── build.ps1 ├── coverlet.runsettings ├── docs ├── api │ └── index.md ├── articles │ ├── index.md │ └── toc.yml ├── docfx.json ├── images │ └── icon.png ├── index.md └── toc.yml ├── global.json ├── scripts ├── bumpVersion.ps1 └── getReleaseNotes.ps1 ├── serve-docs.cmd ├── serve-docs.ps1 └── src ├── Akka.Persistence.MongoDb.Hosting ├── Akka.Persistence.MongoDb.Hosting.csproj ├── AkkaPersistenceMongoDbHostingExtensions.cs ├── MongoDbGridFsSnapshotOptions.cs ├── MongoDbJournalOptions.cs ├── MongoDbSnapshotOptions.cs └── README.md ├── Akka.Persistence.MongoDb.Tests ├── Akka.Persistence.MongoDb.Tests.csproj ├── Bug25FixSpec.cs ├── Bug61FixSpec.cs ├── DatabaseFixture.cs ├── GridFS │ ├── MongoDbGridFsLegacySerializationSnapshotStoreSpec.cs │ ├── MongoDbGridFsSnapshotStoreSaveSnapshotSpec.cs │ ├── MongoDbGridFsSnapshotStoreSpec.cs │ └── Serialization │ │ └── MongoDbGridFsSnapshotStoreSerializationSpec.cs ├── Hosting │ ├── MongoDbJournalOptionsSpec.cs │ └── MongoDbSnapshotOptionsSpec.cs ├── JournalTestActor.cs ├── MongoDbAllEventsSpec.cs ├── MongoDbCurrentAllEventsSpec.cs ├── MongoDbCurrentEventsByPersistenceIdsSpec.cs ├── MongoDbCurrentEventsByTagSpec.cs ├── MongoDbCurrentPersistenceIdsSpec.cs ├── MongoDbEventsByPersistenceIdSpec.cs ├── MongoDbEventsByTagSpec.cs ├── MongoDbJournalPerfSpec.cs ├── MongoDbJournalSetupSpec.cs ├── MongoDbJournalSpec.cs ├── MongoDbLegacySerializationJournalSpec.cs ├── MongoDbLegacySerializationSnapshotStoreSpec.cs ├── MongoDbPersistenceIdsSpec.cs ├── MongoDbSettingsSpec.cs ├── MongoDbSnapshotStoreSaveSnapshotSpec.cs ├── MongoDbSnapshotStoreSetupSpec.cs ├── MongoDbSnapshotStoreSpec.cs └── Serialization │ ├── MongoDbJournalSerializationSpec.cs │ └── MongoDbSnapshotStoreSerializationSpec.cs ├── Akka.Persistence.MongoDb ├── Akka.Persistence.MongoDb.csproj ├── FullTypeNameDiscriminatorConvention.cs ├── FullTypeNameObjectSerializer.cs ├── Journal │ ├── JournalEntry.cs │ ├── MetadataEntry.cs │ ├── MongoDbJournal.cs │ └── MongoDbJournalQueries.cs ├── MongoDbPersistence.cs ├── MongoDbPersistenceProvider.cs ├── MongoDbPersistenceSetup.cs ├── MongoDbSettings.cs ├── Query │ ├── AllEventsPublisher.cs │ ├── AllPersistenceIdsPublisher.cs │ ├── DeliveryBuffer.cs │ ├── EventByPersistenceIdPublisher.cs │ ├── EventsByTagPublisher.cs │ ├── MongoDbReadJournal.cs │ ├── MongoDbReadJournalProvider.cs │ ├── QueryApi.cs │ └── SubscriptionDroppedException.cs ├── Snapshot │ ├── GridFsPayloadEnvelope.cs │ ├── MongoDbGridFSSnapshotStore.cs │ ├── MongoDbSnapshotStore.cs │ └── SnapshotEntry.cs └── reference.conf └── examples ├── LargeSnapshot ├── Actors │ └── PersistentActor.cs ├── LargeSnapshot.csproj ├── Program.cs └── appsettings.json └── start_docker.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = CRLF 8 | 9 | [*.cs] 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | # Custom for Visual Studio 6 | *.cs diff=csharp 7 | *.sln merge=union 8 | *.csproj merge=union 9 | *.vbproj merge=union 10 | *.fsproj merge=union 11 | *.dbproj merge=union 12 | 13 | # Standard to msysgit 14 | *.doc diff=astextplain 15 | *.DOC diff=astextplain 16 | *.docx diff=astextplain 17 | *.DOCX diff=astextplain 18 | *.dot diff=astextplain 19 | *.DOT diff=astextplain 20 | *.pdf diff=astextplain 21 | *.PDF diff=astextplain 22 | *.rtf diff=astextplain 23 | *.RTF diff=astextplain 24 | 25 | # Needed for Mono build shell script 26 | *.sh -text eol=lf 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | target-branch: dev 9 | ignore: 10 | - dependency-name: Mongo2Go 11 | versions: 12 | - 3.0.0 13 | - 3.1.0 14 | - 3.1.1 15 | - dependency-name: Akka.Persistence.TCK 16 | versions: 17 | - 1.4.17 18 | - dependency-name: akka.streams 19 | versions: 20 | - 1.4.17 21 | - dependency-name: Akka.Persistence.Query 22 | versions: 23 | - 1.4.17 24 | - dependency-name: FluentAssertions 25 | versions: 26 | - 5.10.3 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | tools/ 3 | build/ 4 | .nuget/ 5 | .dotnet/ 6 | .idea/ 7 | .[Dd][Ss]_[Ss]tore 8 | 9 | ## Ignore Visual Studio temporary files, build results, and 10 | ## files generated by popular Visual Studio add-ons. 11 | ## 12 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | 35 | #FAKE 36 | .fake 37 | tools/ 38 | 39 | #DocFx output 40 | _site/ 41 | 42 | # Visual Studio 2015 cache/options directory 43 | .vs/ 44 | # Uncomment if you have tasks that create the project's static files in wwwroot 45 | #wwwroot/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUNIT 52 | *.VisualState.xml 53 | TestResult.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | **/Properties/launchSettings.json 65 | 66 | *_i.c 67 | *_p.c 68 | *_i.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.pch 73 | *.pdb 74 | *.pgc 75 | *.pgd 76 | *.rsp 77 | *.sbr 78 | *.tlb 79 | *.tli 80 | *.tlh 81 | *.tmp 82 | *.tmp_proj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # Visual Studio code coverage results 132 | *.coverage 133 | *.coveragexml 134 | 135 | # NCrunch 136 | _NCrunch_* 137 | .*crunch*.local.xml 138 | nCrunchTemp_* 139 | 140 | # MightyMoose 141 | *.mm.* 142 | AutoTest.Net/ 143 | 144 | # Web workbench (sass) 145 | .sass-cache/ 146 | 147 | # Installshield output folder 148 | [Ee]xpress/ 149 | 150 | # DocProject is a documentation generator add-in 151 | DocProject/buildhelp/ 152 | DocProject/Help/*.HxT 153 | DocProject/Help/*.HxC 154 | DocProject/Help/*.hhc 155 | DocProject/Help/*.hhk 156 | DocProject/Help/*.hhp 157 | DocProject/Help/Html2 158 | DocProject/Help/html 159 | 160 | # Click-Once directory 161 | publish/ 162 | 163 | # Publish Web Output 164 | *.[Pp]ublish.xml 165 | *.azurePubxml 166 | # TODO: Comment the next line if you want to checkin your web deploy settings 167 | # but database connection strings (with potential passwords) will be unencrypted 168 | *.pubxml 169 | *.publishproj 170 | 171 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 172 | # checkin your Azure Web App publish settings, but sensitive information contained 173 | # in these scripts will be unencrypted 174 | PublishScripts/ 175 | 176 | # NuGet Packages 177 | *.nupkg 178 | # The packages folder can be ignored because of Package Restore 179 | **/packages/* 180 | # except build/, which is used as an MSBuild target. 181 | !**/packages/build/ 182 | # Uncomment if necessary however generally it will be regenerated when needed 183 | #!**/packages/repositories.config 184 | # NuGet v3's project.json files produces more ignorable files 185 | *.nuget.props 186 | *.nuget.targets 187 | 188 | # Microsoft Azure Build Output 189 | csx/ 190 | *.build.csdef 191 | 192 | # Microsoft Azure Emulator 193 | ecf/ 194 | rcf/ 195 | 196 | # Windows Store app package directories and files 197 | AppPackages/ 198 | BundleArtifacts/ 199 | Package.StoreAssociation.xml 200 | _pkginfo.txt 201 | 202 | # Visual Studio cache files 203 | # files ending in .cache can be ignored 204 | *.[Cc]ache 205 | # but keep track of directories ending in .cache 206 | !*.[Cc]ache/ 207 | 208 | # Others 209 | ClientBin/ 210 | ~$* 211 | *~ 212 | *.dbmdl 213 | *.dbproj.schemaview 214 | *.jfm 215 | *.pfx 216 | *.publishsettings 217 | orleans.codegen.cs 218 | 219 | # Since there are multiple workflows, uncomment next line to ignore bower_components 220 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 221 | #bower_components/ 222 | 223 | # RIA/Silverlight projects 224 | Generated_Code/ 225 | 226 | # Backup & report files from converting an old project file 227 | # to a newer Visual Studio version. Backup files are not needed, 228 | # because we have git ;-) 229 | _UpgradeReport_Files/ 230 | Backup*/ 231 | UpgradeLog*.XML 232 | UpgradeLog*.htm 233 | 234 | # SQL Server files 235 | *.mdf 236 | *.ldf 237 | *.ndf 238 | 239 | # Business Intelligence projects 240 | *.rdl.data 241 | *.bim.layout 242 | *.bim_*.settings 243 | 244 | # Microsoft Fakes 245 | FakesAssemblies/ 246 | 247 | # GhostDoc plugin setting file 248 | *.GhostDoc.xml 249 | 250 | # Node.js Tools for Visual Studio 251 | .ntvs_analysis.dat 252 | node_modules/ 253 | 254 | # Typescript v1 declaration files 255 | typings/ 256 | 257 | # Visual Studio 6 build log 258 | *.plg 259 | 260 | # Visual Studio 6 workspace options file 261 | *.opt 262 | 263 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 264 | *.vbw 265 | 266 | # Visual Studio LightSwitch build output 267 | **/*.HTMLClient/GeneratedArtifacts 268 | **/*.DesktopClient/GeneratedArtifacts 269 | **/*.DesktopClient/ModelManifest.xml 270 | **/*.Server/GeneratedArtifacts 271 | **/*.Server/ModelManifest.xml 272 | _Pvt_Extensions 273 | 274 | # Paket dependency manager 275 | .paket/paket.exe 276 | paket-files/ 277 | 278 | # FAKE - F# Make 279 | .fake/ 280 | 281 | # JetBrains Rider 282 | .idea/ 283 | *.sln.iml 284 | 285 | # CodeRush 286 | .cr/ 287 | 288 | # Python Tools for Visual Studio (PTVS) 289 | __pycache__/ 290 | *.pyc 291 | 292 | # Cake - Uncomment if you are using it 293 | # tools/** 294 | # !tools/packages.config 295 | 296 | # Telerik's JustMock configuration file 297 | *.jmconfig 298 | 299 | # BizTalk build output 300 | *.btp.cs 301 | *.btm.cs 302 | *.odx.cs 303 | *.xsd.cs 304 | -------------------------------------------------------------------------------- /Akka.Persistence.MongoDb.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33723.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.MongoDb", "src\Akka.Persistence.MongoDb\Akka.Persistence.MongoDb.csproj", "{E945AABA-2779-41E8-9B43-8898FFD64F22}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.MongoDb.Tests", "src\Akka.Persistence.MongoDb.Tests\Akka.Persistence.MongoDb.Tests.csproj", "{0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{BE1178E1-2069-4762-82E5-43805913DCB8}" 11 | ProjectSection(SolutionItems) = preProject 12 | build.ps1 = build.ps1 13 | README.md = README.md 14 | RELEASE_NOTES.md = RELEASE_NOTES.md 15 | Directory.Build.props = Directory.Build.props 16 | Directory.Packages.props = Directory.Packages.props 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.MongoDb.Hosting", "src\Akka.Persistence.MongoDb.Hosting\Akka.Persistence.MongoDb.Hosting.csproj", "{72B8C165-FE00-465F-A2E9-60B4B79F81AF}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{57AE52C4-1E22-42B9-9214-4FEE4F5DFC70}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LargeSnapshot", "src\examples\LargeSnapshot\LargeSnapshot.csproj", "{8FA9D3B5-C67E-49A6-8449-74E060458F25}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build-system", "build-system", "{A57DA6C5-C818-499C-B0AF-F22490AE2015}" 26 | ProjectSection(SolutionItems) = preProject 27 | build-system\azure-pipeline.template.yaml = build-system\azure-pipeline.template.yaml 28 | build-system\linux-pr-validation.yaml = build-system\linux-pr-validation.yaml 29 | build-system\windows-pr-validation.yaml = build-system\windows-pr-validation.yaml 30 | build-system\windows-release.yaml = build-system\windows-release.yaml 31 | EndProjectSection 32 | EndProject 33 | Global 34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 35 | Debug|Any CPU = Debug|Any CPU 36 | Release|Any CPU = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 39 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {E945AABA-2779-41E8-9B43-8898FFD64F22}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {8FA9D3B5-C67E-49A6-8449-74E060458F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {8FA9D3B5-C67E-49A6-8449-74E060458F25}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {8FA9D3B5-C67E-49A6-8449-74E060458F25}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {8FA9D3B5-C67E-49A6-8449-74E060458F25}.Release|Any CPU.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(SolutionProperties) = preSolution 57 | HideSolutionNode = FALSE 58 | EndGlobalSection 59 | GlobalSection(ExtensibilityGlobals) = postSolution 60 | SolutionGuid = {D4F3F966-EB9C-443A-8158-419703A68B92} 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {8FA9D3B5-C67E-49A6-8449-74E060458F25} = {57AE52C4-1E22-42B9-9214-4FEE4F5DFC70} 64 | {A57DA6C5-C818-499C-B0AF-F22490AE2015} = {BE1178E1-2069-4762-82E5-43805913DCB8} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © 2013-2021 Akka.NET Project 4 | Akka.NET Contrib 5 | 1.5.41-beta1 6 | http://getakka.net/images/akkalogo.png 7 | https://github.com/akkadotnet/Akka.Persistence.MongoDB 8 | https://github.com/akkadotnet/Akka.Persistence.MongoDB/blob/master/LICENSE.md 9 | * [Optimize write transaction usage](https://github.com/akkadotnet/Akka.Persistence.MongoDB/pull/410) 10 | 11 | We're optimizing how write transaction is being done. Following the [MongoDb documentation](https://www.mongodb.com/docs/manual/core/write-operations-atomicity/#atomicity-and-transactions) that all single document writes are atomic, we're not using transaction for single document writes anymore. 12 | true 13 | Akka Persistence journal and snapshot store backed by MongoDB database. 14 | $(NoWarn);CS1591 15 | latest 16 | 17 | 18 | net7.0 19 | net472 20 | netstandard2.1 21 | net472 22 | 23 | 24 | 25 | 26 | 27 | 28 | true 29 | 30 | true 31 | 32 | true 33 | snupkg 34 | 35 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 3.0.0 5 | 1.5.42 6 | 1.5.42 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '1.0.{build}' 2 | skip_tags: true 3 | image: Visual Studio 2015 4 | configuration: Release 5 | build_script: 6 | - cmd: build.cmd Build 7 | test_script: 8 | - cmd: build.cmd RunTests 9 | -------------------------------------------------------------------------------- /build-system/README.md: -------------------------------------------------------------------------------- 1 | # Azure Pipelines Build Files 2 | These `.yaml` files are used by Windows Azure DevOps Pipelines to help execute the following types of builds: 3 | 4 | - Pull request validation on Linux (Mono / .NET Core) 5 | - Pull request validation on Windows (.NET Framework / .NET Core) 6 | - NuGet releases with automatic release notes posted to a Github Release repository. 7 | 8 | **NOTE**: you will need to change some of the pipeline variables inside the `windows-release.yaml` for your specific project and you will also want to create variable groups with your signing and NuGet push information. -------------------------------------------------------------------------------- /build-system/azure-pipeline.template.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: '' 3 | displayName: '' 4 | vmImage: '' 5 | outputDirectory: '' 6 | artifactName: '' 7 | timeoutInMinutes: 120 8 | 9 | jobs: 10 | - job: ${{ parameters.name }} 11 | displayName: ${{ parameters.displayName }} 12 | timeoutInMinutes: ${{ parameters.timeoutInMinutes }} 13 | 14 | pool: 15 | vmImage: ${{ parameters.vmImage }} 16 | 17 | steps: 18 | - checkout: self # self represents the repo where the initial Pipelines YAML file was found 19 | clean: false # whether to fetch clean each time 20 | submodules: recursive # set to 'true' for a single level of submodules or 'recursive' to get submodules of submodules 21 | persistCredentials: true 22 | 23 | - task: UseDotNet@2 24 | displayName: 'Use .NET' 25 | inputs: 26 | packageType: 'sdk' 27 | useGlobalJson: true 28 | 29 | - task: UseDotNet@2 30 | displayName: "Use .NET 7 runtime" 31 | inputs: 32 | packageType: runtime 33 | version: 7.x 34 | 35 | # install libcrypto.so.1.1 in linux,this is missing in the latest ubuntu release 36 | # see https://github.com/Mongo2Go/Mongo2Go/?tab=readme-ov-file#mongo2go-410-january-30-2025 37 | - script: | 38 | echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list 39 | sudo apt update 40 | sudo apt install -y libssl1.1 41 | displayName: 'Install libcrypto.so.1.1' 42 | condition: eq( variables['Agent.OS'], 'Linux' ) 43 | 44 | - script: dotnet tool restore 45 | displayName: 'Restore dotnet tools' 46 | 47 | - pwsh: | 48 | .\build.ps1 49 | displayName: 'Update Release Notes' 50 | continueOnError: false 51 | 52 | - script: dotnet build -c Release 53 | displayName: 'dotnet build' 54 | continueOnError: false 55 | 56 | - powershell: | 57 | Get-ChildItem ` 58 | -Recurse ` 59 | -File ` 60 | -Include '*.Tests.csproj' ` 61 | -Exclude '*examples*', '*benchmarks*' ` 62 | | ForEach-Object { ` 63 | dotnet test -c Release --no-build --logger:trx --collect:"XPlat Code Coverage" --results-directory TestResults --settings coverlet.runsettings $_.FullName ` 64 | } 65 | displayName: 'Run tests' 66 | env: 67 | TEST_ENVIRONMENT: CICD 68 | continueOnError: true # Allow continuation even if tests fail 69 | 70 | - task: PublishTestResults@2 71 | inputs: 72 | testResultsFormat: VSTest 73 | testResultsFiles: '**/*.trx' #TestResults folder usually 74 | testRunTitle: ${{ parameters.name }} 75 | mergeTestResults: true 76 | failTaskOnFailedTests: false 77 | publishRunAttachments: true 78 | 79 | - pwsh: | 80 | $coverageFiles = Get-ChildItem -Path "$(Build.SourcesDirectory)/TestResults" -Filter "*.cobertura.xml" -Recurse 81 | $hasCoverageFiles = $coverageFiles.Count -gt 0 82 | Write-Host "##vso[task.setvariable variable=HasCoverageFiles]$hasCoverageFiles" 83 | displayName: 'Check for Coverage Files' 84 | condition: always() 85 | continueOnError: true 86 | 87 | - task: reportgenerator@5 88 | displayName: ReportGenerator 89 | # Only run if coverage files exist 90 | condition: and(always(), eq(variables['HasCoverageFiles'], 'True')) 91 | continueOnError: true 92 | inputs: 93 | reports: '$(Build.SourcesDirectory)/TestResults/**/*.cobertura.xml' 94 | targetdir: '$(Build.SourcesDirectory)/coveragereport' 95 | reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges' 96 | assemblyfilters: '-xunit*' 97 | publishCodeCoverageResults: true 98 | 99 | - publish: $(Build.SourcesDirectory)/coveragereport 100 | displayName: 'Publish Coverage Report' 101 | # Only run if coverage files exist 102 | condition: and(always(), eq(variables['HasCoverageFiles'], 'True')) 103 | continueOnError: true 104 | artifact: 'CoverageReports-$(Agent.OS)-$(Build.BuildId)' 105 | 106 | - script: dotnet pack -c Release --no-build -o $(Build.ArtifactStagingDirectory)/nuget 107 | displayName: 'Create packages' 108 | 109 | - task: PublishBuildArtifacts@1 110 | displayName: 'Publish artifacts' 111 | inputs: 112 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/nuget' 113 | ArtifactName: 'nuget' 114 | publishLocation: 'Container' 115 | 116 | - script: 'echo 1>&2' 117 | failOnStderr: true 118 | displayName: 'If above is partially succeeded, then fail' 119 | condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues') 120 | -------------------------------------------------------------------------------- /build-system/linux-pr-validation.yaml: -------------------------------------------------------------------------------- 1 | # Pull request validation for Linux against the `dev` and `master` branches 2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference 3 | trigger: 4 | branches: 5 | include: 6 | - dev 7 | - master 8 | 9 | name: $(SourceBranchName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) 10 | 11 | pr: 12 | autoCancel: true # indicates whether additional pushes to a PR should cancel in-progress runs for the same PR. Defaults to true 13 | branches: 14 | include: [ dev, master ] # branch names which will trigger a build 15 | 16 | jobs: 17 | - template: azure-pipeline.template.yaml 18 | parameters: 19 | name: 'net_7_tests_linux' 20 | displayName: '.NET 7 Unit Tests (Linux)' 21 | vmImage: 'ubuntu-latest' 22 | outputDirectory: 'bin/nuget' 23 | artifactName: 'nuget_pack-$(Build.BuildId)' 24 | -------------------------------------------------------------------------------- /build-system/windows-pr-validation.yaml: -------------------------------------------------------------------------------- 1 | # Pull request validation for Windows against the `dev` and `master` branches 2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference 3 | trigger: 4 | branches: 5 | include: 6 | - dev 7 | - master 8 | 9 | pr: 10 | autoCancel: true # indicates whether additional pushes to a PR should cancel in-progress runs for the same PR. Defaults to true 11 | branches: 12 | include: [ dev, master ] # branch names which will trigger a build 13 | 14 | name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) 15 | 16 | jobs: 17 | - template: azure-pipeline.template.yaml 18 | parameters: 19 | name: 'net_7_tests_windows' 20 | displayName: '.NET 7 Unit Tests (Windows)' 21 | vmImage: 'windows-latest' 22 | outputDirectory: 'bin/nuget' 23 | artifactName: 'nuget_pack-$(Build.BuildId)' 24 | -------------------------------------------------------------------------------- /build-system/windows-release.yaml: -------------------------------------------------------------------------------- 1 | # Release task for Akka.Persistence.MongoDb 2 | # See https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema for reference 3 | 4 | trigger: 5 | branches: 6 | include: 7 | - refs/tags/* 8 | pr: none 9 | 10 | variables: 11 | - group: signingSecrets #create this group with SECRET variables `signingUsername` and `signingPassword` 12 | - group: nugetKeys 13 | - name: githubConnectionName 14 | value: AkkaDotNet_Releases 15 | - name: projectName 16 | value: Akka.Persistence.MongoDB 17 | - name: githubRepositoryName 18 | value: akkadotnet/Akka.Persistence.MongoDB 19 | 20 | jobs: 21 | - job: Release 22 | pool: 23 | vmImage: windows-latest 24 | demands: Cmd 25 | steps: 26 | - task: UseDotNet@2 27 | displayName: 'Use .NET SDK from global.json' 28 | inputs: 29 | useGlobalJson: true 30 | 31 | - powershell: ./build.ps1 32 | displayName: 'Update Release Notes' 33 | 34 | # Pack without version suffix for release 35 | - script: dotnet pack -c Release -o $(Build.ArtifactStagingDirectory)/nuget 36 | displayName: 'Create packages' 37 | 38 | - script: dotnet nuget push "$(Build.ArtifactStagingDirectory)\nuget\*.nupkg" --api-key $(nugetKey) --source https://api.nuget.org/v3/index.json --skip-duplicate 39 | displayName: 'Publish to NuGet.org' 40 | 41 | - task: GitHubRelease@0 42 | displayName: 'GitHub release (create)' 43 | inputs: 44 | gitHubConnection: $(githubConnectionName) 45 | repositoryName: $(githubRepositoryName) 46 | title: '$(projectName) v$(Build.SourceBranchName)' 47 | releaseNotesFile: 'RELEASE_NOTES.md' 48 | assets: '$(Build.ArtifactStagingDirectory)/nuget/*.nupkg' 49 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | . "$PSScriptRoot\scripts\getReleaseNotes.ps1" 2 | . "$PSScriptRoot\scripts\bumpVersion.ps1" 3 | 4 | ###################################################################### 5 | # Step 1: Grab release notes and update solution metadata 6 | ###################################################################### 7 | $releaseNotes = Get-ReleaseNotes -MarkdownFile (Join-Path -Path $PSScriptRoot -ChildPath "RELEASE_NOTES.md") 8 | 9 | # inject release notes into Directory.Buil 10 | UpdateVersionAndReleaseNotes -ReleaseNotesResult $releaseNotes -XmlFilePath (Join-Path -Path $PSScriptRoot -ChildPath "Directory.Build.props") 11 | 12 | Write-Output "Added release notes $releaseNotes" -------------------------------------------------------------------------------- /coverlet.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | json,cobertura,lcov,teamcity,opencover 8 | [*.Tests]*,[*.Benchmark]* 9 | 10 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute 11 | 12 | 13 | **/examples/**/* 14 | 15 | 16 | false 17 | 18 | 19 | false 20 | true 21 | true 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API Docs -------------------------------------------------------------------------------- /docs/articles/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Article text goes here. -------------------------------------------------------------------------------- /docs/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Introduction 2 | href: index.md -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ "**/*.csproj" ], 7 | "exclude": [ 8 | "**/obj/**", 9 | "**/bin/**", 10 | "_site/**", 11 | "**/*Tests*.csproj", 12 | "**/*Tests.*.csproj" 13 | ], 14 | "src": "../src" 15 | } 16 | ], 17 | "dest": "api" 18 | } 19 | ], 20 | "build": { 21 | "content": [ 22 | { 23 | "files": [ 24 | "api/**.yml", 25 | "api/index.md" 26 | ] 27 | }, 28 | { 29 | "files": [ 30 | "articles/**.md", 31 | "articles/**/toc.yml", 32 | "toc.yml", 33 | "*.md" 34 | ], 35 | "exclude": [ 36 | "obj/**", 37 | "_site/**" 38 | ] 39 | }, 40 | ], 41 | "resource": [ 42 | { 43 | "files": [ 44 | "images/**" 45 | ], 46 | "exclude": [ 47 | "obj/**", 48 | "_site/**" 49 | ] 50 | } 51 | ], 52 | "dest": "_site", 53 | "globalMetadata": { 54 | "_appTitle": "Akka.Persistence.MongoDb", 55 | "_disableContribution": "true", 56 | "_appLogoPath": "/images/icon.png", 57 | }, 58 | "globalMetadataFiles": [], 59 | "fileMetadataFiles": [], 60 | "template": [ 61 | "default", 62 | "template" 63 | ], 64 | "postProcessors": [], 65 | "noLangKeyword": false 66 | } 67 | } -------------------------------------------------------------------------------- /docs/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akkadotnet/Akka.Persistence.MongoDB/03d5db4b57f9e18373e99c7b48565d404fc9c5d1/docs/images/icon.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction to My Project -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Home 2 | href: index.md 3 | - name: Documentation 4 | href: articles/ 5 | - name: API Reference 6 | href: api/ -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.203", 4 | "rollForward": "major" 5 | } 6 | } -------------------------------------------------------------------------------- /scripts/bumpVersion.ps1: -------------------------------------------------------------------------------- 1 | function UpdateVersionAndReleaseNotes { 2 | param ( 3 | [Parameter(Mandatory=$true)] 4 | [PSCustomObject]$ReleaseNotesResult, 5 | 6 | [Parameter(Mandatory=$true)] 7 | [string]$XmlFilePath 8 | ) 9 | 10 | # Load XML 11 | $xmlContent = New-Object XML 12 | $xmlContent.Load($XmlFilePath) 13 | 14 | # Update VersionPrefix and PackageReleaseNotes 15 | $versionPrefixElement = $xmlContent.SelectSingleNode("//VersionPrefix") 16 | $versionPrefixElement.InnerText = $ReleaseNotesResult.Version 17 | 18 | $packageReleaseNotesElement = $xmlContent.SelectSingleNode("//PackageReleaseNotes") 19 | $packageReleaseNotesElement.InnerText = $ReleaseNotesResult.ReleaseNotes 20 | 21 | # Save the updated XML 22 | $xmlContent.Save($XmlFilePath) 23 | } 24 | 25 | # Usage example: 26 | # $notes = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" 27 | # $propsPath = Join-Path -Path (Get-Item $PSScriptRoot).Parent.FullName -ChildPath "Directory.Build.props" 28 | # UpdateVersionAndReleaseNotes -ReleaseNotesResult $notes -XmlFilePath $propsPath 29 | -------------------------------------------------------------------------------- /scripts/getReleaseNotes.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReleaseNotes { 2 | param ( 3 | [Parameter(Mandatory=$true)] 4 | [string]$MarkdownFile 5 | ) 6 | 7 | # Read markdown file content 8 | $content = Get-Content -Path $MarkdownFile -Raw 9 | 10 | # Split content based on headers 11 | $sections = $content -split "####" 12 | 13 | # Output object to store result 14 | $outputObject = [PSCustomObject]@{ 15 | Version = $null 16 | Date = $null 17 | ReleaseNotes = $null 18 | } 19 | 20 | # Check if we have at least 3 sections (1. Before the header, 2. Header, 3. Release notes) 21 | if ($sections.Count -ge 3) { 22 | $header = $sections[1].Trim() 23 | $releaseNotes = $sections[2].Trim() 24 | 25 | # Extract version and date from the header 26 | $headerParts = $header -split " ", 2 27 | if ($headerParts.Count -eq 2) { 28 | $outputObject.Version = $headerParts[0] 29 | $outputObject.Date = $headerParts[1] 30 | } 31 | 32 | $outputObject.ReleaseNotes = $releaseNotes 33 | } 34 | 35 | # Return the output object 36 | return $outputObject 37 | } 38 | 39 | # Call function example: 40 | #$result = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" 41 | #Write-Output "Version: $($result.Version)" 42 | #Write-Output "Date: $($result.Date)" 43 | #Write-Output "Release Notes:" 44 | #Write-Output $result.ReleaseNotes 45 | -------------------------------------------------------------------------------- /serve-docs.cmd: -------------------------------------------------------------------------------- 1 | PowerShell.exe -file "serve-docs.ps1" %* .\docs\docfx.json --serve -p 8090 -------------------------------------------------------------------------------- /serve-docs.ps1: -------------------------------------------------------------------------------- 1 | # docfx.ps1 2 | $VisualStudioVersion = "15.0"; 3 | $DotnetSDKVersion = "2.0.0"; 4 | 5 | # Get dotnet paths 6 | $MSBuildExtensionsPath = "C:\Program Files\dotnet\sdk\" + $DotnetSDKVersion; 7 | $MSBuildSDKsPath = $MSBuildExtensionsPath + "\SDKs"; 8 | 9 | # Get Visual Studio install path 10 | $VSINSTALLDIR = $(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\SxS\VS7").$VisualStudioVersion; 11 | 12 | # Add Visual Studio environment variables 13 | $env:VisualStudioVersion = $VisualStudioVersion; 14 | $env:VSINSTALLDIR = $VSINSTALLDIR; 15 | 16 | # Add dotnet environment variables 17 | $env:MSBuildExtensionsPath = $MSBuildExtensionsPath; 18 | $env:MSBuildSDKsPath = $MSBuildSDKsPath; 19 | 20 | # Build our docs 21 | & .\tools\docfx.console\tools\docfx @args -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Hosting/Akka.Persistence.MongoDb.Hosting.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(NetStandardLibVersion);$(NetFrameworkLibVersion) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Hosting/MongoDbGridFsSnapshotOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Akka.Configuration; 4 | using Akka.Hosting; 5 | using Akka.Persistence.Hosting; 6 | using Akka.Persistence.MongoDb.Snapshot; 7 | 8 | #nullable enable 9 | namespace Akka.Persistence.MongoDb.Hosting; 10 | 11 | public class MongoDbGridFsSnapshotOptions : MongoDbSnapshotOptions 12 | { 13 | public MongoDbGridFsSnapshotOptions() : this(true) 14 | { 15 | } 16 | 17 | public MongoDbGridFsSnapshotOptions(bool isDefault, string identifier = "mongodb") : base(isDefault, identifier) 18 | { 19 | } 20 | 21 | public override Type Type { get; } = typeof(MongoDbGridFsSnapshotStore); 22 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Hosting/MongoDbJournalOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Akka.Configuration; 4 | using Akka.Hosting; 5 | using Akka.Persistence.Hosting; 6 | 7 | #nullable enable 8 | namespace Akka.Persistence.MongoDb.Hosting; 9 | 10 | public class MongoDbJournalOptions : JournalOptions 11 | { 12 | private static readonly Config Default = MongoDbPersistence.DefaultConfiguration() 13 | .GetConfig(MongoDbJournalSettings.JournalConfigPath); 14 | 15 | public MongoDbJournalOptions() : this(true) 16 | { 17 | } 18 | 19 | public MongoDbJournalOptions(bool isDefaultPlugin, string identifier = "mongodb") : base(isDefaultPlugin) 20 | { 21 | Identifier = identifier; 22 | AutoInitialize = true; 23 | } 24 | 25 | /// 26 | /// Connection string used to access the MongoDb, also specifies the database. 27 | /// 28 | public string ConnectionString { get; set; } = ""; 29 | 30 | /// 31 | /// Name of the collection for the event journal or snapshots 32 | /// 33 | public string? Collection { get; set; } 34 | 35 | /// 36 | /// Name of the collection for the event journal metadata 37 | /// 38 | public string? MetadataCollection { get; set; } 39 | 40 | /// 41 | /// Write Transaction 42 | /// 43 | public bool? UseWriteTransaction { get; set; } 44 | 45 | /// 46 | /// Read Transaction 47 | /// 48 | public bool? UseReadTransaction { get; set; } 49 | 50 | /// 51 | /// Set the batch size for read operations. 52 | /// If not set (null), this will use the default MongoDb behavior where all queries 53 | /// will have a batch size of 101 on the first page read and then will try to read 54 | /// as many documents as possible that fits the 16 MeBi limit. 55 | /// 56 | public int? ReadBatchSize { get; set; } 57 | 58 | /// 59 | /// When true, enables BSON serialization (which breaks features like Akka.Cluster.Sharding, AtLeastOnceDelivery, and so on.) 60 | /// 61 | public bool? LegacySerialization { get; set; } 62 | 63 | /// 64 | /// Timeout for individual database operations. 65 | /// 66 | /// 67 | /// Defaults to 10s. 68 | /// 69 | public TimeSpan? CallTimeout { get; set; } 70 | 71 | public override string Identifier { get; set; } 72 | protected override Config InternalDefaultConfig { get; } = Default; 73 | 74 | protected override StringBuilder Build(StringBuilder sb) 75 | { 76 | sb.AppendLine($"connection-string = {ConnectionString.ToHocon()}"); 77 | 78 | if(Collection is not null) 79 | sb.AppendLine($"collection = {Collection.ToHocon()}"); 80 | 81 | if(MetadataCollection is not null) 82 | sb.AppendLine($"metadata-collection = {MetadataCollection.ToHocon()}"); 83 | 84 | if(CallTimeout is not null) 85 | sb.AppendLine($"call-timeout = {CallTimeout.ToHocon()}"); 86 | 87 | if(UseWriteTransaction is not null) 88 | sb.AppendLine($"use-write-transaction = {UseWriteTransaction.ToHocon()}"); 89 | 90 | if(UseReadTransaction is not null) 91 | sb.AppendLine($"use-read-transaction = {UseReadTransaction.ToHocon()}"); 92 | 93 | sb.AppendLine($"read-batch-size = {(ReadBatchSize is not null ? ReadBatchSize.ToHocon() : "off")}"); 94 | 95 | if(LegacySerialization is not null) 96 | sb.AppendLine($"legacy-serialization = {LegacySerialization.ToHocon()}"); 97 | 98 | return base.Build(sb); 99 | } 100 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Hosting/MongoDbSnapshotOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Akka.Configuration; 4 | using Akka.Hosting; 5 | using Akka.Persistence.Hosting; 6 | using Akka.Persistence.MongoDb.Snapshot; 7 | 8 | #nullable enable 9 | namespace Akka.Persistence.MongoDb.Hosting; 10 | 11 | public class MongoDbSnapshotOptions : SnapshotOptions 12 | { 13 | private static readonly Config Default = MongoDbPersistence.DefaultConfiguration() 14 | .GetConfig(MongoDbSnapshotSettings.SnapshotStoreConfigPath); 15 | 16 | public MongoDbSnapshotOptions() : this(true) 17 | { 18 | } 19 | 20 | public MongoDbSnapshotOptions(bool isDefault, string identifier = "mongodb") : base(isDefault) 21 | { 22 | Identifier = identifier; 23 | AutoInitialize = true; 24 | } 25 | 26 | public virtual Type Type { get; } = typeof(MongoDbSnapshotStore); 27 | 28 | /// 29 | /// Connection string used to access the MongoDb, also specifies the database. 30 | /// 31 | public string ConnectionString { get; set; } = ""; 32 | 33 | /// 34 | /// Name of the collection for the event journal or snapshots 35 | /// 36 | public string? Collection { get; set; } 37 | 38 | /// 39 | /// Write Transaction 40 | /// 41 | public bool? UseWriteTransaction { get; set; } 42 | 43 | /// 44 | /// Read Transaction 45 | /// 46 | public bool? UseReadTransaction { get; set; } 47 | 48 | /// 49 | /// When true, enables BSON serialization (which breaks features like Akka.Cluster.Sharding, AtLeastOnceDelivery, and so on.) 50 | /// 51 | public bool? LegacySerialization { get; set; } 52 | 53 | /// 54 | /// Timeout for individual database operations. 55 | /// 56 | /// 57 | /// Defaults to 10s. 58 | /// 59 | public TimeSpan? CallTimeout { get; set; } 60 | 61 | public override string Identifier { get; set; } 62 | protected override Config InternalDefaultConfig { get; } = Default; 63 | 64 | protected override StringBuilder Build(StringBuilder sb) 65 | { 66 | sb.AppendLine($"class = {Type.AssemblyQualifiedName.ToHocon()}"); 67 | 68 | sb.AppendLine($"connection-string = {ConnectionString.ToHocon()}"); 69 | 70 | if(UseWriteTransaction is not null) 71 | sb.AppendLine($"use-write-transaction = {UseWriteTransaction.ToHocon()}"); 72 | 73 | if(UseReadTransaction is not null) 74 | sb.AppendLine($"use-read-transaction = {UseReadTransaction.ToHocon()}"); 75 | 76 | if(Collection is not null) 77 | sb.AppendLine($"collection = {Collection.ToHocon()}"); 78 | 79 | if(LegacySerialization is not null) 80 | sb.AppendLine($"legacy-serialization = {LegacySerialization.ToHocon()}"); 81 | 82 | if(CallTimeout is not null) 83 | sb.AppendLine($"call-timeout = {CallTimeout.ToHocon()}"); 84 | 85 | return base.Build(sb); 86 | } 87 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Hosting/README.md: -------------------------------------------------------------------------------- 1 | # Akka.Persistence.MongoDb.Hosting 2 | 3 | Akka.Hosting extension methods to add Akka.Persistence.MongoDb to an ActorSystem 4 | 5 | # Akka.Persistence.MongoDb Extension Methods 6 | 7 | ## WithMongoDbPersistence() Method 8 | 9 | ```csharp 10 | public static AkkaConfigurationBuilder WithMongoDbPersistence( 11 | this AkkaConfigurationBuilder builder, 12 | string connectionString, 13 | PersistenceMode mode = PersistenceMode.Both, 14 | bool autoInitialize = true, 15 | Action? journalBuilder = null, 16 | string pluginIdentifier = "mongodb", 17 | bool isDefaultPlugin = true); 18 | ``` 19 | 20 | ```csharp 21 | public static AkkaConfigurationBuilder WithMongoDbPersistence( 22 | this AkkaConfigurationBuilder builder, 23 | Action? journalOptionConfigurator = null, 24 | Action? snapshotOptionConfigurator = null, 25 | bool isDefaultPlugin = true) 26 | ``` 27 | 28 | ```csharp 29 | public static AkkaConfigurationBuilder WithMongoDbPersistence( 30 | this AkkaConfigurationBuilder builder, 31 | MongoDbJournalOptions? journalOptions = null, 32 | MongoDbSnapshotOptions? snapshotOptions = null) 33 | ``` 34 | 35 | ### Parameters 36 | 37 | * `connectionString` __string__ 38 | 39 | Connection string used for database access. 40 | 41 | * `mode` __PersistenceMode__ 42 | 43 | Determines which settings should be added by this method call. __Default__: `PersistenceMode.Both` 44 | 45 | * `PersistenceMode.Journal`: Only add the journal settings 46 | * `PersistenceMode.SnapshotStore`: Only add the snapshot store settings 47 | * `PersistenceMode.Both`: Add both journal and snapshot store settings 48 | 49 | * `autoInitialize` __bool__ 50 | 51 | Should the Mongo Db store collection be initialized automatically. __Default__: `false` 52 | 53 | * `journalBuilder` __Action\__ 54 | 55 | An Action delegate used to configure an `AkkaPersistenceJournalBuilder` instance. Used to configure [Event Adapters](https://getakka.net/articles/persistence/event-adapters.html) 56 | 57 | * `journalConfigurator` __Action\__ 58 | 59 | An Action delegate to configure a `MongoDbJournalOptions` instance. 60 | 61 | * `snapshotConfigurator` __Action\__ 62 | 63 | An Action delegate to configure a `MongoDbSnapshotOptions` instance. 64 | 65 | * `journalOptions` __MongoDbJournalOptions__ 66 | 67 | An `MongoDbJournalOptions` instance to configure the SqlServer journal. 68 | 69 | * `snapshotOptions` __MongoDbSnapshotOptions__ 70 | 71 | An `MongoDbSnapshotOptions` instance to configure the SqlServer snapshot store. 72 | 73 | ## Example 74 | 75 | ```csharp 76 | using var host = new HostBuilder() 77 | .ConfigureServices((context, services) => 78 | { 79 | services.AddAkka("mongoDbDemo", (builder, provider) => 80 | { 81 | builder 82 | .WithMongoDbPersistence("your-mongodb-connection-string"); 83 | }); 84 | }).Build(); 85 | 86 | await host.RunAsync(); 87 | ``` -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Akka.Persistence.MongoDb.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | 7 | $(NetFrameworkTestVersion);$(NetTestVersion) 8 | 9 | 10 | 11 | 12 | $(NetTestVersion) 13 | 14 | 15 | 16 | $(DefineConstants);CICD 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Bug25FixSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Akka.Configuration; 7 | using Akka.Persistence.MongoDb.Journal; 8 | using Akka.Util.Internal; 9 | using Akka.Actor; 10 | using MongoDB.Driver; 11 | using Xunit; 12 | using Xunit.Abstractions; 13 | 14 | namespace Akka.Persistence.MongoDb.Tests 15 | { 16 | [Collection("MongoDbSpec")] 17 | public class Bug25FixSpec : Akka.TestKit.Xunit2.TestKit, IClassFixture 18 | { 19 | class MyJournalActor : ReceivePersistentActor 20 | { 21 | private readonly IActorRef _target; 22 | 23 | public MyJournalActor(string persistenceId, IActorRef target) 24 | { 25 | PersistenceId = persistenceId; 26 | _target = target; 27 | 28 | Recover(str => 29 | { 30 | target.Tell(str); 31 | }); 32 | 33 | Command(str => 34 | { 35 | Persist(str, s => 36 | { 37 | Sender.Tell(s); 38 | }); 39 | }); 40 | } 41 | 42 | public override string PersistenceId { get; } 43 | } 44 | 45 | private readonly MongoUrl _connectionString; 46 | private Lazy _database; 47 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 48 | private readonly ITestOutputHelper _output; 49 | 50 | public Bug25FixSpec(ITestOutputHelper helper, DatabaseFixture fixture) 51 | : base(CreateSpecConfig(fixture, Counter.Current), output: helper) 52 | { 53 | _connectionString = new MongoUrl(fixture.MongoDbConnectionString(Counter.Current)); 54 | Counter.IncrementAndGet(); 55 | _output = helper; 56 | _database = new Lazy(() => 57 | new MongoClient(_connectionString) 58 | .GetDatabase(_connectionString.DatabaseName)); 59 | } 60 | 61 | [Fact(DisplayName = "Bugfix: Should be able to deserialize older entities written without manifests")] 62 | public async Task Should_deserialize_entries_without_manifest() 63 | { 64 | var persistenceId = "fuber"; 65 | var props = Props.Create(() => new MyJournalActor(persistenceId, TestActor)); 66 | var myPersistentActor = Sys.ActorOf(props); 67 | Watch(myPersistentActor); 68 | 69 | myPersistentActor.Tell("hit"); 70 | ExpectMsg("hit"); 71 | 72 | Sys.Stop(myPersistentActor); 73 | ExpectTerminated(myPersistentActor); 74 | 75 | var records = _database.Value.GetCollection("EventJournal"); 76 | 77 | var builder = Builders.Filter; 78 | var filter = builder.Eq(x => x.PersistenceId, persistenceId); 79 | filter &= builder.Eq(x => x.SequenceNr, 1); 80 | 81 | var sort = Builders.Sort.Ascending(x => x.SequenceNr); 82 | 83 | AwaitCondition(() => records.CountDocuments(filter) > 0); 84 | var collections = await records 85 | .Find(filter) 86 | .Sort(sort) 87 | .ToListAsync(); 88 | 89 | var entry = collections.Single(); 90 | entry.Manifest = null; // null out the manifest 91 | await records.FindOneAndReplaceAsync(filter, entry); 92 | 93 | // recreate the persistent actor 94 | var myPersistentActor2 = Sys.ActorOf(props); 95 | ExpectMsg("hit"); // receive message upon recovery 96 | } 97 | 98 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 99 | { 100 | var specString = @" 101 | akka.test.single-expect-default = 10s 102 | akka.persistence { 103 | publish-plugin-commands = on 104 | journal { 105 | plugin = ""akka.persistence.journal.mongodb"" 106 | mongodb { 107 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 108 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 109 | auto-initialize = on 110 | collection = ""EventJournal"" 111 | } 112 | } 113 | }"; 114 | 115 | return ConfigurationFactory.ParseString(specString) 116 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Bug61FixSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Configuration; 3 | using Akka.Persistence.Journal; 4 | using Akka.Persistence.MongoDb.Query; 5 | using Akka.Persistence.Query; 6 | using Akka.Streams; 7 | using Akka.Util.Internal; 8 | using FluentAssertions; 9 | using MongoDB.Driver.Core.Configuration; 10 | using MongoDB.Driver.Core.Misc; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Collections.Immutable; 14 | using System.Linq; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using Xunit; 18 | using Xunit.Abstractions; 19 | 20 | namespace Akka.Persistence.MongoDb.Tests 21 | { 22 | [Collection("MongoDbSpec")] 23 | public class Bug61FixSpec : Akka.TestKit.Xunit2.TestKit, IClassFixture 24 | { 25 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 26 | private readonly ITestOutputHelper _output; 27 | 28 | protected MongoDbReadJournal ReadJournal { get; } 29 | 30 | protected IMaterializer Materializer { get; } 31 | 32 | public class RealMsg 33 | { 34 | public RealMsg(string msg) 35 | { 36 | Msg = msg; 37 | } 38 | public string Msg { get; } 39 | } 40 | 41 | public const int MessageCount = 20; 42 | 43 | public Bug61FixSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 44 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement()), "MongoDbCurrentEventsByTagSpec", output) 45 | { 46 | _output = output; 47 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 48 | Materializer = Sys.Materializer(); 49 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 50 | } 51 | 52 | /// 53 | /// Reproduction spec for https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/61 54 | /// 55 | [Fact] 56 | public async Task Bug61_Events_Recovered_By_Id_Should_Match_Tag() 57 | { 58 | var actor = Sys.ActorOf(TagActor.Props("x")); 59 | 60 | actor.Tell(MessageCount); 61 | ExpectMsg($"{MessageCount}-done", TimeSpan.FromSeconds(20)); 62 | 63 | var eventsById = await ReadJournal.CurrentEventsByPersistenceId("x", 0L, long.MaxValue) 64 | .RunAggregate(ImmutableHashSet.Empty, (agg, e) => agg.Add(e), Materializer); 65 | 66 | eventsById.Count.Should().Be(MessageCount); 67 | 68 | var eventsByTag = await ReadJournal.CurrentEventsByTag(typeof(RealMsg).Name) 69 | .RunAggregate(ImmutableHashSet.Empty, (agg, e) => agg.Add(e), Materializer); 70 | 71 | eventsByTag.Count.Should().Be(MessageCount); 72 | 73 | eventsById.All(x => x.Event is RealMsg).Should().BeTrue("Expected all events by id to be RealMsg"); 74 | eventsByTag.All(x => x.Event is RealMsg).Should().BeTrue("Expected all events by tag to be RealMsg"); 75 | } 76 | 77 | /// 78 | /// Reproduction spec for https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/80 79 | /// 80 | [Fact] 81 | public void Bug80_CurrentEventsByTag_should_Recover_until_end() 82 | { 83 | var actor = Sys.ActorOf(TagActor.Props("y")); 84 | //increased this to test for non-collision with the generated timestamps 85 | var msgCount = 5000; 86 | actor.Tell(msgCount); 87 | ExpectMsg($"{msgCount}-done", TimeSpan.FromSeconds(20)); 88 | 89 | var eventsByTag = ReadJournal.CurrentEventsByTag(typeof(RealMsg).Name) 90 | .RunForeach(e => TestActor.Tell(e), Materializer); 91 | 92 | ReceiveN(msgCount); 93 | } 94 | 95 | /// 96 | /// Making sure EventsByTag didn't break during implementation of https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/80 97 | /// 98 | [Fact] 99 | public void Bug80_AllEventsByTag_should_Recover_all_messages() 100 | { 101 | var actor = Sys.ActorOf(TagActor.Props("y")); 102 | //increased this to test for non-collision with the generated timestamps 103 | var msgCount = 5000; 104 | actor.Tell(msgCount); 105 | ExpectMsg($"{msgCount}-done", TimeSpan.FromSeconds(20)); 106 | 107 | var eventsByTag = ReadJournal.EventsByTag(typeof(RealMsg).Name) 108 | .RunForeach(e => TestActor.Tell(e), Materializer); 109 | 110 | // can't do this because Offset isn't IComparable 111 | // ReceiveN(msgCount).Cast().Select(x => x.Offset).Should().BeInAscendingOrder(); 112 | ReceiveN(msgCount); 113 | 114 | // should receive more messages after the fact 115 | actor.Tell(msgCount); 116 | ExpectMsg($"{msgCount}-done", TimeSpan.FromSeconds(20)); 117 | ReceiveN(msgCount); 118 | } 119 | 120 | private class TagActor : ReceivePersistentActor 121 | { 122 | public static Props Props(string id) 123 | { 124 | return Akka.Actor.Props.Create(() => new TagActor(id)); 125 | } 126 | 127 | public TagActor(string id) 128 | { 129 | PersistenceId = id; 130 | 131 | Command(i => 132 | { 133 | var msgs = new List(); 134 | foreach (var n in Enumerable.Range(0, i)) 135 | { 136 | msgs.Add(new RealMsg(i.ToString())); 137 | } 138 | PersistAll(msgs, m => 139 | { 140 | if (LastSequenceNr >= i) 141 | { 142 | Sender.Tell($"{i}-done"); 143 | } 144 | }); 145 | }); 146 | 147 | Command(r => 148 | { 149 | Persist(r, e => 150 | { 151 | Sender.Tell($"{e.Msg}-done"); 152 | }); 153 | }); 154 | } 155 | 156 | public override string PersistenceId { get; } 157 | } 158 | 159 | private class EventTagger : IWriteEventAdapter 160 | { 161 | public string DefaultTag { get; } 162 | 163 | public EventTagger() 164 | { 165 | DefaultTag = "accounts"; 166 | } 167 | 168 | public string Manifest(object evt) 169 | { 170 | return string.Empty; 171 | } 172 | 173 | public object ToJournal(object evt) 174 | { 175 | return new Tagged(evt, ImmutableHashSet.Empty.Add(DefaultTag).Add(evt.GetType().Name)); 176 | } 177 | } 178 | 179 | 180 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 181 | { 182 | var specString = @" 183 | akka.test.single-expect-default = 10s 184 | akka.persistence { 185 | publish-plugin-commands = on 186 | journal { 187 | plugin = ""akka.persistence.journal.mongodb"" 188 | mongodb { 189 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 190 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 191 | auto-initialize = on 192 | collection = ""EventJournal"" 193 | event-adapters { 194 | tagger = """ + typeof(EventTagger).AssemblyQualifiedName + @""" 195 | } 196 | event-adapter-bindings = { 197 | """ + typeof(RealMsg).AssemblyQualifiedName + @""" = tagger 198 | } 199 | stored-as = binary 200 | } 201 | } 202 | query { 203 | mongodb { 204 | class = ""Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb"" 205 | refresh-interval = 1s 206 | } 207 | } 208 | }"; 209 | 210 | return ConfigurationFactory.ParseString(specString); 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Util.Internal; 9 | using Mongo2Go; 10 | using MongoDB.Driver.Core.Misc; 11 | using System; 12 | 13 | namespace Akka.Persistence.MongoDb.Tests 14 | { 15 | public class DatabaseFixture : IDisposable 16 | { 17 | private MongoDbRunner _runner; 18 | public string ConnectionString { get; private set; } 19 | 20 | public DatabaseFixture() 21 | { 22 | _runner = MongoDbRunner.Start(singleNodeReplSet: true); 23 | //_runner = MongoDbRunner.Start(); 24 | ConnectionString = ConString(_runner.ConnectionString);// + "akkanet"; 25 | } 26 | public string MongoDbConnectionString(int id) 27 | { 28 | var s = ConnectionString.Split('?'); 29 | var connectionString = s[0] + $"{id}?" + s[1]; 30 | return connectionString; 31 | } 32 | private string ConString(string cString) 33 | { 34 | var s = cString.Split('?'); 35 | var connectionString = s[0] + $"akkanet?" + s[1]; 36 | return connectionString; 37 | } 38 | public void Dispose() 39 | { 40 | _runner.Dispose(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/GridFS/MongoDbGridFsLegacySerializationSnapshotStoreSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using System.Diagnostics; 10 | using System.Security.Cryptography; 11 | using System.Threading.Tasks; 12 | using Akka.Configuration; 13 | using Akka.Event; 14 | using Akka.Persistence.TCK.Snapshot; 15 | using FluentAssertions; 16 | using Xunit; 17 | using Xunit.Abstractions; 18 | 19 | #nullable enable 20 | namespace Akka.Persistence.MongoDb.Tests.GridFS; 21 | 22 | [Collection("MongoDbSpec")] 23 | public class MongoDbGridFsLegacySerializationSnapshotStoreSpec : SnapshotStoreSpec, IClassFixture 24 | { 25 | protected override bool SupportsSerialization => false; 26 | 27 | public MongoDbGridFsLegacySerializationSnapshotStoreSpec(DatabaseFixture databaseFixture, ITestOutputHelper output) 28 | : base(CreateSpecConfig(databaseFixture), nameof(MongoDbGridFsLegacySerializationSnapshotStoreSpec), output) 29 | { 30 | Initialize(); 31 | } 32 | 33 | protected override int SnapshotByteSizeLimit => 128 * 1024 * 1024; 34 | 35 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 36 | { 37 | var specString = @" 38 | akka.test.single-expect-default = 3s 39 | akka.persistence { 40 | publish-plugin-commands = on 41 | snapshot-store { 42 | plugin = ""akka.persistence.snapshot-store.mongodb"" 43 | mongodb { 44 | class = ""Akka.Persistence.MongoDb.Snapshot.MongoDbGridFsSnapshotStore, Akka.Persistence.MongoDb"" 45 | connection-string = """ + databaseFixture.ConnectionString + @""" 46 | auto-initialize = on 47 | collection = ""SnapshotStore"" 48 | legacy-serialization = on 49 | } 50 | } 51 | }"; 52 | 53 | return ConfigurationFactory.ParseString(specString) 54 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 55 | } 56 | 57 | [Fact] 58 | public async Task SnapshotStore_should_save_bigger_size_snapshot_consistently() 59 | { 60 | var metadata = new SnapshotMetadata(Pid, 100, DateTime.MinValue); 61 | var bigSnapshot = new byte[SnapshotByteSizeLimit]; 62 | new Random().NextBytes(bigSnapshot); 63 | var senderProbe = CreateTestProbe(); 64 | SnapshotStore.Tell(new SaveSnapshot(metadata, bigSnapshot), senderProbe.Ref); 65 | var saved = await senderProbe.ExpectMsgAsync(); 66 | 67 | var stopwatch = Stopwatch.StartNew(); 68 | SnapshotStore.Tell( 69 | new LoadSnapshot(Pid, new SnapshotSelectionCriteria(saved.Metadata.SequenceNr), long.MaxValue), 70 | senderProbe.Ref); 71 | var loaded = await senderProbe.ExpectMsgAsync(); 72 | stopwatch.Stop(); 73 | Log.Info($"{SnapshotByteSizeLimit} bytes snapshot loaded in {stopwatch.Elapsed.Milliseconds} milliseconds"); 74 | 75 | MD5.Create().ComputeHash((byte[])loaded.Snapshot.Snapshot).Should() 76 | .BeEquivalentTo(MD5.Create().ComputeHash(bigSnapshot)); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/GridFS/MongoDbGridFsSnapshotStoreSaveSnapshotSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | using Akka.Persistence.TCK.Snapshot; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace Akka.Persistence.MongoDb.Tests.GridFS; 7 | 8 | [Collection("MongoDbSpec")] 9 | public class MongoDbGridFsSnapshotStoreSaveSnapshotSpec: SnapshotStoreSaveSnapshotSpec, IClassFixture 10 | { 11 | public MongoDbGridFsSnapshotStoreSaveSnapshotSpec(DatabaseFixture databaseFixture, ITestOutputHelper output) 12 | : base(CreateSpecConfig(databaseFixture), nameof(MongoDbGridFsSnapshotStoreSaveSnapshotSpec), output) 13 | { 14 | } 15 | 16 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 17 | { 18 | var specString = $$""" 19 | akka.test.single-expect-default = 3s 20 | akka.persistence { 21 | publish-plugin-commands = on 22 | snapshot-store { 23 | plugin = "akka.persistence.snapshot-store.mongodb" 24 | mongodb { 25 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbGridFsSnapshotStore, Akka.Persistence.MongoDb" 26 | connection-string = "{{databaseFixture.ConnectionString}}" 27 | use-write-transaction = off 28 | auto-initialize = on 29 | collection = "SnapshotStore" 30 | } 31 | } 32 | } 33 | """; 34 | 35 | return ConfigurationFactory.ParseString(specString); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/GridFS/MongoDbGridFsSnapshotStoreSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using System.Diagnostics; 10 | using System.Security.Cryptography; 11 | using System.Threading.Tasks; 12 | using Akka.Configuration; 13 | using Akka.Event; 14 | using Akka.Persistence.TCK.Snapshot; 15 | using FluentAssertions; 16 | using Xunit; 17 | using Xunit.Abstractions; 18 | 19 | #nullable enable 20 | namespace Akka.Persistence.MongoDb.Tests.GridFS; 21 | 22 | [Collection("MongoDbSpec")] 23 | public class MongoDbGridFsSnapshotStoreSpec : SnapshotStoreSpec, IClassFixture 24 | { 25 | public MongoDbGridFsSnapshotStoreSpec(DatabaseFixture databaseFixture, ITestOutputHelper output) 26 | : base(CreateSpecConfig(databaseFixture), nameof(MongoDbGridFsSnapshotStoreSpec), output) 27 | { 28 | Initialize(); 29 | } 30 | 31 | protected override int SnapshotByteSizeLimit => 128 * 1024 * 1024; 32 | 33 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 34 | { 35 | var specString = $$""" 36 | akka.test.single-expect-default = 3s 37 | akka.persistence { 38 | publish-plugin-commands = on 39 | snapshot-store { 40 | plugin = "akka.persistence.snapshot-store.mongodb" 41 | mongodb { 42 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbGridFsSnapshotStore, Akka.Persistence.MongoDb" 43 | connection-string = "{{databaseFixture.ConnectionString}}" 44 | use-write-transaction = off 45 | auto-initialize = on 46 | collection = "SnapshotStore" 47 | } 48 | } 49 | } 50 | """; 51 | 52 | return ConfigurationFactory.ParseString(specString); 53 | } 54 | 55 | [Fact] 56 | public async Task SnapshotStore_should_save_bigger_size_snapshot_consistently() 57 | { 58 | var metadata = new SnapshotMetadata(Pid, 100, DateTime.MinValue); 59 | var bigSnapshot = new byte[SnapshotByteSizeLimit]; 60 | new Random().NextBytes(bigSnapshot); 61 | var senderProbe = CreateTestProbe(); 62 | SnapshotStore.Tell(new SaveSnapshot(metadata, bigSnapshot), senderProbe.Ref); 63 | var saved = await senderProbe.ExpectMsgAsync(); 64 | 65 | var stopwatch = Stopwatch.StartNew(); 66 | SnapshotStore.Tell( 67 | new LoadSnapshot(Pid, new SnapshotSelectionCriteria(saved.Metadata.SequenceNr), long.MaxValue), 68 | senderProbe.Ref); 69 | var loaded = await senderProbe.ExpectMsgAsync(); 70 | stopwatch.Stop(); 71 | Log.Info($"{SnapshotByteSizeLimit} bytes snapshot loaded in {stopwatch.Elapsed.Milliseconds} milliseconds"); 72 | 73 | MD5.Create().ComputeHash((byte[])loaded.Snapshot.Snapshot).Should() 74 | .BeEquivalentTo(MD5.Create().ComputeHash(bigSnapshot)); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/GridFS/Serialization/MongoDbGridFsSnapshotStoreSerializationSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | using Akka.Persistence.TCK.Serialization; 3 | using Akka.Util.Internal; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | #nullable enable 8 | namespace Akka.Persistence.MongoDb.Tests.GridFS.Serialization; 9 | 10 | [Collection("MongoDbSpec")] 11 | public class MongoDbSnapshotStoreSerializationSpec : SnapshotStoreSerializationSpec, IClassFixture 12 | { 13 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 14 | 15 | private readonly ITestOutputHelper _output; 16 | 17 | public MongoDbSnapshotStoreSerializationSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 18 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement()), nameof(MongoDbSnapshotStoreSerializationSpec), output) 19 | { 20 | _output = output; 21 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 22 | } 23 | 24 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 25 | { 26 | var specString = @" 27 | akka.test.single-expect-default = 3s 28 | akka.persistence { 29 | publish-plugin-commands = on 30 | snapshot-store { 31 | plugin = ""akka.persistence.snapshot-store.mongodb"" 32 | mongodb { 33 | class = ""Akka.Persistence.MongoDb.Snapshot.MongoDbGridFsSnapshotStore, Akka.Persistence.MongoDb"" 34 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 35 | auto-initialize = on 36 | collection = ""SnapshotStore"" 37 | } 38 | } 39 | }"; 40 | 41 | return ConfigurationFactory.ParseString(specString); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbJournalOptionsSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Akka.Configuration; 5 | using Akka.Persistence.MongoDb.Hosting; 6 | using FluentAssertions; 7 | using FluentAssertions.Extensions; 8 | using Microsoft.Extensions.Configuration; 9 | using Xunit; 10 | 11 | namespace Akka.Persistence.MongoDb.Tests.Hosting 12 | { 13 | public class MongoDbJournalOptionsSpec 14 | { 15 | [Fact(DisplayName = "MongoDbJournalOptions as default plugin should generate plugin setting")] 16 | public void DefaultPluginJournalOptionsTest() 17 | { 18 | var options = new MongoDbJournalOptions(true); 19 | var config = options.ToConfig(); 20 | 21 | config.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.mongodb"); 22 | config.HasPath("akka.persistence.journal.mongodb").Should().BeTrue(); 23 | } 24 | 25 | [Fact(DisplayName = "Empty MongoDbJournalOptions should equal empty config with default fallback")] 26 | public void DefaultJournalOptionsTest() 27 | { 28 | var options = new MongoDbJournalOptions(false); 29 | var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); 30 | var baseRootConfig = Config.Empty 31 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 32 | 33 | emptyRootConfig.GetString("akka.persistence.journal.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.journal.plugin")); 34 | 35 | var config = emptyRootConfig.GetConfig("akka.persistence.journal.mongodb"); 36 | var baseConfig = baseRootConfig.GetConfig("akka.persistence.journal.mongodb"); 37 | config.Should().NotBeNull(); 38 | baseConfig.Should().NotBeNull(); 39 | 40 | config.GetString("class").Should().Be(baseConfig.GetString("class")); 41 | config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); 42 | config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); 43 | config.GetBoolean("use-read-transaction").Should().Be(baseConfig.GetBoolean("use-read-transaction")); 44 | config.GetBoolean("read-batch-size").Should().Be(baseConfig.GetBoolean("read-batch-size")); 45 | config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); 46 | config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); 47 | config.GetString("collection").Should().Be(baseConfig.GetString("collection")); 48 | config.GetString("metadata-collection").Should().Be(baseConfig.GetString("metadata-collection")); 49 | config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); 50 | config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); 51 | } 52 | 53 | [Fact(DisplayName = "Empty MongoDbJournalOptions with custom identifier should equal empty config with default fallback")] 54 | public void CustomIdJournalOptionsTest() 55 | { 56 | var options = new MongoDbJournalOptions(false, "custom"); 57 | var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); 58 | var baseRootConfig = Config.Empty 59 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 60 | 61 | emptyRootConfig.GetString("akka.persistence.journal.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.journal.plugin")); 62 | 63 | var config = emptyRootConfig.GetConfig("akka.persistence.journal.custom"); 64 | var baseConfig = baseRootConfig.GetConfig("akka.persistence.journal.mongodb"); 65 | config.Should().NotBeNull(); 66 | baseConfig.Should().NotBeNull(); 67 | 68 | config.GetString("class").Should().Be(baseConfig.GetString("class")); 69 | config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); 70 | config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); 71 | config.GetBoolean("use-read-transaction").Should().Be(baseConfig.GetBoolean("use-read-transaction")); 72 | config.GetBoolean("read-batch-size").Should().Be(baseConfig.GetBoolean("read-batch-size")); 73 | config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); 74 | config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); 75 | config.GetString("collection").Should().Be(baseConfig.GetString("collection")); 76 | config.GetString("metadata-collection").Should().Be(baseConfig.GetString("metadata-collection")); 77 | config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); 78 | config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); 79 | } 80 | 81 | [Fact(DisplayName = "MongoDbJournalOptions should generate proper config")] 82 | public void JournalOptionsTest() 83 | { 84 | var options = new MongoDbJournalOptions(true) 85 | { 86 | Identifier = "custom", 87 | AutoInitialize = true, 88 | ConnectionString = "testConnection", 89 | Collection = "testCollection", 90 | MetadataCollection = "metadataCollection", 91 | UseWriteTransaction = true, 92 | UseReadTransaction = true, 93 | ReadBatchSize = 16, 94 | LegacySerialization = true, 95 | CallTimeout = TimeSpan.FromHours(2) 96 | }; 97 | 98 | var baseConfig = options.ToConfig(); 99 | 100 | baseConfig.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.custom"); 101 | 102 | var config = baseConfig.GetConfig("akka.persistence.journal.custom"); 103 | config.Should().NotBeNull(); 104 | config.GetString("connection-string").Should().Be(options.ConnectionString); 105 | config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); 106 | config.GetString("collection").Should().Be(options.Collection); 107 | config.GetString("metadata-collection").Should().Be(options.MetadataCollection); 108 | config.GetBoolean("use-write-transaction").Should().Be(options.UseWriteTransaction.Value); 109 | config.GetBoolean("use-read-transaction").Should().Be(options.UseReadTransaction.Value); 110 | config.GetString("read-batch-size").ToLowerInvariant().Should().NotBe("off"); 111 | config.GetString("read-batch-size").ToLowerInvariant().Should().NotBe("false"); 112 | config.GetInt("read-batch-size").Should().Be(options.ReadBatchSize.Value); 113 | config.GetBoolean("legacy-serialization").Should().Be(options.LegacySerialization.Value); 114 | config.GetTimeSpan("call-timeout").Should().Be(options.CallTimeout.Value); 115 | } 116 | 117 | const string Json = @" 118 | { 119 | ""Logging"": { 120 | ""LogLevel"": { 121 | ""Default"": ""Information"", 122 | ""Microsoft.AspNetCore"": ""Warning"" 123 | } 124 | }, 125 | ""Akka"": { 126 | ""JournalOptions"": { 127 | ""ConnectionString"": ""mongodb://localhost:27017"", 128 | ""UseWriteTransaction"": ""true"", 129 | ""UseReadTransaction"": ""true"", 130 | ""ReadBatchSize"": 16, 131 | ""Identifier"": ""custommongodb"", 132 | ""AutoInitialize"": true, 133 | ""IsDefaultPlugin"": false, 134 | ""Collection"": ""CustomEnventJournalCollection"", 135 | ""MetadataCollection"": ""CustomMetadataCollection"", 136 | ""LegacySerialization"" : ""true"", 137 | ""CallTimeout"": ""00:10:00"", 138 | ""Serializer"": ""hyperion"", 139 | } 140 | } 141 | }"; 142 | 143 | [Fact(DisplayName = "MongoDbJournalOptions should be bindable to IConfiguration")] 144 | public void JournalOptionsIConfigurationBindingTest() 145 | { 146 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(Json)); 147 | var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); 148 | 149 | var options = jsonConfig.GetSection("Akka:JournalOptions").Get(); 150 | options.ConnectionString.Should().Be("mongodb://localhost:27017"); 151 | options.UseWriteTransaction.Should().BeTrue(); 152 | options.UseReadTransaction.Should().BeTrue(); 153 | options.ReadBatchSize.Should().Be(16); 154 | options.Identifier.Should().Be("custommongodb"); 155 | options.AutoInitialize.Should().BeTrue(); 156 | options.IsDefaultPlugin.Should().BeFalse(); 157 | options.Collection.Should().Be("CustomEnventJournalCollection"); 158 | options.MetadataCollection.Should().Be("CustomMetadataCollection"); 159 | options.LegacySerialization.Should().BeTrue(); 160 | options.CallTimeout.Should().Be(10.Minutes()); 161 | options.Serializer.Should().Be("hyperion"); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbSnapshotOptionsSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Akka.Configuration; 5 | using Akka.Persistence.MongoDb.Hosting; 6 | using FluentAssertions; 7 | using FluentAssertions.Extensions; 8 | using Microsoft.Extensions.Configuration; 9 | using Xunit; 10 | 11 | namespace Akka.Persistence.MongoDb.Tests.Hosting 12 | { 13 | public class MongoDbSnapshotOptionsSpec 14 | { 15 | [Fact(DisplayName = "MongoDbSnapshotOptions as default plugin should generate plugin setting")] 16 | public void DefaultPluginSnapshotOptionsTest() 17 | { 18 | var options = new MongoDbSnapshotOptions(true); 19 | var config = options.ToConfig(); 20 | 21 | config.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.mongodb"); 22 | config.HasPath("akka.persistence.snapshot-store.mongodb").Should().BeTrue(); 23 | } 24 | 25 | [Fact(DisplayName = "Empty MongoDbSnapshotOptions with default fallback should return default config")] 26 | public void DefaultSnapshotOptionsTest() 27 | { 28 | var options = new MongoDbSnapshotOptions(false); 29 | var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); 30 | var baseRootConfig = Config.Empty 31 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 32 | 33 | emptyRootConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.snapshot-store.plugin")); 34 | 35 | var config = emptyRootConfig.GetConfig("akka.persistence.snapshot-store.mongodb"); 36 | var baseConfig = baseRootConfig.GetConfig("akka.persistence.snapshot-store.mongodb"); 37 | config.Should().NotBeNull(); 38 | baseConfig.Should().NotBeNull(); 39 | 40 | Type.GetType(config.GetString("class")).Should().Be(Type.GetType(baseConfig.GetString("class"))); 41 | config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); 42 | config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); 43 | config.GetBoolean("use-read-transaction").Should().Be(baseConfig.GetBoolean("use-read-transaction")); 44 | config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); 45 | config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); 46 | config.GetString("collection").Should().Be(baseConfig.GetString("collection")); 47 | config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); 48 | config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); 49 | } 50 | 51 | [Fact(DisplayName = "Empty MongoDbSnapshotOptions with custom identifier should equal empty config with default fallback")] 52 | public void CustomIdSnapshotOptionsTest() 53 | { 54 | var options = new MongoDbSnapshotOptions(false, "custom"); 55 | var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); 56 | var baseRootConfig = Config.Empty 57 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 58 | 59 | emptyRootConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.snapshot-store.plugin")); 60 | 61 | var config = emptyRootConfig.GetConfig("akka.persistence.snapshot-store.custom"); 62 | var baseConfig = baseRootConfig.GetConfig("akka.persistence.snapshot-store.mongodb"); 63 | config.Should().NotBeNull(); 64 | baseConfig.Should().NotBeNull(); 65 | 66 | Type.GetType(config.GetString("class")).Should().Be(Type.GetType(baseConfig.GetString("class"))); 67 | config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); 68 | config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); 69 | config.GetBoolean("use-read-transaction").Should().Be(baseConfig.GetBoolean("use-read-transaction")); 70 | config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); 71 | config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); 72 | config.GetString("collection").Should().Be(baseConfig.GetString("collection")); 73 | config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); 74 | config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); 75 | } 76 | 77 | [Fact(DisplayName = "MongoDbSnapshotOptions should generate proper config")] 78 | public void SnapshotOptionsTest() 79 | { 80 | var options = new MongoDbSnapshotOptions(true) 81 | { 82 | Identifier = "custom", 83 | AutoInitialize = true, 84 | ConnectionString = "testConnection", 85 | Collection = "testCollection", 86 | UseWriteTransaction = true, 87 | UseReadTransaction = true, 88 | LegacySerialization = true, 89 | CallTimeout = TimeSpan.FromHours(2) 90 | }; 91 | 92 | var baseConfig = options.ToConfig() 93 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 94 | 95 | baseConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.custom"); 96 | 97 | var config = baseConfig.GetConfig("akka.persistence.snapshot-store.custom"); 98 | config.Should().NotBeNull(); 99 | config.GetString("connection-string").Should().Be(options.ConnectionString); 100 | config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); 101 | config.GetString("collection").Should().Be(options.Collection); 102 | config.GetBoolean("use-write-transaction").Should().Be(options.UseWriteTransaction.Value); 103 | config.GetBoolean("use-read-transaction").Should().Be(options.UseReadTransaction.Value); 104 | config.GetBoolean("legacy-serialization").Should().Be(options.LegacySerialization.Value); 105 | config.GetTimeSpan("call-timeout").Should().Be(options.CallTimeout.Value); 106 | } 107 | 108 | [Fact(DisplayName = "MongoDbSnapshotOptions should be bindable to IConfiguration")] 109 | public void SnapshotOptionsIConfigurationBindingTest() 110 | { 111 | const string json = @" 112 | { 113 | ""Logging"": { 114 | ""LogLevel"": { 115 | ""Default"": ""Information"", 116 | ""Microsoft.AspNetCore"": ""Warning"" 117 | } 118 | }, 119 | ""Akka"": { 120 | ""SnapshotOptions"": { 121 | ""ConnectionString"": ""mongodb://localhost:27017"", 122 | ""UseWriteTransaction"": ""true"", 123 | ""UseReadTransaction"": ""true"", 124 | ""Identifier"": ""custommongodb"", 125 | ""AutoInitialize"": true, 126 | ""IsDefaultPlugin"": false, 127 | ""Collection"": ""CustomEnventJournalCollection"", 128 | ""LegacySerialization"" : ""true"", 129 | ""CallTimeout"": ""00:10:00"", 130 | ""Serializer"": ""hyperion"", 131 | } 132 | } 133 | }"; 134 | 135 | using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); 136 | var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); 137 | 138 | var options = jsonConfig.GetSection("Akka:SnapshotOptions").Get(); 139 | options.ConnectionString.Should().Be("mongodb://localhost:27017"); 140 | options.UseWriteTransaction.Should().BeTrue(); 141 | options.UseReadTransaction.Should().BeTrue(); 142 | options.Identifier.Should().Be("custommongodb"); 143 | options.AutoInitialize.Should().BeTrue(); 144 | options.IsDefaultPlugin.Should().BeFalse(); 145 | options.Collection.Should().Be("CustomEnventJournalCollection"); 146 | options.LegacySerialization.Should().BeTrue(); 147 | options.CallTimeout.Should().Be(10.Minutes()); 148 | options.Serializer.Should().Be("hyperion"); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/JournalTestActor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Akka.Persistence.MongoDb.Tests 7 | { 8 | public class JournalTestActor : UntypedPersistentActor 9 | { 10 | public static Props Props(string persistenceId) => Actor.Props.Create(() => new JournalTestActor(persistenceId)); 11 | 12 | public sealed class DeleteCommand 13 | { 14 | public DeleteCommand(long toSequenceNr) 15 | { 16 | ToSequenceNr = toSequenceNr; 17 | } 18 | 19 | public long ToSequenceNr { get; } 20 | } 21 | 22 | public JournalTestActor(string persistenceId) 23 | { 24 | PersistenceId = persistenceId; 25 | } 26 | 27 | public override string PersistenceId { get; } 28 | 29 | protected override void OnRecover(object message) 30 | { 31 | } 32 | 33 | protected override void OnCommand(object message) 34 | { 35 | switch (message) { 36 | case DeleteCommand delete: 37 | DeleteMessages(delete.ToSequenceNr); 38 | Sender.Tell($"{delete.ToSequenceNr}-deleted"); 39 | break; 40 | case string cmd: 41 | var sender = Sender; 42 | Persist(cmd, e => sender.Tell($"{e}-done")); 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbAllEventsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2017 Akka.NET Contrib 4 | // 5 | //----------------------------------------------------------------------- 6 | 7 | using Akka.Configuration; 8 | using Akka.Persistence.MongoDb.Query; 9 | using Akka.Persistence.Query; 10 | using Akka.Persistence.TCK.Query; 11 | using Akka.Util.Internal; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | 15 | namespace Akka.Persistence.MongoDb.Tests 16 | { 17 | [Collection("MongoDbSpec")] 18 | public class MongoDbTransactionAllEventsSpec : MongoDbAllEventsSpecBase 19 | { 20 | public MongoDbTransactionAllEventsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 21 | { 22 | } 23 | } 24 | 25 | [Collection("MongoDbSpec")] 26 | public class MongoDbAllEventsSpec : MongoDbAllEventsSpecBase 27 | { 28 | public MongoDbAllEventsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 29 | { 30 | } 31 | } 32 | 33 | public abstract class MongoDbAllEventsSpecBase: AllEventsSpec, IClassFixture 34 | { 35 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 36 | { 37 | // akka.test.single-expect-default = 10s 38 | var specString = $$""" 39 | akka.test.single-expect-default = 10s 40 | akka.persistence { 41 | publish-plugin-commands = on 42 | journal { 43 | plugin = "akka.persistence.journal.mongodb" 44 | mongodb { 45 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 46 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 47 | use-write-transaction = {{(transaction ? "on" : "off")}} 48 | auto-initialize = on 49 | collection = "EventJournal" 50 | } 51 | } 52 | snapshot-store { 53 | plugin = "akka.persistence.snapshot-store.mongodb" 54 | mongodb { 55 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 56 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 57 | use-write-transaction = {{(transaction ? "on" : "off")}} 58 | } 59 | } 60 | query { 61 | mongodb { 62 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 63 | refresh-interval = 1s 64 | } 65 | } 66 | } 67 | """; 68 | 69 | return ConfigurationFactory.ParseString(specString); 70 | } 71 | 72 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 73 | 74 | protected MongoDbAllEventsSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 75 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbAllEventsSpec", output) 76 | { 77 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbCurrentAllEventsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2017 Akka.NET Contrib 4 | // 5 | //----------------------------------------------------------------------- 6 | 7 | using Akka.Configuration; 8 | using Akka.Persistence.MongoDb.Query; 9 | using Akka.Persistence.Query; 10 | using Akka.Persistence.TCK.Query; 11 | using Akka.Util.Internal; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | 15 | namespace Akka.Persistence.MongoDb.Tests 16 | { 17 | [Collection("MongoDbSpec")] 18 | public class MongoDbTransactionCurrentAllEventsSpec : MongoDbCurrentAllEventsSpecBase 19 | { 20 | public MongoDbTransactionCurrentAllEventsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 21 | { 22 | } 23 | } 24 | 25 | [Collection("MongoDbSpec")] 26 | public class MongoDbCurrentAllEventsSpec : MongoDbCurrentAllEventsSpecBase 27 | { 28 | public MongoDbCurrentAllEventsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 29 | { 30 | } 31 | } 32 | 33 | public abstract class MongoDbCurrentAllEventsSpecBase : CurrentAllEventsSpec, IClassFixture 34 | { 35 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 36 | { 37 | // akka.test.single-expect-default = 10s 38 | var specString = $$""" 39 | akka.test.single-expect-default = 10s 40 | akka.persistence { 41 | publish-plugin-commands = on 42 | journal { 43 | plugin = "akka.persistence.journal.mongodb" 44 | mongodb { 45 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 46 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 47 | use-write-transaction = {{(transaction ? "on" : "off")}} 48 | auto-initialize = on 49 | collection = "EventJournal" 50 | } 51 | } 52 | snapshot-store { 53 | plugin = "akka.persistence.snapshot-store.mongodb" 54 | mongodb { 55 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 56 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 57 | use-write-transaction = {{(transaction ? "on" : "off")}} 58 | } 59 | } 60 | query { 61 | mongodb { 62 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 63 | refresh-interval = 1s 64 | } 65 | } 66 | } 67 | """; 68 | 69 | return ConfigurationFactory.ParseString(specString); 70 | } 71 | 72 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 73 | 74 | protected MongoDbCurrentAllEventsSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 75 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbCurrentAllEventsSpec", output) 76 | { 77 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbCurrentEventsByPersistenceIdsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | 16 | namespace Akka.Persistence.MongoDb.Tests 17 | { 18 | [Collection("MongoDbSpec")] 19 | public class MongoDbTransactionCurrentEventsByPersistenceIdsSpec: MongoDbCurrentEventsByPersistenceIdsSpecBase 20 | { 21 | public MongoDbTransactionCurrentEventsByPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 22 | : base(output, databaseFixture, true) 23 | { 24 | } 25 | } 26 | 27 | [Collection("MongoDbSpec")] 28 | public class MongoDbCurrentEventsByPersistenceIdsSpec: MongoDbCurrentEventsByPersistenceIdsSpecBase 29 | { 30 | public MongoDbCurrentEventsByPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 31 | : base(output, databaseFixture, false) 32 | { 33 | } 34 | } 35 | 36 | public abstract class MongoDbCurrentEventsByPersistenceIdsSpecBase : TCK.Query.CurrentEventsByPersistenceIdSpec, IClassFixture 37 | { 38 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 39 | private readonly ITestOutputHelper _output; 40 | 41 | protected MongoDbCurrentEventsByPersistenceIdsSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 42 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbCurrentEventsByPersistenceIdsSpec", output) 43 | { 44 | _output = output; 45 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 46 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 47 | } 48 | 49 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 50 | { 51 | var specString = $$""" 52 | akka.test.single-expect-default = 10s 53 | akka.persistence { 54 | publish-plugin-commands = on 55 | journal { 56 | plugin = "akka.persistence.journal.mongodb" 57 | mongodb { 58 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 59 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 60 | use-write-transaction = {{(transaction ? "on" : "off")}} 61 | auto-initialize = on 62 | collection = "EventJournal" 63 | } 64 | } 65 | snapshot-store { 66 | plugin = "akka.persistence.snapshot-store.mongodb" 67 | mongodb { 68 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 69 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 70 | use-write-transaction = {{(transaction ? "on" : "off")}} 71 | } 72 | } 73 | query { 74 | mongodb { 75 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 76 | refresh-interval = 1s 77 | } 78 | } 79 | } 80 | """; 81 | 82 | return ConfigurationFactory.ParseString(specString); 83 | } 84 | 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbCurrentEventsByTagSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | using System; 16 | using Akka.Actor; 17 | using Akka.Streams.TestKit; 18 | using System.Linq; 19 | using System.Diagnostics; 20 | 21 | namespace Akka.Persistence.MongoDb.Tests 22 | { 23 | [Collection("MongoDbSpec")] 24 | public class MongoDbTransactionCurrentEventsByTagSpec : MongoDbCurrentEventsByTagSpecBase 25 | { 26 | public MongoDbTransactionCurrentEventsByTagSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 27 | { 28 | } 29 | } 30 | 31 | [Collection("MongoDbSpec")] 32 | public class MongoDbCurrentEventsByTagSpec : MongoDbCurrentEventsByTagSpecBase 33 | { 34 | public MongoDbCurrentEventsByTagSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 35 | { 36 | } 37 | } 38 | 39 | public abstract class MongoDbCurrentEventsByTagSpecBase : Akka.Persistence.TCK.Query.CurrentEventsByTagSpec, IClassFixture 40 | { 41 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 42 | private readonly ITestOutputHelper _output; 43 | 44 | protected MongoDbCurrentEventsByTagSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 45 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbCurrentEventsByTagSpec", output) 46 | { 47 | 48 | _output = output; 49 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 50 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 51 | 52 | var x = Sys.ActorOf(TestActor.Props("x")); 53 | x.Tell("warm-up"); 54 | ExpectMsg("warm-up-done", TimeSpan.FromSeconds(10)); 55 | } 56 | 57 | protected override bool SupportsTagsInEventEnvelope => true; 58 | 59 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 60 | { 61 | var specString = $$""" 62 | akka.test.single-expect-default = 3s 63 | akka.persistence { 64 | publish-plugin-commands = on 65 | journal { 66 | plugin = "akka.persistence.journal.mongodb" 67 | mongodb { 68 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 69 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 70 | use-write-transaction = {{(transaction ? "on" : "off")}} 71 | auto-initialize = on 72 | collection = "EventJournal" 73 | event-adapters { 74 | color-tagger = "Akka.Persistence.TCK.Query.ColorFruitTagger, Akka.Persistence.TCK" 75 | } 76 | event-adapter-bindings = { 77 | "System.String" = color-tagger 78 | } 79 | } 80 | } 81 | snapshot-store { 82 | plugin = "akka.persistence.snapshot-store.mongodb" 83 | mongodb { 84 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 85 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 86 | use-write-transaction = {{(transaction ? "on" : "off")}} 87 | } 88 | } 89 | query { 90 | mongodb { 91 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 92 | refresh-interval = 1s 93 | } 94 | } 95 | } 96 | """; 97 | 98 | return ConfigurationFactory.ParseString(specString); 99 | } 100 | 101 | internal class TestActor : UntypedPersistentActor 102 | { 103 | public static Props Props(string persistenceId) => Actor.Props.Create(() => new TestActor(persistenceId)); 104 | 105 | public sealed class DeleteCommand 106 | { 107 | public DeleteCommand(long toSequenceNr) 108 | { 109 | ToSequenceNr = toSequenceNr; 110 | } 111 | 112 | public long ToSequenceNr { get; } 113 | } 114 | 115 | public TestActor(string persistenceId) 116 | { 117 | PersistenceId = persistenceId; 118 | } 119 | 120 | public override string PersistenceId { get; } 121 | 122 | protected override void OnRecover(object message) 123 | { 124 | } 125 | 126 | protected override void OnCommand(object message) 127 | { 128 | switch (message) { 129 | case DeleteCommand delete: 130 | DeleteMessages(delete.ToSequenceNr); 131 | Sender.Tell($"{delete.ToSequenceNr}-deleted"); 132 | break; 133 | case string cmd: 134 | var sender = Sender; 135 | Persist(cmd, e => sender.Tell($"{e}-done")); 136 | break; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbCurrentPersistenceIdsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | using System; 16 | using Akka.Actor; 17 | using Akka.Streams.TestKit; 18 | using System.Linq; 19 | using System.Diagnostics; 20 | 21 | namespace Akka.Persistence.MongoDb.Tests 22 | { 23 | [Collection("MongoDbSpec")] 24 | public class MongoDbTransactionCurrentPersistenceIdsSpec : MongoDbCurrentPersistenceIdsSpecBase 25 | { 26 | public MongoDbTransactionCurrentPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 27 | { 28 | } 29 | } 30 | 31 | [Collection("MongoDbSpec")] 32 | public class MongoDbCurrentPersistenceIdsSpec : MongoDbCurrentPersistenceIdsSpecBase 33 | { 34 | public MongoDbCurrentPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 35 | { 36 | } 37 | } 38 | 39 | public abstract class MongoDbCurrentPersistenceIdsSpecBase : Akka.Persistence.TCK.Query.CurrentPersistenceIdsSpec, IClassFixture 40 | { 41 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 42 | private readonly ITestOutputHelper _output; 43 | 44 | protected MongoDbCurrentPersistenceIdsSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 45 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbCurrentPersistenceIdsSpec", output) 46 | { 47 | _output = output; 48 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 49 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 50 | } 51 | 52 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 53 | { 54 | var specString = $$""" 55 | akka.test.single-expect-default = 3s 56 | akka.persistence { 57 | publish-plugin-commands = on 58 | journal { 59 | plugin = "akka.persistence.journal.mongodb" 60 | mongodb { 61 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 62 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 63 | use-write-transaction = {{(transaction ? "on" : "off")}} 64 | auto-initialize = on 65 | collection = "EventJournal" 66 | } 67 | } 68 | query { 69 | mongodb { 70 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 71 | refresh-interval = 1s 72 | } 73 | } 74 | } 75 | """; 76 | 77 | return ConfigurationFactory.ParseString(specString); 78 | } 79 | 80 | public override void ReadJournal_query_CurrentPersistenceIds_should_not_see_new_events_after_complete() 81 | { 82 | var queries = ReadJournal.AsInstanceOf(); 83 | 84 | Setup("a", 1); 85 | Setup("b", 1); 86 | Setup("c", 1); 87 | 88 | var greenSrc = queries.CurrentPersistenceIds(); 89 | var probe = greenSrc.RunWith(this.SinkProbe(), Materializer); 90 | var firstTwo = probe.Request(2).ExpectNextN(2); 91 | Assert.Empty(firstTwo.Except(new[] { "a", "b", "c" }).ToArray()); 92 | 93 | var last = new[] { "a", "b", "c" }.Except(firstTwo).First(); 94 | Setup("d", 1); 95 | 96 | probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); 97 | probe.Request(5) 98 | .ExpectNext(last) 99 | .ExpectComplete(); 100 | } 101 | 102 | private IActorRef Setup(string persistenceId, int n) 103 | { 104 | var sw = Stopwatch.StartNew(); 105 | var pref = Sys.ActorOf(JournalTestActor.Props(persistenceId)); 106 | for (int i = 1; i <= n; i++) { 107 | pref.Tell($"{persistenceId}-{i}"); 108 | ExpectMsg($"{persistenceId}-{i}-done", TimeSpan.FromSeconds(10), $"{persistenceId}-{i}-done"); 109 | } 110 | _output.WriteLine(sw.ElapsedMilliseconds.ToString()); 111 | return pref; 112 | } 113 | 114 | } 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbEventsByPersistenceIdSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | 16 | namespace Akka.Persistence.MongoDb.Tests 17 | { 18 | [Collection("MongoDbSpec")] 19 | public class MongoDbTransactionEventsByPersistenceIdSpec : MongoDbEventsByPersistenceIdSpecBase 20 | { 21 | public MongoDbTransactionEventsByPersistenceIdSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 22 | { 23 | } 24 | } 25 | 26 | [Collection("MongoDbSpec")] 27 | public class MongoDbEventsByPersistenceIdSpec : MongoDbEventsByPersistenceIdSpecBase 28 | { 29 | public MongoDbEventsByPersistenceIdSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 30 | { 31 | } 32 | } 33 | 34 | public abstract class MongoDbEventsByPersistenceIdSpecBase : Akka.Persistence.TCK.Query.EventsByPersistenceIdSpec, IClassFixture 35 | { 36 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 37 | private readonly ITestOutputHelper _output; 38 | 39 | protected MongoDbEventsByPersistenceIdSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 40 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbEventsByPersistenceIdSpec", output) 41 | { 42 | _output = output; 43 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 44 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 45 | } 46 | 47 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 48 | { 49 | 50 | var specString = $$""" 51 | akka.test.single-expect-default = 3s 52 | akka.persistence { 53 | publish-plugin-commands = on 54 | journal { 55 | plugin = "akka.persistence.journal.mongodb" 56 | mongodb { 57 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 58 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 59 | use-write-transaction = {{(transaction ? "on" : "off")}} 60 | auto-initialize = on 61 | collection = "EventJournal" 62 | } 63 | } 64 | query { 65 | mongodb { 66 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 67 | refresh-interval = 1s 68 | } 69 | } 70 | } 71 | """; 72 | 73 | return ConfigurationFactory.ParseString(specString); 74 | } 75 | 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbEventsByTagSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | using System; 16 | using Akka.Actor; 17 | 18 | namespace Akka.Persistence.MongoDb.Tests 19 | { 20 | [Collection("MongoDbSpec")] 21 | public class MongoDbTransactionEventsByTagSpec : MongoDbEventsByTagSpecBase 22 | { 23 | public MongoDbTransactionEventsByTagSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, true) 24 | { 25 | } 26 | } 27 | 28 | [Collection("MongoDbSpec")] 29 | public class MongoDbEventsByTagSpec : MongoDbEventsByTagSpecBase 30 | { 31 | public MongoDbEventsByTagSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture, false) 32 | { 33 | } 34 | } 35 | 36 | public abstract class MongoDbEventsByTagSpecBase : Akka.Persistence.TCK.Query.EventsByTagSpec, IClassFixture 37 | { 38 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 39 | private readonly ITestOutputHelper _output; 40 | 41 | protected MongoDbEventsByTagSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 42 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbCurrentEventsByTagSpec", output) 43 | { 44 | _output = output; 45 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 46 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 47 | 48 | var x = Sys.ActorOf(TestActor.Props("x")); 49 | x.Tell("warm-up"); 50 | ExpectMsg("warm-up-done", TimeSpan.FromSeconds(10)); 51 | } 52 | 53 | protected override bool SupportsTagsInEventEnvelope => true; 54 | 55 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 56 | { 57 | var specString = $$""" 58 | akka.test.single-expect-default = 10s 59 | akka.persistence { 60 | publish-plugin-commands = on 61 | journal { 62 | plugin = "akka.persistence.journal.mongodb" 63 | mongodb { 64 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 65 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 66 | use-write-transaction = {{(transaction ? "on" : "off")}} 67 | auto-initialize = on 68 | collection = "EventJournal" 69 | event-adapters { 70 | color-tagger = "Akka.Persistence.TCK.Query.ColorFruitTagger, Akka.Persistence.TCK" 71 | } 72 | event-adapter-bindings = { 73 | "System.String" = color-tagger 74 | } 75 | } 76 | } 77 | snapshot-store { 78 | plugin = "akka.persistence.snapshot-store.mongodb" 79 | mongodb { 80 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 81 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 82 | use-write-transaction = {{(transaction ? "on" : "off")}} 83 | } 84 | } 85 | query { 86 | mongodb { 87 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 88 | refresh-interval = 1s 89 | } 90 | } 91 | } 92 | """; 93 | 94 | return ConfigurationFactory.ParseString(specString); 95 | } 96 | 97 | internal class TestActor : UntypedPersistentActor 98 | { 99 | public static Props Props(string persistenceId) => Actor.Props.Create(() => new TestActor(persistenceId)); 100 | 101 | public sealed class DeleteCommand 102 | { 103 | public DeleteCommand(long toSequenceNr) 104 | { 105 | ToSequenceNr = toSequenceNr; 106 | } 107 | 108 | public long ToSequenceNr { get; } 109 | } 110 | 111 | public TestActor(string persistenceId) 112 | { 113 | PersistenceId = persistenceId; 114 | } 115 | 116 | public override string PersistenceId { get; } 117 | 118 | protected override void OnRecover(object message) 119 | { 120 | } 121 | 122 | protected override void OnCommand(object message) 123 | { 124 | switch (message) { 125 | case DeleteCommand delete: 126 | DeleteMessages(delete.ToSequenceNr); 127 | Sender.Tell($"{delete.ToSequenceNr}-deleted"); 128 | break; 129 | case string cmd: 130 | var sender = Sender; 131 | Persist(cmd, e => sender.Tell($"{e}-done")); 132 | break; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbJournalPerfSpec.cs: -------------------------------------------------------------------------------- 1 | #if !CICD 2 | using Akka.Configuration; 3 | using Akka.Persistence.TestKit.Performance; 4 | using Akka.Util.Internal; 5 | using System; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace Akka.Persistence.MongoDb.Tests 10 | { 11 | [Collection("MongoDbSpec")] 12 | public class MongoDbJournalPerfSpec: JournalPerfSpec, IClassFixture 13 | { 14 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 15 | { 16 | // akka.test.single-expect-default = 10s 17 | var specString = @" 18 | akka.persistence { 19 | publish-plugin-commands = on 20 | journal { 21 | plugin = ""akka.persistence.journal.mongodb"" 22 | mongodb { 23 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 24 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 25 | auto-initialize = on 26 | collection = ""EventJournal"" 27 | } 28 | } 29 | }"; 30 | 31 | return ConfigurationFactory.ParseString(specString) 32 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 33 | } 34 | 35 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 36 | 37 | public MongoDbJournalPerfSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement()), "MongoDbJournalPerfSpec", output) 38 | { 39 | EventsCount = 1000; 40 | ExpectDuration = TimeSpan.FromMinutes(10); 41 | MeasurementIterations = 1; 42 | } 43 | } 44 | } 45 | #endif -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbJournalSetupSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Akka.Actor; 6 | using Akka.Actor.Setup; 7 | using Akka.Configuration; 8 | using Akka.Persistence.TCK; 9 | using Akka.Persistence.TCK.Journal; 10 | using Akka.Persistence.TCK.Serialization; 11 | using Akka.TestKit; 12 | using MongoDB.Driver; 13 | using MongoDB.Driver.Linq; 14 | using Xunit; 15 | using Xunit.Abstractions; 16 | 17 | namespace Akka.Persistence.MongoDb.Tests 18 | { 19 | [Collection("MongoDbSpec")] 20 | public class MongoDbJournalSetupSpec : JournalSpec, IClassFixture 21 | { 22 | // TEST: MongoDb journal plugin set using Setup should behave exactly like when it is 23 | // set up using connection string. 24 | public MongoDbJournalSetupSpec( 25 | DatabaseFixture databaseFixture, 26 | ITestOutputHelper output) 27 | : base(CreateBootstrapSetup(databaseFixture), nameof(MongoDbSnapshotStoreSetupSpec), output) 28 | { 29 | Initialize(); 30 | } 31 | 32 | private static ActorSystemSetup CreateBootstrapSetup(DatabaseFixture fixture) 33 | { 34 | var connectionString = new MongoUrl(fixture.ConnectionString); 35 | var clientSettings = MongoClientSettings.FromUrl(connectionString); 36 | var client = new MongoClient(clientSettings); 37 | var databaseName = connectionString.DatabaseName; 38 | var settings = client.Settings; 39 | 40 | return BootstrapSetup.Create() 41 | .WithConfig(CreateSpecConfig()) 42 | .And(new MongoDbPersistenceSetup(null, null, databaseName, settings)); 43 | } 44 | 45 | private static Config CreateSpecConfig() 46 | { 47 | var specString = @" 48 | akka.test.single-expect-default = 3s 49 | akka.persistence { 50 | publish-plugin-commands = on 51 | journal { 52 | plugin = ""akka.persistence.journal.mongodb"" 53 | mongodb { 54 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 55 | auto-initialize = on 56 | collection = ""EventJournal"" 57 | } 58 | } 59 | }"; 60 | 61 | return ConfigurationFactory.ParseString(specString) 62 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbJournalSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | 12 | namespace Akka.Persistence.MongoDb.Tests 13 | { 14 | [Collection("MongoDbSpec")] 15 | public class MongoDbTransactionJournalSpec : MongoDbJournalSpecBase 16 | { 17 | public MongoDbTransactionJournalSpec(DatabaseFixture databaseFixture) : base(databaseFixture, true) 18 | { 19 | } 20 | } 21 | 22 | [Collection("MongoDbSpec")] 23 | public class MongoDbJournalSpec : MongoDbJournalSpecBase 24 | { 25 | public MongoDbJournalSpec(DatabaseFixture databaseFixture) : base(databaseFixture, false) 26 | { 27 | } 28 | } 29 | 30 | public abstract class MongoDbJournalSpecBase : JournalSpec, IClassFixture 31 | { 32 | protected override bool SupportsRejectingNonSerializableObjects { get; } = false; 33 | 34 | protected MongoDbJournalSpecBase(DatabaseFixture databaseFixture, bool transaction) 35 | : base(CreateSpecConfig(databaseFixture, transaction), "MongoDbJournalSpec") 36 | { 37 | Initialize(); 38 | } 39 | 40 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, bool transaction) 41 | { 42 | var specString = $$""" 43 | akka.test.single-expect-default = 3s 44 | akka.persistence { 45 | publish-plugin-commands = on 46 | journal { 47 | plugin = "akka.persistence.journal.mongodb" 48 | mongodb { 49 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 50 | connection-string = "{{databaseFixture.ConnectionString}}" 51 | use-write-transaction = {{(transaction ? "on" : "off")}} 52 | auto-initialize = on 53 | collection = "EventJournal" 54 | } 55 | } 56 | } 57 | """; 58 | 59 | return ConfigurationFactory.ParseString(specString) 60 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbLegacySerializationJournalSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2019 Lightbend Inc. 4 | // Copyright (C) 2013-2019 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Configuration; 9 | using Akka.Persistence.TCK.Journal; 10 | using Xunit; 11 | 12 | namespace Akka.Persistence.MongoDb.Tests 13 | { 14 | [Collection("MongoDbSpec")] 15 | public class MongoDbLegacySerializationJournalSpec : JournalSpec, IClassFixture 16 | { 17 | protected override bool SupportsRejectingNonSerializableObjects { get; } = false; 18 | 19 | protected override bool SupportsSerialization => false; 20 | 21 | public MongoDbLegacySerializationJournalSpec(DatabaseFixture databaseFixture) : base(CreateSpecConfig(databaseFixture), "MongoDbJournalSpec") 22 | { 23 | Initialize(); 24 | } 25 | 26 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 27 | { 28 | var specString = @" 29 | akka.test.single-expect-default = 3s 30 | akka.persistence { 31 | publish-plugin-commands = on 32 | journal { 33 | plugin = ""akka.persistence.journal.mongodb"" 34 | mongodb { 35 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 36 | connection-string = """ + databaseFixture.ConnectionString + @""" 37 | auto-initialize = on 38 | collection = ""EventJournal"" 39 | legacy-serialization = on 40 | } 41 | } 42 | }"; 43 | 44 | return ConfigurationFactory.ParseString(specString) 45 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbLegacySerializationSnapshotStoreSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Configuration; 9 | using Akka.Persistence.TCK.Snapshot; 10 | using Xunit; 11 | 12 | namespace Akka.Persistence.MongoDb.Tests 13 | { 14 | [Collection("MongoDbSpec")] 15 | public class MongoDbLegacySerializationSnapshotStoreSpec : SnapshotStoreSpec, IClassFixture 16 | { 17 | protected override bool SupportsSerialization => false; 18 | 19 | public MongoDbLegacySerializationSnapshotStoreSpec(DatabaseFixture databaseFixture) : base(CreateSpecConfig(databaseFixture), "MongoDbSnapshotStoreSpec") 20 | { 21 | Initialize(); 22 | } 23 | 24 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 25 | { 26 | var specString = @" 27 | akka.test.single-expect-default = 3s 28 | akka.persistence { 29 | publish-plugin-commands = on 30 | snapshot-store { 31 | plugin = ""akka.persistence.snapshot-store.mongodb"" 32 | mongodb { 33 | class = ""Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb"" 34 | connection-string = """ + databaseFixture.ConnectionString + @""" 35 | auto-initialize = on 36 | collection = ""SnapshotStore"" 37 | legacy-serialization = on 38 | } 39 | } 40 | }"; 41 | 42 | return ConfigurationFactory.ParseString(specString) 43 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbPersistenceIdsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Persistence.TCK.Journal; 9 | using Xunit; 10 | using Akka.Configuration; 11 | using Akka.Persistence.MongoDb.Query; 12 | using Akka.Persistence.Query; 13 | using Xunit.Abstractions; 14 | using Akka.Util.Internal; 15 | using System; 16 | using Akka.Actor; 17 | using Akka.Streams.TestKit; 18 | using System.Linq; 19 | using System.Diagnostics; 20 | using System.Threading.Tasks; 21 | using System.Reflection; 22 | using Reactive.Streams; 23 | using MongoDB.Driver.Core.Misc; 24 | 25 | namespace Akka.Persistence.MongoDb.Tests 26 | { 27 | [Collection("MongoDbSpec")] 28 | public class MongoDbTransactionPersistenceIdsSpec : MongoDbPersistenceIdsSpecBase 29 | { 30 | public MongoDbTransactionPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 31 | : base(output, databaseFixture, true) 32 | { 33 | } 34 | } 35 | 36 | [Collection("MongoDbSpec")] 37 | public class MongoDbPersistenceIdsSpec : MongoDbPersistenceIdsSpecBase 38 | { 39 | public MongoDbPersistenceIdsSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 40 | : base(output, databaseFixture, false) 41 | { 42 | } 43 | } 44 | 45 | public abstract class MongoDbPersistenceIdsSpecBase : Akka.Persistence.TCK.Query.PersistenceIdsSpec, IClassFixture 46 | { 47 | private static readonly AtomicCounter Counter = new AtomicCounter(0); 48 | 49 | private readonly ITestOutputHelper _output; 50 | 51 | protected MongoDbPersistenceIdsSpecBase(ITestOutputHelper output, DatabaseFixture databaseFixture, bool transaction) 52 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement(), transaction), "MongoDbPersistenceIdsSpec", output) 53 | { 54 | _output = output; 55 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 56 | ReadJournal = Sys.ReadJournalFor(MongoDbReadJournal.Identifier); 57 | } 58 | 59 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id, bool transaction) 60 | { 61 | var specString = $$""" 62 | akka.test.single-expect-default = 3s 63 | akka.persistence { 64 | publish-plugin-commands = on 65 | journal { 66 | plugin = "akka.persistence.journal.mongodb" 67 | mongodb { 68 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 69 | connection-string = "{{databaseFixture.MongoDbConnectionString(id)}}" 70 | use-write-transaction = {{(transaction ? "on" : "off")}} 71 | auto-initialize = on 72 | collection = "EventJournal" 73 | } 74 | } 75 | query { 76 | mongodb { 77 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 78 | refresh-interval = 1s 79 | } 80 | } 81 | } 82 | """; 83 | 84 | return ConfigurationFactory.ParseString(specString); 85 | } 86 | 87 | [Fact] 88 | public void ReadJournal_ConcurrentMessaging_should_work() 89 | { 90 | Enumerable.Range(1, 100).AsParallel().ForEach(_ => { 91 | Setup(Guid.NewGuid().ToString(), 1); 92 | Setup(Guid.NewGuid().ToString(), 1); 93 | }); 94 | } 95 | 96 | private IActorRef Setup(string persistenceId, int n) 97 | { 98 | var sw = Stopwatch.StartNew(); 99 | var pref = Sys.ActorOf(JournalTestActor.Props(persistenceId)); 100 | for (int i = 1; i <= n; i++) { 101 | pref.Tell($"{persistenceId}-{i}"); 102 | ExpectMsg($"{persistenceId}-{i}-done", TimeSpan.FromSeconds(10), $"{persistenceId}-{i}-done"); 103 | } 104 | _output.WriteLine(sw.ElapsedMilliseconds.ToString()); 105 | return pref; 106 | } 107 | 108 | public override void ReadJournal_AllPersistenceIds_should_find_new_events_after_demand_request() 109 | { 110 | var queries = ReadJournal.AsInstanceOf(); 111 | 112 | Setup("h", 1); 113 | Setup("i", 1); 114 | 115 | var source = queries.PersistenceIds(); 116 | var probe = source.RunWith(this.SinkProbe(), Materializer); 117 | 118 | probe.Within(TimeSpan.FromSeconds(10), () => 119 | { 120 | probe.Request(1).ExpectNext(); 121 | return probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); 122 | }); 123 | 124 | Setup("j", 1); 125 | probe.Within(TimeSpan.FromSeconds(10), () => 126 | { 127 | probe.Request(5).ExpectNext(); 128 | return probe.ExpectNext(); 129 | }); 130 | } 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbSettingsSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using FluentAssertions; 9 | using FluentAssertions.Extensions; 10 | using Xunit; 11 | 12 | namespace Akka.Persistence.MongoDb.Tests 13 | { 14 | [Collection("MongoDbSpec")] 15 | public class MongoDbSettingsSpec : Akka.TestKit.Xunit2.TestKit 16 | { 17 | [Fact] 18 | public void Mongo_JournalSettings_must_have_default_values() 19 | { 20 | var mongoPersistence = MongoDbPersistence.Get(Sys); 21 | 22 | mongoPersistence.JournalSettings.ConnectionString.Should().Be(string.Empty); 23 | mongoPersistence.JournalSettings.AutoInitialize.Should().BeTrue(); 24 | mongoPersistence.JournalSettings.Collection.Should().Be("EventJournal"); 25 | mongoPersistence.JournalSettings.MetadataCollection.Should().Be("Metadata"); 26 | mongoPersistence.JournalSettings.LegacySerialization.Should().BeFalse(); 27 | mongoPersistence.JournalSettings.CallTimeout.Should().Be(10.Seconds()); 28 | } 29 | 30 | [Fact] 31 | public void Mongo_SnapshotStoreSettingsSettings_must_have_default_values() 32 | { 33 | var mongoPersistence = MongoDbPersistence.Get(Sys); 34 | 35 | mongoPersistence.SnapshotStoreSettings.ConnectionString.Should().Be(string.Empty); 36 | mongoPersistence.SnapshotStoreSettings.AutoInitialize.Should().BeTrue(); 37 | mongoPersistence.SnapshotStoreSettings.Collection.Should().Be("SnapshotStore"); 38 | mongoPersistence.SnapshotStoreSettings.LegacySerialization.Should().BeFalse(); 39 | mongoPersistence.SnapshotStoreSettings.CallTimeout.Should().Be(10.Seconds()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbSnapshotStoreSaveSnapshotSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | using Akka.Persistence.TCK.Snapshot; 3 | using Xunit; 4 | 5 | namespace Akka.Persistence.MongoDb.Tests; 6 | 7 | [Collection("MongoDbSpec")] 8 | public class MongoDbSnapshotStoreSaveSnapshotSpec: SnapshotStoreSaveSnapshotSpec, IClassFixture 9 | { 10 | public MongoDbSnapshotStoreSaveSnapshotSpec(DatabaseFixture databaseFixture) 11 | : base(CreateSpecConfig(databaseFixture), "MongoDbSnapshotStoreSpec") 12 | { 13 | } 14 | 15 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture) 16 | { 17 | var specString = $$""" 18 | akka.test.single-expect-default = 3s 19 | akka.persistence { 20 | publish-plugin-commands = on 21 | snapshot-store { 22 | plugin = "akka.persistence.snapshot-store.mongodb" 23 | mongodb { 24 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 25 | connection-string = "{{databaseFixture.ConnectionString}}" 26 | use-write-transaction = on 27 | auto-initialize = on 28 | collection = "SnapshotStore" 29 | } 30 | } 31 | } 32 | """; 33 | 34 | return ConfigurationFactory.ParseString(specString); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbSnapshotStoreSetupSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Actor.Setup; 3 | using Akka.Configuration; 4 | using Akka.Persistence.TCK.Snapshot; 5 | using MongoDB.Driver; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace Akka.Persistence.MongoDb.Tests 10 | { 11 | [Collection("MongoDbSpec")] 12 | public class MongoDbSnapshotStoreSetupSpec : SnapshotStoreSpec, IClassFixture 13 | { 14 | // TEST: MongoDb snapshot plugin set using Setup should behave exactly like when it is 15 | // set up using connection string. 16 | public MongoDbSnapshotStoreSetupSpec( 17 | DatabaseFixture databaseFixture, 18 | ITestOutputHelper output) 19 | : base(CreateBootstrapSetup(databaseFixture), nameof(MongoDbSnapshotStoreSetupSpec), output) 20 | { 21 | Initialize(); 22 | } 23 | 24 | private static ActorSystemSetup CreateBootstrapSetup(DatabaseFixture fixture) 25 | { 26 | var connectionString = new MongoUrl(fixture.ConnectionString); 27 | var clientSettings = MongoClientSettings.FromUrl(connectionString); 28 | var client = new MongoClient(clientSettings); 29 | var databaseName = connectionString.DatabaseName; 30 | var settings = client.Settings; 31 | 32 | return BootstrapSetup.Create() 33 | .WithConfig(CreateSpecConfig()) 34 | .And(new MongoDbPersistenceSetup(databaseName, settings, null, null)); 35 | } 36 | 37 | private static Config CreateSpecConfig() 38 | { 39 | var specString = @" 40 | akka.test.single-expect-default = 3s 41 | akka.persistence { 42 | publish-plugin-commands = on 43 | snapshot-store { 44 | plugin = ""akka.persistence.snapshot-store.mongodb"" 45 | mongodb { 46 | class = ""Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb"" 47 | auto-initialize = on 48 | collection = ""SnapshotStore"" 49 | } 50 | } 51 | }"; 52 | 53 | return ConfigurationFactory.ParseString(specString); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/MongoDbSnapshotStoreSpec.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Xunit; 9 | using Akka.Persistence.TCK.Snapshot; 10 | using Akka.Configuration; 11 | 12 | namespace Akka.Persistence.MongoDb.Tests 13 | { 14 | [Collection("MongoDbSpec")] 15 | public class MongoDbTransactionSnapshotStoreSpec : MongoDbSnapshotStoreSpecBase 16 | { 17 | public MongoDbTransactionSnapshotStoreSpec(DatabaseFixture databaseFixture) : base(databaseFixture, true) 18 | { 19 | } 20 | } 21 | 22 | [Collection("MongoDbSpec")] 23 | public class MongoDbSnapshotStoreSpec : MongoDbSnapshotStoreSpecBase 24 | { 25 | public MongoDbSnapshotStoreSpec(DatabaseFixture databaseFixture) : base(databaseFixture, false) 26 | { 27 | } 28 | } 29 | 30 | public abstract class MongoDbSnapshotStoreSpecBase : SnapshotStoreSpec, IClassFixture 31 | { 32 | protected MongoDbSnapshotStoreSpecBase(DatabaseFixture databaseFixture, bool transaction) 33 | : base(CreateSpecConfig(databaseFixture, transaction), "MongoDbSnapshotStoreSpec") 34 | { 35 | Initialize(); 36 | } 37 | 38 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, bool transaction) 39 | { 40 | var specString = $$""" 41 | akka.test.single-expect-default = 3s 42 | akka.persistence { 43 | publish-plugin-commands = on 44 | snapshot-store { 45 | plugin = "akka.persistence.snapshot-store.mongodb" 46 | mongodb { 47 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 48 | connection-string = "{{databaseFixture.ConnectionString}}" 49 | use-write-transaction = {{(transaction ? "on" : "off")}} 50 | auto-initialize = on 51 | collection = "SnapshotStore" 52 | } 53 | } 54 | } 55 | """; 56 | 57 | return ConfigurationFactory.ParseString(specString); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Serialization/MongoDbJournalSerializationSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Akka.Configuration; 3 | using Akka.Persistence.TCK.Serialization; 4 | using Akka.Util.Internal; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace Akka.Persistence.MongoDb.Tests.Serialization 9 | { 10 | [Collection("MongoDbSpec")] 11 | public class MongoDbJournalSerializationSpec : JournalSerializationSpec, IClassFixture 12 | { 13 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 14 | 15 | private readonly ITestOutputHelper _output; 16 | 17 | public MongoDbJournalSerializationSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 18 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement()), nameof(MongoDbJournalSerializationSpec), output) 19 | { 20 | _output = output; 21 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 22 | } 23 | 24 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 25 | { 26 | var specString = @" 27 | akka.test.single-expect-default = 3s 28 | akka.persistence { 29 | publish-plugin-commands = on 30 | journal { 31 | plugin = ""akka.persistence.journal.mongodb"" 32 | mongodb { 33 | class = ""Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb"" 34 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 35 | auto-initialize = on 36 | collection = ""EventJournal"" 37 | } 38 | } 39 | }"; 40 | 41 | return ConfigurationFactory.ParseString(specString) 42 | .WithFallback(MongoDbPersistence.DefaultConfiguration()); 43 | } 44 | 45 | [Fact(Skip = "Waiting on better error messages")] 46 | public override void Journal_should_serialize_Persistent_with_EventAdapter_manifest() 47 | { 48 | base.Journal_should_serialize_Persistent_with_EventAdapter_manifest(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb.Tests/Serialization/MongoDbSnapshotStoreSerializationSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | using Akka.Persistence.TCK.Serialization; 3 | using Akka.Util.Internal; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace Akka.Persistence.MongoDb.Tests.Serialization 8 | { 9 | [Collection("MongoDbSpec")] 10 | public class MongoDbSnapshotStoreSerializationSpec : SnapshotStoreSerializationSpec, IClassFixture 11 | { 12 | public static readonly AtomicCounter Counter = new AtomicCounter(0); 13 | 14 | private readonly ITestOutputHelper _output; 15 | 16 | public MongoDbSnapshotStoreSerializationSpec(ITestOutputHelper output, DatabaseFixture databaseFixture) 17 | : base(CreateSpecConfig(databaseFixture, Counter.GetAndIncrement()), nameof(MongoDbSnapshotStoreSerializationSpec), output) 18 | { 19 | _output = output; 20 | output.WriteLine(databaseFixture.MongoDbConnectionString(Counter.Current)); 21 | } 22 | 23 | private static Config CreateSpecConfig(DatabaseFixture databaseFixture, int id) 24 | { 25 | var specString = @" 26 | akka.test.single-expect-default = 3s 27 | akka.persistence { 28 | publish-plugin-commands = on 29 | snapshot-store { 30 | plugin = ""akka.persistence.snapshot-store.mongodb"" 31 | mongodb { 32 | class = ""Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb"" 33 | connection-string = """ + databaseFixture.MongoDbConnectionString(id) + @""" 34 | auto-initialize = on 35 | collection = ""SnapshotStore"" 36 | } 37 | } 38 | }"; 39 | 40 | return ConfigurationFactory.ParseString(specString); 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Akka.Persistence.MongoDb.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(NetStandardLibVersion);$(NetFrameworkLibVersion) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/FullTypeNameDiscriminatorConvention.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | using System; 8 | using System.Reflection; 9 | using MongoDB.Bson; 10 | using MongoDB.Bson.Serialization.Conventions; 11 | 12 | namespace Akka.Persistence.MongoDb 13 | { 14 | /// 15 | /// Our discriminator is the type's full name with the assembly name. 16 | /// Additional assembly information is excluded for forward compatibility. 17 | /// 18 | class FullTypeNameDiscriminatorConvention : StandardDiscriminatorConvention 19 | { 20 | public static readonly FullTypeNameDiscriminatorConvention Instance = new FullTypeNameDiscriminatorConvention("_t"); 21 | 22 | public FullTypeNameDiscriminatorConvention(string element) : base(element) { } 23 | 24 | /// 25 | /// Our discriminator is the full type name with the assembly name. 26 | /// Additional assembly information is excluded for forward compatibility. 27 | /// 28 | /// 29 | /// 30 | /// full type name with the simple assembly name 31 | public override BsonValue GetDiscriminator(Type nominalType, Type actualType) 32 | { 33 | var assemblyName = actualType.GetTypeInfo().Assembly.FullName.Split(',')[0]; 34 | return $"{actualType.FullName}, {assemblyName}"; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/FullTypeNameObjectSerializer.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using System.Reflection; 10 | using MongoDB.Bson; 11 | using MongoDB.Bson.Serialization; 12 | using MongoDB.Bson.Serialization.Conventions; 13 | using MongoDB.Bson.Serialization.Serializers; 14 | 15 | namespace Akka.Persistence.MongoDb 16 | { 17 | /// 18 | /// Represents a serializer for objects. 19 | /// 20 | internal class FullTypeNameObjectSerializer : ClassSerializerBase, IHasDiscriminatorConvention 21 | { 22 | private readonly ObjectSerializer _serializer; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public FullTypeNameObjectSerializer() 28 | { 29 | _serializer = new ObjectSerializer(DiscriminatorConvention, ObjectSerializer.AllAllowedTypes); 30 | } 31 | 32 | public IDiscriminatorConvention DiscriminatorConvention => FullTypeNameDiscriminatorConvention.Instance; 33 | 34 | /// 35 | /// Deserializes a value. 36 | /// 37 | public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) 38 | { 39 | var bsonReader = context.Reader; 40 | 41 | if (BsonType.Document == bsonReader.GetCurrentBsonType()) 42 | { 43 | RegisterNewTypesToDiscriminator(DiscriminatorConvention.GetActualType(bsonReader, typeof(object))); 44 | } 45 | 46 | return _serializer.Deserialize(context, args); 47 | } 48 | 49 | /// 50 | /// Serializes a value. 51 | /// 52 | public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) 53 | { 54 | if (value != null) 55 | { 56 | // auto-register new types with MongoDB on serialization, using their full assembly name 57 | RegisterNewTypesToDiscriminator(value.GetType()); 58 | } 59 | 60 | _serializer.Serialize(context, args, value); 61 | } 62 | 63 | /// 64 | /// If the type is not registered, attach it to our discriminator 65 | /// 66 | /// the type to examine 67 | private void RegisterNewTypesToDiscriminator(Type actualType) 68 | { 69 | // we've detected a new concrete type that isn't registered in MongoDB's serializer 70 | if (actualType != typeof(object) && !actualType.GetTypeInfo().IsInterface && !BsonSerializer.IsTypeDiscriminated(actualType)) 71 | { 72 | try 73 | { 74 | BsonSerializer.RegisterDiscriminatorConvention(actualType, DiscriminatorConvention); 75 | BsonSerializer.RegisterDiscriminator(actualType, DiscriminatorConvention.GetDiscriminator(typeof(object), actualType)); 76 | } 77 | catch (BsonSerializationException) 78 | { 79 | // the MongoDB driver library has no nice mechanism for checking if a discriminator convention is registerd. 80 | // The "Lookup" logic tends to define a default if it doesn't exist. 81 | // So we're forced to eat the "duplicate registration" exception. 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Journal/JournalEntry.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using MongoDB.Bson; 9 | using MongoDB.Bson.Serialization.Attributes; 10 | using System.Collections.Generic; 11 | 12 | namespace Akka.Persistence.MongoDb.Journal 13 | { 14 | /// 15 | /// Class used for storing intermediate result of the 16 | /// as BsonDocument into the MongoDB-Collection 17 | /// 18 | public class JournalEntry 19 | { 20 | [BsonId] 21 | public string Id { get; set; } 22 | 23 | [BsonElement("PersistenceId")] 24 | public string PersistenceId { get; set; } 25 | 26 | [BsonElement("SequenceNr")] 27 | public long SequenceNr { get; set; } 28 | 29 | [BsonElement("IsDeleted")] 30 | public bool IsDeleted { get; set; } 31 | 32 | [BsonElement("Payload")] 33 | public object Payload { get; set; } 34 | 35 | [BsonElement("Manifest")] 36 | public string Manifest { get; set; } 37 | 38 | [BsonElement("Ordering")] 39 | public BsonTimestamp Ordering { get; set; } 40 | 41 | [BsonElement("Tags")] 42 | public ICollection Tags { get; set; } = new HashSet(); 43 | 44 | [BsonElement("SerializerId")] 45 | public int? SerializerId { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Journal/MetadataEntry.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using MongoDB.Bson.Serialization.Attributes; 9 | 10 | namespace Akka.Persistence.MongoDb.Journal 11 | { 12 | public class MetadataEntry 13 | { 14 | [BsonId] 15 | public string Id { get; set; } 16 | 17 | [BsonElement("PersistenceId")] 18 | public string PersistenceId { get; set; } 19 | 20 | [BsonElement("SequenceNr")] 21 | public long SequenceNr { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/MongoDbPersistence.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using Akka.Actor; 10 | using Akka.Configuration; 11 | using MongoDB.Bson.Serialization; 12 | 13 | namespace Akka.Persistence.MongoDb 14 | { 15 | /// 16 | /// An actor system extension initializing support for MongoDb persistence layer. 17 | /// 18 | public class MongoDbPersistence : IExtension 19 | { 20 | static MongoDbPersistence() 21 | { 22 | // Some MongoDB things are statically configured. 23 | 24 | // Register our own serializer for objects that uses the type's FullName + Assembly for the discriminator 25 | BsonSerializer.RegisterSerializer(typeof(object), new FullTypeNameObjectSerializer()); 26 | } 27 | /// 28 | /// Returns a default configuration for akka persistence MongoDb journal and snapshot store. 29 | /// 30 | /// 31 | public static Config DefaultConfiguration() 32 | { 33 | return ConfigurationFactory.FromResource("Akka.Persistence.MongoDb.reference.conf"); 34 | } 35 | 36 | public static MongoDbPersistence Get(ActorSystem system) 37 | { 38 | return system.WithExtension(); 39 | } 40 | 41 | /// 42 | /// The settings for the MongoDb journal. 43 | /// 44 | public MongoDbJournalSettings JournalSettings { get; } 45 | 46 | /// 47 | /// The settings for the MongoDb snapshot store. 48 | /// 49 | public MongoDbSnapshotSettings SnapshotStoreSettings { get; } 50 | 51 | public MongoDbPersistence(ExtendedActorSystem system) 52 | { 53 | if (system == null) 54 | throw new ArgumentNullException(nameof(system)); 55 | 56 | // Initialize fallback configuration defaults 57 | system.Settings.InjectTopLevelFallback(DefaultConfiguration()); 58 | 59 | // Read config 60 | var journalConfig = system.Settings.Config.GetConfig("akka.persistence.journal.mongodb"); 61 | JournalSettings = new MongoDbJournalSettings(journalConfig); 62 | 63 | var snapshotConfig = system.Settings.Config.GetConfig("akka.persistence.snapshot-store.mongodb"); 64 | SnapshotStoreSettings = new MongoDbSnapshotSettings(snapshotConfig); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/MongoDbPersistenceProvider.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Actor; 9 | 10 | namespace Akka.Persistence.MongoDb 11 | { 12 | /// 13 | /// Extension Id provider for the MongoDb Persistence extension. 14 | /// 15 | public class MongoDbPersistenceProvider : ExtensionIdProvider 16 | { 17 | /// 18 | /// Creates an actor system extension for akka persistence MongoDb support. 19 | /// 20 | /// 21 | /// 22 | public override MongoDbPersistence CreateExtension(ExtendedActorSystem system) 23 | { 24 | return new MongoDbPersistence(system); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/MongoDbPersistenceSetup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Akka.Actor.Setup; 5 | using Akka.Persistence.MongoDb.Journal; 6 | using Akka.Persistence.MongoDb.Snapshot; 7 | using MongoDB.Driver; 8 | 9 | namespace Akka.Persistence.MongoDb 10 | { 11 | /// 12 | /// A setup class to allow for programmatic configuration of the and 13 | /// 14 | public class MongoDbPersistenceSetup : Setup 15 | { 16 | /// 17 | /// Constructor to instantiate a new instance of 18 | /// 19 | /// 20 | /// The database name to be used by when it connects the server 21 | /// 22 | /// 23 | /// The to be used by when it connects the server. 24 | /// 25 | /// 26 | /// The database name to be used by when it connects the server 27 | /// 28 | /// 29 | /// The to be used by when it connects the server 30 | /// 31 | public MongoDbPersistenceSetup( 32 | string snapshotDatabaseName, 33 | MongoClientSettings snapshotConnectionSettings, 34 | string journalDatabaseName, 35 | MongoClientSettings journalConnectionSettings) 36 | { 37 | SnapshotConnectionSettings = snapshotConnectionSettings; 38 | JournalConnectionSettings = journalConnectionSettings; 39 | SnapshotDatabaseName = snapshotDatabaseName; 40 | JournalDatabaseName = journalDatabaseName; 41 | } 42 | 43 | /// 44 | /// The database name to be used by when it connects the server 45 | /// 46 | public string SnapshotDatabaseName { get; } 47 | /// 48 | /// The to be used by when it connects the server. 49 | /// Setting this property will override 'akka.persistence.snapshot-store.mongodb.connection-string' in the HOCON configuration. 50 | /// Set this to null if you're not overriding this connection string. 51 | /// 52 | public MongoClientSettings SnapshotConnectionSettings { get; } 53 | /// 54 | /// The database name to be used by when it connects the server 55 | /// 56 | public string JournalDatabaseName { get; } 57 | /// 58 | /// The to be used by when it connects the server 59 | /// Setting this property will override 'akka.persistence.journal.mongodb.connection-string' in the HOCON configuration. 60 | /// Set this to null if you're not overriding this connection string. 61 | /// 62 | public MongoClientSettings JournalConnectionSettings { get; } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/MongoDbSettings.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using Akka.Configuration; 10 | 11 | namespace Akka.Persistence.MongoDb 12 | { 13 | /// 14 | /// Settings for the MongoDB persistence implementation, parsed from HOCON configuration. 15 | /// 16 | public abstract class MongoDbSettings 17 | { 18 | /// 19 | /// Connection string used to access the MongoDb, also specifies the database. 20 | /// 21 | public string ConnectionString { get; } 22 | 23 | /// 24 | /// Flag determining in case of event journal or metadata table missing, they should be automatically initialized. 25 | /// 26 | public bool AutoInitialize { get; } 27 | 28 | /// 29 | /// Name of the collection for the event journal or snapshots 30 | /// 31 | public string Collection { get; } 32 | 33 | /// 34 | /// Transaction 35 | /// 36 | public bool Transaction { get; } 37 | 38 | /// 39 | /// Transaction 40 | /// 41 | public bool ReadTransaction { get; } 42 | 43 | /// 44 | /// When true, enables BSON serialization (which breaks features like Akka.Cluster.Sharding, AtLeastOnceDelivery, and so on.) 45 | /// 46 | public bool LegacySerialization { get; } 47 | 48 | /// 49 | /// Timeout for individual database operations. 50 | /// 51 | /// 52 | /// Defaults to 10s. 53 | /// 54 | public TimeSpan CallTimeout { get; } 55 | 56 | protected MongoDbSettings(Config config) 57 | { 58 | ConnectionString = config.GetString("connection-string"); 59 | Transaction = config.GetBoolean("use-write-transaction"); 60 | ReadTransaction = config.GetBoolean("use-read-transaction"); 61 | Collection = config.GetString("collection"); 62 | AutoInitialize = config.GetBoolean("auto-initialize"); 63 | LegacySerialization = config.GetBoolean("legacy-serialization"); 64 | CallTimeout = config.GetTimeSpan("call-timeout", TimeSpan.FromSeconds(10)); 65 | } 66 | } 67 | 68 | /// 69 | /// Settings for the MongoDB journal implementation, parsed from HOCON configuration. 70 | /// 71 | public class MongoDbJournalSettings : MongoDbSettings 72 | { 73 | public const string JournalConfigPath = "akka.persistence.journal.mongodb"; 74 | 75 | public string MetadataCollection { get; } 76 | 77 | public int? ReadBatchSize { get; } 78 | 79 | public MongoDbJournalSettings(Config config) : base(config) 80 | { 81 | if (config == null) 82 | throw new ArgumentNullException(nameof(config), 83 | "MongoDb journal settings cannot be initialized, because required HOCON section couldn't been found"); 84 | 85 | MetadataCollection = config.GetString("metadata-collection"); 86 | var readBatchSize = config.GetString("read-batch-size")?.ToLowerInvariant(); 87 | if(!string.IsNullOrWhiteSpace(readBatchSize) && readBatchSize != "off" && readBatchSize != "false") 88 | ReadBatchSize = int.Parse(readBatchSize); 89 | } 90 | } 91 | 92 | /// 93 | /// Settings for the MongoDB snapshot implementation, parsed from HOCON configuration. 94 | /// 95 | public class MongoDbSnapshotSettings : MongoDbSettings 96 | { 97 | public const string SnapshotStoreConfigPath = "akka.persistence.snapshot-store.mongodb"; 98 | 99 | public MongoDbSnapshotSettings(Config config) : base(config) 100 | { 101 | if (config == null) 102 | throw new ArgumentNullException(nameof(config), 103 | "MongoDb snapshot settings cannot be initialized, because required HOCON section couldn't been found"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/AllEventsPublisher.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2017 Akka.NET Contrib 4 | // 5 | //----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using Akka.Event; 9 | using Akka.Persistence.Query; 10 | using Akka.Streams.Actors; 11 | using System; 12 | 13 | namespace Akka.Persistence.MongoDb.Query 14 | { 15 | internal static class AllEventsPublisher 16 | { 17 | [Serializable] 18 | public sealed class Continue 19 | { 20 | public static readonly Continue Instance = new Continue(); 21 | 22 | private Continue() { } 23 | } 24 | 25 | public static Props Props(long fromOffset, TimeSpan? refreshInterval, int maxBufferSize, string writeJournalPluginId) 26 | { 27 | return refreshInterval.HasValue ? 28 | Actor.Props.Create(() => new LiveAllEventsPublisher(fromOffset, refreshInterval.Value, maxBufferSize, writeJournalPluginId)) : 29 | Actor.Props.Create(() => new CurrentAllEventsPublisher(fromOffset, maxBufferSize, writeJournalPluginId)); 30 | } 31 | } 32 | 33 | internal abstract class AbstractAllEventsPublisher : ActorPublisher 34 | { 35 | 36 | private ILoggingAdapter _log; 37 | protected long CurrentOffset; 38 | 39 | protected AbstractAllEventsPublisher(long fromOffset, int maxBufferSize, string writeJournalPluginId) 40 | { 41 | CurrentOffset = FromOffset = fromOffset; 42 | MaxBufferSize = maxBufferSize; 43 | Buffer = new DeliveryBuffer(OnNext); 44 | JournalRef = Persistence.Instance.Apply(Context.System).JournalFor(writeJournalPluginId); 45 | } 46 | 47 | protected ILoggingAdapter Log => _log ?? (_log = Context.GetLogger()); 48 | protected IActorRef JournalRef { get; } 49 | protected DeliveryBuffer Buffer { get; } 50 | protected long FromOffset { get; } 51 | protected abstract long ToOffset { get; } 52 | protected int MaxBufferSize { get; } 53 | protected bool IsTimeForReplay => (Buffer.IsEmpty || Buffer.Length <= MaxBufferSize / 2) && (CurrentOffset <= ToOffset); 54 | 55 | protected abstract void ReceiveInitialRequest(); 56 | protected abstract void ReceiveIdleRequest(); 57 | protected abstract void ReceiveRecoverySuccess(long highestOrderingNr); 58 | 59 | protected override bool Receive(object message) 60 | { 61 | switch (message) 62 | { 63 | case Request _: 64 | ReceiveInitialRequest(); 65 | return true; 66 | case Cancel _: 67 | Context.Stop(Self); 68 | return true; 69 | case AllEventsPublisher.Continue _: 70 | return true; 71 | default: 72 | return false; 73 | } 74 | } 75 | 76 | protected bool Idle(object message) 77 | { 78 | switch (message) 79 | { 80 | case AllEventsPublisher.Continue _: 81 | if (IsTimeForReplay) Replay(); 82 | return true; 83 | case Request _: 84 | ReceiveIdleRequest(); 85 | return true; 86 | case Cancel _: 87 | Context.Stop(Self); 88 | return true; 89 | default: 90 | return false; 91 | } 92 | } 93 | 94 | protected void Replay() 95 | { 96 | var limit = MaxBufferSize - Buffer.Length; 97 | Log.Debug("replay all events request from [{0}] to [{1}], limit [{2}]", CurrentOffset, ToOffset, limit); 98 | JournalRef.Tell(new ReplayAllEvents(CurrentOffset, ToOffset, limit, Self)); 99 | Context.Become(Replaying); 100 | } 101 | 102 | protected bool Replaying(object message) 103 | { 104 | switch (message) 105 | { 106 | case ReplayedEvent replayed: 107 | // ReplayEvent might overshoot the current ToOffset target 108 | if (replayed.Offset > ToOffset) 109 | return true; 110 | 111 | Buffer.Add(new EventEnvelope( 112 | offset: new Sequence(replayed.Offset), 113 | persistenceId: replayed.Persistent.PersistenceId, 114 | sequenceNr: replayed.Persistent.SequenceNr, 115 | timestamp: replayed.Persistent.Timestamp, 116 | @event: replayed.Persistent.Payload, 117 | tags: replayed.Tags)); 118 | 119 | CurrentOffset = replayed.Offset; 120 | Buffer.DeliverBuffer(TotalDemand); 121 | return true; 122 | case EventReplaySuccess success: 123 | Log.Debug("event replay completed, currOffset [{0}], highestSequenceNr [{1}]", CurrentOffset, success.HighestSequenceNr); 124 | ReceiveRecoverySuccess(success.HighestSequenceNr); 125 | return true; 126 | case EventReplayFailure failure: 127 | Log.Debug("event replay failed, due to [{0}]", failure.Cause.Message); 128 | Buffer.DeliverBuffer(TotalDemand); 129 | OnErrorThenStop(failure.Cause); 130 | return true; 131 | case Request _: 132 | Buffer.DeliverBuffer(TotalDemand); 133 | return true; 134 | case Cancel _: 135 | Context.Stop(Self); 136 | return true; 137 | case AllEventsPublisher.Continue _: 138 | return true; 139 | default: 140 | return false; 141 | } 142 | } 143 | 144 | } 145 | 146 | internal sealed class LiveAllEventsPublisher : AbstractAllEventsPublisher 147 | { 148 | private readonly ICancelable _tickCancelable; 149 | public LiveAllEventsPublisher(long fromOffset, TimeSpan refreshInterval, int maxBufferSize, string writeJournalPluginId) 150 | : base(fromOffset, maxBufferSize, writeJournalPluginId) 151 | { 152 | _tickCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(refreshInterval, refreshInterval, Self, AllEventsPublisher.Continue.Instance, Self); 153 | } 154 | 155 | protected override long ToOffset => long.MaxValue; 156 | 157 | protected override void PostStop() 158 | { 159 | _tickCancelable.Cancel(); 160 | base.PostStop(); 161 | } 162 | 163 | protected override void ReceiveInitialRequest() 164 | { 165 | Replay(); 166 | } 167 | 168 | protected override void ReceiveIdleRequest() 169 | { 170 | Buffer.DeliverBuffer(TotalDemand); 171 | if (Buffer.IsEmpty && CurrentOffset > ToOffset) 172 | OnCompleteThenStop(); 173 | } 174 | 175 | protected override void ReceiveRecoverySuccess(long highestOrderingNr) 176 | { 177 | Buffer.DeliverBuffer(TotalDemand); 178 | if (Buffer.IsEmpty && CurrentOffset > ToOffset) 179 | OnCompleteThenStop(); 180 | 181 | Context.Become(Idle); 182 | } 183 | } 184 | 185 | internal sealed class CurrentAllEventsPublisher : AbstractAllEventsPublisher 186 | { 187 | public CurrentAllEventsPublisher(long fromOffset, int maxBufferSize, string writeJournalPluginId) 188 | : base(fromOffset, maxBufferSize, writeJournalPluginId) 189 | { } 190 | 191 | private long _toOffset = long.MaxValue; 192 | protected override long ToOffset => _toOffset; 193 | 194 | protected override void ReceiveInitialRequest() 195 | { 196 | Replay(); 197 | } 198 | 199 | protected override void ReceiveIdleRequest() 200 | { 201 | Buffer.DeliverBuffer(TotalDemand); 202 | if (Buffer.IsEmpty && CurrentOffset > ToOffset) 203 | OnCompleteThenStop(); 204 | else 205 | Self.Tell(AllEventsPublisher.Continue.Instance); 206 | } 207 | 208 | protected override void ReceiveRecoverySuccess(long highestOrderingNr) 209 | { 210 | Buffer.DeliverBuffer(TotalDemand); 211 | 212 | if (highestOrderingNr < ToOffset) 213 | _toOffset = highestOrderingNr; 214 | 215 | if (Buffer.IsEmpty && CurrentOffset >= ToOffset) 216 | OnCompleteThenStop(); 217 | else 218 | Self.Tell(AllEventsPublisher.Continue.Instance); 219 | 220 | Context.Become(Idle); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/AllPersistenceIdsPublisher.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2018 Lightbend Inc. 4 | // Copyright (C) 2013-2018 .NET Foundation 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using Akka.Actor; 9 | using Akka.Streams.Actors; 10 | using System; 11 | 12 | namespace Akka.Persistence.MongoDb.Query 13 | { 14 | internal sealed class CurrentPersistenceIdsPublisher : ActorPublisher, IWithUnboundedStash 15 | { 16 | public static Props Props(string writeJournalPluginId) 17 | { 18 | return Actor.Props.Create(() => new CurrentPersistenceIdsPublisher(writeJournalPluginId)); 19 | } 20 | 21 | private readonly IActorRef _journalRef; 22 | 23 | private readonly DeliveryBuffer _buffer; 24 | 25 | public IStash Stash { get; set; } 26 | 27 | public CurrentPersistenceIdsPublisher(string writeJournalPluginId) 28 | { 29 | _buffer = new DeliveryBuffer(OnNext); 30 | _journalRef = Persistence.Instance.Apply(Context.System).JournalFor(writeJournalPluginId); 31 | } 32 | 33 | protected override bool Receive(object message) 34 | { 35 | switch (message) 36 | { 37 | case Request _: 38 | _journalRef.Tell(new SelectCurrentPersistenceIds(0, Self)); 39 | Become(Initializing); 40 | return true; 41 | case Cancel _: 42 | Context.Stop(Self); 43 | return true; 44 | default: 45 | return false; 46 | } 47 | } 48 | 49 | private bool Initializing(object message) 50 | { 51 | switch (message) 52 | { 53 | case CurrentPersistenceIds current: 54 | _buffer.AddRange(current.AllPersistenceIds); 55 | _buffer.DeliverBuffer(TotalDemand); 56 | 57 | if (_buffer.IsEmpty) 58 | { 59 | OnCompleteThenStop(); 60 | return true; 61 | } 62 | 63 | Become(Active); 64 | Stash.UnstashAll(); 65 | return true; 66 | case Cancel _: 67 | Context.Stop(Self); 68 | return true; 69 | default: 70 | Stash.Stash(); 71 | return true; 72 | } 73 | } 74 | 75 | private bool Active(object message) 76 | { 77 | switch (message) 78 | { 79 | case Request _: 80 | _buffer.DeliverBuffer(TotalDemand); 81 | if (_buffer.IsEmpty) 82 | OnCompleteThenStop(); 83 | return true; 84 | case Cancel _: 85 | Context.Stop(Self); 86 | return true; 87 | default: 88 | return false; 89 | } 90 | } 91 | } 92 | 93 | internal sealed class LivePersistenceIdsPublisher : ActorPublisher, IWithUnboundedStash 94 | { 95 | private class Continue 96 | { 97 | public static readonly Continue Instance = new Continue(); 98 | 99 | private Continue() { } 100 | } 101 | 102 | public static Props Props(TimeSpan refreshInterval, string writeJournalPluginId) 103 | { 104 | return Actor.Props.Create(() => new LivePersistenceIdsPublisher(refreshInterval, writeJournalPluginId)); 105 | } 106 | 107 | private long _lastOrderingOffset; 108 | private readonly ICancelable _tickCancelable; 109 | private readonly IActorRef _journalRef; 110 | private readonly DeliveryBuffer _buffer; 111 | 112 | public IStash Stash { get; set; } 113 | 114 | public LivePersistenceIdsPublisher(TimeSpan refreshInterval, string writeJournalPluginId) 115 | { 116 | _tickCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable( 117 | refreshInterval, 118 | refreshInterval, 119 | Self, 120 | Continue.Instance, 121 | Self); 122 | _buffer = new DeliveryBuffer(OnNext); 123 | _journalRef = Persistence.Instance.Apply(Context.System).JournalFor(writeJournalPluginId); 124 | } 125 | 126 | protected override void PostStop() 127 | { 128 | _tickCancelable.Cancel(); 129 | base.PostStop(); 130 | } 131 | 132 | protected override bool Receive(object message) 133 | { 134 | switch (message) 135 | { 136 | case Request _: 137 | _journalRef.Tell(new SelectCurrentPersistenceIds(0, Self)); 138 | Become(Initializing); 139 | return true; 140 | case Continue _: 141 | return true; 142 | case Cancel _: 143 | Context.Stop(Self); 144 | return true; 145 | default: 146 | return false; 147 | } 148 | } 149 | 150 | private bool Initializing(object message) 151 | { 152 | switch (message) 153 | { 154 | case CurrentPersistenceIds current: 155 | _lastOrderingOffset = current.HighestOrderingNumber; 156 | _buffer.AddRange(current.AllPersistenceIds); 157 | _buffer.DeliverBuffer(TotalDemand); 158 | 159 | Become(Active); 160 | Stash.UnstashAll(); 161 | return true; 162 | case Continue _: 163 | return true; 164 | case Cancel _: 165 | Context.Stop(Self); 166 | return true; 167 | default: 168 | Stash.Stash(); 169 | return true; 170 | } 171 | } 172 | 173 | private bool Active(object message) 174 | { 175 | switch (message) 176 | { 177 | case CurrentPersistenceIds added: 178 | _lastOrderingOffset = added.HighestOrderingNumber; 179 | _buffer.AddRange(added.AllPersistenceIds); 180 | _buffer.DeliverBuffer(TotalDemand); 181 | return true; 182 | case Request _: 183 | _buffer.DeliverBuffer(TotalDemand); 184 | return true; 185 | case Continue _: 186 | _journalRef.Tell(new SelectCurrentPersistenceIds(_lastOrderingOffset, Self)); 187 | return true; 188 | case Cancel _: 189 | Context.Stop(Self); 190 | return true; 191 | default: 192 | return false; 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/DeliveryBuffer.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2018 Lightbend Inc. 4 | // Copyright (C) 2013-2018 .NET Foundation 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Collections.Immutable; 11 | 12 | namespace Akka.Persistence.MongoDb.Query 13 | { 14 | internal class DeliveryBuffer 15 | { 16 | public ImmutableArray Buffer { get; private set; } = ImmutableArray.Empty; 17 | public bool IsEmpty => Buffer.IsEmpty; 18 | public int Length => Buffer.Length; 19 | 20 | private readonly Action _onNext; 21 | 22 | public DeliveryBuffer(Action onNext) 23 | { 24 | _onNext = onNext; 25 | } 26 | 27 | public void Add(T element) 28 | { 29 | Buffer = Buffer.Add(element); 30 | } 31 | public void AddRange(IEnumerable elements) 32 | { 33 | Buffer = Buffer.AddRange(elements); 34 | } 35 | 36 | public void DeliverBuffer(long demand) 37 | { 38 | if (!Buffer.IsEmpty && demand > 0) 39 | { 40 | var totalDemand = Math.Min((int)demand, Buffer.Length); 41 | if (Buffer.Length == 1) 42 | { 43 | // optimize for this common case 44 | _onNext(Buffer[0]); 45 | Buffer = ImmutableArray.Empty; 46 | } 47 | else if (demand <= int.MaxValue) 48 | { 49 | for (var i = 0; i < totalDemand; i++) 50 | _onNext(Buffer[i]); 51 | 52 | Buffer = Buffer.RemoveRange(0, totalDemand); 53 | } 54 | else { 55 | foreach (var element in Buffer) 56 | _onNext(element); 57 | 58 | Buffer = ImmutableArray.Empty; 59 | } 60 | } 61 | 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/EventsByTagPublisher.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2018 Lightbend Inc. 4 | // Copyright (C) 2013-2018 .NET Foundation 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using System; 9 | using Akka.Actor; 10 | using Akka.Event; 11 | using Akka.Persistence.Query; 12 | using Akka.Streams.Actors; 13 | 14 | namespace Akka.Persistence.MongoDb.Query 15 | { 16 | internal static class EventsByTagPublisher 17 | { 18 | [Serializable] 19 | public sealed class Continue 20 | { 21 | public static readonly Continue Instance = new Continue(); 22 | 23 | private Continue() 24 | { 25 | } 26 | } 27 | 28 | public static Props Props(string tag, long fromOffset, long toOffset, TimeSpan? refreshInterval, int maxBufferSize, string writeJournalPluginId) 29 | { 30 | return refreshInterval.HasValue 31 | ? Actor.Props.Create(() => new LiveEventsByTagPublisher(tag, fromOffset, toOffset, refreshInterval.Value, maxBufferSize, writeJournalPluginId)) 32 | : Actor.Props.Create(() => new CurrentEventsByTagPublisher(tag, fromOffset, toOffset, maxBufferSize, writeJournalPluginId)); 33 | } 34 | } 35 | 36 | internal abstract class AbstractEventsByTagPublisher : ActorPublisher 37 | { 38 | private ILoggingAdapter _log; 39 | 40 | protected readonly DeliveryBuffer Buffer; 41 | protected readonly IActorRef JournalRef; 42 | protected long CurrentOffset; 43 | protected AbstractEventsByTagPublisher(string tag, long fromOffset, int maxBufferSize, string writeJournalPluginId) 44 | { 45 | Tag = tag; 46 | CurrentOffset = FromOffset = fromOffset; 47 | MaxBufferSize = maxBufferSize; 48 | WriteJournalPluginId = writeJournalPluginId; 49 | Buffer = new DeliveryBuffer(OnNext); 50 | JournalRef = Persistence.Instance.Apply(Context.System).JournalFor(writeJournalPluginId); 51 | } 52 | 53 | protected ILoggingAdapter Log => _log ?? (_log = Context.GetLogger()); 54 | protected string Tag { get; } 55 | protected long FromOffset { get; } 56 | protected abstract long ToOffset { get; } 57 | protected int MaxBufferSize { get; } 58 | protected string WriteJournalPluginId { get; } 59 | 60 | protected bool IsTimeForReplay => (Buffer.IsEmpty || Buffer.Length <= MaxBufferSize / 2) && (CurrentOffset <= ToOffset); 61 | 62 | protected abstract void ReceiveInitialRequest(); 63 | protected abstract void ReceiveIdleRequest(); 64 | protected abstract void ReceiveRecoverySuccess(long highestSequenceNr); 65 | 66 | protected override bool Receive(object message) 67 | { 68 | switch (message) 69 | { 70 | case Request _: 71 | ReceiveInitialRequest(); 72 | break; 73 | case EventsByTagPublisher.Continue _: 74 | // ignore 75 | break; 76 | case Cancel _: 77 | Context.Stop(Self); 78 | break; 79 | default: 80 | return false; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | 87 | 88 | protected bool Idle(object message) 89 | { 90 | switch (message) 91 | { 92 | case EventsByTagPublisher.Continue _: 93 | if (IsTimeForReplay) Replay(); 94 | break; 95 | case Request _: 96 | ReceiveIdleRequest(); 97 | break; 98 | case Cancel _: 99 | Context.Stop(Self); 100 | break; 101 | default: 102 | return false; 103 | } 104 | 105 | return true; 106 | } 107 | 108 | protected void Replay() 109 | { 110 | var limit = MaxBufferSize - Buffer.Length; 111 | Log.Debug("request replay for tag [{0}] from [{1}] to [{2}] limit [{3}]", Tag, CurrentOffset, ToOffset, limit); 112 | JournalRef.Tell(new ReplayTaggedMessages(CurrentOffset, ToOffset, limit, Tag, Self)); 113 | Context.Become(Replaying(limit)); 114 | } 115 | 116 | protected Receive Replaying(int limit) 117 | { 118 | return message => 119 | { 120 | switch (message) 121 | { 122 | case ReplayedTaggedMessage replayed: 123 | Buffer.Add(new EventEnvelope( 124 | offset: new Sequence(replayed.Offset), 125 | persistenceId: replayed.Persistent.PersistenceId, 126 | sequenceNr: replayed.Persistent.SequenceNr, 127 | timestamp: replayed.Persistent.Timestamp, 128 | @event: replayed.Persistent.Payload, 129 | tags: new [] { replayed.Tag })); 130 | 131 | CurrentOffset = replayed.Offset; 132 | Buffer.DeliverBuffer(TotalDemand); 133 | break; 134 | case RecoverySuccess success: 135 | Log.Debug("replay completed for tag [{0}], currOffset [{1}]", Tag, CurrentOffset); 136 | ReceiveRecoverySuccess(success.HighestSequenceNr); 137 | break; 138 | case ReplayMessagesFailure failure: 139 | Log.Debug("replay failed for tag [{0}], due to [{1}]", Tag, failure.Cause.Message); 140 | Buffer.DeliverBuffer(TotalDemand); 141 | OnErrorThenStop(failure.Cause); 142 | break; 143 | case Request _: 144 | Buffer.DeliverBuffer(TotalDemand); 145 | break; 146 | case EventsByTagPublisher.Continue _: 147 | // ignore 148 | break; 149 | case Cancel _: 150 | Context.Stop(Self); 151 | break; 152 | default: 153 | return false; 154 | } 155 | 156 | return true; 157 | }; 158 | } 159 | } 160 | 161 | internal sealed class LiveEventsByTagPublisher : AbstractEventsByTagPublisher 162 | { 163 | private readonly ICancelable _tickCancelable; 164 | public LiveEventsByTagPublisher(string tag, long fromOffset, long toOffset, TimeSpan refreshInterval, int maxBufferSize, string writeJournalPluginId) 165 | : base(tag, fromOffset, maxBufferSize, writeJournalPluginId) 166 | { 167 | ToOffset = toOffset; 168 | _tickCancelable = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(refreshInterval, refreshInterval, Self, EventsByTagPublisher.Continue.Instance, Self); 169 | } 170 | 171 | protected override long ToOffset { get; } 172 | 173 | protected override void PostStop() 174 | { 175 | _tickCancelable.Cancel(); 176 | base.PostStop(); 177 | } 178 | 179 | protected override void ReceiveInitialRequest() 180 | { 181 | Replay(); 182 | } 183 | 184 | protected override void ReceiveIdleRequest() 185 | { 186 | Buffer.DeliverBuffer(TotalDemand); 187 | if (Buffer.IsEmpty && CurrentOffset > ToOffset) 188 | OnCompleteThenStop(); 189 | } 190 | 191 | protected override void ReceiveRecoverySuccess(long highestSequenceNr) 192 | { 193 | Buffer.DeliverBuffer(TotalDemand); 194 | if (Buffer.IsEmpty && CurrentOffset > ToOffset) 195 | OnCompleteThenStop(); 196 | 197 | Context.Become(Idle); 198 | } 199 | } 200 | 201 | internal sealed class CurrentEventsByTagPublisher : AbstractEventsByTagPublisher 202 | { 203 | public CurrentEventsByTagPublisher(string tag, long fromOffset, long toOffset, int maxBufferSize, string writeJournalPluginId) 204 | : base(tag, fromOffset, maxBufferSize, writeJournalPluginId) 205 | { 206 | _toOffset = toOffset; 207 | } 208 | 209 | private long _toOffset; 210 | protected override long ToOffset => _toOffset; 211 | protected override void ReceiveInitialRequest() 212 | { 213 | Replay(); 214 | } 215 | 216 | protected override void ReceiveIdleRequest() 217 | { 218 | Buffer.DeliverBuffer(TotalDemand); 219 | if (Buffer.IsEmpty && CurrentOffset >= ToOffset) 220 | OnCompleteThenStop(); 221 | else 222 | Self.Tell(EventsByTagPublisher.Continue.Instance); 223 | } 224 | 225 | protected override void ReceiveRecoverySuccess(long highestSequenceNr) 226 | { 227 | Buffer.DeliverBuffer(TotalDemand); 228 | if (highestSequenceNr > 0 && highestSequenceNr < ToOffset) 229 | _toOffset = highestSequenceNr; 230 | 231 | if (Buffer.IsEmpty && (CurrentOffset >= ToOffset || CurrentOffset == FromOffset)) 232 | OnCompleteThenStop(); 233 | else 234 | Self.Tell(EventsByTagPublisher.Continue.Instance); 235 | 236 | Context.Become(Idle); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/MongoDbReadJournalProvider.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Configuration; 3 | using Akka.Persistence.Query; 4 | 5 | namespace Akka.Persistence.MongoDb.Query 6 | { 7 | public class MongoDbReadJournalProvider : IReadJournalProvider 8 | { 9 | private readonly ExtendedActorSystem _system; 10 | private readonly Config _config; 11 | 12 | /// 13 | /// 14 | /// 15 | /// instance of actor system at which read journal should be started 16 | /// 17 | public MongoDbReadJournalProvider(ExtendedActorSystem system, Config config) 18 | { 19 | _system = system; 20 | _config = config; 21 | } 22 | 23 | /// 24 | /// Returns instance of EventStoreReadJournal 25 | /// 26 | /// 27 | public IReadJournal GetReadJournal() 28 | { 29 | return new MongoDbReadJournal(_system, _config); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Query/SubscriptionDroppedException.cs: -------------------------------------------------------------------------------- 1 | using Akka.Event; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Akka.Persistence.MongoDb.Query 9 | { 10 | public class SubscriptionDroppedException : Exception, IDeadLetterSuppression 11 | { 12 | 13 | public SubscriptionDroppedException() : this("Unknown error", null) 14 | { 15 | 16 | } 17 | 18 | public SubscriptionDroppedException(string message, Exception inner) : base(message, inner) 19 | { 20 | 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Snapshot/GridFsPayloadEnvelope.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | using MongoDB.Bson.Serialization.Attributes; 3 | 4 | namespace Akka.Persistence.MongoDb.Snapshot; 5 | 6 | public class GridFsPayloadEnvelope 7 | { 8 | [BsonElement("_v")] 9 | public object Payload { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/Snapshot/SnapshotEntry.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2009-2016 Lightbend Inc. 4 | // Copyright (C) 2013-2016 Akka.NET project 5 | // 6 | //----------------------------------------------------------------------- 7 | 8 | using MongoDB.Bson.Serialization.Attributes; 9 | 10 | namespace Akka.Persistence.MongoDb.Snapshot 11 | { 12 | /// 13 | /// Class used for storing a Snapshot as BsonDocument 14 | /// 15 | public class SnapshotEntry 16 | { 17 | [BsonId] 18 | public string Id { get; set; } 19 | 20 | [BsonElement("PersistenceId")] 21 | public string PersistenceId { get; set; } 22 | 23 | [BsonElement("SequenceNr")] 24 | public long SequenceNr { get; set; } 25 | 26 | [BsonElement("Timestamp")] 27 | public long Timestamp { get; set; } 28 | 29 | [BsonElement("Snapshot")] 30 | public object Snapshot { get; set; } 31 | 32 | [BsonElement("Manifest")] 33 | public string Manifest { get; set; } 34 | 35 | [BsonElement("SerializerId")] 36 | public int? SerializerId { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.MongoDb/reference.conf: -------------------------------------------------------------------------------- 1 | akka.persistence { 2 | journal { 3 | mongodb { 4 | # qualified type name of the MongoDb persistence journal actor 5 | class = "Akka.Persistence.MongoDb.Journal.MongoDbJournal, Akka.Persistence.MongoDb" 6 | 7 | # connection string used for database access 8 | connection-string = "" 9 | 10 | # transaction 11 | use-write-transaction = on 12 | 13 | use-read-transaction = on 14 | 15 | # Set the batch size for read operations. 16 | # If set to off or false, this will use the default MongoDb behavior where all queries 17 | # will have a batch size of 101 on the first page read and then will try to read 18 | # as many documents as possible that fits the 16 MeBi limit. 19 | read-batch-size = off 20 | 21 | # should corresponding journal table's indexes be initialized automatically 22 | auto-initialize = on 23 | 24 | # dispatcher used to drive journal actor 25 | plugin-dispatcher = "akka.actor.default-dispatcher" 26 | 27 | # MongoDb collection corresponding with persistent journal 28 | collection = "EventJournal" 29 | 30 | # metadata collection 31 | metadata-collection = "Metadata" 32 | 33 | # For users with legacy data, who want to keep writing data to MongoDb using the original BSON format 34 | # and not the standard binary format introduced in v1.4.0 (see https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/72) 35 | # enable this setting via `legacy-serialization = on`. 36 | # 37 | # NOTE: this will likely break features such as Akka.Cluster.Sharding, IActorRef serialization, AtLeastOnceDelivery, and more. 38 | legacy-serialization = off 39 | 40 | # Per-call timeout setting - Journal will err on the side of caution and fail calls that take longer than this 41 | # to complete. This is to prevent the journal from blocking indefinitely if the database is slow or unresponsive. 42 | # If you experience frequent failures due to timeouts, you may want to increase this value. 43 | # Default: 10 seconds 44 | call-timeout = 10s 45 | } 46 | } 47 | 48 | snapshot-store { 49 | mongodb { 50 | # qualified type name of the MongoDB persistence snapshot actor 51 | class = "Akka.Persistence.MongoDb.Snapshot.MongoDbSnapshotStore, Akka.Persistence.MongoDb" 52 | 53 | # connection string used for database access 54 | connection-string = "" 55 | 56 | # transaction 57 | use-write-transaction = off 58 | 59 | use-read-transaction = on 60 | 61 | # should corresponding snapshot's indexes be initialized automatically 62 | auto-initialize = on 63 | 64 | # dispatcher used to drive snapshot storage actor 65 | plugin-dispatcher = "akka.actor.default-dispatcher" 66 | 67 | # MongoDb collection corresponding with persistent snapshot store 68 | collection = "SnapshotStore" 69 | 70 | # For users with legacy data, who want to keep writing data to MongoDb using the original BSON format 71 | # and not the standard binary format introduced in v1.4.0 (see https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/72) 72 | # enable this setting via `legacy-serialization = on`. 73 | # 74 | # NOTE: this will likely break features such as Akka.Cluster.Sharding, IActorRef serialization, AtLeastOnceDelivery, and more. 75 | legacy-serialization = off 76 | 77 | # Per-call timeout setting - Journal will err on the side of caution and fail calls that take longer than this 78 | # to complete. This is to prevent the journal from blocking indefinitely if the database is slow or unresponsive. 79 | # If you experience frequent failures due to timeouts, you may want to increase this value. 80 | # Default: 10 seconds 81 | call-timeout = 10s 82 | } 83 | } 84 | 85 | query { 86 | mongodb { 87 | # Implementation class of the EventStore ReadJournalProvider 88 | class = "Akka.Persistence.MongoDb.Query.MongoDbReadJournalProvider, Akka.Persistence.MongoDb" 89 | 90 | # Absolute path to the write journal plugin configuration entry that this 91 | # query journal will connect to. 92 | # If undefined (or "") it will connect to the default journal as specified by the 93 | # akka.persistence.journal.plugin property. 94 | write-plugin = "" 95 | 96 | # The SQL write journal is notifying the query side as soon as things 97 | # are persisted, but for efficiency reasons the query side retrieves the events 98 | # in batches that sometimes can be delayed up to the configured `refresh-interval`. 99 | refresh-interval = 3s 100 | 101 | # How many events to fetch in one query (replay) and keep buffered until they 102 | # are delivered downstreams. 103 | max-buffer-size = 500 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/examples/LargeSnapshot/Actors/PersistentActor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Event; 3 | using Akka.Persistence; 4 | using Akka.Util; 5 | 6 | namespace LargeSnapshot.Actors; 7 | 8 | public class PersistentActor: ReceivePersistentActor, IWithTimers 9 | { 10 | private const int MinData = 16 * 1024; 11 | private const int MaxDataSize = 24 * 1024 * 1024; 12 | private readonly MemoryStream _buffer; 13 | 14 | public PersistentActor(string persistenceId) 15 | { 16 | _buffer = new MemoryStream(); 17 | var currentLength = 0; 18 | PersistenceId = persistenceId; 19 | 20 | var log = Context.GetLogger(); 21 | 22 | Recover(offer => 23 | { 24 | var bytes = (byte[]) offer.Snapshot; 25 | log.Info($"Snapshot recovered. Size: {bytes.Length}"); 26 | currentLength = bytes.Length; 27 | _buffer.Write(bytes); 28 | }); 29 | 30 | Command(_ => 31 | { 32 | log.Info("Snapshot saved"); 33 | }); 34 | 35 | Command( 36 | msg => msg is "send", 37 | _ => 38 | { 39 | if (currentLength >= MaxDataSize) 40 | { 41 | var data = _buffer.ToArray(); 42 | log.Info($"Saving snapshot, size: {data.Length}"); 43 | SaveSnapshot(data); 44 | } 45 | else 46 | { 47 | var nextLength = currentLength * 2; 48 | if (nextLength == 0) 49 | nextLength = MinData; 50 | else if (nextLength > MaxDataSize) 51 | nextLength = MaxDataSize; 52 | 53 | var diff = nextLength - currentLength; 54 | currentLength = nextLength; 55 | var buffer = new byte[diff]; 56 | ThreadLocalRandom.Current.NextBytes(buffer); 57 | _buffer.Write(buffer); 58 | 59 | buffer = _buffer.ToArray(); 60 | log.Info($"Saving snapshot, size: {buffer.Length}"); 61 | SaveSnapshot(buffer); 62 | } 63 | 64 | }); 65 | 66 | Command( 67 | msg => msg is "crash", 68 | _ => throw new Exception("CRASH!")); 69 | } 70 | 71 | public override string PersistenceId { get; } 72 | public ITimerScheduler Timers { get; set; } = null!; 73 | 74 | protected override void PreStart() 75 | { 76 | Timers.StartPeriodicTimer("send-key", "send", TimeSpan.FromSeconds(10)); 77 | Timers.StartSingleTimer("crash-key", "crash", TimeSpan.FromSeconds(35)); 78 | } 79 | 80 | protected override void PostStop() 81 | { 82 | _buffer.Dispose(); 83 | } 84 | } -------------------------------------------------------------------------------- /src/examples/LargeSnapshot/LargeSnapshot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Always 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/examples/LargeSnapshot/Program.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Hosting; 3 | using Akka.Persistence.Hosting; 4 | using Akka.Persistence.MongoDb.Hosting; 5 | using LargeSnapshot.Actors; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | var hostBuilder = Host.CreateDefaultBuilder(args); 12 | hostBuilder 13 | .ConfigureLogging(builder => 14 | { 15 | builder.ClearProviders(); 16 | builder.AddConsole(); 17 | builder.Services.Configure(opt => 18 | { 19 | opt.MinLevel = LogLevel.Debug; 20 | }); 21 | }) 22 | .ConfigureServices((context, services) => 23 | { 24 | services.AddAkka("LargeSnapshotSys", builder => 25 | { 26 | var snapshotOptions = new MongoDbGridFsSnapshotOptions() 27 | { 28 | ConnectionString = context.Configuration["ConnectionStrings:MongoDb"], 29 | AutoInitialize = true 30 | }; 31 | 32 | builder 33 | .ConfigureLoggers(logger => 34 | { 35 | logger.ClearLoggers(); 36 | logger.AddDefaultLogger(); 37 | }) 38 | .WithSnapshot(snapshotOptions) 39 | .WithInMemoryJournal() 40 | .WithActors((system, registry) => 41 | { 42 | system.ActorOf(Props.Create(() => new PersistentActor("persisted")), "persisted-actor"); 43 | }); 44 | }); 45 | }); 46 | 47 | await hostBuilder 48 | .UseConsoleLifetime() 49 | .RunConsoleAsync(); -------------------------------------------------------------------------------- /src/examples/LargeSnapshot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information", 7 | "Akka": "Debug" 8 | } 9 | }, 10 | "ConnectionStrings": { 11 | "MongoDb": "mongodb://localhost:27017/akka-db" 12 | } 13 | } -------------------------------------------------------------------------------- /src/examples/start_docker.ps1: -------------------------------------------------------------------------------- 1 | docker run --name mongo --rm -p 27017:27017 -e MONGO_INITDB_DATABASE=akka-db mongo:latest --------------------------------------------------------------------------------