├── src ├── Akka.Persistence.Cassandra.Tests │ ├── app.config │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── CassandraJournalSpec.cs │ ├── CassandraSnapshotStoreSpec.cs │ ├── packages.config │ ├── TestSetupHelpers.cs │ ├── Akka.Persistence.Cassandra.Tests.csproj │ └── CassandraIntegrationSpec.cs ├── .nuget │ ├── NuGet.Config │ ├── NuGet.Dev.Config │ └── NuGet.targets ├── SharedAssemblyInfo.cs ├── Akka.Persistence.Cassandra │ ├── packages.config │ ├── SessionManagement │ │ ├── CassandraSession.cs │ │ ├── IManageSessions.cs │ │ ├── DefaultSessionManager.cs │ │ └── SessionSettings.cs │ ├── CassandraPersistence.cs │ ├── Snapshot │ │ ├── CassandraSnapshotStoreSettings.cs │ │ ├── SnapshotStoreStatements.cs │ │ └── CassandraSnapshotStore.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Akka.Persistence.Cassandra.nuspec │ ├── Journal │ │ ├── CassandraJournalSettings.cs │ │ ├── JournalStatements.cs │ │ └── CassandraJournal.cs │ ├── ExtensionMethods.cs │ ├── CassandraExtension.cs │ ├── CassandraSettings.cs │ ├── reference.conf │ └── Akka.Persistence.Cassandra.csproj └── Akka.Persistence.Cassandra.sln ├── .editorconfig ├── teardown_db_container.sh ├── .gitattributes ├── start_db_container.sh ├── RELEASE_NOTES.md ├── wait-for-it.sh ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md └── README.md /src/Akka.Persistence.Cassandra.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/SharedAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyCompanyAttribute("Akka.NET Team")] 5 | [assembly: AssemblyCopyrightAttribute("Copyright © 2013-2015 Akka.NET Team")] 6 | [assembly: AssemblyTrademarkAttribute("")] 7 | [assembly: AssemblyVersionAttribute("1.0.7.0")] 8 | [assembly: AssemblyFileVersionAttribute("1.0.7.0")] 9 | -------------------------------------------------------------------------------- /teardown_db_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Tearing down any existing containers with deployer=akkadotnet" 4 | runningContainers=$(docker ps -aq -f label=deployer=akkadotnet) 5 | if [ ${#runningContainers[@]} -gt 0 ] 6 | then 7 | for i in $runningContainers 8 | do 9 | if [ "$i" != "" ] # 1st query can return non-empty array with null element 10 | then 11 | docker stop $i 12 | docker rm $i 13 | fi 14 | done 15 | fi -------------------------------------------------------------------------------- /src/.nuget/NuGet.Dev.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/SessionManagement/CassandraSession.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace Akka.Persistence.Cassandra.SessionManagement 4 | { 5 | /// 6 | /// Extension Id provider for Cassandra Session management extension. 7 | /// 8 | public class CassandraSession : ExtensionIdProvider 9 | { 10 | public static CassandraSession Instance = new CassandraSession(); 11 | 12 | public override IManageSessions CreateExtension(ExtendedActorSystem system) 13 | { 14 | return new DefaultSessionManager(system); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/SessionManagement/IManageSessions.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Cassandra; 3 | 4 | namespace Akka.Persistence.Cassandra.SessionManagement 5 | { 6 | /// 7 | /// Contract for extension responsible for resolving/releasing Cassandra ISession instances used by the 8 | /// Cassandra Persistence plugin. 9 | /// 10 | public interface IManageSessions : IExtension 11 | { 12 | /// 13 | /// Resolves the session with the key specified. 14 | /// 15 | ISession ResolveSession(string key); 16 | 17 | /// 18 | /// Releases the session instance. 19 | /// 20 | void ReleaseSession(ISession session); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/CassandraPersistence.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Akka.Configuration; 3 | 4 | namespace Akka.Persistence.Cassandra 5 | { 6 | /// 7 | /// Extension Id provider for the Cassandra Persistence extension. 8 | /// 9 | public class CassandraPersistence : ExtensionIdProvider 10 | { 11 | public static readonly CassandraPersistence Instance = new CassandraPersistence(); 12 | 13 | public override CassandraExtension CreateExtension(ExtendedActorSystem system) 14 | { 15 | return new CassandraExtension(system); 16 | } 17 | 18 | public static Config DefaultConfig() 19 | { 20 | return ConfigurationFactory.FromResource("Akka.Persistence.Cassandra.reference.conf"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Snapshot/CassandraSnapshotStoreSettings.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | 3 | namespace Akka.Persistence.Cassandra.Snapshot 4 | { 5 | /// 6 | /// Settings for the Cassandra snapshot store implementation, parsed from HOCON configuration. 7 | /// 8 | public class CassandraSnapshotStoreSettings : CassandraSettings 9 | { 10 | /// 11 | /// The maximum number of snapshot metadata records to retrieve in a single request when trying to find 12 | /// snapshots that meet criteria. 13 | /// 14 | public int MaxMetadataResultSize { get; private set; } 15 | 16 | public CassandraSnapshotStoreSettings(Config config) 17 | : base(config) 18 | { 19 | MaxMetadataResultSize = config.GetInt("max-metadata-result-size"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Akka.Persistence.Cassandra")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyProduct("Akka.Persistence.Cassandra")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("3c96b6f4-572a-4559-9487-bb91db490506")] -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Akka.Persistence.Cassandra.Tests")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyProduct("Akka.Persistence.Cassandra.Tests")] 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("64d0cc80-1160-4ae7-89fe-304252260860")] -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Akka.Persistence.Cassandra.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @project@ 5 | @project@@title@ 6 | @build.number@ 7 | @authors@ 8 | @authors@ 9 | Cassandra Persistence support for Akka.NET 10 | https://github.com/akkadotnet/akka.net/blob/master/LICENSE 11 | https://github.com/akkadotnet/akka.net 12 | http://getakka.net/images/AkkaNetLogo.Normal.png 13 | false 14 | @releaseNotes@ 15 | @copyright@ 16 | @tags@ Persistence Cassandra 17 | @dependencies@ 18 | @references@ 19 | 20 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Journal/CassandraJournalSettings.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | 3 | namespace Akka.Persistence.Cassandra.Journal 4 | { 5 | /// 6 | /// Settings for the Cassandra journal implementation, parsed from HOCON configuration. 7 | /// 8 | public class CassandraJournalSettings : CassandraSettings 9 | { 10 | /// 11 | /// The approximate number of rows per partition to use. Cannot be changed after table creation. 12 | /// 13 | public long PartitionSize { get; private set; } 14 | 15 | /// 16 | /// The maximum number of messages to retrieve in one request when replaying messages. 17 | /// 18 | public int MaxResultSize { get; private set; } 19 | 20 | public CassandraJournalSettings(Config config) 21 | : base(config) 22 | { 23 | PartitionSize = config.GetLong("partition-size"); 24 | MaxResultSize = config.GetInt("max-result-size"); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/CassandraJournalSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | using Akka.Configuration; 3 | using Akka.Persistence.TestKit.Journal; 4 | using Xunit.Abstractions; 5 | 6 | namespace Akka.Persistence.Cassandra.Tests 7 | { 8 | public class CassandraJournalSpec : JournalSpec 9 | { 10 | private static readonly Config JournalConfig = ConfigurationFactory.ParseString($@" 11 | akka.persistence.journal.plugin = ""cassandra-journal"" 12 | akka.test.single-expect-default = 10s 13 | cassandra-sessions.default.contact-points = [ ""{ ConfigurationManager.AppSettings["cassandraContactPoint"] }"" ] 14 | "); 15 | 16 | public CassandraJournalSpec(ITestOutputHelper output) 17 | : base(JournalConfig, "CassandraJournalSystem", output: output) 18 | { 19 | TestSetupHelpers.ResetJournalData(Sys); 20 | Initialize(); 21 | } 22 | 23 | protected override void Dispose(bool disposing) 24 | { 25 | TestSetupHelpers.ResetJournalData(Sys); 26 | base.Dispose(disposing); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/CassandraSnapshotStoreSpec.cs: -------------------------------------------------------------------------------- 1 | using Akka.Configuration; 2 | using Akka.Persistence.TestKit.Snapshot; 3 | using System.Configuration; 4 | using Xunit.Abstractions; 5 | 6 | namespace Akka.Persistence.Cassandra.Tests 7 | { 8 | public class CassandraSnapshotStoreSpec : SnapshotStoreSpec 9 | { 10 | private static readonly Config SnapshotConfig = ConfigurationFactory.ParseString($@" 11 | akka.persistence.snapshot-store.plugin = ""cassandra-snapshot-store"" 12 | akka.test.single-expect-default = 10s 13 | cassandra-sessions.default.contact-points = [ ""{ ConfigurationManager.AppSettings["cassandraContactPoint"] }"" ] 14 | "); 15 | 16 | public CassandraSnapshotStoreSpec(ITestOutputHelper output) 17 | : base(SnapshotConfig, "CassandraSnapshotSystem", output: output) 18 | { 19 | TestSetupHelpers.ResetSnapshotStoreData(Sys); 20 | Initialize(); 21 | } 22 | 23 | protected override void Dispose(bool disposing) 24 | { 25 | TestSetupHelpers.ResetSnapshotStoreData(Sys); 26 | base.Dispose(disposing); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /start_db_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while getopts ":i:" opt; do 4 | case $opt in 5 | i) 6 | imageName=$OPTARG 7 | echo "imageName = $imageName" 8 | echo "Tearing down any existing containers with deployer=akkadotnet" 9 | runningContainers=$(docker ps -aq -f label=deployer=akkadotnet) 10 | if [ ${#runningContainers[@]} -gt 0 ] 11 | then 12 | for i in $runningContainers 13 | do 14 | if [ "$i" != "" ] # 1st query can return non-empty array with null element 15 | then 16 | docker stop $i 17 | docker rm $i 18 | fi 19 | done 20 | fi 21 | echo "Starting docker container with imageName=$imageName and name=akka-cassandra-db" 22 | container_id=$(docker run -d --name=akka-cassandra-db -l deployer=akkadotnet $imageName) 23 | container_ip=$(docker inspect --format='{{ range .NetworkSettings.Networks }}{{ .IPAddress }}{{ end }}' $container_id) 24 | # sets environment variables just in case they're needed later in the build pipeline 25 | export CONTAINER_ID=$container_id 26 | export CONTAINER_IP=$container_ip 27 | ;; 28 | :) 29 | echo "imageName (-i) argument required" >&2 30 | exit 1 31 | ;; 32 | \?) 33 | echo "imageName (-i) flag is required" >&2 34 | exit 1 35 | ;; 36 | esac 37 | done -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/TestSetupHelpers.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using Cassandra; 3 | 4 | namespace Akka.Persistence.Cassandra.Tests 5 | { 6 | /// 7 | /// Some static helper methods for resetting Cassandra between tests or test contexts. 8 | /// 9 | public static class TestSetupHelpers 10 | { 11 | public static void ResetJournalData(ActorSystem sys) 12 | { 13 | // Get or add the extension 14 | var ext = CassandraPersistence.Instance.Apply(sys); 15 | 16 | // Use session to remove keyspace 17 | ISession session = ext.SessionManager.ResolveSession(ext.JournalSettings.SessionKey); 18 | session.DeleteKeyspaceIfExists(ext.JournalSettings.Keyspace); 19 | ext.SessionManager.ReleaseSession(session); 20 | } 21 | 22 | public static void ResetSnapshotStoreData(ActorSystem sys) 23 | { 24 | // Get or add the extension 25 | var ext = CassandraPersistence.Instance.Apply(sys); 26 | 27 | // Use session to remove the keyspace 28 | ISession session = ext.SessionManager.ResolveSession(ext.SnapshotStoreSettings.SessionKey); 29 | session.DeleteKeyspaceIfExists(ext.SnapshotStoreSettings.Keyspace); 30 | ext.SessionManager.ReleaseSession(session); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Snapshot/SnapshotStoreStatements.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cassandra; 3 | 4 | namespace Akka.Persistence.Cassandra.Snapshot 5 | { 6 | /// 7 | /// CQL strings used by the CassandraSnapshotStore. 8 | /// 9 | internal static class SnapshotStoreStatements 10 | { 11 | public const string CreateKeyspace = @" 12 | CREATE KEYSPACE IF NOT EXISTS {0} 13 | WITH {1}"; 14 | 15 | public const string CreateTable = @" 16 | CREATE TABLE IF NOT EXISTS {0} ( 17 | persistence_id text, 18 | sequence_number bigint, 19 | timestamp_ticks bigint, 20 | snapshot blob, 21 | PRIMARY KEY (persistence_id, sequence_number) 22 | ) WITH CLUSTERING ORDER BY (sequence_number DESC){1}{2}"; 23 | 24 | public const string WriteSnapshot = @" 25 | INSERT INTO {0} (persistence_id, sequence_number, timestamp_ticks, snapshot) 26 | VALUES (?, ?, ?, ?)"; 27 | 28 | public const string DeleteSnapshot = @" 29 | DELETE FROM {0} WHERE persistence_id = ? AND sequence_number = ?"; 30 | 31 | public const string SelectSnapshot = @" 32 | SELECT snapshot FROM {0} WHERE persistence_id = ? AND sequence_number = ?"; 33 | 34 | public const string SelectSnapshotMetadata = @" 35 | SELECT persistence_id, sequence_number, timestamp_ticks FROM {0} 36 | WHERE persistence_id = ? AND sequence_number <= ?"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Actor; 3 | using Cassandra; 4 | 5 | namespace Akka.Persistence.Cassandra 6 | { 7 | /// 8 | /// Extension methods used by the Cassandra persistence plugin. 9 | /// 10 | internal static class ExtensionMethods 11 | { 12 | /// 13 | /// Gets the PersistenceExtension instance registered with the ActorSystem. Throws an InvalidOperationException if not found. 14 | /// 15 | internal static PersistenceExtension PersistenceExtension(this ActorSystem system) 16 | { 17 | var ext = system.GetExtension(); 18 | if (ext == null) 19 | throw new InvalidOperationException("Persistence extension not found."); 20 | 21 | return ext; 22 | } 23 | 24 | /// 25 | /// Converts a Type to a string representation that can be stored in Cassandra. 26 | /// 27 | internal static string ToQualifiedString(this Type t) 28 | { 29 | return string.Format("{0}, {1}", t.FullName, t.Assembly.GetName().Name); 30 | } 31 | 32 | /// 33 | /// Prepares a CQL string with format arguments using the session. 34 | /// 35 | internal static PreparedStatement PrepareFormat(this ISession session, string cqlFormatString, params object[] args) 36 | { 37 | return session.Prepare(string.Format(cqlFormatString, args)); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 1.0.7 Feb 08 2016 #### 2 | Place holder for next version. 3 | 4 | #### 1.0.6 Feb 08 2016 #### 5 | Upgrades to Akka.NET v1.0.6 internally. 6 | 7 | #### 1.0.5 Dec 14 2015 #### 8 | Upgrades to Akka.NET v1.0.5 internally. 9 | 10 | #### 1.0.4 August 07 2015 #### 11 | 12 | #### 1.0.3 June 12 2015 #### 13 | **Bugfix release for Akka.NET v1.0.2.** 14 | 15 | This release addresses an issue with Akka.Persistence.SqlServer and Akka.Persistence.PostgreSql where both packages were missing a reference to Akka.Persistence.Sql.Common. 16 | 17 | In Akka.NET v1.0.3 we've packaged Akka.Persistence.Sql.Common into its own NuGet package and referenced it in the affected packages. 18 | 19 | #### 1.0.2 June 2 2015 20 | Initial Release of Akka.Persistence.PostgreSql 21 | 22 | Fixes & Changes - Akka.Persistence 23 | * [Renamed GuaranteedDelivery classes to AtLeastOnceDelivery](https://github.com/akkadotnet/akka.net/pull/984) 24 | * [Changes in Akka.Persistence SQL backend](https://github.com/akkadotnet/akka.net/pull/963) 25 | * [PostgreSQL persistence plugin for both event journal and snapshot store](https://github.com/akkadotnet/akka.net/pull/971) 26 | * [Cassandra persistence plugin](https://github.com/akkadotnet/akka.net/pull/995) 27 | 28 | **New Features:** 29 | 30 | **Akka.Persistence.PostgreSql** and **Akka.Persistence.Cassandra** 31 | Akka.Persistence now has two additional concrete implementations for PostgreSQL and Cassandra! You can install either of the packages using the following commandline: 32 | 33 | [Akka.Persistence.PostgreSql Configuration Docs](https://github.com/akkadotnet/akka.net/tree/dev/src/contrib/persistence/Akka.Persistence.PostgreSql) 34 | ``` 35 | PM> Install-Package Akka.Persistence.PostgreSql 36 | ``` 37 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/CassandraExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Actor; 3 | using Akka.Persistence.Cassandra.Journal; 4 | using Akka.Persistence.Cassandra.SessionManagement; 5 | using Akka.Persistence.Cassandra.Snapshot; 6 | 7 | namespace Akka.Persistence.Cassandra 8 | { 9 | /// 10 | /// An Akka.NET extension for Cassandra persistence. 11 | /// 12 | public class CassandraExtension : IExtension 13 | { 14 | /// 15 | /// The settings for the Cassandra journal. 16 | /// 17 | public CassandraJournalSettings JournalSettings { get; private set; } 18 | 19 | /// 20 | /// The settings for the Cassandra snapshot store. 21 | /// 22 | public CassandraSnapshotStoreSettings SnapshotStoreSettings { get; private set; } 23 | 24 | /// 25 | /// The session manager for resolving session instances. 26 | /// 27 | public IManageSessions SessionManager { get; private set; } 28 | 29 | public CassandraExtension(ExtendedActorSystem system) 30 | { 31 | if (system == null) throw new ArgumentNullException("system"); 32 | 33 | // Initialize fallback configuration defaults 34 | system.Settings.InjectTopLevelFallback(CassandraPersistence.DefaultConfig()); 35 | 36 | // Get or add the session manager 37 | SessionManager = CassandraSession.Instance.Apply(system); 38 | 39 | // Read config 40 | var journalConfig = system.Settings.Config.GetConfig("cassandra-journal"); 41 | JournalSettings = new CassandraJournalSettings(journalConfig); 42 | 43 | var snapshotConfig = system.Settings.Config.GetConfig("cassandra-snapshot-store"); 44 | SnapshotStoreSettings = new CassandraSnapshotStoreSettings(snapshotConfig); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/SessionManagement/DefaultSessionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using Akka.Actor; 4 | using Akka.Configuration; 5 | using Cassandra; 6 | 7 | namespace Akka.Persistence.Cassandra.SessionManagement 8 | { 9 | /// 10 | /// A default session manager implementation that reads configuration from the system's "cassandra-cluster" 11 | /// section and builds ISession instances from that configuration. Caches session instances for reuse. 12 | /// 13 | public class DefaultSessionManager : IManageSessions 14 | { 15 | private readonly Config _sessionConfigs; 16 | private readonly ConcurrentDictionary> _sessionCache; 17 | 18 | public DefaultSessionManager(ExtendedActorSystem system) 19 | { 20 | if (system == null) throw new ArgumentNullException("system"); 21 | 22 | // Read configuration sections 23 | _sessionConfigs = system.Settings.Config.GetConfig("cassandra-sessions"); 24 | 25 | _sessionCache = new ConcurrentDictionary>(); 26 | } 27 | 28 | /// 29 | /// Resolves the session with the key specified. 30 | /// 31 | public ISession ResolveSession(string key) 32 | { 33 | return _sessionCache.GetOrAdd(key, k => new Lazy(() => CreateSession(k))).Value; 34 | } 35 | 36 | /// 37 | /// Releases the session instance. 38 | /// 39 | public void ReleaseSession(ISession session) 40 | { 41 | // No-op since we want session instance to live for actor system's duration 42 | // (TODO: Dispose of session instance if hooks are added to listen for Actor system shutdown?) 43 | } 44 | 45 | private ISession CreateSession(string clusterName) 46 | { 47 | if (_sessionConfigs.HasPath(clusterName) == false) 48 | throw new ConfigurationException(string.Format("Cannot find cluster configuration named '{0}'", clusterName)); 49 | 50 | // Get a cluster builder from the settings, build the cluster, and connect for a session 51 | var clusterSettings = new SessionSettings(_sessionConfigs.GetConfig(clusterName)); 52 | return clusterSettings.Builder.Build().Connect(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Cassandra", "Akka.Persistence.Cassandra\Akka.Persistence.Cassandra.csproj", "{54BD0B45-8A46-4194-8C33-AD287CAC8FA4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Cassandra.Tests", "Akka.Persistence.Cassandra.Tests\Akka.Persistence.Cassandra.Tests.csproj", "{1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{8595E89A-2B9E-44CE-8286-0B755DA92226}" 11 | ProjectSection(SolutionItems) = preProject 12 | .nuget\NuGet.Config = .nuget\NuGet.Config 13 | .nuget\NuGet.Dev.Config = .nuget\NuGet.Dev.Config 14 | .nuget\NuGet.targets = .nuget\NuGet.targets 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{ECDF1F2F-FDAE-4F68-A6D1-7D76FDAC0526}" 18 | ProjectSection(SolutionItems) = preProject 19 | ..\build.cmd = ..\build.cmd 20 | ..\build.fsx = ..\build.fsx 21 | ..\build.sh = ..\build.sh 22 | ..\start_db_container.sh = ..\start_db_container.sh 23 | ..\teardown_db_container.sh = ..\teardown_db_container.sh 24 | ..\wait-for-it.sh = ..\wait-for-it.sh 25 | EndProjectSection 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {54BD0B45-8A46-4194-8C33-AD287CAC8FA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {54BD0B45-8A46-4194-8C33-AD287CAC8FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {54BD0B45-8A46-4194-8C33-AD287CAC8FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {54BD0B45-8A46-4194-8C33-AD287CAC8FA4}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/SessionManagement/SessionSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using Akka.Configuration; 6 | using Cassandra; 7 | 8 | namespace Akka.Persistence.Cassandra.SessionManagement 9 | { 10 | /// 11 | /// Internal class for converting basic session settings in HOCON configuration to a Builder instance from the 12 | /// DataStax driver for Cassandra. 13 | /// 14 | internal class SessionSettings 15 | { 16 | /// 17 | /// A Builder instance with the appropriate configuration settings applied to it. 18 | /// 19 | public Builder Builder { get; private set; } 20 | 21 | public SessionSettings(Config config) 22 | { 23 | if (config == null) throw new ArgumentNullException("config"); 24 | 25 | Builder = Cluster.Builder(); 26 | 27 | // Get IP and port configuration 28 | int port = config.GetInt("port", 9042); 29 | IPEndPoint[] contactPoints = ParseContactPoints(config.GetStringList("contact-points"), port); 30 | Builder.AddContactPoints(contactPoints); 31 | 32 | // Support user/pass authentication 33 | if (config.HasPath("credentials")) 34 | Builder.WithCredentials(config.GetString("credentials.username"), config.GetString("credentials.password")); 35 | 36 | // Support SSL 37 | if (config.GetBoolean("ssl")) 38 | Builder.WithSSL(); 39 | 40 | // Support compression 41 | string compressionTypeConfig = config.GetString("compression"); 42 | if (compressionTypeConfig != null) 43 | { 44 | var compressionType = (CompressionType) Enum.Parse(typeof (CompressionType), compressionTypeConfig, true); 45 | Builder.WithCompression(compressionType); 46 | } 47 | } 48 | 49 | private static IPEndPoint[] ParseContactPoints(IList contactPoints, int port) 50 | { 51 | if (contactPoints == null || contactPoints.Count == 0) 52 | throw new ConfigurationException("List of contact points cannot be empty."); 53 | 54 | return contactPoints.Select(cp => 55 | { 56 | string[] ipAndPort = cp.Split(':'); 57 | if (ipAndPort.Length == 1) 58 | return new IPEndPoint(IPAddress.Parse(ipAndPort[0]), port); 59 | 60 | if (ipAndPort.Length == 2) 61 | return new IPEndPoint(IPAddress.Parse(ipAndPort[0]), int.Parse(ipAndPort[1])); 62 | 63 | throw new ConfigurationException(string.Format("Contact points should have format [host:post] or [host] but found: {0}", cp)); 64 | }).ToArray(); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Journal/JournalStatements.cs: -------------------------------------------------------------------------------- 1 | namespace Akka.Persistence.Cassandra.Journal 2 | { 3 | /// 4 | /// CQL strings for use with the CassandraJournal. 5 | /// 6 | internal static class JournalStatements 7 | { 8 | public const string CreateKeyspace = @" 9 | CREATE KEYSPACE IF NOT EXISTS {0} 10 | WITH {1}"; 11 | 12 | public const string CreateTable = @" 13 | CREATE TABLE IF NOT EXISTS {0} ( 14 | persistence_id text, 15 | partition_number bigint, 16 | marker text, 17 | sequence_number bigint, 18 | message blob, 19 | PRIMARY KEY ((persistence_id, partition_number), marker, sequence_number) 20 | ){1}{2}"; 21 | 22 | public const string WriteMessage = @" 23 | INSERT INTO {0} (persistence_id, partition_number, marker, sequence_number, message) 24 | VALUES (?, ?, 'A', ?, ?)"; 25 | 26 | public const string WriteHeader = @" 27 | INSERT INTO {0} (persistence_id, partition_number, marker, sequence_number) 28 | VALUES (?, ?, 'H', ?)"; 29 | 30 | public const string SelectMessages = @" 31 | SELECT message FROM {0} WHERE persistence_id = ? AND partition_number = ? 32 | AND marker = 'A' AND sequence_number >= ? AND sequence_number <= ?"; 33 | 34 | public const string WriteDeleteMarker = @" 35 | INSERT INTO {0} (persistence_id, partition_number, marker, sequence_number) 36 | VALUES (?, ?, 'D', ?)"; 37 | 38 | public const string DeleteMessagePermanent = @" 39 | DELETE FROM {0} WHERE persistence_id = ? AND partition_number = ? 40 | AND marker = 'A' AND sequence_number = ?"; 41 | 42 | public const string SelectDeletedToSequence = @" 43 | SELECT sequence_number FROM {0} WHERE persistence_id = ? AND partition_number = ? 44 | AND marker = 'D' ORDER BY marker DESC, sequence_number DESC LIMIT 1"; 45 | 46 | public const string SelectLastMessageSequence = @" 47 | SELECT sequence_number FROM {0} WHERE persistence_id = ? AND partition_number = ? 48 | AND marker = 'A' AND sequence_number >= ? 49 | ORDER BY marker DESC, sequence_number DESC LIMIT 1"; 50 | 51 | public const string SelectHeaderSequence = @" 52 | SELECT sequence_number FROM {0} WHERE persistence_id = ? AND partition_number = ? 53 | AND marker = 'H'"; 54 | 55 | public const string SelectConfigurationValue = @" 56 | SELECT message FROM {0} 57 | WHERE persistence_id = 'akkanet-configuration-values' AND partition_number = 0 AND marker = ?"; 58 | 59 | public const string WriteConfigurationValue = @" 60 | INSERT INTO {0} (persistence_id, partition_number, marker, sequence_number, message) 61 | VALUES ('akkanet-configuration-values', 0, ?, 0, ?)"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/CassandraSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Configuration; 3 | using Cassandra; 4 | 5 | namespace Akka.Persistence.Cassandra 6 | { 7 | /// 8 | /// Abstract class for parsing common settings used by both the Journal and Snapshot store from HOCON configuration. 9 | /// 10 | public abstract class CassandraSettings 11 | { 12 | /// 13 | /// The name (key) of the session to use when resolving an ISession instance. When using default session management, 14 | /// this points at configuration under the "cassandra-sessions" section where the session's configuration is found. 15 | /// 16 | public string SessionKey { get; private set; } 17 | 18 | /// 19 | /// The keyspace to be created/used. 20 | /// 21 | public string Keyspace { get; private set; } 22 | 23 | /// 24 | /// A string to be appended to the CREATE KEYSPACE statement after the WITH clause when the keyspace is 25 | /// automatically created. Use this to define options like replication strategy. 26 | /// 27 | public string KeyspaceCreationOptions { get; private set; } 28 | 29 | /// 30 | /// When true the plugin will automatically try to create the keyspace if it doesn't already exist on start. 31 | /// 32 | public bool KeyspaceAutocreate { get; private set; } 33 | 34 | /// 35 | /// Name of the table to be created/used. 36 | /// 37 | public string Table { get; private set; } 38 | 39 | /// 40 | /// A string to be appended to the CREATE TABLE statement after the WITH clause. Use this to define things 41 | /// like gc_grace_seconds or one of the many other table options. 42 | /// 43 | public string TableCreationProperties { get; private set; } 44 | 45 | /// 46 | /// Consistency level for reads. 47 | /// 48 | public ConsistencyLevel ReadConsistency { get; private set; } 49 | 50 | /// 51 | /// Consistency level for writes. 52 | /// 53 | public ConsistencyLevel WriteConsistency { get; private set; } 54 | 55 | protected CassandraSettings(Config config) 56 | { 57 | SessionKey = config.GetString("session-key"); 58 | 59 | Keyspace = config.GetString("keyspace"); 60 | KeyspaceCreationOptions = config.GetString("keyspace-creation-options"); 61 | KeyspaceAutocreate = config.GetBoolean("keyspace-autocreate"); 62 | 63 | Table = config.GetString("table"); 64 | TableCreationProperties = config.GetString("table-creation-properties"); 65 | 66 | // Quote keyspace and table if necessary 67 | if (config.GetBoolean("use-quoted-identifiers")) 68 | { 69 | Keyspace = string.Format("\"{0}\"", Keyspace); 70 | Table = string.Format("\"{0}\"", Keyspace); 71 | } 72 | 73 | ReadConsistency = (ConsistencyLevel) Enum.Parse(typeof(ConsistencyLevel), config.GetString("read-consistency"), true); 74 | WriteConsistency = (ConsistencyLevel) Enum.Parse(typeof(ConsistencyLevel), config.GetString("write-consistency"), true); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/reference.conf: -------------------------------------------------------------------------------- 1 | cassandra-sessions { 2 | 3 | # The "default" Cassandra session, used by both the journal and snapshot store if not changed in 4 | # the cassandra-journal and cassandra-snapshot-store configuration sections below 5 | default { 6 | 7 | # Comma-seperated list of contact points in the cluster in the format of either [host] or [host:port] 8 | contact-points = [ "127.0.0.1" ] 9 | 10 | # Default port for contact points in the cluster, used if a contact point is not in [host:port] format 11 | port = 9042 12 | } 13 | } 14 | 15 | cassandra-journal { 16 | 17 | # Type name of the cassandra journal plugin 18 | class = "Akka.Persistence.Cassandra.Journal.CassandraJournal, Akka.Persistence.Cassandra" 19 | 20 | # The name (key) of the session to use when resolving an ISession instance. When using default session management, 21 | # this points at configuration under the "cassandra-sessions" section where the session's configuration is found. 22 | session-key = "default" 23 | 24 | # Whether or not to quote table and keyspace names when executing statements against Cassandra 25 | use-quoted-identifiers = false 26 | 27 | # The keyspace to be created/used by the journal 28 | keyspace = "akkanet" 29 | 30 | # A string to be appended to the CREATE KEYSPACE statement after the WITH clause when the keyspace is 31 | # automatically created. Use this to define options like replication strategy. 32 | keyspace-creation-options = "REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }" 33 | 34 | # When true the journal will automatically try to create the keyspace if it doesn't already exist on start 35 | keyspace-autocreate = true 36 | 37 | # Name of the table to be created/used by the journal 38 | table = "messages" 39 | 40 | # A string to be appended to the CREATE TABLE statement after the WITH clause. Use this to define things 41 | # like gc_grace_seconds or one of the many other table options. 42 | table-creation-properties = "" 43 | 44 | # The approximate number of rows per partition to use. Cannot be changed after table creation. 45 | partition-size = 5000000 46 | 47 | # The maximum number of messages to retrieve in one request when replaying messages 48 | max-result-size = 50001 49 | 50 | # Consistency level for reads 51 | read-consistency = "Quorum" 52 | 53 | # Consistency level for writes 54 | write-consistency = "Quorum" 55 | } 56 | 57 | cassandra-snapshot-store { 58 | 59 | # Type name of the cassandra snapshot store plugin 60 | class = "Akka.Persistence.Cassandra.Snapshot.CassandraSnapshotStore, Akka.Persistence.Cassandra" 61 | 62 | # The name (key) of the session to use when resolving an ISession instance. When using default session management, 63 | # this points at configuration under the "cassandra-sessions" section where the session's configuration is found. 64 | session-key = "default" 65 | 66 | # Whether or not to quote table and keyspace names when executing statements against Cassandra 67 | use-quoted-identifiers = false 68 | 69 | # The keyspace to be created/used by the snapshot store 70 | keyspace = "akkanet" 71 | 72 | # A string to be appended to the CREATE KEYSPACE statement after the WITH clause when the keyspace is 73 | # automatically created. Use this to define options like replication strategy. 74 | keyspace-creation-options = "REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }" 75 | 76 | # When true the journal will automatically try to create the keyspace if it doesn't already exist on start 77 | keyspace-autocreate = true 78 | 79 | # Name of the table to be created/used by the snapshot store 80 | table = "snapshots" 81 | 82 | # A string to be appended to the CREATE TABLE statement after the WITH clause. Use this to define things 83 | # like gc_grace_seconds or one of the many other table options. 84 | table-creation-properties = "" 85 | 86 | # The maximum number of snapshot metadata instances to retrieve in a single request when trying to find a 87 | # snapshot that matches the criteria 88 | max-metadata-result-size = 10 89 | 90 | # Consistency level for reads 91 | read-consistency = "One" 92 | 93 | # Consistency level for writes 94 | write-consistency = "One" 95 | } -------------------------------------------------------------------------------- /wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | # https://github.com/vishnubob/wait-for-it 4 | 5 | cmdname=$(basename $0) 6 | 7 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 8 | 9 | usage() 10 | { 11 | cat << USAGE >&2 12 | Usage: 13 | $cmdname host:port [-s] [-t timeout] [-- command args] 14 | -h HOST | --host=HOST Host or IP under test 15 | -p PORT | --port=PORT TCP port under test 16 | Alternatively, you specify the host and port as host:port 17 | -s | --strict Only execute subcommand if the test succeeds 18 | -q | --quiet Don't output any status messages 19 | -t TIMEOUT | --timeout=TIMEOUT 20 | Timeout in seconds, zero for no timeout 21 | -- COMMAND ARGS Execute command with args after the test finishes 22 | USAGE 23 | exit 1 24 | } 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 36 | result=$? 37 | if [[ $result -eq 0 ]]; then 38 | end_ts=$(date +%s) 39 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 40 | break 41 | fi 42 | sleep 1 43 | done 44 | return $result 45 | } 46 | wait_for_wrapper() 47 | { 48 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 49 | if [[ $QUIET -eq 1 ]]; then 50 | timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 51 | else 52 | timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 53 | fi 54 | PID=$! 55 | trap "kill -INT -$PID" INT 56 | wait $PID 57 | RESULT=$? 58 | if [[ $RESULT -ne 0 ]]; then 59 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 60 | fi 61 | return $RESULT 62 | } 63 | # process arguments 64 | while [[ $# -gt 0 ]] 65 | do 66 | case "$1" in 67 | *:* ) 68 | hostport=(${1//:/ }) 69 | HOST=${hostport[0]} 70 | PORT=${hostport[1]} 71 | shift 1 72 | ;; 73 | --child) 74 | CHILD=1 75 | shift 1 76 | ;; 77 | -q | --quiet) 78 | QUIET=1 79 | shift 1 80 | ;; 81 | -s | --strict) 82 | STRICT=1 83 | shift 1 84 | ;; 85 | -h) 86 | HOST="$2" 87 | if [[ $HOST == "" ]]; then break; fi 88 | shift 2 89 | ;; 90 | --host=*) 91 | HOST="${1#*=}" 92 | shift 1 93 | ;; 94 | -p) 95 | PORT="$2" 96 | if [[ $PORT == "" ]]; then break; fi 97 | shift 2 98 | ;; 99 | --port=*) 100 | PORT="${1#*=}" 101 | shift 1 102 | ;; 103 | -t) 104 | TIMEOUT="$2" 105 | if [[ $TIMEOUT == "" ]]; then break; fi 106 | shift 2 107 | ;; 108 | --timeout=*) 109 | TIMEOUT="${1#*=}" 110 | shift 1 111 | ;; 112 | --) 113 | shift 114 | CLI="$@" 115 | break 116 | ;; 117 | --help) 118 | usage 119 | ;; 120 | *) 121 | echoerr "Unknown argument: $1" 122 | usage 123 | ;; 124 | esac 125 | done 126 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 127 | echoerr "Error: you need to provide a host and port to test." 128 | usage 129 | fi 130 | TIMEOUT=${TIMEOUT:-15} 131 | STRICT=${STRICT:-0} 132 | CHILD=${CHILD:-0} 133 | QUIET=${QUIET:-0} 134 | if [[ $CHILD -gt 0 ]]; then 135 | wait_for 136 | RESULT=$? 137 | exit $RESULT 138 | else 139 | if [[ $TIMEOUT -gt 0 ]]; then 140 | wait_for_wrapper 141 | RESULT=$? 142 | else 143 | wait_for 144 | RESULT=$? 145 | fi 146 | fi 147 | if [[ $CLI != "" ]]; then 148 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 149 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 150 | exit $RESULT 151 | fi 152 | exec $CLI 153 | else 154 | exit $RESULT 155 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Akka 2 | 3 | # Fake directories 4 | src/.build/** 5 | 6 | 7 | 8 | #GitExtensions 9 | us.stackdump 10 | 11 | #KDiff3 and other git merge tools 12 | *.orig 13 | 14 | #------------------------------------------------------------------------------- 15 | #Based on https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 16 | 17 | ## Ignore Visual Studio temporary files, build results, and 18 | ## files generated by popular Visual Studio add-ons. 19 | 20 | # User-specific files 21 | *.suo 22 | *.user 23 | *.sln.docstates 24 | 25 | #MonoDevelop 26 | *.userprefs 27 | 28 | # Build results 29 | [Dd]ebug/ 30 | [Dd]ebugPublic/ 31 | [Rr]elease/ 32 | [Rr]eleases/ 33 | x64/ 34 | x86/ 35 | build/ 36 | bld/ 37 | [Bb]in/ 38 | [Oo]bj/ 39 | 40 | # Roslyn cache directories 41 | *.ide/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | #NUNIT 48 | *.VisualState.xml 49 | TestResult.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding addin-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment out the next line if you want to keep your passwords hidden 148 | *.pubxml 149 | 150 | # NuGet Packages 151 | *.nupkg 152 | # The packages folder can be ignored because of Package Restore 153 | **/packages/* 154 | # except build/, which is used as an MSBuild target. 155 | !**/packages/build/ 156 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 157 | !**/packages/repositories.config 158 | 159 | # NuGet.exe 160 | /src/.nuget/[Nn]u[Gg]et.exe 161 | 162 | # Windows Azure Build Output 163 | csx/ 164 | *.build.csdef 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | 169 | # Others 170 | sql/ 171 | *.Cache 172 | ClientBin/ 173 | [Ss]tyle[Cc]op.* 174 | ~$* 175 | *~ 176 | *.dbmdl 177 | *.dbproj.schemaview 178 | *.pfx 179 | *.publishsettings 180 | node_modules/ 181 | 182 | # RIA/Silverlight projects 183 | Generated_Code/ 184 | 185 | # Backup & report files from converting an old project file 186 | # to a newer Visual Studio version. Backup files are not needed, 187 | # because we have git ;-) 188 | _UpgradeReport_Files/ 189 | Backup*/ 190 | UpgradeLog*.XML 191 | UpgradeLog*.htm 192 | 193 | # SQL Server files 194 | *.mdf 195 | *.ldf 196 | 197 | # make exception for Akka.Persistence.SqlServer database file 198 | !AkkaPersistenceSqlServerSpecDb.mdf 199 | !AkkaPersistenceSqlServerSpecDb_log.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | /src/.Akka.boltdata/NCover/Executions/0.jf 209 | /src/.Akka.boltdata/NCover/Executions/ProjectId/0.jf 210 | /src/.Akka.boltdata/NCover/Executions/ProjectOrderIndex/0.jf 211 | /src/.Akka.boltdata/NCover/Projects/0.jf 212 | /src/.Akka.boltdata/NCover/Projects/Name/0.jf 213 | /src/.Akka.boltdata/Settings.json 214 | /src/.Akka.boltdata/TestResults.json 215 | resetdev.bat 216 | /src/packages/repositories.config 217 | 218 | # FAKE build folder 219 | .fake/ 220 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Akka.Persistence.Cassandra.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {54BD0B45-8A46-4194-8C33-AD287CAC8FA4} 8 | Library 9 | Properties 10 | Akka.Persistence.Cassandra 11 | Akka.Persistence.Cassandra 12 | v4.5 13 | 512 14 | ..\..\..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\packages\Akka.1.0.6\lib\net45\Akka.dll 37 | True 38 | 39 | 40 | ..\packages\Akka.Persistence.1.0.6.17-beta\lib\net45\Akka.Persistence.dll 41 | True 42 | 43 | 44 | ..\packages\CassandraCSharpDriver.2.7.3\lib\net40\Cassandra.dll 45 | True 46 | 47 | 48 | ..\packages\Google.ProtocolBuffers.2.4.1.555\lib\net40\Google.ProtocolBuffers.dll 49 | True 50 | 51 | 52 | ..\packages\Google.ProtocolBuffers.2.4.1.555\lib\net40\Google.ProtocolBuffers.Serialization.dll 53 | True 54 | 55 | 56 | ..\packages\lz4net.1.0.5.93\lib\net40-client\LZ4.dll 57 | True 58 | 59 | 60 | ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll 61 | True 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Properties\SharedAssemblyInfo.cs 74 | 75 | 76 | Code 77 | 78 | 79 | Code 80 | 81 | 82 | 83 | 84 | Code 85 | 86 | 87 | 88 | Code 89 | 90 | 91 | 92 | Code 93 | 94 | 95 | Code 96 | 97 | 98 | 99 | 100 | Code 101 | 102 | 103 | Code 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 117 | 118 | 119 | 120 | 127 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 27 | 28 | 29 | 30 | 31 | $(SolutionDir).nuget 32 | 33 | 34 | 35 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 36 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 37 | 38 | 39 | 40 | $(MSBuildProjectDirectory)\packages.config 41 | $(PackagesProjectConfig) 42 | 43 | 44 | 45 | 46 | $(NuGetToolsPath)\NuGet.exe 47 | @(PackageSource) 48 | 49 | "$(NuGetExePath)" 50 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 51 | 52 | $(TargetDir.Trim('\\')) 53 | 54 | -RequireConsent 55 | -NonInteractive 56 | 57 | "$(SolutionDir) " 58 | "$(SolutionDir)" 59 | 60 | 61 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 62 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 63 | 64 | 65 | 66 | RestorePackages; 67 | $(BuildDependsOn); 68 | 69 | 70 | 71 | 72 | $(BuildDependsOn); 73 | BuildPackage; 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Akka.NET 2 | Akka.NET is a large project and contributions are more than welcome, so thank you for wanting to contribute to Akka.NET! 3 | 4 | --- 5 | 6 | ### Checklist before creating a Pull Request 7 | Submit only relevant commits. We don't mind many commits in a pull request, but they must be relevant as explained below. 8 | 9 | - __Use a feature branch__ The pull request should be created from a feature branch, and not from _dev_. See below for why. 10 | - __No merge-commits__ 11 | If you have commits that looks like this _"Merge branch 'my-branch' into dev"_ or _"Merge branch 'dev' of github .com/akkadotnet/akka.net into dev"_ you're probaly using merge instead of [rebase](https://help.github.com/articles/about-git-rebase) locally. See below on _Handling updates from upstream_. 12 | - __Squash commits__ Often we create temporary commits like _"Started implementing feature x"_ and then _"Did a bit more on feature x"_. Squash these commits together using [interactive rebase](https://help.github.com/articles/about-git-rebase). Also see [Squashing commits with rebase](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html). 13 | - __Descriptive commit messages__ If a commit's message isn't descriptive, change it using [interactive rebase](https://help.github.com/articles/about-git-rebase). Refer to issues using `#issue`. Example of a bad message ~~"Small cleanup"~~. Example of good message: _"Removed Security.Claims header from FSM, which broke Mono build per #62"_. Don't be afraid to write long messages, if needed. Try to explain _why_ you've done the changes. The Erlang repo has some info on [writing good commit messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages). 14 | - __No one-commit-to-rule-them-all__ Large commits that changes too many things at the same time are very hard to review. Split large commits into smaller. See this [StackOverflow question](http://stackoverflow.com/questions/6217156/break-a-previous-commit-into-multiple-commits) for information on how to do this. 15 | - __Tests__ Add relevant tests and make sure all existing ones still passes. Tests can be run using the command 16 | - __No Warnings__ Make sure your code do not produce any build warnings. 17 | 18 | After reviewing a Pull request, we might ask you to fix some commits. After you've done that you need to force push to update your branch in your local fork. 19 | 20 | ####Title and Description for the Pull Request#### 21 | Give the PR a descriptive title and in the description field describe what you have done in general terms and why. This will help the reviewers greatly, and provide a history for the future. 22 | 23 | Especially if you modify something existing, be very clear! Have you changed any algorithms, or did you just intend to reorder the code? Justify why the changes are needed. 24 | 25 | 26 | --- 27 | 28 | ### Getting started 29 | Make sure you have a [GitHub](https://github.com/) account. 30 | 31 | - Fork, clone, add upstream to the Akka.NET repository. See [Fork a repo](https://help.github.com/articles/fork-a-repo) for more detailed instructions or follow the instructions below. 32 | 33 | - Fork by clicking _Fork_ on https://github.com/akkadotnet/akka.net 34 | - Clone your fork locally. 35 | ``` 36 | git clone https://github.com/YOUR-USERNAME/akka.net 37 | ``` 38 | - Add an upstream remote. 39 | ``` 40 | git remote add upstream https://github.com/akkadotnet/akka.net 41 | ``` 42 | You now have two remotes: _upstream_ points to https://github.com/akkadotnet/akka.net, and _origin_ points to your fork on GitHub. 43 | 44 | - Make changes. See below. 45 | 46 | Unsure where to start? Issues marked with [_up for grabs_](https://github.com/akkadotnet/akka.net/labels/up%20for%20grabs) are things we want help with. 47 | 48 | See also: [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) 49 | 50 | New to Git? See https://help.github.com/articles/what-are-other-good-resources-for-learning-git-and-github 51 | 52 | ### Making changes 53 | __Never__ work directly on _dev_ or _master_ and you should never send a pull request from master - always from a feature branch created by you. 54 | 55 | - Pick an [issue](https://github.com/akkadotnet/akka.net/issues). If no issue exists (search first) create one. 56 | - Get any changes from _upstream_. 57 | ``` 58 | git checkout dev 59 | git fetch upstream 60 | git merge --ff-only upstream/dev 61 | git push origin dev #(optional) this makes sure dev in your own fork on GitHub is up to date 62 | ``` 63 | 64 | See https://help.github.com/articles/fetching-a-remote for more info 65 | 66 | - Create a new feature branch. It's important that you do your work on your own branch and that it's created off of _dev_. Tip: Give it a descriptive name and include the issue number, e.g. `implement-testkits-eventfilter-323` or `295-implement-tailchopping-router`, so that others can see what is being worked on. 67 | ``` 68 | git checkout -b my-new-branch-123 69 | ``` 70 | - Work on your feature. Commit. 71 | - Rebase often, see below. 72 | - Make sure you adhere to _Checklist before creating a Pull Request_ described above. 73 | - Push the branch to your fork on GitHub 74 | ``` 75 | git push origin my-new-branch-123 76 | ``` 77 | - Send a Pull Request, see https://help.github.com/articles/using-pull-requests to the _dev_ branch. 78 | 79 | See also: [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/) (we're using `dev` as our master branch) 80 | 81 | ### Handling updates from upstream 82 | 83 | While you're working away in your branch it's quite possible that your upstream _dev_ may be updated. If this happens you should: 84 | 85 | - [Stash](http://git-scm.com/book/en/Git-Tools-Stashing) any un-committed changes you need to 86 | ``` 87 | git stash 88 | ``` 89 | - Update your local _dev_ by fetching from _upstream_ 90 | ``` 91 | git checkout dev 92 | git fetch upstream 93 | git merge --ff-only upstream/dev 94 | ``` 95 | - Rebase your feature branch on _dev_. See [Git Branching - Rebasing](http://git-scm.com/book/en/Git-Branching-Rebasing) for more info on rebasing 96 | ``` 97 | git checkout my-new-branch-123 98 | git rebase dev 99 | git push origin dev #(optional) this makes sure dev in your own fork on GitHub is up to date 100 | ``` 101 | This ensures that your history is "clean" i.e. you have one branch off from _dev_ followed by your changes in a straight line. Failing to do this ends up with several "messy" merges in your history, which we don't want. This is the reason why you should always work in a branch and you should never be working in, or sending pull requests from _dev_. 102 | 103 | If you're working on a long running feature then you may want to do this quite often, rather than run the risk of potential merge issues further down the line. 104 | 105 | ### Making changes to a Pull request 106 | If you realize you've missed something after submitting a Pull request, just commit to your local branch and push the branch just like you did the first time. This commit will automatically be included in the Pull request. 107 | If we ask you to change already published commits using interactive rebase (like squashing or splitting commits or rewriting commit messages) you need to force push using `-f`: 108 | ``` 109 | git push -f origin my-new-branch-123 110 | ``` 111 | 112 | ### All my commits are on dev. How do I get them to a new branch? ### 113 | If all commits are on _dev_ you need to move them to a new feature branch. 114 | 115 | You can rebase your local _dev_ on _upstream/dev_ (to remove any merge commits), rename it, and recreate _dev_ 116 | ``` 117 | git checkout dev 118 | git rebase upstream/dev 119 | git branch -m my-new-branch-123 120 | git branch dev upstream/dev 121 | ``` 122 | Or you can create a new branch off of _dev_ and then cherry pick the commits 123 | ``` 124 | git checkout -b my-new-branch-123 upstream/dev 125 | git cherry-pick rev #rev is the revisions you want to pick 126 | git cherry-pick rev #repeat until you have picked all commits 127 | git branch -m dev old-dev #rename dev 128 | git branch dev upstream/dev #create a new dev 129 | ``` 130 | 131 | ## Code guidelines 132 | 133 | See [Contributor Guidelines](http://akkadotnet.github.io/wiki/Contributor%20guidelines) on the wiki. 134 | 135 | --- 136 | Props to [NancyFX](https://github.com/NancyFx/Nancy) from which we've "borrowed" some of this text. 137 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/Akka.Persistence.Cassandra.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Debug 8 | AnyCPU 9 | {1FE6CA3D-4996-4A2A-AC0F-76F3BD66B4C4} 10 | Library 11 | Properties 12 | Akka.Persistence.Cassandra.Tests 13 | Akka.Persistence.Cassandra.Tests 14 | v4.5 15 | 512 16 | ..\..\..\ 17 | true 18 | 19 | 20 | 21 | 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | 30 | 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | ..\packages\Akka.1.0.6\lib\net45\Akka.dll 41 | True 42 | 43 | 44 | ..\packages\Akka.Persistence.1.0.6.17-beta\lib\net45\Akka.Persistence.dll 45 | True 46 | 47 | 48 | ..\packages\Akka.Persistence.TestKit.1.0.6.17-beta\lib\net45\Akka.Persistence.TestKit.dll 49 | True 50 | 51 | 52 | ..\packages\Akka.TestKit.1.0.6\lib\net45\Akka.TestKit.dll 53 | True 54 | 55 | 56 | ..\packages\Akka.TestKit.Xunit2.1.0.6\lib\net45\Akka.TestKit.Xunit2.dll 57 | True 58 | 59 | 60 | ..\packages\CassandraCSharpDriver.2.7.3\lib\net40\Cassandra.dll 61 | True 62 | 63 | 64 | ..\packages\Google.ProtocolBuffers.2.4.1.555\lib\net40\Google.ProtocolBuffers.dll 65 | True 66 | 67 | 68 | ..\packages\Google.ProtocolBuffers.2.4.1.555\lib\net40\Google.ProtocolBuffers.Serialization.dll 69 | True 70 | 71 | 72 | ..\packages\lz4net.1.0.5.93\lib\net40-client\LZ4.dll 73 | True 74 | 75 | 76 | ..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll 77 | True 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 89 | True 90 | 91 | 92 | ..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll 93 | True 94 | 95 | 96 | ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll 97 | True 98 | 99 | 100 | 101 | 102 | Properties\SharedAssemblyInfo.cs 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {54bd0b45-8a46-4194-8c33-ad287cac8fa4} 113 | Akka.Persistence.Cassandra 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 128 | 129 | 130 | 131 | 138 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Snapshot/CassandraSnapshotStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Akka.Event; 6 | using Akka.Persistence.Snapshot; 7 | using Akka.Serialization; 8 | using Cassandra; 9 | 10 | namespace Akka.Persistence.Cassandra.Snapshot 11 | { 12 | /// 13 | /// A SnapshotStore implementation for writing snapshots to Cassandra. 14 | /// 15 | public class CassandraSnapshotStore : SnapshotStore 16 | { 17 | private static readonly Type SnapshotType = typeof (Serialization.Snapshot); 18 | 19 | private readonly CassandraExtension _cassandraExtension; 20 | private readonly Serializer _serializer; 21 | private readonly ILoggingAdapter _log; 22 | private readonly bool _publish; 23 | 24 | private ISession _session; 25 | private PreparedStatement _writeSnapshot; 26 | private PreparedStatement _deleteSnapshot; 27 | private PreparedStatement _selectSnapshot; 28 | private PreparedStatement _selectSnapshotMetadata; 29 | 30 | public CassandraSnapshotStore() 31 | { 32 | _cassandraExtension = CassandraPersistence.Instance.Apply(Context.System); 33 | _serializer = Context.System.Serialization.FindSerializerForType(SnapshotType); 34 | _log = Context.System.Log; 35 | 36 | // Here so we can emulate the base class behavior but do deletes async 37 | PersistenceExtension persistence = Context.System.PersistenceExtension(); 38 | _publish = persistence.Settings.Internal.PublishPluginCommands; 39 | } 40 | 41 | protected override void PreStart() 42 | { 43 | base.PreStart(); 44 | 45 | // Get a session to talk to Cassandra with 46 | CassandraSnapshotStoreSettings settings = _cassandraExtension.SnapshotStoreSettings; 47 | _session = _cassandraExtension.SessionManager.ResolveSession(settings.SessionKey); 48 | 49 | // Create the keyspace if necessary and always attempt to create the table 50 | if (settings.KeyspaceAutocreate) 51 | _session.Execute(string.Format(SnapshotStoreStatements.CreateKeyspace, settings.Keyspace, settings.KeyspaceCreationOptions)); 52 | 53 | var fullyQualifiedTableName = string.Format("{0}.{1}", settings.Keyspace, settings.Table); 54 | var createTable = string.IsNullOrWhiteSpace(settings.TableCreationProperties) 55 | ? string.Format(SnapshotStoreStatements.CreateTable, fullyQualifiedTableName, string.Empty, string.Empty) 56 | : string.Format(SnapshotStoreStatements.CreateTable, fullyQualifiedTableName, " AND ", 57 | settings.TableCreationProperties); 58 | 59 | _session.Execute(createTable); 60 | 61 | // Prepare some statements 62 | _writeSnapshot = _session.PrepareFormat(SnapshotStoreStatements.WriteSnapshot, fullyQualifiedTableName); 63 | _deleteSnapshot = _session.PrepareFormat(SnapshotStoreStatements.DeleteSnapshot, fullyQualifiedTableName); 64 | _selectSnapshot = _session.PrepareFormat(SnapshotStoreStatements.SelectSnapshot, fullyQualifiedTableName); 65 | _selectSnapshotMetadata = _session.PrepareFormat(SnapshotStoreStatements.SelectSnapshotMetadata, fullyQualifiedTableName); 66 | } 67 | 68 | protected override bool Receive(object message) 69 | { 70 | // Make deletes async as well, but make sure we still publish like the base class does 71 | if (message is DeleteSnapshot) 72 | { 73 | HandleDeleteAsync((DeleteSnapshot) message, msg => DeleteAsync(msg.Metadata)); 74 | } 75 | else if (message is DeleteSnapshots) 76 | { 77 | HandleDeleteAsync((DeleteSnapshots) message, msg => DeleteAsync(msg.PersistenceId, msg.Criteria)); 78 | } 79 | else 80 | { 81 | return base.Receive(message); 82 | } 83 | 84 | return true; 85 | } 86 | 87 | protected override async Task LoadAsync(string persistenceId, SnapshotSelectionCriteria criteria) 88 | { 89 | bool hasNextPage = true; 90 | byte[] nextPageState = null; 91 | 92 | while (hasNextPage) 93 | { 94 | // Get a page of metadata that match the criteria 95 | IStatement getMetadata = _selectSnapshotMetadata.Bind(persistenceId, criteria.MaxSequenceNr) 96 | .SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.ReadConsistency) 97 | .SetPageSize(_cassandraExtension.SnapshotStoreSettings.MaxMetadataResultSize) 98 | .SetPagingState(nextPageState) 99 | .SetAutoPage(false); 100 | RowSet metadataRows = await _session.ExecuteAsync(getMetadata).ConfigureAwait(false); 101 | 102 | nextPageState = metadataRows.PagingState; 103 | hasNextPage = nextPageState != null; 104 | IEnumerable page = metadataRows.Select(MapRowToSnapshotMetadata) 105 | .Where(md => md.Timestamp <= criteria.MaxTimeStamp); 106 | 107 | // Try to get the first available snapshot from the page 108 | foreach (SnapshotMetadata md in page) 109 | { 110 | try 111 | { 112 | IStatement getSnapshot = _selectSnapshot.Bind(md.PersistenceId, md.SequenceNr) 113 | .SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.ReadConsistency); 114 | RowSet snapshotRows = await _session.ExecuteAsync(getSnapshot).ConfigureAwait(false); 115 | 116 | // If we didn't get a snapshot for some reason, just try the next one 117 | Row snapshotRow = snapshotRows.SingleOrDefault(); 118 | if (snapshotRow == null) 119 | continue; 120 | 121 | // We found a snapshot so create the necessary class and return the result 122 | return new SelectedSnapshot(md, Deserialize(snapshotRow.GetValue("snapshot"))); 123 | } 124 | catch (Exception e) 125 | { 126 | // If there is a problem, just try the next snapshot 127 | _log.Warning("Unexpected exception while retrieveing snapshot {0} for id {1}: {2}", md.SequenceNr, md.PersistenceId, e); 128 | } 129 | } 130 | 131 | // Just try the next page if available 132 | } 133 | 134 | // Out of snapshots that match or none found 135 | return null; 136 | } 137 | 138 | protected override Task SaveAsync(SnapshotMetadata metadata, object snapshot) 139 | { 140 | IStatement bound = _writeSnapshot.Bind(metadata.PersistenceId, metadata.SequenceNr, metadata.Timestamp.Ticks, Serialize(snapshot)) 141 | .SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.WriteConsistency); 142 | return _session.ExecuteAsync(bound); 143 | } 144 | 145 | protected override Task DeleteAsync(SnapshotMetadata metadata) 146 | { 147 | IStatement bound = _deleteSnapshot.Bind(metadata.PersistenceId, metadata.SequenceNr) 148 | .SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.WriteConsistency); 149 | return _session.ExecuteAsync(bound); 150 | } 151 | 152 | protected override async Task DeleteAsync(string persistenceId, SnapshotSelectionCriteria criteria) 153 | { 154 | // Use a batch to delete all matching snapshots 155 | var batch = new BatchStatement(); 156 | 157 | bool hasNextPage = true; 158 | byte[] nextPageState = null; 159 | 160 | while (hasNextPage) 161 | { 162 | // Get a page of metadata that match the criteria 163 | IStatement getMetadata = _selectSnapshotMetadata.Bind(persistenceId, criteria.MaxSequenceNr) 164 | .SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.ReadConsistency) 165 | .SetPageSize(_cassandraExtension.SnapshotStoreSettings.MaxMetadataResultSize) 166 | .SetPagingState(nextPageState) 167 | .SetAutoPage(false); 168 | RowSet metadataRows = await _session.ExecuteAsync(getMetadata).ConfigureAwait(false); 169 | 170 | nextPageState = metadataRows.PagingState; 171 | hasNextPage = nextPageState != null; 172 | IEnumerable page = metadataRows.Select(MapRowToSnapshotMetadata) 173 | .Where(md => md.Timestamp <= criteria.MaxTimeStamp); 174 | // Add any matching snapshots from the page to the batch 175 | foreach (SnapshotMetadata md in page) 176 | batch.Add(_deleteSnapshot.Bind(md.PersistenceId, md.SequenceNr)); 177 | 178 | // Go to next page if available 179 | } 180 | 181 | if (batch.IsEmpty) 182 | return; 183 | 184 | // Send the batch of deletes 185 | batch.SetConsistencyLevel(_cassandraExtension.SnapshotStoreSettings.WriteConsistency); 186 | await _session.ExecuteAsync(batch).ConfigureAwait(false); 187 | } 188 | 189 | protected override void Saved(SnapshotMetadata metadata) 190 | { 191 | // No op 192 | } 193 | 194 | protected override void PostStop() 195 | { 196 | base.PostStop(); 197 | 198 | if (_cassandraExtension != null && _session != null) 199 | { 200 | _cassandraExtension.SessionManager.ReleaseSession(_session); 201 | _session = null; 202 | } 203 | } 204 | 205 | private async Task HandleDeleteAsync(T message, Func handler) 206 | { 207 | try 208 | { 209 | // Capture event stream so we can use it after await 210 | EventStream es = Context.System.EventStream; 211 | 212 | // Delete async, then publish if necessary 213 | await handler(message).ConfigureAwait(false); 214 | if (_publish) 215 | es.Publish(message); 216 | } 217 | catch (Exception e) 218 | { 219 | _log.Error(e, "Unexpected error while deleting snapshot(s)."); 220 | } 221 | } 222 | 223 | private object Deserialize(byte[] bytes) 224 | { 225 | return ((Serialization.Snapshot) _serializer.FromBinary(bytes, SnapshotType)).Data; 226 | } 227 | 228 | private byte[] Serialize(object snapshotData) 229 | { 230 | return _serializer.ToBinary(new Serialization.Snapshot(snapshotData)); 231 | } 232 | 233 | private static SnapshotMetadata MapRowToSnapshotMetadata(Row row) 234 | { 235 | return new SnapshotMetadata(row.GetValue("persistence_id"), row.GetValue("sequence_number"), 236 | new DateTime(row.GetValue("timestamp_ticks"))); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Akka.Persistence.Cassandra 2 | ========================== 3 | A replicated journal and snapshot store implementation for Akka.Persistence backed by 4 | [Apache Cassandra](http://planetcassandra.org/). 5 | 6 | **WARNING: The Akka.Persistence.Cassandra plugin is still in beta and the mechanics described below are subject to 7 | change.** 8 | 9 | Quick Start 10 | ----------- 11 | To activate the journal plugin, add the following line to the actor system configuration file: 12 | ``` 13 | akka.persistence.journal.plugin = "cassandra-journal" 14 | ``` 15 | To activate the snapshot store plugin, add the following line to the actor system configuration file: 16 | ``` 17 | akka.persistence.snasphot-store.plugin = "cassandra-snapshot-store" 18 | ``` 19 | The default configuration will try to connect to a Cassandra cluster running on `127.0.0.1` for persisting messages 20 | and snapshots. More information on the available configuration options is in the sections below. 21 | 22 | Connecting to the Cluster 23 | ------------------------- 24 | Both the journal and the snapshot store plugins use the [DataStax .NET Driver](https://github.com/datastax/csharp-driver) 25 | for Cassandra to communicate with the cluster. The driver has an `ISession` object which is used to execute statements 26 | against the cluster (very similar to a `DbConnection` object in ADO.NET). You can control the creation and 27 | configuration of these session instance(s) by modifying the configuration under `cassandra-sessions`. Out of the 28 | box, both the journal and the snapshot store plugin will try to use a session called `default`. You can override 29 | the settings for that session with the following configuration keys: 30 | 31 | - `cassandra-sessions.default.contact-points`: A comma-seperated list of contact points in the cluster in the format 32 | of either `host` or `host:port`. Default value is *`[ "127.0.0.1" ]`*. 33 | - `cassandra-sessions.default.port`: Default port for contact points in the cluster, used if a contact point is not 34 | in [host:port] format. Default value is *`9042`*. 35 | - `cassandra-sessions.default.credentials.username`: The username to login to Cassandra hosts. No authentication is 36 | used by default. 37 | - `cassandra-sessions.default.credentials.password`: The password corresponding to the username. No authentication 38 | is used by default. 39 | - `cassandra-sessions.default.ssl`: Boolean value indicating whether to use SSL when connecting to the cluster. No 40 | default value is set and so SSL is not used by default. 41 | - `cassandra-sessions.default.compression`: The [type of compression](https://github.com/datastax/csharp-driver/blob/master/src/Cassandra/CompressionType.cs) 42 | to use when communicating with the cluster. No default value is set and so compression is not used by default. 43 | 44 | If you require more advanced configuration of the `ISession` object than the options provided here (for example, to 45 | use a different session for the journal and snapshot store plugins or to configure the session via code or manage 46 | it with an IoC container), see the [Advanced Session Management](#advanced-session-management) section below. 47 | 48 | Journal 49 | ------- 50 | ### Features 51 | - All operations of the journal plugin API are fully supported 52 | - Uses Cassandra in a log-oriented way (i.e. data is only ever inserted but never updated) 53 | - Uses marker records for permanent deletes to try and avoid the problem of [reading many tombstones](http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) 54 | when replaying messages. 55 | - Messages for a single persistence Id are partitioned across the cluster to avoid unbounded partition 56 | growth and support scalability by adding more nodes to the cluster. 57 | 58 | ### Configuration 59 | As mentioned in the Quick Start section, you can activate the journal plugin by adding the following line to your 60 | actor system configuration file: 61 | ``` 62 | akka.persistence.journal.plugin = "cassandra-journal" 63 | ``` 64 | You can also override the journal's default settings with the following configuration keys: 65 | - `cassandra-journal.class`: The Type name of the Cassandra journal plugin. Default value is *`Akka.Persistence.Cassandra.Journal.CassandraJournal, Akka.Persistence.Cassandra`*. 66 | - `cassandra-journal.session-key`: The name (key) of the session to use when resolving an `ISession` instance. When 67 | using default session management, this points at a configuration section under `cassandra-sessions` where the 68 | session's configuration is found. Default value is *`default`*. 69 | - `cassandra-journal.use-quoted-identifiers`: Whether or not to quote the table and keyspace names when executing 70 | statements against Cassandra. Default value is *`false`*. 71 | - `cassandra-journal.keyspace`: The keyspace to be created/used by the journal. Default value is *`akkanet`*. 72 | - `cassandra-journal.keyspace-creation-options`: A string to be appended to the `CREATE KEYSPACE` statement after 73 | the `WITH` clause when the keyspace is automatically created. Use this to define options like the replication 74 | strategy. Default value is *`REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }`*. 75 | - `cassandra-journal.keyspace-autocreate`: When true, the journal will automatically try to create the keyspace if 76 | it doesn't already exist on startup. Default value is *`true`*. 77 | - `cassandra-journal.table`: The name of the table to be created/used by the journal. Default value is *`messages`*. 78 | - `cassandra-journal.table-creation-properties`: A string to be appended to the `CREATE TABLE` statement after the 79 | `WITH` clause. Use this to define advanced table options like `gc_grace_seconds` or one of the other many table 80 | options. Default value is *an empty string*. 81 | - `cassandra-journal.partition-size`: The approximate number of message rows to store in a single partition. Cannot 82 | be changed after table creation. Default value is *`5000000`*. 83 | - `cassandra-journal.max-result-size`: The maximum number of messages to retrieve in a single request to Cassandra 84 | when replaying messages. Default value is *`50001`*. 85 | - `cassandra-journal.read-consistency`: The consistency level to use for read operations. Default value is *`Quorum`*. 86 | - `cassandra-journal.write-consistency`: The consistency level to use for write operations. Default value is 87 | *`Quorum`*. 88 | 89 | The default value for read and write consistency levels ensure that persistent actors can read their own writes. 90 | Consider using `LocalQuorum` for both reads and writes if using a Cassandra cluster with multiple datacenters. 91 | 92 | Snapshot Store 93 | -------------- 94 | ### Features 95 | - Snapshot IO is done in a fully asynchronous fashion, including deletes (the snapshot store plugin API only 96 | directly specifies synchronous methods for doing deletes) 97 | 98 | ### Configuration 99 | As mentioned in the Quick Start section, you can activate the snapshot store plugin by adding the following line 100 | to your actor system configuration file: 101 | ``` 102 | akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store" 103 | ``` 104 | You can also override the snapshot store's default settings with the following configuration keys: 105 | - `cassandra-snapshot-store.class`: The Type name of the Cassandra snapshot store plugin. Default value is 106 | *`Akka.Persistence.Cassandra.Snapshot.CassandraSnapshotStore, Akka.Persistence.Cassandra`*. 107 | - `cassandra-snapshot-store.session-key`: The name (key) of the session to use when resolving an `ISession` 108 | instance. When using default session management, this points at a configuration section under `cassandra-sessions` 109 | where the session's configuration is found. Default value is *`default`*. 110 | - `cassandra-snapshot-store.use-quoted-identifiers`: Whether or not to quote the table and keyspace names when 111 | executing statements against Cassandra. Default value is *`false`*. 112 | - `cassandra-snapshot-store.keyspace`: The keyspace to be created/used by the snapshot store. Default value is 113 | *`akkanet`*. 114 | - `cassandra-snapshot-store.keyspace-creation-options`: A string to be appended to the `CREATE KEYSPACE` statement 115 | after the `WITH` clause when the keyspace is automatically created. Use this to define options like the replication 116 | strategy. Default value is *`REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }`*. 117 | - `cassandra-snapshot-store.keyspace-autocreate`: When true, the snapshot store will automatically try to create 118 | the keyspace if it doesn't already exist on startup. Default value is *`true`*. 119 | - `cassandra-snapshot-store.table`: The name of the table to be created/used by the snapshot store. Default value 120 | is *`snapshots`*. 121 | - `cassandra-snapshot-store.table-creation-properties`: A string to be appended to the `CREATE TABLE` statement 122 | after the `WITH` clause. Use this to define advanced table options like `gc_grace_seconds` or one of the other 123 | many table options. Default value is *an empty string*. 124 | - `cassandra-snapshot-store.max-metadata-result-size`: The maximum number of snapshot metadata instances to 125 | retrieve in a single request when trying to find a snapshot that matches criteria. Default value is *`10`*. 126 | - `cassandra-snapshot-store.read-consistency`: The consistency level to use for read operations. Default value 127 | is *`One`*. 128 | - `cassandra-snapshot-store.write-consistency`: The consistency level to use for write operations. Default value 129 | is *`One`*. 130 | 131 | Consider using `LocalOne` consistency level for both reads and writes if using a Cassandra cluster with multiple 132 | datacenters. 133 | 134 | Advanced Session Management 135 | --------------------------- 136 | In some advanced scenarios, you may want to have more control over how `ISession` instances are created. Some 137 | example scenarios might include: 138 | - to use a different session instance for the journal and snapshot store plugins (i.e. maybe you have more than one 139 | Cassandra cluster and are storing journal messages and snapshots in different clusters) 140 | - to access more advanced configuration options for building the session instance in code using the DataStax 141 | driver's cluster builder API directly 142 | - to use session instances that have already been registered with an IoC container and are being managed there 143 | 144 | If you want more control over how session instances are created or managed, you have two options depending on how 145 | much control you need. 146 | 147 | ### Defining multiple session instances in the `cassandra-sessions` section 148 | It is possible to define configuration for more than one session instance under the `cassandra-sessions` section of 149 | your actor system's configuration file. To do this, just create your own section with a unique name/key for the 150 | sub-section. All of the same options listed above in the [Connecting to the Cluster](#connecting-to-the-cluster) 151 | can then be used to configure that session. For example, I might define seperate configurations for my journal and 152 | snapshot store plugins like this: 153 | ``` 154 | cassandra-sessions { 155 | my-journal-session { 156 | contact-points = [ "10.1.1.1", "10.1.1.2" ] 157 | port = 9042 158 | credentials { 159 | username = "myusername" 160 | password = "mypassword" 161 | } 162 | } 163 | 164 | my-snapshot-session { 165 | contact-points = [ "10.2.1.1:9142", "10.2.1.2:9142" ] 166 | } 167 | } 168 | ``` 169 | I can then tell the journal and snapshot store plugins to use those sessions by overriding each plugin's `session-key` 170 | configuration like this: 171 | ``` 172 | cassandra-journal.session-key = "my-journal-session" 173 | cassandra-snapshot-store.session-key = "my-snapshot-session" 174 | ``` 175 | 176 | ### Controlling session configuration and management with code 177 | You can also override how sessions are created, managed and resolved with your own code. Session management is 178 | done as its own plugin for Akka.NET and a default implementation that uses the `cassandra-sessions` section is 179 | provided out of the box. If you want to provide your own implementation for doing this (for example, to manage 180 | sessions with an IoC container or use the DataStax driver's cluster builder API to do more advanced configuration), 181 | here are the steps you'll need to follow: 182 | 183 | 1. Create a class that implements the `IManageSessions` interface from `Akka.Persistence.Cassandra.SessionManagement`. 184 | This interface is simple and just requires that you provide a way for resolving and releasing session instances. For 185 | example: 186 | 187 | ```cs 188 | public class MySessionManager : IManageSessions 189 | { 190 | public override ISession ResolveSession(string key) 191 | { 192 | // Do something here to get the ISession instance (pull from IoC container, etc) 193 | } 194 | 195 | public override ISession ReleaseSession(ISession session) 196 | { 197 | // Do something here to release the session instance if necessary 198 | } 199 | } 200 | ``` 201 | 1. Next, you'll need to create an extension id provider class by inheriting from 202 | `ExtensionIdProvider`. This class is responsible for actually providing a copy of your 203 | `IManageSessions` implementation. For example: 204 | 205 | ```cs 206 | public class MySessionExtension : ExtensionIdProvider 207 | { 208 | public override IManageSessions CreateExtension(ExtendedActorSystem system) 209 | { 210 | // Return a copy of your implementation of IManageSessions 211 | return new MySessionManager(); 212 | } 213 | } 214 | ``` 215 | 1. Lastly, you'll need to register your extension with the actor system when creating it in your application. For 216 | example: 217 | 218 | ```cs 219 | var actorSystem = ActorSystem.Create("MyApplicationActorSystem"); 220 | var extensionId = new MySessionExtension(); 221 | actorSystem.RegisterExtension(extensionId); 222 | ``` 223 | 224 | The journal and snapshot store plugins will now call your code when resolving or releasing sessions. 225 | 226 | ### Tests 227 | The Cassandra tests are packaged and run as part of the default "All" build task. 228 | 229 | In order to run the tests, you must do the following things: 230 | 231 | 1. Download and install DataStax Community Edition of Cassandra from http://planetcassandra.org/cassandra/ 232 | 2. Install Cassandra with the default settings. The default connection string will connect to a Cassandra instance running at **127.0.0.1:9042** with no authentication. Here's the full default settings for an Akka.Persistence.Cassandra connection: 233 | 234 | ```xml 235 | cassandra-sessions { 236 | 237 | # The "default" Cassandra session, used by both the journal and snapshot store if not changed in 238 | # the cassandra-journal and cassandra-snapshot-store configuration sections below 239 | default { 240 | 241 | # Comma-seperated list of contact points in the cluster in the format of either [host] or [host:port] 242 | contact-points = [ "127.0.0.1" ] 243 | 244 | # Default port for contact points in the cluster, used if a contact point is not in [host:port] format 245 | port = 9042 246 | } 247 | } 248 | 249 | cassandra-journal { 250 | 251 | # Type name of the cassandra journal plugin 252 | class = "Akka.Persistence.Cassandra.Journal.CassandraJournal, Akka.Persistence.Cassandra" 253 | 254 | # The name (key) of the session to use when resolving an ISession instance. When using default session management, 255 | # this points at configuration under the "cassandra-sessions" section where the session's configuration is found. 256 | session-key = "default" 257 | 258 | # Whether or not to quote table and keyspace names when executing statements against Cassandra 259 | use-quoted-identifiers = false 260 | 261 | # The keyspace to be created/used by the journal 262 | keyspace = "akkanet" 263 | 264 | # A string to be appended to the CREATE KEYSPACE statement after the WITH clause when the keyspace is 265 | # automatically created. Use this to define options like replication strategy. 266 | keyspace-creation-options = "REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }" 267 | 268 | # When true the journal will automatically try to create the keyspace if it doesn't already exist on start 269 | keyspace-autocreate = true 270 | 271 | # Name of the table to be created/used by the journal 272 | table = "messages" 273 | 274 | # A string to be appended to the CREATE TABLE statement after the WITH clause. Use this to define things 275 | # like gc_grace_seconds or one of the many other table options. 276 | table-creation-properties = "" 277 | 278 | # The approximate number of rows per partition to use. Cannot be changed after table creation. 279 | partition-size = 5000000 280 | 281 | # The maximum number of messages to retrieve in one request when replaying messages 282 | max-result-size = 50001 283 | 284 | # Consistency level for reads 285 | read-consistency = "Quorum" 286 | 287 | # Consistency level for writes 288 | write-consistency = "Quorum" 289 | } 290 | 291 | cassandra-snapshot-store { 292 | 293 | # Type name of the cassandra snapshot store plugin 294 | class = "Akka.Persistence.Cassandra.Snapshot.CassandraSnapshotStore, Akka.Persistence.Cassandra" 295 | 296 | # The name (key) of the session to use when resolving an ISession instance. When using default session management, 297 | # this points at configuration under the "cassandra-sessions" section where the session's configuration is found. 298 | session-key = "default" 299 | 300 | # Whether or not to quote table and keyspace names when executing statements against Cassandra 301 | use-quoted-identifiers = false 302 | 303 | # The keyspace to be created/used by the snapshot store 304 | keyspace = "akkanet" 305 | 306 | # A string to be appended to the CREATE KEYSPACE statement after the WITH clause when the keyspace is 307 | # automatically created. Use this to define options like replication strategy. 308 | keyspace-creation-options = "REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }" 309 | 310 | # When true the journal will automatically try to create the keyspace if it doesn't already exist on start 311 | keyspace-autocreate = true 312 | 313 | # Name of the table to be created/used by the snapshot store 314 | table = "snapshots" 315 | 316 | # A string to be appended to the CREATE TABLE statement after the WITH clause. Use this to define things 317 | # like gc_grace_seconds or one of the many other table options. 318 | table-creation-properties = "" 319 | 320 | # The maximum number of snapshot metadata instances to retrieve in a single request when trying to find a 321 | # snapshot that matches the criteria 322 | max-metadata-result-size = 10 323 | 324 | # Consistency level for reads 325 | read-consistency = "One" 326 | 327 | # Consistency level for writes 328 | write-consistency = "One" 329 | } 330 | ``` -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra.Tests/CassandraIntegrationSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Akka.Actor; 3 | using Akka.Configuration; 4 | using Akka.TestKit; 5 | using Akka.Util.Internal; 6 | using Xunit; 7 | using System.Configuration; 8 | using Xunit.Abstractions; 9 | 10 | namespace Akka.Persistence.Cassandra.Tests 11 | { 12 | /// 13 | /// Some integration tests for Cassandra Journal and Snapshot plugins. 14 | /// 15 | public class CassandraIntegrationSpec : Akka.TestKit.Xunit2.TestKit 16 | { 17 | private static readonly Config IntegrationConfig = ConfigurationFactory.ParseString($@" 18 | akka.persistence.journal.plugin = ""cassandra-journal"" 19 | akka.persistence.snapshot-store.plugin = ""cassandra-snapshot-store"" 20 | akka.persistence.publish-plugin-commands = on 21 | akka.test.single-expect-default = 10s 22 | cassandra-journal.partition-size = 5 23 | cassandra-journal.max-result-size = 3 24 | cassandra-sessions.default.contact-points = [ ""{ ConfigurationManager.AppSettings["cassandraContactPoint"] }"" ] 25 | "); 26 | 27 | // Static so that each test run gets a different Id number 28 | private static readonly AtomicCounter ActorIdCounter = new AtomicCounter(); 29 | 30 | private readonly string _actorId; 31 | 32 | public CassandraIntegrationSpec(ITestOutputHelper output) 33 | : base(IntegrationConfig, "CassandraIntegration", output: output) 34 | { 35 | TestSetupHelpers.ResetJournalData(Sys); 36 | TestSetupHelpers.ResetSnapshotStoreData(Sys); 37 | 38 | // Increment actor Id with each test that's run 39 | int id = ActorIdCounter.IncrementAndGet(); 40 | _actorId = string.Format("p{0}", id); 41 | } 42 | 43 | [Fact] 44 | public void Cassandra_journal_should_write_and_replay_messages() 45 | { 46 | // Start a persistence actor and write some messages to it 47 | var actor1 = Sys.ActorOf(Props.Create(_actorId)); 48 | WriteAndVerifyMessages(actor1, 1L, 16L); 49 | 50 | // Now start a new instance (same persistence Id) and it should recover with those same messages 51 | var actor2 = Sys.ActorOf(Props.Create(_actorId)); 52 | for (long i = 1L; i <= 16L; i++) 53 | { 54 | string msg = string.Format("a-{0}", i); 55 | ExpectHandled(msg, i, true); 56 | } 57 | 58 | // We should then be able to send that actor another message and have it be persisted 59 | actor2.Tell("b"); 60 | ExpectHandled("b", 17L, false); 61 | } 62 | 63 | [Theory] 64 | [InlineData(true)] 65 | [InlineData(false)] 66 | public void Cassandra_journal_should_not_replay_deleted_messages(bool permanentDelete) 67 | { 68 | // Listen for delete messages on the event stream 69 | TestProbe deleteProbe = CreateTestProbe(); 70 | Sys.EventStream.Subscribe(deleteProbe.Ref, typeof (DeleteMessagesTo)); 71 | 72 | var actor1 = Sys.ActorOf(Props.Create(_actorId)); 73 | WriteAndVerifyMessages(actor1, 1L, 16L); 74 | 75 | // Tell the actor to delete some messages and make sure it's finished 76 | actor1.Tell(new DeleteToCommand(3L, permanentDelete)); 77 | deleteProbe.ExpectMsg(); 78 | 79 | // Start a second copy of the actor and verify it starts replaying from the correct spot 80 | Sys.ActorOf(Props.Create(_actorId)); 81 | for (long i = 4L; i <= 16L; i++) 82 | { 83 | string msg = string.Format("a-{0}", i); 84 | ExpectHandled(msg, i, true); 85 | } 86 | 87 | // Delete some more messages and wait for confirmation 88 | actor1.Tell(new DeleteToCommand(7L, permanentDelete)); 89 | deleteProbe.ExpectMsg(); 90 | 91 | // Start another copy and verify playback again 92 | Sys.ActorOf(Props.Create(_actorId)); 93 | for (long i = 8L; i <= 16L; i++) 94 | { 95 | string msg = string.Format("a-{0}", i); 96 | ExpectHandled(msg, i, true); 97 | } 98 | } 99 | 100 | [Fact] 101 | public void Cassandra_journal_should_replay_message_incrementally() 102 | { 103 | // Write some messages to a Persistent Actor 104 | var actor = Sys.ActorOf(Props.Create(_actorId)); 105 | WriteAndVerifyMessages(actor, 1L, 6L); 106 | 107 | TestProbe probe = CreateTestProbe(); 108 | 109 | // Create a persistent view from the actor that does not do auto-updating 110 | var view = Sys.ActorOf(Props.Create(_actorId + "-view", _actorId, probe.Ref)); 111 | probe.ExpectNoMsg(200); 112 | 113 | // Tell the view to update and verify we get the messages we wrote earlier replayed 114 | view.Tell(new Update(true, 3L)); 115 | probe.ExpectMsg("a-1"); 116 | probe.ExpectMsg("a-2"); 117 | probe.ExpectMsg("a-3"); 118 | probe.ExpectNoMsg(200); 119 | 120 | // Update the view again and verify we get the rest of the messages 121 | view.Tell(new Update(true, 3L)); 122 | probe.ExpectMsg("a-4"); 123 | probe.ExpectMsg("a-5"); 124 | probe.ExpectMsg("a-6"); 125 | probe.ExpectNoMsg(200); 126 | } 127 | 128 | [Fact] 129 | public void Persistent_actor_should_recover_from_a_snapshot_with_follow_up_messages() 130 | { 131 | // Write a message, snapshot, then another follow-up message 132 | var actor1 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 133 | actor1.Tell("a"); 134 | ExpectHandled("a", 1, false); 135 | actor1.Tell("snap"); 136 | ExpectMsg("snapped-a-1"); 137 | actor1.Tell("b"); 138 | ExpectHandled("b", 2, false); 139 | 140 | // Start the actor again and verify we get a snapshot, followed by the message that wasn't in the snapshot 141 | var actor2 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 142 | ExpectMsg("offered-a-1"); 143 | ExpectHandled("b", 2, true); 144 | } 145 | 146 | [Fact] 147 | public void Persistent_actor_should_recover_from_a_snapshot_with_follow_up_messages_and_an_upper_bound() 148 | { 149 | // Create an actor and trigger manual recovery so it will accept new messages 150 | var actor1 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 151 | actor1.Tell(new Recover(SnapshotSelectionCriteria.None)); 152 | 153 | // Write a message, snapshot, then write some follow-up messages 154 | actor1.Tell("a"); 155 | ExpectHandled("a", 1, false); 156 | actor1.Tell("snap"); 157 | ExpectMsg("snapped-a-1"); 158 | WriteSameMessageAndVerify(actor1, "a", 2L, 7L); 159 | 160 | // Create another copy of that actor and manually recover to an upper bound (i.e. past state) and verify 161 | // we get the expected messages after the snapshot 162 | var actor2 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 163 | actor2.Tell(new Recover(SnapshotSelectionCriteria.Latest, toSequenceNr: 3L)); 164 | ExpectMsg("offered-a-1"); 165 | ExpectHandled("a", 2, true); 166 | ExpectHandled("a", 3, true); 167 | 168 | // Should continue working after recovery to previous state, but highest sequence number should take into 169 | // account other messages that were written but not replayed 170 | actor2.Tell("d"); 171 | ExpectHandled("d", 8L, false); 172 | } 173 | 174 | [Fact] 175 | public void Persistent_actor_should_recover_from_a_snapshot_without_follow_up_messages_inside_a_partition() 176 | { 177 | // Write a message, then snapshot, no follow-up messages after snapshot 178 | var actor1 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 179 | actor1.Tell("a"); 180 | ExpectHandled("a", 1L, false); 181 | actor1.Tell("snap"); 182 | ExpectMsg("snapped-a-1"); 183 | 184 | // Start another copy and verify we recover with the snapshot 185 | var actor2 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 186 | ExpectMsg("offered-a-1"); 187 | 188 | // Write another message to verify 189 | actor2.Tell("b"); 190 | ExpectHandled("b", 2L, false); 191 | } 192 | 193 | [Fact] 194 | public void Persistent_actor_should_recover_from_a_snapshot_without_follow_up_messages_at_a_partition_boundary_where_next_partition_is_invalid() 195 | { 196 | // Partition size for tests is 5 (see Config above), so write messages up to partition boundary (but don't write any 197 | // messages to the next partition) 198 | var actor1 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 199 | WriteSameMessageAndVerify(actor1, "a", 1L, 5L); 200 | 201 | // Snapshot and verify without any follow-up messages 202 | actor1.Tell("snap"); 203 | ExpectMsg("snapped-a-5"); 204 | 205 | // Create a second copy of that actor and verify it recovers from the snapshot and continues working 206 | var actor2 = Sys.ActorOf(Props.Create(_actorId, TestActor)); 207 | ExpectMsg("offered-a-5"); 208 | actor2.Tell("b"); 209 | ExpectHandled("b", 6L, false); 210 | } 211 | 212 | /// 213 | /// Write messages "a-xxx" where xxx is an index number from start to end and verify that each message returns 214 | /// a Handled response. 215 | /// 216 | private void WriteAndVerifyMessages(IActorRef persistentActor, long start, long end) 217 | { 218 | for (long i = start; i <= end; i++) 219 | { 220 | string msg = string.Format("a-{0}", i); 221 | persistentActor.Tell(msg, TestActor); 222 | ExpectHandled(msg, i, false); 223 | } 224 | } 225 | 226 | /// 227 | /// Writes the same message multiple times and verify that we get a Handled response. 228 | /// 229 | private void WriteSameMessageAndVerify(IActorRef persistentActor, string message, long start, long end) 230 | { 231 | for (long i = start; i <= end; i++) 232 | { 233 | persistentActor.Tell(message, TestActor); 234 | ExpectHandled(message, i, false); 235 | } 236 | } 237 | 238 | private void ExpectHandled(string message, long sequenceNumber, bool isRecovering) 239 | { 240 | object msg = ReceiveOne(); 241 | var handledMsg = Assert.IsType(msg); 242 | Assert.Equal(message, handledMsg.Message); 243 | Assert.Equal(sequenceNumber, handledMsg.SequenceNumber); 244 | Assert.Equal(isRecovering, handledMsg.IsRecovering); 245 | } 246 | 247 | #region Test Messages and Actors 248 | 249 | [Serializable] 250 | public class DeleteToCommand 251 | { 252 | public long SequenceNumber { get; private set; } 253 | public bool Permanent { get; private set; } 254 | 255 | public DeleteToCommand(long sequenceNumber, bool permanent) 256 | { 257 | SequenceNumber = sequenceNumber; 258 | Permanent = permanent; 259 | } 260 | } 261 | 262 | [Serializable] 263 | public class HandledMessage 264 | { 265 | public string Message { get; private set; } 266 | public long SequenceNumber { get; private set; } 267 | public bool IsRecovering { get; private set; } 268 | 269 | public HandledMessage(string message, long sequenceNumber, bool isRecovering) 270 | { 271 | Message = message; 272 | SequenceNumber = sequenceNumber; 273 | IsRecovering = isRecovering; 274 | } 275 | } 276 | 277 | public class PersistentActorA : PersistentActor 278 | { 279 | private readonly string _persistenceId; 280 | 281 | public PersistentActorA(string persistenceId) 282 | { 283 | _persistenceId = persistenceId; 284 | } 285 | 286 | public override string PersistenceId 287 | { 288 | get { return _persistenceId; } 289 | } 290 | 291 | protected override bool ReceiveRecover(object message) 292 | { 293 | if (message is string) 294 | { 295 | var payload = (string) message; 296 | Handle(payload); 297 | return true; 298 | } 299 | 300 | return false; 301 | } 302 | 303 | protected override bool ReceiveCommand(object message) 304 | { 305 | if (message is DeleteToCommand) 306 | { 307 | var delete = (DeleteToCommand) message; 308 | DeleteMessages(delete.SequenceNumber, delete.Permanent); 309 | return true; 310 | } 311 | 312 | if (message is string) 313 | { 314 | var payload = (string) message; 315 | Persist(payload, Handle); 316 | return true; 317 | } 318 | 319 | return false; 320 | } 321 | 322 | private void Handle(string payload) 323 | { 324 | Context.Sender.Tell(new HandledMessage(payload, LastSequenceNr, IsRecovering), Self); 325 | } 326 | } 327 | 328 | public class PersistentActorC : PersistentActor 329 | { 330 | private readonly string _persistenceId; 331 | private readonly IActorRef _probe; 332 | 333 | private string _last; 334 | 335 | public override string PersistenceId 336 | { 337 | get { return _persistenceId; } 338 | } 339 | 340 | public PersistentActorC(string persistenceId, IActorRef probe) 341 | { 342 | _persistenceId = persistenceId; 343 | _probe = probe; 344 | } 345 | 346 | protected override bool ReceiveRecover(object message) 347 | { 348 | if (message is SnapshotOffer) 349 | { 350 | var offer = (SnapshotOffer) message; 351 | _last = (string) offer.Snapshot; 352 | _probe.Tell(string.Format("offered-{0}", _last)); 353 | return true; 354 | } 355 | 356 | if (message is string) 357 | { 358 | var payload = (string) message; 359 | Handle(payload); 360 | return true; 361 | } 362 | 363 | return false; 364 | } 365 | 366 | protected override bool ReceiveCommand(object message) 367 | { 368 | if (message is string) 369 | { 370 | var msg = (string) message; 371 | if (msg == "snap") 372 | SaveSnapshot(_last); 373 | else 374 | Persist(msg, Handle); 375 | 376 | return true; 377 | } 378 | 379 | if (message is SaveSnapshotSuccess) 380 | { 381 | _probe.Tell(string.Format("snapped-{0}", _last), Context.Sender); 382 | return true; 383 | } 384 | 385 | if (message is DeleteToCommand) 386 | { 387 | var delete = (DeleteToCommand) message; 388 | DeleteMessages(delete.SequenceNumber, delete.Permanent); 389 | return true; 390 | } 391 | 392 | return false; 393 | } 394 | 395 | private void Handle(string payload) 396 | { 397 | _last = string.Format("{0}-{1}", payload, LastSequenceNr); 398 | _probe.Tell(new HandledMessage(payload, LastSequenceNr, IsRecovering)); 399 | } 400 | } 401 | 402 | public class PersistentActorCWithManualRecovery : PersistentActorC 403 | { 404 | public PersistentActorCWithManualRecovery(string persistenceId, IActorRef probe) 405 | : base(persistenceId, probe) 406 | { 407 | } 408 | 409 | protected override void PreRestart(Exception reason, object message) 410 | { 411 | // Don't do automatic recovery 412 | } 413 | } 414 | 415 | public class ViewA : PersistentView 416 | { 417 | private readonly string _viewId; 418 | private readonly string _persistenceId; 419 | private readonly IActorRef _probe; 420 | 421 | public override string ViewId 422 | { 423 | get { return _viewId; } 424 | } 425 | 426 | public override string PersistenceId 427 | { 428 | get { return _persistenceId; } 429 | } 430 | 431 | public override bool IsAutoUpdate 432 | { 433 | get { return false; } 434 | } 435 | 436 | public override long AutoUpdateReplayMax 437 | { 438 | get { return 0L; } 439 | } 440 | 441 | public ViewA(string viewId, string persistenceId, IActorRef probe) 442 | { 443 | _viewId = viewId; 444 | _persistenceId = persistenceId; 445 | _probe = probe; 446 | } 447 | 448 | protected override bool Receive(object message) 449 | { 450 | // Just forward messages to the test probe 451 | _probe.Tell(message, Context.Sender); 452 | return true; 453 | } 454 | } 455 | 456 | #endregion 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/Akka.Persistence.Cassandra/Journal/CassandraJournal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Akka.Configuration; 6 | using Akka.Persistence.Journal; 7 | using Akka.Serialization; 8 | using Cassandra; 9 | 10 | namespace Akka.Persistence.Cassandra.Journal 11 | { 12 | /// 13 | /// An Akka.NET journal implementation that writes events asynchronously to Cassandra. 14 | /// 15 | public class CassandraJournal : AsyncWriteJournal 16 | { 17 | private const string InvalidPartitionSizeException = 18 | "Partition size cannot change after initial table creation. (Value at creation: {0}, Currently configured value in Akka configuration: {1})"; 19 | 20 | private static readonly Type PersistentRepresentationType = typeof (IPersistentRepresentation); 21 | 22 | private readonly CassandraExtension _cassandraExtension; 23 | private readonly Serializer _serializer; 24 | private readonly int _maxDeletionBatchSize; 25 | 26 | private ISession _session; 27 | private PreparedStatement _writeMessage; 28 | private PreparedStatement _writeHeader; 29 | private PreparedStatement _selectHeaderSequence; 30 | private PreparedStatement _selectLastMessageSequence; 31 | private PreparedStatement _selectMessages; 32 | private PreparedStatement _writeDeleteMarker; 33 | private PreparedStatement _deleteMessagePermanent; 34 | private PreparedStatement _selectDeletedToSequence; 35 | private PreparedStatement _selectConfigurationValue; 36 | private PreparedStatement _writeConfigurationValue; 37 | 38 | public CassandraJournal() 39 | { 40 | _cassandraExtension = CassandraPersistence.Instance.Apply(Context.System); 41 | _serializer = Context.System.Serialization.FindSerializerForType(PersistentRepresentationType); 42 | 43 | // Use setting from the persistence extension when batch deleting 44 | PersistenceExtension persistence = Context.System.PersistenceExtension(); 45 | _maxDeletionBatchSize = persistence.Settings.Journal.MaxDeletionBatchSize; 46 | } 47 | 48 | protected override void PreStart() 49 | { 50 | base.PreStart(); 51 | 52 | // Create session 53 | CassandraJournalSettings settings = _cassandraExtension.JournalSettings; 54 | _session = _cassandraExtension.SessionManager.ResolveSession(settings.SessionKey); 55 | 56 | // Create keyspace if necessary and always try to create table 57 | if (settings.KeyspaceAutocreate) 58 | _session.Execute(string.Format(JournalStatements.CreateKeyspace, settings.Keyspace, settings.KeyspaceCreationOptions)); 59 | 60 | var fullyQualifiedTableName = string.Format("{0}.{1}", settings.Keyspace, settings.Table); 61 | 62 | string createTable = string.IsNullOrWhiteSpace(settings.TableCreationProperties) 63 | ? string.Format(JournalStatements.CreateTable, fullyQualifiedTableName, string.Empty, string.Empty) 64 | : string.Format(JournalStatements.CreateTable, fullyQualifiedTableName, " WITH ", 65 | settings.TableCreationProperties); 66 | _session.Execute(createTable); 67 | 68 | // Prepare some statements against C* 69 | _writeMessage = _session.PrepareFormat(JournalStatements.WriteMessage, fullyQualifiedTableName); 70 | _writeHeader = _session.PrepareFormat(JournalStatements.WriteHeader, fullyQualifiedTableName); 71 | _selectHeaderSequence = _session.PrepareFormat(JournalStatements.SelectHeaderSequence, fullyQualifiedTableName); 72 | _selectLastMessageSequence = _session.PrepareFormat(JournalStatements.SelectLastMessageSequence, fullyQualifiedTableName); 73 | _selectMessages = _session.PrepareFormat(JournalStatements.SelectMessages, fullyQualifiedTableName); 74 | _writeDeleteMarker = _session.PrepareFormat(JournalStatements.WriteDeleteMarker, fullyQualifiedTableName); 75 | _deleteMessagePermanent = _session.PrepareFormat(JournalStatements.DeleteMessagePermanent, fullyQualifiedTableName); 76 | _selectDeletedToSequence = _session.PrepareFormat(JournalStatements.SelectDeletedToSequence, fullyQualifiedTableName); 77 | _selectConfigurationValue = _session.PrepareFormat(JournalStatements.SelectConfigurationValue, fullyQualifiedTableName); 78 | _writeConfigurationValue = _session.PrepareFormat(JournalStatements.WriteConfigurationValue, fullyQualifiedTableName); 79 | 80 | // The partition size can only be set once (the first time the table is created) so see if it's already been set 81 | long partitionSize = GetConfigurationValueOrDefault("partition-size", -1L); 82 | if (partitionSize == -1L) 83 | { 84 | // Persist the partition size specified in the cluster settings 85 | WriteConfigurationValue("partition-size", settings.PartitionSize); 86 | } 87 | else if (partitionSize != settings.PartitionSize) 88 | { 89 | throw new ConfigurationException(string.Format(InvalidPartitionSizeException, partitionSize, settings.PartitionSize)); 90 | } 91 | } 92 | 93 | public override async Task ReplayMessagesAsync(string persistenceId, long fromSequenceNr, long toSequenceNr, long max, 94 | Action replayCallback) 95 | { 96 | long partitionNumber = GetPartitionNumber(fromSequenceNr); 97 | 98 | // A sequence number may have been moved to the next partition if it was part of a batch that was too large 99 | // to write to a single partition 100 | long maxPartitionNumber = GetPartitionNumber(toSequenceNr) + 1L; 101 | long count = 0L; 102 | 103 | while (partitionNumber <= maxPartitionNumber && count < max) 104 | { 105 | // Check for header and deleted to sequence number in parallel 106 | RowSet[] rowSets = await GetHeaderAndDeletedTo(persistenceId, partitionNumber).ConfigureAwait(false); 107 | 108 | // If header doesn't exist, just bail on the non-existent partition 109 | if (rowSets[0].SingleOrDefault() == null) 110 | return; 111 | 112 | // See what's been deleted in the partition and if no record found, just use long's min value 113 | Row deletedToRow = rowSets[1].SingleOrDefault(); 114 | long deletedTo = deletedToRow == null ? long.MinValue : deletedToRow.GetValue("sequence_number"); 115 | 116 | // Page through messages in the partition 117 | bool hasNextPage = true; 118 | byte[] pageState = null; 119 | while (count < max && hasNextPage) 120 | { 121 | // Get next page from current partition 122 | IStatement getRows = _selectMessages.Bind(persistenceId, partitionNumber, fromSequenceNr, toSequenceNr) 123 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency) 124 | .SetPageSize(_cassandraExtension.JournalSettings.MaxResultSize) 125 | .SetPagingState(pageState) 126 | .SetAutoPage(false); 127 | 128 | RowSet messageRows = await _session.ExecuteAsync(getRows).ConfigureAwait(false); 129 | pageState = messageRows.PagingState; 130 | hasNextPage = pageState != null; 131 | IEnumerator messagesEnumerator = 132 | messageRows.Select(row => MapRowToPersistentRepresentation(row, deletedTo)) 133 | .GetEnumerator(); 134 | 135 | // Process page 136 | while (count < max && messagesEnumerator.MoveNext()) 137 | { 138 | replayCallback(messagesEnumerator.Current); 139 | count++; 140 | } 141 | } 142 | 143 | // Go to next partition 144 | partitionNumber++; 145 | } 146 | } 147 | 148 | public override async Task ReadHighestSequenceNrAsync(string persistenceId, long fromSequenceNr) 149 | { 150 | fromSequenceNr = Math.Max(1L, fromSequenceNr); 151 | long partitionNumber = GetPartitionNumber(fromSequenceNr); 152 | long maxSequenceNumber = 0L; 153 | while (true) 154 | { 155 | // Check for header and deleted to sequence number in parallel 156 | RowSet[] rowSets = await GetHeaderAndDeletedTo(persistenceId, partitionNumber).ConfigureAwait(false); 157 | 158 | // If header doesn't exist, just bail on the non-existent partition 159 | if (rowSets[0].SingleOrDefault() == null) 160 | break; 161 | 162 | // See what's been deleted in the partition and if no record found, just use long's min value 163 | Row deletedToRow = rowSets[1].SingleOrDefault(); 164 | long deletedTo = deletedToRow == null ? long.MinValue : deletedToRow.GetValue("sequence_number"); 165 | 166 | // Try to avoid reading possible tombstones by skipping deleted records if higher than the fromSequenceNr provided 167 | long from = Math.Max(fromSequenceNr, deletedTo); 168 | 169 | // Get the last sequence number in the partition, skipping deleted messages 170 | IStatement getLastMessageSequence = _selectLastMessageSequence.Bind(persistenceId, partitionNumber, from) 171 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency); 172 | RowSet sequenceRows = await _session.ExecuteAsync(getLastMessageSequence).ConfigureAwait(false); 173 | 174 | // If there aren't any non-deleted messages, use the delete marker's value as the max, otherwise, use whatever value was returned 175 | Row sequenceRow = sequenceRows.SingleOrDefault(); 176 | maxSequenceNumber = sequenceRow == null ? Math.Max(maxSequenceNumber, deletedTo) : sequenceRow.GetValue("sequence_number"); 177 | 178 | // Go to next partition 179 | partitionNumber++; 180 | } 181 | 182 | return maxSequenceNumber; 183 | } 184 | 185 | protected override async Task WriteMessagesAsync(IEnumerable messages) 186 | { 187 | // It's implied by the API/docs that a batch of messages will be for a single persistence id 188 | List messageList = messages.ToList(); 189 | 190 | if (!messageList.Any()) 191 | return; 192 | 193 | string persistenceId = messageList[0].PersistenceId; 194 | 195 | long seqNr = messageList[0].SequenceNr; 196 | bool writeHeader = IsNewPartition(seqNr); 197 | long partitionNumber = GetPartitionNumber(seqNr); 198 | 199 | if (messageList.Count > 1) 200 | { 201 | // See if this collection of writes would span multiple partitions and if so, move all the writes to the next partition 202 | long lastMessagePartition = GetPartitionNumber(messageList[messageList.Count - 1].SequenceNr); 203 | if (lastMessagePartition != partitionNumber) 204 | { 205 | partitionNumber = lastMessagePartition; 206 | writeHeader = true; 207 | } 208 | } 209 | 210 | // No need for a batch if writing a single message 211 | if (messageList.Count == 1 && writeHeader == false) 212 | { 213 | IPersistentRepresentation message = messageList[0]; 214 | IStatement statement = _writeMessage.Bind(persistenceId, partitionNumber, message.SequenceNr, Serialize(message)) 215 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.WriteConsistency); 216 | await _session.ExecuteAsync(statement); 217 | return; 218 | } 219 | 220 | // Use a batch and add statements for each message 221 | var batch = new BatchStatement(); 222 | foreach (IPersistentRepresentation message in messageList) 223 | { 224 | batch.Add(_writeMessage.Bind(message.PersistenceId, partitionNumber, message.SequenceNr, Serialize(message))); 225 | } 226 | 227 | // Add header if necessary 228 | if (writeHeader) 229 | batch.Add(_writeHeader.Bind(persistenceId, partitionNumber, seqNr)); 230 | 231 | batch.SetConsistencyLevel(_cassandraExtension.JournalSettings.WriteConsistency); 232 | await _session.ExecuteAsync(batch); 233 | } 234 | 235 | protected override async Task DeleteMessagesToAsync(string persistenceId, long toSequenceNr, bool isPermanent) 236 | { 237 | long maxPartitionNumber = GetPartitionNumber(toSequenceNr) + 1L; 238 | long partitionNumber = 0L; 239 | 240 | while (partitionNumber <= maxPartitionNumber) 241 | { 242 | // Check for header and deleted to sequence number in parallel 243 | RowSet[] rowSets = await GetHeaderAndDeletedTo(persistenceId, partitionNumber).ConfigureAwait(false); 244 | 245 | // If header doesn't exist, just bail on the non-existent partition 246 | Row headerRow = rowSets[0].SingleOrDefault(); 247 | if (headerRow == null) 248 | return; 249 | 250 | // Start deleting either from the first sequence number after the last deletion, or the beginning of the partition 251 | Row deletedToRow = rowSets[1].SingleOrDefault(); 252 | long deleteFrom = deletedToRow == null 253 | ? headerRow.GetValue("sequence_number") 254 | : deletedToRow.GetValue("sequence_number") + 1L; 255 | 256 | // Nothing to delete if we're going to start higher than the specified sequence number 257 | if (deleteFrom > toSequenceNr) 258 | return; 259 | 260 | // Get the last sequence number in the partition and try to avoid tombstones by skipping deletes 261 | IStatement getLastMessageSequence = _selectLastMessageSequence.Bind(persistenceId, partitionNumber, deleteFrom) 262 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency); 263 | RowSet lastSequenceRows = await _session.ExecuteAsync(getLastMessageSequence).ConfigureAwait(false); 264 | 265 | // If we have a sequence number, we've got messages to delete still in the partition 266 | Row lastSequenceRow = lastSequenceRows.SingleOrDefault(); 267 | if (lastSequenceRow != null) 268 | { 269 | // Delete either to the end of the partition or to the number specified, whichever comes first 270 | long deleteTo = Math.Min(lastSequenceRow.GetValue("sequence_number"), toSequenceNr); 271 | if (isPermanent == false) 272 | { 273 | IStatement writeMarker = _writeDeleteMarker.Bind(persistenceId, partitionNumber, deleteTo) 274 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.WriteConsistency); 275 | await _session.ExecuteAsync(writeMarker).ConfigureAwait(false); 276 | } 277 | else 278 | { 279 | // Permanently delete using batches in parallel 280 | long batchFrom = deleteFrom; 281 | long batchTo; 282 | var batches = new List(); 283 | do 284 | { 285 | batchTo = Math.Min(batchFrom + _maxDeletionBatchSize - 1L, deleteTo); 286 | 287 | var batch = new BatchStatement(); 288 | for (long seq = batchFrom; seq <= batchTo; seq++) 289 | batch.Add(_deleteMessagePermanent.Bind(persistenceId, partitionNumber, seq)); 290 | 291 | batch.Add(_writeDeleteMarker.Bind(persistenceId, partitionNumber, batchTo)); 292 | batch.SetConsistencyLevel(_cassandraExtension.JournalSettings.WriteConsistency); 293 | 294 | batches.Add(_session.ExecuteAsync(batch)); 295 | batchFrom = batchTo + 1L; 296 | } while (batchTo < deleteTo); 297 | 298 | await Task.WhenAll(batches).ConfigureAwait(false); 299 | } 300 | 301 | // If we've deleted everything we're supposed to, no need to continue 302 | if (deleteTo == toSequenceNr) 303 | return; 304 | } 305 | 306 | // Go to next partition 307 | partitionNumber++; 308 | } 309 | } 310 | 311 | private Task GetHeaderAndDeletedTo(string persistenceId, long partitionNumber) 312 | { 313 | return Task.WhenAll(new[] 314 | { 315 | _selectHeaderSequence.Bind(persistenceId, partitionNumber).SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency), 316 | _selectDeletedToSequence.Bind(persistenceId, partitionNumber).SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency) 317 | }.Select(_session.ExecuteAsync)); 318 | } 319 | 320 | private IPersistentRepresentation MapRowToPersistentRepresentation(Row row, long deletedTo) 321 | { 322 | IPersistentRepresentation pr = Deserialize(row.GetValue("message")); 323 | if (pr.SequenceNr <= deletedTo) 324 | pr = pr.Update(pr.SequenceNr, pr.PersistenceId, true, pr.Sender); 325 | 326 | return pr; 327 | } 328 | 329 | private long GetPartitionNumber(long sequenceNumber) 330 | { 331 | return (sequenceNumber - 1L)/_cassandraExtension.JournalSettings.PartitionSize; 332 | } 333 | 334 | private bool IsNewPartition(long sequenceNumber) 335 | { 336 | return (sequenceNumber - 1L)%_cassandraExtension.JournalSettings.PartitionSize == 0L; 337 | } 338 | 339 | private T GetConfigurationValueOrDefault(string key, T defaultValue) 340 | { 341 | IStatement bound = _selectConfigurationValue.Bind(key).SetConsistencyLevel(_cassandraExtension.JournalSettings.ReadConsistency); 342 | RowSet rows = _session.Execute(bound); 343 | Row row = rows.SingleOrDefault(); 344 | if (row == null) 345 | return defaultValue; 346 | 347 | IPersistentRepresentation persistent = Deserialize(row.GetValue("message")); 348 | return (T) persistent.Payload; 349 | } 350 | 351 | private void WriteConfigurationValue(string key, T value) 352 | { 353 | var persistent = new Persistent(value); 354 | IStatement bound = _writeConfigurationValue.Bind(key, Serialize(persistent)) 355 | .SetConsistencyLevel(_cassandraExtension.JournalSettings.WriteConsistency); 356 | _session.Execute(bound); 357 | } 358 | 359 | private IPersistentRepresentation Deserialize(byte[] bytes) 360 | { 361 | return (IPersistentRepresentation) _serializer.FromBinary(bytes, PersistentRepresentationType); 362 | } 363 | 364 | private byte[] Serialize(IPersistentRepresentation message) 365 | { 366 | return _serializer.ToBinary(message); 367 | } 368 | 369 | protected override void PostStop() 370 | { 371 | base.PostStop(); 372 | 373 | if (_cassandraExtension != null && _session != null) 374 | { 375 | _cassandraExtension.SessionManager.ReleaseSession(_session); 376 | _session = null; 377 | } 378 | } 379 | } 380 | } 381 | --------------------------------------------------------------------------------