Commit 47d579cd authored by Savorboard's avatar Savorboard

merge from develop.

parents d46dab2d 6f917898
...@@ -32,3 +32,4 @@ obj/ ...@@ -32,3 +32,4 @@ obj/
bin/ bin/
/.idea/.idea.CAP /.idea/.idea.CAP
/.idea/.idea.CAP /.idea/.idea.CAP
/.idea
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.26430.14 VisualStudioVersion = 15.0.26430.15
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B2AE124-6636-4DE9-83A3-70360DABD0C4}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B2AE124-6636-4DE9-83A3-70360DABD0C4}"
EndProject EndProject
...@@ -33,8 +33,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{9E5A7F ...@@ -33,8 +33,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{9E5A7F
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP", "src\DotNetCore.CAP\DotNetCore.CAP.csproj", "{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP", "src\DotNetCore.CAP\DotNetCore.CAP.csproj", "{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3A6B6931-A123-477A-9469-8B468B5385AF}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka", "samples\Sample.Kafka\Sample.Kafka.csproj", "{2F095ED9-5BC9-4512-9013-A47685FB2508}" 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 ...@@ -55,10 +53,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{10C0818D
build\version.props = build\version.props build\version.props = build\version.props
EndProjectSection EndProjectSection
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.Test", "test\DotNetCore.CAP.Test\DotNetCore.CAP.Test.csproj", "{F608B509-A99B-4AC7-8227-42051DD4A578}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
...@@ -69,10 +69,6 @@ Global ...@@ -69,10 +69,6 @@ Global
{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{2F095ED9-5BC9-4512-9013-A47685FB2508}.Debug|Any CPU.Build.0 = 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 {2F095ED9-5BC9-4512-9013-A47685FB2508}.Release|Any CPU.ActiveCfg = Release|Any CPU
...@@ -85,14 +81,17 @@ Global ...@@ -85,14 +81,17 @@ Global
{9961B80E-0718-4280-B2A0-271B003DE26B}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{9961B80E-0718-4280-B2A0-271B003DE26B}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{F608B509-A99B-4AC7-8227-42051DD4A578}.Debug|Any CPU.Build.0 = 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.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
...@@ -100,11 +99,11 @@ Global ...@@ -100,11 +99,11 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{9E5A7F49-8E31-4A71-90CC-1DA9AEDA99EE} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {9E5A7F49-8E31-4A71-90CC-1DA9AEDA99EE} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0}
{E8AF8611-0EA4-4B19-BC48-87C57A87DC66} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {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} {2F095ED9-5BC9-4512-9013-A47685FB2508} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{C42CDE33-0878-4BA0-96F2-4CB7C8FDEAAD} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {C42CDE33-0878-4BA0-96F2-4CB7C8FDEAAD} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{9961B80E-0718-4280-B2A0-271B003DE26B} = {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} {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 EndGlobalSection
EndGlobal EndGlobal
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
[![Travis branch](https://img.shields.io/travis/dotnetcore/CAP/master.svg?label=travis-ci)](https://travis-ci.org/dotnetcore/CAP) [![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) [![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/) [![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) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dotnetcore/CAP/master/LICENSE.txt)
CAP 是一个在分布式系统(SOA、MicroService)中实现最终一致性的库,它具有轻量级、易使用、高性能等特点。 CAP 是一个在分布式系统(SOA、MicroService)中实现最终一致性的库,它具有轻量级、易使用、高性能等特点。
...@@ -30,6 +31,10 @@ CAP 具有消息持久化的功能,当你的服务进行重启或者宕机时 ...@@ -30,6 +31,10 @@ CAP 具有消息持久化的功能,当你的服务进行重启或者宕机时
你可以运行以下下命令在你的项目中安装 CAP。 你可以运行以下下命令在你的项目中安装 CAP。
```
PM> Install-Package DotNetCore.CAP -Pre
```
如果你的消息队列使用的是 Kafka 的话,你可以: 如果你的消息队列使用的是 Kafka 的话,你可以:
``` ```
...@@ -42,10 +47,10 @@ PM> Install-Package DotNetCore.CAP.Kafka -Pre ...@@ -42,10 +47,10 @@ PM> Install-Package DotNetCore.CAP.Kafka -Pre
PM> Install-Package DotNetCore.CAP.RabbitMQ -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 ### Configuration
...@@ -59,9 +64,21 @@ public void ConfigureServices(IServiceCollection services) ...@@ -59,9 +64,21 @@ public void ConfigureServices(IServiceCollection services)
services.AddDbContext<AppDbContext>(); services.AddDbContext<AppDbContext>();
services.AddCap() services.AddCap(x =>
.AddEntityFrameworkStores<AppDbContext>() {
.AddKafka(x => x.Servers = "localhost:9092"); // 如果你的 SqlServer 使用的 EF 进行数据操作,你需要添加如下配置:
// 注意: 你不需要再次配置 x.UseSqlServer(""")
x.UseEntityFramework<AppDbContext>();
// 如果你使用的Dapper,你需要添加如下配置:
x.UseSqlServer("数据库连接字符串");
// 如果你使用的 RabbitMQ 作为MQ,你需要添加如下配置:
x.UseRabbitMQ("localhost");
//如果你使用的 Kafka 作为MQ,你需要添加如下配置:
x.UseKafka("localhost");
});
} }
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
...@@ -96,6 +113,18 @@ public class PublishController : Controller ...@@ -96,6 +113,18 @@ public class PublishController : Controller
return Ok(); return Ok();
} }
[Route("~/checkAccountWithTrans")]
public async Task<IActionResult> 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();
}
} }
``` ```
......
...@@ -3,9 +3,9 @@ os: Visual Studio 2015 ...@@ -3,9 +3,9 @@ os: Visual Studio 2015
environment: environment:
BUILDING_ON_PLATFORM: win BUILDING_ON_PLATFORM: win
BuildEnvironment: appveyor 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: services:
- mssql2012sp1 - mssql2014
build_script: build_script:
- ps: ./ConfigureMSDTC.ps1 - ps: ./ConfigureMSDTC.ps1
- ps: ./build.ps1 - ps: ./build.ps1
...@@ -17,6 +17,6 @@ deploy: ...@@ -17,6 +17,6 @@ deploy:
on: on:
appveyor_repo_tag: true appveyor_repo_tag: true
api_key: api_key:
secure: P4da9c6a6-00e1-47d0-a821-b62380362dc9 secure: U62rpGTEqztrUO4ncscm4XSaAoCSmWwT/rOWO/2JJS44psJvl0QpjRL0o0ughMoY
skip_symbols: true skip_symbols: true
artifact: /artifacts\/packages\/.+\.nupkg/ artifact: /artifacts\/packages\/.+\.nupkg/
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<VersionMajor>0</VersionMajor> <VersionMajor>0</VersionMajor>
<VersionMinor>1</VersionMinor> <VersionMinor>1</VersionMinor>
<VersionPatch>0</VersionPatch> <VersionPatch>1</VersionPatch>
<VersionQuality></VersionQuality> <VersionQuality></VersionQuality>
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
</PropertyGroup> </PropertyGroup>
......
using System; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Sample.Kafka namespace Sample.Kafka
{ {
public class AppDbContext : DbContext public class AppDbContext : DbContext
{ {
public DbSet<CapSentMessage> SentMessages { get; set; }
public DbSet<CapReceivedMessage> ReceivedMessages { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=Test;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True"); optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=Test;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True");
} //optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Test;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CapSentMessage>().Property(x => x.StatusName).HasMaxLength(50);
modelBuilder.Entity<CapReceivedMessage>().Property(x => x.StatusName).HasMaxLength(50);
base.OnModelCreating(modelBuilder);
} }
} }
} }
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP; using DotNetCore.CAP;
using DotNetCore.CAP.Kafka; using DotNetCore.CAP.RabbitMQ;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
...@@ -11,10 +11,12 @@ namespace Sample.Kafka.Controllers ...@@ -11,10 +11,12 @@ namespace Sample.Kafka.Controllers
public class ValuesController : Controller, ICapSubscribe public class ValuesController : Controller, ICapSubscribe
{ {
private readonly ICapPublisher _producer; private readonly ICapPublisher _producer;
private readonly AppDbContext _dbContext ;
public ValuesController(ICapPublisher producer) public ValuesController(ICapPublisher producer, AppDbContext dbContext)
{ {
_producer = producer; _producer = producer;
_dbContext = dbContext;
} }
[Route("/")] [Route("/")]
...@@ -27,15 +29,19 @@ namespace Sample.Kafka.Controllers ...@@ -27,15 +29,19 @@ namespace Sample.Kafka.Controllers
[CapSubscribe("zzwl.topic.finace.callBack", Group = "test")] [CapSubscribe("zzwl.topic.finace.callBack", Group = "test")]
public void KafkaTest(Person person) public void KafkaTest(Person person)
{ {
Console.WriteLine(person.Name); Console.WriteLine(DateTime.Now);
Console.WriteLine(person.Age);
} }
[Route("~/send")] [Route("~/send")]
public async Task<IActionResult> SendTopic() public async Task<IActionResult> 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(); return Ok();
} }
......
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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("Added");
b.Property<string>("Content");
b.Property<string>("Group");
b.Property<string>("KeyName");
b.Property<DateTime>("LastRun");
b.Property<int>("Retries");
b.Property<string>("StatusName")
.HasMaxLength(50);
b.HasKey("Id");
b.ToTable("ReceivedMessages");
});
modelBuilder.Entity("DotNetCore.CAP.Infrastructure.CapSentMessage", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("Added");
b.Property<string>("Content");
b.Property<string>("KeyName");
b.Property<DateTime>("LastRun");
b.Property<int>("Retries");
b.Property<string>("StatusName")
.HasMaxLength(50);
b.HasKey("Id");
b.ToTable("SentMessages");
});
}
}
}
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<string>(nullable: false),
Added = table.Column<DateTime>(nullable: false),
Content = table.Column<string>(nullable: true),
Group = table.Column<string>(nullable: true),
KeyName = table.Column<string>(nullable: true),
LastRun = table.Column<DateTime>(nullable: false),
Retries = table.Column<int>(nullable: false),
StatusName = table.Column<string>(maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ReceivedMessages", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SentMessages",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Added = table.Column<DateTime>(nullable: false),
Content = table.Column<string>(nullable: true),
KeyName = table.Column<string>(nullable: true),
LastRun = table.Column<DateTime>(nullable: false),
Retries = table.Column<int>(nullable: false),
StatusName = table.Column<string>(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");
}
}
}
...@@ -24,9 +24,8 @@ ...@@ -24,9 +24,8 @@
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.EntityFrameworkCore\DotNetCore.CAP.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.Kafka\DotNetCore.CAP.Kafka.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.RabbitMQ\DotNetCore.CAP.RabbitMQ.csproj" /> <ProjectReference Include="..\..\src\DotNetCore.CAP.RabbitMQ\DotNetCore.CAP.RabbitMQ.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" /> <ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup> </ItemGroup>
......
...@@ -25,15 +25,12 @@ namespace Sample.Kafka ...@@ -25,15 +25,12 @@ namespace Sample.Kafka
{ {
services.AddDbContext<AppDbContext>(); services.AddDbContext<AppDbContext>();
services.AddCap() services.AddCap(x =>
.AddEntityFrameworkStores<AppDbContext>()
.AddRabbitMQ(x =>
{ {
x.HostName = "192.168.2.206"; x.UseEntityFramework<AppDbContext>();
x.UserName = "admin"; //x.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Test;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True");
x.Password = "123123"; x.UseRabbitMQ(o => { o.HostName = "192.168.2.206"; o.UserName = "admin"; o.Password = "123123"; });
}); });
//.AddKafka(x => x.Servers = "");
// Add framework services. // Add framework services.
services.AddMvc(); services.AddMvc();
......
using DotNetCore.CAP;
using DotNetCore.CAP.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Contains extension methods to <see cref="CapBuilder"/> for adding entity framework stores.
/// </summary>
public static class CapEntityFrameworkBuilderExtensions
{
/// <summary>
/// Adds an Entity Framework implementation of message stores.
/// </summary>
/// <typeparam name="TContext">The Entity Framework database context to use.</typeparam>
/// <returns>The <see cref="CapBuilder"/> instance this method extends.</returns>
public static CapBuilder AddEntityFrameworkStores<TContext>(this CapBuilder builder)
where TContext : DbContext
{
builder.Services.AddScoped<ICapMessageStore, CapMessageStore<TContext>>();
return builder;
}
}
}
\ No newline at end of file
using DotNetCore.CAP.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace DotNetCore.CAP.EntityFrameworkCore
{
/// <summary>
/// Base class for the Entity Framework database context used for CAP.
/// </summary>
public class CapDbContext : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="CapDbContext"/>.
/// </summary>
public CapDbContext() { }
/// <summary>
/// Initializes a new instance of the <see cref="CapDbContext"/>.
/// </summary>
/// <param name="options">The options to be used by a <see cref="DbContext"/>.</param>
public CapDbContext(DbContextOptions options) : base(options) { }
/// <summary>
/// Gets or sets the <see cref="CapSentMessage"/> of Messages.
/// </summary>
public DbSet<CapSentMessage> CapSentMessages { get; set; }
/// <summary>
/// Gets or sets the <see cref="CapReceivedMessages"/> of Messages.
/// </summary>
public DbSet<CapReceivedMessage> CapReceivedMessages { get; set; }
/// <summary>
/// Configures the schema for the identity framework.
/// </summary>
/// <param name="modelBuilder">
/// The builder being used to construct the model for this context.
/// </param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CapSentMessage>(b =>
{
b.HasKey(m => m.Id);
b.Property(p => p.StatusName).HasMaxLength(50);
});
modelBuilder.Entity<CapReceivedMessage>(b =>
{
b.Property(p => p.StatusName).HasMaxLength(50);
});
}
}
}
\ No newline at end of file
using System;
using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace DotNetCore.CAP.EntityFrameworkCore
{
/// <summary>
/// Represents a new instance of a persistence store for the specified message types.
/// </summary>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
public class CapMessageStore<TContext> : ICapMessageStore where TContext : DbContext
{
/// <summary>
/// Constructs a new instance of <see cref="TContext"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
public CapMessageStore(TContext context)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
}
public TContext Context { get; private set; }
private DbSet<CapSentMessage> SentMessages => Context.Set<CapSentMessage>();
private DbSet<CapReceivedMessage> ReceivedMessages => Context.Set<CapReceivedMessage>();
/// <summary>
/// Creates the specified <paramref name="message"/> in the cap message store.
/// </summary>
/// <param name="message">The message to create.</param>
public async Task<OperateResult> StoreSentMessageAsync(CapSentMessage message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
Context.Add(message);
await Context.SaveChangesAsync();
return OperateResult.Success;
}
public async Task<OperateResult> 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;
}
/// <summary>
/// First Enqueued Message.
/// </summary>
public async Task<CapSentMessage> GetNextSentMessageToBeEnqueuedAsync()
{
return await SentMessages.FirstOrDefaultAsync(x => x.StatusName == StatusName.Enqueued);
}
/// <summary>
/// Updates a message in a store as an asynchronous operation.
/// </summary>
/// <param name="message">The message to update in the store.</param>
public async Task<OperateResult> 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
});
}
}
/// <summary>
/// Deletes the specified <paramref name="message"/> from the consistency message store.
/// </summary>
/// <param name="message">The message to delete.</param>
public async Task<OperateResult> 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
});
}
}
/// <summary>
/// Creates the specified <paramref name="message"/> in the consistency message store.
/// </summary>
/// <param name="message">The message to create.</param>
public async Task<OperateResult> StoreReceivedMessageAsync(CapReceivedMessage message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
Context.Add(message);
await Context.SaveChangesAsync();
return OperateResult.Success;
}
public async Task<OperateResult> 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<CapReceivedMessage> GetNextReceivedMessageToBeExcuted()
{
return await ReceivedMessages.FirstOrDefaultAsync(x => x.StatusName == StatusName.Enqueued);
}
/// <summary>
/// Updates the specified <paramref name="message"/> in the message store.
/// </summary>
/// <param name="message">The message to update.</param>
public async Task<OperateResult> 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
using System;
using DotNetCore.CAP;
using DotNetCore.CAP.Job;
using DotNetCore.CAP.Kafka;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Contains extension methods to <see cref="CapBuilder"/> for adding kafka service.
/// </summary>
public static class CapBuilderExtensions
{
/// <summary>
/// Adds an Kafka implementation of CAP messages queue.
/// </summary>
/// <param name="builder">The <see cref="CapBuilder"/> instance this method extends</param>
/// <param name="setupAction">An action to configure the <see cref="KafkaOptions"/>.</param>
/// <returns>An <see cref="CapBuilder"/> for creating and configuring the CAP system.</returns>
public static CapBuilder AddKafka(this CapBuilder builder, Action<KafkaOptions> setupAction)
{
if (setupAction == null) throw new ArgumentNullException(nameof(setupAction));
builder.Services.Configure(setupAction);
builder.Services.AddSingleton<IConsumerClientFactory, KafkaConsumerClientFactory>();
builder.Services.AddTransient<IJobProcessor, KafkaJobProcessor>();
return builder;
}
}
}
\ No newline at end of file
using System;
using DotNetCore.CAP.Kafka;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class KafkaCapOptionsExtension : ICapOptionsExtension
{
private readonly Action<KafkaOptions> _configure;
public KafkaCapOptionsExtension(Action<KafkaOptions> configure)
{
_configure = configure;
}
public void AddServices(IServiceCollection services)
{
services.Configure(_configure);
var kafkaOptions = new KafkaOptions();
_configure(kafkaOptions);
services.AddSingleton(kafkaOptions);
services.AddSingleton<IConsumerClientFactory, KafkaConsumerClientFactory>();
services.AddTransient<IQueueExecutor, PublishQueueExecutor>();
}
}
}
\ No newline at end of file
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace DotNetCore.CAP.Kafka // ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{ {
/// <summary> /// <summary>
/// Provides programmatic configuration for the CAP kafka project. /// Provides programmatic configuration for the CAP kafka project.
......
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<KafkaOptions> configure)
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
options.RegisterExtension(new KafkaCapOptionsExtension(configure));
return options;
}
}
}
\ No newline at end of file
using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions;
namespace DotNetCore.CAP.Kafka // ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{ {
public class CapSubscribeAttribute : TopicAttribute public class CapSubscribeAttribute : TopicAttribute
{ {
public CapSubscribeAttribute(string topicName) public CapSubscribeAttribute(string name)
: this(topicName, 0) : this(name, 0)
{ {
} }
/// <summary> /// <summary>
/// Not support /// Not support
/// </summary> /// </summary>
public CapSubscribeAttribute(string topicName, int partition) public CapSubscribeAttribute(string name, int partition)
: this(topicName, partition, 0) : this(name, partition, 0)
{ {
} }
/// <summary> /// <summary>
/// Not support /// Not support
/// </summary> /// </summary>
public CapSubscribeAttribute(string topicName, int partition, long offset) public CapSubscribeAttribute(string name, int partition, long offset)
: base(topicName) : base(name)
{ {
Offset = offset; Offset = offset;
Partition = partition; Partition = partition;
......
using System; using System;
using System.Text; using System.Text;
using System.Threading;
using Confluent.Kafka; using Confluent.Kafka;
using Confluent.Kafka.Serialization; using Confluent.Kafka.Serialization;
using DotNetCore.CAP.Infrastructure;
namespace DotNetCore.CAP.Kafka namespace DotNetCore.CAP.Kafka
{ {
...@@ -38,10 +38,11 @@ namespace DotNetCore.CAP.Kafka ...@@ -38,10 +38,11 @@ namespace DotNetCore.CAP.Kafka
_consumerClient.Subscribe(topicName); _consumerClient.Subscribe(topicName);
} }
public void Listening(TimeSpan timeout) public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
{ {
while (true) while (true)
{ {
cancellationToken.ThrowIfCancellationRequested();
_consumerClient.Poll(timeout); _consumerClient.Poll(timeout);
} }
} }
...@@ -73,13 +74,12 @@ namespace DotNetCore.CAP.Kafka ...@@ -73,13 +74,12 @@ namespace DotNetCore.CAP.Kafka
var message = new MessageContext var message = new MessageContext
{ {
Group = _groupId, Group = _groupId,
KeyName = e.Topic, Name = e.Topic,
Content = e.Value Content = e.Value
}; };
MessageReceieved?.Invoke(sender, message); MessageReceieved?.Invoke(sender, message);
} }
#endregion private methods #endregion private methods
} }
} }
\ No newline at end of file
using System;
using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP.Kafka
{
internal static class LoggerExtensions
{
private static readonly Action<ILogger, Exception> _collectingExpiredEntities;
private static readonly Action<ILogger, Exception> _installing;
private static readonly Action<ILogger, Exception> _installingError;
private static readonly Action<ILogger, Exception> _installingSuccess;
private static readonly Action<ILogger, Exception> _jobFailed;
private static readonly Action<ILogger, Exception> _jobFailedWillRetry;
private static readonly Action<ILogger, double, Exception> _jobExecuted;
private static readonly Action<ILogger, int, Exception> _jobRetrying;
private static readonly Action<ILogger, int, Exception> _jobCouldNotBeLoaded;
private static readonly Action<ILogger, string, Exception> _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<int>(
LogLevel.Debug,
3,
"Retrying a job: {Retries}...");
_jobExecuted = LoggerMessage.Define<double>(
LogLevel.Debug,
4,
"Job executed. Took: {Seconds} secs.");
_jobCouldNotBeLoaded = LoggerMessage.Define<int>(
LogLevel.Warning,
5,
"Could not load a job: '{JobId}'.");
_exceptionOccuredWhileExecutingJob = LoggerMessage.Define<string>(
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
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<KafkaOptions> options,
ILogger<PublishQueueExecutor> logger)
: base(stateChanger, logger)
{
_logger = logger;
_kafkaOptions = options.Value;
}
public override Task<OperateResult> PublishAsync(string keyName, string content)
{
try
{
var config = _kafkaOptions.AsRdkafkaConfig();
using (var producer = new Producer<Null, string>(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
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<RabbitMQOptions> setupOptions)
{
if (setupOptions == null) throw new ArgumentNullException(nameof(setupOptions));
builder.Services.Configure(setupOptions);
builder.Services.AddSingleton<IConsumerClientFactory, RabbitMQConsumerClientFactory>();
builder.Services.AddTransient<IJobProcessor, RabbitJobProcessor>();
return builder;
}
}
}
\ No newline at end of file
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<RabbitMQOptions> configure)
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
options.RegisterExtension(new RabbitMQCapOptionsExtension(configure));
return options;
}
}
}
\ No newline at end of file
namespace DotNetCore.CAP.RabbitMQ // ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{ {
public class RabbitMQOptions public class RabbitMQOptions
{ {
...@@ -34,7 +35,7 @@ ...@@ -34,7 +35,7 @@
public string HostName { get; set; } = "localhost"; public string HostName { get; set; } = "localhost";
/// <summary> The topic exchange type. </summary> /// <summary> The topic exchange type. </summary>
internal string EXCHANGE_TYPE = "topic"; internal const string ExchangeType = "topic";
/// <summary> /// <summary>
/// Password to use when authenticating to the server. /// Password to use when authenticating to the server.
......
using System;
using DotNetCore.CAP.RabbitMQ;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class RabbitMQCapOptionsExtension : ICapOptionsExtension
{
private readonly Action<RabbitMQOptions> _configure;
public RabbitMQCapOptionsExtension(Action<RabbitMQOptions> configure)
{
_configure = configure;
}
public void AddServices(IServiceCollection services)
{
services.Configure(_configure);
var rabbitMQOptions = new RabbitMQOptions();
_configure(rabbitMQOptions);
services.AddSingleton(rabbitMQOptions);
services.AddSingleton<IConsumerClientFactory, RabbitMQConsumerClientFactory>();
services.AddTransient<IQueueExecutor, PublishQueueExecutor>();
}
}
}
\ No newline at end of file
using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions;
namespace DotNetCore.CAP.RabbitMQ // ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{ {
public class CapSubscribeAttribute : TopicAttribute public class CapSubscribeAttribute : TopicAttribute
{ {
public CapSubscribeAttribute(string routingKey) : base(routingKey) public CapSubscribeAttribute(string name) : base(name)
{ {
} }
} }
} }
\ No newline at end of file
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> capOptions,
IOptions<RabbitMQOptions> rabbitMQOptions,
ILogger<RabbitJobProcessor> 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<bool> Step(ProcessingContext context)
{
using (var scopedContext = context.CreateScope())
{
var provider = scopedContext.Provider;
var messageStore = provider.GetRequiredService<ICapMessageStore>();
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
using System;
using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP.RabbitMQ
{
internal static class LoggerExtensions
{
private static Action<ILogger, Exception> _collectingExpiredEntities;
private static Action<ILogger, Exception> _installing;
private static Action<ILogger, Exception> _installingError;
private static Action<ILogger, Exception> _installingSuccess;
private static Action<ILogger, Exception> _jobFailed;
private static Action<ILogger, Exception> _jobFailedWillRetry;
private static Action<ILogger, double, Exception> _jobExecuted;
private static Action<ILogger, int, Exception> _jobRetrying;
private static Action<ILogger, int, Exception> _jobCouldNotBeLoaded;
private static Action<ILogger, string, Exception> _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<int>(
LogLevel.Debug,
3,
"Retrying a job: {Retries}...");
_jobExecuted = LoggerMessage.Define<double>(
LogLevel.Debug,
4,
"Job executed. Took: {Seconds} secs.");
_jobCouldNotBeLoaded = LoggerMessage.Define<int>(
LogLevel.Warning,
5,
"Could not load a job: '{JobId}'.");
_exceptionOccuredWhileExecutingJob = LoggerMessage.Define<string>(
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
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<RabbitMQOptions> options,
ILogger<PublishQueueExecutor> logger)
: base(stateChanger, logger)
{
_logger = logger;
_rabbitMQOptions = options.Value;
}
public override Task<OperateResult> 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
using System; using System;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using RabbitMQ.Client; using RabbitMQ.Client;
using RabbitMQ.Client.Events; using RabbitMQ.Client.Events;
...@@ -45,18 +45,18 @@ namespace DotNetCore.CAP.RabbitMQ ...@@ -45,18 +45,18 @@ namespace DotNetCore.CAP.RabbitMQ
_connection = _connectionFactory.CreateConnection(); _connection = _connectionFactory.CreateConnection();
_channel = _connection.CreateModel(); _channel = _connection.CreateModel();
_channel.ExchangeDeclare(exchange: _exchageName, type: _rabbitMQOptions.EXCHANGE_TYPE); _channel.ExchangeDeclare(exchange: _exchageName, type: RabbitMQOptions.ExchangeType);
_channel.QueueDeclare(_queueName, exclusive: false); _channel.QueueDeclare(_queueName, exclusive: false);
} }
public void Listening(TimeSpan timeout) public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
{ {
var consumer = new EventingBasicConsumer(_channel); var consumer = new EventingBasicConsumer(_channel);
consumer.Received += OnConsumerReceived; consumer.Received += OnConsumerReceived;
_channel.BasicConsume(_queueName, false, consumer); _channel.BasicConsume(_queueName, false, consumer);
while (true) while (true)
{ {
Task.Delay(timeout); Task.Delay(timeout, cancellationToken).Wait();
} }
} }
...@@ -87,7 +87,7 @@ namespace DotNetCore.CAP.RabbitMQ ...@@ -87,7 +87,7 @@ namespace DotNetCore.CAP.RabbitMQ
var message = new MessageContext var message = new MessageContext
{ {
Group = _queueName, Group = _queueName,
KeyName = e.RoutingKey, Name = e.RoutingKey,
Content = Encoding.UTF8.GetString(e.Body) Content = Encoding.UTF8.GetString(e.Body)
}; };
MessageReceieved?.Invoke(sender, message); MessageReceieved?.Invoke(sender, message);
......
using System;
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class EFOptions
{
public const string DefaultSchema = "Cap";
/// <summary>
/// Gets or sets the schema to use when creating database objects.
/// Default is <see cref="DefaultSchema"/>.
/// </summary>
public string Schema { get; set; } = DefaultSchema;
/// <summary>
/// EF dbcontext type.
/// </summary>
public Type DbContextType { get; internal set; }
}
}
\ No newline at end of file
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<SqlServerOptions> configure)
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
options.RegisterExtension(new SqlServerCapOptionsExtension(configure));
return options;
}
public static CapOptions UseEntityFramework<TContext>(this CapOptions options)
where TContext : DbContext
{
return options.UseEntityFramework<TContext>(opt =>
{
opt.DbContextType = typeof(TContext);
});
}
public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> 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
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<SqlServerOptions> _configure;
public SqlServerCapOptionsExtension(Action<SqlServerOptions> configure)
{
_configure = configure;
}
public void AddServices(IServiceCollection services)
{
services.AddSingleton<IStorage, SqlServerStorage>();
services.AddScoped<IStorageConnection, SqlServerStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddTransient<IAdditionalProcessor, DefaultAdditionalProcessor>();
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
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class SqlServerOptions : EFOptions
{
/// <summary>
/// Gets or sets the database's connection string that will be used to store database entities.
/// </summary>
public string ConnectionString { get; set; }
}
}
\ No newline at end of file
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<T>(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
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework> <TargetFramework>netstandard1.6</TargetFramework>
<AssemblyName>DotNetCore.CAP.EntityFrameworkCore</AssemblyName> <AssemblyName>DotNetCore.CAP.SqlServer</AssemblyName>
<PackageId>DotNetCore.CAP.EntityFrameworkCore</PackageId> <PackageId>DotNetCore.CAP.SqlServer</PackageId>
<NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion> <NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion>
<PackageTargetFallback>$(PackageTargetFallback);dnxcore50</PackageTargetFallback> <PackageTargetFallback>$(PackageTargetFallback);dnxcore50</PackageTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.2" />
<PackageReference Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
......
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
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<DefaultAdditionalProcessor> 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
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
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<SqlServerStorage> 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
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<CapPublishedMessage> 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<CapPublishedMessage>(sql);
}
}
public Task<IFetchedMessage> 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<CapPublishedMessage> 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<CapPublishedMessage>(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<CapReceivedMessage> 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<CapReceivedMessage>(sql);
}
}
public async Task<CapReceivedMessage> 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<CapReceivedMessage>(sql);
}
}
public void Dispose()
{
}
private async Task<IFetchedMessage> 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<FetchedMessage>(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
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
...@@ -8,9 +8,9 @@ namespace DotNetCore.CAP.Abstractions ...@@ -8,9 +8,9 @@ namespace DotNetCore.CAP.Abstractions
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public abstract class TopicAttribute : Attribute public abstract class TopicAttribute : Attribute
{ {
protected TopicAttribute(string topicName) protected TopicAttribute(string name)
{ {
Name = topicName; Name = name;
} }
/// <summary> /// <summary>
......
using System; using System;
using DotNetCore.CAP.Job;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP namespace DotNetCore.CAP
...@@ -35,38 +34,6 @@ namespace DotNetCore.CAP ...@@ -35,38 +34,6 @@ namespace DotNetCore.CAP
return this; return this;
} }
/// <summary>
/// Adds a singleton service of the type specified in serviceType with an implementation
/// </summary>
private CapBuilder AddSingleton<TService, TImplementation>()
where TService : class
where TImplementation : class, TService
{
Services.AddSingleton<TService, TImplementation>();
return this;
}
/// <summary>
/// Add an <see cref="ICapMessageStore"/> .
/// </summary>
/// <typeparam name="T">The type for the <see cref="ICapMessageStore"/> to add. </typeparam>
/// <returns>The current <see cref="CapBuilder"/> instance.</returns>
public virtual CapBuilder AddMessageStore<T>()
where T : class, ICapMessageStore
{
return AddScoped(typeof(ICapMessageStore), typeof(T));
}
/// <summary>
/// Add an <see cref="IJob"/> for process <see cref="CapJob"/>.
/// </summary>
/// <typeparam name="T">The type of the job.</typeparam>
public virtual CapBuilder AddJobs<T>()
where T : class, IJob
{
return AddSingleton<IJob, T>();
}
/// <summary> /// <summary>
/// Add an <see cref="ICapPublisher"/>. /// Add an <see cref="ICapPublisher"/>.
/// </summary> /// </summary>
......
namespace DotNetCore.CAP using System;
using System.Collections.Generic;
namespace DotNetCore.CAP
{ {
/// <summary> /// <summary>
/// Represents all the options you can use to configure the system. /// Represents all the options you can use to configure the system.
/// </summary> /// </summary>
public class CapOptions public class CapOptions
{ {
internal IList<ICapOptionsExtension> Extensions { get; private set; }
/// <summary> /// <summary>
/// Default value for polling delay timeout, in seconds. /// Default value for polling delay timeout, in seconds.
/// </summary> /// </summary>
public const int DefaultPollingDelay = 8; public const int DefaultPollingDelay = 8;
/// <summary>
/// Default value for CAP job.
/// </summary>
public const string DefaultCronExp = "* * * * *";
public CapOptions() public CapOptions()
{ {
CronExp = DefaultCronExp;
PollingDelay = DefaultPollingDelay; PollingDelay = DefaultPollingDelay;
Extensions = new List<ICapOptionsExtension>();
} }
/// <summary> /// <summary>
/// Corn expression for configuring retry cron job. Default is 1 min. /// Productor job polling delay time. Default is 8 sec.
/// </summary> /// </summary>
public string CronExp { get; set; } public int PollingDelay { get; set; } = 8;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public int PollingDelay { get; set; } = 8; public WebHook WebHook { get; set; }
/// <summary>
/// Registers an extension that will be executed when building services.
/// </summary>
/// <param name="extension"></param>
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
...@@ -6,7 +6,8 @@ using DotNetCore.CAP.Abstractions; ...@@ -6,7 +6,8 @@ using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Abstractions.ModelBinding; using DotNetCore.CAP.Abstractions.ModelBinding;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Internal; using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Job; using DotNetCore.CAP.Processor;
using DotNetCore.CAP.Processor.States;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
...@@ -16,16 +17,6 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -16,16 +17,6 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary> /// </summary>
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
/// <summary>
/// Adds and configures the CAP services for the consitence.
/// </summary>
/// <param name="services">The services available in the application.</param>
/// <returns>An <see cref="CapBuilder"/> for application services.</returns>
public static CapBuilder AddCap(this IServiceCollection services)
{
return services.AddCap(x => new CapOptions());
}
/// <summary> /// <summary>
/// Adds and configures the consistence services for the consitence. /// Adds and configures the consistence services for the consitence.
/// </summary> /// </summary>
...@@ -36,10 +27,12 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -36,10 +27,12 @@ namespace Microsoft.Extensions.DependencyInjection
this IServiceCollection services, this IServiceCollection services,
Action<CapOptions> setupAction) Action<CapOptions> setupAction)
{ {
if (setupAction == null) throw new ArgumentNullException(nameof(setupAction));
services.TryAddSingleton<CapMarkerService>(); services.TryAddSingleton<CapMarkerService>();
services.Configure(setupAction); services.Configure(setupAction);
AddConsumerServices(services); AddSubscribeServices(services);
services.TryAddSingleton<IConsumerServiceSelector, DefaultConsumerServiceSelector>(); services.TryAddSingleton<IConsumerServiceSelector, DefaultConsumerServiceSelector>();
services.TryAddSingleton<IModelBinder, DefaultModelBinder>(); services.TryAddSingleton<IModelBinder, DefaultModelBinder>();
...@@ -47,19 +40,32 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -47,19 +40,32 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<MethodMatcherCache>(); services.TryAddSingleton<MethodMatcherCache>();
services.AddSingleton<IProcessingServer, ConsumerHandler>(); services.AddSingleton<IProcessingServer, ConsumerHandler>();
services.AddSingleton<IProcessingServer, JobProcessingServer>(); services.AddSingleton<IProcessingServer, CapProcessingServer>();
services.AddSingleton<IBootstrapper, DefaultBootstrapper>(); services.AddSingleton<IBootstrapper, DefaultBootstrapper>();
services.AddSingleton<IStateChanger, StateChanger>();
//Processors
services.AddTransient<PublishQueuer>();
services.AddTransient<SubscribeQueuer>();
services.AddTransient<IDispatcher, DefaultDispatcher>();
services.TryAddTransient<IJobProcessor, CronJobProcessor>(); //Executors
services.TryAddSingleton<IJob, CapJob>(); services.AddSingleton<IQueueExecutorFactory, QueueExecutorFactory>();
services.TryAddTransient<DefaultCronJobRegistry>(); services.AddSingleton<IQueueExecutor, SubscibeQueueExecutor>();
services.TryAddScoped<ICapPublisher, DefaultCapPublisher>(); //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); return new CapBuilder(services);
} }
private static void AddConsumerServices(IServiceCollection services) private static void AddSubscribeServices(IServiceCollection services)
{ {
var consumerListenerServices = new Dictionary<Type, Type>(); var consumerListenerServices = new Dictionary<Type, Type>();
foreach (var rejectedServices in services) foreach (var rejectedServices in services)
......
...@@ -20,8 +20,8 @@ ...@@ -20,8 +20,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" />
<PackageReference Include="ncrontab" Version="3.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="System.Data.Common" Version="4.3.0" />
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" /> <PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
</ItemGroup> </ItemGroup>
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
...@@ -24,7 +23,7 @@ namespace DotNetCore.CAP ...@@ -24,7 +23,7 @@ namespace DotNetCore.CAP
public DefaultBootstrapper( public DefaultBootstrapper(
ILogger<DefaultBootstrapper> logger, ILogger<DefaultBootstrapper> logger,
IOptions<CapOptions> options, IOptions<CapOptions> options,
ICapMessageStore storage, IStorage storage,
IApplicationLifetime appLifetime, IApplicationLifetime appLifetime,
IServiceProvider provider) IServiceProvider provider)
{ {
...@@ -52,7 +51,7 @@ namespace DotNetCore.CAP ...@@ -52,7 +51,7 @@ namespace DotNetCore.CAP
protected CapOptions Options { get; } protected CapOptions Options { get; }
protected ICapMessageStore Storage { get; } protected IStorage Storage { get; }
protected IEnumerable<IProcessingServer> Servers { get; } protected IEnumerable<IProcessingServer> Servers { get; }
...@@ -65,7 +64,7 @@ namespace DotNetCore.CAP ...@@ -65,7 +64,7 @@ namespace DotNetCore.CAP
private async Task BootstrapTaskAsync() private async Task BootstrapTaskAsync()
{ {
if (_cts.IsCancellationRequested) return; await Storage.InitializeAsync(_cts.Token);
if (_cts.IsCancellationRequested) return; if (_cts.IsCancellationRequested) return;
...@@ -98,7 +97,7 @@ namespace DotNetCore.CAP ...@@ -98,7 +97,7 @@ namespace DotNetCore.CAP
item.Dispose(); item.Dispose();
} }
}); });
return Task.FromResult(0); return Task.CompletedTask;
} }
} }
} }
\ No newline at end of file
using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
namespace DotNetCore.CAP
{
/// <summary>
/// Provides an abstraction for a store which manages CAP message.
/// </summary>
public interface ICapMessageStore
{
/// <summary>
/// Creates a new message in a store as an asynchronous operation.
/// </summary>
/// <param name="message">The message to create in the store.</param>
Task<OperateResult> StoreSentMessageAsync(CapSentMessage message);
/// <summary>
/// Change <see cref="CapSentMessage"/> model status name.
/// </summary>
/// <param name="message">The type of <see cref="CapSentMessage"/>.</param>
/// <param name="statusName">The status name.</param>
/// <param name="autoSaveChanges">auto save dbcontext changes.</param>
/// <returns></returns>
Task<OperateResult> ChangeSentMessageStateAsync(CapSentMessage message, string statusName,
bool autoSaveChanges = true);
/// <summary>
/// Fetches the next message to be executed.
/// </summary>
/// <returns></returns>
Task<CapSentMessage> GetNextSentMessageToBeEnqueuedAsync();
/// <summary>
/// Updates a message in a store as an asynchronous operation.
/// </summary>
/// <param name="message">The message to update in the store.</param>
Task<OperateResult> UpdateSentMessageAsync(CapSentMessage message);
/// <summary>
/// Deletes a message from the store as an asynchronous operation.
/// </summary>
/// <param name="message">The message to delete in the store.</param>
Task<OperateResult> RemoveSentMessageAsync(CapSentMessage message);
/// <summary>
/// Creates a new message in a store as an asynchronous operation.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task<OperateResult> StoreReceivedMessageAsync(CapReceivedMessage message);
/// <summary>
/// Change <see cref="CapReceivedMessage"/> model status name.
/// </summary>
/// <param name="message">The type of <see cref="CapReceivedMessage"/>.</param>
/// <param name="statusName">The status name.</param>
/// <param name="autoSaveChanges">auto save dbcontext changes.</param>
/// <returns></returns>
Task<OperateResult> ChangeReceivedMessageStateAsync(CapReceivedMessage message, string statusName,
bool autoSaveChanges = true);
/// <summary>
/// Fetches the next message to be executed.
/// </summary>
Task<CapReceivedMessage> GetNextReceivedMessageToBeExcuted();
/// <summary>
/// Updates a message in a store as an asynchronous operation.
/// </summary>
/// <param name="message">The message to update in the store.</param>
Task<OperateResult> UpdateReceivedMessageAsync(CapReceivedMessage message);
}
}
\ No newline at end of file
using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP
{
public interface ICapOptionsExtension
{
void AddServices(IServiceCollection services);
}
}
\ No newline at end of file
using System;
using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP
{
/// <summary>
/// Cap <see cref="ICapPublisher"/> default implement.
/// </summary>
public class DefaultCapPublisher : ICapPublisher
{
private readonly ICapMessageStore _store;
private readonly ILogger _logger;
public DefaultCapPublisher(
ICapMessageStore store,
ILogger<DefaultCapPublisher> 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<T>(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
using System.Threading.Tasks; using System.Data;
using System.Threading.Tasks;
namespace DotNetCore.CAP namespace DotNetCore.CAP
{ {
...@@ -9,18 +10,42 @@ namespace DotNetCore.CAP ...@@ -9,18 +10,42 @@ namespace DotNetCore.CAP
{ {
/// <summary> /// <summary>
/// Publish a string message to specified topic. /// Publish a string message to specified topic.
/// <para>
/// If you are using the EntityFramework, you need to configure the DbContextType first.
/// otherwise you need to use overloaded method with IDbConnection and IDbTransaction.
/// </para>
/// </summary> /// </summary>
/// <param name="topic">the topic name or exchange router key.</param> /// <param name="name">the topic name or exchange router key.</param>
/// <param name="content">message body content.</param> /// <param name="content">message body content.</param>
Task PublishAsync(string topic, string content); Task PublishAsync(string name, string content);
/// <summary> /// <summary>
/// Publis a object message to specified topic. /// Publis a object message to specified topic.
/// <para>
/// If you are using the EntityFramework, you need to configure the DbContextType first.
/// otherwise you need to use overloaded method with IDbConnection and IDbTransaction.
/// </para>
/// </summary> /// </summary>
/// <typeparam name="T">The type of conetent object.</typeparam> /// <typeparam name="T">The type of conetent object.</typeparam>
/// <param name="topic">the topic name or exchange router key.</param> /// <param name="name">the topic name or exchange router key.</param>
/// <param name="contentObj">object instance that will be serialized of json.</param> /// <param name="contentObj">object instance that will be serialized of json.</param>
/// <returns></returns> Task PublishAsync<T>(string name, T contentObj);
Task PublishAsync<T>(string topic, T contentObj);
/// <summary>
/// Publish a string message to specified topic with transacton.
/// </summary>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="content">message body content.</param>
/// <param name="dbConnection">the dbConnection of <see cref="IDbConnection"/></param>
Task PublishAsync(string name, string content, IDbConnection dbConnection);
/// <summary>
/// Publish a string message to specified topic with transacton.
/// </summary>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="content">message body content.</param>
/// <param name="dbConnection">the connection of <see cref="IDbConnection"/></param>
/// <param name="dbTransaction">the transaction of <see cref="IDbTransaction"/></param>
Task PublishAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction);
} }
} }
\ No newline at end of file
using System; using System;
using System.Threading;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
namespace DotNetCore.CAP namespace DotNetCore.CAP
...@@ -12,7 +13,7 @@ namespace DotNetCore.CAP ...@@ -12,7 +13,7 @@ namespace DotNetCore.CAP
void Subscribe(string topic, int partition); void Subscribe(string topic, int partition);
void Listening(TimeSpan timeout); void Listening(TimeSpan timeout, CancellationToken cancellationToken);
void Commit(); void Commit();
......
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Internal; using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Models;
using DotNetCore.CAP.Processor;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
...@@ -16,12 +16,11 @@ namespace DotNetCore.CAP ...@@ -16,12 +16,11 @@ namespace DotNetCore.CAP
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IConsumerInvokerFactory _consumerInvokerFactory; private readonly IConsumerInvokerFactory _consumerInvokerFactory;
private readonly IConsumerClientFactory _consumerClientFactory; private readonly IConsumerClientFactory _consumerClientFactory;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly CancellationTokenSource _cts;
private readonly MethodMatcherCache _selector; private readonly MethodMatcherCache _selector;
private readonly CapOptions _options; private readonly CapOptions _options;
private readonly CancellationTokenSource _cts;
private readonly TimeSpan _pollingDelay = TimeSpan.FromSeconds(1); private readonly TimeSpan _pollingDelay = TimeSpan.FromSeconds(1);
...@@ -32,13 +31,12 @@ namespace DotNetCore.CAP ...@@ -32,13 +31,12 @@ namespace DotNetCore.CAP
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IConsumerInvokerFactory consumerInvokerFactory, IConsumerInvokerFactory consumerInvokerFactory,
IConsumerClientFactory consumerClientFactory, IConsumerClientFactory consumerClientFactory,
ILoggerFactory loggerFactory, ILogger<ConsumerHandler> logger,
MethodMatcherCache selector, MethodMatcherCache selector,
IOptions<CapOptions> options) IOptions<CapOptions> options)
{ {
_selector = selector; _selector = selector;
_logger = loggerFactory.CreateLogger<ConsumerHandler>(); _logger = logger;
_loggerFactory = loggerFactory;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_consumerInvokerFactory = consumerInvokerFactory; _consumerInvokerFactory = consumerInvokerFactory;
_consumerClientFactory = consumerClientFactory; _consumerClientFactory = consumerClientFactory;
...@@ -63,9 +61,9 @@ namespace DotNetCore.CAP ...@@ -63,9 +61,9 @@ namespace DotNetCore.CAP
client.Subscribe(item.Attribute.Name); 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; _compositeTask = Task.CompletedTask;
} }
...@@ -83,7 +81,7 @@ namespace DotNetCore.CAP ...@@ -83,7 +81,7 @@ namespace DotNetCore.CAP
try try
{ {
_compositeTask.Wait((int)TimeSpan.FromSeconds(60).TotalMilliseconds); _compositeTask.Wait(TimeSpan.FromSeconds(60));
} }
catch (AggregateException ex) catch (AggregateException ex)
{ {
...@@ -99,56 +97,32 @@ namespace DotNetCore.CAP ...@@ -99,56 +97,32 @@ namespace DotNetCore.CAP
{ {
client.MessageReceieved += (sender, message) => client.MessageReceieved += (sender, message) =>
{ {
_logger.EnqueuingReceivedMessage(message.KeyName, message.Content); _logger.EnqueuingReceivedMessage(message.Name, message.Content);
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
var receviedMessage = StoreMessage(scope, message); var receviedMessage = StoreMessage(scope, message);
client.Commit(); client.Commit();
ProcessMessage(scope, receviedMessage);
} }
Pulse();
}; };
} }
private CapReceivedMessage StoreMessage(IServiceScope serviceScope, MessageContext messageContext) private CapReceivedMessage StoreMessage(IServiceScope serviceScope, MessageContext messageContext)
{ {
var provider = serviceScope.ServiceProvider; var provider = serviceScope.ServiceProvider;
var messageStore = provider.GetRequiredService<ICapMessageStore>(); var messageStore = provider.GetRequiredService<IStorageConnection>();
var receivedMessage = new CapReceivedMessage(messageContext) var receivedMessage = new CapReceivedMessage(messageContext)
{ {
StatusName = StatusName.Enqueued, StatusName = StatusName.Scheduled,
}; };
messageStore.StoreReceivedMessageAsync(receivedMessage).Wait(); messageStore.StoreReceivedMessageAsync(receivedMessage).Wait();
return receivedMessage; return receivedMessage;
} }
private void ProcessMessage(IServiceScope serviceScope, CapReceivedMessage receivedMessage) public void Pulse()
{
var provider = serviceScope.ServiceProvider;
var messageStore = provider.GetRequiredService<ICapMessageStore>();
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
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
...@@ -7,6 +7,8 @@ namespace DotNetCore.CAP ...@@ -7,6 +7,8 @@ namespace DotNetCore.CAP
/// </summary> /// </summary>
public interface IProcessingServer : IDisposable public interface IProcessingServer : IDisposable
{ {
void Pulse();
void Start(); void Start();
} }
} }
\ No newline at end of file
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<BasePublishQueueExecutor> logger)
{
_stateChanger = stateChanger;
_logger = logger;
}
public abstract Task<OperateResult> PublishAsync(string keyName, string content);
public async Task<OperateResult> 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<bool> 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
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<BasePublishQueueExecutor> logger)
{
_selector = selector;
_consumerInvokerFactory = consumerInvokerFactory;
_stateChanger = stateChanger;
_logger = logger;
}
public async Task<OperateResult> 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<OperateResult> 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<bool> 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
using System.Threading.Tasks;
namespace DotNetCore.CAP
{
public interface IQueueExecutor
{
Task<OperateResult> ExecuteAsync(IStorageConnection connection, IFetchedMessage message);
}
}
\ No newline at end of file
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP
{
public interface IQueueExecutorFactory
{
IQueueExecutor GetInstance(MessageType messageType);
}
}
\ No newline at end of file
using System.Threading;
using System.Threading.Tasks;
namespace DotNetCore.CAP
{
/// <summary>
/// Represents a persisted storage.
/// </summary>
public interface IStorage
{
/// <summary>
/// Initializes the storage. For example, making sure a database is created and migrations are applied.
/// </summary>
Task InitializeAsync(CancellationToken cancellationToken);
}
}
\ No newline at end of file
using System;
using System.Threading.Tasks;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP
{
/// <summary>
/// Represents a connection to the storage.
/// </summary>
public interface IStorageConnection : IDisposable
{
//Sent messages
/// <summary>
/// Returns the message with the given id.
/// </summary>
/// <param name="id">The message's id.</param>
Task<CapPublishedMessage> GetPublishedMessageAsync(int id);
/// <summary>
/// Fetches the next message to be executed.
/// </summary>
Task<IFetchedMessage> FetchNextMessageAsync();
/// <summary>
/// Returns the next message to be enqueued.
/// </summary>
Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync();
// Received messages
/// <summary>
/// Stores the message.
/// </summary>
/// <param name="message">The message to store.</param>
Task StoreReceivedMessageAsync(CapReceivedMessage message);
/// <summary>
/// Returns the message with the given id.
/// </summary>
/// <param name="id">The message's id.</param>
Task<CapReceivedMessage> GetReceivedMessageAsync(int id);
/// <summary>
/// Returns the next message to be enqueued.
/// </summary>
Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync();
//-----------------------------------------
/// <summary>
/// Creates and returns an <see cref="IStorageTransaction"/>.
/// </summary>
IStorageTransaction CreateTransaction();
}
}
\ No newline at end of file
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
...@@ -4,9 +4,9 @@ using Newtonsoft.Json; ...@@ -4,9 +4,9 @@ using Newtonsoft.Json;
namespace DotNetCore.CAP.Infrastructure 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; private static JsonSerializerSettings _serializerSettings;
public static void SetSerializerSettings(JsonSerializerSettings setting) public static void SetSerializerSettings(JsonSerializerSettings setting)
...@@ -40,7 +40,7 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -40,7 +40,7 @@ namespace DotNetCore.CAP.Infrastructure
public static long ToTimestamp(DateTime value) public static long ToTimestamp(DateTime value)
{ {
var elapsedTime = value - Epoch; var elapsedTime = value - Epoch;
return (long) elapsedTime.TotalSeconds; return (long)elapsedTime.TotalSeconds;
} }
public static DateTime FromTimestamp(long value) public static DateTime FromTimestamp(long value)
......
using System; namespace DotNetCore.CAP.Infrastructure
using System.Collections.Generic;
using System.Text;
namespace DotNetCore.CAP.Infrastructure
{ {
/// <summary> /// <summary>
/// The message status name. /// The message status name.
/// </summary> /// </summary>
public struct StatusName public struct StatusName
{ {
public const string Scheduled = nameof(Scheduled);
public const string Enqueued = nameof(Enqueued); public const string Enqueued = nameof(Enqueued);
public const string Processing = nameof(Processing); public const string Processing = nameof(Processing);
public const string Succeeded = nameof(Succeeded); public const string Succeeded = nameof(Succeeded);
......
...@@ -6,8 +6,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -6,8 +6,6 @@ namespace DotNetCore.CAP.Infrastructure
{ {
public static class WaitHandleEx public static class WaitHandleEx
{ {
public static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true);
public static Task WaitAnyAsync(WaitHandle handle1, WaitHandle handle2, TimeSpan timeout) public static Task WaitAnyAsync(WaitHandle handle1, WaitHandle handle2, TimeSpan timeout)
{ {
var t1 = handle1.WaitOneAsync(timeout); var t1 = handle1.WaitOneAsync(timeout);
...@@ -23,7 +21,7 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -23,7 +21,7 @@ namespace DotNetCore.CAP.Infrastructure
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
registeredHandle = ThreadPool.RegisterWaitForSingleObject( registeredHandle = ThreadPool.RegisterWaitForSingleObject(
handle, handle,
(state, timedOut) => ((TaskCompletionSource<bool>) state).TrySetResult(!timedOut), (state, timedOut) => ((TaskCompletionSource<bool>)state).TrySetResult(!timedOut),
tcs, tcs,
timeout, timeout,
true); true);
......
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Xunit;
namespace DotNetCore.CAP.EntityFrameworkCore.Test namespace DotNetCore.CAP.Infrastructure
{ {
public class CapMessageStoreTest public class WebHookProvider
{ {
public WebHookProvider()
{
throw new NotImplementedException();
}
} }
} }
using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions;
namespace DotNetCore.CAP.Infrastructure namespace DotNetCore.CAP.Internal
{ {
public interface IConsumerInvokerFactory public interface IConsumerInvokerFactory
{ {
......
...@@ -44,7 +44,6 @@ namespace DotNetCore.CAP.Internal ...@@ -44,7 +44,6 @@ namespace DotNetCore.CAP.Internal
return executorDescriptorList; return executorDescriptorList;
} }
private static IEnumerable<ConsumerExecutorDescriptor> FindConsumersFromInterfaceTypes( private static IEnumerable<ConsumerExecutorDescriptor> FindConsumersFromInterfaceTypes(
IServiceProvider provider) IServiceProvider provider)
{ {
......
using System; using System;
using System.Linq;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions;
namespace DotNetCore.CAP.Internal namespace DotNetCore.CAP.Internal
......
...@@ -130,7 +130,7 @@ namespace DotNetCore.CAP.Internal ...@@ -130,7 +130,7 @@ namespace DotNetCore.CAP.Internal
private static ConsumerMethodExecutor WrapVoidAction(VoidActionExecutor executor) private static ConsumerMethodExecutor WrapVoidAction(VoidActionExecutor executor)
{ {
return delegate(object target, object[] parameters) return delegate (object target, object[] parameters)
{ {
executor(target, parameters); executor(target, parameters);
return null; return null;
...@@ -192,7 +192,7 @@ namespace DotNetCore.CAP.Internal ...@@ -192,7 +192,7 @@ namespace DotNetCore.CAP.Internal
/// </summary> /// </summary>
private static async Task<object> CastToObject<T>(Task<T> task) private static async Task<object> CastToObject<T>(Task<T> task)
{ {
return (object) await task; return (object)await task;
} }
private static Type GetTaskInnerTypeOrNull(Type type) private static Type GetTaskInnerTypeOrNull(Type type)
...@@ -279,7 +279,7 @@ namespace DotNetCore.CAP.Internal ...@@ -279,7 +279,7 @@ namespace DotNetCore.CAP.Internal
private static Task<object> Convert<T>(object taskAsObject) private static Task<object> Convert<T>(object taskAsObject)
{ {
var task = (Task<T>) taskAsObject; var task = (Task<T>)taskAsObject;
return CastToObject<T>(task); return CastToObject<T>(task);
} }
......
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
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
using System;
namespace DotNetCore.CAP.Job
{
public class Cron
{
/// <summary>
/// Returns cron expression that fires every minute.
/// </summary>
public static string Minutely()
{
return "* * * * *";
}
/// <summary>
/// Returns cron expression that fires every hour at the first minute.
/// </summary>
public static string Hourly()
{
return Hourly(minute: 0);
}
/// <summary>
/// Returns cron expression that fires every hour at the specified minute.
/// </summary>
/// <param name="minute">The minute in which the schedule will be activated (0-59).</param>
public static string Hourly(int minute)
{
return string.Format("{0} * * * *", minute);
}
/// <summary>
/// Returns cron expression that fires every day at 00:00 UTC.
/// </summary>
public static string Daily()
{
return Daily(hour: 0);
}
/// <summary>
/// Returns cron expression that fires every day at the first minute of
/// the specified hour in UTC.
/// </summary>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
public static string Daily(int hour)
{
return Daily(hour, minute: 0);
}
/// <summary>
/// Returns cron expression that fires every day at the specified hour and minute
/// in UTC.
/// </summary>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
/// <param name="minute">The minute in which the schedule will be activated (0-59).</param>
public static string Daily(int hour, int minute)
{
return string.Format("{0} {1} * * *", minute, hour);
}
/// <summary>
/// Returns cron expression that fires every week at Monday, 00:00 UTC.
/// </summary>
public static string Weekly()
{
return Weekly(DayOfWeek.Monday);
}
/// <summary>
/// Returns cron expression that fires every week at 00:00 UTC of the specified
/// day of the week.
/// </summary>
/// <param name="dayOfWeek">The day of week in which the schedule will be activated.</param>
public static string Weekly(DayOfWeek dayOfWeek)
{
return Weekly(dayOfWeek, hour: 0);
}
/// <summary>
/// Returns cron expression that fires every week at the first minute
/// of the specified day of week and hour in UTC.
/// </summary>
/// <param name="dayOfWeek">The day of week in which the schedule will be activated.</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
public static string Weekly(DayOfWeek dayOfWeek, int hour)
{
return Weekly(dayOfWeek, hour, minute: 0);
}
/// <summary>
/// Returns cron expression that fires every week at the specified day
/// of week, hour and minute in UTC.
/// </summary>
/// <param name="dayOfWeek">The day of week in which the schedule will be activated.</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
/// <param name="minute">The minute in which the schedule will be activated (0-59).</param>
public static string Weekly(DayOfWeek dayOfWeek, int hour, int minute)
{
return string.Format("{0} {1} * * {2}", minute, hour, (int) dayOfWeek);
}
/// <summary>
/// Returns cron expression that fires every month at 00:00 UTC of the first
/// day of month.
/// </summary>
public static string Monthly()
{
return Monthly(day: 1);
}
/// <summary>
/// Returns cron expression that fires every month at 00:00 UTC of the specified
/// day of month.
/// </summary>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
public static string Monthly(int day)
{
return Monthly(day, hour: 0);
}
/// <summary>
/// Returns cron expression that fires every month at the first minute of the
/// specified day of month and hour in UTC.
/// </summary>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
public static string Monthly(int day, int hour)
{
return Monthly(day, hour, minute: 0);
}
/// <summary>
/// Returns cron expression that fires every month at the specified day of month,
/// hour and minute in UTC.
/// </summary>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
/// <param name="minute">The minute in which the schedule will be activated (0-59).</param>
public static string Monthly(int day, int hour, int minute)
{
return string.Format("{0} {1} {2} * *", minute, hour, day);
}
/// <summary>
/// Returns cron expression that fires every year on Jan, 1st at 00:00 UTC.
/// </summary>
public static string Yearly()
{
return Yearly(month: 1);
}
/// <summary>
/// Returns cron expression that fires every year in the first day at 00:00 UTC
/// of the specified month.
/// </summary>
/// <param name="month">The month in which the schedule will be activated (1-12).</param>
public static string Yearly(int month)
{
return Yearly(month, day: 1);
}
/// <summary>
/// Returns cron expression that fires every year at 00:00 UTC of the specified
/// month and day of month.
/// </summary>
/// <param name="month">The month in which the schedule will be activated (1-12).</param>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
public static string Yearly(int month, int day)
{
return Yearly(month, day, hour: 0);
}
/// <summary>
/// Returns cron expression that fires every year at the first minute of the
/// specified month, day and hour in UTC.
/// </summary>
/// <param name="month">The month in which the schedule will be activated (1-12).</param>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
public static string Yearly(int month, int day, int hour)
{
return Yearly(month, day, hour, minute: 0);
}
/// <summary>
/// Returns cron expression that fires every year at the specified month, day,
/// hour and minute in UTC.
/// </summary>
/// <param name="month">The month in which the schedule will be activated (1-12).</param>
/// <param name="day">The day of month in which the schedule will be activated (1-31).</param>
/// <param name="hour">The hour in which the schedule will be activated (0-23).</param>
/// <param name="minute">The minute in which the schedule will be activated (0-59).</param>
public static string Yearly(int month, int day, int hour, int minute)
{
return $"{minute} {hour} {day} {month} *";
}
}
}
\ No newline at end of file
using System;
namespace DotNetCore.CAP.Job
{
/// <summary>
/// Represents a cron job to be executed at specified intervals of time.
/// </summary>
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
using DotNetCore.CAP.Infrastructure;
using Microsoft.Extensions.Options;
namespace DotNetCore.CAP.Job
{
public class DefaultCronJobRegistry : CronJobRegistry
{
public DefaultCronJobRegistry(IOptions<CapOptions> options)
{
var options1 = options.Value;
RegisterJob<CapJob>(nameof(DefaultCronJobRegistry), options1.CronExp, RetryBehavior.DefaultRetry);
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Reflection;
using NCrontab;
namespace DotNetCore.CAP.Job
{
public abstract class CronJobRegistry
{
private readonly List<Entry> _entries;
protected CronJobRegistry()
{
_entries = new List<Entry>();
}
protected void RegisterJob<T>(string name, string cron, RetryBehavior retryBehavior = null)
where T : IJob
{
RegisterJob(name, typeof(T), cron, retryBehavior);
}
/// <summary>
/// Registers a cron job.
/// </summary>
/// <param name="name">The name of the job.</param>
/// <param name="jobType">The job's type.</param>
/// <param name="cron">The cron expression to use.</param>
/// <param name="retryBehavior">The <see cref="RetryBehavior"/> to use.</param>
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
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<CapJob> _logger;
private readonly ICapMessageStore _messageStore;
public CapJob(
ILogger<CapJob> 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<ICapMessageStore>();
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
using System.Threading.Tasks;
namespace DotNetCore.CAP.Job
{
public interface IJob
{
/// <summary>
/// Executes the job.
/// </summary>
Task ExecuteAsync();
}
}
\ No newline at end of file
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<CronJobProcessor> 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<IJob>();
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<CronJob>();
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<CronJob> 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
using System; using System;
using System.Collections.Generic;
using System.Linq;
using DotNetCore.CAP.Job;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP namespace DotNetCore.CAP
...@@ -13,16 +10,17 @@ namespace DotNetCore.CAP ...@@ -13,16 +10,17 @@ namespace DotNetCore.CAP
private static readonly Action<ILogger, Exception> _serverShuttingDown; private static readonly Action<ILogger, Exception> _serverShuttingDown;
private static readonly Action<ILogger, string, Exception> _expectedOperationCanceledException; private static readonly Action<ILogger, string, Exception> _expectedOperationCanceledException;
private static readonly Action<ILogger, Exception> _cronJobsNotFound;
private static readonly Action<ILogger, int, Exception> _cronJobsScheduling;
private static readonly Action<ILogger, string, double, Exception> _cronJobExecuted;
private static readonly Action<ILogger, string, Exception> _cronJobFailed;
private static readonly Action<ILogger, string, string, Exception> _enqueuingSentMessage; private static readonly Action<ILogger, string, string, Exception> _enqueuingSentMessage;
private static readonly Action<ILogger, string, string, Exception> _enqueuingReceivdeMessage; private static readonly Action<ILogger, string, string, Exception> _enqueuingReceivdeMessage;
private static readonly Action<ILogger, string, Exception> _executingConsumerMethod; private static readonly Action<ILogger, string, Exception> _executingConsumerMethod;
private static readonly Action<ILogger, string, Exception> _receivedMessageRetryExecuting; private static readonly Action<ILogger, string, Exception> _receivedMessageRetryExecuting;
private static Action<ILogger, Exception> _jobFailed;
private static Action<ILogger, Exception> _jobFailedWillRetry;
private static Action<ILogger, double, Exception> _jobExecuted;
private static Action<ILogger, int, Exception> _jobRetrying;
private static Action<ILogger, string, Exception> _exceptionOccuredWhileExecutingJob;
static LoggerExtensions() static LoggerExtensions()
{ {
_serverStarting = LoggerMessage.Define<int, int>( _serverStarting = LoggerMessage.Define<int, int>(
...@@ -45,26 +43,6 @@ namespace DotNetCore.CAP ...@@ -45,26 +43,6 @@ namespace DotNetCore.CAP
3, 3,
"Expected an OperationCanceledException, but found '{ExceptionMessage}'."); "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<int>(
LogLevel.Debug,
2,
"Found {JobCount} cron job(s) to schedule.");
_cronJobExecuted = LoggerMessage.Define<string, double>(
LogLevel.Debug,
3,
"Cron job '{JobName}' executed succesfully. Took: {Seconds} secs.");
_cronJobFailed = LoggerMessage.Define<string>(
LogLevel.Warning,
4,
"Cron job '{jobName}' failed to execute.");
_enqueuingSentMessage = LoggerMessage.Define<string, string>( _enqueuingSentMessage = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
2, 2,
...@@ -84,6 +62,52 @@ namespace DotNetCore.CAP ...@@ -84,6 +62,52 @@ namespace DotNetCore.CAP
LogLevel.Error, LogLevel.Error,
5, 5,
"Received message topic method '{topicName}' failed to execute."); "Received message topic method '{topicName}' failed to execute.");
_jobRetrying = LoggerMessage.Define<int>(
LogLevel.Debug,
3,
"Retrying a job: {Retries}...");
_jobExecuted = LoggerMessage.Define<double>(
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<string>(
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) public static void ConsumerMethodExecutingFailed(this ILogger logger, string methodName, Exception ex)
...@@ -126,24 +150,9 @@ namespace DotNetCore.CAP ...@@ -126,24 +150,9 @@ namespace DotNetCore.CAP
_expectedOperationCanceledException(logger, ex.Message, ex); _expectedOperationCanceledException(logger, ex.Message, ex);
} }
public static void CronJobsNotFound(this ILogger logger) public static void ExceptionOccuredWhileExecutingJob(this ILogger logger, string jobId, Exception ex)
{
_cronJobsNotFound(logger, null);
}
public static void CronJobsScheduling(this ILogger logger, IEnumerable<CronJob> 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)
{ {
_cronJobFailed(logger, name, ex); _exceptionOccuredWhileExecutingJob(logger, jobId, ex);
} }
} }
} }
\ No newline at end of file
namespace DotNetCore.CAP.Infrastructure namespace DotNetCore.CAP
{ {
public class MessageContext public class MessageContext
{ {
public string Group { get; set; } public string Group { get; set; }
public string KeyName { get; set; } public string Name { get; set; }
public string Content { get; set; } public string Content { get; set; }
} }
......
using System; using System;
using DotNetCore.CAP.Infrastructure;
namespace DotNetCore.CAP.Infrastructure namespace DotNetCore.CAP.Models
{ {
public class CapSentMessage public class CapPublishedMessage
{ {
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="CapSentMessage"/>. /// Initializes a new instance of <see cref="CapPublishedMessage"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The Id property is initialized to from a new GUID string value. /// The Id property is initialized to from a new GUID string value.
/// </remarks> /// </remarks>
public CapSentMessage() public CapPublishedMessage()
{ {
Id = Guid.NewGuid().ToString();
Added = DateTime.Now; Added = DateTime.Now;
} }
public CapSentMessage(MessageContext message) public CapPublishedMessage(MessageContext message)
{ {
KeyName = message.KeyName; Name = message.Name;
Content = message.Content; 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 string Content { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public DateTime LastRun { get; set; } public DateTime? ExpiresAt { get; set; }
public int Retries { get; set; } public int Retries { get; set; }
......
namespace DotNetCore.CAP.Models
{
public class CapQueue
{
public int MessageId { get; set; }
/// <summary>
/// 0 is CapSentMessage, 1 is CapReceviedMessage
/// </summary>
public MessageType MessageType { get; set; }
}
}
\ No newline at end of file
using System; using System;
using DotNetCore.CAP.Infrastructure;
namespace DotNetCore.CAP.Infrastructure namespace DotNetCore.CAP.Models
{ {
public class CapReceivedMessage public class CapReceivedMessage
{ {
...@@ -12,28 +13,27 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -12,28 +13,27 @@ namespace DotNetCore.CAP.Infrastructure
/// </remarks> /// </remarks>
public CapReceivedMessage() public CapReceivedMessage()
{ {
Id = Guid.NewGuid().ToString();
Added = DateTime.Now; Added = DateTime.Now;
} }
public CapReceivedMessage(MessageContext message) : this() public CapReceivedMessage(MessageContext message) : this()
{ {
Group = message.Group; Group = message.Group;
KeyName = message.KeyName; Name = message.Name;
Content = message.Content; Content = message.Content;
} }
public string Id { get; set; } public int Id { get; set; }
public string Group { get; set; } public string Group { get; set; }
public string KeyName { get; set; } public string Name { get; set; }
public string Content { get; set; } public string Content { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public DateTime LastRun { get; set; } public DateTime? ExpiresAt { get; set; }
public int Retries { get; set; } public int Retries { get; set; }
...@@ -44,7 +44,7 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -44,7 +44,7 @@ namespace DotNetCore.CAP.Infrastructure
return new MessageContext return new MessageContext
{ {
Group = Group, Group = Group,
KeyName = KeyName, Name = Name,
Content = Content Content = Content
}; };
} }
......
namespace DotNetCore.CAP.Models
{
public enum MessageType
{
Publish,
Subscribe
}
}
\ No newline at end of file
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace DotNetCore.CAP namespace DotNetCore.CAP
...@@ -18,6 +19,8 @@ namespace DotNetCore.CAP ...@@ -18,6 +19,8 @@ namespace DotNetCore.CAP
/// </summary> /// </summary>
public bool Succeeded { get; set; } public bool Succeeded { get; set; }
public Exception Exception { get; set; }
/// <summary> /// <summary>
/// An <see cref="IEnumerable{T}"/> of <see cref="OperateError"/>s containing an errors /// An <see cref="IEnumerable{T}"/> of <see cref="OperateError"/>s containing an errors
/// that occurred during the operation. /// that occurred during the operation.
...@@ -29,7 +32,7 @@ namespace DotNetCore.CAP ...@@ -29,7 +32,7 @@ namespace DotNetCore.CAP
/// Returns an <see cref="OperateResult"/> indicating a successful identity operation. /// Returns an <see cref="OperateResult"/> indicating a successful identity operation.
/// </summary> /// </summary>
/// <returns>An <see cref="OperateResult"/> indicating a successful operation.</returns> /// <returns>An <see cref="OperateResult"/> indicating a successful operation.</returns>
public static OperateResult Success { get; } = new OperateResult {Succeeded = true}; public static OperateResult Success { get; } = new OperateResult { Succeeded = true };
/// <summary> /// <summary>
/// Creates an <see cref="OperateResult"/> indicating a failed operation, with a list of <paramref name="errors"/> if applicable. /// Creates an <see cref="OperateResult"/> indicating a failed operation, with a list of <paramref name="errors"/> if applicable.
...@@ -38,7 +41,18 @@ namespace DotNetCore.CAP ...@@ -38,7 +41,18 @@ namespace DotNetCore.CAP
/// <returns>An <see cref="OperateResult"/> indicating a failed operation, with a list of <paramref name="errors"/> if applicable.</returns> /// <returns>An <see cref="OperateResult"/> indicating a failed operation, with a list of <paramref name="errors"/> if applicable.</returns>
public static OperateResult Failed(params OperateError[] errors) 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) if (errors != null)
{ {
result._errors.AddRange(errors); result._errors.AddRange(errors);
......
namespace DotNetCore.CAP.Processor
{
public interface IAdditionalProcessor : IProcessor
{
}
}
\ No newline at end of file
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Confluent.Kafka;
using Confluent.Kafka.Serialization;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Job;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace DotNetCore.CAP.Kafka namespace DotNetCore.CAP.Processor
{ {
public class KafkaJobProcessor : IJobProcessor public class DefaultDispatcher : IDispatcher
{ {
private readonly KafkaOptions _kafkaOptions; private readonly IQueueExecutorFactory _queueExecutorFactory;
private readonly CancellationTokenSource _cts;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly CancellationTokenSource _cts;
private readonly TimeSpan _pollingDelay; private readonly TimeSpan _pollingDelay;
public KafkaJobProcessor( internal static readonly AutoResetEvent PulseEvent = new AutoResetEvent(true);
public DefaultDispatcher(
IServiceProvider provider,
IQueueExecutorFactory queueExecutorFactory,
IOptions<CapOptions> capOptions, IOptions<CapOptions> capOptions,
IOptions<KafkaOptions> kafkaOptions, ILogger<DefaultDispatcher> logger)
ILogger<KafkaJobProcessor> logger,
IServiceProvider provider)
{ {
_logger = logger; _logger = logger;
_kafkaOptions = kafkaOptions.Value; _queueExecutorFactory = queueExecutorFactory;
_provider = provider; _provider = provider;
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
_pollingDelay = TimeSpan.FromSeconds(capOptions.Value.PollingDelay); _pollingDelay = TimeSpan.FromSeconds(capOptions.Value.PollingDelay);
...@@ -41,9 +36,11 @@ namespace DotNetCore.CAP.Kafka ...@@ -41,9 +36,11 @@ namespace DotNetCore.CAP.Kafka
public Task ProcessAsync(ProcessingContext context) public Task ProcessAsync(ProcessingContext context)
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null)
throw new ArgumentNullException(nameof(context));
context.ThrowIfStopping(); context.ThrowIfStopping();
return ProcessCoreAsync(context); return ProcessCoreAsync(context);
} }
...@@ -60,10 +57,8 @@ namespace DotNetCore.CAP.Kafka ...@@ -60,10 +57,8 @@ namespace DotNetCore.CAP.Kafka
if (!worked) if (!worked)
{ {
var token = GetTokenToWaitOn(context); var token = GetTokenToWaitOn(context);
await WaitHandleEx.WaitAnyAsync(PulseEvent, token.WaitHandle, _pollingDelay);
} }
await WaitHandleEx.WaitAnyAsync(WaitHandleEx.PulseEvent,
context.CancellationToken.WaitHandle, _pollingDelay);
} }
finally finally
{ {
...@@ -78,44 +73,22 @@ namespace DotNetCore.CAP.Kafka ...@@ -78,44 +73,22 @@ namespace DotNetCore.CAP.Kafka
private async Task<bool> Step(ProcessingContext context) private async Task<bool> Step(ProcessingContext context)
{ {
var fetched = default(IFetchedMessage);
using (var scopedContext = context.CreateScope()) using (var scopedContext = context.CreateScope())
{ {
var provider = scopedContext.Provider; var provider = scopedContext.Provider;
var messageStore = provider.GetRequiredService<ICapMessageStore>(); var connection = provider.GetRequiredService<IStorageConnection>();
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); if ((fetched = await connection.FetchNextMessageAsync()) != null)
sp.Stop();
message.StatusName = StatusName.Succeeded;
await messageStore.UpdateSentMessageAsync(message);
_logger.JobExecuted(sp.Elapsed.TotalSeconds);
}
catch (Exception ex)
{ {
_logger.ExceptionOccuredWhileExecutingJob(message.KeyName, ex); using (fetched)
return false; {
} var queueExecutor = _queueExecutorFactory.GetInstance(fetched.MessageType);
await queueExecutor.ExecuteAsync(connection, fetched);
} }
return true;
} }
private Task ExecuteJobAsync(string topic, string content)
{
var config = _kafkaOptions.AsRdkafkaConfig();
using (var producer = new Producer<Null, string>(config, null, new StringSerializer(Encoding.UTF8)))
{
producer.ProduceAsync(topic, null, content);
producer.Flush();
} }
return Task.CompletedTask; return fetched != null;
} }
} }
} }
\ No newline at end of file
namespace DotNetCore.CAP.Processor
{
public interface IDispatcher : IProcessor
{
bool Waiting { get; }
}
}
\ No newline at end of file
...@@ -3,60 +3,67 @@ using System.Collections.Generic; ...@@ -3,60 +3,67 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; 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 ILogger _logger;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
private readonly CancellationTokenSource _cts; private readonly CancellationTokenSource _cts;
private readonly CapOptions _options; private readonly CapOptions _options;
private readonly DefaultCronJobRegistry _defaultJobRegistry;
private IJobProcessor[] _processors; private IProcessor[] _processors;
private IList<IDispatcher> _messageDispatchers;
private ProcessingContext _context; private ProcessingContext _context;
private Task _compositeTask; private Task _compositeTask;
private bool _disposed; private bool _disposed;
public JobProcessingServer( public CapProcessingServer(
ILogger<JobProcessingServer> logger, ILogger<CapProcessingServer> logger,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServiceProvider provider, IServiceProvider provider,
DefaultCronJobRegistry defaultJobRegistry,
IOptions<CapOptions> options) IOptions<CapOptions> options)
{ {
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_provider = provider; _provider = provider;
_defaultJobRegistry = defaultJobRegistry;
_options = options.Value; _options = options.Value;
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
_messageDispatchers = new List<IDispatcher>();
} }
public void Start() public void Start()
{ {
var processorCount = Environment.ProcessorCount; var processorCount = Environment.ProcessorCount;
//processorCount = 1;
_processors = GetProcessors(processorCount); _processors = GetProcessors(processorCount);
_logger.ServerStarting(processorCount, processorCount); _logger.ServerStarting(processorCount, _processors.Length);
_context = new ProcessingContext( _context = new ProcessingContext(_provider, _cts.Token);
_provider,
_defaultJobRegistry,
_cts.Token);
var processorTasks = _processors var processorTasks = _processors
.Select(InfiniteRetry) .Select(p => InfiniteRetry(p))
.Select(p => p.ProcessAsync(_context)); .Select(p => p.ProcessAsync(_context));
_compositeTask = Task.WhenAll(processorTasks); _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() public void Dispose()
{ {
if (_disposed) if (_disposed)
...@@ -69,7 +76,7 @@ namespace DotNetCore.CAP.Job ...@@ -69,7 +76,7 @@ namespace DotNetCore.CAP.Job
_cts.Cancel(); _cts.Cancel();
try try
{ {
_compositeTask.Wait((int) TimeSpan.FromSeconds(60).TotalMilliseconds); _compositeTask.Wait((int)TimeSpan.FromSeconds(60).TotalMilliseconds);
} }
catch (AggregateException ex) catch (AggregateException ex)
{ {
...@@ -81,30 +88,37 @@ namespace DotNetCore.CAP.Job ...@@ -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); return new InfiniteRetryProcessor(inner, _loggerFactory);
} }
private IJobProcessor[] GetProcessors(int processorCount) private IProcessor[] GetProcessors(int processorCount)
{ {
var returnedProcessors = new List<IJobProcessor>(); var returnedProcessors = new List<IProcessor>();
for (int i = 0; i < processorCount; i++) for (int i = 0; i < processorCount; i++)
{ {
var processors = _provider.GetServices<IJobProcessor>(); var messageProcessors = _provider.GetRequiredService<IDispatcher>();
foreach (var processor in processors) _messageDispatchers.Add(messageProcessors);
{
if (processor is CronJobProcessor)
{
if (i == 0) // only add first cronJob
returnedProcessors.Add(processor);
}
else
{
returnedProcessors.Add(processor);
}
}
} }
returnedProcessors.AddRange(_messageDispatchers);
returnedProcessors.Add(_provider.GetRequiredService<PublishQueuer>());
returnedProcessors.Add(_provider.GetRequiredService<SubscribeQueuer>());
returnedProcessors.Add(_provider.GetRequiredService<IAdditionalProcessor>());
return returnedProcessors.ToArray(); return returnedProcessors.ToArray();
} }
......
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; 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; private readonly ILogger _logger;
public InfiniteRetryProcessor( public InfiniteRetryProcessor(
IJobProcessor inner, IProcessor inner,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_inner = inner; _inner = inner;
...@@ -33,10 +33,7 @@ namespace DotNetCore.CAP.Job ...@@ -33,10 +33,7 @@ namespace DotNetCore.CAP.Job
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning( _logger.LogWarning(1, ex, "Prcessor '{ProcessorName}' failed. Retrying...", _inner.ToString());
1,
ex,
"Prcessor '{ProcessorName}' failed. Retrying...", _inner.ToString());
} }
} }
} }
......
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<PublishQueuer> logger,
IOptions<CapOptions> 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<IStorageConnection>();
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
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<SubscribeQueuer> logger,
IOptions<CapOptions> 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<IStorageConnection>();
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
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotNetCore.CAP.Job namespace DotNetCore.CAP.Processor
{ {
public interface IJobProcessor public interface IProcessor
{ {
Task ProcessAsync(ProcessingContext context); Task ProcessAsync(ProcessingContext context);
} }
......
...@@ -3,7 +3,7 @@ using System.Threading; ...@@ -3,7 +3,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP.Job namespace DotNetCore.CAP.Processor
{ {
public class ProcessingContext : IDisposable public class ProcessingContext : IDisposable
{ {
...@@ -16,24 +16,19 @@ namespace DotNetCore.CAP.Job ...@@ -16,24 +16,19 @@ namespace DotNetCore.CAP.Job
private ProcessingContext(ProcessingContext other) private ProcessingContext(ProcessingContext other)
{ {
Provider = other.Provider; Provider = other.Provider;
CronJobRegistry = other.CronJobRegistry;
CancellationToken = other.CancellationToken; CancellationToken = other.CancellationToken;
} }
public ProcessingContext( public ProcessingContext(
IServiceProvider provider, IServiceProvider provider,
CronJobRegistry cronJobRegistry,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Provider = provider; Provider = provider;
CronJobRegistry = cronJobRegistry;
CancellationToken = cancellationToken; CancellationToken = cancellationToken;
} }
public IServiceProvider Provider { get; private set; } public IServiceProvider Provider { get; private set; }
public CronJobRegistry CronJobRegistry { get; private set; }
public CancellationToken CancellationToken { get; } public CancellationToken CancellationToken { get; }
public bool IsStopping => CancellationToken.IsCancellationRequested; public bool IsStopping => CancellationToken.IsCancellationRequested;
......
using System; using System;
namespace DotNetCore.CAP.Job namespace DotNetCore.CAP.Processor
{ {
public class RetryBehavior public class RetryBehavior
{ {
...@@ -18,7 +18,7 @@ namespace DotNetCore.CAP.Job ...@@ -18,7 +18,7 @@ namespace DotNetCore.CAP.Job
{ {
DefaultRetryCount = 25; DefaultRetryCount = 25;
DefaultRetryInThunk = retries => 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); DefaultRetry = new RetryBehavior(true);
NoRetry = new RetryBehavior(false); NoRetry = new RetryBehavior(false);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment