From e5a681156c49f13a2596c37eb94fde0388db5c63 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 17 Mar 2024 09:01:10 +0100 Subject: [PATCH] Use deterministic hash code. --- .../src/Squidex.Infrastructure/DomainId.cs | 28 ++++++++++++++++++- .../IDeterministicHashCode.cs | 13 +++++++++ .../States/ShardedService.cs | 2 +- .../Squidex.Infrastructure/States/Sharding.cs | 8 +++--- .../AppProviderExtensionsTests.cs | 10 +++++++ .../DomainIdTests.cs | 2 ++ .../States/ShardedServiceTests.cs | 8 +++--- .../States/ShardingTests.cs | 4 +-- .../notification-dropdown.component.scss | 2 +- 9 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs diff --git a/backend/src/Squidex.Infrastructure/DomainId.cs b/backend/src/Squidex.Infrastructure/DomainId.cs index d9746aa58..ef56b7fdc 100644 --- a/backend/src/Squidex.Infrastructure/DomainId.cs +++ b/backend/src/Squidex.Infrastructure/DomainId.cs @@ -10,7 +10,7 @@ using System.ComponentModel; namespace Squidex.Infrastructure; [TypeConverter(typeof(DomainIdTypeConverter))] -public readonly struct DomainId : IEquatable, IComparable +public readonly struct DomainId : IEquatable, IComparable, IDeterministicHashCode { private static readonly string EmptyString = Guid.Empty.ToString(); @@ -74,6 +74,32 @@ public readonly struct DomainId : IEquatable, IComparable return id ?? EmptyString; } + public int GetDeterministicHashCode() + { + unchecked + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + + if (id != null) + { + for (int i = 0; i < id.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ id[i]; + + if (i == id.Length - 1) + { + break; + } + + hash2 = ((hash2 << 5) + hash2) ^ id[i + 1]; + } + } + + return hash1 + (hash2 * 1566083941); + } + } + public int CompareTo(DomainId other) { return string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); diff --git a/backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs b/backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs new file mode 100644 index 000000000..1507a9fbc --- /dev/null +++ b/backend/src/Squidex.Infrastructure/IDeterministicHashCode.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure; + +public interface IDeterministicHashCode +{ + int GetDeterministicHashCode(); +} diff --git a/backend/src/Squidex.Infrastructure/States/ShardedService.cs b/backend/src/Squidex.Infrastructure/States/ShardedService.cs index ac2bc79f9..40fa79c07 100644 --- a/backend/src/Squidex.Infrastructure/States/ShardedService.cs +++ b/backend/src/Squidex.Infrastructure/States/ShardedService.cs @@ -9,7 +9,7 @@ using Squidex.Hosting; namespace Squidex.Infrastructure.States; -public abstract class ShardedService : IInitializable where TKey : notnull +public abstract class ShardedService : IInitializable where TKey : notnull, IDeterministicHashCode { private readonly Dictionary shards = []; private readonly IShardingStrategy sharding; diff --git a/backend/src/Squidex.Infrastructure/States/Sharding.cs b/backend/src/Squidex.Infrastructure/States/Sharding.cs index 028e63394..ef7e7e750 100644 --- a/backend/src/Squidex.Infrastructure/States/Sharding.cs +++ b/backend/src/Squidex.Infrastructure/States/Sharding.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States; public interface IShardingStrategy { - string GetShardKey(T key) where T : notnull; + string GetShardKey(T key) where T : notnull, IDeterministicHashCode; IEnumerable GetShardKeys(); } @@ -24,7 +24,7 @@ public sealed class SingleSharding : IShardingStrategy { } - public string GetShardKey(T key) where T : notnull + public string GetShardKey(T key) where T : notnull, IDeterministicHashCode { return string.Empty; } @@ -44,9 +44,9 @@ public sealed class PartitionedSharding : IShardingStrategy this.numPartitions = numPartitions; } - public string GetShardKey(T key) where T : notnull + public string GetShardKey(T key) where T : notnull, IDeterministicHashCode { - var partition = Math.Abs(key.GetHashCode()) % numPartitions; + var partition = Math.Abs(key.GetDeterministicHashCode()) % numPartitions; return GetShardKey(partition); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs index 61cc18106..3a43cc26e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs @@ -9,6 +9,7 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities; @@ -17,6 +18,15 @@ public class AppProviderExtensionsTests : GivenContext private readonly NamedId componentId1 = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly NamedId componentId2 = NamedId.Of(DomainId.NewGuid(), "my-schema"); + [Fact] + public void X() + { + var y = DomainId.Create("c3750ec4-baf1-44af-85f0-1495ab4f9f1a"); + + var x = new PartitionedSharding(20).GetShardKey(y); + } + + [Fact] public async Task Should_do_nothing_if_no_component_found() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs index 13efb36c6..b1ba0a94a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/DomainIdTests.cs @@ -138,10 +138,12 @@ public class DomainIdTests Assert.Equal(domainId_1_a, domainId_1_b); Assert.Equal(domainId_1_a.GetHashCode(), domainId_1_b.GetHashCode()); + Assert.Equal(domainId_1_a.GetDeterministicHashCode(), domainId_1_b.GetDeterministicHashCode()); Assert.True(domainId_1_a.Equals((object)domainId_1_b)); Assert.NotEqual(domainId_1_a, domainId_2_a); Assert.NotEqual(domainId_1_a.GetHashCode(), domainId_2_a.GetHashCode()); + Assert.NotEqual(domainId_1_a.GetDeterministicHashCode(), domainId_2_a.GetDeterministicHashCode()); Assert.False(domainId_1_a.Equals((object)domainId_2_a)); Assert.True(domainId_1_a == domainId_1_b); diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs index b6fa895f8..8882234f5 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/ShardedServiceTests.cs @@ -19,14 +19,14 @@ public class ShardedServiceTests { } - private class TestSut : ShardedService + private class TestSut : ShardedService { public TestSut(IShardingStrategy sharding, Func factory) : base(sharding, factory) { } - public IInner ExposeShard(int key) + public IInner ExposeShard(DomainId key) { return Shard(key); } @@ -76,7 +76,7 @@ public class ShardedServiceTests [Fact] public void Should_provide_shards() { - Assert.Equal(inner1, sut.ExposeShard(0)); - Assert.Equal(inner2, sut.ExposeShard(1)); + Assert.Equal(inner1, sut.ExposeShard(DomainId.Create("2"))); + Assert.Equal(inner2, sut.ExposeShard(DomainId.Create("3"))); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs index 810f6c05a..872fdaec4 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/ShardingTests.cs @@ -16,7 +16,7 @@ public class ShardingTests for (var i = 0; i < 1000; i++) { - var shardKey = strategy.GetShardKey(Guid.NewGuid()); + var shardKey = strategy.GetShardKey(DomainId.NewGuid()); Assert.Equal(string.Empty, shardKey); } @@ -39,7 +39,7 @@ public class ShardingTests for (var i = 0; i < 1000; i++) { - var shardKey = strategy.GetShardKey(Guid.NewGuid()); + var shardKey = strategy.GetShardKey(DomainId.NewGuid()); Assert.True(shardKey is "_0" or "_1" or "_2"); } diff --git a/frontend/src/app/shell/pages/internal/notification-dropdown.component.scss b/frontend/src/app/shell/pages/internal/notification-dropdown.component.scss index 19ac53553..4efaee585 100644 --- a/frontend/src/app/shell/pages/internal/notification-dropdown.component.scss +++ b/frontend/src/app/shell/pages/internal/notification-dropdown.component.scss @@ -14,6 +14,6 @@ } .badge { - @include absolute(-.5rem, 0, null, null); + @include absolute(-.375rem, 0, null, null); font-size: 80%; } \ No newline at end of file