From 204846b8509a63d41be38a2235e0825ebc8bb66c Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Thu, 12 Mar 2020 01:50:26 +0700 Subject: [PATCH 01/11] Updates for 0.6.0 release (#84) --- RELEASE_NOTES.md | 12 +++--------- src/common.props | 9 +++------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 43fa524..e98676c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,4 @@ -#### 0.6.0-rc2 March 10 2020 #### -**Beta Release of Akka.Persistence.Azure** +#### 0.6.0 March 12 2020 #### +**Release of Akka.Persistence.Azure** -Upgraded Akka.Persistence.Azure v0.5.0 to target [the new Akka.NET v1.4 interfaces](https://getakka.net/community/whats-new/akkadotnet-v1.4.html). - ----- - -Akka.Persistence.Azure v0.5.0 is a major leap forward ahead of v0.1.0. It fully implements Akka.Persistence.Query and fully implements Akka.NET v1.4.0-compatible serialization techniques. - -There are still some issues with respect to ordering and result sets from Akka.Persistence.Query and those will be addressed in a future release of Akka.Persistence.Azure. \ No newline at end of file +Updates Akka version to 1.4.1 \ No newline at end of file diff --git a/src/common.props b/src/common.props index 5169351..e0e5f2f 100644 --- a/src/common.props +++ b/src/common.props @@ -3,11 +3,8 @@ Copyright © 2017-2020 Petabridge Petabridge 0.6.0 - Beta Release of Akka.Persistence.Azure** -Upgraded Akka.Persistence.Azure v0.5.0 to target [the new Akka.NET v1.4 interfaces](https://getakka.net/community/whats-new/akkadotnet-v1.4.html). ----- -Akka.Persistence.Azure v0.5.0 is a major leap forward ahead of v0.1.0. It fully implements Akka.Persistence.Query and fully implements Akka.NET v1.4.0-compatible serialization techniques. -There are still some issues with respect to ordering and result sets from Akka.Persistence.Query and those will be addressed in a future release of Akka.Persistence.Azure. + Release of Akka.Persistence.Azure** +Updates Akka version to 1.4.1 @@ -18,7 +15,7 @@ There are still some issues with respect to ordering and result sets from Akka.P 2.4.1 - 1.4.1-rc3 + 1.4.1 16.5.0 netcoreapp3.1 net461 From 122bba3b5abe3e5fded29278470d5d132bf92976 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 5 May 2020 14:35:47 +0000 Subject: [PATCH 02/11] Bump AkkaVersion from 1.4.1 to 1.4.5 (#92) --- src/common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.props b/src/common.props index e0e5f2f..d698ae5 100644 --- a/src/common.props +++ b/src/common.props @@ -15,7 +15,7 @@ Updates Akka version to 1.4.1 2.4.1 - 1.4.1 + 1.4.5 16.5.0 netcoreapp3.1 net461 From d448b1094b0417423d90d60720a31368ef228114 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 5 May 2020 14:58:52 +0000 Subject: [PATCH 03/11] Bump Microsoft.NET.Test.Sdk from 16.5.0 to 16.6.1 (#91) --- src/common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.props b/src/common.props index d698ae5..b13b1c7 100644 --- a/src/common.props +++ b/src/common.props @@ -16,7 +16,7 @@ Updates Akka version to 1.4.1 2.4.1 1.4.5 - 16.5.0 + 16.6.1 netcoreapp3.1 net461 netstandard2.0 From cd1204dcdf824772ded44344d92dccb43b493eaf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2020 17:28:02 +0000 Subject: [PATCH 04/11] Bump AkkaVersion from 1.4.5 to 1.4.8 (#96) --- src/common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.props b/src/common.props index b13b1c7..dde1ae8 100644 --- a/src/common.props +++ b/src/common.props @@ -15,7 +15,7 @@ Updates Akka version to 1.4.1 2.4.1 - 1.4.5 + 1.4.8 16.6.1 netcoreapp3.1 net461 From f8139e93f5416eb7e86541a96aca69bc5f39c2f6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2020 17:40:22 +0000 Subject: [PATCH 05/11] Bump xunit.runner.visualstudio from 2.4.1 to 2.4.2 (#95) --- .../Akka.Persistence.Azure.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj b/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj index 28784ec..564e1d2 100644 --- a/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj +++ b/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 388d25b9c51793062e68bb08e1c6af64a8ecb397 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2020 17:50:36 +0000 Subject: [PATCH 06/11] Bump FluentAssertions from 5.10.2 to 5.10.3 (#88) --- .../Akka.Persistence.Azure.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj b/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj index 564e1d2..f2c0517 100644 --- a/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj +++ b/src/Akka.Persistence.Azure.Tests/Akka.Persistence.Azure.Tests.csproj @@ -7,7 +7,7 @@ - + From bf84958a615154f5b3d2eef02fb16d921fcf9349 Mon Sep 17 00:00:00 2001 From: Igor Fedchenko Date: Fri, 10 Jul 2020 17:54:04 +0300 Subject: [PATCH 07/11] Added default table and snapshot container names (#99) * Added default table and container names * Refactored and added specs * Fixed default empty values * Moved default names to HOCON * Fixed specs --- .../AzurePersistenceConfigSpec.cs | 25 +++++++++++++++++++ .../AzureTableStorageJournalSettings.cs | 2 -- .../AzureBlobSnapshotStoreSettings.cs | 8 ++++-- src/Akka.Persistence.Azure/reference.conf | 4 +-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Akka.Persistence.Azure.Tests/AzurePersistenceConfigSpec.cs b/src/Akka.Persistence.Azure.Tests/AzurePersistenceConfigSpec.cs index cbbf699..b6685c6 100644 --- a/src/Akka.Persistence.Azure.Tests/AzurePersistenceConfigSpec.cs +++ b/src/Akka.Persistence.Azure.Tests/AzurePersistenceConfigSpec.cs @@ -42,6 +42,19 @@ public void ShouldParseDefaultSnapshotConfig() blobSettings.RequestTimeout.Should().Be(TimeSpan.FromSeconds(3)); blobSettings.VerboseLogging.Should().BeFalse(); } + + [Fact] + public void ShouldProvideDefaultContainerNameValue() + { + var blobSettings = + AzureBlobSnapshotStoreSettings.Create( + ConfigurationFactory.ParseString(@"akka.persistence.snapshot-store.azure-blob-store{ + connection-string = foo + }").WithFallback(AzurePersistence.DefaultConfig) + .GetConfig("akka.persistence.snapshot-store.azure-blob-store")); + + blobSettings.ContainerName.Should().Be("akka-persistence-default-container"); + } [Fact] public void ShouldParseTableConfig() @@ -61,6 +74,18 @@ public void ShouldParseTableConfig() tableSettings.VerboseLogging.Should().BeFalse(); } + [Fact] + public void ShouldProvideDefaultTableNameValue() + { + var tableSettings = + AzureTableStorageJournalSettings.Create( + ConfigurationFactory.ParseString(@"akka.persistence.journal.azure-table{ + connection-string = foo + }").WithFallback(AzurePersistence.DefaultConfig) + .GetConfig("akka.persistence.journal.azure-table")); + tableSettings.TableName.Should().Be("AkkaPersistenceDefaultTable"); + } + [Fact] public void ShouldParseQueryConfig() { diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs index 67b47be..1d7f1bc 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs @@ -16,7 +16,6 @@ namespace Akka.Persistence.Azure.Journal /// public sealed class AzureTableStorageJournalSettings { - private static readonly string[] ReservedTableNames = {"tables"}; public AzureTableStorageJournalSettings( @@ -79,7 +78,6 @@ public static AzureTableStorageJournalSettings Create(Config config) var connectTimeout = config.GetTimeSpan("connect-timeout", TimeSpan.FromSeconds(3)); var requestTimeout = config.GetTimeSpan("request-timeout", TimeSpan.FromSeconds(3)); var verbose = config.GetBoolean("verbose-logging", false); - return new AzureTableStorageJournalSettings( connectionString, tableName, diff --git a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs index 8cf2512..b885911 100644 --- a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs +++ b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs @@ -61,11 +61,15 @@ public AzureBlobSnapshotStoreSettings(string connectionString, string containerN public static AzureBlobSnapshotStoreSettings Create(Config config) { var connectionString = config.GetString("connection-string"); - var containerName = config.GetString("container-name"); var connectTimeout = config.GetTimeSpan("connect-timeout", TimeSpan.FromSeconds(3)); var requestTimeout = config.GetTimeSpan("request-timeout", TimeSpan.FromSeconds(3)); var verbose = config.GetBoolean("verbose-logging", false); - return new AzureBlobSnapshotStoreSettings(connectionString, containerName, connectTimeout, requestTimeout, + var containerName = config.GetString("container-name"); + return new AzureBlobSnapshotStoreSettings( + connectionString, + containerName, + connectTimeout, + requestTimeout, verbose); } } diff --git a/src/Akka.Persistence.Azure/reference.conf b/src/Akka.Persistence.Azure/reference.conf index 8ed299f..39fb63f 100644 --- a/src/Akka.Persistence.Azure/reference.conf +++ b/src/Akka.Persistence.Azure/reference.conf @@ -12,7 +12,7 @@ akka.persistence { connection-string = "" # the name of the Windows Azure Table used to persist journal events - table-name = "" + table-name = "AkkaPersistenceDefaultTable" # Initial timeout to use when connecting to Azure Storage for the first time. connect-timeout = 3s @@ -63,7 +63,7 @@ akka.persistence { connection-string = "" # the name of the Windows Azure Blob Store container used to persist snapshots - container-name = "" + container-name = "akka-persistence-default-container" # Initial timeout to use when connecting to Azure Storage for the first time. connect-timeout = 3s From 2fe532bb4b3a955ae3aab9dcf621a18a8e005904 Mon Sep 17 00:00:00 2001 From: Igor Fedchenko Date: Fri, 10 Jul 2020 18:37:44 +0300 Subject: [PATCH 08/11] Updated readme with configuration hints (#100) * Fixed indent * Updated readme * Fixed config indent * Converted tabs to spaces in reference.conf --- README.md | 19 ++++++ src/Akka.Persistence.Azure/reference.conf | 78 +++++++++++------------ 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b8b39bf..26825e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,25 @@ Akka.Persistence implementation that uses Windows Azure table and blob storage. +## Configuration + +Here is a default configuration used by this plugin: https://github.com/petabridge/Akka.Persistence.Azure/blob/dev/src/Akka.Persistence.Azure/reference.conf + +You will need to provide connection string and Azure Table name for journal, and connection string with container name for Azure Blob Store: +``` +# Need to enable plugin +akka.persistence.journal.plugin = akka.persistence.journal.azure-table +akka.persistence.snapshot-store.plugin = akka.persistence.snapshot-store.azure-blob-store + +# Configure journal +akka.persistence.journal.azure-table.connection-string = "Your Azure Storage connection string" +akka.persistence.journal.azure-table.table-name = "Your table name" + +# Configure snapshots +akka.persistence.snapshot-store.azure-blob-store.connection-string = "Your Azure Storage connection string" +akka.persistence.snapshot-store.azure-blob-store.container-name = "Your container name" +``` + ## Building this solution To run the build script associated with this solution, execute the following: diff --git a/src/Akka.Persistence.Azure/reference.conf b/src/Akka.Persistence.Azure/reference.conf index 39fb63f..07f9475 100644 --- a/src/Akka.Persistence.Azure/reference.conf +++ b/src/Akka.Persistence.Azure/reference.conf @@ -11,19 +11,19 @@ akka.persistence { # connection string, as described here: https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string connection-string = "" - # the name of the Windows Azure Table used to persist journal events - table-name = "AkkaPersistenceDefaultTable" - - # Initial timeout to use when connecting to Azure Storage for the first time. - connect-timeout = 3s - - # Timeouts for individual read, write, and delete requests to Azure Storage. - request-timeout = 3s - - # Toggle for verbose logging. Logs individual requests to Azure. - # Intended only for debugging purposes. akka.loglevel must be set to DEBUG - # in order for this setting to be effective. - verbose-logging = off + # the name of the Windows Azure Table used to persist journal events + table-name = "AkkaPersistenceDefaultTable" + + # Initial timeout to use when connecting to Azure Storage for the first time. + connect-timeout = 3s + + # Timeouts for individual read, write, and delete requests to Azure Storage. + request-timeout = 3s + + # Toggle for verbose logging. Logs individual requests to Azure. + # Intended only for debugging purposes. akka.loglevel must be set to DEBUG + # in order for this setting to be effective. + verbose-logging = off # dispatcher used to drive journal actor plugin-dispatcher = "akka.actor.default-dispatcher" @@ -32,19 +32,19 @@ akka.persistence { query { journal { - azure-table { - # Implementation class of the Azure ReadJournalProvider - class = "Akka.Persistence.Azure.Query.AzureTableStorageReadJournalProvider, Akka.Persistence.Azure" - - # Absolute path to the write journal plugin configuration entry that this - # query journal will connect to. - # If undefined (or "") it will connect to the default journal as specified by the - # akka.persistence.journal.plugin property. - write-plugin = "" - - # How many events to fetch in one query (replay) and keep buffered until they - # are delivered downstreams. - max-buffer-size = 100 + azure-table { + # Implementation class of the Azure ReadJournalProvider + class = "Akka.Persistence.Azure.Query.AzureTableStorageReadJournalProvider, Akka.Persistence.Azure" + + # Absolute path to the write journal plugin configuration entry that this + # query journal will connect to. + # If undefined (or "") it will connect to the default journal as specified by the + # akka.persistence.journal.plugin property. + write-plugin = "" + + # How many events to fetch in one query (replay) and keep buffered until they + # are delivered downstreams. + max-buffer-size = 100 # The Azure Table write journal is notifying the query side as soon as things # are persisted, but for efficiency reasons the query side retrieves the events @@ -63,20 +63,20 @@ akka.persistence { connection-string = "" # the name of the Windows Azure Blob Store container used to persist snapshots - container-name = "akka-persistence-default-container" - - # Initial timeout to use when connecting to Azure Storage for the first time. - connect-timeout = 3s - - # Timeouts for individual read, write, and delete requests to Azure Storage. - request-timeout = 3s - - # Toggle for verbose logging. Logs individual requests to Azure. - # Intended only for debugging purposes. akka.loglevel must be set to DEBUG - # in order for this setting to be effective. - verbose-logging = off + container-name = "akka-persistence-default-container" + + # Initial timeout to use when connecting to Azure Storage for the first time. + connect-timeout = 3s + + # Timeouts for individual read, write, and delete requests to Azure Storage. + request-timeout = 3s + + # Toggle for verbose logging. Logs individual requests to Azure. + # Intended only for debugging purposes. akka.loglevel must be set to DEBUG + # in order for this setting to be effective. + verbose-logging = off - # dispatcher used to drive snapshot storage actor + # dispatcher used to drive snapshot storage actor plugin-dispatcher = "akka.actor.default-dispatcher" } } From 53b289ef002493296fff4bf914158aeb0e5ad416 Mon Sep 17 00:00:00 2001 From: Igor Fedchenko Date: Fri, 10 Jul 2020 21:17:54 +0300 Subject: [PATCH 09/11] Escape partition keys to support persistent actor ids (#101) * Added partition key escape helper with specs * Added new spec - just need to see if it will pass * Spec with overriden Pid should fail * Changed Pid overriding code * Added escape/unescape partition keys --- ...AzureTableJournalEscapePersistentIdSpec.cs | 32 ++++++++++ .../PartitionKeyEscapeHelperSpecs.cs | 24 ++++++++ .../Journal/AzureTableStorageJournal.cs | 25 +++++--- .../Util/PartitionKeyEscapeHelper.cs | 60 +++++++++++++++++++ 4 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 src/Akka.Persistence.Azure.Tests/AzureTableJournalEscapePersistentIdSpec.cs create mode 100644 src/Akka.Persistence.Azure.Tests/PartitionKeyEscapeHelperSpecs.cs create mode 100644 src/Akka.Persistence.Azure/Util/PartitionKeyEscapeHelper.cs diff --git a/src/Akka.Persistence.Azure.Tests/AzureTableJournalEscapePersistentIdSpec.cs b/src/Akka.Persistence.Azure.Tests/AzureTableJournalEscapePersistentIdSpec.cs new file mode 100644 index 0000000..705a2a4 --- /dev/null +++ b/src/Akka.Persistence.Azure.Tests/AzureTableJournalEscapePersistentIdSpec.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; +using Akka.Actor; +using Akka.Configuration; +using Akka.Persistence.Azure.TestHelpers; +using Akka.Persistence.TCK; +using Akka.Persistence.TCK.Journal; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; +using static Akka.Persistence.Azure.Tests.Helper.AzureStorageConfigHelper; + +namespace Akka.Persistence.Azure.Tests +{ + public class AzureTableJournalEscapePersistentIdSpec : AzureTableJournalSpec, IClassFixture + { + public AzureTableJournalEscapePersistentIdSpec(ITestOutputHelper output) : base(output) + { + } + + /// + protected override void PreparePersistenceId(string pid) + { + base.PreparePersistenceId(pid); + + // Before storage is initialized, let's set Pid to the value that needs to be encoded + var persistenceIdUsedForTests = typeof(PluginSpec).GetField($"<{nameof(Pid)}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); + var currentValue = persistenceIdUsedForTests.GetValue(this).ToString(); + persistenceIdUsedForTests.SetValue(this, $"some/path/to/encode/{currentValue}"); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Azure.Tests/PartitionKeyEscapeHelperSpecs.cs b/src/Akka.Persistence.Azure.Tests/PartitionKeyEscapeHelperSpecs.cs new file mode 100644 index 0000000..c9854ac --- /dev/null +++ b/src/Akka.Persistence.Azure.Tests/PartitionKeyEscapeHelperSpecs.cs @@ -0,0 +1,24 @@ +using Akka.Actor; +using Akka.Persistence.Azure.Util; +using FluentAssertions; +using Xunit; + +namespace Akka.Persistence.Azure.Tests +{ + public class PartitionKeyEscapeHelperSpecs + { + [Theory] + [InlineData("/system/sharding/areaCoordinator/singleton/coordinator")] + [InlineData("/system/sha$rding/areaCoordinator/single$ton/coordinator$")] + [InlineData("/system/sha$$$rding/areaCoord/inator$$/single$ton/coord$inator$")] + public void Should_escape_correctly(string partitionKey) + { + var escapedKey = PartitionKeyEscapeHelper.Escape(partitionKey); + escapedKey.Should().NotContain("/"); + var originalKey = PartitionKeyEscapeHelper.Unescape(escapedKey); + originalKey.Should().Be(partitionKey); + } + } + + class A : ReceiveActor{ } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs index 2eec0fd..e24e28e 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs @@ -162,7 +162,7 @@ public override async Task ReplayMessagesAsync( new Persistent( deserialized.Payload, deserialized.SequenceNr, - deserialized.PersistenceId, + PartitionKeyEscapeHelper.Unescape(deserialized.PersistenceId), deserialized.Manifest, deserialized.IsDeleted, ActorRefs.NoSender, @@ -359,6 +359,12 @@ protected override async Task> WriteMessagesAsync( x => batchItems = batchItems.Add( new HighestSequenceNrEntry(x.Key, x.Value))); + // Encode partition keys for writing + foreach (var tableEntity in batchItems) + { + tableEntity.PartitionKey = PartitionKeyEscapeHelper.Escape(tableEntity.PartitionKey); + } + batchItems.ForEach(x => persistenceBatch.InsertOrReplace(x)); if (_log.IsDebugEnabled && _settings.VerboseLogging) @@ -381,9 +387,11 @@ protected override async Task> WriteMessagesAsync( { var allPersistenceIdsBatch = new TableBatchOperation(); - highSequenceNumbers.ForEach( - x => allPersistenceIdsBatch.InsertOrReplace( - new AllPersistenceIdsEntry(x.Key))); + highSequenceNumbers.ForEach(x => + { + var encodedKey = PartitionKeyEscapeHelper.Escape(x.Key); + allPersistenceIdsBatch.InsertOrReplace(new AllPersistenceIdsEntry(encodedKey)); + }); var allPersistenceResults = await Table.ExecuteBatchAsync(allPersistenceIdsBatch); @@ -406,6 +414,7 @@ protected override async Task> WriteMessagesAsync( foreach (var item in kvp.Value) { + item.PartitionKey = PartitionKeyEscapeHelper.Escape(item.PartitionKey); eventTagsBatch.InsertOrReplace(item); } @@ -464,7 +473,7 @@ private static TableQuery GenerateHighestSequenceNumberQ TableQuery.GenerateFilterCondition( "PartitionKey", QueryComparisons.Equal, - persistenceId), + PartitionKeyEscapeHelper.Escape(persistenceId)), TableOperators.And, TableQuery.GenerateFilterCondition( "RowKey", @@ -485,7 +494,7 @@ private static TableQuery GeneratePersistentJournalEntry TableQuery.GenerateFilterCondition( "PartitionKey", QueryComparisons.Equal, - persistenceId); + PartitionKeyEscapeHelper.Escape(persistenceId)); var highestSequenceNrFilter = TableQuery.GenerateFilterCondition( @@ -577,7 +586,7 @@ private static TableQuery GeneratePersistentJournalEntry TableQuery.GenerateFilterCondition( "PartitionKey", QueryComparisons.Equal, - persistentId); + PartitionKeyEscapeHelper.Escape(persistentId)); var highestSequenceNrFilter = TableQuery.GenerateFilterCondition( @@ -627,7 +636,7 @@ private static TableQuery GenerateTaggedMessageQuery( TableQuery.GenerateFilterCondition( "PartitionKey", QueryComparisons.Equal, - EventTagEntry.GetPartitionKey(replay.Tag)); + PartitionKeyEscapeHelper.Escape(EventTagEntry.GetPartitionKey(replay.Tag))); var utcTicksTRowKeyFilter = TableQuery.CombineFilters( diff --git a/src/Akka.Persistence.Azure/Util/PartitionKeyEscapeHelper.cs b/src/Akka.Persistence.Azure/Util/PartitionKeyEscapeHelper.cs new file mode 100644 index 0000000..4e02641 --- /dev/null +++ b/src/Akka.Persistence.Azure/Util/PartitionKeyEscapeHelper.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Akka.Persistence.Azure.Util +{ + /// + /// PartitionKeyEscapeHelper + /// + public static class PartitionKeyEscapeHelper + { + /// + /// Sequence we need to escape + /// + private const string InvalidSequence = "/"; + /// + /// Sequence we use to escape invalid chars + /// + /// + /// Using $ here to resolve https://github.com/petabridge/Akka.Persistence.Azure/issues/98 + /// Actor names never start with $ sign, which helps to decode encoded keys + /// + private const string EscapeSequence = "$"; + + /// + /// Escape special characters in partition key + /// + /// Escaped partition key + public static string Escape(string partitionKey) + { + var escapedKey = partitionKey; + // First, replate escape sequence in case if it is used in original key + escapedKey = escapedKey.Replace(EscapeSequence, EscapeSequence + EscapeSequence); + + // Now escape special chars + escapedKey = escapedKey.Replace(InvalidSequence, EscapeSequence); + + return escapedKey; + } + + /// + /// Unescape previously escaped partition key + /// + /// Original, unescaped partition key + public static string Unescape(string escapedKey) + { + var originalKey = escapedKey; + var uuid = Guid.NewGuid().ToString("N"); + // Do not touch duplicated escape sequence - we will replace them later + originalKey = originalKey.Replace(EscapeSequence + EscapeSequence, uuid); + + // Restore escaped characters + originalKey = originalKey.Replace(EscapeSequence, InvalidSequence); + + // Now it is safe to decode duplicated sequences + originalKey = originalKey.Replace(uuid, EscapeSequence); + + return originalKey; + } + } +} \ No newline at end of file From dc609bfb89f6b2f426014e42cbc9e919dfa05693 Mon Sep 17 00:00:00 2001 From: Igor Fedchenko Date: Fri, 10 Jul 2020 21:45:25 +0300 Subject: [PATCH 10/11] Updated release notes (#103) --- RELEASE_NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e98676c..6fbd18f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,9 @@ +#### 0.6.1 July 10 2020 #### +**Release of Akka.Persistence.Azure** + +- Default configuration and documentation improvements +- Fixed Akka.Cluster.Sharding support (see https://github.com/petabridge/Akka.Persistence.Azure/issues/98) + #### 0.6.0 March 12 2020 #### **Release of Akka.Persistence.Azure** From 6fc7fad5a948ebb1cde6b716c3c7ca6fb40fd4fb Mon Sep 17 00:00:00 2001 From: Igor Fedchenko Date: Tue, 14 Jul 2020 19:05:50 +0300 Subject: [PATCH 11/11] Make CI to fail when spec fails (#105) * Added failing spec * Fail CI on test errors * Disabled partition key escape - check if specs will pass * Deleted failing spec * Get escape keys back * Skipped failing specs - moved them to separate issue --- build.fsx | 2 +- .../Query/AzureTableQueryEdgeCaseSpecs.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.fsx b/build.fsx index b4243c8..118a734 100644 --- a/build.fsx +++ b/build.fsx @@ -115,7 +115,7 @@ Target "RunTests" (fun _ -> info.WorkingDirectory <- (Directory.GetParent project).FullName info.Arguments <- arguments) (TimeSpan.FromMinutes 30.0) - ResultHandling.failBuildIfXUnitReportedError TestRunnerErrorLevel.DontFailBuild result + ResultHandling.failBuildIfXUnitReportedError TestRunnerErrorLevel.Error result projects |> Seq.iter (log) projects |> Seq.iter (runSingleProject) diff --git a/src/Akka.Persistence.Azure.Tests/Query/AzureTableQueryEdgeCaseSpecs.cs b/src/Akka.Persistence.Azure.Tests/Query/AzureTableQueryEdgeCaseSpecs.cs index 0cae1ab..4d788f4 100644 --- a/src/Akka.Persistence.Azure.Tests/Query/AzureTableQueryEdgeCaseSpecs.cs +++ b/src/Akka.Persistence.Azure.Tests/Query/AzureTableQueryEdgeCaseSpecs.cs @@ -51,7 +51,7 @@ public AzureTableQueryEdgeCaseSpecs(ITestOutputHelper output) /// /// Reproduction spec for https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/61 /// - [Fact] + [Fact(Skip = "Need to fix this in https://github.com/petabridge/Akka.Persistence.Azure/issues/107")] public async Task Bug61_Events_Recovered_By_Id_Should_Match_Tag() { var actor = Sys.ActorOf(TagActor.Props("x")); @@ -67,7 +67,7 @@ public async Task Bug61_Events_Recovered_By_Id_Should_Match_Tag() var eventsByTag = await ReadJournal.CurrentEventsByTag(typeof(RealMsg).Name) .RunAggregate(ImmutableHashSet.Empty, (agg, e) => agg.Add(e), Materializer); - eventsByTag.Count.Should().Be(MessageCount); + eventsByTag.Count.Should().Be(MessageCount, "All events should be loaded by tag"); eventsById.All(x => x.Event is RealMsg).Should().BeTrue("Expected all events by id to be RealMsg"); eventsByTag.All(x => x.Event is RealMsg).Should().BeTrue("Expected all events by tag to be RealMsg"); @@ -76,7 +76,7 @@ public async Task Bug61_Events_Recovered_By_Id_Should_Match_Tag() /// /// Reproduction spec for https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/80 /// - [Fact] + [Fact(Skip = "Need to fix this in https://github.com/petabridge/Akka.Persistence.Azure/issues/107")] public void Bug80_CurrentEventsByTag_should_Recover_until_end() { var actor = Sys.ActorOf(TagActor.Props("y")); @@ -93,7 +93,7 @@ public void Bug80_CurrentEventsByTag_should_Recover_until_end() /// /// Making sure EventsByTag didn't break during implementation of https://github.com/akkadotnet/Akka.Persistence.MongoDB/issues/80 /// - [Fact] + [Fact(Skip = "Need to fix this in https://github.com/petabridge/Akka.Persistence.Azure/issues/107")] public void Bug80_AllEventsByTag_should_Recover_all_messages() { var actor = Sys.ActorOf(TagActor.Props("y"));