From d76891dc220969e2aafcd84f03dbe5bffd268bd7 Mon Sep 17 00:00:00 2001 From: LemonNoCry Date: Mon, 9 Dec 2024 11:34:58 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=E2=9C=A8Add=20hosting=20of=20Snowf?= =?UTF-8?q?lake=20ID=20tool=20=E2=9A=A1=EF=B8=8FOptimize=20core=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #1, fixes #2 --- README.en.md | 63 ++++- README.md | 52 +++- .../SnowflakeId.AutoRegister.Db.csproj | 2 +- .../Storage/BaseDbStorage.cs | 20 +- .../SnowflakeId.AutoRegister.MySql.csproj | 2 +- .../Storage/MySqlQueryProvider.cs | 11 +- .../SnowflakeId.AutoRegister.SqlServer.csproj | 2 +- .../Storage/SqlServerQueryProvider.cs | 11 +- ...eId.AutoRegister.StackExchangeRedis.csproj | 2 +- .../Storage/RedisStorage.cs | 34 ++- .../Builder/AutoRegisterBuilder.cs | 22 ++ src/SnowflakeId.AutoRegister/Core/AppConst.cs | 11 + .../Core/DefaultAutoRegister.cs | 95 ++++--- .../Core/DefaultStore.cs | 6 +- .../Core/IdGeneratorAutoRegister.cs | 99 ++++++++ src/SnowflakeId.AutoRegister/GlobalUsings.cs | 5 +- .../Interfaces/IAutoRegister.cs | 26 ++ .../Interfaces/IStorage.cs | 19 +- .../SnowflakeId.AutoRegister.csproj | 2 +- test/Directory.Build.props | 2 +- .../IdGeneratorTests/YitterIdGeneratorTest.cs | 36 +++ .../Examples/RedisAutoRegister.cs | 9 +- .../IdGeneratorTests/YitterIdGeneratorTest.cs | 36 +++ .../RedisTestConfig.cs | 20 +- .../Examples/SqlServerAutoRegisterTest.cs | 5 +- .../IdGeneratorTests/YitterIdGeneratorTest.cs | 37 +++ .../SqlServerTestConfig.cs | 60 ++++- .../Base/TestBaseAutoRegister.cs | 56 ++++- .../Base/TestLog.cs | 17 +- .../Core/NonParallelCollectionDefinition.cs | 9 + .../GlobalUsings.cs | 1 + .../BaseYitterIdGeneratorTest.cs | 233 ++++++++++++++++++ .../SnowflakeId.AutoRegister.Tests.csproj | 4 + 33 files changed, 893 insertions(+), 116 deletions(-) create mode 100644 src/SnowflakeId.AutoRegister/Core/AppConst.cs create mode 100644 src/SnowflakeId.AutoRegister/Core/IdGeneratorAutoRegister.cs create mode 100644 test/SnowflakeId.AutoRegister.Tests.MySql/IdGeneratorTests/YitterIdGeneratorTest.cs create mode 100644 test/SnowflakeId.AutoRegister.Tests.Redis/IdGeneratorTests/YitterIdGeneratorTest.cs create mode 100644 test/SnowflakeId.AutoRegister.Tests.SqlServer/IdGeneratorTests/YitterIdGeneratorTest.cs create mode 100644 test/SnowflakeId.AutoRegister.Tests/Core/NonParallelCollectionDefinition.cs create mode 100644 test/SnowflakeId.AutoRegister.Tests/IdGeneratorTests/BaseYitterIdGeneratorTest.cs diff --git a/README.en.md b/README.en.md index a86c115..aba550a 100644 --- a/README.en.md +++ b/README.en.md @@ -19,6 +19,7 @@ registration, making it compatible with any framework or library using Snowflake - **Flexible configuration**: Chainable API to customize registration logic. - **High compatibility**: Supports .NET Standard 2.0, allowing cross-platform usage. - **Simplifies development**: Reduces complexity in managing WorkerId for distributed systems. +- **High reliability**: Supports automatic renewal of WorkerId to prevent duplicate assignments. --- @@ -173,22 +174,66 @@ long id = idGenInstance.NewLong(); Console.WriteLine($"Generated ID: {id}"); ``` -For other Snowflake ID libraries, follow a similar approach to pass the WorkerId obtained from -`SnowflakeId.AutoRegister`. +## AdvancedUsage + +### Managing the Lifecycle of `Snowflake ID Tool Library` + +Delegate the lifecycle of the Snowflake ID tool library to the `AutoRegister` instance to avoid the "zombie problem". +**Principle: Process A registers WorkerId 1, but due to various reasons (such as a short lifecycle, network issues, etc.), it cannot renew in time. In other processes, this +WorkerId is considered invalid, and process B will register the same WorkerId 1. When process A recovers, it will detect that WorkerId 1 is already in use and will cancel the +registration, re-registering the next time it is acquired.** + +Usage only requires adjusting `Build`. + +Here is an example of using `Yitter.IdGenerator`: + +```csharp +//IAutoRegister => IAutoRegister +static readonly IAutoRegister AutoRegister = new AutoRegisterBuilder() + + // Same as other configurations + ... + + // The key point is here + .Build(config => new DefaultIdGenerator(new IdGeneratorOptions() + { + WorkerId = (ushort)config.WorkerId + })); + + //Get Id + // Ensure to use `GetIdGenerator()` to get the `IdGenerator` instance each time, do not cache it, as it may re-register + long id =autoRegister.GetIdGenerator().NewLong(); + Console.WriteLine($"Id: {id}"); +``` + +### For other Snowflake ID generation libraries, refer to the above examples for integration. --- ## FAQ -- **What happens if the program crashes?** - WorkerId will not be released. On the next startup, the library attempts to reuse the previous WorkerId. If - unsuccessful, a new WorkerId is assigned. +* Q: Why do we need to auto-register WorkerId? +* A: Snowflake ID requires WorkerId to generate unique IDs. Auto-registering WorkerId can reduce the complexity of manual maintenance. + + +* Q: Will WorkerId be released if the program crashes? +* A: No. WorkerId has a lifecycle. If the program exits abnormally, it will try to register the previous WorkerId on the next startup. If it fails, it will re-register a new + WorkerId. + + +* Q: **What is the "zombie problem"?** +* A: **For example, process A registers a WorkerId, but due to various reasons (such as a short lifecycle, network issues, etc.), it cannot renew in time. In other processes, this + WorkerId is considered invalid, and process B will register the same WorkerId. If process A recovers, both process A and process B will use the same WorkerId, causing ID + duplication. See [Advanced Usage](#AdvancedUsage) for the solution.** + + +* Q: How to avoid multiple processes in the same file from being assigned the same WorkerId? +* A: Add a process-related identifier in SetExtraIdentifier, such as the current process ID. -- **How to prevent duplicate WorkerId across processes?** - Use `SetExtraIdentifier` with process-specific data, such as the current process ID. -- **Is the default storage mechanism suitable for production?** - No, it is recommended only for development and testing. Use Redis, SQL Server, or MySQL for production environments. +* Q: Is the default storage mechanism suitable for production environments? +* A: The default storage mechanism is only suitable for development and local testing (to maintain consistency). In production environments, it is recommended to use Redis, SQL + Server, MySQL, etc. --- diff --git a/README.md b/README.md index efbe500..64ec733 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ SnowflakeId AutoRegister 是一个库,提供了一种简单的方法在 Snowfl * 灵活配置:通过链式 API 自定义注册逻辑 * 高兼容性:支持 .NET Standard 2.0,可在多种平台运行 * 简化开发流程:减少手动维护 WorkerId 的复杂性 +* 高可靠性:支持 WorkerId 的自动续期,避免重复分配 --- @@ -120,7 +121,7 @@ static readonly IAutoRegister AutoRegister = new AutoRegisterBuilder() // 使用以下行使用 SQL Server 存储。 //.UseSqlServerStore("Server=localhost;Database=SnowflakeTest;User Id=sa;Password=123456;") - // Use the following line to use the MySQL store. + // 使用以下行使用 MySQL 存储。 .UseMySqlStore("Server=localhost;Port=3306;Database=snowflaketest;Uid=test;Pwd=123456;SslMode=None;") .Build(); @@ -163,7 +164,7 @@ applicationLifetime.ApplicationStopping.Register(() => --- -## 集成其他 Snowflake ID 库 +## 集成 Snowflake ID 库 ### Yitter.IdGenerator @@ -180,22 +181,63 @@ long id = idGenInstance.NewLong(); Console.WriteLine($"Id: {id}"); ``` +## 高级用法 + +### 托管`雪花Id工具库`生命周期 + +将雪花Id工具库的生命周期托管到`AutoRegister`实例中,以避免`假死问题`。 +**原理:进程A注册了WorkerId 1,但是进程A因为各种原因(如生命周期太短、网络问题等) +导致无法及时续期,在其他进程看来此WorkerId已无效,进程B注册就会获得相同的WorkerId 1,在进程A恢复正常后,重新续期时会检测当前WorkId 1已被使用,会取消注册下次获取时会重新注册,** + +用法只需要调整`Build`。 + +一下是`Yitter.IdGenerator` 的用法示例: + +```csharp +//IAutoRegister => IAutoRegister +static readonly IAutoRegister AutoRegister = new AutoRegisterBuilder() + + //与其他配置一样 + ... + + //重点在于这里 + .Build(config => new DefaultIdGenerator(new IdGeneratorOptions() + { + WorkerId = (ushort)config.WorkerId + })); + + //获取Id + //确保每次都要使用`GetIdGenerator()`来获取`IdGenerator`实例,不要缓存,因为可能会重新注册 + long id =autoRegister.GetIdGenerator().NewLong(); + Console.WriteLine($"Id: {id}"); +``` + ### 对于其他 Snowflake ID 生成库,可以参考上述示例进行集成。 --- ## 常见问题 (FAQ) +* Q: 为什么需要自动注册 WorkerId? +* A: Snowflake ID 需要 WorkerId 来生成唯一的 ID。自动注册 WorkerId 可以减少手动维护的复杂性。 + + * Q: 如果程序崩溃了,WorkerId 会被释放吗? -* A: 不会。程序异常退出时,下次启动会尝试分配上一次的 WorkerId。如果失败,则重新注册新的 WorkerId。 +* A: 不会。WorkerId存在生命周期,程序异常退出时,下次启动会尝试注册上一次的 WorkerId。如果失败,则重新注册新的 WorkerId。 + + +* Q: **"假死问题"是什么?** +* A: **例如:进程A注册了WorkerId,但是进程A因为各种原因(如生命周期太短、网络问题等) + 导致无法及时续期,在其他进程看来此WorkerId已无效,进程B注册就会获得相同的WorkerId,如果进程A恢复正常,此时进程A和进程B都会使用相同的WorkerId,导致ID重复 + 解决方案看[高级用法](#高级用法)** -* Q: 如何避免多进程重复分配 WorkerId? +* Q: 如何避免同文件多进程重复分配 WorkerId? * A: 在 SetExtraIdentifier 中添加进程相关的标识符,例如当前进程 ID。 * Q: 默认存储机制适合生产环境吗? -* A: 默认存储机制仅适合开发和本地测试。在生产环境中,建议使用 Redis、SQL Server 或 MySQL 存储。 +* A: 默认存储机制仅适合开发和本地测试(为了保持一致性)。在生产环境中,建议使用 Redis、SQL Server、MySql等等。 --- diff --git a/src/SnowflakeId.AutoRegister.Db/SnowflakeId.AutoRegister.Db.csproj b/src/SnowflakeId.AutoRegister.Db/SnowflakeId.AutoRegister.Db.csproj index 21e2f8a..b144b16 100644 --- a/src/SnowflakeId.AutoRegister.Db/SnowflakeId.AutoRegister.Db.csproj +++ b/src/SnowflakeId.AutoRegister.Db/SnowflakeId.AutoRegister.Db.csproj @@ -2,7 +2,7 @@ Snowflake;SnowflakeId;AutoRegister;Id AutoRegister; - 1.0.1 + 1.0.2 SnowflakeId AutoRegister Db: A base library for SnowflakeId AutoRegister, providing database support for automatic WorkerId registration in SnowflakeId systems diff --git a/src/SnowflakeId.AutoRegister.Db/Storage/BaseDbStorage.cs b/src/SnowflakeId.AutoRegister.Db/Storage/BaseDbStorage.cs index 9a8ee3b..ffa5f9a 100644 --- a/src/SnowflakeId.AutoRegister.Db/Storage/BaseDbStorage.cs +++ b/src/SnowflakeId.AutoRegister.Db/Storage/BaseDbStorage.cs @@ -18,7 +18,7 @@ protected BaseDbStorage(BaseDbStorageOptions options, ISqlQueryProvider sqlQuery ConnectionFactory = Options.ConnectionFactory ?? DefaultConnectionFactory; } - #region disponse + #region Dispose public void Dispose() { @@ -50,7 +50,7 @@ public bool Exist(string key) public bool Set(string key, string value, int millisecond) { ClearExpiredValues(); - return UseConnection((connection) => + return UseTransaction((connection, transaction) => { var parameters = new { @@ -59,15 +59,13 @@ public bool Set(string key, string value, int millisecond) expireTime = DateTime.Now.AddMilliseconds(millisecond) }; - return connection.Execute(SqlQueryProvider.GetInsertOrUpdateQuery(Options.SchemaName), parameters) > 0; + return connection.Execute(SqlQueryProvider.GetInsertOrUpdateQuery(Options.SchemaName), parameters, transaction) > 0; }); } public bool SetNotExists(string key, string value, int millisecond) { - ClearExpiredValues(); - - return UseConnection((connection) => + return UseTransaction((connection, transaction) => { var parameters = new { @@ -76,17 +74,17 @@ public bool SetNotExists(string key, string value, int millisecond) expireTime = DateTime.Now.AddMilliseconds(millisecond) }; - return connection.Execute(SqlQueryProvider.GetInsertIfNotExistsQuery(Options.SchemaName), parameters) > 0; + return connection.Execute(SqlQueryProvider.GetInsertIfNotExistsQuery(Options.SchemaName), parameters, transaction) > 0; }); } - public bool Expire(string key, int millisecond) + public bool Expire(string key, string value, int millisecond) { return UseConnection((connection) => { var parameters = new { - key, + key, value, expireTime = DateTime.Now.AddMilliseconds(millisecond) }; @@ -94,13 +92,13 @@ public bool Expire(string key, int millisecond) }); } - public Task ExpireAsync(string key, int millisecond) + public Task ExpireAsync(string key, string value, int millisecond) { return UseConnectionAsync(Func); async Task Func(DbConnection connection) { - var parameters = new { key, expireTime = DateTime.Now.AddMilliseconds(millisecond) }; + var parameters = new { key, value, expireTime = DateTime.Now.AddMilliseconds(millisecond) }; return await connection.ExecuteAsync(SqlQueryProvider.GetUpdateExpireQuery(Options.SchemaName), parameters) > 0; } diff --git a/src/SnowflakeId.AutoRegister.MySql/SnowflakeId.AutoRegister.MySql.csproj b/src/SnowflakeId.AutoRegister.MySql/SnowflakeId.AutoRegister.MySql.csproj index bc8a20c..24cddad 100644 --- a/src/SnowflakeId.AutoRegister.MySql/SnowflakeId.AutoRegister.MySql.csproj +++ b/src/SnowflakeId.AutoRegister.MySql/SnowflakeId.AutoRegister.MySql.csproj @@ -2,7 +2,7 @@ Snowflake;SnowflakeId;AutoRegister;Id AutoRegister;MySql - 1.0.2 + 1.0.3 SnowflakeId AutoRegister MySql: An extension of the SnowflakeId AutoRegister library, enabling automatic WorkerId registration in SnowflakeId systems using MySql diff --git a/src/SnowflakeId.AutoRegister.MySql/Storage/MySqlQueryProvider.cs b/src/SnowflakeId.AutoRegister.MySql/Storage/MySqlQueryProvider.cs index f9af3b4..feaf3a2 100644 --- a/src/SnowflakeId.AutoRegister.MySql/Storage/MySqlQueryProvider.cs +++ b/src/SnowflakeId.AutoRegister.MySql/Storage/MySqlQueryProvider.cs @@ -17,16 +17,13 @@ public string GetInsertOrUpdateQuery(string schemaName) => public string GetInsertIfNotExistsQuery(string schemaName) => $""" - INSERT INTO `{schemaName}_RegisterKeyValues` (`Key`, `Value`, `ExpireTime`) - SELECT @key, @value, @expireTime - FROM DUAL - WHERE NOT EXISTS ( - SELECT 1 FROM `{schemaName}_RegisterKeyValues` WHERE `Key` = @key - ); + INSERT IGNORE INTO `{schemaName}_RegisterKeyValues` (`Key`, `Value`, `ExpireTime`) + VALUES (@key, @value, @expireTime); """; + public string GetUpdateExpireQuery(string schemaName) => - $"UPDATE `{schemaName}_RegisterKeyValues` SET `ExpireTime` = @expireTime WHERE `Key` = @key;"; + $"UPDATE `{schemaName}_RegisterKeyValues` SET `ExpireTime` = @expireTime WHERE `Key` = @key AND `Value` = @value;"; public string GetDeleteQuery(string schemaName) => $"DELETE FROM `{schemaName}_RegisterKeyValues` WHERE `Key` = @key;"; diff --git a/src/SnowflakeId.AutoRegister.SqlServer/SnowflakeId.AutoRegister.SqlServer.csproj b/src/SnowflakeId.AutoRegister.SqlServer/SnowflakeId.AutoRegister.SqlServer.csproj index e97bae4..376295c 100644 --- a/src/SnowflakeId.AutoRegister.SqlServer/SnowflakeId.AutoRegister.SqlServer.csproj +++ b/src/SnowflakeId.AutoRegister.SqlServer/SnowflakeId.AutoRegister.SqlServer.csproj @@ -2,7 +2,7 @@ Snowflake;SnowflakeId;AutoRegister;Id AutoRegister;SqlServer - 1.0.4 + 1.0.5 SnowflakeId AutoRegister StackExchangeRedis: An extension of the SnowflakeId AutoRegister library, enabling automatic WorkerId registration in SnowflakeId systems using SqlServer diff --git a/src/SnowflakeId.AutoRegister.SqlServer/Storage/SqlServerQueryProvider.cs b/src/SnowflakeId.AutoRegister.SqlServer/Storage/SqlServerQueryProvider.cs index 23597de..2333ba7 100644 --- a/src/SnowflakeId.AutoRegister.SqlServer/Storage/SqlServerQueryProvider.cs +++ b/src/SnowflakeId.AutoRegister.SqlServer/Storage/SqlServerQueryProvider.cs @@ -10,7 +10,7 @@ public string GetQuery(string schemaName) => public string GetInsertOrUpdateQuery(string schemaName) => $""" - MERGE INTO [{schemaName}].[RegisterKeyValues] AS Target + MERGE INTO [{schemaName}].[RegisterKeyValues] WITH (HOLDLOCK, UPDLOCK) AS Target USING (VALUES (@key, @value, @expireTime)) AS Source ([Key], Value, ExpireTime) ON Target.[Key] = Source.[Key] WHEN MATCHED THEN @@ -23,11 +23,16 @@ public string GetInsertIfNotExistsQuery(string schemaName) => $""" INSERT INTO [{schemaName}].[RegisterKeyValues] ([Key], Value, ExpireTime) SELECT @key, @value, @expireTime - WHERE NOT EXISTS (SELECT 1 FROM [{schemaName}].[RegisterKeyValues] WHERE [Key] = @key); + FROM (SELECT 1 AS tmp) AS t + WHERE NOT EXISTS ( + SELECT 1 + FROM [{schemaName}].[RegisterKeyValues] WITH (UPDLOCK, HOLDLOCK) + WHERE [Key] = @key + ); """; public string GetUpdateExpireQuery(string schemaName) => - $"UPDATE [{schemaName}].[RegisterKeyValues] SET ExpireTime = @expireTime WHERE [Key] = @key"; + $"UPDATE [{schemaName}].[RegisterKeyValues] SET ExpireTime = @expireTime WHERE [Key] = @key AND [Value] = @value"; public string GetDeleteQuery(string schemaName) => $"DELETE FROM [{schemaName}].[RegisterKeyValues] WHERE [Key] = @key"; diff --git a/src/SnowflakeId.AutoRegister.StackExchangeRedis/SnowflakeId.AutoRegister.StackExchangeRedis.csproj b/src/SnowflakeId.AutoRegister.StackExchangeRedis/SnowflakeId.AutoRegister.StackExchangeRedis.csproj index c63a0cd..26f7149 100644 --- a/src/SnowflakeId.AutoRegister.StackExchangeRedis/SnowflakeId.AutoRegister.StackExchangeRedis.csproj +++ b/src/SnowflakeId.AutoRegister.StackExchangeRedis/SnowflakeId.AutoRegister.StackExchangeRedis.csproj @@ -2,7 +2,7 @@ Snowflake;SnowflakeId;AutoRegister;Id AutoRegister;StackExchange.Redis;Redis - 1.0.1 + 1.0.2 SnowflakeId AutoRegister StackExchangeRedis: An extension of the SnowflakeId AutoRegister library, enabling automatic WorkerId registration in SnowflakeId systems using StackExchange.Redis diff --git a/src/SnowflakeId.AutoRegister.StackExchangeRedis/Storage/RedisStorage.cs b/src/SnowflakeId.AutoRegister.StackExchangeRedis/Storage/RedisStorage.cs index 7bb1a79..d558bb4 100644 --- a/src/SnowflakeId.AutoRegister.StackExchangeRedis/Storage/RedisStorage.cs +++ b/src/SnowflakeId.AutoRegister.StackExchangeRedis/Storage/RedisStorage.cs @@ -5,6 +5,16 @@ /// internal class RedisStorage : IStorage { + /// + /// Lua script: Checks if the key's value matches the provided value and sets the expiration time + /// + private const string ExpireScript = @" + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('pexpire', KEYS[1], ARGV[2]) + else + return 0 + end"; + private readonly IConnectionMultiplexer _connection; private readonly RedisStorageOption _storageOption; @@ -46,16 +56,30 @@ public bool SetNotExists(string key, string value, int millisecond) return db.StringSet(_storageOption.InstanceName + key, value, TimeSpan.FromMilliseconds(millisecond), When.NotExists); } - public bool Expire(string key, int millisecond) + public bool Expire(string key, string value, int millisecond) { var db = _connection.GetDatabase(); - return db.KeyExpire(_storageOption.InstanceName + key, TimeSpan.FromMilliseconds(millisecond)); + var redisKey = _storageOption.InstanceName + key; + + var result = (int)db.ScriptEvaluate( + ExpireScript, + [redisKey], + [value, millisecond]); + + return result == 1; } - public Task ExpireAsync(string key, int millisecond) + public async Task ExpireAsync(string key, string value, int millisecond) { var db = _connection.GetDatabase(); - return db.KeyExpireAsync(_storageOption.InstanceName + key, TimeSpan.FromMilliseconds(millisecond)); + var redisKey = _storageOption.InstanceName + key; + + var result = (int)await db.ScriptEvaluateAsync( + ExpireScript, + [redisKey], + [value, millisecond]); + + return result == 1; } public bool Delete(string key) @@ -65,7 +89,7 @@ public bool Delete(string key) } - #region disponse + #region Dispose public void Dispose() { diff --git a/src/SnowflakeId.AutoRegister/Builder/AutoRegisterBuilder.cs b/src/SnowflakeId.AutoRegister/Builder/AutoRegisterBuilder.cs index fd93450..2023219 100644 --- a/src/SnowflakeId.AutoRegister/Builder/AutoRegisterBuilder.cs +++ b/src/SnowflakeId.AutoRegister/Builder/AutoRegisterBuilder.cs @@ -34,6 +34,11 @@ public AutoRegisterBuilder SetRegisterFactory(Func + /// Builds an instance of IAutoRegister using the configured options and storage. + /// + /// An instance of IAutoRegister. + /// Thrown when the storage is not set. public IAutoRegister Build() { if (_store is null) throw new ArgumentNullException(nameof(_store), "Please use UseDefaultStore or UseStore method to set the storage"); @@ -43,6 +48,23 @@ public IAutoRegister Build() return _registerFactory(_store, _registerOption); } + /// + /// Build a SnowflakeId generator. + /// + /// A function to build the SnowflakeId generator. + /// The type of the SnowflakeId generator. + /// An instance of IAutoRegister<T>. + /// Thrown when the storage or registerBuild function is null. + public IAutoRegister Build(Func registerBuild) where T : class + { + if (_store is null) throw new ArgumentNullException(nameof(_store), "Please use UseDefaultStore or UseStore method to set the storage"); + if (registerBuild is null) throw new ArgumentNullException(nameof(registerBuild), "Please provide a registerBuild function"); + + _registerOption.Validate(); + + return new IdGeneratorAutoRegister(_store, _registerOption, registerBuild); + } + #region Storage /// diff --git a/src/SnowflakeId.AutoRegister/Core/AppConst.cs b/src/SnowflakeId.AutoRegister/Core/AppConst.cs new file mode 100644 index 0000000..2a0296a --- /dev/null +++ b/src/SnowflakeId.AutoRegister/Core/AppConst.cs @@ -0,0 +1,11 @@ +namespace SnowflakeId.AutoRegister.Core; + +public class AppConst +{ + internal const string WorkerIdKeyPrefix = "WorkerId:"; + + internal static string WorkerIdFormat(long workerId) + { + return WorkerIdKeyPrefix + workerId; + } +} \ No newline at end of file diff --git a/src/SnowflakeId.AutoRegister/Core/DefaultAutoRegister.cs b/src/SnowflakeId.AutoRegister/Core/DefaultAutoRegister.cs index fb035fa..e8beea4 100644 --- a/src/SnowflakeId.AutoRegister/Core/DefaultAutoRegister.cs +++ b/src/SnowflakeId.AutoRegister/Core/DefaultAutoRegister.cs @@ -7,7 +7,6 @@ namespace SnowflakeId.AutoRegister.Core; /// internal class DefaultAutoRegister : IAutoRegister { - internal const string WorkerIdKeyPrefix = "WorkerId:{0}"; private readonly SemaphoreSlim _semaphore = new(1, 1); public readonly string Identifier; protected readonly SnowflakeIdRegisterOption RegisterOption; @@ -34,9 +33,9 @@ public DefaultAutoRegister(IStorage storage, SnowflakeIdRegisterOption registerO /// The registered Snowflake ID configuration. public virtual SnowflakeIdConfig Register() { - _semaphore.Wait(); try { + _semaphore.Wait(); if (SnowflakeIdConfig != null) return SnowflakeIdConfig; SnowflakeIdConfig = new SnowflakeIdConfig @@ -50,7 +49,7 @@ public virtual SnowflakeIdConfig Register() cancellationToken => ExtendLifeTimeOperation(SnowflakeIdConfig, cancellationToken)); ExtendLifeTimeTask.Start(); - LogManager.Instance.LogInfo($"Register WorkerId:{SnowflakeIdConfig.WorkerId}"); + LogManager.Instance.LogInfo($"Register Success: WorkerId:{SnowflakeIdConfig.WorkerId}"); return SnowflakeIdConfig; } finally @@ -64,9 +63,9 @@ public virtual SnowflakeIdConfig Register() /// public virtual void UnRegister() { - _semaphore.Wait(); try { + _semaphore.Wait(); if (SnowflakeIdConfig == null) return; // First stop the task, then delete the cache @@ -74,9 +73,10 @@ public virtual void UnRegister() ExtendLifeTimeTask?.Stop(); ExtendLifeTimeTask = null; - Clear(SnowflakeIdConfig); + Storage.Delete(AppConst.WorkerIdFormat(SnowflakeIdConfig.WorkerId)); + Storage.Delete(Identifier); - LogManager.Instance.LogInfo($"UnRegister WorkerId:{SnowflakeIdConfig.WorkerId}"); + LogManager.Instance.LogInfo($"UnRegister WorkerId:{SnowflakeIdConfig.WorkerId} Identifier:{Identifier}"); SnowflakeIdConfig = null; } finally @@ -85,7 +85,25 @@ public virtual void UnRegister() } } - protected internal string WorkerIdFormat(long workerId) => string.Format(WorkerIdKeyPrefix, workerId); + /// + /// Resets the current Snowflake ID. + /// + protected virtual void Reset() + { + if (SnowflakeIdConfig == null) return; + + // First stop the task, then delete the cache + // Avoid extending the time again in the task after deleting the Cache first + ExtendLifeTimeTask?.Stop(); + ExtendLifeTimeTask = null; + + Storage.Delete(Identifier); + + LogManager.Instance.LogInfo($"Reset WorkerId:{SnowflakeIdConfig.WorkerId} Identifier:{Identifier}"); + SnowflakeIdConfig = null; + } + + #region WorkerId operation /// /// Run long time task. Extend the WorkerId life cycle @@ -95,15 +113,14 @@ public virtual void UnRegister() /// private async Task ExtendLifeTimeOperation(SnowflakeIdConfig config, CancellationToken cancellationToken) { - // To prevent tasks from executing during logout - await _semaphore.WaitAsync(cancellationToken); - try { + // To prevent tasks from executing during logout + await _semaphore.WaitAsync(cancellationToken); + // The task has been canceled, preventing further execution if (cancellationToken.IsCancellationRequested) { - Clear(config); LogManager.Instance.LogInfo($"Extend WorkerId:{config.WorkerId} Identifier:{Identifier} canceled"); return; } @@ -111,22 +128,23 @@ private async Task ExtendLifeTimeOperation(SnowflakeIdConfig config, Cancellatio LogManager.Instance.LogInfo($"Start extend WorkerId:{config.WorkerId} Identifier:{Identifier} "); // Extend the life of the WorkerId - var key = WorkerIdFormat(config.WorkerId); + var key = AppConst.WorkerIdFormat(config.WorkerId); - var flag = await Storage.ExpireAsync(key, RegisterOption.WorkerIdLifeMillisecond); + // Try to extend the life of the WorkerId + var flag = await Storage.ExpireAsync(key, Identifier, RegisterOption.WorkerIdLifeMillisecond); if (!flag) { - LogManager.Instance.LogWarn($"Extend WorkerId:{config.WorkerId} Identifier:{Identifier} failed,The WorkerId has expired"); + LogManager.Instance.LogWarn($""" + Extend WorkerId:{config.WorkerId} Identifier:{Identifier} failed. + The WorkerId may expired, try to reset + """); // In theory, you shouldn't go here // If the WorkerId is not found in the cache, it means that the WorkerId has expired. // Try to re-register the WorkerId flag = Storage.SetNotExists(key, Identifier, RegisterOption.WorkerIdLifeMillisecond); if (!flag) { - LogManager.Instance.LogFatal($"Extend WorkerId:{config.WorkerId} Identifier:{Identifier - } failed,Cannot be renewed and cannot be reset"); - - // TODO Renewal failed, try to re-register the WorkerId + ExtendFailedOperation(config); return; } } @@ -146,6 +164,19 @@ private async Task ExtendLifeTimeOperation(SnowflakeIdConfig config, Cancellatio } } + /// + /// The operation to be performed when the extension fails. + /// + /// + protected virtual void ExtendFailedOperation(SnowflakeIdConfig config) + { + LogManager.Instance.LogError($""" + Extend WorkerId:{config.WorkerId} Identifier:{Identifier} failed. + Cannot be expire and cannot be reset, UnRegister the WorkerId and next time will be re-registered + """); + Reset(); + } + /// /// Gets the next available worker ID. /// @@ -161,7 +192,7 @@ protected virtual int GetValidWorkerId() if (!string.IsNullOrEmpty(workerIdStr) && int.TryParse(workerIdStr, out var usedWorkerId)) { LogManager.Instance.LogDebug($"Get last WorkerId:{workerIdStr}"); - var key = WorkerIdFormat(usedWorkerId); + var key = AppConst.WorkerIdFormat(usedWorkerId); // Valid WorkerId owned by the current process var value = Storage.Get(key); @@ -169,7 +200,7 @@ protected virtual int GetValidWorkerId() { LogManager.Instance.LogDebug("The WorkerId is still valid"); // Extend the life of the WorkerId - var flag = Storage.Expire(key, RegisterOption.WorkerIdLifeMillisecond); + var flag = Storage.Expire(key, Identifier, RegisterOption.WorkerIdLifeMillisecond); if (flag) { LogManager.Instance.LogDebug("Extend WorkerId life success"); @@ -203,7 +234,7 @@ protected virtual int GetValidWorkerId() for (var workerId = RegisterOption.MinWorkerId; workerId <= RegisterOption.MaxWorkerId; workerId++) { LogManager.Instance.LogTrace($"Try to get WorkerId:{workerId}"); - var key = WorkerIdFormat(workerId); + var key = AppConst.WorkerIdFormat(workerId); if (Storage.Exist(key)) { LogManager.Instance.LogTrace($"{key} already exists"); @@ -233,27 +264,16 @@ protected virtual int GetValidWorkerId() throw new InvalidOperationException("No available worker id."); } - /// - /// Clear resources for the specified configuration. - /// - /// - private void Clear(SnowflakeIdConfig? config) - { - if (config is null) return; - - var key = WorkerIdFormat(config.WorkerId); - Storage.Delete(key); - Storage.Delete(Identifier); - LogManager.Instance.LogInfo($"Cleaned up resources for WorkerId:{config.WorkerId} Identifier:{Identifier}"); - } + #endregion - #region disponse + #region Dispose protected virtual void Dispose(bool disposing) { if (disposing) { UnRegister(); + Storage.Dispose(); } } @@ -263,5 +283,10 @@ public void Dispose() GC.SuppressFinalize(this); } + ~DefaultAutoRegister() + { + Dispose(false); + } + #endregion } \ No newline at end of file diff --git a/src/SnowflakeId.AutoRegister/Core/DefaultStore.cs b/src/SnowflakeId.AutoRegister/Core/DefaultStore.cs index 22b3fe4..d3b1be1 100644 --- a/src/SnowflakeId.AutoRegister/Core/DefaultStore.cs +++ b/src/SnowflakeId.AutoRegister/Core/DefaultStore.cs @@ -27,12 +27,12 @@ public bool SetNotExists(string key, string value, int millisecond) return _store.TryAdd(key, value); } - public bool Expire(string key, int millisecond) + public bool Expire(string key, string value, int millisecond) { return true; } - public Task ExpireAsync(string key, int millisecond) + public Task ExpireAsync(string key, string value, int millisecond) { return Task.FromResult(true); } @@ -42,7 +42,7 @@ public bool Delete(string key) return _store.TryRemove(key, out _); } - #region disponse + #region Dispose public void Dispose() { diff --git a/src/SnowflakeId.AutoRegister/Core/IdGeneratorAutoRegister.cs b/src/SnowflakeId.AutoRegister/Core/IdGeneratorAutoRegister.cs new file mode 100644 index 0000000..160d98f --- /dev/null +++ b/src/SnowflakeId.AutoRegister/Core/IdGeneratorAutoRegister.cs @@ -0,0 +1,99 @@ +namespace SnowflakeId.AutoRegister.Core; + +/// +/// Class for auto-registering ID generators. +/// +internal class IdGeneratorAutoRegister : DefaultAutoRegister, IAutoRegister where T : class +{ + private readonly object _lock = new(); + private readonly Func _registerBuild; + private T? _idGenerator; + + /// + /// Initializes a new instance of the class. + /// + /// The storage instance. + /// The registration options. + /// The function to build the ID generator. + public IdGeneratorAutoRegister(IStorage storage, SnowflakeIdRegisterOption registerOption, Func? registerBuild) + : base(storage, registerOption) + { + _registerBuild = registerBuild ?? throw new ArgumentNullException(nameof(registerBuild)); + } + + public SnowflakeIdConfig? GetSnowflakeIdConfig() + { + return SnowflakeIdConfig; + } + + /// + /// Gets the ID generator instance. + /// + /// The ID generator instance. + public T GetIdGenerator() + { + lock (_lock) + { + if (_idGenerator != null) return _idGenerator; + + _idGenerator = _registerBuild(base.Register()); + return _idGenerator; + } + } + + /// + /// Unregisters the ID generator. + /// + public void UnRegisterIdGenerator() + { + UnRegister(); + } + + /// + /// Unregisters the current Snowflake ID. + /// + public override void UnRegister() + { + lock (_lock) + { + if (_idGenerator is null) return; + + base.UnRegister(); + _idGenerator = null; + } + } + + /// + /// Resets the ID generator. + /// + protected override void Reset() + { + lock (_lock) + { + base.Reset(); + _idGenerator = null; + } + } + + #region Dispose + + /// + /// Disposes the resources used by the instance. + /// + /// Indicates whether the method is called from Dispose. + protected override void Dispose(bool disposing) + { + if (disposing) + { + base.Dispose(disposing); + UnRegister(); + } + } + + ~IdGeneratorAutoRegister() + { + Dispose(false); + } + + #endregion +} \ No newline at end of file diff --git a/src/SnowflakeId.AutoRegister/GlobalUsings.cs b/src/SnowflakeId.AutoRegister/GlobalUsings.cs index b888cf6..061fad6 100644 --- a/src/SnowflakeId.AutoRegister/GlobalUsings.cs +++ b/src/SnowflakeId.AutoRegister/GlobalUsings.cs @@ -6,4 +6,7 @@ global using SnowflakeId.AutoRegister.Logging; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("SnowflakeId.AutoRegister.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("SnowflakeId.AutoRegister.Tests")] +[assembly: InternalsVisibleTo("SnowflakeId.AutoRegister.Tests.Redis")] +[assembly: InternalsVisibleTo("SnowflakeId.AutoRegister.Tests.MySql")] +[assembly: InternalsVisibleTo("SnowflakeId.AutoRegister.Tests.SqlServer")] \ No newline at end of file diff --git a/src/SnowflakeId.AutoRegister/Interfaces/IAutoRegister.cs b/src/SnowflakeId.AutoRegister/Interfaces/IAutoRegister.cs index c155121..6a4cf8f 100644 --- a/src/SnowflakeId.AutoRegister/Interfaces/IAutoRegister.cs +++ b/src/SnowflakeId.AutoRegister/Interfaces/IAutoRegister.cs @@ -15,4 +15,30 @@ public interface IAutoRegister : IDisposable /// UnRegister the SnowflakeIdConfig. /// void UnRegister(); +} + +/// +/// Used to build the Snowflake ID library
+/// Defines an interface for automatic registration. +///
+/// The type of the SnowflakeId generator. +public interface IAutoRegister : IDisposable where T : class +{ + /// + /// Retrieves the configuration for the SnowflakeId generator. + /// This method should be called after to obtain the relevant configuration. + /// + /// A instance, or null if the generator is not initialized. + SnowflakeIdConfig? GetSnowflakeIdConfig(); + + /// + /// Provides an instance of the SnowflakeId generator. + /// + /// An instance of the SnowflakeId generator of type . + T GetIdGenerator(); + + /// + /// Unregisters the SnowflakeIdConfig. + /// + void UnRegisterIdGenerator(); } \ No newline at end of file diff --git a/src/SnowflakeId.AutoRegister/Interfaces/IStorage.cs b/src/SnowflakeId.AutoRegister/Interfaces/IStorage.cs index 36a619e..4a12d99 100644 --- a/src/SnowflakeId.AutoRegister/Interfaces/IStorage.cs +++ b/src/SnowflakeId.AutoRegister/Interfaces/IStorage.cs @@ -5,13 +5,16 @@ /// public interface IStorage : IDisposable { + /// + /// Checks if the specified key exists in the storage. + /// bool Exist(string key); /// - /// Gets the value associated with the specified key. + /// Retrieves the value associated with the specified key. /// - /// The key of the value to get. - /// The value associated with the specified key, if the key is found; otherwise, . + /// The key whose associated value is to be retrieved. + /// The value associated with the specified key, or if the key is not found. string? Get(string key); /// @@ -31,18 +34,20 @@ public interface IStorage : IDisposable bool SetNotExists(string key, string value, int millisecond); /// - /// Sets the time to live in milliseconds of the specified key. + /// Sets the time to live in milliseconds for the specified key. /// /// The key of the value to expire. + /// The value associated with the key. /// The life of the value in milliseconds. - bool Expire(string key, int millisecond); + bool Expire(string key, string value, int millisecond); /// - /// Sets the time to live in milliseconds of the specified key. + /// Sets the time to live in milliseconds for the specified key asynchronously. /// /// The key of the value to expire. + /// The value associated with the key. /// The life of the value in milliseconds. - Task ExpireAsync(string key, int millisecond); + Task ExpireAsync(string key, string value, int millisecond); /// /// Deletes the specified key. diff --git a/src/SnowflakeId.AutoRegister/SnowflakeId.AutoRegister.csproj b/src/SnowflakeId.AutoRegister/SnowflakeId.AutoRegister.csproj index f937deb..924abdf 100644 --- a/src/SnowflakeId.AutoRegister/SnowflakeId.AutoRegister.csproj +++ b/src/SnowflakeId.AutoRegister/SnowflakeId.AutoRegister.csproj @@ -2,7 +2,7 @@ Snowflake;SnowflakeId;AutoRegister;Id AutoRegister - 1.0.3 + 1.0.4 A C# library for automatic registration of WorkerId in SnowflakeId systems,supporting SQL Server, Redis , and other. diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 35b1320..33d1ded 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,7 +1,7 @@  false - net48;net6.0; + net6.0; default enable diff --git a/test/SnowflakeId.AutoRegister.Tests.MySql/IdGeneratorTests/YitterIdGeneratorTest.cs b/test/SnowflakeId.AutoRegister.Tests.MySql/IdGeneratorTests/YitterIdGeneratorTest.cs new file mode 100644 index 0000000..fc90a75 --- /dev/null +++ b/test/SnowflakeId.AutoRegister.Tests.MySql/IdGeneratorTests/YitterIdGeneratorTest.cs @@ -0,0 +1,36 @@ +using SnowflakeId.AutoRegister.Tests.IdGeneratorTests; + +namespace SnowflakeId.AutoRegister.Tests.MySql.IdGeneratorTests; + +[TestSubject(typeof(MySqlStorage))] +public class YitterIdGeneratorTest : BaseYitterIdGeneratorTest +{ + public YitterIdGeneratorTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + SetRegisterBuild = builder => builder.UseMySqlStore(ConnectionString); + } + + [Fact] + protected override void Test_Register() + { + base.Test_Register(); + } + + [Fact] + protected override void Test_MultipleRegister() + { + base.Test_MultipleRegister(); + } + + [Fact] + protected override Task Test_MultipleConcurrentRegistrations() + { + return base.Test_MultipleConcurrentRegistrations(); + } + + [Fact] + protected override void Test_WorkerId_Own_Scramble() + { + base.Test_WorkerId_Own_Scramble(); + } +} \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests.Redis/Examples/RedisAutoRegister.cs b/test/SnowflakeId.AutoRegister.Tests.Redis/Examples/RedisAutoRegister.cs index faa9302..ecf6bae 100644 --- a/test/SnowflakeId.AutoRegister.Tests.Redis/Examples/RedisAutoRegister.cs +++ b/test/SnowflakeId.AutoRegister.Tests.Redis/Examples/RedisAutoRegister.cs @@ -1,14 +1,13 @@ namespace SnowflakeId.AutoRegister.Tests.Redis.Examples; [TestSubject(typeof(RedisStorage))] -public class RedisAutoRegister : TestBaseAutoRegister +public class RedisAutoRegister : TestBaseAutoRegister, IClassFixture { - private static readonly ConnectionMultiplexer _connection = ConnectionMultiplexer.Connect(ConnectionString); - - public RedisAutoRegister(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + public RedisAutoRegister(ITestOutputHelper testOutputHelper, RedisFixture redisFixture) + : base(testOutputHelper) { SetRegisterBuild = builder => builder - .UseRedisStore(option => { option.ConnectionMultiplexerFactory = () => _connection; }); + .UseRedisStore(option => { option.ConnectionMultiplexerFactory = () => redisFixture.Connection; }); } [Fact] diff --git a/test/SnowflakeId.AutoRegister.Tests.Redis/IdGeneratorTests/YitterIdGeneratorTest.cs b/test/SnowflakeId.AutoRegister.Tests.Redis/IdGeneratorTests/YitterIdGeneratorTest.cs new file mode 100644 index 0000000..c831151 --- /dev/null +++ b/test/SnowflakeId.AutoRegister.Tests.Redis/IdGeneratorTests/YitterIdGeneratorTest.cs @@ -0,0 +1,36 @@ +using SnowflakeId.AutoRegister.Tests.IdGeneratorTests; + +namespace SnowflakeId.AutoRegister.Tests.Redis.IdGeneratorTests; + +[TestSubject(typeof(RedisStorage))] +public class YitterIdGeneratorTest : BaseYitterIdGeneratorTest, IClassFixture +{ + public YitterIdGeneratorTest(ITestOutputHelper testOutputHelper, RedisFixture redisFixture) : base(testOutputHelper) + { + SetRegisterBuild = builder => builder.UseRedisStore(option => { option.ConnectionMultiplexerFactory = () => redisFixture.Connection; }); + } + + [Fact] + protected override void Test_Register() + { + base.Test_Register(); + } + + [Fact] + protected override void Test_MultipleRegister() + { + base.Test_MultipleRegister(); + } + + [Fact] + protected override Task Test_MultipleConcurrentRegistrations() + { + return base.Test_MultipleConcurrentRegistrations(); + } + + [Fact] + protected override void Test_WorkerId_Own_Scramble() + { + base.Test_WorkerId_Own_Scramble(); + } +} \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests.Redis/RedisTestConfig.cs b/test/SnowflakeId.AutoRegister.Tests.Redis/RedisTestConfig.cs index 7e81314..c1af46d 100644 --- a/test/SnowflakeId.AutoRegister.Tests.Redis/RedisTestConfig.cs +++ b/test/SnowflakeId.AutoRegister.Tests.Redis/RedisTestConfig.cs @@ -1,6 +1,24 @@ -namespace SnowflakeId.AutoRegister.Tests.Redis; +using System; + +namespace SnowflakeId.AutoRegister.Tests.Redis; public class RedisTestConfig { public const string ConnectionString = "localhost:6379,allowAdmin=true"; +} + +public class RedisFixture : IDisposable +{ + public ConnectionMultiplexer Connection { get; } = ConnectionMultiplexer.Connect(ConnectionString); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) Connection.Dispose(); + } } \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests.SqlServer/Examples/SqlServerAutoRegisterTest.cs b/test/SnowflakeId.AutoRegister.Tests.SqlServer/Examples/SqlServerAutoRegisterTest.cs index 837b41b..a510ed8 100644 --- a/test/SnowflakeId.AutoRegister.Tests.SqlServer/Examples/SqlServerAutoRegisterTest.cs +++ b/test/SnowflakeId.AutoRegister.Tests.SqlServer/Examples/SqlServerAutoRegisterTest.cs @@ -1,11 +1,12 @@ namespace SnowflakeId.AutoRegister.Tests.SqlServer.Examples; [TestSubject(typeof(SqlServerStorage))] -public class SqlServerAutoRegisterTest : TestBaseAutoRegister +public class SqlServerAutoRegisterTest : TestBaseAutoRegister, IClassFixture { - public SqlServerAutoRegisterTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + public SqlServerAutoRegisterTest(ITestOutputHelper testOutputHelper, SqlServerFixture sqlServerFixture) : base(testOutputHelper) { SetRegisterBuild = builder => builder.UseSqlServerStore(ConnectionString); + testOutputHelper.WriteLine("Count: " + sqlServerFixture.GetCount()); } [Fact] diff --git a/test/SnowflakeId.AutoRegister.Tests.SqlServer/IdGeneratorTests/YitterIdGeneratorTest.cs b/test/SnowflakeId.AutoRegister.Tests.SqlServer/IdGeneratorTests/YitterIdGeneratorTest.cs new file mode 100644 index 0000000..553c70d --- /dev/null +++ b/test/SnowflakeId.AutoRegister.Tests.SqlServer/IdGeneratorTests/YitterIdGeneratorTest.cs @@ -0,0 +1,37 @@ +using SnowflakeId.AutoRegister.Tests.IdGeneratorTests; + +namespace SnowflakeId.AutoRegister.Tests.SqlServer.IdGeneratorTests; + +[TestSubject(typeof(SqlServerStorage))] +public class YitterIdGeneratorTest : BaseYitterIdGeneratorTest, IClassFixture +{ + public YitterIdGeneratorTest(ITestOutputHelper testOutputHelper, SqlServerFixture sqlServerFixture) : base(testOutputHelper) + { + SetRegisterBuild = builder => builder.UseSqlServerStore(ConnectionString); + testOutputHelper.WriteLine("Count: " + sqlServerFixture.GetCount()); + } + + [Fact] + protected override void Test_Register() + { + base.Test_Register(); + } + + [Fact] + protected override void Test_MultipleRegister() + { + base.Test_MultipleRegister(); + } + + [Fact] + protected override Task Test_MultipleConcurrentRegistrations() + { + return base.Test_MultipleConcurrentRegistrations(); + } + + [Fact] + protected override void Test_WorkerId_Own_Scramble() + { + base.Test_WorkerId_Own_Scramble(); + } +} \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests.SqlServer/SqlServerTestConfig.cs b/test/SnowflakeId.AutoRegister.Tests.SqlServer/SqlServerTestConfig.cs index 177cd07..3b770e1 100644 --- a/test/SnowflakeId.AutoRegister.Tests.SqlServer/SqlServerTestConfig.cs +++ b/test/SnowflakeId.AutoRegister.Tests.SqlServer/SqlServerTestConfig.cs @@ -1,6 +1,64 @@ -namespace SnowflakeId.AutoRegister.Tests.SqlServer; +using System; +using Microsoft.Data.SqlClient; +using SnowflakeId.AutoRegister.Db.Configs; + +namespace SnowflakeId.AutoRegister.Tests.SqlServer; public class SqlServerTestConfig { public const string ConnectionString = "Server=localhost;Database=SnowflakeTest;Integrated Security=SSPI;TrustServerCertificate=true;"; +} + +public class SqlServerFixture : IDisposable +{ + public SqlServerFixture() + { + Connection = new SqlConnection(ConnectionString); + Connection.Open(); + } + + public SqlConnection Connection { get; } + + public void Dispose() + { + Connection.Close(); + Connection.Dispose(); + } + + private bool DoesTableExist(string tableName, string schemaName = "dbo") + { + using var cmd = Connection.CreateCommand(); + cmd.CommandText = """ + SELECT COUNT(*) + FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + WHERE t.name = @TableName + AND s.name = @SchemaName + """; + cmd.Parameters.AddWithValue("@SchemaName", schemaName); + cmd.Parameters.AddWithValue("@TableName", tableName); + + var count = Convert.ToInt32(cmd.ExecuteScalar()); + return count > 0; + } + + public void ClearSnowflakeTable() + { + var tableName = $"[{BaseDbStorageOptions.DefaultSchema}].[RegisterKeyValues]"; + if (!DoesTableExist(tableName, BaseDbStorageOptions.DefaultSchema)) return; + + using var cmd = Connection.CreateCommand(); + cmd.CommandText = $"TRUNCATE TABLE [{BaseDbStorageOptions.DefaultSchema}].[RegisterKeyValues]"; + cmd.ExecuteNonQuery(); + } + + public int GetCount() + { + var tableName = $"[{BaseDbStorageOptions.DefaultSchema}].[RegisterKeyValues]"; + if (!DoesTableExist(tableName, BaseDbStorageOptions.DefaultSchema)) return 0; + + using var cmd = Connection.CreateCommand(); + cmd.CommandText = $"SELECT COUNT(*) FROM [{BaseDbStorageOptions.DefaultSchema}].[RegisterKeyValues]"; + return Convert.ToInt32(cmd.ExecuteScalar()); + } } \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/Base/TestBaseAutoRegister.cs b/test/SnowflakeId.AutoRegister.Tests/Base/TestBaseAutoRegister.cs index cfa50b9..362d54f 100644 --- a/test/SnowflakeId.AutoRegister.Tests/Base/TestBaseAutoRegister.cs +++ b/test/SnowflakeId.AutoRegister.Tests/Base/TestBaseAutoRegister.cs @@ -1,11 +1,19 @@ namespace SnowflakeId.AutoRegister.Tests.Base; +[Collection("Non-Parallel Collection")] +[TestSubject(typeof(IStorage))] +[TestSubject(typeof(IAutoRegister))] +[TestSubject(typeof(AutoRegisterBuilder))] public class TestBaseAutoRegister { + private readonly ITestOutputHelper _testOutputHelper; + protected TestBaseAutoRegister(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; SetRegisterBuild = builder => builder; LogAction = testOutputHelper.GetLogAction(); + _testOutputHelper.WriteLine($"Current Test: [{Environment.CurrentManagedThreadId}]"); } protected Func SetRegisterBuild { get; set; } @@ -49,10 +57,10 @@ protected virtual async Task Test_StorageAsync() var getValue = storage.Get(key); Assert.Equal(value, getValue); - var expire = storage.Expire(key, millisecond * 10); + var expire = storage.Expire(key, value, millisecond * 10); Assert.True(expire); - expire = await storage.ExpireAsync(key, millisecond * 10); + expire = await storage.ExpireAsync(key, value, millisecond * 10); Assert.True(expire); var delete = storage.Delete(key); @@ -103,10 +111,22 @@ protected virtual async Task Test_MultipleConcurrentRegistrations() Assert.NotEqual(0, workerId); } - // dispose all + Assert.NotNull(storage); + + // Unregister all foreach (var register in registers) { - register.Dispose(); + register.UnRegister(); + } + + foreach (var task in tasks) + { + //Wait for the WorkerId to be deleted + while (storage.Exist(AppConst.WorkerIdFormat(task.Result.WorkerId))) + { + _testOutputHelper.WriteLine("[Test_MultipleConcurrentRegistrations] Wait for WorkerId to expire"); + Thread.Sleep(101); + } } // check all keys are expired @@ -115,6 +135,9 @@ protected virtual async Task Test_MultipleConcurrentRegistrations() var flag = storage?.Exist($@"WorkerId:{task.Result.WorkerId}"); Assert.False(flag); } + + // dispose all + foreach (var register in registers) register.Dispose(); } protected virtual void Test_WorkerId_Own() @@ -160,7 +183,7 @@ protected virtual void Test_WorkerId_Expired() var identifier = storage?.Get("WorkerId:" + idConfig.WorkerId); Assert.Equal(idConfig.Identifier, identifier); - // Sleep for a while to expire the worker id + // Sleep for a while, is it expired? Thread.Sleep(901); // Get the worker id @@ -178,7 +201,7 @@ protected virtual void Test_WorkerId_Expired() protected virtual void Test_WorkerId_Own_Scramble() { var builder = GetAutoRegisterBuilder() - .SetWorkerIdLifeMillisecond(9000) + .SetWorkerIdLifeMillisecond(900) .SetExtraIdentifier(GetType().FullName + nameof(Test_WorkerId_Own_Scramble) + "1"); var storage = builder.Storage; Assert.NotNull(storage); @@ -189,18 +212,21 @@ protected virtual void Test_WorkerId_Own_Scramble() Assert.NotEqual(0, idConfig.WorkerId); Assert.NotEmpty(idConfig.Identifier); - Assert.True(storage.Exist(register.WorkerIdFormat(idConfig.WorkerId))); + Assert.True(storage.Exist(AppConst.WorkerIdFormat(idConfig.WorkerId))); Assert.True(storage.Exist(idConfig.Identifier)); //Actively delete key,Simulate fake death problem Assert.NotNull(register.ExtendLifeTimeTask); register.ExtendLifeTimeTask.Stop(); - storage.Delete(register.WorkerIdFormat(idConfig.WorkerId)); - storage.Delete(register.Identifier); + while (storage.Exist(AppConst.WorkerIdFormat(idConfig.WorkerId))) + { + _testOutputHelper.WriteLine("[Test_WorkerId_Own_Scramble] Wait for WorkerId to expire"); + Thread.Sleep(301); + } var builder2 = GetAutoRegisterBuilder() - .SetWorkerIdLifeMillisecond(9000) + .SetWorkerIdLifeMillisecond(900) .SetExtraIdentifier(GetType().FullName + nameof(Test_WorkerId_Own_Scramble) + "2"); using var register2 = (DefaultAutoRegister)GetAutoRegister(builder2); var idConfig2 = register2.Register(); @@ -218,10 +244,14 @@ protected virtual void Test_WorkerId_Own_Scramble() Assert.NotNull(register2.ExtendLifeTimeTask); register.ExtendLifeTimeTask.Start(); - Thread.Sleep(300); + while (storage.Exist(idConfig.Identifier)) + { + _testOutputHelper.WriteLine("Wait for Process 1 to reset"); + Thread.Sleep(100); + } // Because process 1 starts to recover, WorkId is still held by process 2, and process 1 also marks that it also has WorkId - Assert.True(storage.Exist(idConfig.Identifier)); - Assert.Equal(idConfig2.Identifier, storage.Get(register.WorkerIdFormat(idConfig2.WorkerId))); + Assert.False(storage.Exist(idConfig.Identifier)); + Assert.Equal(idConfig2.Identifier, storage.Get(AppConst.WorkerIdFormat(idConfig2.WorkerId))); } } \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/Base/TestLog.cs b/test/SnowflakeId.AutoRegister.Tests/Base/TestLog.cs index 15f4878..d782aea 100644 --- a/test/SnowflakeId.AutoRegister.Tests/Base/TestLog.cs +++ b/test/SnowflakeId.AutoRegister.Tests/Base/TestLog.cs @@ -1,4 +1,6 @@ -namespace SnowflakeId.AutoRegister.Tests.Base; +using System.Diagnostics; + +namespace SnowflakeId.AutoRegister.Tests.Base; public static class TestLog { @@ -6,6 +8,17 @@ public static class TestLog { if (testOutputHelper is null) return default; - return (logLevel, message, exception) => { testOutputHelper.WriteLine($"{logLevel}: {message} {Environment.NewLine} {exception}"); }; + return (logLevel, message, exception) => + { + try + { + testOutputHelper.WriteLine($"[{Environment.CurrentManagedThreadId}]{logLevel}: {message} {Environment.NewLine} {exception}"); + } + catch (Exception ex) + { + // Fallback to console or another logging mechanism + Trace.WriteLine($"Logging failed: {ex.Message}"); + } + }; } } \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/Core/NonParallelCollectionDefinition.cs b/test/SnowflakeId.AutoRegister.Tests/Core/NonParallelCollectionDefinition.cs new file mode 100644 index 0000000..1babc8b --- /dev/null +++ b/test/SnowflakeId.AutoRegister.Tests/Core/NonParallelCollectionDefinition.cs @@ -0,0 +1,9 @@ +namespace SnowflakeId.AutoRegister.Tests.Core; + +/// +/// Collection definition to disable parallelization. +/// +[CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)] +public class NonParallelCollectionDefinition +{ +} \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/GlobalUsings.cs b/test/SnowflakeId.AutoRegister.Tests/GlobalUsings.cs index 6b5faac..055779d 100644 --- a/test/SnowflakeId.AutoRegister.Tests/GlobalUsings.cs +++ b/test/SnowflakeId.AutoRegister.Tests/GlobalUsings.cs @@ -10,6 +10,7 @@ global using SnowflakeId.AutoRegister.Core; global using SnowflakeId.AutoRegister.Interfaces; global using SnowflakeId.AutoRegister.Logging; +global using SnowflakeId.AutoRegister.Tests.Base; global using SnowflakeId.AutoRegister.Util; global using Xunit; global using Xunit.Abstractions; \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/IdGeneratorTests/BaseYitterIdGeneratorTest.cs b/test/SnowflakeId.AutoRegister.Tests/IdGeneratorTests/BaseYitterIdGeneratorTest.cs new file mode 100644 index 0000000..ec3a715 --- /dev/null +++ b/test/SnowflakeId.AutoRegister.Tests/IdGeneratorTests/BaseYitterIdGeneratorTest.cs @@ -0,0 +1,233 @@ +using Yitter.IdGenerator; + +namespace SnowflakeId.AutoRegister.Tests.IdGeneratorTests; + +[Collection("Non-Parallel Collection")] +[TestSubject(typeof(IAutoRegister<>))] +public class BaseYitterIdGeneratorTest +{ + private readonly ITestOutputHelper _testOutputHelper; + + protected BaseYitterIdGeneratorTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + SetRegisterBuild = builder => builder; + LogAction = testOutputHelper.GetLogAction(); + _testOutputHelper.WriteLine($"Current Test: [{Environment.CurrentManagedThreadId}]"); + } + + protected Func SetRegisterBuild { get; set; } + protected Action? LogAction { get; set; } + + protected AutoRegisterBuilder GetAutoRegisterBuilder(AutoRegisterBuilder? builder = null) + { + builder ??= new AutoRegisterBuilder(); + return SetRegisterBuild(builder) + .SetLogger(LogAction) + .SetLogMinimumLevel(LogLevel.Trace); + } + + protected IAutoRegister GetAutoRegister(AutoRegisterBuilder? builder = null) + { + return GetAutoRegisterBuilder(builder) + .Build(config => new DefaultIdGenerator(new IdGeneratorOptions + { + WorkerId = (ushort)config.WorkerId + })); + } + + protected virtual void Test_Register() + { + var builder = GetAutoRegisterBuilder() + .SetExtraIdentifier(GetType().FullName + nameof(Test_Register)); + var storage = builder.Storage; + Assert.NotNull(storage); + + using var autoRegister = GetAutoRegister(builder); + + var idGenerator = autoRegister.GetIdGenerator(); + Assert.NotNull(idGenerator); + + Assert.True(autoRegister.GetIdGenerator().NewLong() > 0); + + var idConfig = autoRegister.GetSnowflakeIdConfig(); + Assert.NotNull(idConfig); + + // Get the worker id + var workerId = storage.Get(idConfig.Identifier); + Assert.Equal(idConfig.WorkerId.ToString(), workerId); + + // Get the identifier + var identifier = storage.Get("WorkerId:" + idConfig.WorkerId); + Assert.Equal(idConfig.Identifier, identifier); + } + + protected virtual void Test_MultipleRegister() + { + var builder = GetAutoRegisterBuilder() + .SetExtraIdentifier(GetType().FullName + nameof(Test_MultipleRegister)); + var storage = builder.Storage; + Assert.NotNull(storage); + + using var autoRegister = GetAutoRegister(builder); + + var idGenerator = autoRegister.GetIdGenerator(); + Assert.NotNull(idGenerator); + + var idGenerator2 = autoRegister.GetIdGenerator(); + Assert.NotNull(idGenerator2); + + Assert.Same(idGenerator, idGenerator2); + Assert.True(autoRegister.GetIdGenerator().NewLong() > 0); + + var idConfig = autoRegister.GetSnowflakeIdConfig(); + Assert.NotNull(idConfig); + + // Get the worker id + var workerId = storage.Get(idConfig.Identifier); + Assert.Equal(idConfig.WorkerId.ToString(), workerId); + + // Get the identifier + var identifier = storage.Get("WorkerId:" + idConfig.WorkerId); + Assert.Equal(idConfig.Identifier, identifier); + } + + protected virtual async Task Test_MultipleConcurrentRegistrations() + { + IStorage? storage = null; + var registers = new IAutoRegister[5]; + var tasks = new Task[5]; + + for (var i = 0; i < 5; i++) + { + var builder = GetAutoRegisterBuilder() + .SetExtraIdentifier(GetType().FullName + nameof(Test_MultipleConcurrentRegistrations) + i); + storage ??= builder.Storage; + + var idAutoRegister = GetAutoRegister(builder); + + registers[i] = idAutoRegister; + tasks[i] = Task.Run(() => + { + idAutoRegister.GetIdGenerator(); + return idAutoRegister.GetSnowflakeIdConfig() ?? throw new ArgumentNullException(nameof(idAutoRegister.GetSnowflakeIdConfig)); + }); + } + + await Task.WhenAll(tasks); + + foreach (var task in tasks) + { + Assert.Null(task.Exception); + Assert.NotNull(task.Result); + } + + var workerIds = tasks.Select(x => x.Result.WorkerId).ToArray(); + Assert.Equal(tasks.Length, workerIds.Length); + // Assert all worker ids are unique + Assert.Equal(workerIds.Length, workerIds.Distinct().Count()); + + foreach (var workerId in workerIds) Assert.NotEqual(0, workerId); + + Assert.NotNull(storage); + + // Unregister all + foreach (var register in registers) + { + register.UnRegisterIdGenerator(); + + //Wait for the WorkerId to be deleted + while (storage.Exist(AppConst.WorkerIdFormat(register.GetSnowflakeIdConfig()?.WorkerId ?? 0))) + { + _testOutputHelper.WriteLine("[Test_MultipleConcurrentRegistrations] Wait for WorkerId to expire"); + Thread.Sleep(101); + } + } + + // check all keys are expired + foreach (var task in tasks) + { + var flag = storage.Exist($@"WorkerId:{task.Result.WorkerId}"); + Assert.False(flag); + } + + // dispose all + foreach (var register in registers) register.Dispose(); + } + + /// + /// https://github.com/LemonNoCry/SnowflakeId.AutoRegister/issues/2 + /// + protected virtual void Test_WorkerId_Own_Scramble() + { + var builder = GetAutoRegisterBuilder() + .SetWorkerIdLifeMillisecond(900) + .SetExtraIdentifier(GetType().FullName + nameof(Test_WorkerId_Own_Scramble) + "1"); + var storage = builder.Storage; + Assert.NotNull(storage); + + _testOutputHelper.WriteLine("WorkId Value: " + storage.Get(AppConst.WorkerIdFormat(1))); + + using var register = GetAutoRegister(builder); + var idGenerator = register.GetIdGenerator(); + Assert.NotNull(idGenerator); + + var idConfig = register.GetSnowflakeIdConfig(); + Assert.NotNull(idConfig); + Assert.Equal(1, idConfig.WorkerId); + + //Actively delete key,Simulate fake death problem + var dar = register as DefaultAutoRegister; + Assert.NotNull(dar); + Assert.NotNull(dar.ExtendLifeTimeTask); + dar.ExtendLifeTimeTask.Stop(); + + while (storage.Exist(AppConst.WorkerIdFormat(idConfig.WorkerId))) + { + _testOutputHelper.WriteLine("[BaseYitterIdGeneratorTest.Test_WorkerId_Own_Scramble] Wait for WorkerId to expire"); + Thread.Sleep(301); + } + + var builder2 = GetAutoRegisterBuilder() + .SetWorkerIdLifeMillisecond(900) + .SetExtraIdentifier(GetType().FullName + nameof(Test_WorkerId_Own_Scramble) + "2"); + + using var register2 = GetAutoRegister(builder2); + var idGenerator2 = register2.GetIdGenerator(); + Assert.NotNull(idGenerator2); + + var idConfig2 = register2.GetSnowflakeIdConfig(); + Assert.NotNull(idConfig2); + + //Because process 1 is in a "pseudo-dead state" or other situations where renewal is not possible, process 2 obtains the same WorkId as process 1. + Assert.Equal(idConfig.WorkerId, idConfig2.WorkerId); + Assert.NotEqual(idConfig.Identifier, idConfig2.Identifier); + + // Process 1 recovers the life cycle of the WorkerId. + // Because process 2 has the WorkerId, process 1 will regenerate its WorkerId. + dar.ExtendLifeTimeTask.Start(); + + while (register.GetSnowflakeIdConfig() != null) + { + _testOutputHelper.WriteLine("Wait for Process 1 to reset"); + Thread.Sleep(101); + } + + var idGenerator3 = register.GetIdGenerator(); + Assert.NotNull(idGenerator3); + Assert.NotSame(idGenerator, idGenerator3); + + var idConfig3 = register.GetSnowflakeIdConfig(); + Assert.NotNull(idConfig3); + Assert.Equal(idConfig.Identifier, idConfig3.Identifier); + Assert.NotEqual(idConfig.WorkerId, idConfig3.WorkerId); + + //Check Store + Assert.True(storage.Exist(idConfig.Identifier)); + Assert.True(storage.Exist(idConfig2.Identifier)); + Assert.True(storage.Exist(AppConst.WorkerIdFormat(idConfig2.WorkerId))); + Assert.True(storage.Exist(AppConst.WorkerIdFormat(idConfig3.WorkerId))); + Assert.Equal(idConfig2.Identifier, storage.Get(AppConst.WorkerIdFormat(idConfig2.WorkerId))); + Assert.Equal(idConfig3.Identifier, storage.Get(AppConst.WorkerIdFormat(idConfig3.WorkerId))); + } +} \ No newline at end of file diff --git a/test/SnowflakeId.AutoRegister.Tests/SnowflakeId.AutoRegister.Tests.csproj b/test/SnowflakeId.AutoRegister.Tests/SnowflakeId.AutoRegister.Tests.csproj index 6c45882..985d2e6 100644 --- a/test/SnowflakeId.AutoRegister.Tests/SnowflakeId.AutoRegister.Tests.csproj +++ b/test/SnowflakeId.AutoRegister.Tests/SnowflakeId.AutoRegister.Tests.csproj @@ -7,4 +7,8 @@ + + + + \ No newline at end of file