diff --git a/.gitignore b/.gitignore index 3ad5fdf..9960815 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ obj/ bin/ /.idea/.idea.CAP /.idea/.idea.CAP +/.idea diff --git a/CAP.sln b/CAP.sln index 26769fb..35a9e3d 100644 --- a/CAP.sln +++ b/CAP.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.14 +VisualStudioVersion = 15.0.26430.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B2AE124-6636-4DE9-83A3-70360DABD0C4}" EndProject @@ -33,8 +33,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{9E5A7F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP", "src\DotNetCore.CAP\DotNetCore.CAP.csproj", "{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.EntityFrameworkCore", "src\DotNetCore.CAP.EntityFrameworkCore\DotNetCore.CAP.EntityFrameworkCore.csproj", "{96111249-C4C3-4DC9-A887-32D583723AB1}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3A6B6931-A123-477A-9469-8B468B5385AF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka", "samples\Sample.Kafka\Sample.Kafka.csproj", "{2F095ED9-5BC9-4512-9013-A47685FB2508}" @@ -55,10 +53,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{10C0818D build\version.props = build\version.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.EntityFrameworkCore.Test", "test\DotNetCore.CAP.EntityFrameworkCore.Test\DotNetCore.CAP.EntityFrameworkCore.Test.csproj", "{69370370-9873-4D6A-965D-D1E16694047D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.Test", "test\DotNetCore.CAP.Test\DotNetCore.CAP.Test.csproj", "{F608B509-A99B-4AC7-8227-42051DD4A578}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.SqlServer", "src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj", "{3B577468-6792-4EF1-9237-15180B176A24}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.SqlServer.Test", "test\DotNetCore.CAP.SqlServer.Test\DotNetCore.CAP.SqlServer.Test.csproj", "{DA00FA38-C4B9-4F55-8756-D480FBC1084F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,10 +69,6 @@ Global {E8AF8611-0EA4-4B19-BC48-87C57A87DC66}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8AF8611-0EA4-4B19-BC48-87C57A87DC66}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8AF8611-0EA4-4B19-BC48-87C57A87DC66}.Release|Any CPU.Build.0 = Release|Any CPU - {96111249-C4C3-4DC9-A887-32D583723AB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {96111249-C4C3-4DC9-A887-32D583723AB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {96111249-C4C3-4DC9-A887-32D583723AB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {96111249-C4C3-4DC9-A887-32D583723AB1}.Release|Any CPU.Build.0 = Release|Any CPU {2F095ED9-5BC9-4512-9013-A47685FB2508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F095ED9-5BC9-4512-9013-A47685FB2508}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F095ED9-5BC9-4512-9013-A47685FB2508}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -85,14 +81,17 @@ Global {9961B80E-0718-4280-B2A0-271B003DE26B}.Debug|Any CPU.Build.0 = Debug|Any CPU {9961B80E-0718-4280-B2A0-271B003DE26B}.Release|Any CPU.ActiveCfg = Release|Any CPU {9961B80E-0718-4280-B2A0-271B003DE26B}.Release|Any CPU.Build.0 = Release|Any CPU - {69370370-9873-4D6A-965D-D1E16694047D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {69370370-9873-4D6A-965D-D1E16694047D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {69370370-9873-4D6A-965D-D1E16694047D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {69370370-9873-4D6A-965D-D1E16694047D}.Release|Any CPU.Build.0 = Release|Any CPU {F608B509-A99B-4AC7-8227-42051DD4A578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F608B509-A99B-4AC7-8227-42051DD4A578}.Debug|Any CPU.Build.0 = Debug|Any CPU {F608B509-A99B-4AC7-8227-42051DD4A578}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F608B509-A99B-4AC7-8227-42051DD4A578}.Release|Any CPU.Build.0 = Release|Any CPU + {3B577468-6792-4EF1-9237-15180B176A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B577468-6792-4EF1-9237-15180B176A24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B577468-6792-4EF1-9237-15180B176A24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B577468-6792-4EF1-9237-15180B176A24}.Release|Any CPU.Build.0 = Release|Any CPU + {DA00FA38-C4B9-4F55-8756-D480FBC1084F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA00FA38-C4B9-4F55-8756-D480FBC1084F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA00FA38-C4B9-4F55-8756-D480FBC1084F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA00FA38-C4B9-4F55-8756-D480FBC1084F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -100,11 +99,11 @@ Global GlobalSection(NestedProjects) = preSolution {9E5A7F49-8E31-4A71-90CC-1DA9AEDA99EE} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {E8AF8611-0EA4-4B19-BC48-87C57A87DC66} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} - {96111249-C4C3-4DC9-A887-32D583723AB1} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {2F095ED9-5BC9-4512-9013-A47685FB2508} = {3A6B6931-A123-477A-9469-8B468B5385AF} {C42CDE33-0878-4BA0-96F2-4CB7C8FDEAAD} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {9961B80E-0718-4280-B2A0-271B003DE26B} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} - {69370370-9873-4D6A-965D-D1E16694047D} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {F608B509-A99B-4AC7-8227-42051DD4A578} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} + {3B577468-6792-4EF1-9237-15180B176A24} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} + {DA00FA38-C4B9-4F55-8756-D480FBC1084F} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} EndGlobalSection EndGlobal diff --git a/README.zh-cn.md b/README.zh-cn.md index f04265d..bbb356b 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -6,6 +6,7 @@ [![Travis branch](https://img.shields.io/travis/dotnetcore/CAP/master.svg?label=travis-ci)](https://travis-ci.org/dotnetcore/CAP) [![AppVeyor](https://ci.appveyor.com/api/projects/status/4mpe0tbu7n126vyw?svg=true)](https://ci.appveyor.com/project/yuleyule66/cap) [![NuGet](https://img.shields.io/nuget/vpre/DotNetCore.CAP.svg)](https://www.nuget.org/packages/DotNetCore.CAP/) +[![Member Project Of .NET China Foundation](https://github.com/dotnetcore/Home/raw/master/icons/member-project-of-netchina.png)](https://github.com/dotnetcore) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dotnetcore/CAP/master/LICENSE.txt) CAP 是一个在分布式系统(SOA、MicroService)中实现最终一致性的库,它具有轻量级、易使用、高性能等特点。 @@ -30,6 +31,10 @@ CAP 具有消息持久化的功能,当你的服务进行重启或者宕机时 你可以运行以下下命令在你的项目中安装 CAP。 +``` +PM> Install-Package DotNetCore.CAP -Pre +``` + 如果你的消息队列使用的是 Kafka 的话,你可以: ``` @@ -42,10 +47,10 @@ PM> Install-Package DotNetCore.CAP.Kafka -Pre PM> Install-Package DotNetCore.CAP.RabbitMQ -Pre ``` -CAP 默认提供了 Entity Framwork 作为数据库存储: +CAP 默认提供了 Sql Server 的扩展作为数据库存储(MySql的正在开发中): ``` -PM> Install-Package DotNetCore.CAP.EntityFrameworkCore -Pre +PM> Install-Package DotNetCore.CAP.SqlServer -Pre ``` ### Configuration @@ -57,11 +62,23 @@ public void ConfigureServices(IServiceCollection services) { ...... - services.AddDbContext(); + services.AddDbContext(); - services.AddCap() - .AddEntityFrameworkStores() - .AddKafka(x => x.Servers = "localhost:9092"); + services.AddCap(x => + { + // 如果你的 SqlServer 使用的 EF 进行数据操作,你需要添加如下配置: + // 注意: 你不需要再次配置 x.UseSqlServer(""") + x.UseEntityFramework(); + + // 如果你使用的Dapper,你需要添加如下配置: + x.UseSqlServer("数据库连接字符串"); + + // 如果你使用的 RabbitMQ 作为MQ,你需要添加如下配置: + x.UseRabbitMQ("localhost"); + + //如果你使用的 Kafka 作为MQ,你需要添加如下配置: + x.UseKafka("localhost"); + }); } public void Configure(IApplicationBuilder app) @@ -96,6 +113,18 @@ public class PublishController : Controller return Ok(); } + + [Route("~/checkAccountWithTrans")] + public async Task PublishMessageWithTransaction([FromServices]AppDbContext dbContext) + { + using (var trans = dbContext.Database.BeginTransaction()) + { + await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }); + + trans.Commit(); + } + return Ok(); + } } ``` diff --git a/appveyor.yml b/appveyor.yml index ecd2784..203f11c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,9 @@ os: Visual Studio 2015 environment: BUILDING_ON_PLATFORM: win BuildEnvironment: appveyor - Cap_SqlServer_ConnectionStringTemplate: Server=.\SQL2012SP1;Database={0};User ID=sa;Password=Password12! + Cap_SqlServer_ConnectionStringTemplate: Server=(local)\SQL2014;Database={0};User ID=sa;Password=Password12! services: - - mssql2012sp1 + - mssql2014 build_script: - ps: ./ConfigureMSDTC.ps1 - ps: ./build.ps1 @@ -17,6 +17,6 @@ deploy: on: appveyor_repo_tag: true api_key: - secure: P4da9c6a6-00e1-47d0-a821-b62380362dc9 + secure: U62rpGTEqztrUO4ncscm4XSaAoCSmWwT/rOWO/2JJS44psJvl0QpjRL0o0ughMoY skip_symbols: true artifact: /artifacts\/packages\/.+\.nupkg/ diff --git a/build/version.props b/build/version.props index 3e9fc0c..c1235f0 100644 --- a/build/version.props +++ b/build/version.props @@ -2,7 +2,7 @@ 0 1 - 0 + 1 $(VersionMajor).$(VersionMinor).$(VersionPatch) diff --git a/samples/Sample.Kafka/AppDbContext.cs b/samples/Sample.Kafka/AppDbContext.cs index d1900a1..fdfd723 100644 --- a/samples/Sample.Kafka/AppDbContext.cs +++ b/samples/Sample.Kafka/AppDbContext.cs @@ -1,31 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace Sample.Kafka { public class AppDbContext : DbContext { - - public DbSet SentMessages { get; set; } - - public DbSet ReceivedMessages { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=Test;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True"); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(x => x.StatusName).HasMaxLength(50); - modelBuilder.Entity().Property(x => x.StatusName).HasMaxLength(50); - - base.OnModelCreating(modelBuilder); + //optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Test;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True"); } } } diff --git a/samples/Sample.Kafka/Controllers/ValuesController.cs b/samples/Sample.Kafka/Controllers/ValuesController.cs index 3c09caf..13142a5 100644 --- a/samples/Sample.Kafka/Controllers/ValuesController.cs +++ b/samples/Sample.Kafka/Controllers/ValuesController.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; using DotNetCore.CAP; -using DotNetCore.CAP.Kafka; +using DotNetCore.CAP.RabbitMQ; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; @@ -11,10 +11,12 @@ namespace Sample.Kafka.Controllers public class ValuesController : Controller, ICapSubscribe { private readonly ICapPublisher _producer; + private readonly AppDbContext _dbContext ; - public ValuesController(ICapPublisher producer) + public ValuesController(ICapPublisher producer, AppDbContext dbContext) { _producer = producer; + _dbContext = dbContext; } [Route("/")] @@ -27,15 +29,19 @@ namespace Sample.Kafka.Controllers [CapSubscribe("zzwl.topic.finace.callBack", Group = "test")] public void KafkaTest(Person person) { - Console.WriteLine(person.Name); - Console.WriteLine(person.Age); - + Console.WriteLine(DateTime.Now); } [Route("~/send")] public async Task SendTopic() { - await _producer.PublishAsync("zzwl.topic.finace.callBack", new Person { Name = "Test", Age = 11 }); + using (var trans = _dbContext.Database.BeginTransaction()) + { + await _producer.PublishAsync("zzwl.topic.finace.callBack",""); + + trans.Commit(); + } + return Ok(); } diff --git a/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.Designer.cs b/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.Designer.cs deleted file mode 100644 index 5ed1422..0000000 --- a/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.Designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Sample.Kafka; - -namespace Sample.Kafka.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20170710102614_InitilizeDB")] - partial class InitilizeDB - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapReceivedMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("Group"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("ReceivedMessages"); - }); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapSentMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("SentMessages"); - }); - } - } -} diff --git a/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.cs b/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.cs deleted file mode 100644 index 58c85e8..0000000 --- a/samples/Sample.Kafka/Migrations/20170710102614_InitilizeDB.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Sample.Kafka.Migrations -{ - public partial class InitilizeDB : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ReceivedMessages", - columns: table => new - { - Id = table.Column(nullable: false), - Added = table.Column(nullable: false), - Content = table.Column(nullable: true), - Group = table.Column(nullable: true), - KeyName = table.Column(nullable: true), - LastRun = table.Column(nullable: false), - Retries = table.Column(nullable: false), - StatusName = table.Column(maxLength: 50, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ReceivedMessages", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "SentMessages", - columns: table => new - { - Id = table.Column(nullable: false), - Added = table.Column(nullable: false), - Content = table.Column(nullable: true), - KeyName = table.Column(nullable: true), - LastRun = table.Column(nullable: false), - Retries = table.Column(nullable: false), - StatusName = table.Column(maxLength: 50, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_SentMessages", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ReceivedMessages"); - - migrationBuilder.DropTable( - name: "SentMessages"); - } - } -} diff --git a/samples/Sample.Kafka/Sample.Kafka.csproj b/samples/Sample.Kafka/Sample.Kafka.csproj index 506780b..3788806 100644 --- a/samples/Sample.Kafka/Sample.Kafka.csproj +++ b/samples/Sample.Kafka/Sample.Kafka.csproj @@ -24,9 +24,8 @@ - - + diff --git a/samples/Sample.Kafka/Startup.cs b/samples/Sample.Kafka/Startup.cs index d234b37..5cb3fd8 100644 --- a/samples/Sample.Kafka/Startup.cs +++ b/samples/Sample.Kafka/Startup.cs @@ -25,16 +25,13 @@ namespace Sample.Kafka { services.AddDbContext(); - services.AddCap() - .AddEntityFrameworkStores() - .AddRabbitMQ(x => - { - x.HostName = "192.168.2.206"; - x.UserName = "admin"; - x.Password = "123123"; - }); - //.AddKafka(x => x.Servers = ""); - + services.AddCap(x => + { + x.UseEntityFramework(); + //x.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Test;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True"); + x.UseRabbitMQ(o => { o.HostName = "192.168.2.206"; o.UserName = "admin"; o.Password = "123123"; }); + }); + // Add framework services. services.AddMvc(); } diff --git a/src/DotNetCore.CAP.EntityFrameworkCore/CAP.BuilderExtensions.cs b/src/DotNetCore.CAP.EntityFrameworkCore/CAP.BuilderExtensions.cs deleted file mode 100644 index 709eb99..0000000 --- a/src/DotNetCore.CAP.EntityFrameworkCore/CAP.BuilderExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DotNetCore.CAP; -using DotNetCore.CAP.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Contains extension methods to for adding entity framework stores. - /// - public static class CapEntityFrameworkBuilderExtensions - { - /// - /// Adds an Entity Framework implementation of message stores. - /// - /// The Entity Framework database context to use. - /// The instance this method extends. - public static CapBuilder AddEntityFrameworkStores(this CapBuilder builder) - where TContext : DbContext - { - builder.Services.AddScoped>(); - - return builder; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.EntityFrameworkCore/CapDbContext.cs b/src/DotNetCore.CAP.EntityFrameworkCore/CapDbContext.cs deleted file mode 100644 index 829a2fb..0000000 --- a/src/DotNetCore.CAP.EntityFrameworkCore/CapDbContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using DotNetCore.CAP.Infrastructure; -using Microsoft.EntityFrameworkCore; - -namespace DotNetCore.CAP.EntityFrameworkCore -{ - /// - /// Base class for the Entity Framework database context used for CAP. - /// - public class CapDbContext : DbContext - { - /// - /// Initializes a new instance of the . - /// - public CapDbContext() { } - - /// - /// Initializes a new instance of the . - /// - /// The options to be used by a . - public CapDbContext(DbContextOptions options) : base(options) { } - - /// - /// Gets or sets the of Messages. - /// - public DbSet CapSentMessages { get; set; } - - /// - /// Gets or sets the of Messages. - /// - public DbSet CapReceivedMessages { get; set; } - - /// - /// Configures the schema for the identity framework. - /// - /// - /// The builder being used to construct the model for this context. - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(b => - { - b.HasKey(m => m.Id); - b.Property(p => p.StatusName).HasMaxLength(50); - }); - - modelBuilder.Entity(b => - { - b.Property(p => p.StatusName).HasMaxLength(50); - }); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.EntityFrameworkCore/CapMessageStore.cs b/src/DotNetCore.CAP.EntityFrameworkCore/CapMessageStore.cs deleted file mode 100644 index bc3fedf..0000000 --- a/src/DotNetCore.CAP.EntityFrameworkCore/CapMessageStore.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using Microsoft.EntityFrameworkCore; - -namespace DotNetCore.CAP.EntityFrameworkCore -{ - /// - /// Represents a new instance of a persistence store for the specified message types. - /// - /// The type of the data context class used to access the store. - public class CapMessageStore : ICapMessageStore where TContext : DbContext - { - /// - /// Constructs a new instance of . - /// - /// The . - public CapMessageStore(TContext context) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - } - - public TContext Context { get; private set; } - - private DbSet SentMessages => Context.Set(); - - private DbSet ReceivedMessages => Context.Set(); - - /// - /// Creates the specified in the cap message store. - /// - /// The message to create. - public async Task StoreSentMessageAsync(CapSentMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - Context.Add(message); - await Context.SaveChangesAsync(); - return OperateResult.Success; - } - - public async Task ChangeSentMessageStateAsync(CapSentMessage message, string status, - bool autoSaveChanges = true) - { - Context.Attach(message); - message.LastRun = DateTime.Now; - message.StatusName = status; - try - { - if (autoSaveChanges) - { - await Context.SaveChangesAsync(); - } - } - catch (DbUpdateConcurrencyException ex) - { - return OperateResult.Failed( - new OperateError() - { - Code = "DbUpdateConcurrencyException", - Description = ex.Message - }); - } - return OperateResult.Success; - } - - /// - /// First Enqueued Message. - /// - public async Task GetNextSentMessageToBeEnqueuedAsync() - { - return await SentMessages.FirstOrDefaultAsync(x => x.StatusName == StatusName.Enqueued); - } - - /// - /// Updates a message in a store as an asynchronous operation. - /// - /// The message to update in the store. - public async Task UpdateSentMessageAsync(CapSentMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - Context.Attach(message); - message.LastRun = DateTime.Now; - Context.Update(message); - - try - { - await Context.SaveChangesAsync(); - return OperateResult.Success; - } - catch (DbUpdateConcurrencyException ex) - { - return OperateResult.Failed(new OperateError() - { - Code = "DbUpdateConcurrencyException", - Description = ex.Message - }); - } - } - - /// - /// Deletes the specified from the consistency message store. - /// - /// The message to delete. - public async Task RemoveSentMessageAsync(CapSentMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - Context.Remove(message); - try - { - await Context.SaveChangesAsync(); - return OperateResult.Success; - } - catch (DbUpdateConcurrencyException ex) - { - return OperateResult.Failed(new OperateError() - { - Code = "DbUpdateConcurrencyException", - Description = ex.Message - }); - } - } - - /// - /// Creates the specified in the consistency message store. - /// - /// The message to create. - public async Task StoreReceivedMessageAsync(CapReceivedMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - Context.Add(message); - await Context.SaveChangesAsync(); - return OperateResult.Success; - } - - public async Task ChangeReceivedMessageStateAsync(CapReceivedMessage message, string status, - bool autoSaveChanges = true) - { - Context.Attach(message); - message.LastRun = DateTime.Now; - message.StatusName = status; - try - { - if (autoSaveChanges) - { - await Context.SaveChangesAsync(); - } - } - catch (DbUpdateConcurrencyException ex) - { - return OperateResult.Failed(new OperateError() - { - Code = "DbUpdateConcurrencyException", - Description = ex.Message - }); - } - return OperateResult.Success; - } - - public async Task GetNextReceivedMessageToBeExcuted() - { - return await ReceivedMessages.FirstOrDefaultAsync(x => x.StatusName == StatusName.Enqueued); - } - - /// - /// Updates the specified in the message store. - /// - /// The message to update. - public async Task UpdateReceivedMessageAsync(CapReceivedMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - - Context.Attach(message); - message.LastRun = DateTime.Now; - Context.Update(message); - - try - { - await Context.SaveChangesAsync(); - return OperateResult.Success; - } - catch (DbUpdateConcurrencyException ex) - { - return OperateResult.Failed(new OperateError() - { - Code = "DbUpdateConcurrencyException", - Description = ex.Message - }); - } - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/CAP.BuilderExtensions.cs b/src/DotNetCore.CAP.Kafka/CAP.BuilderExtensions.cs deleted file mode 100644 index 4c3375e..0000000 --- a/src/DotNetCore.CAP.Kafka/CAP.BuilderExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using DotNetCore.CAP; -using DotNetCore.CAP.Job; -using DotNetCore.CAP.Kafka; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Contains extension methods to for adding kafka service. - /// - public static class CapBuilderExtensions - { - /// - /// Adds an Kafka implementation of CAP messages queue. - /// - /// The instance this method extends - /// An action to configure the . - /// An for creating and configuring the CAP system. - public static CapBuilder AddKafka(this CapBuilder builder, Action setupAction) - { - if (setupAction == null) throw new ArgumentNullException(nameof(setupAction)); - - builder.Services.Configure(setupAction); - - builder.Services.AddSingleton(); - - builder.Services.AddTransient(); - - return builder; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs b/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs new file mode 100644 index 0000000..2dbf888 --- /dev/null +++ b/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs @@ -0,0 +1,29 @@ +using System; +using DotNetCore.CAP.Kafka; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class KafkaCapOptionsExtension : ICapOptionsExtension + { + private readonly Action _configure; + + public KafkaCapOptionsExtension(Action configure) + { + _configure = configure; + } + + public void AddServices(IServiceCollection services) + { + services.Configure(_configure); + + var kafkaOptions = new KafkaOptions(); + _configure(kafkaOptions); + services.AddSingleton(kafkaOptions); + + services.AddSingleton(); + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs index b7836b6..34cf92d 100644 --- a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs +++ b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Linq; -namespace DotNetCore.CAP.Kafka +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP { /// /// Provides programmatic configuration for the CAP kafka project. @@ -32,9 +33,9 @@ namespace DotNetCore.CAP.Kafka internal IEnumerable> AsRdkafkaConfig() { - if (MainConfig.ContainsKey("bootstrap.servers")) + if (MainConfig.ContainsKey("bootstrap.servers")) return MainConfig.AsEnumerable(); - + if (string.IsNullOrEmpty(Servers)) { throw new ArgumentNullException(nameof(Servers)); diff --git a/src/DotNetCore.CAP.Kafka/CAP.Options.Extensions.cs b/src/DotNetCore.CAP.Kafka/CAP.Options.Extensions.cs new file mode 100644 index 0000000..65314f4 --- /dev/null +++ b/src/DotNetCore.CAP.Kafka/CAP.Options.Extensions.cs @@ -0,0 +1,26 @@ +using System; +using DotNetCore.CAP; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CapOptionsExtensions + { + public static CapOptions UseKafka(this CapOptions options, string bootstrapServers) + { + return options.UseRabbitMQ(opt => + { + opt.Servers = bootstrapServers; + }); + } + + public static CapOptions UseRabbitMQ(this CapOptions options, Action configure) + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + options.RegisterExtension(new KafkaCapOptionsExtension(configure)); + + return options; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/CapSubscribeAttribute.cs b/src/DotNetCore.CAP.Kafka/CAP.SubscribeAttribute.cs similarity index 64% rename from src/DotNetCore.CAP.Kafka/CapSubscribeAttribute.cs rename to src/DotNetCore.CAP.Kafka/CAP.SubscribeAttribute.cs index 3d906d1..a914df6 100644 --- a/src/DotNetCore.CAP.Kafka/CapSubscribeAttribute.cs +++ b/src/DotNetCore.CAP.Kafka/CAP.SubscribeAttribute.cs @@ -1,27 +1,28 @@ using DotNetCore.CAP.Abstractions; -namespace DotNetCore.CAP.Kafka +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP { public class CapSubscribeAttribute : TopicAttribute { - public CapSubscribeAttribute(string topicName) - : this(topicName, 0) + public CapSubscribeAttribute(string name) + : this(name, 0) { } /// /// Not support /// - public CapSubscribeAttribute(string topicName, int partition) - : this(topicName, partition, 0) + public CapSubscribeAttribute(string name, int partition) + : this(name, partition, 0) { } /// /// Not support /// - public CapSubscribeAttribute(string topicName, int partition, long offset) - : base(topicName) + public CapSubscribeAttribute(string name, int partition, long offset) + : base(name) { Offset = offset; Partition = partition; diff --git a/src/DotNetCore.CAP.Kafka/IProcessor.KafkaJobProcessor.cs b/src/DotNetCore.CAP.Kafka/IProcessor.KafkaJobProcessor.cs deleted file mode 100644 index 3edda96..0000000 --- a/src/DotNetCore.CAP.Kafka/IProcessor.KafkaJobProcessor.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Confluent.Kafka; -using Confluent.Kafka.Serialization; -using DotNetCore.CAP.Infrastructure; -using DotNetCore.CAP.Job; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace DotNetCore.CAP.Kafka -{ - public class KafkaJobProcessor : IJobProcessor - { - private readonly KafkaOptions _kafkaOptions; - private readonly CancellationTokenSource _cts; - - private readonly IServiceProvider _provider; - private readonly ILogger _logger; - - private readonly TimeSpan _pollingDelay; - - public KafkaJobProcessor( - IOptions capOptions, - IOptions kafkaOptions, - ILogger logger, - IServiceProvider provider) - { - _logger = logger; - _kafkaOptions = kafkaOptions.Value; - _provider = provider; - _cts = new CancellationTokenSource(); - _pollingDelay = TimeSpan.FromSeconds(capOptions.Value.PollingDelay); - } - - public bool Waiting { get; private set; } - - public Task ProcessAsync(ProcessingContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - - context.ThrowIfStopping(); - return ProcessCoreAsync(context); - } - - public async Task ProcessCoreAsync(ProcessingContext context) - { - try - { - var worked = await Step(context); - - context.ThrowIfStopping(); - - Waiting = true; - - if (!worked) - { - var token = GetTokenToWaitOn(context); - } - - await WaitHandleEx.WaitAnyAsync(WaitHandleEx.PulseEvent, - context.CancellationToken.WaitHandle, _pollingDelay); - } - finally - { - Waiting = false; - } - } - - protected virtual CancellationToken GetTokenToWaitOn(ProcessingContext context) - { - return context.CancellationToken; - } - - private async Task Step(ProcessingContext context) - { - using (var scopedContext = context.CreateScope()) - { - var provider = scopedContext.Provider; - var messageStore = provider.GetRequiredService(); - var message = await messageStore.GetNextSentMessageToBeEnqueuedAsync(); - if (message == null) return true; - try - { - var sp = Stopwatch.StartNew(); - message.StatusName = StatusName.Processing; - await messageStore.UpdateSentMessageAsync(message); - - await ExecuteJobAsync(message.KeyName, message.Content); - - sp.Stop(); - - message.StatusName = StatusName.Succeeded; - await messageStore.UpdateSentMessageAsync(message); - _logger.JobExecuted(sp.Elapsed.TotalSeconds); - } - catch (Exception ex) - { - _logger.ExceptionOccuredWhileExecutingJob(message.KeyName, ex); - return false; - } - } - return true; - } - - private Task ExecuteJobAsync(string topic, string content) - { - var config = _kafkaOptions.AsRdkafkaConfig(); - using (var producer = new Producer(config, null, new StringSerializer(Encoding.UTF8))) - { - producer.ProduceAsync(topic, null, content); - producer.Flush(); - } - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs b/src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs index 52b34e3..1993d80 100644 --- a/src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs +++ b/src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs @@ -1,8 +1,8 @@ using System; using System.Text; +using System.Threading; using Confluent.Kafka; using Confluent.Kafka.Serialization; -using DotNetCore.CAP.Infrastructure; namespace DotNetCore.CAP.Kafka { @@ -38,10 +38,11 @@ namespace DotNetCore.CAP.Kafka _consumerClient.Subscribe(topicName); } - public void Listening(TimeSpan timeout) + public void Listening(TimeSpan timeout, CancellationToken cancellationToken) { while (true) { + cancellationToken.ThrowIfCancellationRequested(); _consumerClient.Poll(timeout); } } @@ -73,13 +74,12 @@ namespace DotNetCore.CAP.Kafka var message = new MessageContext { Group = _groupId, - KeyName = e.Topic, + Name = e.Topic, Content = e.Value }; MessageReceieved?.Invoke(sender, message); } - #endregion private methods } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/LoggerExtensions.cs b/src/DotNetCore.CAP.Kafka/LoggerExtensions.cs deleted file mode 100644 index 1dec1c7..0000000 --- a/src/DotNetCore.CAP.Kafka/LoggerExtensions.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace DotNetCore.CAP.Kafka -{ - internal static class LoggerExtensions - { - private static readonly Action _collectingExpiredEntities; - - private static readonly Action _installing; - private static readonly Action _installingError; - private static readonly Action _installingSuccess; - - private static readonly Action _jobFailed; - private static readonly Action _jobFailedWillRetry; - private static readonly Action _jobExecuted; - private static readonly Action _jobRetrying; - private static readonly Action _jobCouldNotBeLoaded; - private static readonly Action _exceptionOccuredWhileExecutingJob; - - static LoggerExtensions() - { - _collectingExpiredEntities = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Collecting expired entities."); - - _installing = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Installing Jobs SQL objects..."); - - _installingError = LoggerMessage.Define( - LogLevel.Warning, - 2, - "Exception occurred during automatic migration. Retrying..."); - - _installingSuccess = LoggerMessage.Define( - LogLevel.Debug, - 3, - "Jobs SQL objects installed."); - - _jobFailed = LoggerMessage.Define( - LogLevel.Warning, - 1, - "Job failed to execute."); - - _jobFailedWillRetry = LoggerMessage.Define( - LogLevel.Warning, - 2, - "Job failed to execute. Will retry."); - - _jobRetrying = LoggerMessage.Define( - LogLevel.Debug, - 3, - "Retrying a job: {Retries}..."); - - _jobExecuted = LoggerMessage.Define( - LogLevel.Debug, - 4, - "Job executed. Took: {Seconds} secs."); - - _jobCouldNotBeLoaded = LoggerMessage.Define( - LogLevel.Warning, - 5, - "Could not load a job: '{JobId}'."); - - _exceptionOccuredWhileExecutingJob = LoggerMessage.Define( - LogLevel.Error, - 6, - "An exception occured while trying to execute a job: '{JobId}'. " + - "Requeuing for another retry."); - } - - public static void CollectingExpiredEntities(this ILogger logger) - { - _collectingExpiredEntities(logger, null); - } - - public static void Installing(this ILogger logger) - { - _installing(logger, null); - } - - public static void InstallingError(this ILogger logger, Exception ex) - { - _installingError(logger, ex); - } - - public static void InstallingSuccess(this ILogger logger) - { - _installingSuccess(logger, null); - } - - public static void JobFailed(this ILogger logger, Exception ex) - { - _jobFailed(logger, ex); - } - - public static void JobFailedWillRetry(this ILogger logger, Exception ex) - { - _jobFailedWillRetry(logger, ex); - } - - public static void JobRetrying(this ILogger logger, int retries) - { - _jobRetrying(logger, retries, null); - } - - public static void JobExecuted(this ILogger logger, double seconds) - { - _jobExecuted(logger, seconds, null); - } - - public static void JobCouldNotBeLoaded(this ILogger logger, int jobId, Exception ex) - { - _jobCouldNotBeLoaded(logger, jobId, ex); - } - - public static void ExceptionOccuredWhileExecutingJob(this ILogger logger, string jobId, Exception ex) - { - _exceptionOccuredWhileExecutingJob(logger, jobId, ex); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs b/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs new file mode 100644 index 0000000..8252629 --- /dev/null +++ b/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs @@ -0,0 +1,54 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.Kafka.Serialization; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DotNetCore.CAP.Kafka +{ + public class PublishQueueExecutor : BasePublishQueueExecutor + { + private readonly ILogger _logger; + private readonly KafkaOptions _kafkaOptions; + + public PublishQueueExecutor(IStateChanger stateChanger, + IOptions options, + ILogger logger) + : base(stateChanger, logger) + { + _logger = logger; + _kafkaOptions = options.Value; + } + + public override Task PublishAsync(string keyName, string content) + { + try + { + var config = _kafkaOptions.AsRdkafkaConfig(); + using (var producer = new Producer(config, null, new StringSerializer(Encoding.UTF8))) + { + producer.ProduceAsync(keyName, null, content); + producer.Flush(); + } + + _logger.LogDebug($"kafka topic message [{keyName}] has been published."); + + return Task.FromResult(OperateResult.Success); + } + catch (Exception ex) + { + _logger.LogError($"kafka topic message [{keyName}] has benn raised an exception of sending. the exception is: {ex.Message}"); + + return Task.FromResult(OperateResult.Failed(ex, + new OperateError() + { + Code = ex.HResult.ToString(), + Description = ex.Message + })); + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.BuilderExtensions.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.BuilderExtensions.cs deleted file mode 100644 index 6c0808a..0000000 --- a/src/DotNetCore.CAP.RabbitMQ/CAP.BuilderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using DotNetCore.CAP; -using DotNetCore.CAP.Job; -using DotNetCore.CAP.RabbitMQ; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class CapBuilderExtensions - { - public static CapBuilder AddRabbitMQ(this CapBuilder builder, Action setupOptions) - { - if (setupOptions == null) throw new ArgumentNullException(nameof(setupOptions)); - - builder.Services.Configure(setupOptions); - - builder.Services.AddSingleton(); - - builder.Services.AddTransient(); - - return builder; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.Options.Extensions.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.Options.Extensions.cs new file mode 100644 index 0000000..4d59112 --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.Options.Extensions.cs @@ -0,0 +1,26 @@ +using System; +using DotNetCore.CAP; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CapOptionsExtensions + { + public static CapOptions UseRabbitMQ(this CapOptions options, string hostName) + { + return options.UseRabbitMQ(opt => + { + opt.HostName = hostName; + }); + } + + public static CapOptions UseRabbitMQ(this CapOptions options, Action configure) + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + options.RegisterExtension(new RabbitMQCapOptionsExtension(configure)); + + return options; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbiMQOptions.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbiMQOptions.cs index 9dcc030..09a90ef 100644 --- a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbiMQOptions.cs +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbiMQOptions.cs @@ -1,4 +1,5 @@ -namespace DotNetCore.CAP.RabbitMQ +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP { public class RabbitMQOptions { @@ -34,7 +35,7 @@ public string HostName { get; set; } = "localhost"; /// The topic exchange type. - internal string EXCHANGE_TYPE = "topic"; + internal const string ExchangeType = "topic"; /// /// Password to use when authenticating to the server. @@ -72,7 +73,7 @@ public int SocketWriteTimeout { get; set; } = DefaultConnectionTimeout; /// - /// The port to connect on. + /// The port to connect on. /// public int Port { get; set; } = -1; } diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs new file mode 100644 index 0000000..14eedbb --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs @@ -0,0 +1,30 @@ +using System; +using DotNetCore.CAP.RabbitMQ; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class RabbitMQCapOptionsExtension : ICapOptionsExtension + { + private readonly Action _configure; + + public RabbitMQCapOptionsExtension(Action configure) + { + _configure = configure; + } + + public void AddServices(IServiceCollection services) + { + services.Configure(_configure); + + var rabbitMQOptions = new RabbitMQOptions(); + _configure(rabbitMQOptions); + + services.AddSingleton(rabbitMQOptions); + + services.AddSingleton(); + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.SubscribeAttribute.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.SubscribeAttribute.cs new file mode 100644 index 0000000..92fb295 --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.SubscribeAttribute.cs @@ -0,0 +1,12 @@ +using DotNetCore.CAP.Abstractions; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class CapSubscribeAttribute : TopicAttribute + { + public CapSubscribeAttribute(string name) : base(name) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CapSubscribeAttribute.cs b/src/DotNetCore.CAP.RabbitMQ/CapSubscribeAttribute.cs deleted file mode 100644 index 5e33a1c..0000000 --- a/src/DotNetCore.CAP.RabbitMQ/CapSubscribeAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using DotNetCore.CAP.Abstractions; - -namespace DotNetCore.CAP.RabbitMQ -{ - public class CapSubscribeAttribute : TopicAttribute - { - public CapSubscribeAttribute(string routingKey) : base(routingKey) - { - - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/IProcessor.RabbitJobProcessor.cs b/src/DotNetCore.CAP.RabbitMQ/IProcessor.RabbitJobProcessor.cs deleted file mode 100644 index 8534f76..0000000 --- a/src/DotNetCore.CAP.RabbitMQ/IProcessor.RabbitJobProcessor.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using DotNetCore.CAP.Job; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using RabbitMQ.Client; - -namespace DotNetCore.CAP.RabbitMQ -{ - public class RabbitJobProcessor : IJobProcessor - { - private readonly RabbitMQOptions _rabbitMqOptions; - private readonly CancellationTokenSource _cts; - - private readonly IServiceProvider _provider; - private readonly ILogger _logger; - - private readonly TimeSpan _pollingDelay; - - public RabbitJobProcessor( - IOptions capOptions, - IOptions rabbitMQOptions, - ILogger logger, - IServiceProvider provider) - { - _logger = logger; - _rabbitMqOptions = rabbitMQOptions.Value; - _provider = provider; - _cts = new CancellationTokenSource(); - - var capOptions1 = capOptions.Value; - _pollingDelay = TimeSpan.FromSeconds(capOptions1.PollingDelay); - } - - public bool Waiting { get; private set; } - - public Task ProcessAsync(ProcessingContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - - context.ThrowIfStopping(); - return ProcessCoreAsync(context); - } - - public async Task ProcessCoreAsync(ProcessingContext context) - { - try - { - var worked = await Step(context); - - context.ThrowIfStopping(); - - Waiting = true; - - if (!worked) - { - var token = GetTokenToWaitOn(context); - } - - await WaitHandleEx.WaitAnyAsync(WaitHandleEx.PulseEvent, - context.CancellationToken.WaitHandle, _pollingDelay); - } - finally - { - Waiting = false; - } - } - - protected virtual CancellationToken GetTokenToWaitOn(ProcessingContext context) - { - return context.CancellationToken; - } - - private async Task Step(ProcessingContext context) - { - using (var scopedContext = context.CreateScope()) - { - var provider = scopedContext.Provider; - var messageStore = provider.GetRequiredService(); - var message = await messageStore.GetNextSentMessageToBeEnqueuedAsync(); - try - { - if (message != null) - { - var sp = Stopwatch.StartNew(); - message.StatusName = StatusName.Processing; - await messageStore.UpdateSentMessageAsync(message); - - ExecuteJob(message.KeyName, message.Content); - - sp.Stop(); - - message.StatusName = StatusName.Succeeded; - await messageStore.UpdateSentMessageAsync(message); - - _logger.JobExecuted(sp.Elapsed.TotalSeconds); - } - } - catch (Exception ex) - { - _logger.ExceptionOccuredWhileExecutingJob(message?.KeyName, ex); - return false; - } - } - return true; - } - - private void ExecuteJob(string topic, string content) - { - var factory = new ConnectionFactory() - { - HostName = _rabbitMqOptions.HostName, - UserName = _rabbitMqOptions.UserName, - Port = _rabbitMqOptions.Port, - Password = _rabbitMqOptions.Password, - VirtualHost = _rabbitMqOptions.VirtualHost, - RequestedConnectionTimeout = _rabbitMqOptions.RequestedConnectionTimeout, - SocketReadTimeout = _rabbitMqOptions.SocketReadTimeout, - SocketWriteTimeout = _rabbitMqOptions.SocketWriteTimeout - }; - - using (var connection = factory.CreateConnection()) - using (var channel = connection.CreateModel()) - { - var body = Encoding.UTF8.GetBytes(content); - - channel.ExchangeDeclare(_rabbitMqOptions.TopicExchangeName, _rabbitMqOptions.EXCHANGE_TYPE); - channel.BasicPublish(exchange: _rabbitMqOptions.TopicExchangeName, - routingKey: topic, - basicProperties: null, - body: body); - } - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/LoggerExtensions.cs b/src/DotNetCore.CAP.RabbitMQ/LoggerExtensions.cs deleted file mode 100644 index f264326..0000000 --- a/src/DotNetCore.CAP.RabbitMQ/LoggerExtensions.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace DotNetCore.CAP.RabbitMQ -{ - internal static class LoggerExtensions - { - private static Action _collectingExpiredEntities; - - private static Action _installing; - private static Action _installingError; - private static Action _installingSuccess; - - private static Action _jobFailed; - private static Action _jobFailedWillRetry; - private static Action _jobExecuted; - private static Action _jobRetrying; - private static Action _jobCouldNotBeLoaded; - private static Action _exceptionOccuredWhileExecutingJob; - - static LoggerExtensions() - { - _collectingExpiredEntities = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Collecting expired entities."); - - _installing = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Installing Jobs SQL objects..."); - - _installingError = LoggerMessage.Define( - LogLevel.Warning, - 2, - "Exception occurred during automatic migration. Retrying..."); - - _installingSuccess = LoggerMessage.Define( - LogLevel.Debug, - 3, - "Jobs SQL objects installed."); - - _jobFailed = LoggerMessage.Define( - LogLevel.Warning, - 1, - "Job failed to execute."); - - _jobFailedWillRetry = LoggerMessage.Define( - LogLevel.Warning, - 2, - "Job failed to execute. Will retry."); - - _jobRetrying = LoggerMessage.Define( - LogLevel.Debug, - 3, - "Retrying a job: {Retries}..."); - - _jobExecuted = LoggerMessage.Define( - LogLevel.Debug, - 4, - "Job executed. Took: {Seconds} secs."); - - _jobCouldNotBeLoaded = LoggerMessage.Define( - LogLevel.Warning, - 5, - "Could not load a job: '{JobId}'."); - - _exceptionOccuredWhileExecutingJob = LoggerMessage.Define( - LogLevel.Error, - 6, - "An exception occured while trying to execute a job: '{JobId}'. " + - "Requeuing for another retry."); - } - - public static void CollectingExpiredEntities(this ILogger logger) - { - _collectingExpiredEntities(logger, null); - } - - public static void Installing(this ILogger logger) - { - _installing(logger, null); - } - - public static void InstallingError(this ILogger logger, Exception ex) - { - _installingError(logger, ex); - } - - public static void InstallingSuccess(this ILogger logger) - { - _installingSuccess(logger, null); - } - - public static void JobFailed(this ILogger logger, Exception ex) - { - _jobFailed(logger, ex); - } - - public static void JobFailedWillRetry(this ILogger logger, Exception ex) - { - _jobFailedWillRetry(logger, ex); - } - - public static void JobRetrying(this ILogger logger, int retries) - { - _jobRetrying(logger, retries, null); - } - - public static void JobExecuted(this ILogger logger, double seconds) - { - _jobExecuted(logger, seconds, null); - } - - public static void JobCouldNotBeLoaded(this ILogger logger, int jobId, Exception ex) - { - _jobCouldNotBeLoaded(logger, jobId, ex); - } - - public static void ExceptionOccuredWhileExecutingJob(this ILogger logger, string jobId, Exception ex) - { - _exceptionOccuredWhileExecutingJob(logger, jobId, ex); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs b/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs new file mode 100644 index 0000000..3e439e2 --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; + +namespace DotNetCore.CAP.RabbitMQ +{ + public class PublishQueueExecutor : BasePublishQueueExecutor + { + private readonly ILogger _logger; + private readonly RabbitMQOptions _rabbitMQOptions; + + public PublishQueueExecutor(IStateChanger stateChanger, + IOptions options, + ILogger logger) + : base(stateChanger, logger) + { + _logger = logger; + _rabbitMQOptions = options.Value; + } + + public override Task PublishAsync(string keyName, string content) + { + var factory = new ConnectionFactory() + { + HostName = _rabbitMQOptions.HostName, + UserName = _rabbitMQOptions.UserName, + Port = _rabbitMQOptions.Port, + Password = _rabbitMQOptions.Password, + VirtualHost = _rabbitMQOptions.VirtualHost, + RequestedConnectionTimeout = _rabbitMQOptions.RequestedConnectionTimeout, + SocketReadTimeout = _rabbitMQOptions.SocketReadTimeout, + SocketWriteTimeout = _rabbitMQOptions.SocketWriteTimeout + }; + + try + { + using (var connection = factory.CreateConnection()) + using (var channel = connection.CreateModel()) + { + var body = Encoding.UTF8.GetBytes(content); + + channel.ExchangeDeclare(_rabbitMQOptions.TopicExchangeName, RabbitMQOptions.ExchangeType); + channel.BasicPublish(exchange: _rabbitMQOptions.TopicExchangeName, + routingKey: keyName, + basicProperties: null, + body: body); + + _logger.LogDebug($"rabbitmq topic message [{keyName}] has been published."); + } + return Task.FromResult(OperateResult.Success); + } + catch (Exception ex) + { + _logger.LogError($"rabbitmq topic message [{keyName}] has benn raised an exception of sending. the exception is: {ex.Message}"); + + return Task.FromResult(OperateResult.Failed(ex, + new OperateError() + { + Code = ex.HResult.ToString(), + Description = ex.Message + })); + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs index adfe91d..4dd2883 100644 --- a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs +++ b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs @@ -1,7 +1,7 @@ using System; using System.Text; +using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -45,18 +45,18 @@ namespace DotNetCore.CAP.RabbitMQ _connection = _connectionFactory.CreateConnection(); _channel = _connection.CreateModel(); - _channel.ExchangeDeclare(exchange: _exchageName, type: _rabbitMQOptions.EXCHANGE_TYPE); + _channel.ExchangeDeclare(exchange: _exchageName, type: RabbitMQOptions.ExchangeType); _channel.QueueDeclare(_queueName, exclusive: false); } - public void Listening(TimeSpan timeout) + public void Listening(TimeSpan timeout, CancellationToken cancellationToken) { var consumer = new EventingBasicConsumer(_channel); consumer.Received += OnConsumerReceived; _channel.BasicConsume(_queueName, false, consumer); while (true) { - Task.Delay(timeout); + Task.Delay(timeout, cancellationToken).Wait(); } } @@ -87,7 +87,7 @@ namespace DotNetCore.CAP.RabbitMQ var message = new MessageContext { Group = _queueName, - KeyName = e.RoutingKey, + Name = e.RoutingKey, Content = Encoding.UTF8.GetString(e.Body) }; MessageReceieved?.Invoke(sender, message); diff --git a/src/DotNetCore.CAP.SqlServer/CAP.EFOptions.cs b/src/DotNetCore.CAP.SqlServer/CAP.EFOptions.cs new file mode 100644 index 0000000..6d162c5 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/CAP.EFOptions.cs @@ -0,0 +1,21 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class EFOptions + { + public const string DefaultSchema = "Cap"; + + /// + /// Gets or sets the schema to use when creating database objects. + /// Default is . + /// + public string Schema { get; set; } = DefaultSchema; + + /// + /// EF dbcontext type. + /// + public Type DbContextType { get; internal set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CAP.Options.Extensions.cs b/src/DotNetCore.CAP.SqlServer/CAP.Options.Extensions.cs new file mode 100644 index 0000000..d87e260 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/CAP.Options.Extensions.cs @@ -0,0 +1,49 @@ +using System; +using DotNetCore.CAP; +using Microsoft.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CapOptionsExtensions + { + public static CapOptions UseSqlServer(this CapOptions options, string connectionString) + { + return options.UseSqlServer(opt => + { + opt.ConnectionString = connectionString; + }); + } + + public static CapOptions UseSqlServer(this CapOptions options, Action configure) + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + options.RegisterExtension(new SqlServerCapOptionsExtension(configure)); + + return options; + } + + public static CapOptions UseEntityFramework(this CapOptions options) + where TContext : DbContext + { + return options.UseEntityFramework(opt => + { + opt.DbContextType = typeof(TContext); + }); + } + + public static CapOptions UseEntityFramework(this CapOptions options, Action configure) + where TContext : DbContext + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + var efOptions = new EFOptions { DbContextType = typeof(TContext) }; + configure(efOptions); + + options.RegisterExtension(new SqlServerCapOptionsExtension(configure)); + + return options; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs new file mode 100644 index 0000000..0425b8f --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs @@ -0,0 +1,45 @@ +using System; +using DotNetCore.CAP.Processor; +using DotNetCore.CAP.SqlServer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class SqlServerCapOptionsExtension : ICapOptionsExtension + { + private readonly Action _configure; + + public SqlServerCapOptionsExtension(Action configure) + { + _configure = configure; + } + + public void AddServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + + var sqlServerOptions = new SqlServerOptions(); + _configure(sqlServerOptions); + + var provider = TempBuildService(services); + var dbContextObj = provider.GetService(sqlServerOptions.DbContextType); + if (dbContextObj != null) + { + var dbContext = (DbContext)dbContextObj; + sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + } + services.Configure(_configure); + services.AddSingleton(sqlServerOptions); + } + + private IServiceProvider TempBuildService(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CAP.SqlServerOptions.cs b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerOptions.cs new file mode 100644 index 0000000..a0f3fa2 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerOptions.cs @@ -0,0 +1,11 @@ +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class SqlServerOptions : EFOptions + { + /// + /// Gets or sets the database's connection string that will be used to store database entities. + /// + public string ConnectionString { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CapPublisher.cs b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs new file mode 100644 index 0000000..3cba0a0 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs @@ -0,0 +1,96 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace DotNetCore.CAP.SqlServer +{ + public class CapPublisher : ICapPublisher + { + private readonly SqlServerOptions _options; + private readonly DbContext _dbContext; + + protected bool IsUsingEF { get; } + protected IServiceProvider ServiceProvider { get; } + + public CapPublisher(IServiceProvider provider, SqlServerOptions options) + { + ServiceProvider = provider; + _options = options; + + if (_options.DbContextType != null) + { + IsUsingEF = true; + _dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType); + } + } + + public Task PublishAsync(string name, string content) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (!IsUsingEF) throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + + " otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); + + return Publish(name, content); + } + + public Task PublishAsync(string name, T contentObj) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (!IsUsingEF) throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + + " otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); + + var content = Helper.ToJson(contentObj); + return Publish(name, content); + } + + public Task PublishAsync(string name, string content, IDbConnection dbConnection) + { + if (IsUsingEF) throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); + if (name == null) throw new ArgumentNullException(nameof(name)); + if (dbConnection == null) throw new ArgumentNullException(nameof(dbConnection)); + + var dbTransaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + return PublishWithTrans(name, content, dbConnection, dbTransaction); + } + + public Task PublishAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + { + if (IsUsingEF) throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); + if (name == null) throw new ArgumentNullException(nameof(name)); + if (dbConnection == null) throw new ArgumentNullException(nameof(dbConnection)); + if (dbTransaction == null) throw new ArgumentNullException(nameof(dbTransaction)); + + return PublishWithTrans(name, content, dbConnection, dbTransaction); + } + + private async Task Publish(string name, string content) + { + var connection = _dbContext.Database.GetDbConnection(); + var transaction = _dbContext.Database.CurrentTransaction; + transaction = transaction ?? await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); + var dbTransaction = transaction.GetDbTransaction(); + await PublishWithTrans(name, content, connection, dbTransaction); + } + + private async Task PublishWithTrans(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + { + var message = new CapPublishedMessage + { + Name = name, + Content = content, + StatusName = StatusName.Scheduled + }; + + var sql = $"INSERT INTO {_options.Schema}.[Published] ([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName])VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)"; + await dbConnection.ExecuteAsync(sql, message, transaction: dbTransaction); + + PublishQueuer.PulseEvent.Set(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.EntityFrameworkCore/DotNetCore.CAP.EntityFrameworkCore.csproj b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj similarity index 78% rename from src/DotNetCore.CAP.EntityFrameworkCore/DotNetCore.CAP.EntityFrameworkCore.csproj rename to src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj index a1f9510..c2b12cb 100644 --- a/src/DotNetCore.CAP.EntityFrameworkCore/DotNetCore.CAP.EntityFrameworkCore.csproj +++ b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj @@ -4,8 +4,8 @@ netstandard1.6 - DotNetCore.CAP.EntityFrameworkCore - DotNetCore.CAP.EntityFrameworkCore + DotNetCore.CAP.SqlServer + DotNetCore.CAP.SqlServer 1.6.1 $(PackageTargetFallback);dnxcore50 false @@ -14,9 +14,9 @@ + - - + diff --git a/src/DotNetCore.CAP.SqlServer/FetchedMessage.cs b/src/DotNetCore.CAP.SqlServer/FetchedMessage.cs new file mode 100644 index 0000000..521fdeb --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/FetchedMessage.cs @@ -0,0 +1,11 @@ +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.SqlServer +{ + public class FetchedMessage + { + public int MessageId { get; set; } + + public MessageType MessageType { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs new file mode 100644 index 0000000..65490ab --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs @@ -0,0 +1,63 @@ +using System; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Processor; +using Microsoft.Extensions.Logging; + +namespace DotNetCore.CAP.SqlServer +{ + public class DefaultAdditionalProcessor : IAdditionalProcessor + { + private readonly IServiceProvider _provider; + private readonly ILogger _logger; + private readonly SqlServerOptions _options; + + private const int MaxBatch = 1000; + private readonly TimeSpan _delay = TimeSpan.FromSeconds(1); + private readonly TimeSpan _waitingInterval = TimeSpan.FromHours(2); + + private static readonly string[] Tables = + { + "Published","Received" + }; + + public DefaultAdditionalProcessor( + IServiceProvider provider, + ILogger logger, + SqlServerOptions sqlServerOptions) + { + _logger = logger; + _provider = provider; + _options = sqlServerOptions; + } + + public async Task ProcessAsync(ProcessingContext context) + { + _logger.LogDebug("Collecting expired entities."); + + foreach (var table in Tables) + { + var removedCount = 0; + do + { + using (var connection = new SqlConnection(_options.ConnectionString)) + { + removedCount = await connection.ExecuteAsync($@" +DELETE TOP (@count) +FROM [{_options.Schema}].[{table}] WITH (readpast) +WHERE ExpiresAt < @now;", new { now = DateTime.Now, count = MaxBatch }); + } + + if (removedCount != 0) + { + await context.WaitAsync(_delay); + context.ThrowIfStopping(); + } + } while (removedCount != 0); + } + + await context.WaitAsync(_waitingInterval); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerFetchedMessage.cs b/src/DotNetCore.CAP.SqlServer/SqlServerFetchedMessage.cs new file mode 100644 index 0000000..06bc461 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/SqlServerFetchedMessage.cs @@ -0,0 +1,74 @@ +using System; +using System.Data; +using System.Threading; +using Dapper; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.SqlServer +{ + public class SqlServerFetchedMessage : IFetchedMessage + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + private readonly Timer _timer; + private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromMinutes(1); + private readonly object _lockObject = new object(); + + public SqlServerFetchedMessage(int messageId, + MessageType type, + IDbConnection connection, + IDbTransaction transaction) + { + MessageId = messageId; + MessageType = type; + _connection = connection; + _transaction = transaction; + _timer = new Timer(ExecuteKeepAliveQuery, null, KeepAliveInterval, KeepAliveInterval); + } + + public int MessageId { get; } + + public MessageType MessageType { get; } + + public void RemoveFromQueue() + { + lock (_lockObject) + { + _transaction.Commit(); + } + } + + public void Requeue() + { + lock (_lockObject) + { + _transaction.Rollback(); + } + } + + public void Dispose() + { + lock (_lockObject) + { + _timer?.Dispose(); + _transaction.Dispose(); + _connection.Dispose(); + } + } + + private void ExecuteKeepAliveQuery(object obj) + { + lock (_lockObject) + { + try + { + _connection?.Execute("SELECT 1", _transaction); + } + catch + { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs b/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs new file mode 100644 index 0000000..57d7e5d --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs @@ -0,0 +1,88 @@ +using System.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace DotNetCore.CAP.SqlServer +{ + public class SqlServerStorage : IStorage + { + private readonly SqlServerOptions _options; + private readonly ILogger _logger; + + public SqlServerStorage(ILogger logger, SqlServerOptions options) + { + _options = options; + _logger = logger; + } + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + + var sql = CreateDbTablesScript(_options.Schema); + + using (var connection = new SqlConnection(_options.ConnectionString)) + { + await connection.ExecuteAsync(sql); + } + _logger.LogDebug("Ensuring all create database tables script are applied."); + } + + protected virtual string CreateDbTablesScript(string schema) + { + var batchSql = + $@" +IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '{schema}') +BEGIN + EXEC('CREATE SCHEMA {schema}') +END; + +IF OBJECT_ID(N'[{schema}].[Queue]',N'U') IS NULL +BEGIN + CREATE TABLE [{schema}].[Queue]( + [MessageId] [int] NOT NULL, + [MessageType] [tinyint] NOT NULL + ) ON [PRIMARY] +END; + +IF OBJECT_ID(N'[{schema}].[Received]',N'U') IS NULL +BEGIN +CREATE TABLE [{schema}].[Received]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [Name] [nvarchar](200) NOT NULL, + [Group] [nvarchar](200) NULL, + [Content] [nvarchar](max) NULL, + [Retries] [int] NOT NULL, + [Added] [datetime2](7) NOT NULL, + [ExpiresAt] [datetime2](7) NULL, + [StatusName] [nvarchar](50) NOT NULL, + CONSTRAINT [PK_{schema}.Received] PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +END; + +IF OBJECT_ID(N'[{schema}].[Published]',N'U') IS NULL +BEGIN +CREATE TABLE [{schema}].[Published]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [Name] [nvarchar](200) NOT NULL, + [Content] [nvarchar](max) NULL, + [Retries] [int] NOT NULL, + [Added] [datetime2](7) NOT NULL, + [ExpiresAt] [datetime2](7) NULL, + [StatusName] [nvarchar](50) NOT NULL, + CONSTRAINT [PK_{schema}.Published] PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +END;"; + return batchSql; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerStorageConnection.cs b/src/DotNetCore.CAP.SqlServer/SqlServerStorageConnection.cs new file mode 100644 index 0000000..b5d56c3 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/SqlServerStorageConnection.cs @@ -0,0 +1,124 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using Microsoft.EntityFrameworkCore; + +namespace DotNetCore.CAP.SqlServer +{ + public class SqlServerStorageConnection : IStorageConnection + { + private readonly SqlServerOptions _options; + + public SqlServerStorageConnection(SqlServerOptions options) + { + _options = options; + } + + public SqlServerOptions Options => _options; + + public IStorageTransaction CreateTransaction() + { + return new SqlServerStorageTransaction(this); + } + + public async Task GetPublishedMessageAsync(int id) + { + var sql = $@"SELECT * FROM [{_options.Schema}].[Published] WITH (readpast) WHERE Id={id}"; + + using (var connection = new SqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public Task FetchNextMessageAsync() + { + var sql = $@" +DELETE TOP (1) +FROM [{_options.Schema}].[Queue] WITH (readpast, updlock, rowlock) +OUTPUT DELETED.MessageId,DELETED.[MessageType];"; + + return FetchNextMessageCoreAsync(sql); + } + + public async Task GetNextPublishedMessageToBeEnqueuedAsync() + { + var sql = $"SELECT TOP (1) * FROM [{_options.Schema}].[Published] WITH (readpast) WHERE StatusName = '{StatusName.Scheduled}'"; + + using (var connection = new SqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + // CapReceviedMessage + + public async Task StoreReceivedMessageAsync(CapReceivedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $@" +INSERT INTO [{_options.Schema}].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) +VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; + + using (var connection = new SqlConnection(_options.ConnectionString)) + { + await connection.ExecuteAsync(sql, message); + } + } + + public async Task GetReceivedMessageAsync(int id) + { + var sql = $@"SELECT * FROM [{_options.Schema}].[Received] WITH (readpast) WHERE Id={id}"; + using (var connection = new SqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public async Task GetNextReceviedMessageToBeEnqueuedAsync() + { + var sql = $"SELECT TOP (1) * FROM [{_options.Schema}].[Received] WITH (readpast) WHERE StatusName = '{StatusName.Scheduled}'"; + using (var connection = new SqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public void Dispose() + { + } + + private async Task FetchNextMessageCoreAsync(string sql, object args = null) + { + //here don't use `using` to dispose + var connection = new SqlConnection(_options.ConnectionString); + await connection.OpenAsync(); + var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + FetchedMessage fetchedMessage = null; + try + { + fetchedMessage = await connection.QueryFirstOrDefaultAsync(sql, args, transaction); + } + catch (SqlException) + { + transaction.Dispose(); + throw; + } + + if (fetchedMessage == null) + { + transaction.Rollback(); + transaction.Dispose(); + connection.Dispose(); + return null; + } + + return new SqlServerFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerStorageTransaction.cs b/src/DotNetCore.CAP.SqlServer/SqlServerStorageTransaction.cs new file mode 100644 index 0000000..b30c616 --- /dev/null +++ b/src/DotNetCore.CAP.SqlServer/SqlServerStorageTransaction.cs @@ -0,0 +1,71 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.SqlServer +{ + public class SqlServerStorageTransaction : IStorageTransaction, IDisposable + { + private readonly string _schema; + + private readonly IDbTransaction _dbTransaction; + private readonly IDbConnection _dbConnection; + + public SqlServerStorageTransaction(SqlServerStorageConnection connection) + { + var options = connection.Options; + _schema = options.Schema; + + _dbConnection = new SqlConnection(options.ConnectionString); + _dbConnection.Open(); + _dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + } + + public void UpdateMessage(CapPublishedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $"UPDATE [{_schema}].[Published] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;"; + _dbConnection.Execute(sql, message, _dbTransaction); + } + + public void UpdateMessage(CapReceivedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $"UPDATE [{_schema}].[Received] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;"; + _dbConnection.Execute(sql, message, _dbTransaction); + } + + public void EnqueueMessage(CapPublishedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);"; + _dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Publish }, _dbTransaction); + } + + public void EnqueueMessage(CapReceivedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);"; + _dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Subscribe }, _dbTransaction); + } + + public Task CommitAsync() + { + _dbTransaction.Commit(); + return Task.CompletedTask; + } + + public void Dispose() + { + _dbTransaction.Dispose(); + _dbConnection.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Abstractions/ModelBinding/IModelBinder.cs b/src/DotNetCore.CAP/Abstractions/ModelBinding/IModelBinder.cs index c8c976c..f03b105 100644 --- a/src/DotNetCore.CAP/Abstractions/ModelBinding/IModelBinder.cs +++ b/src/DotNetCore.CAP/Abstractions/ModelBinding/IModelBinder.cs @@ -14,7 +14,7 @@ namespace DotNetCore.CAP.Abstractions.ModelBinding /// /// /// A which will complete when the model binding process completes. - /// + /// /// Task BindModelAsync(ModelBindingContext bindingContext); } diff --git a/src/DotNetCore.CAP/Abstractions/TopicAttribute.cs b/src/DotNetCore.CAP/Abstractions/TopicAttribute.cs index 1e1ef2d..bc4bb8f 100644 --- a/src/DotNetCore.CAP/Abstractions/TopicAttribute.cs +++ b/src/DotNetCore.CAP/Abstractions/TopicAttribute.cs @@ -8,9 +8,9 @@ namespace DotNetCore.CAP.Abstractions [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public abstract class TopicAttribute : Attribute { - protected TopicAttribute(string topicName) + protected TopicAttribute(string name) { - Name = topicName; + Name = name; } /// diff --git a/src/DotNetCore.CAP/CAP.Builder.cs b/src/DotNetCore.CAP/CAP.Builder.cs index 29d5fc5..e9cf165 100644 --- a/src/DotNetCore.CAP/CAP.Builder.cs +++ b/src/DotNetCore.CAP/CAP.Builder.cs @@ -1,5 +1,4 @@ using System; -using DotNetCore.CAP.Job; using Microsoft.Extensions.DependencyInjection; namespace DotNetCore.CAP @@ -35,38 +34,6 @@ namespace DotNetCore.CAP return this; } - /// - /// Adds a singleton service of the type specified in serviceType with an implementation - /// - private CapBuilder AddSingleton() - where TService : class - where TImplementation : class, TService - { - Services.AddSingleton(); - return this; - } - - /// - /// Add an . - /// - /// The type for the to add. - /// The current instance. - public virtual CapBuilder AddMessageStore() - where T : class, ICapMessageStore - { - return AddScoped(typeof(ICapMessageStore), typeof(T)); - } - - /// - /// Add an for process . - /// - /// The type of the job. - public virtual CapBuilder AddJobs() - where T : class, IJob - { - return AddSingleton(); - } - /// /// Add an . /// diff --git a/src/DotNetCore.CAP/CAP.Options.cs b/src/DotNetCore.CAP/CAP.Options.cs index 1b18152..334c709 100644 --- a/src/DotNetCore.CAP/CAP.Options.cs +++ b/src/DotNetCore.CAP/CAP.Options.cs @@ -1,34 +1,53 @@ -namespace DotNetCore.CAP +using System; +using System.Collections.Generic; + +namespace DotNetCore.CAP { /// /// Represents all the options you can use to configure the system. /// public class CapOptions { + internal IList Extensions { get; private set; } + /// /// Default value for polling delay timeout, in seconds. /// public const int DefaultPollingDelay = 8; - /// - /// Default value for CAP job. - /// - public const string DefaultCronExp = "* * * * *"; - public CapOptions() { - CronExp = DefaultCronExp; PollingDelay = DefaultPollingDelay; + Extensions = new List(); } /// - /// Corn expression for configuring retry cron job. Default is 1 min. + /// Productor job polling delay time. Default is 8 sec. /// - public string CronExp { get; set; } + public int PollingDelay { get; set; } = 8; /// - /// Productor job polling delay time. Default is 8 sec. + /// We’ll send a POST request to the URL below with details of any subscribed events. /// - public int PollingDelay { get; set; } = 8; + public WebHook WebHook { get; set; } + + /// + /// Registers an extension that will be executed when building services. + /// + /// + public void RegisterExtension(ICapOptionsExtension extension) + { + if (extension == null) + throw new ArgumentNullException(nameof(extension)); + + Extensions.Add(extension); + } + } + + public class WebHook + { + public string PayloadUrl { get; set; } + + public string Secret { get; set; } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs b/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs index bd58997..7e3908d 100644 --- a/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs +++ b/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs @@ -6,7 +6,8 @@ using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions.ModelBinding; using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Internal; -using DotNetCore.CAP.Job; +using DotNetCore.CAP.Processor; +using DotNetCore.CAP.Processor.States; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection @@ -16,16 +17,6 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class ServiceCollectionExtensions { - /// - /// Adds and configures the CAP services for the consitence. - /// - /// The services available in the application. - /// An for application services. - public static CapBuilder AddCap(this IServiceCollection services) - { - return services.AddCap(x => new CapOptions()); - } - /// /// Adds and configures the consistence services for the consitence. /// @@ -36,10 +27,12 @@ namespace Microsoft.Extensions.DependencyInjection this IServiceCollection services, Action setupAction) { + if (setupAction == null) throw new ArgumentNullException(nameof(setupAction)); + services.TryAddSingleton(); services.Configure(setupAction); - AddConsumerServices(services); + AddSubscribeServices(services); services.TryAddSingleton(); services.TryAddSingleton(); @@ -47,19 +40,32 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); - services.TryAddTransient(); - services.TryAddSingleton(); - services.TryAddTransient(); + //Processors + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - services.TryAddScoped(); + //Executors + services.AddSingleton(); + services.AddSingleton(); + + //Options and extension service + var options = new CapOptions(); + setupAction(options); + foreach (var serviceExtension in options.Extensions) + { + serviceExtension.AddServices(services); + } + services.AddSingleton(options); return new CapBuilder(services); } - private static void AddConsumerServices(IServiceCollection services) + private static void AddSubscribeServices(IServiceCollection services) { var consumerListenerServices = new Dictionary(); foreach (var rejectedServices in services) diff --git a/src/DotNetCore.CAP/DotNetCore.CAP.csproj b/src/DotNetCore.CAP/DotNetCore.CAP.csproj index b497c57..2bf7ab4 100644 --- a/src/DotNetCore.CAP/DotNetCore.CAP.csproj +++ b/src/DotNetCore.CAP/DotNetCore.CAP.csproj @@ -12,7 +12,7 @@ False - + @@ -20,8 +20,8 @@ - + diff --git a/src/DotNetCore.CAP/IBootstrapper.Default.cs b/src/DotNetCore.CAP/IBootstrapper.Default.cs index a1d2b87..09b9291 100644 --- a/src/DotNetCore.CAP/IBootstrapper.Default.cs +++ b/src/DotNetCore.CAP/IBootstrapper.Default.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,7 +23,7 @@ namespace DotNetCore.CAP public DefaultBootstrapper( ILogger logger, IOptions options, - ICapMessageStore storage, + IStorage storage, IApplicationLifetime appLifetime, IServiceProvider provider) { @@ -52,7 +51,7 @@ namespace DotNetCore.CAP protected CapOptions Options { get; } - protected ICapMessageStore Storage { get; } + protected IStorage Storage { get; } protected IEnumerable Servers { get; } @@ -65,7 +64,7 @@ namespace DotNetCore.CAP private async Task BootstrapTaskAsync() { - if (_cts.IsCancellationRequested) return; + await Storage.InitializeAsync(_cts.Token); if (_cts.IsCancellationRequested) return; @@ -98,7 +97,7 @@ namespace DotNetCore.CAP item.Dispose(); } }); - return Task.FromResult(0); + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/ICapMessageStore.cs b/src/DotNetCore.CAP/ICapMessageStore.cs deleted file mode 100644 index 4440a6a..0000000 --- a/src/DotNetCore.CAP/ICapMessageStore.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; - -namespace DotNetCore.CAP -{ - /// - /// Provides an abstraction for a store which manages CAP message. - /// - public interface ICapMessageStore - { - /// - /// Creates a new message in a store as an asynchronous operation. - /// - /// The message to create in the store. - Task StoreSentMessageAsync(CapSentMessage message); - - /// - /// Change model status name. - /// - /// The type of . - /// The status name. - /// auto save dbcontext changes. - /// - Task ChangeSentMessageStateAsync(CapSentMessage message, string statusName, - bool autoSaveChanges = true); - - /// - /// Fetches the next message to be executed. - /// - /// - Task GetNextSentMessageToBeEnqueuedAsync(); - - /// - /// Updates a message in a store as an asynchronous operation. - /// - /// The message to update in the store. - Task UpdateSentMessageAsync(CapSentMessage message); - - /// - /// Deletes a message from the store as an asynchronous operation. - /// - /// The message to delete in the store. - Task RemoveSentMessageAsync(CapSentMessage message); - - - /// - /// Creates a new message in a store as an asynchronous operation. - /// - /// - /// - Task StoreReceivedMessageAsync(CapReceivedMessage message); - - /// - /// Change model status name. - /// - /// The type of . - /// The status name. - /// auto save dbcontext changes. - /// - Task ChangeReceivedMessageStateAsync(CapReceivedMessage message, string statusName, - bool autoSaveChanges = true); - - /// - /// Fetches the next message to be executed. - /// - Task GetNextReceivedMessageToBeExcuted(); - - /// - /// Updates a message in a store as an asynchronous operation. - /// - /// The message to update in the store. - Task UpdateReceivedMessageAsync(CapReceivedMessage message); - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/ICapOptionsExtension.cs b/src/DotNetCore.CAP/ICapOptionsExtension.cs new file mode 100644 index 0000000..bfeb693 --- /dev/null +++ b/src/DotNetCore.CAP/ICapOptionsExtension.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetCore.CAP +{ + public interface ICapOptionsExtension + { + void AddServices(IServiceCollection services); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/ICapPublisher.Default.cs b/src/DotNetCore.CAP/ICapPublisher.Default.cs deleted file mode 100644 index 454e6e5..0000000 --- a/src/DotNetCore.CAP/ICapPublisher.Default.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using Microsoft.Extensions.Logging; - -namespace DotNetCore.CAP -{ - /// - /// Cap default implement. - /// - public class DefaultCapPublisher : ICapPublisher - { - private readonly ICapMessageStore _store; - private readonly ILogger _logger; - - public DefaultCapPublisher( - ICapMessageStore store, - ILogger logger) - { - _store = store; - _logger = logger; - } - - public Task PublishAsync(string topic, string content) - { - if (topic == null) throw new ArgumentNullException(nameof(topic)); - if (content == null) throw new ArgumentNullException(nameof(content)); - - return StoreMessage(topic, content); - } - - public Task PublishAsync(string topic, T contentObj) - { - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - var content = Helper.ToJson(contentObj); - if (content == null) - throw new InvalidCastException(nameof(contentObj)); - - return StoreMessage(topic, content); - } - - private async Task StoreMessage(string topic, string content) - { - var message = new CapSentMessage - { - KeyName = topic, - Content = content, - StatusName = StatusName.Enqueued - }; - - await _store.StoreSentMessageAsync(message); - - WaitHandleEx.PulseEvent.Set(); - - _logger.EnqueuingSentMessage(topic, content); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/ICapPublisher.cs b/src/DotNetCore.CAP/ICapPublisher.cs index c1bcb99..95f28b0 100644 --- a/src/DotNetCore.CAP/ICapPublisher.cs +++ b/src/DotNetCore.CAP/ICapPublisher.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Data; +using System.Threading.Tasks; namespace DotNetCore.CAP { @@ -9,18 +10,42 @@ namespace DotNetCore.CAP { /// /// Publish a string message to specified topic. + /// + /// If you are using the EntityFramework, you need to configure the DbContextType first. + /// otherwise you need to use overloaded method with IDbConnection and IDbTransaction. + /// /// - /// the topic name or exchange router key. + /// the topic name or exchange router key. /// message body content. - Task PublishAsync(string topic, string content); + Task PublishAsync(string name, string content); /// /// Publis a object message to specified topic. + /// + /// If you are using the EntityFramework, you need to configure the DbContextType first. + /// otherwise you need to use overloaded method with IDbConnection and IDbTransaction. + /// /// /// The type of conetent object. - /// the topic name or exchange router key. + /// the topic name or exchange router key. /// object instance that will be serialized of json. - /// - Task PublishAsync(string topic, T contentObj); + Task PublishAsync(string name, T contentObj); + + /// + /// Publish a string message to specified topic with transacton. + /// + /// the topic name or exchange router key. + /// message body content. + /// the dbConnection of + Task PublishAsync(string name, string content, IDbConnection dbConnection); + + /// + /// Publish a string message to specified topic with transacton. + /// + /// the topic name or exchange router key. + /// message body content. + /// the connection of + /// the transaction of + Task PublishAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction); } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/IConsumerClient.cs b/src/DotNetCore.CAP/IConsumerClient.cs index e3dc29d..c59c20c 100644 --- a/src/DotNetCore.CAP/IConsumerClient.cs +++ b/src/DotNetCore.CAP/IConsumerClient.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using DotNetCore.CAP.Infrastructure; namespace DotNetCore.CAP @@ -12,7 +13,7 @@ namespace DotNetCore.CAP void Subscribe(string topic, int partition); - void Listening(TimeSpan timeout); + void Listening(TimeSpan timeout, CancellationToken cancellationToken); void Commit(); diff --git a/src/DotNetCore.CAP/IConsumerHandler.Default.cs b/src/DotNetCore.CAP/IConsumerHandler.Default.cs index aebf720..6bcf22d 100644 --- a/src/DotNetCore.CAP/IConsumerHandler.Default.cs +++ b/src/DotNetCore.CAP/IConsumerHandler.Default.cs @@ -1,10 +1,10 @@ using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Internal; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,12 +16,11 @@ namespace DotNetCore.CAP private readonly IServiceProvider _serviceProvider; private readonly IConsumerInvokerFactory _consumerInvokerFactory; private readonly IConsumerClientFactory _consumerClientFactory; - private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + private readonly CancellationTokenSource _cts; private readonly MethodMatcherCache _selector; private readonly CapOptions _options; - private readonly CancellationTokenSource _cts; private readonly TimeSpan _pollingDelay = TimeSpan.FromSeconds(1); @@ -32,13 +31,12 @@ namespace DotNetCore.CAP IServiceProvider serviceProvider, IConsumerInvokerFactory consumerInvokerFactory, IConsumerClientFactory consumerClientFactory, - ILoggerFactory loggerFactory, + ILogger logger, MethodMatcherCache selector, IOptions options) { _selector = selector; - _logger = loggerFactory.CreateLogger(); - _loggerFactory = loggerFactory; + _logger = logger; _serviceProvider = serviceProvider; _consumerInvokerFactory = consumerInvokerFactory; _consumerClientFactory = consumerClientFactory; @@ -63,9 +61,9 @@ namespace DotNetCore.CAP client.Subscribe(item.Attribute.Name); } - client.Listening(_pollingDelay); + client.Listening(_pollingDelay, _cts.Token); } - }, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); + }, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } _compositeTask = Task.CompletedTask; } @@ -83,7 +81,7 @@ namespace DotNetCore.CAP try { - _compositeTask.Wait((int)TimeSpan.FromSeconds(60).TotalMilliseconds); + _compositeTask.Wait(TimeSpan.FromSeconds(60)); } catch (AggregateException ex) { @@ -99,56 +97,32 @@ namespace DotNetCore.CAP { client.MessageReceieved += (sender, message) => { - _logger.EnqueuingReceivedMessage(message.KeyName, message.Content); + _logger.EnqueuingReceivedMessage(message.Name, message.Content); using (var scope = _serviceProvider.CreateScope()) { var receviedMessage = StoreMessage(scope, message); client.Commit(); - ProcessMessage(scope, receviedMessage); } + Pulse(); }; } private CapReceivedMessage StoreMessage(IServiceScope serviceScope, MessageContext messageContext) { var provider = serviceScope.ServiceProvider; - var messageStore = provider.GetRequiredService(); + var messageStore = provider.GetRequiredService(); var receivedMessage = new CapReceivedMessage(messageContext) { - StatusName = StatusName.Enqueued, + StatusName = StatusName.Scheduled, }; messageStore.StoreReceivedMessageAsync(receivedMessage).Wait(); return receivedMessage; } - private void ProcessMessage(IServiceScope serviceScope, CapReceivedMessage receivedMessage) + public void Pulse() { - var provider = serviceScope.ServiceProvider; - var messageStore = provider.GetRequiredService(); - try - { - var executeDescriptorGroup = _selector.GetTopicExector(receivedMessage.KeyName); - - if (executeDescriptorGroup.ContainsKey(receivedMessage.Group)) - { - messageStore.ChangeReceivedMessageStateAsync(receivedMessage, StatusName.Processing).Wait(); - - // If there are multiple consumers in the same group, we will take the first - var executeDescriptor = executeDescriptorGroup[receivedMessage.Group][0]; - var consumerContext = new ConsumerContext(executeDescriptor, receivedMessage.ToMessageContext()); - - _consumerInvokerFactory.CreateInvoker(consumerContext).InvokeAsync(); - - messageStore.ChangeReceivedMessageStateAsync(receivedMessage, StatusName.Succeeded).Wait(); - } - } - catch (Exception ex) - { - _logger.ConsumerMethodExecutingFailed($"Group:{receivedMessage.Group}, Topic:{receivedMessage.KeyName}", ex); - } + SubscribeQueuer.PulseEvent.Set(); } - - } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/IFetchedMessage.cs b/src/DotNetCore.CAP/IFetchedMessage.cs new file mode 100644 index 0000000..ca7acee --- /dev/null +++ b/src/DotNetCore.CAP/IFetchedMessage.cs @@ -0,0 +1,16 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP +{ + public interface IFetchedMessage : IDisposable + { + int MessageId { get; } + + MessageType MessageType { get; } + + void RemoveFromQueue(); + + void Requeue(); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IProcessingServer.cs b/src/DotNetCore.CAP/IProcessingServer.cs index ccb01f3..f5f3533 100644 --- a/src/DotNetCore.CAP/IProcessingServer.cs +++ b/src/DotNetCore.CAP/IProcessingServer.cs @@ -7,6 +7,8 @@ namespace DotNetCore.CAP /// public interface IProcessingServer : IDisposable { + void Pulse(); + void Start(); } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs b/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs new file mode 100644 index 0000000..a032af6 --- /dev/null +++ b/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.Logging; + +namespace DotNetCore.CAP +{ + public abstract class BasePublishQueueExecutor : IQueueExecutor + { + private readonly IStateChanger _stateChanger; + private readonly ILogger _logger; + + protected BasePublishQueueExecutor(IStateChanger stateChanger, + ILogger logger) + { + _stateChanger = stateChanger; + _logger = logger; + } + + public abstract Task PublishAsync(string keyName, string content); + + public async Task ExecuteAsync(IStorageConnection connection, IFetchedMessage fetched) + { + var message = await connection.GetPublishedMessageAsync(fetched.MessageId); + try + { + var sp = Stopwatch.StartNew(); + await _stateChanger.ChangeStateAsync(message, new ProcessingState(), connection); + + if (message.Retries > 0) + { + _logger.JobRetrying(message.Retries); + } + var result = await PublishAsync(message.Name, message.Content); + sp.Stop(); + + var newState = default(IState); + if (!result.Succeeded) + { + var shouldRetry = await UpdateMessageForRetryAsync(message, connection); + if (shouldRetry) + { + newState = new ScheduledState(); + _logger.JobFailedWillRetry(result.Exception); + } + else + { + newState = new FailedState(); + _logger.JobFailed(result.Exception); + } + } + else + { + newState = new SucceededState(); + } + await _stateChanger.ChangeStateAsync(message, newState, connection); + + fetched.RemoveFromQueue(); + + if (result.Succeeded) + { + _logger.JobExecuted(sp.Elapsed.TotalSeconds); + } + + return OperateResult.Success; + } + catch (Exception ex) + { + _logger.ExceptionOccuredWhileExecutingJob(message?.Name, ex); + return OperateResult.Failed(ex); + } + } + + private async Task UpdateMessageForRetryAsync(CapPublishedMessage message, IStorageConnection connection) + { + var retryBehavior = RetryBehavior.DefaultRetry; + + var now = DateTime.Now; + var retries = ++message.Retries; + if (retries >= retryBehavior.RetryCount) + { + return false; + } + + var due = message.Added.AddSeconds(retryBehavior.RetryIn(retries)); + message.ExpiresAt = due; + using (var transaction = connection.CreateTransaction()) + { + transaction.UpdateMessage(message); + await transaction.CommitAsync(); + } + return true; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs b/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs new file mode 100644 index 0000000..e335637 --- /dev/null +++ b/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs @@ -0,0 +1,143 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using DotNetCore.CAP.Abstractions; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Internal; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.Logging; + +namespace DotNetCore.CAP +{ + public class SubscibeQueueExecutor : IQueueExecutor + { + private readonly IConsumerInvokerFactory _consumerInvokerFactory; + private readonly IStateChanger _stateChanger; + private readonly ILogger _logger; + + private readonly MethodMatcherCache _selector; + + public SubscibeQueueExecutor( + IStateChanger stateChanger, + MethodMatcherCache selector, + IConsumerInvokerFactory consumerInvokerFactory, + ILogger logger) + { + _selector = selector; + _consumerInvokerFactory = consumerInvokerFactory; + _stateChanger = stateChanger; + _logger = logger; + } + + public async Task ExecuteAsync(IStorageConnection connection, IFetchedMessage fetched) + { + var message = await connection.GetReceivedMessageAsync(fetched.MessageId); + try + { + var sp = Stopwatch.StartNew(); + await _stateChanger.ChangeStateAsync(message, new ProcessingState(), connection); + + if (message.Retries > 0) + { + _logger.JobRetrying(message.Retries); + } + var result = await ExecuteSubscribeAsync(message); + sp.Stop(); + + var newState = default(IState); + if (!result.Succeeded) + { + var shouldRetry = await UpdateMessageForRetryAsync(message, connection); + if (shouldRetry) + { + newState = new ScheduledState(); + _logger.JobFailedWillRetry(result.Exception); + } + else + { + newState = new FailedState(); + _logger.JobFailed(result.Exception); + } + } + else + { + newState = new SucceededState(); + } + await _stateChanger.ChangeStateAsync(message, newState, connection); + + fetched.RemoveFromQueue(); + + if (result.Succeeded) + { + _logger.JobExecuted(sp.Elapsed.TotalSeconds); + } + + return OperateResult.Success; + } + catch (SubscriberNotFoundException ex) + { + _logger.LogError(ex.Message); + return OperateResult.Failed(ex); + } + catch (Exception ex) + { + _logger.ExceptionOccuredWhileExecutingJob(message?.Name, ex); + return OperateResult.Failed(ex); + } + } + + protected virtual async Task ExecuteSubscribeAsync(CapReceivedMessage receivedMessage) + { + try + { + var executeDescriptorGroup = _selector.GetTopicExector(receivedMessage.Name); + + if (!executeDescriptorGroup.ContainsKey(receivedMessage.Group)) + { + throw new SubscriberNotFoundException(receivedMessage.Name + " has not been found."); + } + + // If there are multiple consumers in the same group, we will take the first + var executeDescriptor = executeDescriptorGroup[receivedMessage.Group][0]; + var consumerContext = new ConsumerContext(executeDescriptor, receivedMessage.ToMessageContext()); + + await _consumerInvokerFactory.CreateInvoker(consumerContext).InvokeAsync(); + + return OperateResult.Success; + } + catch (SubscriberNotFoundException ex) + { + _logger.LogError("Can not be found subscribe method of name: " + receivedMessage.Name); + return OperateResult.Failed(ex); + } + catch (Exception ex) + { + _logger.ConsumerMethodExecutingFailed($"Group:{receivedMessage.Group}, Topic:{receivedMessage.Name}", ex); + return OperateResult.Failed(ex); + } + } + + private async Task UpdateMessageForRetryAsync(CapReceivedMessage message, IStorageConnection connection) + { + var retryBehavior = RetryBehavior.DefaultRetry; + + var now = DateTime.Now; + var retries = ++message.Retries; + if (retries >= retryBehavior.RetryCount) + { + return false; + } + + var due = message.Added.AddSeconds(retryBehavior.RetryIn(retries)); + message.ExpiresAt = due; + using (var transaction = connection.CreateTransaction()) + { + transaction.UpdateMessage(message); + await transaction.CommitAsync(); + } + return true; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IQueueExecutor.cs b/src/DotNetCore.CAP/IQueueExecutor.cs new file mode 100644 index 0000000..ad4a6b1 --- /dev/null +++ b/src/DotNetCore.CAP/IQueueExecutor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace DotNetCore.CAP +{ + public interface IQueueExecutor + { + Task ExecuteAsync(IStorageConnection connection, IFetchedMessage message); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IQueueExecutorFactory.cs b/src/DotNetCore.CAP/IQueueExecutorFactory.cs new file mode 100644 index 0000000..5b46ac6 --- /dev/null +++ b/src/DotNetCore.CAP/IQueueExecutorFactory.cs @@ -0,0 +1,9 @@ +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP +{ + public interface IQueueExecutorFactory + { + IQueueExecutor GetInstance(MessageType messageType); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IStorage.cs b/src/DotNetCore.CAP/IStorage.cs new file mode 100644 index 0000000..ea5726d --- /dev/null +++ b/src/DotNetCore.CAP/IStorage.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace DotNetCore.CAP +{ + /// + /// Represents a persisted storage. + /// + public interface IStorage + { + /// + /// Initializes the storage. For example, making sure a database is created and migrations are applied. + /// + Task InitializeAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IStorageConnection.cs b/src/DotNetCore.CAP/IStorageConnection.cs new file mode 100644 index 0000000..4c16a5f --- /dev/null +++ b/src/DotNetCore.CAP/IStorageConnection.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP +{ + /// + /// Represents a connection to the storage. + /// + public interface IStorageConnection : IDisposable + { + //Sent messages + + /// + /// Returns the message with the given id. + /// + /// The message's id. + Task GetPublishedMessageAsync(int id); + + /// + /// Fetches the next message to be executed. + /// + Task FetchNextMessageAsync(); + + /// + /// Returns the next message to be enqueued. + /// + Task GetNextPublishedMessageToBeEnqueuedAsync(); + + // Received messages + + /// + /// Stores the message. + /// + /// The message to store. + Task StoreReceivedMessageAsync(CapReceivedMessage message); + + /// + /// Returns the message with the given id. + /// + /// The message's id. + Task GetReceivedMessageAsync(int id); + + /// + /// Returns the next message to be enqueued. + /// + Task GetNextReceviedMessageToBeEnqueuedAsync(); + + //----------------------------------------- + + /// + /// Creates and returns an . + /// + IStorageTransaction CreateTransaction(); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/IStorageTransaction.cs b/src/DotNetCore.CAP/IStorageTransaction.cs new file mode 100644 index 0000000..788d4e1 --- /dev/null +++ b/src/DotNetCore.CAP/IStorageTransaction.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP +{ + public interface IStorageTransaction : IDisposable + { + void UpdateMessage(CapPublishedMessage message); + + void UpdateMessage(CapReceivedMessage message); + + void EnqueueMessage(CapPublishedMessage message); + + void EnqueueMessage(CapReceivedMessage message); + + Task CommitAsync(); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Infrastructure/Helper.cs b/src/DotNetCore.CAP/Infrastructure/Helper.cs index a6c8bb8..b3b6c29 100644 --- a/src/DotNetCore.CAP/Infrastructure/Helper.cs +++ b/src/DotNetCore.CAP/Infrastructure/Helper.cs @@ -4,9 +4,9 @@ using Newtonsoft.Json; namespace DotNetCore.CAP.Infrastructure { - internal static class Helper + public static class Helper { - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local); private static JsonSerializerSettings _serializerSettings; public static void SetSerializerSettings(JsonSerializerSettings setting) @@ -40,7 +40,7 @@ namespace DotNetCore.CAP.Infrastructure public static long ToTimestamp(DateTime value) { var elapsedTime = value - Epoch; - return (long) elapsedTime.TotalSeconds; + return (long)elapsedTime.TotalSeconds; } public static DateTime FromTimestamp(long value) diff --git a/src/DotNetCore.CAP/Infrastructure/StatusName.cs b/src/DotNetCore.CAP/Infrastructure/StatusName.cs index 459c17d..62ee0fd 100644 --- a/src/DotNetCore.CAP/Infrastructure/StatusName.cs +++ b/src/DotNetCore.CAP/Infrastructure/StatusName.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DotNetCore.CAP.Infrastructure +namespace DotNetCore.CAP.Infrastructure { /// /// The message status name. /// public struct StatusName { + public const string Scheduled = nameof(Scheduled); public const string Enqueued = nameof(Enqueued); public const string Processing = nameof(Processing); public const string Succeeded = nameof(Succeeded); public const string Failed = nameof(Failed); } -} +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Infrastructure/WaitHandleEx.cs b/src/DotNetCore.CAP/Infrastructure/WaitHandleEx.cs index 385959f..4afa6d6 100644 --- a/src/DotNetCore.CAP/Infrastructure/WaitHandleEx.cs +++ b/src/DotNetCore.CAP/Infrastructure/WaitHandleEx.cs @@ -6,8 +6,6 @@ namespace DotNetCore.CAP.Infrastructure { public static class WaitHandleEx { - public static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true); - public static Task WaitAnyAsync(WaitHandle handle1, WaitHandle handle2, TimeSpan timeout) { var t1 = handle1.WaitOneAsync(timeout); @@ -23,7 +21,7 @@ namespace DotNetCore.CAP.Infrastructure var tcs = new TaskCompletionSource(); registeredHandle = ThreadPool.RegisterWaitForSingleObject( handle, - (state, timedOut) => ((TaskCompletionSource) state).TrySetResult(!timedOut), + (state, timedOut) => ((TaskCompletionSource)state).TrySetResult(!timedOut), tcs, timeout, true); diff --git a/src/DotNetCore.CAP/Infrastructure/WebHookProvider.cs b/src/DotNetCore.CAP/Infrastructure/WebHookProvider.cs new file mode 100644 index 0000000..d2492dd --- /dev/null +++ b/src/DotNetCore.CAP/Infrastructure/WebHookProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DotNetCore.CAP.Infrastructure +{ + public class WebHookProvider + { + public WebHookProvider() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetCore.CAP/Infrastructure/IConsumerInvokerFactory.cs b/src/DotNetCore.CAP/Internal/IConsumerInvokerFactory.cs similarity index 80% rename from src/DotNetCore.CAP/Infrastructure/IConsumerInvokerFactory.cs rename to src/DotNetCore.CAP/Internal/IConsumerInvokerFactory.cs index de2153b..561c99c 100644 --- a/src/DotNetCore.CAP/Infrastructure/IConsumerInvokerFactory.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerInvokerFactory.cs @@ -1,6 +1,6 @@ using DotNetCore.CAP.Abstractions; -namespace DotNetCore.CAP.Infrastructure +namespace DotNetCore.CAP.Internal { public interface IConsumerInvokerFactory { diff --git a/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs index 44721d2..ab67b97 100644 --- a/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs @@ -44,7 +44,6 @@ namespace DotNetCore.CAP.Internal return executorDescriptorList; } - private static IEnumerable FindConsumersFromInterfaceTypes( IServiceProvider provider) { diff --git a/src/DotNetCore.CAP/Internal/DefaultModelBinder.cs b/src/DotNetCore.CAP/Internal/IModelBinder.Default.cs similarity index 100% rename from src/DotNetCore.CAP/Internal/DefaultModelBinder.cs rename to src/DotNetCore.CAP/Internal/IModelBinder.Default.cs diff --git a/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs b/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs index 0483ab7..332158b 100644 --- a/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs +++ b/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using DotNetCore.CAP.Abstractions; namespace DotNetCore.CAP.Internal @@ -41,7 +41,7 @@ namespace DotNetCore.CAP.Internal /// /// Get a dictionary of specify topic candidates. - /// The Key is Group name, the value is specify topic candidates. + /// The Key is Group name, the value is specify topic candidates. /// /// message topic name public IDictionary> GetTopicExector(string topicName) diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs index ca81421..cb0f99d 100644 --- a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs @@ -130,7 +130,7 @@ namespace DotNetCore.CAP.Internal private static ConsumerMethodExecutor WrapVoidAction(VoidActionExecutor executor) { - return delegate(object target, object[] parameters) + return delegate (object target, object[] parameters) { executor(target, parameters); return null; @@ -192,7 +192,7 @@ namespace DotNetCore.CAP.Internal /// private static async Task CastToObject(Task task) { - return (object) await task; + return (object)await task; } private static Type GetTaskInnerTypeOrNull(Type type) @@ -279,7 +279,7 @@ namespace DotNetCore.CAP.Internal private static Task Convert(object taskAsObject) { - var task = (Task) taskAsObject; + var task = (Task)taskAsObject; return CastToObject(task); } diff --git a/src/DotNetCore.CAP/Internal/SubscriberNotFoundException.cs b/src/DotNetCore.CAP/Internal/SubscriberNotFoundException.cs new file mode 100644 index 0000000..4889a8a --- /dev/null +++ b/src/DotNetCore.CAP/Internal/SubscriberNotFoundException.cs @@ -0,0 +1,19 @@ +using System; + +namespace DotNetCore.CAP.Internal +{ + public class SubscriberNotFoundException : Exception + { + public SubscriberNotFoundException() + { + } + + public SubscriberNotFoundException(string message) : base(message) + { + } + + public SubscriberNotFoundException(string message, Exception inner) : + base(message, inner) + { } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/ComputedCronJob.cs b/src/DotNetCore.CAP/Job/ComputedCronJob.cs deleted file mode 100644 index dcfbe28..0000000 --- a/src/DotNetCore.CAP/Job/ComputedCronJob.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using NCrontab; - -namespace DotNetCore.CAP.Job -{ - public class ComputedCronJob - { - private readonly CronJobRegistry.Entry _entry; - - public ComputedCronJob() - { - } - - public ComputedCronJob(CronJob job) - { - Job = job; - - Schedule = CrontabSchedule.Parse(job.Cron); - if (job.TypeName != null) - { - JobType = Type.GetType(job.TypeName); - } - } - - public ComputedCronJob(CronJob job, CronJobRegistry.Entry entry) - : this(job) - { - _entry = entry; - } - - public CronJob Job { get; set; } - - public CrontabSchedule Schedule { get; set; } - - public Type JobType { get; set; } - - public DateTime Next { get; set; } - - public int Retries { get; set; } - - public DateTime FirstTry { get; set; } - - public RetryBehavior RetryBehavior => _entry.RetryBehavior; - - public void Update(DateTime baseTime) - { - Job.LastRun = baseTime; - } - - public void UpdateNext(DateTime now) - { - var next = Schedule.GetNextOccurrence(now); - var previousNext = Schedule.GetNextOccurrence(Job.LastRun); - Next = next > previousNext ? now : next; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/Cron.cs b/src/DotNetCore.CAP/Job/Cron.cs deleted file mode 100644 index 9899f41..0000000 --- a/src/DotNetCore.CAP/Job/Cron.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; - -namespace DotNetCore.CAP.Job -{ - public class Cron - { - /// - /// Returns cron expression that fires every minute. - /// - public static string Minutely() - { - return "* * * * *"; - } - - /// - /// Returns cron expression that fires every hour at the first minute. - /// - public static string Hourly() - { - return Hourly(minute: 0); - } - - /// - /// Returns cron expression that fires every hour at the specified minute. - /// - /// The minute in which the schedule will be activated (0-59). - public static string Hourly(int minute) - { - return string.Format("{0} * * * *", minute); - } - - /// - /// Returns cron expression that fires every day at 00:00 UTC. - /// - public static string Daily() - { - return Daily(hour: 0); - } - - /// - /// Returns cron expression that fires every day at the first minute of - /// the specified hour in UTC. - /// - /// The hour in which the schedule will be activated (0-23). - public static string Daily(int hour) - { - return Daily(hour, minute: 0); - } - - /// - /// Returns cron expression that fires every day at the specified hour and minute - /// in UTC. - /// - /// The hour in which the schedule will be activated (0-23). - /// The minute in which the schedule will be activated (0-59). - public static string Daily(int hour, int minute) - { - return string.Format("{0} {1} * * *", minute, hour); - } - - /// - /// Returns cron expression that fires every week at Monday, 00:00 UTC. - /// - public static string Weekly() - { - return Weekly(DayOfWeek.Monday); - } - - /// - /// Returns cron expression that fires every week at 00:00 UTC of the specified - /// day of the week. - /// - /// The day of week in which the schedule will be activated. - public static string Weekly(DayOfWeek dayOfWeek) - { - return Weekly(dayOfWeek, hour: 0); - } - - /// - /// Returns cron expression that fires every week at the first minute - /// of the specified day of week and hour in UTC. - /// - /// The day of week in which the schedule will be activated. - /// The hour in which the schedule will be activated (0-23). - public static string Weekly(DayOfWeek dayOfWeek, int hour) - { - return Weekly(dayOfWeek, hour, minute: 0); - } - - /// - /// Returns cron expression that fires every week at the specified day - /// of week, hour and minute in UTC. - /// - /// The day of week in which the schedule will be activated. - /// The hour in which the schedule will be activated (0-23). - /// The minute in which the schedule will be activated (0-59). - public static string Weekly(DayOfWeek dayOfWeek, int hour, int minute) - { - return string.Format("{0} {1} * * {2}", minute, hour, (int) dayOfWeek); - } - - /// - /// Returns cron expression that fires every month at 00:00 UTC of the first - /// day of month. - /// - public static string Monthly() - { - return Monthly(day: 1); - } - - /// - /// Returns cron expression that fires every month at 00:00 UTC of the specified - /// day of month. - /// - /// The day of month in which the schedule will be activated (1-31). - public static string Monthly(int day) - { - return Monthly(day, hour: 0); - } - - /// - /// Returns cron expression that fires every month at the first minute of the - /// specified day of month and hour in UTC. - /// - /// The day of month in which the schedule will be activated (1-31). - /// The hour in which the schedule will be activated (0-23). - public static string Monthly(int day, int hour) - { - return Monthly(day, hour, minute: 0); - } - - /// - /// Returns cron expression that fires every month at the specified day of month, - /// hour and minute in UTC. - /// - /// The day of month in which the schedule will be activated (1-31). - /// The hour in which the schedule will be activated (0-23). - /// The minute in which the schedule will be activated (0-59). - public static string Monthly(int day, int hour, int minute) - { - return string.Format("{0} {1} {2} * *", minute, hour, day); - } - - /// - /// Returns cron expression that fires every year on Jan, 1st at 00:00 UTC. - /// - public static string Yearly() - { - return Yearly(month: 1); - } - - /// - /// Returns cron expression that fires every year in the first day at 00:00 UTC - /// of the specified month. - /// - /// The month in which the schedule will be activated (1-12). - public static string Yearly(int month) - { - return Yearly(month, day: 1); - } - - /// - /// Returns cron expression that fires every year at 00:00 UTC of the specified - /// month and day of month. - /// - /// The month in which the schedule will be activated (1-12). - /// The day of month in which the schedule will be activated (1-31). - public static string Yearly(int month, int day) - { - return Yearly(month, day, hour: 0); - } - - /// - /// Returns cron expression that fires every year at the first minute of the - /// specified month, day and hour in UTC. - /// - /// The month in which the schedule will be activated (1-12). - /// The day of month in which the schedule will be activated (1-31). - /// The hour in which the schedule will be activated (0-23). - public static string Yearly(int month, int day, int hour) - { - return Yearly(month, day, hour, minute: 0); - } - - /// - /// Returns cron expression that fires every year at the specified month, day, - /// hour and minute in UTC. - /// - /// The month in which the schedule will be activated (1-12). - /// The day of month in which the schedule will be activated (1-31). - /// The hour in which the schedule will be activated (0-23). - /// The minute in which the schedule will be activated (0-59). - public static string Yearly(int month, int day, int hour, int minute) - { - return $"{minute} {hour} {day} {month} *"; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/CronJob.cs b/src/DotNetCore.CAP/Job/CronJob.cs deleted file mode 100644 index f05fd50..0000000 --- a/src/DotNetCore.CAP/Job/CronJob.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace DotNetCore.CAP.Job -{ - /// - /// Represents a cron job to be executed at specified intervals of time. - /// - public class CronJob - { - public CronJob() - { - Id = Guid.NewGuid().ToString(); - } - - public CronJob(string cron) - : this() - { - Cron = cron; - } - - public CronJob(string cron, DateTime lastRun) - : this(cron) - { - LastRun = lastRun; - } - - public string Id { get; set; } - - public string Name { get; set; } - - public string TypeName { get; set; } - - public string Cron { get; set; } - - public DateTime LastRun { get; set; } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/CronJobRegistry.Default.cs b/src/DotNetCore.CAP/Job/CronJobRegistry.Default.cs deleted file mode 100644 index ee6b294..0000000 --- a/src/DotNetCore.CAP/Job/CronJobRegistry.Default.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DotNetCore.CAP.Infrastructure; -using Microsoft.Extensions.Options; - -namespace DotNetCore.CAP.Job -{ - public class DefaultCronJobRegistry : CronJobRegistry - { - public DefaultCronJobRegistry(IOptions options) - { - var options1 = options.Value; - - RegisterJob(nameof(DefaultCronJobRegistry), options1.CronExp, RetryBehavior.DefaultRetry); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/CronJobRegistry.cs b/src/DotNetCore.CAP/Job/CronJobRegistry.cs deleted file mode 100644 index df0d016..0000000 --- a/src/DotNetCore.CAP/Job/CronJobRegistry.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using NCrontab; - -namespace DotNetCore.CAP.Job -{ - public abstract class CronJobRegistry - { - private readonly List _entries; - - protected CronJobRegistry() - { - _entries = new List(); - } - - protected void RegisterJob(string name, string cron, RetryBehavior retryBehavior = null) - where T : IJob - { - RegisterJob(name, typeof(T), cron, retryBehavior); - } - - /// - /// Registers a cron job. - /// - /// The name of the job. - /// The job's type. - /// The cron expression to use. - /// The to use. - protected void RegisterJob(string name, Type jobType, string cron, RetryBehavior retryBehavior = null) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(nameof(cron)); - if (jobType == null) throw new ArgumentNullException(nameof(jobType)); - if (cron == null) throw new ArgumentNullException(nameof(cron)); - retryBehavior = retryBehavior ?? RetryBehavior.DefaultRetry; - - CrontabSchedule.TryParse(cron); - - if (!typeof(IJob).GetTypeInfo().IsAssignableFrom(jobType)) - { - throw new ArgumentException( - "Cron jobs should extend IJob.", nameof(jobType)); - } - - _entries.Add(new Entry(name, jobType, cron)); - } - - public Entry[] Build() => _entries.ToArray(); - - public class Entry - { - public Entry(string name, Type jobType, string cron) - { - Name = name; - JobType = jobType; - Cron = cron; - } - - public string Name { get; set; } - - public Type JobType { get; set; } - - public string Cron { get; set; } - - public RetryBehavior RetryBehavior { get; set; } - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/IJob.CapJob.cs b/src/DotNetCore.CAP/Job/IJob.CapJob.cs deleted file mode 100644 index 138281d..0000000 --- a/src/DotNetCore.CAP/Job/IJob.CapJob.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP.Internal; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using DotNetCore.CAP.Abstractions; -using DotNetCore.CAP.Infrastructure; - -namespace DotNetCore.CAP.Job -{ - public class CapJob : IJob - { - private readonly MethodMatcherCache _selector; - private readonly IConsumerInvokerFactory _consumerInvokerFactory; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly ICapMessageStore _messageStore; - - public CapJob( - ILogger logger, - IServiceProvider serviceProvider, - IConsumerInvokerFactory consumerInvokerFactory, - ICapMessageStore messageStore, - MethodMatcherCache selector) - { - _logger = logger; - _serviceProvider = serviceProvider; - _consumerInvokerFactory = consumerInvokerFactory; - _messageStore = messageStore; - _selector = selector; - } - - public async Task ExecuteAsync() - { - var groupedCandidates = _selector.GetCandidatesMethodsOfGroupNameGrouped(_serviceProvider); - using (var scope = _serviceProvider.CreateScope()) - { - var provider = scope.ServiceProvider; - - var messageStore = provider.GetService(); - var nextReceivedMessage = await messageStore.GetNextReceivedMessageToBeExcuted(); - if (nextReceivedMessage != null && groupedCandidates.ContainsKey(nextReceivedMessage.Group)) - { - try - { - await messageStore.ChangeReceivedMessageStateAsync(nextReceivedMessage, StatusName.Processing); - // If there are multiple consumers in the same group, we will take the first - var executeDescriptor = groupedCandidates[nextReceivedMessage.Group][0]; - var consumerContext = new ConsumerContext(executeDescriptor, nextReceivedMessage.ToMessageContext()); - var invoker = _consumerInvokerFactory.CreateInvoker(consumerContext); - await invoker.InvokeAsync(); - await messageStore.ChangeReceivedMessageStateAsync(nextReceivedMessage, StatusName.Succeeded); - - } - catch (Exception ex) - { - _logger.ReceivedMessageRetryExecutingFailed(nextReceivedMessage.KeyName, ex); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/IJob.cs b/src/DotNetCore.CAP/Job/IJob.cs deleted file mode 100644 index 64518ad..0000000 --- a/src/DotNetCore.CAP/Job/IJob.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading.Tasks; - -namespace DotNetCore.CAP.Job -{ - public interface IJob - { - /// - /// Executes the job. - /// - Task ExecuteAsync(); - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/IJobProcessor.CronJob.cs b/src/DotNetCore.CAP/Job/IJobProcessor.CronJob.cs deleted file mode 100644 index 2c21bf7..0000000 --- a/src/DotNetCore.CAP/Job/IJobProcessor.CronJob.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DotNetCore.CAP.Job -{ - public class CronJobProcessor : IJobProcessor - { - private readonly ILogger _logger; - private IServiceProvider _provider; - private readonly DefaultCronJobRegistry _jobRegistry; - - public CronJobProcessor( - DefaultCronJobRegistry jobRegistry, - ILogger logger, - IServiceProvider provider) - { - _jobRegistry = jobRegistry; - _logger = logger; - _provider = provider; - } - - public override string ToString() => nameof(CronJobProcessor); - - public Task ProcessAsync(ProcessingContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - return ProcessCoreAsync(context); - } - - private async Task ProcessCoreAsync(ProcessingContext context) - { - //var storage = context.Storage; - //var jobs = await GetJobsAsync(storage); - - var jobs = GetJobs(); - if (!jobs.Any()) - { - _logger.CronJobsNotFound(); - - // This will cancel this processor. - throw new OperationCanceledException(); - } - _logger.CronJobsScheduling(jobs); - - context.ThrowIfStopping(); - - var computedJobs = Compute(jobs, context.CronJobRegistry.Build()); - if (context.IsStopping) - { - return; - } - - await Task.WhenAll(computedJobs.Select(j => RunAsync(j, context))); - } - - private async Task RunAsync(ComputedCronJob computedJob, ProcessingContext context) - { - //var storage = context.Storage; - var retryBehavior = computedJob.RetryBehavior; - - while (!context.IsStopping) - { - var now = DateTime.UtcNow; - - var due = ComputeDue(computedJob, now); - var timeSpan = due - now; - - if (timeSpan.TotalSeconds > 0) - { - await context.WaitAsync(timeSpan); - } - - context.ThrowIfStopping(); - - using (var scopedContext = context.CreateScope()) - { - var provider = scopedContext.Provider; - - var job = provider.GetService(); - var success = true; - - try - { - var sw = Stopwatch.StartNew(); - await job.ExecuteAsync(); - sw.Stop(); - computedJob.Retries = 0; - _logger.CronJobExecuted(computedJob.Job.Name, sw.Elapsed.TotalSeconds); - } - catch (Exception ex) - { - success = false; - if (computedJob.Retries == 0) - { - computedJob.FirstTry = DateTime.UtcNow; - } - computedJob.Retries++; - _logger.CronJobFailed(computedJob.Job.Name, ex); - } - - if (success) - { - computedJob.Update(DateTime.UtcNow); - } - } - } - } - - private DateTime ComputeDue(ComputedCronJob computedJob, DateTime now) - { - computedJob.UpdateNext(now); - - var retryBehavior = computedJob.RetryBehavior ?? RetryBehavior.DefaultRetry; - var retries = computedJob.Retries; - - if (retries == 0) - { - return computedJob.Next; - } - - var realNext = computedJob.Schedule.GetNextOccurrence(now); - - if (!retryBehavior.Retry) - { - // No retry. If job failed before, we don't care, just schedule it next as usual. - return realNext; - } - - if (retries >= retryBehavior.RetryCount) - { - // Max retries. Just schedule it for the next occurance. - return realNext; - } - - // Delay a bit. - return computedJob.FirstTry.AddSeconds(retryBehavior.RetryIn(retries)); - } - - private CronJob[] GetJobs() - { - var cronJobs = new List(); - var entries = _jobRegistry.Build() ?? new CronJobRegistry.Entry[0]; - foreach (var entry in entries) - { - cronJobs.Add(new CronJob - { - Name = entry.Name, - TypeName = entry.JobType.AssemblyQualifiedName, - Cron = entry.Cron, - LastRun = DateTime.MinValue - }); - } - return cronJobs.ToArray(); - } - - private ComputedCronJob[] Compute(IEnumerable jobs, CronJobRegistry.Entry[] entries) - => jobs.Select(j => CreateComputedCronJob(j, entries)).ToArray(); - - private ComputedCronJob CreateComputedCronJob(CronJob job, CronJobRegistry.Entry[] entries) - { - var entry = entries.First(e => e.Name == job.Name); - return new ComputedCronJob(job, entry); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/LoggerExtensions.cs b/src/DotNetCore.CAP/LoggerExtensions.cs index 72e8399..c921247 100644 --- a/src/DotNetCore.CAP/LoggerExtensions.cs +++ b/src/DotNetCore.CAP/LoggerExtensions.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using DotNetCore.CAP.Job; using Microsoft.Extensions.Logging; namespace DotNetCore.CAP @@ -13,16 +10,17 @@ namespace DotNetCore.CAP private static readonly Action _serverShuttingDown; private static readonly Action _expectedOperationCanceledException; - private static readonly Action _cronJobsNotFound; - private static readonly Action _cronJobsScheduling; - private static readonly Action _cronJobExecuted; - private static readonly Action _cronJobFailed; - private static readonly Action _enqueuingSentMessage; private static readonly Action _enqueuingReceivdeMessage; private static readonly Action _executingConsumerMethod; private static readonly Action _receivedMessageRetryExecuting; + private static Action _jobFailed; + private static Action _jobFailedWillRetry; + private static Action _jobExecuted; + private static Action _jobRetrying; + private static Action _exceptionOccuredWhileExecutingJob; + static LoggerExtensions() { _serverStarting = LoggerMessage.Define( @@ -45,26 +43,6 @@ namespace DotNetCore.CAP 3, "Expected an OperationCanceledException, but found '{ExceptionMessage}'."); - _cronJobsNotFound = LoggerMessage.Define( - LogLevel.Debug, - 1, - "No cron jobs found to schedule, cancelling processing of cron jobs."); - - _cronJobsScheduling = LoggerMessage.Define( - LogLevel.Debug, - 2, - "Found {JobCount} cron job(s) to schedule."); - - _cronJobExecuted = LoggerMessage.Define( - LogLevel.Debug, - 3, - "Cron job '{JobName}' executed succesfully. Took: {Seconds} secs."); - - _cronJobFailed = LoggerMessage.Define( - LogLevel.Warning, - 4, - "Cron job '{jobName}' failed to execute."); - _enqueuingSentMessage = LoggerMessage.Define( LogLevel.Debug, 2, @@ -84,6 +62,52 @@ namespace DotNetCore.CAP LogLevel.Error, 5, "Received message topic method '{topicName}' failed to execute."); + + _jobRetrying = LoggerMessage.Define( + LogLevel.Debug, + 3, + "Retrying a job: {Retries}..."); + + _jobExecuted = LoggerMessage.Define( + LogLevel.Debug, + 4, + "Job executed. Took: {Seconds} secs."); + + _jobFailed = LoggerMessage.Define( + LogLevel.Warning, + 1, + "Job failed to execute."); + + _jobFailedWillRetry = LoggerMessage.Define( + LogLevel.Warning, + 2, + "Job failed to execute. Will retry."); + + _exceptionOccuredWhileExecutingJob = LoggerMessage.Define( + LogLevel.Error, + 6, + "An exception occured while trying to execute a job: '{JobId}'. " + + "Requeuing for another retry."); + } + + public static void JobFailed(this ILogger logger, Exception ex) + { + _jobFailed(logger, ex); + } + + public static void JobFailedWillRetry(this ILogger logger, Exception ex) + { + _jobFailedWillRetry(logger, ex); + } + + public static void JobRetrying(this ILogger logger, int retries) + { + _jobRetrying(logger, retries, null); + } + + public static void JobExecuted(this ILogger logger, double seconds) + { + _jobExecuted(logger, seconds, null); } public static void ConsumerMethodExecutingFailed(this ILogger logger, string methodName, Exception ex) @@ -126,24 +150,9 @@ namespace DotNetCore.CAP _expectedOperationCanceledException(logger, ex.Message, ex); } - public static void CronJobsNotFound(this ILogger logger) - { - _cronJobsNotFound(logger, null); - } - - public static void CronJobsScheduling(this ILogger logger, IEnumerable jobs) - { - _cronJobsScheduling(logger, jobs.Count(), null); - } - - public static void CronJobExecuted(this ILogger logger, string name, double seconds) - { - _cronJobExecuted(logger, name, seconds, null); - } - - public static void CronJobFailed(this ILogger logger, string name, Exception ex) + public static void ExceptionOccuredWhileExecutingJob(this ILogger logger, string jobId, Exception ex) { - _cronJobFailed(logger, name, ex); + _exceptionOccuredWhileExecutingJob(logger, jobId, ex); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Infrastructure/MessageContext.cs b/src/DotNetCore.CAP/MessageContext.cs similarity index 60% rename from src/DotNetCore.CAP/Infrastructure/MessageContext.cs rename to src/DotNetCore.CAP/MessageContext.cs index 48982d6..9e1f867 100644 --- a/src/DotNetCore.CAP/Infrastructure/MessageContext.cs +++ b/src/DotNetCore.CAP/MessageContext.cs @@ -1,10 +1,10 @@ -namespace DotNetCore.CAP.Infrastructure +namespace DotNetCore.CAP { public class MessageContext { public string Group { get; set; } - public string KeyName { get; set; } + public string Name { get; set; } public string Content { get; set; } } diff --git a/src/DotNetCore.CAP/Infrastructure/CapSentMessage.cs b/src/DotNetCore.CAP/Models/CapPublishedMessage.cs similarity index 52% rename from src/DotNetCore.CAP/Infrastructure/CapSentMessage.cs rename to src/DotNetCore.CAP/Models/CapPublishedMessage.cs index c250a48..24dfbcc 100644 --- a/src/DotNetCore.CAP/Infrastructure/CapSentMessage.cs +++ b/src/DotNetCore.CAP/Models/CapPublishedMessage.cs @@ -1,36 +1,36 @@ using System; +using DotNetCore.CAP.Infrastructure; -namespace DotNetCore.CAP.Infrastructure +namespace DotNetCore.CAP.Models { - public class CapSentMessage + public class CapPublishedMessage { /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// /// /// The Id property is initialized to from a new GUID string value. /// - public CapSentMessage() + public CapPublishedMessage() { - Id = Guid.NewGuid().ToString(); Added = DateTime.Now; } - public CapSentMessage(MessageContext message) + public CapPublishedMessage(MessageContext message) { - KeyName = message.KeyName; + Name = message.Name; Content = message.Content; } - public string Id { get; set; } + public int Id { get; set; } - public string KeyName { get; set; } + public string Name { get; set; } public string Content { get; set; } public DateTime Added { get; set; } - public DateTime LastRun { get; set; } + public DateTime? ExpiresAt { get; set; } public int Retries { get; set; } diff --git a/src/DotNetCore.CAP/Models/CapQueue.cs b/src/DotNetCore.CAP/Models/CapQueue.cs new file mode 100644 index 0000000..d0aa41c --- /dev/null +++ b/src/DotNetCore.CAP/Models/CapQueue.cs @@ -0,0 +1,12 @@ +namespace DotNetCore.CAP.Models +{ + public class CapQueue + { + public int MessageId { get; set; } + + /// + /// 0 is CapSentMessage, 1 is CapReceviedMessage + /// + public MessageType MessageType { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Infrastructure/CapReceivedMessage.cs b/src/DotNetCore.CAP/Models/CapReceivedMessage.cs similarity index 77% rename from src/DotNetCore.CAP/Infrastructure/CapReceivedMessage.cs rename to src/DotNetCore.CAP/Models/CapReceivedMessage.cs index 7ca0525..bfb0103 100644 --- a/src/DotNetCore.CAP/Infrastructure/CapReceivedMessage.cs +++ b/src/DotNetCore.CAP/Models/CapReceivedMessage.cs @@ -1,6 +1,7 @@ using System; +using DotNetCore.CAP.Infrastructure; -namespace DotNetCore.CAP.Infrastructure +namespace DotNetCore.CAP.Models { public class CapReceivedMessage { @@ -12,28 +13,27 @@ namespace DotNetCore.CAP.Infrastructure /// public CapReceivedMessage() { - Id = Guid.NewGuid().ToString(); Added = DateTime.Now; } public CapReceivedMessage(MessageContext message) : this() { Group = message.Group; - KeyName = message.KeyName; + Name = message.Name; Content = message.Content; } - public string Id { get; set; } + public int Id { get; set; } public string Group { get; set; } - public string KeyName { get; set; } + public string Name { get; set; } public string Content { get; set; } public DateTime Added { get; set; } - public DateTime LastRun { get; set; } + public DateTime? ExpiresAt { get; set; } public int Retries { get; set; } @@ -44,7 +44,7 @@ namespace DotNetCore.CAP.Infrastructure return new MessageContext { Group = Group, - KeyName = KeyName, + Name = Name, Content = Content }; } diff --git a/src/DotNetCore.CAP/Models/MessageType.cs b/src/DotNetCore.CAP/Models/MessageType.cs new file mode 100644 index 0000000..5097962 --- /dev/null +++ b/src/DotNetCore.CAP/Models/MessageType.cs @@ -0,0 +1,8 @@ +namespace DotNetCore.CAP.Models +{ + public enum MessageType + { + Publish, + Subscribe + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/OperateResult.cs b/src/DotNetCore.CAP/OperateResult.cs index c52929e..04aa787 100644 --- a/src/DotNetCore.CAP/OperateResult.cs +++ b/src/DotNetCore.CAP/OperateResult.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace DotNetCore.CAP @@ -18,6 +19,8 @@ namespace DotNetCore.CAP /// public bool Succeeded { get; set; } + public Exception Exception { get; set; } + /// /// An of s containing an errors /// that occurred during the operation. @@ -29,7 +32,7 @@ namespace DotNetCore.CAP /// Returns an indicating a successful identity operation. /// /// An indicating a successful operation. - public static OperateResult Success { get; } = new OperateResult {Succeeded = true}; + public static OperateResult Success { get; } = new OperateResult { Succeeded = true }; /// /// Creates an indicating a failed operation, with a list of if applicable. @@ -38,7 +41,18 @@ namespace DotNetCore.CAP /// An indicating a failed operation, with a list of if applicable. public static OperateResult Failed(params OperateError[] errors) { - var result = new OperateResult {Succeeded = false}; + var result = new OperateResult { Succeeded = false }; + if (errors != null) + { + result._errors.AddRange(errors); + } + return result; + } + + public static OperateResult Failed(Exception ex, params OperateError[] errors) + { + var result = new OperateResult { Succeeded = false }; + result.Exception = ex; if (errors != null) { result._errors.AddRange(errors); diff --git a/src/DotNetCore.CAP/Processor/IAdditionalProcessor.cs b/src/DotNetCore.CAP/Processor/IAdditionalProcessor.cs new file mode 100644 index 0000000..b678614 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/IAdditionalProcessor.cs @@ -0,0 +1,6 @@ +namespace DotNetCore.CAP.Processor +{ + public interface IAdditionalProcessor : IProcessor + { + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/IDispatcher.Default.cs b/src/DotNetCore.CAP/Processor/IDispatcher.Default.cs new file mode 100644 index 0000000..203b564 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/IDispatcher.Default.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using DotNetCore.CAP.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DotNetCore.CAP.Processor +{ + public class DefaultDispatcher : IDispatcher + { + private readonly IQueueExecutorFactory _queueExecutorFactory; + private readonly IServiceProvider _provider; + private readonly ILogger _logger; + + private readonly CancellationTokenSource _cts; + private readonly TimeSpan _pollingDelay; + + internal static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true); + + public DefaultDispatcher( + IServiceProvider provider, + IQueueExecutorFactory queueExecutorFactory, + IOptions capOptions, + ILogger logger) + { + _logger = logger; + _queueExecutorFactory = queueExecutorFactory; + _provider = provider; + _cts = new CancellationTokenSource(); + _pollingDelay = TimeSpan.FromSeconds(capOptions.Value.PollingDelay); + } + + public bool Waiting { get; private set; } + + public Task ProcessAsync(ProcessingContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + context.ThrowIfStopping(); + + return ProcessCoreAsync(context); + } + + public async Task ProcessCoreAsync(ProcessingContext context) + { + try + { + var worked = await Step(context); + + context.ThrowIfStopping(); + + Waiting = true; + + if (!worked) + { + var token = GetTokenToWaitOn(context); + await WaitHandleEx.WaitAnyAsync(PulseEvent, token.WaitHandle, _pollingDelay); + } + } + finally + { + Waiting = false; + } + } + + protected virtual CancellationToken GetTokenToWaitOn(ProcessingContext context) + { + return context.CancellationToken; + } + + private async Task Step(ProcessingContext context) + { + var fetched = default(IFetchedMessage); + using (var scopedContext = context.CreateScope()) + { + var provider = scopedContext.Provider; + var connection = provider.GetRequiredService(); + + if ((fetched = await connection.FetchNextMessageAsync()) != null) + { + using (fetched) + { + var queueExecutor = _queueExecutorFactory.GetInstance(fetched.MessageType); + await queueExecutor.ExecuteAsync(connection, fetched); + } + } + } + return fetched != null; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/IDispatcher.cs b/src/DotNetCore.CAP/Processor/IDispatcher.cs new file mode 100644 index 0000000..f612d02 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/IDispatcher.cs @@ -0,0 +1,7 @@ +namespace DotNetCore.CAP.Processor +{ + public interface IDispatcher : IProcessor + { + bool Waiting { get; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/IProcessingServer.Job.cs b/src/DotNetCore.CAP/Processor/IProcessingServer.Cap.cs similarity index 53% rename from src/DotNetCore.CAP/Job/IProcessingServer.Job.cs rename to src/DotNetCore.CAP/Processor/IProcessingServer.Cap.cs index 0bb912f..5843382 100644 --- a/src/DotNetCore.CAP/Job/IProcessingServer.Job.cs +++ b/src/DotNetCore.CAP/Processor/IProcessingServer.Cap.cs @@ -3,60 +3,67 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace DotNetCore.CAP.Job +namespace DotNetCore.CAP.Processor { - public class JobProcessingServer : IProcessingServer, IDisposable + public class CapProcessingServer : IProcessingServer, IDisposable { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly IServiceProvider _provider; private readonly CancellationTokenSource _cts; private readonly CapOptions _options; - private readonly DefaultCronJobRegistry _defaultJobRegistry; - private IJobProcessor[] _processors; + private IProcessor[] _processors; + private IList _messageDispatchers; private ProcessingContext _context; private Task _compositeTask; private bool _disposed; - public JobProcessingServer( - ILogger logger, + public CapProcessingServer( + ILogger logger, ILoggerFactory loggerFactory, IServiceProvider provider, - DefaultCronJobRegistry defaultJobRegistry, IOptions options) { _logger = logger; _loggerFactory = loggerFactory; _provider = provider; - _defaultJobRegistry = defaultJobRegistry; _options = options.Value; _cts = new CancellationTokenSource(); + _messageDispatchers = new List(); } public void Start() { var processorCount = Environment.ProcessorCount; - //processorCount = 1; _processors = GetProcessors(processorCount); - _logger.ServerStarting(processorCount, processorCount); + _logger.ServerStarting(processorCount, _processors.Length); - _context = new ProcessingContext( - _provider, - _defaultJobRegistry, - _cts.Token); + _context = new ProcessingContext(_provider, _cts.Token); var processorTasks = _processors - .Select(InfiniteRetry) + .Select(p => InfiniteRetry(p)) .Select(p => p.ProcessAsync(_context)); _compositeTask = Task.WhenAll(processorTasks); } + public void Pulse() + { + if (!AllProcessorsWaiting()) + { + // Some processor is still executing jobs so no need to pulse. + return; + } + + _logger.LogTrace("Pulsing the Queuer."); + + PublishQueuer.PulseEvent.Set(); + } + public void Dispose() { if (_disposed) @@ -69,7 +76,7 @@ namespace DotNetCore.CAP.Job _cts.Cancel(); try { - _compositeTask.Wait((int) TimeSpan.FromSeconds(60).TotalMilliseconds); + _compositeTask.Wait((int)TimeSpan.FromSeconds(60).TotalMilliseconds); } catch (AggregateException ex) { @@ -81,30 +88,37 @@ namespace DotNetCore.CAP.Job } } - private IJobProcessor InfiniteRetry(IJobProcessor inner) + private bool AllProcessorsWaiting() + { + foreach (var processor in _messageDispatchers) + { + if (!processor.Waiting) + { + return false; + } + } + return true; + } + + private IProcessor InfiniteRetry(IProcessor inner) { return new InfiniteRetryProcessor(inner, _loggerFactory); } - private IJobProcessor[] GetProcessors(int processorCount) + private IProcessor[] GetProcessors(int processorCount) { - var returnedProcessors = new List(); + var returnedProcessors = new List(); for (int i = 0; i < processorCount; i++) { - var processors = _provider.GetServices(); - foreach (var processor in processors) - { - if (processor is CronJobProcessor) - { - if (i == 0) // only add first cronJob - returnedProcessors.Add(processor); - } - else - { - returnedProcessors.Add(processor); - } - } + var messageProcessors = _provider.GetRequiredService(); + _messageDispatchers.Add(messageProcessors); } + returnedProcessors.AddRange(_messageDispatchers); + + returnedProcessors.Add(_provider.GetRequiredService()); + returnedProcessors.Add(_provider.GetRequiredService()); + + returnedProcessors.Add(_provider.GetRequiredService()); return returnedProcessors.ToArray(); } diff --git a/src/DotNetCore.CAP/Job/IJobProcessor.InfiniteRetry.cs b/src/DotNetCore.CAP/Processor/IProcessor.InfiniteRetry.cs similarity index 70% rename from src/DotNetCore.CAP/Job/IJobProcessor.InfiniteRetry.cs rename to src/DotNetCore.CAP/Processor/IProcessor.InfiniteRetry.cs index b8803fb..2c28d03 100644 --- a/src/DotNetCore.CAP/Job/IJobProcessor.InfiniteRetry.cs +++ b/src/DotNetCore.CAP/Processor/IProcessor.InfiniteRetry.cs @@ -2,15 +2,15 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace DotNetCore.CAP.Job +namespace DotNetCore.CAP.Processor { - public class InfiniteRetryProcessor : IJobProcessor + public class InfiniteRetryProcessor : IProcessor { - private readonly IJobProcessor _inner; + private readonly IProcessor _inner; private readonly ILogger _logger; public InfiniteRetryProcessor( - IJobProcessor inner, + IProcessor inner, ILoggerFactory loggerFactory) { _inner = inner; @@ -33,10 +33,7 @@ namespace DotNetCore.CAP.Job } catch (Exception ex) { - _logger.LogWarning( - 1, - ex, - "Prcessor '{ProcessorName}' failed. Retrying...", _inner.ToString()); + _logger.LogWarning(1, ex, "Prcessor '{ProcessorName}' failed. Retrying...", _inner.ToString()); } } } diff --git a/src/DotNetCore.CAP/Processor/IProcessor.PublishQueuer.cs b/src/DotNetCore.CAP/Processor/IProcessor.PublishQueuer.cs new file mode 100644 index 0000000..4537c17 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/IProcessor.PublishQueuer.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DotNetCore.CAP.Processor +{ + public class PublishQueuer : IProcessor + { + private ILogger _logger; + private CapOptions _options; + private IStateChanger _stateChanger; + private IServiceProvider _provider; + private TimeSpan _pollingDelay; + + public static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true); + + public PublishQueuer( + ILogger logger, + IOptions options, + IStateChanger stateChanger, + IServiceProvider provider) + { + _logger = logger; + _options = options.Value; + _stateChanger = stateChanger; + _provider = provider; + + _pollingDelay = TimeSpan.FromSeconds(_options.PollingDelay); + } + + public async Task ProcessAsync(ProcessingContext context) + { + using (var scope = _provider.CreateScope()) + { + CapPublishedMessage sentMessage; + var provider = scope.ServiceProvider; + var connection = provider.GetRequiredService(); + + while ( + !context.IsStopping && + (sentMessage = await connection.GetNextPublishedMessageToBeEnqueuedAsync()) != null) + + { + var state = new EnqueuedState(); + + using (var transaction = connection.CreateTransaction()) + { + _stateChanger.ChangeState(sentMessage, state, transaction); + await transaction.CommitAsync(); + } + } + } + + context.ThrowIfStopping(); + + DefaultDispatcher.PulseEvent.Set(); + + await WaitHandleEx.WaitAnyAsync(PulseEvent, + context.CancellationToken.WaitHandle, _pollingDelay); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/IProcessor.SubscribeQueuer.cs b/src/DotNetCore.CAP/Processor/IProcessor.SubscribeQueuer.cs new file mode 100644 index 0000000..4a85fb0 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/IProcessor.SubscribeQueuer.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DotNetCore.CAP.Processor +{ + public class SubscribeQueuer : IProcessor + { + private ILogger _logger; + private CapOptions _options; + private IStateChanger _stateChanger; + private IServiceProvider _provider; + private TimeSpan _pollingDelay; + + internal static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true); + + public SubscribeQueuer( + ILogger logger, + IOptions options, + IStateChanger stateChanger, + IServiceProvider provider) + { + _logger = logger; + _options = options.Value; + _stateChanger = stateChanger; + _provider = provider; + + _pollingDelay = TimeSpan.FromSeconds(_options.PollingDelay); + } + + public async Task ProcessAsync(ProcessingContext context) + { + using (var scope = _provider.CreateScope()) + { + CapReceivedMessage message; + var provider = scope.ServiceProvider; + var connection = provider.GetRequiredService(); + + while ( + !context.IsStopping && + (message = await connection.GetNextReceviedMessageToBeEnqueuedAsync()) != null) + + { + var state = new EnqueuedState(); + + using (var transaction = connection.CreateTransaction()) + { + _stateChanger.ChangeState(message, state, transaction); + await transaction.CommitAsync(); + } + } + } + + context.ThrowIfStopping(); + + DefaultDispatcher.PulseEvent.Set(); + + await WaitHandleEx.WaitAnyAsync(PulseEvent, + context.CancellationToken.WaitHandle, _pollingDelay); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Job/IJobProcessor.cs b/src/DotNetCore.CAP/Processor/IProcessor.cs similarity index 60% rename from src/DotNetCore.CAP/Job/IJobProcessor.cs rename to src/DotNetCore.CAP/Processor/IProcessor.cs index d7ffc76..20d484f 100644 --- a/src/DotNetCore.CAP/Job/IJobProcessor.cs +++ b/src/DotNetCore.CAP/Processor/IProcessor.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -namespace DotNetCore.CAP.Job +namespace DotNetCore.CAP.Processor { - public interface IJobProcessor + public interface IProcessor { Task ProcessAsync(ProcessingContext context); } diff --git a/src/DotNetCore.CAP/Job/ProcessingContext.cs b/src/DotNetCore.CAP/Processor/ProcessingContext.cs similarity index 85% rename from src/DotNetCore.CAP/Job/ProcessingContext.cs rename to src/DotNetCore.CAP/Processor/ProcessingContext.cs index 7498298..f1fb203 100644 --- a/src/DotNetCore.CAP/Job/ProcessingContext.cs +++ b/src/DotNetCore.CAP/Processor/ProcessingContext.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -namespace DotNetCore.CAP.Job +namespace DotNetCore.CAP.Processor { public class ProcessingContext : IDisposable { @@ -16,24 +16,19 @@ namespace DotNetCore.CAP.Job private ProcessingContext(ProcessingContext other) { Provider = other.Provider; - CronJobRegistry = other.CronJobRegistry; CancellationToken = other.CancellationToken; } public ProcessingContext( IServiceProvider provider, - CronJobRegistry cronJobRegistry, CancellationToken cancellationToken) { Provider = provider; - CronJobRegistry = cronJobRegistry; CancellationToken = cancellationToken; } public IServiceProvider Provider { get; private set; } - public CronJobRegistry CronJobRegistry { get; private set; } - public CancellationToken CancellationToken { get; } public bool IsStopping => CancellationToken.IsCancellationRequested; diff --git a/src/DotNetCore.CAP/Job/RetryBehavior.cs b/src/DotNetCore.CAP/Processor/RetryBehavior.cs similarity index 94% rename from src/DotNetCore.CAP/Job/RetryBehavior.cs rename to src/DotNetCore.CAP/Processor/RetryBehavior.cs index 9d48d59..1421fae 100644 --- a/src/DotNetCore.CAP/Job/RetryBehavior.cs +++ b/src/DotNetCore.CAP/Processor/RetryBehavior.cs @@ -1,6 +1,6 @@ using System; -namespace DotNetCore.CAP.Job +namespace DotNetCore.CAP.Processor { public class RetryBehavior { @@ -18,7 +18,7 @@ namespace DotNetCore.CAP.Job { DefaultRetryCount = 25; DefaultRetryInThunk = retries => - (int) Math.Round(Math.Pow(retries - 1, 4) + 15 + (_random.Next(30) * (retries))); + (int)Math.Round(Math.Pow(retries - 1, 4) + 15 + (_random.Next(30) * (retries))); DefaultRetry = new RetryBehavior(true); NoRetry = new RetryBehavior(false); diff --git a/src/DotNetCore.CAP/Processor/States/IState.Enqueued.cs b/src/DotNetCore.CAP/Processor/States/IState.Enqueued.cs new file mode 100644 index 0000000..ca53699 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.Enqueued.cs @@ -0,0 +1,24 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class EnqueuedState : IState + { + public const string StateName = "Enqueued"; + + public TimeSpan? ExpiresAfter => null; + + public string Name => StateName; + + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) + { + transaction.EnqueueMessage(message); + } + + public void Apply(CapReceivedMessage message, IStorageTransaction transaction) + { + transaction.EnqueueMessage(message); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.Failed.cs b/src/DotNetCore.CAP/Processor/States/IState.Failed.cs new file mode 100644 index 0000000..49fda9b --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.Failed.cs @@ -0,0 +1,22 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class FailedState : IState + { + public const string StateName = "Failed"; + + public TimeSpan? ExpiresAfter => TimeSpan.FromDays(15); + + public string Name => StateName; + + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) + { + } + + public void Apply(CapReceivedMessage message, IStorageTransaction transaction) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.Processing.cs b/src/DotNetCore.CAP/Processor/States/IState.Processing.cs new file mode 100644 index 0000000..9827e76 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.Processing.cs @@ -0,0 +1,22 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class ProcessingState : IState + { + public const string StateName = "Processing"; + + public TimeSpan? ExpiresAfter => null; + + public string Name => StateName; + + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) + { + } + + public void Apply(CapReceivedMessage message, IStorageTransaction transaction) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.Scheduled.cs b/src/DotNetCore.CAP/Processor/States/IState.Scheduled.cs new file mode 100644 index 0000000..49f0c95 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.Scheduled.cs @@ -0,0 +1,22 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class ScheduledState : IState + { + public const string StateName = "Scheduled"; + + public TimeSpan? ExpiresAfter => null; + + public string Name => StateName; + + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) + { + } + + public void Apply(CapReceivedMessage message, IStorageTransaction transaction) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs b/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs new file mode 100644 index 0000000..294591c --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs @@ -0,0 +1,22 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class SucceededState : IState + { + public const string StateName = "Succeeded"; + + public TimeSpan? ExpiresAfter => TimeSpan.FromHours(1); + + public string Name => StateName; + + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) + { + } + + public void Apply(CapReceivedMessage message, IStorageTransaction transaction) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.cs b/src/DotNetCore.CAP/Processor/States/IState.cs new file mode 100644 index 0000000..c43fc74 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IState.cs @@ -0,0 +1,16 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public interface IState + { + TimeSpan? ExpiresAfter { get; } + + string Name { get; } + + void Apply(CapPublishedMessage message, IStorageTransaction transaction); + + void Apply(CapReceivedMessage message, IStorageTransaction transaction); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IStateChanger.Default.cs b/src/DotNetCore.CAP/Processor/States/IStateChanger.Default.cs new file mode 100644 index 0000000..1982e46 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IStateChanger.Default.cs @@ -0,0 +1,42 @@ +using System; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public class StateChanger : IStateChanger + { + public void ChangeState(CapPublishedMessage message, IState state, IStorageTransaction transaction) + { + var now = DateTime.Now; + if (state.ExpiresAfter != null) + { + message.ExpiresAt = now.Add(state.ExpiresAfter.Value); + } + else + { + message.ExpiresAt = null; + } + + message.StatusName = state.Name; + state.Apply(message, transaction); + transaction.UpdateMessage(message); + } + + public void ChangeState(CapReceivedMessage message, IState state, IStorageTransaction transaction) + { + var now = DateTime.Now; + if (state.ExpiresAfter != null) + { + message.ExpiresAt = now.Add(state.ExpiresAfter.Value); + } + else + { + message.ExpiresAt = null; + } + + message.StatusName = state.Name; + state.Apply(message, transaction); + transaction.UpdateMessage(message); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IStateChanger.Extensions.cs b/src/DotNetCore.CAP/Processor/States/IStateChanger.Extensions.cs new file mode 100644 index 0000000..6bd1d12 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IStateChanger.Extensions.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public static class StateChangerExtensions + { + public static async Task ChangeStateAsync( + this IStateChanger @this, CapPublishedMessage message, IState state, IStorageConnection connection) + { + using (var transaction = connection.CreateTransaction()) + { + @this.ChangeState(message, state, transaction); + await transaction.CommitAsync(); + } + } + + public static async Task ChangeStateAsync( + this IStateChanger @this, CapReceivedMessage message, IState state, IStorageConnection connection) + { + using (var transaction = connection.CreateTransaction()) + { + @this.ChangeState(message, state, transaction); + await transaction.CommitAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IStateChanger.cs b/src/DotNetCore.CAP/Processor/States/IStateChanger.cs new file mode 100644 index 0000000..949ea31 --- /dev/null +++ b/src/DotNetCore.CAP/Processor/States/IStateChanger.cs @@ -0,0 +1,11 @@ +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.Processor.States +{ + public interface IStateChanger + { + void ChangeState(CapPublishedMessage message, IState state, IStorageTransaction transaction); + + void ChangeState(CapReceivedMessage message, IState state, IStorageTransaction transaction); + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/QueueExecutorFactory.cs b/src/DotNetCore.CAP/QueueExecutorFactory.cs new file mode 100644 index 0000000..d76f09f --- /dev/null +++ b/src/DotNetCore.CAP/QueueExecutorFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using DotNetCore.CAP.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetCore.CAP +{ + public class QueueExecutorFactory : IQueueExecutorFactory + { + private readonly IServiceProvider _serviceProvider; + + public QueueExecutorFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IQueueExecutor GetInstance(MessageType messageType) + { + var queueExectors = _serviceProvider.GetServices(); + + return messageType == MessageType.Publish + ? queueExectors.FirstOrDefault(x => x is BasePublishQueueExecutor) + : queueExectors.FirstOrDefault(x => !(x is BasePublishQueueExecutor)); + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/CapMessageStoreTest.cs b/test/DotNetCore.CAP.EntityFrameworkCore.Test/CapMessageStoreTest.cs deleted file mode 100644 index efda431..0000000 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/CapMessageStoreTest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Xunit; - -namespace DotNetCore.CAP.EntityFrameworkCore.Test -{ - public class CapMessageStoreTest - { - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/EFMessageStoreTest.cs b/test/DotNetCore.CAP.EntityFrameworkCore.Test/EFMessageStoreTest.cs deleted file mode 100644 index c138173..0000000 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/EFMessageStoreTest.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Linq; -using DotNetCore.CAP.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace DotNetCore.CAP.EntityFrameworkCore.Test -{ - public class EFMessageStoreTest : DatabaseTestHost - { - [Fact] - public void CanCreateSentMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapSentMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - Assert.True(db.CapSentMessages.Any(u => u.Id == guid)); - Assert.NotNull(db.CapSentMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued)); - } - } - - [Fact] - public void CanUpdateSentMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapSentMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - var selectedMessage = db.CapSentMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.NotNull(selectedMessage); - - selectedMessage.StatusName = StatusName.Succeeded; - selectedMessage.Content = "Test"; - db.SaveChanges(); - - selectedMessage = db.CapSentMessages.FirstOrDefault(u => u.StatusName == StatusName.Succeeded); - Assert.NotNull(selectedMessage); - Assert.True(selectedMessage.Content == "Test"); - } - } - - [Fact] - public void CanRemoveSentMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapSentMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - var selectedMessage = db.CapSentMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.NotNull(selectedMessage); - - db.CapSentMessages.Remove(selectedMessage); - db.SaveChanges(); - selectedMessage = db.CapSentMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.Null(selectedMessage); - } - } - - [Fact] - public void CanCreateReceivedMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapReceivedMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - Assert.True(db.CapReceivedMessages.Any(u => u.Id == guid)); - Assert.NotNull(db.CapReceivedMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued)); - } - } - - [Fact] - public void CanUpdateReceivedMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapReceivedMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - var selectedMessage = db.CapReceivedMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.NotNull(selectedMessage); - - selectedMessage.StatusName = StatusName.Succeeded; - selectedMessage.Content = "Test"; - db.SaveChanges(); - - selectedMessage = db.CapReceivedMessages.FirstOrDefault(u => u.StatusName == StatusName.Succeeded); - Assert.NotNull(selectedMessage); - Assert.True(selectedMessage.Content == "Test"); - } - } - - [Fact] - public void CanRemoveReceivedMessageUsingEF() - { - using (var db = CreateContext()) - { - var guid = Guid.NewGuid().ToString(); - var message = new CapReceivedMessage - { - Id = guid, - Content = "this is message body", - StatusName = StatusName.Enqueued - }; - db.Attach(message).State = Microsoft.EntityFrameworkCore.EntityState.Added; - - db.SaveChanges(); - - var selectedMessage = db.CapReceivedMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.NotNull(selectedMessage); - - db.CapReceivedMessages.Remove(selectedMessage); - db.SaveChanges(); - selectedMessage = db.CapReceivedMessages.FirstOrDefault(u => u.StatusName == StatusName.Enqueued); - Assert.Null(selectedMessage); - } - } - - public TestDbContext CreateContext(bool delete = false) - { - var db = Provider.GetRequiredService(); - if (delete) - { - db.Database.EnsureDeleted(); - } - db.Database.EnsureCreated(); - return db; - } - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.Designer.cs b/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.Designer.cs deleted file mode 100644 index a018f8f..0000000 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.Designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using DotNetCore.CAP.EntityFrameworkCore.Test; - -namespace DotNetCore.CAP.EntityFrameworkCore.Test.Migrations -{ - [DbContext(typeof(TestDbContext))] - [Migration("20170708050416_InitDB")] - partial class InitDB - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapReceivedMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("Group"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("CapReceivedMessages"); - }); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapSentMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("CapSentMessages"); - }); - } - } -} diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.cs b/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.cs deleted file mode 100644 index af02548..0000000 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/20170708050416_InitDB.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace DotNetCore.CAP.EntityFrameworkCore.Test.Migrations -{ - public partial class InitDB : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CapReceivedMessages", - columns: table => new - { - Id = table.Column(nullable: false), - Added = table.Column(nullable: false), - Content = table.Column(nullable: true), - Group = table.Column(nullable: true), - KeyName = table.Column(nullable: true), - LastRun = table.Column(nullable: false), - Retries = table.Column(nullable: false), - StatusName = table.Column(maxLength: 50, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CapReceivedMessages", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "CapSentMessages", - columns: table => new - { - Id = table.Column(nullable: false), - Added = table.Column(nullable: false), - Content = table.Column(nullable: true), - KeyName = table.Column(nullable: true), - LastRun = table.Column(nullable: false), - Retries = table.Column(nullable: false), - StatusName = table.Column(maxLength: 50, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CapSentMessages", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CapReceivedMessages"); - - migrationBuilder.DropTable( - name: "CapSentMessages"); - } - } -} diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/TestDbContextModelSnapshot.cs b/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/TestDbContextModelSnapshot.cs deleted file mode 100644 index a4e6e0a..0000000 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Migrations/TestDbContextModelSnapshot.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using DotNetCore.CAP.EntityFrameworkCore.Test; - -namespace DotNetCore.CAP.EntityFrameworkCore.Test.Migrations -{ - [DbContext(typeof(TestDbContext))] - partial class TestDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapReceivedMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("Group"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("CapReceivedMessages"); - }); - - modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapSentMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("Added"); - - b.Property("Content"); - - b.Property("KeyName"); - - b.Property("LastRun"); - - b.Property("Retries"); - - b.Property("StatusName") - .HasMaxLength(50); - - b.HasKey("Id"); - - b.ToTable("CapSentMessages"); - }); - } - } -} diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/ConnectionUtil.cs b/test/DotNetCore.CAP.SqlServer.Test/ConnectionUtil.cs similarity index 95% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/ConnectionUtil.cs rename to test/DotNetCore.CAP.SqlServer.Test/ConnectionUtil.cs index 80fa0c8..93e45d1 100644 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/ConnectionUtil.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/ConnectionUtil.cs @@ -1,7 +1,7 @@ using System; using System.Data.SqlClient; -namespace DotNetCore.CAP.EntityFrameworkCore.Test +namespace DotNetCore.CAP.SqlServer.Test { public static class ConnectionUtil { @@ -9,7 +9,7 @@ namespace DotNetCore.CAP.EntityFrameworkCore.Test private const string ConnectionStringTemplateVariable = "Cap_SqlServer_ConnectionStringTemplate"; private const string MasterDatabaseName = "master"; - private const string DefaultDatabaseName = @"DotNetCore.CAP.EntityFrameworkCore.Test"; + private const string DefaultDatabaseName = @"DotNetCore.CAP.SqlServer.Test"; private const string DefaultConnectionStringTemplate = @"Server=192.168.2.206;Initial Catalog={0};User Id=sa;Password=123123;MultipleActiveResultSets=True"; diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/DatabaseTestHost.cs b/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs similarity index 55% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/DatabaseTestHost.cs rename to test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs index 993e685..b32fd74 100644 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/DatabaseTestHost.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs @@ -1,18 +1,25 @@ using System.Data; -using System.Threading.Tasks; +using System.Threading; using Dapper; using Microsoft.EntityFrameworkCore; -namespace DotNetCore.CAP.EntityFrameworkCore.Test +namespace DotNetCore.CAP.SqlServer.Test { public abstract class DatabaseTestHost : TestHost { private static bool _sqlObjectInstalled; + public static object _lock = new object(); protected override void PostBuildServices() { base.PostBuildServices(); - InitializeDatabase(); + lock (_lock) + { + if (!_sqlObjectInstalled) + { + InitializeDatabase(); + } + } } public override void Dispose() @@ -23,15 +30,25 @@ namespace DotNetCore.CAP.EntityFrameworkCore.Test private void InitializeDatabase() { - if (!_sqlObjectInstalled) + using (CreateScope()) { - using (CreateScope()) - { - var context = GetService(); - context.Database.EnsureDeleted(); - context.Database.Migrate(); - _sqlObjectInstalled = true; - } + var storage = GetService(); + var token = new CancellationTokenSource().Token; + CreateDatabase(); + storage.InitializeAsync(token).Wait(); + _sqlObjectInstalled = true; + } + } + + private void CreateDatabase() + { + var masterConn = ConnectionUtil.GetMasterConnectionString(); + var databaseName = ConnectionUtil.GetDatabaseName(); + using (var connection = ConnectionUtil.CreateConnection(masterConn)) + { + connection.Execute($@" +IF NOT EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}') +CREATE DATABASE [{databaseName}];"); } } @@ -53,7 +70,7 @@ namespace DotNetCore.CAP.EntityFrameworkCore.Test { context.Database.GetDbConnection().Execute( "sp_MSforeachtable", - new {command1 = command}, + new { command1 = command }, commandType: CommandType.StoredProcedure); } } diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/DotNetCore.CAP.EntityFrameworkCore.Test.csproj b/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj similarity index 86% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/DotNetCore.CAP.EntityFrameworkCore.Test.csproj rename to test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj index 8a35540..177d825 100644 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/DotNetCore.CAP.EntityFrameworkCore.Test.csproj +++ b/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj @@ -3,8 +3,8 @@ netcoreapp1.1 true - DotNetCore.CAP.EntityFrameworkCore.Test - DotNetCore.CAP.EntityFrameworkCore.Test + DotNetCore.CAP.SqlServer.Test + DotNetCore.CAP.SqlServer.Test true $(PackageTargetFallback);dnxcore50;portable-net451+win8 1.1.1 @@ -18,8 +18,8 @@ + - @@ -41,8 +41,7 @@ - - + diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/Properties/AssemblyInfo.cs b/test/DotNetCore.CAP.SqlServer.Test/Properties/AssemblyInfo.cs similarity index 100% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/Properties/AssemblyInfo.cs rename to test/DotNetCore.CAP.SqlServer.Test/Properties/AssemblyInfo.cs diff --git a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs new file mode 100644 index 0000000..bd0bab4 --- /dev/null +++ b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs @@ -0,0 +1,134 @@ +using System; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using Xunit; + +namespace DotNetCore.CAP.SqlServer.Test +{ + [Collection("sqlserver")] + public class SqlServerStorageConnectionTest : DatabaseTestHost + { + private SqlServerStorageConnection _storage; + + public SqlServerStorageConnectionTest() + { + var options = GetService(); + _storage = new SqlServerStorageConnection(options); + } + + [Fact] + public async Task GetPublishedMessageAsync_Test() + { + var sql = "INSERT INTO [Cap].[Published]([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) OUTPUT INSERTED.Id VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; + var publishMessage = new CapPublishedMessage + { + Name = "SqlServerStorageConnectionTest", + Content = "", + StatusName = StatusName.Scheduled + }; + var insertedId = default(int); + using (var connection = ConnectionUtil.CreateConnection()) + { + insertedId = connection.QueryFirst(sql, publishMessage); + } + var message = await _storage.GetPublishedMessageAsync(insertedId); + Assert.NotNull(message); + Assert.Equal("SqlServerStorageConnectionTest", message.Name); + Assert.Equal(StatusName.Scheduled, message.StatusName); + } + + [Fact] + public async Task FetchNextMessageAsync_Test() + { + var sql = "INSERT INTO [Cap].[Queue]([MessageId],[MessageType]) VALUES(@MessageId,@MessageType);"; + var queue = new CapQueue + { + MessageId = 3333, + MessageType = MessageType.Publish + }; + using (var connection = ConnectionUtil.CreateConnection()) + { + connection.Execute(sql, queue); + } + var fetchedMessage = await _storage.FetchNextMessageAsync(); + fetchedMessage.Dispose(); + Assert.NotNull(fetchedMessage); + Assert.Equal(MessageType.Publish, fetchedMessage.MessageType); + Assert.Equal(3333, fetchedMessage.MessageId); + } + + [Fact] + public async Task StoreReceivedMessageAsync_Test() + { + var receivedMessage = new CapReceivedMessage + { + Name = "SqlServerStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + + Exception exception = null; + try + { + await _storage.StoreReceivedMessageAsync(receivedMessage); + } + catch (Exception ex) + { + exception = ex; + } + Assert.Null(exception); + } + + [Fact] + public async Task GetReceivedMessageAsync_Test() + { + + var sql = $@" + INSERT INTO [Cap].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) OUTPUT INSERTED.Id + VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; + var receivedMessage = new CapReceivedMessage + { + Name = "SqlServerStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + var insertedId = default(int); + using (var connection = ConnectionUtil.CreateConnection()) + { + insertedId = connection.QueryFirst(sql, receivedMessage); + } + + var message = await _storage.GetReceivedMessageAsync(insertedId); + + Assert.NotNull(message); + Assert.Equal(StatusName.Scheduled, message.StatusName); + Assert.Equal("SqlServerStorageConnectionTest", message.Name); + Assert.Equal("mygroup", message.Group); + } + + [Fact] + public async Task GetNextReceviedMessageToBeEnqueuedAsync_Test() + { + var receivedMessage = new CapReceivedMessage + { + Name = "SqlServerStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + await _storage.StoreReceivedMessageAsync(receivedMessage); + + var message = await _storage.GetNextReceviedMessageToBeEnqueuedAsync(); + + Assert.NotNull(message); + Assert.Equal(StatusName.Scheduled, message.StatusName); + Assert.Equal("SqlServerStorageConnectionTest", message.Name); + Assert.Equal("mygroup", message.Group); + } + + } +} diff --git a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs new file mode 100644 index 0000000..38fb6c1 --- /dev/null +++ b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs @@ -0,0 +1,71 @@ +using Xunit; +using Dapper; + +namespace DotNetCore.CAP.SqlServer.Test +{ + [Collection("sqlserver")] + public class SqlServerStorageTest : DatabaseTestHost + { + [Fact] + public void Database_IsExists() + { + var master = ConnectionUtil.GetMasterConnectionString(); + using (var connection = ConnectionUtil.CreateConnection(master)) + { + var databaseName = ConnectionUtil.GetDatabaseName(); + var sql = $@" +IF EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}') +SELECT 'True' +ELSE +SELECT 'False'"; + var result = connection.QueryFirst(sql); + Assert.Equal(true, result); + } + } + + [Fact] + public void DatabaseTable_Published_IsExists() + { + using (var connection = ConnectionUtil.CreateConnection()) + { + var sql = @" +IF OBJECT_ID(N'[Cap].[Published]',N'U') IS NOT NULL +SELECT 'True' +ELSE +SELECT 'False'"; + var result = connection.QueryFirst(sql); + Assert.Equal(true, result); + } + } + + [Fact] + public void DatabaseTable_Queue_IsExists() + { + using (var connection = ConnectionUtil.CreateConnection()) + { + var sql = @" +IF OBJECT_ID(N'[Cap].[Queue]',N'U') IS NOT NULL +SELECT 'True' +ELSE +SELECT 'False'"; + var result = connection.QueryFirst(sql); + Assert.Equal(true, result); + } + } + + [Fact] + public void DatabaseTable_Received_IsExists() + { + using (var connection = ConnectionUtil.CreateConnection()) + { + var sql = @" +IF OBJECT_ID(N'[Cap].[Received]',N'U') IS NOT NULL +SELECT 'True' +ELSE +SELECT 'False'"; + var result = connection.QueryFirst(sql); + Assert.Equal(true, result); + } + } + } +} diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/TestDbContext.cs b/test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs similarity index 75% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/TestDbContext.cs rename to test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs index 4b7cbef..d59bdf1 100644 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/TestDbContext.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -namespace DotNetCore.CAP.EntityFrameworkCore.Test +namespace DotNetCore.CAP.SqlServer.Test { - public class TestDbContext : CapDbContext + public class TestDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/test/DotNetCore.CAP.EntityFrameworkCore.Test/TestHost.cs b/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs similarity index 87% rename from test/DotNetCore.CAP.EntityFrameworkCore.Test/TestHost.cs rename to test/DotNetCore.CAP.SqlServer.Test/TestHost.cs index cc69061..31cbfd1 100644 --- a/test/DotNetCore.CAP.EntityFrameworkCore.Test/TestHost.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs @@ -2,11 +2,12 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace DotNetCore.CAP.EntityFrameworkCore.Test +namespace DotNetCore.CAP.SqlServer.Test { public abstract class TestHost : IDisposable { protected IServiceCollection _services; + protected string _connectionString; private IServiceProvider _provider; private IServiceProvider _scopedProvider; @@ -27,9 +28,10 @@ namespace DotNetCore.CAP.EntityFrameworkCore.Test services.AddOptions(); services.AddLogging(); - var connectionString = ConnectionUtil.GetConnectionString(); - //services.AddSingleton(new SqlServerOptions { ConnectionString = connectionString }); - services.AddDbContext(options => options.UseSqlServer(connectionString)); + _connectionString = ConnectionUtil.GetConnectionString(); + services.AddSingleton(new SqlServerOptions { ConnectionString = _connectionString }); + services.AddSingleton(); + services.AddDbContext(options => options.UseSqlServer(_connectionString)); _services = services; } diff --git a/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs b/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs index d526c75..2b18691 100644 --- a/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs +++ b/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs @@ -1,44 +1,47 @@ using System; -using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using DotNetCore.CAP.Job; using Microsoft.Extensions.DependencyInjection; using Xunit; +using System.Data; namespace DotNetCore.CAP.Test { public class CapBuilderTest { [Fact] - public void CanOverrideMessageStore() + public void CanCreateInstanceAndGetService() { var services = new ServiceCollection(); - services.AddCap().AddMessageStore(); - var thingy = services.BuildServiceProvider() - .GetRequiredService() as MyMessageStore; + services.AddSingleton(); + var builder = new CapBuilder(services); + Assert.NotNull(builder); - Assert.NotNull(thingy); + var count = builder.Services.Count; + Assert.Equal(1, count); + + var provider = services.BuildServiceProvider(); + var capPublisher = provider.GetService(); + Assert.NotNull(capPublisher); } [Fact] - public void CanOverrideJobs() + public void CanAddCapService() { var services = new ServiceCollection(); - services.AddCap().AddJobs(); + services.AddCap(x => { }); + var builder = services.BuildServiceProvider(); - var thingy = services.BuildServiceProvider() - .GetRequiredService() as MyJobTest; - - Assert.NotNull(thingy); + var markService = builder.GetService(); + Assert.NotNull(markService); } + [Fact] - public void CanOverrideProducerService() + public void CanOverridePublishService() { var services = new ServiceCollection(); - services.AddCap().AddProducerService(); + services.AddCap(x => { }).AddProducerService(); var thingy = services.BuildServiceProvider() .GetRequiredService() as MyProducerService; @@ -46,6 +49,15 @@ namespace DotNetCore.CAP.Test Assert.NotNull(thingy); } + [Fact] + public void CanResolveCapOptions() + { + var services = new ServiceCollection(); + services.AddCap(x => { }); + var builder = services.BuildServiceProvider(); + var capOptions = builder.GetService(); + Assert.NotNull(capOptions); + } private class MyProducerService : ICapPublisher { @@ -58,62 +70,13 @@ namespace DotNetCore.CAP.Test { throw new NotImplementedException(); } - } - - - private class MyJobTest : IJob - { - public Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private class MyMessageStore : ICapMessageStore - { - public Task ChangeReceivedMessageStateAsync(CapReceivedMessage message, string statusName, - bool autoSaveChanges = true) - { - throw new NotImplementedException(); - } - - public Task ChangeSentMessageStateAsync(CapSentMessage message, string statusName, - bool autoSaveChanges = true) - { - throw new NotImplementedException(); - } - - public Task GetNextReceivedMessageToBeExcuted() - { - throw new NotImplementedException(); - } - - public Task GetNextSentMessageToBeEnqueuedAsync() - { - throw new NotImplementedException(); - } - - public Task RemoveSentMessageAsync(CapSentMessage message) - { - throw new NotImplementedException(); - } - - public Task StoreReceivedMessageAsync(CapReceivedMessage message) - { - throw new NotImplementedException(); - } - - public Task StoreSentMessageAsync(CapSentMessage message) - { - throw new NotImplementedException(); - } - public Task UpdateReceivedMessageAsync(CapReceivedMessage message) + public Task PublishAsync(string topic, string content, IDbConnection dbConnection) { throw new NotImplementedException(); } - public Task UpdateSentMessageAsync(CapSentMessage message) + public Task PublishAsync(string topic, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) { throw new NotImplementedException(); } diff --git a/test/DotNetCore.CAP.Test/ConsistencyOptionsTest.cs b/test/DotNetCore.CAP.Test/ConsistencyOptionsTest.cs deleted file mode 100644 index b9484aa..0000000 --- a/test/DotNetCore.CAP.Test/ConsistencyOptionsTest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CDotNetCore.CAPTest -{ - public class ConsistencyOptionsTest - { - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs index da670c9..1fc5ca8 100644 --- a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs +++ b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs @@ -20,7 +20,7 @@ namespace DotNetCore.CAP.Test services.AddScoped(); services.AddScoped(); services.AddLogging(); - services.AddCap(); + services.AddCap(x=> { }); _provider = services.BuildServiceProvider(); } diff --git a/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj b/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj index 47cebe2..81b5e2d 100644 --- a/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj +++ b/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj @@ -14,12 +14,9 @@ - - - - + @@ -28,6 +25,10 @@ + + + + diff --git a/test/DotNetCore.CAP.Test/Job/ComputedJobTest.cs b/test/DotNetCore.CAP.Test/Job/ComputedJobTest.cs deleted file mode 100644 index f511175..0000000 --- a/test/DotNetCore.CAP.Test/Job/ComputedJobTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using DotNetCore.CAP.Job; -using Xunit; - -namespace DotNetCore.CAP.Test.Job -{ - public class ComputedJobTest - { - [Fact] - public void UpdateNext_LastRunNever_SchedulesNow() - { - // Arrange - var now = new DateTime(2000, 1, 1, 8, 0, 0); - var cronJob = new CronJob(Cron.Daily()); - var computed = new ComputedCronJob(cronJob); - - // Act - computed.UpdateNext(now); - - // Assert - Assert.Equal(computed.Next, now); - } - - [Fact] - public void UpdateNext_LastRun_BeforePrev_SchedulesNow() - { - // Arrange - var now = new DateTime(2000, 1, 1, 8, 0, 0); - var cronJob = new CronJob(Cron.Daily(), now.Subtract(TimeSpan.FromDays(2))); - var computed = new ComputedCronJob(cronJob); - - // Act - computed.UpdateNext(now); - - // Assert - Assert.Equal(computed.Next, now); - } - - [Fact] - public void UpdateNext_LastRun_AfterPrev_SchedulesNormal() - { - // Arrange - var now = new DateTime(2000, 1, 1, 8, 0, 0); - var cronJob = new CronJob(Cron.Daily(), now.Subtract(TimeSpan.FromSeconds(5))); - var computed = new ComputedCronJob(cronJob); - - // Act - computed.UpdateNext(now); - - // Assert - Assert.True(computed.Next > now); - } - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/Job/JobProcessingServerTest.cs b/test/DotNetCore.CAP.Test/Job/JobProcessingServerTest.cs deleted file mode 100644 index 50df041..0000000 --- a/test/DotNetCore.CAP.Test/Job/JobProcessingServerTest.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using DotNetCore.CAP.Job; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; - -namespace DotNetCore.CAP.Test.Job -{ - public class JobProcessingServerTest - { - private CancellationTokenSource _cancellationTokenSource; - private ProcessingContext _context; - private CapOptions _options; - private IServiceProvider _provider; - private Mock _mockStorage; - - public JobProcessingServerTest() - { - _options = new CapOptions() - { - PollingDelay = 0 - }; - _mockStorage = new Mock(); - _cancellationTokenSource = new CancellationTokenSource(); - - var services = new ServiceCollection(); - services.AddTransient(); - services.AddTransient(); - services.AddLogging(); - services.AddSingleton(_options); - services.AddSingleton(_mockStorage.Object); - _provider = services.BuildServiceProvider(); - - _context = new ProcessingContext(_provider, null, _cancellationTokenSource.Token); - } - - //[Fact] - //public async Task ProcessAsync_CancellationTokenCancelled_ThrowsImmediately() - //{ - // // Arrange - // _cancellationTokenSource.Cancel(); - // var fixture = Create(); - - // // Act - // await Assert.ThrowsAsync(() => fixture.s(_context)); - //} - - //[Fact] - //public async Task ProcessAsync() - //{ - // // Arrange - // var job = new CronJob( - // InvocationData.Serialize( - // MethodInvocation.FromExpression(() => Method())).Serialize()); - - // var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.JobId == 42)); - - // _mockStorageConnection - // .Setup(m => m.FetchNextJobAsync()) - // .ReturnsAsync(mockFetchedJob.Object).Verifiable(); - - // _mockStorageConnection - // .Setup(m => m.GetJobAsync(42)) - // .ReturnsAsync(job).Verifiable(); - - // var fixture = Create(); - - // // Act - // fixture.Start(); - - // // Assert - // _mockStorageConnection.VerifyAll(); - // _mockStateChanger.Verify(m => m.ChangeState(job, It.IsAny(), It.IsAny())); - // mockFetchedJob.Verify(m => m.Requeue(), Times.Never); - // mockFetchedJob.Verify(m => m.RemoveFromQueue()); - //} - - //[Fact] - //public async Task ProcessAsync_Exception() - //{ - // // Arrange - // var job = new Job( - // InvocationData.Serialize( - // MethodInvocation.FromExpression(() => Throw())).Serialize()); - - // var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.JobId == 42)); - - // _mockStorageConnection - // .Setup(m => m.FetchNextJobAsync()) - // .ReturnsAsync(mockFetchedJob.Object); - - // _mockStorageConnection - // .Setup(m => m.GetJobAsync(42)) - // .ReturnsAsync(job); - - // _mockStateChanger.Setup(m => m.ChangeState(job, It.IsAny(), It.IsAny())) - // .Throws(); - - // var fixture = Create(); - - // // Act - // await fixture.ProcessAsync(_context); - - // // Assert - // job.Retries.Should().Be(0); - // mockFetchedJob.Verify(m => m.Requeue()); - //} - - //[Fact] - //public async Task ProcessAsync_JobThrows() - //{ - // // Arrange - // var job = new Job( - // InvocationData.Serialize( - // MethodInvocation.FromExpression(() => Throw())).Serialize()); - - // var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.JobId == 42)); - - // _mockStorageConnection - // .Setup(m => m.FetchNextJobAsync()) - // .ReturnsAsync(mockFetchedJob.Object).Verifiable(); - - // _mockStorageConnection - // .Setup(m => m.GetJobAsync(42)) - // .ReturnsAsync(job).Verifiable(); - - // var fixture = Create(); - - // // Act - // await fixture.ProcessAsync(_context); - - // // Assert - // job.Retries.Should().Be(1); - // _mockStorageTransaction.Verify(m => m.UpdateJob(job)); - // _mockStorageConnection.VerifyAll(); - // _mockStateChanger.Verify(m => m.ChangeState(job, It.IsAny(), It.IsAny())); - // mockFetchedJob.Verify(m => m.RemoveFromQueue()); - //} - - //[Fact] - //public async Task ProcessAsync_JobThrows_WithNoRetry() - //{ - // // Arrange - // var job = new Job( - // InvocationData.Serialize( - // MethodInvocation.FromExpression(j => j.Throw())).Serialize()); - - // var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.JobId == 42)); - - // _mockStorageConnection - // .Setup(m => m.FetchNextJobAsync()) - // .ReturnsAsync(mockFetchedJob.Object); - - // _mockStorageConnection - // .Setup(m => m.GetJobAsync(42)) - // .ReturnsAsync(job); - - // var fixture = Create(); - - // // Act - // await fixture.ProcessAsync(_context); - - // // Assert - // _mockStateChanger.Verify(m => m.ChangeState(job, It.IsAny(), It.IsAny())); - //} - - private JobProcessingServer Create() - => _provider.GetService(); - - //public static void Method() { } - - //public static void Throw() { throw new Exception(); } - - //private class NoRetryJob : IRetryable - //{ - // public RetryBehavior RetryBehavior => new RetryBehavior(false); - // public void Throw() { throw new Exception(); } - //} - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/NoopMessageStore.cs b/test/DotNetCore.CAP.Test/NoopMessageStore.cs deleted file mode 100644 index 7100a21..0000000 --- a/test/DotNetCore.CAP.Test/NoopMessageStore.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; - -namespace DotNetCore.CAP.Test -{ - public class NoopMessageStore : ICapMessageStore - { - public Task ChangeReceivedMessageStateAsync(CapReceivedMessage message, string statusName, - bool autoSaveChanges = true) - { - throw new NotImplementedException(); - } - - public Task ChangeSentMessageStateAsync(CapSentMessage message, string statusName, - bool autoSaveChanges = true) - { - throw new NotImplementedException(); - } - - public Task GetNextReceivedMessageToBeExcuted() - { - throw new NotImplementedException(); - } - - public Task GetNextSentMessageToBeEnqueuedAsync() - { - throw new NotImplementedException(); - } - - public Task RemoveSentMessageAsync(CapSentMessage message) - { - throw new NotImplementedException(); - } - - public Task StoreReceivedMessageAsync(CapReceivedMessage message) - { - throw new NotImplementedException(); - } - - public Task StoreSentMessageAsync(CapSentMessage message) - { - throw new NotImplementedException(); - } - - public Task UpdateReceivedMessageAsync(CapReceivedMessage message) - { - throw new NotImplementedException(); - } - - public Task UpdateSentMessageAsync(CapSentMessage message) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs b/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs new file mode 100644 index 0000000..7ff1d19 --- /dev/null +++ b/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; +using DotNetCore.CAP.Processor.States; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class DefaultDispatcherTest + { + private CancellationTokenSource _cancellationTokenSource; + private ProcessingContext _context; + private IServiceProvider _provider; + private Mock _mockStorageConnection; + private Mock _mockQueueExecutorFactory; + private Mock _mockQueueExecutor; + + public DefaultDispatcherTest() + { + _mockStorageConnection = new Mock(); + _mockQueueExecutorFactory = new Mock(); + _mockQueueExecutor = new Mock(); + _mockQueueExecutorFactory.Setup(x => x.GetInstance(MessageType.Publish)).Returns(_mockQueueExecutor.Object); + _cancellationTokenSource = new CancellationTokenSource(); + + var services = new ServiceCollection(); + services.AddTransient(); + services.AddLogging(); + services.Configure>(x => { }); + services.AddOptions(); + services.AddSingleton(_mockStorageConnection.Object); + services.AddSingleton(_mockQueueExecutorFactory.Object); + _provider = services.BuildServiceProvider(); + + _context = new ProcessingContext(_provider, _cancellationTokenSource.Token); + } + + [Fact] + public void MockTest() + { + Assert.NotNull(_provider.GetServices()); + } + + [Fact] + public async void ProcessAsync_CancellationTokenCancelled_ThrowsImmediately() + { + // Arrange + _cancellationTokenSource.Cancel(); + var fixture = Create(); + + // Act + await Assert.ThrowsAsync(() => fixture.ProcessAsync(_context)); + } + + [Fact] + public async Task ProcessAsync() + { + // Arrange + var job = new CapPublishedMessage { + + }; + + var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.MessageId == 42 && fj.MessageType == MessageType.Publish )); + + _mockStorageConnection + .Setup(m => m.FetchNextMessageAsync()) + .ReturnsAsync(mockFetchedJob.Object).Verifiable(); + + _mockQueueExecutor + .Setup(x => x.ExecuteAsync(_mockStorageConnection.Object, mockFetchedJob.Object)) + .Returns(Task.FromResult(OperateResult.Success)); + + var fixture = Create(); + + // Act + await fixture.ProcessAsync(_context); + + // Assert + _mockStorageConnection.VerifyAll(); + } + + private DefaultDispatcher Create() + => _provider.GetService(); + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs b/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs new file mode 100644 index 0000000..51101bd --- /dev/null +++ b/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs @@ -0,0 +1,59 @@ +using System; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor.States; +using Moq; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class StateChangerTest + { + [Fact] + public void ChangeState() + { + // Arrange + var fixture = Create(); + var message = new CapPublishedMessage + { + StatusName = StatusName.Enqueued + }; + var state = Mock.Of(s => s.Name == "s" && s.ExpiresAfter == null); + var mockTransaction = new Mock(); + + // Act + fixture.ChangeState(message, state, mockTransaction.Object); + + // Assert + Assert.Equal(message.StatusName, "s"); + Assert.Null(message.ExpiresAt); + Mock.Get(state).Verify(s => s.Apply(message, mockTransaction.Object), Times.Once); + mockTransaction.Verify(t => t.UpdateMessage(message), Times.Once); + mockTransaction.Verify(t => t.CommitAsync(), Times.Never); + } + + [Fact] + public void ChangeState_ExpiresAfter() + { + // Arrange + var fixture = Create(); + var message = new CapPublishedMessage + { + StatusName = StatusName.Enqueued + }; + var state = Mock.Of(s => s.Name == "s" && s.ExpiresAfter == TimeSpan.FromHours(1)); + var mockTransaction = new Mock(); + + // Act + fixture.ChangeState(message, state, mockTransaction.Object); + + // Assert + Assert.Equal(message.StatusName, "s"); + Assert.NotNull(message.ExpiresAt); + mockTransaction.Verify(t => t.UpdateMessage(message), Times.Once); + mockTransaction.Verify(t => t.CommitAsync(), Times.Never); + } + + private StateChanger Create() => new StateChanger(); + } +} diff --git a/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs b/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs new file mode 100644 index 0000000..f1bf1e3 --- /dev/null +++ b/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class QueueExecutorFactoryTest + { + private IServiceProvider _provider; + + public QueueExecutorFactoryTest() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddCap(x => { }); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void CanCreateInstance() + { + var queueExecutorFactory = _provider.GetService(); + Assert.NotNull(queueExecutorFactory); + + var publishExecutor = queueExecutorFactory.GetInstance(Models.MessageType.Publish); + Assert.Null(publishExecutor); + + var disPatchExector = queueExecutorFactory.GetInstance(Models.MessageType.Subscribe); + Assert.NotNull(disPatchExector); + } + + [Fact] + public void CanGetSubscribeExector() + { + var queueExecutorFactory = _provider.GetService(); + Assert.NotNull(queueExecutorFactory); + + var publishExecutor = queueExecutorFactory.GetInstance(Models.MessageType.Publish); + Assert.Equal(null, publishExecutor); + } + + + + } +} diff --git a/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs b/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs new file mode 100644 index 0000000..54e2052 --- /dev/null +++ b/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotNetCore.CAP.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class SubscribeFinderTest + { + private IServiceProvider _provider; + + public SubscribeFinderTest() + { + var services = new ServiceCollection(); + services.AddScoped(); + services.AddCap(x => { }); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void CanFindControllers() + { + + } + + [Fact] + public void CanFindSubscribeService() + { + var testService = _provider.GetService(); + Assert.NotNull(testService); + Assert.IsType(testService); + } + } + + public class HomeController + { + + } + + public interface ITestService { } + + public class TestService : ITestService, ICapSubscribe + { + [CapSubscribe("test")] + public void Index() + { + + } + } + + public class CapSubscribeAttribute : TopicAttribute + { + public CapSubscribeAttribute(string name) : base(name) + { + } + } +} diff --git a/test/Shared/MessageManagerTestBase.cs b/test/Shared/MessageManagerTestBase.cs index 0f981e2..21f641a 100644 --- a/test/Shared/MessageManagerTestBase.cs +++ b/test/Shared/MessageManagerTestBase.cs @@ -1,112 +1,113 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit; - -namespace DotNetCore.CAP.Test -{ - public abstract class MessageManagerTestBase - { - private const string NullValue = "(null)"; - - protected virtual bool ShouldSkipDbTests() - { - return false; - } - - protected virtual void SetupMessageServices(IServiceCollection services, object context = null) - { - services.AddSingleton(); - services.AddCap(); - AddMessageStore(services, context); - - services.AddSingleton>(new TestLogger()); - } - - protected virtual ICapMessageStore CreateManager(object context = null, IServiceCollection services = null, - Action configureServices = null) - { - if (services == null) - { - services = new ServiceCollection(); - } - if (context == null) - { - context = CreateTestContext(); - } - SetupMessageServices(services, context); - - configureServices?.Invoke(services); - - return services.BuildServiceProvider().GetService(); - } - - protected abstract object CreateTestContext(); - - protected abstract CapSentMessage CreateTestSentMessage(string content = ""); - protected abstract CapReceivedMessage CreateTestReceivedMessage(string content = ""); - - protected abstract void AddMessageStore(IServiceCollection services, object context = null); - - [Fact] - public async Task CanDeleteSentMessage() - { - if (ShouldSkipDbTests()) - { - return; - } - - var manager = CreateManager(); - var message = CreateTestSentMessage(); - var operateResult = await manager.StoreSentMessageAsync(message); - Assert.NotNull(operateResult); - Assert.True(operateResult.Succeeded); - - operateResult = await manager.RemoveSentMessageAsync(message); - Assert.NotNull(operateResult); - Assert.True(operateResult.Succeeded); - } - - [Fact] - public async Task CanUpdateReceivedMessage() - { - if (ShouldSkipDbTests()) - { - return; - } - - var manager = CreateManager(); - var message = CreateTestReceivedMessage(); - var operateResult = await manager.StoreReceivedMessageAsync(message); - Assert.NotNull(operateResult); - Assert.True(operateResult.Succeeded); - - message.StatusName = StatusName.Processing; - operateResult = await manager.UpdateReceivedMessageAsync(message); - Assert.NotNull(operateResult); - Assert.True(operateResult.Succeeded); - } - - [Fact] - public async Task CanGetNextSendMessage() - { - if (ShouldSkipDbTests()) - { - return; - } - var manager = CreateManager(); - var message = CreateTestSentMessage(); - - var operateResult = await manager.StoreSentMessageAsync(message); - Assert.NotNull(operateResult); - Assert.True(operateResult.Succeeded); - - var storeMessage = await manager.GetNextSentMessageToBeEnqueuedAsync(); - - Assert.Equal(message, storeMessage); - } - } -} \ No newline at end of file +//using System; +//using System.Threading.Tasks; +//using DotNetCore.CAP.Infrastructure; +//using DotNetCore.CAP.Models; +//using Microsoft.AspNetCore.Http; +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Logging; +//using Xunit; + +//namespace DotNetCore.CAP.Test +//{ +// public abstract class MessageManagerTestBase +// { +// private const string NullValue = "(null)"; + +// protected virtual bool ShouldSkipDbTests() +// { +// return false; +// } + +// protected virtual void SetupMessageServices(IServiceCollection services, object context = null) +// { +// services.AddSingleton(); +// services.AddCap(); +// AddMessageStore(services, context); + +// services.AddSingleton>(new TestLogger()); +// } + +// protected virtual ICapMessageStore CreateManager(object context = null, IServiceCollection services = null, +// Action configureServices = null) +// { +// if (services == null) +// { +// services = new ServiceCollection(); +// } +// if (context == null) +// { +// context = CreateTestContext(); +// } +// SetupMessageServices(services, context); + +// configureServices?.Invoke(services); + +// return services.BuildServiceProvider().GetService(); +// } + +// protected abstract object CreateTestContext(); + +// protected abstract CapSentMessage CreateTestSentMessage(string content = ""); +// protected abstract CapReceivedMessage CreateTestReceivedMessage(string content = ""); + +// protected abstract void AddMessageStore(IServiceCollection services, object context = null); + +// [Fact] +// public async Task CanDeleteSentMessage() +// { +// if (ShouldSkipDbTests()) +// { +// return; +// } + +// var manager = CreateManager(); +// var message = CreateTestSentMessage(); +// var operateResult = await manager.StoreSentMessageAsync(message); +// Assert.NotNull(operateResult); +// Assert.True(operateResult.Succeeded); + +// // operateResult = await manager.RemoveSentMessageAsync(message); +// // Assert.NotNull(operateResult); +// // Assert.True(operateResult.Succeeded); +// } + +// //[Fact] +// //public async Task CanUpdateReceivedMessage() +// //{ +// // if (ShouldSkipDbTests()) +// // { +// // return; +// // } + +// // var manager = CreateManager(); +// // var message = CreateTestReceivedMessage(); +// // // var operateResult = await manager.StoreReceivedMessageAsync(message); +// // // Assert.NotNull(operateResult); +// // // Assert.True(operateResult.Succeeded); + +// // // message.StatusName = StatusName.Processing; +// // // operateResult = await manager.UpdateReceivedMessageAsync(message); +// // // Assert.NotNull(operateResult); +// // // Assert.True(operateResult.Succeeded); +// //} + +// [Fact] +// public async Task CanGetNextSendMessage() +// { +// if (ShouldSkipDbTests()) +// { +// return; +// } +// var manager = CreateManager(); +// var message = CreateTestSentMessage(); + +// var operateResult = await manager.StoreSentMessageAsync(message); +// Assert.NotNull(operateResult); +// Assert.True(operateResult.Succeeded); + +// // var storeMessage = await manager.GetNextSentMessageToBeEnqueuedAsync(); + +// // Assert.Equal(message, storeMessage); +// } +// } +//} \ No newline at end of file