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