Commit 544325a7 authored by Savorboard's avatar Savorboard Committed by GitHub

Release 2.1 (#55)

* add dashboard branch.

* add dashboard

* add helper methods.

* Add data model.

* add options.

* add empty implement

* add dashbaord

* add dashboard

* Fixed spelling error

* rename file.

* add dashbaord feature

* impl dashboard storage

* update nuget reference.

* add pages.

* deleted unused fiels.

* modify resource.

* update resource.

* rename

* update dashboard

* rename

* impl monitoring api interface.

* update solution files.

* update samples.

* dashboard.

* add jsonview resource files.

* add json dispatcher.

* modify published pages.

* add routes.

* update pages.

* update resources.

* update dashboard.

* add dashboard of sql server storage impl

* nesting files.

* fixed query bug.

* update resource.

* remove some api.

* add subscriber page.

* update sample

* add resource.

* remove api.

* add SubscriberPage

* add resource

* generate cshtml.

* modify html two table to one table.

* update resource.

* update css

* update subscriber page.

* refactor.

* cleanup.

* cleanup code.

* impl history monitoring api.

* add home page recevied message real-time

* add legend styles.

* update js.

* modify axis color.

* add resource.

* update dashboard home page.

* update css.

* update resource.

* modify DefaultSucceedMessageExpirationAfter to 24 hours.

* add resx,

* add consul discovery.

* remove unused file.

* add node page.

* add node page

* node discovery

* add kafka sqlserver sample.

* update sample.

* add okstats.

* refactor.

* fixed kafka client bugs.

* modify node and subscriber pages

* refactor.

* add Gateway middleware

* remove unused files.

* update resource.

* update gateway.

* refactor.

* update samples.

* remove base middleware

* add node switch click event.

* add NodeId config to options.

* upgrade dependent version.

* add PathMatch configuration

* update NodePage.cshtml

* remove session

* remove matchPath

* refactor

* refactor dashboard middleware

* gateway proxy middleware function maturation

* remove cookie exp

* refactor and remove files.

* add CapCache to cache server nodes.

* refactor.

* renamed message dto.

* add extended interface of IContentSerializer and JsonContentSerializer

* modify unit test

* check the requirement when CAP start.

* correct spelling

* cleanup code.

* add resources.

* processing pages will contains  Scheduled  and Enqueued messages.

* processing pages will contains  Scheduled  and Enqueued messages.

* ignore NU1701 Warning.

* renamed file.

* refactor

* implements dashboard interface.

* rename reference class.

* add mysql monitoring api impl

* fix bug of connection driver.

* remove cap.UseDashboard.  It's will be automatically enabled by registerd services.

* fix sql bug

* cleanup code and fix spelling

* fix postgre sql bug.

* fix mysql sql bug.

* when storage a received message raising an eception, we will reject the message to queue.

* fix spelling mistake

* add dashboard instructions to readme

* modify error log content.

* fix postger sql bug.

* fix consul discovery bug.

* add dashboard introduction to readme.md

* update english resource.

* Update README.md

* renamed files.

* fix postgre sql bug.

* cleanup code.

* update tests.

* rename file.

* update samples.

* Improved query performance without lock table. (#36)

* update sample.

* update samples.

* fix data reader uncolsed bug.

* update add jsonproperty

* refactor

* revert FetchNextMessageAsync sql

* add helper method.

* rafactor subscriber handler.

* add FailedRetryCount options.

* rafactor publisher excutor.

* add IPublishExecutor

* add failed message processor.

* inject failed message processor.

* refactor sql storage.

* fixed unit tests.

* fixed unit test.

* fixed postgresql tests.

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* Update .travis.yml

* add LAN ip to LocalRequestsOnlyAuthorizationFilter

* add and update resource.

* add current node name to layout page if user enabled node discovery

* modify unit tests.

* add callback message sende tests.

* add deserlizer by type to  IContentSerializer

* refactor unit tests

* add summary comment.

* refactor async method.

* add comment and fixed spell error.

* refactor.

* add custom content serializer extension to CapBuilder

* add IMessagePacker

* refactor.

* add connection pool for kafka producer.

* add determines whether the query is null.

* add connection pool size config to KafkaOption.

* fixed json JObject bug

* add custom message wapper interface

* remove unused code.

* fixed callback topic send error bug.

* refactor.

* update unit tests.

* update samples.

* upgrade dependent package.

* remove some class from Abstraction namespce to Internal.

* optimize consumer related code

* add ICallbackMessageSender to DI with singleton.

* add and fixed some unit tests.

* refactor namespace.

* modify class protected level

* assemblies internal class are visible to test project

* add DeSerialize method to IContentSerializer with type deseralize

* refactoring

* Fix the phone style dispaly problem

* add logs

* refactoring

* upgrading `Confluent.Kafka` package

* Optimize message queue error message prompt.

* reorganize error message prompts.

* modify error message prompt

* add summary comments.

* modify dependent

* disabled print connection closed log. see: https://github.com/edenhill/librdkafka/issues/516

* Update README.md

* Update README.md

* Update README.zh-cn.md

* Update README.zh-cn.md

* fix dashboard not config discovery throw exceptions bug.

* Update README.md

* update readme

* Fixed serialized the message type bug. (#53)

* Update README.md

* Update README.md

* refactoring

* refactoring

* update readme.

* update readme.

* add summary comment.

* refactoring

* optimizing publisher interface

* update readme

* update readme.

* add summary comment.

* add summary comment.

* add summary comment.

* add summary comment.

* upgrading package

* add summary comment.

* optimize the RabbitMQ connection pool

* fix the producer connection returned

* dispose resource when connection pool is full
parent 8744532f
language: csharp
language: cpp
sudo: required
mono: none
dist: trusty
matrix:
include:
- os: linux
dist: trusty # Ubuntu 14.04
dotnet: 2.0.0
mono: none
env: DOTNETCORE=1
sudo: required
- os: osx
osx_image: xcode8.3 # macOS 10.12
dotnet: 2.0.0
mono: none
env: DOTNETCORE=1
env:
global:
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
- CLI_VERSION=2.0.0
addons:
apt:
packages:
- gettext
- libcurl4-openssl-dev
- libicu-dev
- libssl-dev
- libunwind8
- zlib1g
# Make sure build dependencies are installed.
before_install:
- chmod a+x ./build.sh
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then ulimit -n 1024 ; fi
- if test "$TRAVIS_OS_NAME" == "osx"; then ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi
- export DOTNET_INSTALL_DIR="$PWD/.dotnetcli"
- export PATH="$DOTNET_INSTALL_DIR:$PATH"
install:
- export DOTNET_CLI_TELEMETRY_OPTOUT=1
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then rvm get stable; brew update; brew install openssl; fi
- travis_retry curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 2.0 --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR"
# Run the build script
script:
- ./build.sh
\ No newline at end of file
- dotnet --info
- dotnet restore
- dotnet test test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj -f netcoreapp2.0

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
VisualStudioVersion = 15.0.26730.15
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B2AE124-6636-4DE9-83A3-70360DABD0C4}"
EndProject
......@@ -60,7 +60,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.PostgreSql", "samples\Sample.RabbitMQ.PostgreSql\Sample.RabbitMQ.PostgreSql.csproj", "{A17E8E72-DFFC-4822-BB38-73D59A8B264E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCore.CAP.PostgreSql.Test", "test\DotNetCore.CAP.PostgreSql.Test\DotNetCore.CAP.PostgreSql.Test.csproj", "{7CA3625D-1817-4695-881D-7E79A1E1DED2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql.Test", "test\DotNetCore.CAP.PostgreSql.Test\DotNetCore.CAP.PostgreSql.Test.csproj", "{7CA3625D-1817-4695-881D-7E79A1E1DED2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka.SqlServer", "samples\Sample.Kafka.SqlServer\Sample.Kafka.SqlServer.csproj", "{573B4D39-5489-48B3-9B6C-5234249CB980}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -119,6 +121,10 @@ Global
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Release|Any CPU.Build.0 = Release|Any CPU
{573B4D39-5489-48B3-9B6C-5234249CB980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{573B4D39-5489-48B3-9B6C-5234249CB980}.Debug|Any CPU.Build.0 = Debug|Any CPU
{573B4D39-5489-48B3-9B6C-5234249CB980}.Release|Any CPU.ActiveCfg = Release|Any CPU
{573B4D39-5489-48B3-9B6C-5234249CB980}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -137,6 +143,7 @@ Global
{82C403AB-ED68-4084-9A1D-11334F9F08F9} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{A17E8E72-DFFC-4822-BB38-73D59A8B264E} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{7CA3625D-1817-4695-881D-7E79A1E1DED2} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0}
{573B4D39-5489-48B3-9B6C-5234249CB980} = {3A6B6931-A123-477A-9469-8B468B5385AF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB}
......
......@@ -6,15 +6,13 @@
[![Member project of .NET China Foundation](https://img.shields.io/badge/member_project_of-.NET_CHINA-red.svg?style=flat&colorB=9E20C8)](https://github.com/dotnetcore)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dotnetcore/CAP/master/LICENSE.txt)
CAP is a .Net Standard library to achieve eventually consistent in distributed architectures system like SOA,MicroService. It is lightweight,easy to use and efficiently.
CAP is a library based on .Net standard, which is a solution to deal with distributed transactions, also has the function of EventBus, it is lightweight, easy to use, and efficiently.
## OverView
CAP is a library that used in an ASP.NET Core project, Of Course you can ues it in ASP.NET Core with .NET Framework.
In the process of building an SOA or MicroService system, we usually need to use the event to integrate each services. In the process, the simple use of message queue does not guarantee the reliability. CAP is adopted the local message table program integrated with the current database to solve the exception may occur in the process of the distributed system calling each other. It can ensure that the event messages are not lost in any case.
You can think of CAP as an EventBus because it has all the features of EventBus, and CAP provides a easier way to handle the publishing and subscribing than EventBus.
CAP has the function of Message Persistence, and it makes messages reliability when your service is restarted or down. CAP provides a Publish Service based on Microsoft DI that integrates seamlessly with your business services and supports strong consistency transactions.
You can also use the CAP as an EventBus. The CAP provides a simpler way to implement event publishing and subscriptions. You do not need to inherit or implement any interface during the process of subscription and sending.
This is a diagram of the CAP working in the ASP.NET Core MicroService architecture:
......@@ -32,22 +30,24 @@ You can run the following command to install the CAP in your project.
PM> Install-Package DotNetCore.CAP
```
If your Message Queue is using Kafka, you can:
If you want use Kafka to send integrating event, installing by:
```
PM> Install-Package DotNetCore.CAP.Kafka
```
If your Message Queue is using RabbitMQ, you can:
If you want use RabbitMQ to send integrating event, installing by:
```
PM> Install-Package DotNetCore.CAP.RabbitMQ
```
CAP supported SqlServer, MySql, PostgreSql as message store extension:
CAP supports SqlServer, MySql, PostgreSql as event log storage.
```
//Select a database provider you are using
// select a database provider you are using, event log table will integrate into.
PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql
......@@ -60,32 +60,32 @@ First,You need to config CAP in your Startup.cs:
```cs
public void ConfigureServices(IServiceCollection services)
{
......
//......
services.AddDbContext<AppDbContext>();
services.AddCap(x =>
{
// If your SqlServer is using EF for data operations, you need to add the following configuration:
// Notice: You don't need to config x.UseSqlServer(""") again!
// If you are using EF, you need to add the following configuration:
// Notice: You don't need to config x.UseSqlServer(""") again! CAP can autodiscovery.
x.UseEntityFramework<AppDbContext>();
// If you are using Dapper,you need to add the config
// If you are using ado.net,you need to add the configuration
x.UseSqlServer("Your ConnectionStrings");
//x.UseMySql("Your ConnectionStrings");
//x.UsePostgreSql("Your ConnectionStrings");
x.UseMySql("Your ConnectionStrings");
x.UsePostgreSql("Your ConnectionStrings");
// If your Message Queue is using RabbitMQ you need to add the config
// If you are using RabbitMQ, you need to add the configuration
x.UseRabbitMQ("localhost");
// If your Message Queue is using Kafka you need to add the config
// If you are using Kafka, you need to add the configuration
x.UseKafka("localhost");
});
}
public void Configure(IApplicationBuilder app)
{
.....
//.....
app.UseCap();
}
......@@ -96,34 +96,40 @@ public void Configure(IApplicationBuilder app)
Inject `ICapPublisher` in your Controller, then use the `ICapPublisher` to send message
```cs
```c#
public class PublishController : Controller
{
private readonly ICapPublisher _publisher;
public PublishController(ICapPublisher publisher)
[Route("~/publishWithTransactionUsingEF")]
public async Task<IActionResult> PublishMessageWithTransactionUsingEF([FromServices]AppDbContext dbContext, [FromServices]ICapPublisher publisher)
{
_publisher = publisher;
}
[Route("~/checkAccount")]
public async Task<IActionResult> PublishMessage()
using (var trans = dbContext.Database.BeginTransaction())
{
// Specifies the message header and content to be sent
await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
// your business code
//If you are using EF, CAP will automatic discovery current environment transaction, so you do not need to explicit pass parameters.
//Achieving atomicity between original database operation and the publish event log thanks to a local transaction.
await publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
trans.Commit();
}
return Ok();
}
[Route("~/checkAccountWithTrans")]
public async Task<IActionResult> PublishMessageWithTransaction([FromServices]AppDbContext dbContext)
[Route("~/publishWithTransactionUsingAdonet")]
public async Task<IActionResult> PublishMessageWithTransactionUsingAdonet([FromServices]ICapPublisher publisher)
{
using (var trans = dbContext.Database.BeginTransaction())
var connectionString = "";
using (var sqlConnection = new SqlConnection(connectionString))
{
sqlConnection.Open();
using (var sqlTransaction = sqlConnection.BeginTransaction())
{
await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
// your business code
trans.Commit();
publisher.Publish("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }, sqlTransaction);
sqlTransaction.Commit();
}
}
return Ok();
}
......@@ -137,18 +143,9 @@ public class PublishController : Controller
Add the Attribute `[CapSubscribe()]` on Action to subscribe message:
```cs
```c#
public class PublishController : Controller
{
private readonly ICapPublisher _publisher;
public PublishController(ICapPublisher publisher)
{
_publisher = publisher;
}
[NoAction]
[CapSubscribe("xxx.services.account.check")]
public async Task CheckReceivedMessage(Person person)
{
......@@ -164,7 +161,7 @@ public class PublishController : Controller
If your subscribe method is not in the Controller,then your subscribe class need to Inheritance `ICapSubscribe`:
```cs
```c#
namespace xxx.Service
{
......@@ -179,7 +176,6 @@ namespace xxx.Service
[CapSubscribe("xxx.services.account.check")]
public void CheckReceivedMessage(Person person)
{
}
}
}
......@@ -188,13 +184,49 @@ namespace xxx.Service
Then inject your `ISubscriberService` class in Startup.cs
```cs
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISubscriberService,SubscriberService>();
}
```
### Dashboard
CAP 2.1 and above provides the dashboard pages, you can easily view the sent and received messages. In addition, you can also view the message status in real time on the dashboard.
In the distributed environment, the dashboard built-in integrated [Consul](http://consul.io) as a node discovery, while the realization of the gateway agent function, you can also easily view the node or other node data, It's like you are visiting local resources.
```c#
services.AddCap(x =>
{
//...
// Register Dashboard
x.UseDashboard();
// Register to Consul
x.UseDiscovery(d =>
{
d.DiscoveryServerHostName = "localhost";
d.DiscoveryServerPort = 8500;
d.CurrentNodeHostName = "localhost";
d.CurrentNodePort = 5800;
d.NodeId = 1;
d.NodeName = "CAP No.1 Node";
});
});
```
![dashboard](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220827302-189215107.png)
![received](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220934115-1107747665.png)
![subscibers](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220949193-884674167.png)
![nodes](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004221001880-1162918362.png)
## Contribute
One of the easiest ways to contribute is to participate in discussions and discuss issues. You can also contribute by submitting pull requests with code changes.
......
......@@ -6,17 +6,16 @@
[![Member project of .NET China Foundation](https://img.shields.io/badge/member_project_of-.NET_CHINA-red.svg?style=flat&colorB=9E20C8)](https://github.com/dotnetcore)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dotnetcore/CAP/master/LICENSE.txt)
CAP 是一个在分布式系统(SOA、MicroService)中实现最终一致性的库,它具有轻量级、易使用、高性能等特点。
CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级、易使用、高性能等特点。
你可以在这里[CAP Wiki](https://github.com/dotnetcore/CAP/wiki)看到更多详细资料。
## 预览(OverView)
CAP 是在一个 ASP.NET Core 项目中使用的库,当然他可以用于 ASP.NET Core On .NET Framework 中。
在我们构建 SOA 或者 微服务系统的过程中,我们通常需要使用事件来对各个服务进行集成,在这过程中简单的使用消息队列并不能保证数据的最终一致性,
CAP 采用的是和当前数据库集成的本地消息表的方案来解决在分布式系统互相调用的各个环节可能出现的异常,它能够保证任何情况下事件消息都是不会丢失的。
你可以把 CAP 看成是一个 EventBus,因为它具有 EventBus 的所有功能,并且 CAP 提供了更加简化的方式来处理 EventBus 中的发布和订阅。
CAP 具有消息持久化的功能,当你的服务进行重启或者宕机时它可以保证消息的可靠性。CAP提供了基于Microsoft DI 的 Publisher Service 服务,它可以和你的业务服务进行无缝结合,并且支持强一致性的事务。
你同样可以把 CAP 当做 EventBus 来使用,CAP提供了一种更加简单的方式来实现事件消息的发布和订阅,在订阅以及发布的过程中,你不需要继承或实现任何接口。
这是CAP集在ASP.NET Core 微服务架构中的一个示意图:
......@@ -59,7 +58,7 @@ PM> Install-Package DotNetCore.CAP.PostgreSql
首先配置CAP到 Startup.cs 文件中,如下:
```cs
```c#
public void ConfigureServices(IServiceCollection services)
{
......
......@@ -96,34 +95,40 @@ public void Configure(IApplicationBuilder app)
在 Controller 中注入 `ICapPublisher` 然后使用 `ICapPublisher` 进行消息发送
```cs
```c#
public class PublishController : Controller
{
private readonly ICapPublisher _publisher;
public PublishController(ICapPublisher publisher)
[Route("~/checkAccountWithTrans")]
public async Task<IActionResult> PublishMessageWithTransaction([FromServices]AppDbContext dbContext, [FromServices]ICapPublisher publisher)
{
_publisher = publisher;
}
[Route("~/checkAccount")]
public async Task<IActionResult> PublishMessage()
using (var trans = dbContext.Database.BeginTransaction())
{
//指定发送的消息头和内容
await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
// 此处填写你的业务代码
//如果你使用的是EF,CAP会自动发现当前环境中的事务,所以你不必显式传递事务参数。
//由于本地事务, 当前数据库的业务操作和发布事件日志之间将实现原子性。
await publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
trans.Commit();
}
return Ok();
}
[Route("~/checkAccountWithTrans")]
public async Task<IActionResult> PublishMessageWithTransaction([FromServices]AppDbContext dbContext)
[Route("~/publishWithTransactionUsingAdonet")]
public async Task<IActionResult> PublishMessageWithTransactionUsingAdonet([FromServices]ICapPublisher publisher)
{
using (var trans = dbContext.Database.BeginTransaction())
var connectionString = "";
using (var sqlConnection = new SqlConnection(connectionString))
{
await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
sqlConnection.Open();
using (var sqlTransaction = sqlConnection.BeginTransaction())
{
// 此处填写你的业务代码,通常情况下,你可以将业务代码使用一个委托传递进来进行封装该区域代码。
trans.Commit();
publisher.Publish("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }, sqlTransaction);
sqlTransaction.Commit();
}
}
return Ok();
}
......@@ -137,18 +142,9 @@ public class PublishController : Controller
在 Action 上添加 CapSubscribeAttribute 来订阅相关消息。
```cs
```c#
public class PublishController : Controller
{
private readonly ICapPublisher _publisher;
public PublishController(ICapPublisher publisher)
{
_publisher = publisher;
}
[NoAction]
[CapSubscribe("xxx.services.account.check")]
public async Task CheckReceivedMessage(Person person)
{
......@@ -164,7 +160,7 @@ public class PublishController : Controller
如果你的订阅方法没有位于 Controller 中,则你订阅的类需要继承 `ICapSubscribe`
```cs
```c#
namespace xxx.Service
{
......@@ -188,13 +184,48 @@ namespace xxx.Service
然后在 Startup.cs 中的 `ConfigureServices()` 中注入你的 `ISubscriberService`
```cs
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISubscriberService,SubscriberService>();
}
```
### Dashboard
CAP 2.1+ 以上版本中提供了仪表盘(Dashboard)功能,你可以很方便的查看发出和接收到的消息。除此之外,你还可以在仪表盘中实时查看发送或者接收到的消息。
在分布式环境中,仪表盘内置集成了 [Consul](http://consul.io) 作为节点的注册发现,同时实现了网关代理功能,你同样可以方便的查看本节点或者其他节点的数据,它就像你访问本地资源一样。
```c#
services.AddCap(x =>
{
//...
// 注册 Dashboard
x.UseDashboard();
// 注册节点到 Consul
x.UseDiscovery(d =>
{
d.DiscoveryServerHostName = "localhost";
d.DiscoveryServerPort = 8500;
d.CurrentNodeHostName = "localhost";
d.CurrentNodePort = 5800;
d.NodeId = 1;
d.NodeName = "CAP No.1 Node";
});
});
```
![dashboard](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220827302-189215107.png)
![received](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220934115-1107747665.png)
![subscibers](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220949193-884674167.png)
![nodes](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004221001880-1162918362.png)
## 贡献
贡献的最简单的方法之一就是是参与讨论和讨论问题(issue)。你也可以通过提交的 Pull Request 代码变更作出贡献。
......
......@@ -4,7 +4,7 @@ environment:
BUILDING_ON_PLATFORM: win
BuildEnvironment: appveyor
Cap_SqlServer_ConnectionStringTemplate: Server=(local)\SQL2014;Database={0};User ID=sa;Password=Password12!
Cap_MySql_ConnectionStringTemplate: Server=localhost;Database={0};Uid=root;Pwd=Password12!
Cap_MySql_ConnectionStringTemplate: Server=localhost;Database={0};Uid=root;Pwd=Password12!;Allow User Variables=True
Cap_PostgreSql_ConnectionStringTemplate: Server=localhost;Database={0};UserId=postgres;Password=Password12!
services:
- mssql2014
......
<Project>
<PropertyGroup>
<VersionMajor>2</VersionMajor>
<VersionMinor>0</VersionMinor>
<VersionPatch>2</VersionPatch>
<VersionMinor>1</VersionMinor>
<VersionPatch>0</VersionPatch>
<VersionQuality></VersionQuality>
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
</PropertyGroup>
......
using Microsoft.EntityFrameworkCore;
namespace Sample.Kafka.SqlServer
{
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=Sample.Kafka.SqlServer;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True");
optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=TestCap;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True");
}
}
}
using System;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Newtonsoft.Json;
namespace Sample.RabbitMQ.SqlServer
{
public class MessageContent : CapMessage
{
[JsonProperty("id")]
public override string Id { get; set; }
[JsonProperty("createdTime")]
public override DateTime Timestamp { get; set; }
[JsonProperty("msgBody")]
public override string Content { get; set; }
[JsonProperty("callbackTopicName")]
public override string CallbackName { get; set; }
}
public class MyMessagePacker : IMessagePacker
{
private readonly IContentSerializer _serializer;
public MyMessagePacker(IContentSerializer serializer)
{
_serializer = serializer;
}
public string Pack(CapMessage obj)
{
var content = new MessageContent
{
Id = obj.Id,
Content = obj.Content,
CallbackName = obj.CallbackName,
Timestamp = obj.Timestamp
};
return _serializer.Serialize(content);
}
public CapMessage UnPack(string packingMessage)
{
return _serializer.DeSerialize<MessageContent>(packingMessage);
}
}
}
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace Sample.Kafka.SqlServer.Controllers
{
public class Person
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("uname")]
public string Name { get; set; }
public HAHA Haha { get; set; }
public override string ToString()
{
return "Name:" + Name + ";Id:" + Id + "Haha:" + Haha?.ToString();
}
}
public class HAHA
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("uname")]
public string Name { get; set; }
public override string ToString()
{
return "Name:" + Name + ";Id:" + Id;
}
}
[Route("api/[controller]")]
public class ValuesController : Controller, ICapSubscribe
{
private readonly ICapPublisher _capBus;
private readonly AppDbContext _dbContext;
public ValuesController(ICapPublisher producer, AppDbContext dbContext)
{
_capBus = producer;
_dbContext = dbContext;
}
[Route("~/publish")]
public IActionResult PublishMessage()
{
var p = new Person
{
Id = Guid.NewGuid().ToString(),
Name = "杨晓东",
Haha = new HAHA
{
Id = Guid.NewGuid().ToString(),
Name = "1-1杨晓东",
}
};
_capBus.Publish("wl.yxd.test", p, "wl.yxd.test.callback");
//_capBus.Publish("wl.cj.test", p);
return Ok();
}
[CapSubscribe("wl.yxd.test.callback")]
public void KafkaTestCallback(Person p)
{
Console.WriteLine("回调内容:" + p);
}
[CapSubscribe("wl.cj.test")]
public string KafkaTestReceived(Person person)
{
Console.WriteLine(person);
Debug.WriteLine(person);
return "this is callback message";
}
[Route("~/publishWithTrans")]
public async Task<IActionResult> PublishMessageWithTransaction()
{
using (var trans = await _dbContext.Database.BeginTransactionAsync())
{
await _capBus.PublishAsync("sample.rabbitmq.mysql", "");
trans.Commit();
}
return Ok();
}
[CapSubscribe("sample.rabbitmq.mysql33333", Group = "Test.Group")]
public void KafkaTest22(Person person)
{
var aa = _dbContext.Database;
_dbContext.Dispose();
Console.WriteLine("[sample.kafka.sqlserver] message received " + person.ToString());
Debug.WriteLine("[sample.kafka.sqlserver] message received " + person.ToString());
}
//[CapSubscribe("sample.rabbitmq.mysql22222")]
//public void KafkaTest22(DateTime time)
//{
// Console.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString());
// Debug.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString());
//}
[CapSubscribe("sample.rabbitmq.mysql22222")]
public async Task<DateTime> KafkaTest33(DateTime time)
{
Console.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString());
Debug.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString());
return await Task.FromResult(time);
}
[NonAction]
[CapSubscribe("sample.kafka.sqlserver3")]
[CapSubscribe("sample.kafka.sqlserver4")]
public void KafkaTest()
{
Console.WriteLine("[sample.kafka.sqlserver] message received");
Debug.WriteLine("[sample.kafka.sqlserver] message received");
}
}
}
\ No newline at end of file
using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
namespace Sample.Kafka.SqlServer
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyName>Sample.Kafka.SqlServer</AssemblyName>
<WarningsAsErrors>NU1701</WarningsAsErrors>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Kafka\DotNetCore.CAP.Kafka.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>
</Project>
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sample.RabbitMQ.SqlServer;
namespace Sample.Kafka.SqlServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>();
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>();
x.UseKafka("192.168.2.215:9092");
x.UseDashboard();
//x.UseDiscovery(d =>
//{
// d.DiscoveryServerHostName = "localhost";
// d.DiscoveryServerPort = 8500;
// d.CurrentNodeHostName = "localhost";
// d.CurrentNodePort = 5820;
// d.NodeName = "CAP 2号节点";
//});
}).AddMessagePacker<MyMessagePacker>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc();
app.UseCap();
}
}
}
\ No newline at end of file
......@@ -10,8 +10,8 @@ namespace Sample.RabbitMQ.MySql
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//optionsBuilder.UseMySql("Server=localhost;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;");
optionsBuilder.UseMySql("Server=192.168.2.206;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;");
optionsBuilder.UseMySql("Server=localhost;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;Allow User Variables=True");
//optionsBuilder.UseMySql("Server=192.168.2.206;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;");
}
}
}
......@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
......@@ -13,20 +14,12 @@ namespace Sample.RabbitMQ.MySql
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables("ASPNETCORE_")
.Build();
BuildWebHost(args).Run();
}
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
......@@ -10,14 +10,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.0.0-rtm-10056" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
......
......@@ -18,11 +18,8 @@ namespace Sample.RabbitMQ.MySql
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>();
x.UseRabbitMQ(y => {
y.HostName = "192.168.2.206";
y.UserName = "admin";
y.Password = "123123";
});
x.UseRabbitMQ("localhost");
x.UseDashboard();
});
services.AddMvc();
......
......@@ -10,7 +10,7 @@ namespace Sample.RabbitMQ.PostgreSql
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("Server=localhost;Database=Sample.RabbitMQ.PostgreSql;UserId=postgre;Password=123123;");
optionsBuilder.UseNpgsql("Server=localhost;Database=Sample.RabbitMQ.PostgreSql;UserId=postgres;Password=123123;");
}
}
}
......@@ -4,10 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Sample.RabbitMQ.PostgreSql
{
......@@ -16,7 +13,21 @@ namespace Sample.RabbitMQ.PostgreSql
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>();
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>();
x.UseRabbitMQ("localhost");
x.UseDashboard();
x.UseDiscovery(d =>
{
d.DiscoveryServerHostName = "localhost";
d.DiscoveryServerPort = 8500;
d.CurrentNodeHostName = "localhost";
d.CurrentNodePort = 5800;
d.NodeName = "CAP 一号节点";
});
});
services.AddMvc();
}
......@@ -24,6 +35,8 @@ namespace Sample.RabbitMQ.PostgreSql
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc();
app.UseCap();
}
}
}
using Microsoft.EntityFrameworkCore;
using Sample.RabbitMQ.SqlServer.Controllers;
namespace Sample.RabbitMQ.SqlServer
{
public class AppDbContext : DbContext
{
public DbSet<Person> Persons { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=TestCap;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True");
//optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Sample.Kafka.SqlServer;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True");
//optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=TestCap;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True");
optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Sample.Kafka.SqlServer;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True");
}
}
}
......@@ -2,12 +2,14 @@
using System.Diagnostics;
using System.Threading.Tasks;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Sample.RabbitMQ.SqlServer.Controllers
{
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
......@@ -33,12 +35,17 @@ namespace Sample.RabbitMQ.SqlServer.Controllers
[Route("~/publish")]
public IActionResult PublishMessage()
{
using(var trans = _dbContext.Database.BeginTransaction())
{
//_capBus.Publish("sample.rabbitmq.mysql22222", DateTime.Now);
_capBus.Publish("sample.rabbitmq.mysql33333", new Person { Name = "宜兴", Age = 11 });
trans.Commit();
}
_capBus.Publish("sample.rabbitmq.sqlserver.order.check", DateTime.Now);
//var person = new Person
//{
// Name = "杨晓东",
// Age = 11,
// Id = 23
//};
//_capBus.Publish("sample.rabbitmq.mysql33333", person);
return Ok();
}
......@@ -54,7 +61,7 @@ namespace Sample.RabbitMQ.SqlServer.Controllers
return Ok();
}
[CapSubscribe("sample.rabbitmq.mysql33333")]
[CapSubscribe("sample.rabbitmq.mysql33333",Group ="Test.Group")]
public void KafkaTest22(Person person)
{
var aa = _dbContext.Database;
......
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Sample.RabbitMQ.SqlServer;
using System;
namespace Sample.RabbitMQ.SqlServer.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20170824130007_AddPersons")]
partial class AddPersons
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Sample.RabbitMQ.SqlServer.Controllers.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Age");
b.Property<string>("Name");
b.HasKey("Id");
b.ToTable("Persons");
});
#pragma warning restore 612, 618
}
}
}
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Sample.RabbitMQ.SqlServer.Migrations
{
public partial class AddPersons : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Persons",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Age = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Persons", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Persons");
}
}
}
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Sample.RabbitMQ.SqlServer;
using System;
namespace Sample.RabbitMQ.SqlServer.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Sample.RabbitMQ.SqlServer.Controllers.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Age");
b.Property<string>("Name");
b.HasKey("Id");
b.ToTable("Persons");
});
#pragma warning restore 612, 618
}
}
}
......@@ -9,20 +9,6 @@ namespace Sample.RabbitMQ.SqlServer
public class Program
{
//var config = new ConfigurationBuilder()
// .AddCommandLine(args)
// .AddEnvironmentVariables("ASPNETCORE_")
// .Build();
//var host = new WebHostBuilder()
// .UseConfiguration(config)
// .UseKestrel()
// .UseContentRoot(Directory.GetCurrentDirectory())
// .UseIISIntegration()
// .UseStartup<Startup>()
// .Build();
//host.Run();
public static void Main(string[] args)
{
BuildWebHost(args).Run();
......@@ -30,6 +16,7 @@ namespace Sample.RabbitMQ.SqlServer
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5800")
.UseStartup<Startup>()
.Build();
......
......@@ -6,14 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNetCore.CAP;
namespace Sample.RabbitMQ.SqlServer.Services
{
public interface ICmsService
{
void Add();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Sample.RabbitMQ.SqlServer.Services
{
public interface IOrderService
{
void Check();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNetCore.CAP;
namespace Sample.RabbitMQ.SqlServer.Services.Impl
{
public class CmsService : ICmsService, ICapSubscribe
{
public void Add()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNetCore.CAP;
namespace Sample.RabbitMQ.SqlServer.Services.Impl
{
public class OrderService : IOrderService, ICapSubscribe
{
[CapSubscribe("sample.rabbitmq.sqlserver.order.check")]
public void Check()
{
Console.WriteLine("out");
}
}
}
......@@ -2,6 +2,8 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sample.RabbitMQ.SqlServer.Services;
using Sample.RabbitMQ.SqlServer.Services.Impl;
namespace Sample.RabbitMQ.SqlServer
{
......@@ -11,13 +13,23 @@ namespace Sample.RabbitMQ.SqlServer
{
services.AddDbContext<AppDbContext>();
services.AddScoped<IOrderService, OrderService>();
services.AddTransient<ICmsService, CmsService>();
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>();
x.UseRabbitMQ(y=> {
y.HostName = "192.168.2.206";
y.UserName = "admin";
y.Password = "123123";
x.UseRabbitMQ("localhost");
x.UseDashboard();
x.UseDiscovery(d =>
{
d.DiscoveryServerHostName = "localhost";
d.DiscoveryServerPort = 8500;
d.CurrentNodeHostName = "192.168.1.11";
d.CurrentNodePort = 5800;
d.NodeName = "CAP Node Windows";
d.NodeId = 1;
});
});
......
......@@ -16,12 +16,16 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapMessageQueueMakerService>();
var kafkaOptions = new KafkaOptions();
_configure?.Invoke(kafkaOptions);
services.AddSingleton(kafkaOptions);
services.AddSingleton<IConsumerClientFactory, KafkaConsumerClientFactory>();
services.AddSingleton<IQueueExecutor, PublishQueueExecutor>();
services.AddSingleton<IPublishExecutor, PublishQueueExecutor>();
services.AddSingleton<ConnectionPool>();
}
}
}
\ No newline at end of file
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
......@@ -10,46 +11,51 @@ namespace DotNetCore.CAP
/// </summary>
public class KafkaOptions
{
public KafkaOptions()
{
MainConfig = new Dictionary<string, object>();
}
/// <summary>
/// librdkafka configuration parameters (refer to https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md).
/// <para>
/// Topic configuration parameters are specified via the "default.topic.config" sub-dictionary config parameter.
/// </para>
/// </summary>
public readonly IDictionary<string, object> MainConfig;
public readonly ConcurrentDictionary<string, object> MainConfig;
private IEnumerable<KeyValuePair<string, object>> _kafkaConfig;
public KafkaOptions()
{
MainConfig = new ConcurrentDictionary<string, object>();
}
/// <summary>
/// The `bootstrap.servers` item config of <see cref="MainConfig"/>.
/// Producer connection pool size, default is 10
/// </summary>
public int ConnectionPoolSize { get; set; } = 10;
/// <summary>
/// The `bootstrap.servers` item config of <see cref="MainConfig" />.
/// <para>
/// Initial list of brokers as a CSV list of broker host or host:port.
/// </para>
/// </summary>
public string Servers { get; set; }
internal IEnumerable<KeyValuePair<string, object>> AskafkaConfig()
internal IEnumerable<KeyValuePair<string, object>> AsKafkaConfig()
{
if (MainConfig.ContainsKey("bootstrap.servers"))
if (_kafkaConfig == null)
{
return MainConfig.AsEnumerable();
}
if (string.IsNullOrWhiteSpace(Servers))
{
throw new ArgumentNullException(nameof(Servers));
}
MainConfig.Add("bootstrap.servers", Servers);
MainConfig["bootstrap.servers"] = Servers;
MainConfig["queue.buffering.max.ms"] = "10";
MainConfig["socket.blocking.max.ms"] = "10";
MainConfig["enable.auto.commit"] = "false";
MainConfig["log.connection.close"] = "false";
return MainConfig.AsEnumerable();
_kafkaConfig = MainConfig.AsEnumerable();
}
return _kafkaConfig;
}
}
}
\ No newline at end of file
......@@ -9,18 +9,17 @@ namespace Microsoft.Extensions.DependencyInjection
/// <summary>
/// Configuration to use kafka in CAP.
/// </summary>
/// <param name="options">CAP configuration options</param>
/// <param name="bootstrapServers">Kafka bootstrap server urls.</param>
public static CapOptions UseKafka(this CapOptions options, string bootstrapServers)
{
return options.UseKafka(opt =>
{
opt.Servers = bootstrapServers;
});
return options.UseKafka(opt => { opt.Servers = bootstrapServers; });
}
/// <summary>
/// Configuration to use kafka in CAP.
/// </summary>
/// <param name="options">CAP configuration options</param>
/// <param name="configure">Provides programmatic configuration for the kafka .</param>
/// <returns></returns>
public static CapOptions UseKafka(this CapOptions options, Action<KafkaOptions> configure)
......
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using Confluent.Kafka;
namespace DotNetCore.CAP.Kafka
{
public class ConnectionPool : IConnectionPool, IDisposable
{
private readonly Func<Producer> _activator;
private readonly ConcurrentQueue<Producer> _pool = new ConcurrentQueue<Producer>();
private int _count;
private int _maxSize;
public ConnectionPool(KafkaOptions options)
{
_maxSize = options.ConnectionPoolSize;
_activator = CreateActivator(options);
}
Producer IConnectionPool.Rent()
{
return Rent();
}
bool IConnectionPool.Return(Producer connection)
{
return Return(connection);
}
public void Dispose()
{
_maxSize = 0;
while (_pool.TryDequeue(out var context))
context.Dispose();
}
private static Func<Producer> CreateActivator(KafkaOptions options)
{
return () => new Producer(options.AsKafkaConfig());
}
public virtual Producer Rent()
{
if (_pool.TryDequeue(out var connection))
{
Interlocked.Decrement(ref _count);
Debug.Assert(_count >= 0);
return connection;
}
connection = _activator();
return connection;
}
public virtual bool Return(Producer connection)
{
if (Interlocked.Increment(ref _count) <= _maxSize)
{
_pool.Enqueue(connection);
return true;
}
Interlocked.Decrement(ref _count);
Debug.Assert(_maxSize == 0 || _pool.Count <= _maxSize);
return false;
}
}
}
\ No newline at end of file
......@@ -9,12 +9,12 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<WarningsAsErrors>NU1605</WarningsAsErrors>
<WarningsAsErrors>NU1605;NU1701</WarningsAsErrors>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="0.11.0" />
<PackageReference Include="Confluent.Kafka" Version="0.11.2" />
</ItemGroup>
<ItemGroup>
......

using Confluent.Kafka;
namespace DotNetCore.CAP.Kafka
{
public interface IConnectionPool
{
Producer Rent();
bool Return(Producer context);
}
}
\ No newline at end of file
......@@ -13,12 +13,6 @@ namespace DotNetCore.CAP.Kafka
private readonly KafkaOptions _kafkaOptions;
private Consumer<Null, string> _consumerClient;
public event EventHandler<MessageContext> OnMessageReceieved;
public event EventHandler<string> OnError;
public IDeserializer<string> StringDeserializer { get; set; }
public KafkaConsumerClient(string groupId, KafkaOptions options)
{
_groupId = groupId;
......@@ -26,15 +20,19 @@ namespace DotNetCore.CAP.Kafka
StringDeserializer = new StringDeserializer(Encoding.UTF8);
}
public IDeserializer<string> StringDeserializer { get; set; }
public event EventHandler<MessageContext> OnMessageReceived;
public event EventHandler<string> OnError;
public void Subscribe(IEnumerable<string> topics)
{
if (topics == null)
throw new ArgumentNullException(nameof(topics));
if (_consumerClient == null)
{
InitKafkaClient();
}
//_consumerClient.Assign(topics.Select(x=> new TopicPartition(x, 0)));
_consumerClient.Subscribe(topics);
......@@ -47,6 +45,7 @@ namespace DotNetCore.CAP.Kafka
cancellationToken.ThrowIfCancellationRequested();
_consumerClient.Poll(timeout);
}
// ReSharper disable once FunctionNeverReturns
}
public void Commit()
......@@ -54,6 +53,11 @@ namespace DotNetCore.CAP.Kafka
_consumerClient.CommitAsync();
}
public void Reject()
{
// Ignore, Kafka will not commit offset when not commit.
}
public void Dispose()
{
_consumerClient.Dispose();
......@@ -63,15 +67,23 @@ namespace DotNetCore.CAP.Kafka
private void InitKafkaClient()
{
_kafkaOptions.MainConfig.Add("group.id", _groupId);
_kafkaOptions.MainConfig["group.id"] = _groupId;
var config = _kafkaOptions.AskafkaConfig();
var config = _kafkaOptions.AsKafkaConfig();
_consumerClient = new Consumer<Null, string>(config, null, StringDeserializer);
_consumerClient.OnConsumeError += ConsumerClient_OnConsumeError;
_consumerClient.OnMessage += ConsumerClient_OnMessage;
_consumerClient.OnError += ConsumerClient_OnError;
}
private void ConsumerClient_OnConsumeError(object sender, Message e)
{
var message = e.Deserialize<Null, string>(null, StringDeserializer);
OnError?.Invoke(sender, $"An error occurred during consume the message; Topic:'{e.Topic}'," +
$"Message:'{message.Value}', Reason:'{e.Error}'.");
}
private void ConsumerClient_OnMessage(object sender, Message<Null, string> e)
{
var message = new MessageContext
......@@ -81,12 +93,12 @@ namespace DotNetCore.CAP.Kafka
Content = e.Value
};
OnMessageReceieved?.Invoke(sender, message);
OnMessageReceived?.Invoke(sender, message);
}
private void ConsumerClient_OnError(object sender, Error e)
{
OnError?.Invoke(sender, e.Reason);
OnError?.Invoke(sender, e.ToString());
}
#endregion private methods
......
using System;
using System.Text;
using System.Threading.Tasks;
using Confluent.Kafka;
using DotNetCore.CAP.Processor.States;
using Microsoft.Extensions.Logging;
......@@ -9,51 +8,54 @@ namespace DotNetCore.CAP.Kafka
{
internal class PublishQueueExecutor : BasePublishQueueExecutor
{
private readonly ConnectionPool _connectionPool;
private readonly ILogger _logger;
private readonly KafkaOptions _kafkaOptions;
public PublishQueueExecutor(
CapOptions options,
IStateChanger stateChanger,
KafkaOptions kafkaOptions,
ConnectionPool connectionPool,
ILogger<PublishQueueExecutor> logger)
: base(options, stateChanger, logger)
{
_logger = logger;
_kafkaOptions = kafkaOptions;
_connectionPool = connectionPool;
}
public override Task<OperateResult> PublishAsync(string keyName, string content)
public override async Task<OperateResult> PublishAsync(string keyName, string content)
{
var producer = _connectionPool.Rent();
try
{
var config = _kafkaOptions.AskafkaConfig();
var contentBytes = Encoding.UTF8.GetBytes(content);
using (var producer = new Producer(config))
{
var message = producer.ProduceAsync(keyName, null, contentBytes).Result;
var message = await producer.ProduceAsync(keyName, null, contentBytes);
if (!message.Error.HasError)
{
_logger.LogDebug($"kafka topic message [{keyName}] has been published.");
return Task.FromResult(OperateResult.Success);
return OperateResult.Success;
}
else
{
return Task.FromResult(OperateResult.Failed(new OperateError
return OperateResult.Failed(new OperateError
{
Code = message.Error.Code.ToString(),
Description = message.Error.Reason
}));
}
}
});
}
catch (Exception ex)
{
_logger.LogError($"kafka topic message [{keyName}] has benn raised an exception of sending. the exception is: {ex.Message}");
_logger.LogError(ex,
$"An error occurred during sending the topic message to kafka. Topic:[{keyName}], Exception: {ex.Message}");
return Task.FromResult(OperateResult.Failed(ex));
return OperateResult.Failed(ex);
}
finally
{
var returned = _connectionPool.Return(producer);
if (!returned)
producer.Dispose();
}
}
}
......
......@@ -6,7 +6,7 @@ namespace DotNetCore.CAP
public class EFOptions
{
/// <summary>
/// EF dbcontext type.
/// EF db context type.
/// </summary>
internal Type DbContextType { get; set; }
}
......
......@@ -18,32 +18,29 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, MySqlStorage>();
services.AddScoped<IStorageConnection, MySqlStorageConnection>();
services.AddSingleton<IStorageConnection, MySqlStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddTransient<ICallbackPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();
services.AddTransient<IAdditionalProcessor, DefaultAdditionalProcessor>();
var mysqlOptions = new MySqlOptions();
_configure(mysqlOptions);
if (mysqlOptions.DbContextType != null)
{
services.AddSingleton(x =>
{
using (var scope = x.CreateScope())
{
var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(mysqlOptions.DbContextType);
var dbContext = (DbContext) provider.GetService(mysqlOptions.DbContextType);
mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return mysqlOptions;
}
});
}
else
{
services.AddSingleton(mysqlOptions);
}
}
}
}
\ No newline at end of file
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class MySqlOptions : EFOptions
......
......@@ -9,10 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static CapOptions UseMySql(this CapOptions options, string connectionString)
{
return options.UseMySql(opt =>
{
opt.ConnectionString = connectionString;
});
return options.UseMySql(opt => { opt.ConnectionString = connectionString; });
}
public static CapOptions UseMySql(this CapOptions options, Action<MySqlOptions> configure)
......@@ -27,10 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static CapOptions UseEntityFramework<TContext>(this CapOptions options)
where TContext : DbContext
{
return options.UseEntityFramework<TContext>(opt =>
{
opt.DbContextType = typeof(TContext);
});
return options.UseEntityFramework<TContext>(opt => { opt.DbContextType = typeof(TContext); });
}
public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> configure)
......@@ -38,7 +32,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
var efOptions = new EFOptions { DbContextType = typeof(TContext) };
var efOptions = new EFOptions {DbContextType = typeof(TContext)};
configure(efOptions);
options.RegisterExtension(new MySqlCapOptionsExtension(configure));
......
......@@ -13,9 +13,9 @@ namespace DotNetCore.CAP.MySql
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly ILogger _logger;
private readonly MySqlOptions _options;
private readonly DbContext _dbContext;
public CapPublisher(IServiceProvider provider,
ILogger<CapPublisher> logger,
......@@ -25,10 +25,16 @@ namespace DotNetCore.CAP.MySql
_options = options;
_logger = logger;
if (_options.DbContextType != null)
{
if (_options.DbContextType == null) return;
IsUsingEF = true;
_dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType);
_dbContext = (DbContext) ServiceProvider.GetService(_options.DbContextType);
}
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new MySqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
}
......@@ -45,36 +51,33 @@ namespace DotNetCore.CAP.MySql
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}
DbTranasaction = dbTrans;
DbTransaction = dbTrans;
}
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
dbConnection.Execute(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
}
protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
}
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new MySqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
return Task.CompletedTask;
}
#region private methods
private string PrepareSql()
{
return $"INSERT INTO `{_options.TableNamePrefix}.published` (`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
return
$"INSERT INTO `{_options.TableNamePrefix}.published` (`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
}
#endregion private methods
......
......@@ -12,7 +12,7 @@
<PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.0.0" />
<PackageReference Include="MySqlConnector" Version="0.25.1" />
<PackageReference Include="MySqlConnector" Version="0.28.2" />
</ItemGroup>
<ItemGroup>
......
......@@ -9,21 +9,16 @@ namespace DotNetCore.CAP.MySql
{
internal class DefaultAdditionalProcessor : IAdditionalProcessor
{
private readonly IServiceProvider _provider;
private readonly ILogger _logger;
private readonly MySqlOptions _options;
private const int MaxBatch = 1000;
private readonly TimeSpan _delay = TimeSpan.FromSeconds(1);
private readonly ILogger _logger;
private readonly MySqlOptions _options;
private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5);
public DefaultAdditionalProcessor(
IServiceProvider provider,
ILogger<DefaultAdditionalProcessor> logger,
public DefaultAdditionalProcessor(ILogger<DefaultAdditionalProcessor> logger,
MySqlOptions mysqlOptions)
{
_logger = logger;
_provider = provider;
_options = mysqlOptions;
}
......@@ -31,20 +26,22 @@ namespace DotNetCore.CAP.MySql
{
_logger.LogDebug("Collecting expired entities.");
var tables = new string[]{
var tables = new[]
{
$"{_options.TableNamePrefix}.published",
$"{_options.TableNamePrefix}.received"
};
foreach (var table in tables)
{
var removedCount = 0;
int removedCount;
do
{
using (var connection = new MySqlConnection(_options.ConnectionString))
{
removedCount = await connection.ExecuteAsync($@"DELETE FROM `{table}` WHERE ExpiresAt < @now limit @count;",
new { now = DateTime.Now, count = MaxBatch });
removedCount = await connection.ExecuteAsync(
$@"DELETE FROM `{table}` WHERE ExpiresAt < @now limit @count;",
new {now = DateTime.Now, count = MaxBatch});
}
if (removedCount != 0)
......
......@@ -8,11 +8,11 @@ namespace DotNetCore.CAP.MySql
{
public class MySqlFetchedMessage : IFetchedMessage
{
private readonly IDbConnection _connection;
private readonly IDbTransaction _transaction;
private readonly Timer _timer;
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromMinutes(1);
private readonly IDbConnection _connection;
private readonly object _lockObject = new object();
private readonly Timer _timer;
private readonly IDbTransaction _transaction;
public MySqlFetchedMessage(int messageId,
MessageType type,
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.Monitoring;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.MySql
{
internal class MySqlMonitoringApi : IMonitoringApi
{
private readonly string _prefix;
private readonly MySqlStorage _storage;
public MySqlMonitoringApi(IStorage storage, MySqlOptions options)
{
_storage = storage as MySqlStorage ?? throw new ArgumentNullException(nameof(storage));
_prefix = options?.TableNamePrefix ?? throw new ArgumentNullException(nameof(options));
}
public StatisticsDto GetStatistics()
{
var sql = string.Format(@"
set transaction isolation level read committed;
select count(Id) from `{0}.published` where StatusName = N'Succeeded';
select count(Id) from `{0}.received` where StatusName = N'Succeeded';
select count(Id) from `{0}.published` where StatusName = N'Failed';
select count(Id) from `{0}.received` where StatusName = N'Failed';
select count(Id) from `{0}.published` where StatusName in (N'Processing',N'Scheduled',N'Enqueued');
select count(Id) from `{0}.received` where StatusName in (N'Processing',N'Scheduled',N'Enqueued');", _prefix);
var statistics = UseConnection(connection =>
{
var stats = new StatisticsDto();
using (var multi = connection.QueryMultiple(sql))
{
stats.PublishedSucceeded = multi.ReadSingle<int>();
stats.ReceivedSucceeded = multi.ReadSingle<int>();
stats.PublishedFailed = multi.ReadSingle<int>();
stats.ReceivedFailed = multi.ReadSingle<int>();
stats.PublishedProcessing = multi.ReadSingle<int>();
stats.ReceivedProcessing = multi.ReadSingle<int>();
}
return stats;
});
return statistics;
}
public IDictionary<DateTime, int> HourlyFailedJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "published" : "received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Failed));
}
public IDictionary<DateTime, int> HourlySucceededJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "published" : "received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Succeeded));
}
public IList<MessageDto> Messages(MessageQueryDto queryDto)
{
var tableName = queryDto.MessageType == MessageType.Publish ? "published" : "received";
var where = string.Empty;
if (!string.IsNullOrEmpty(queryDto.StatusName))
if (string.Equals(queryDto.StatusName, StatusName.Processing,
StringComparison.CurrentCultureIgnoreCase))
where += " and StatusName in (N'Processing',N'Scheduled',N'Enqueued')";
else
where += " and StatusName=@StatusName";
if (!string.IsNullOrEmpty(queryDto.Name))
where += " and Name=@Name";
if (!string.IsNullOrEmpty(queryDto.Group))
where += " and Group=@Group";
if (!string.IsNullOrEmpty(queryDto.Content))
where += " and Content like '%@Content%'";
var sqlQuery =
$"select * from `{_prefix}.{tableName}` where 1=1 {where} order by Added desc limit @Limit offset @Offset";
return UseConnection(conn => conn.Query<MessageDto>(sqlQuery, new
{
queryDto.StatusName,
queryDto.Group,
queryDto.Name,
queryDto.Content,
Offset = queryDto.CurrentPage * queryDto.PageSize,
Limit = queryDto.PageSize
}).ToList());
}
public int PublishedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Failed));
}
public int PublishedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Processing));
}
public int PublishedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Succeeded));
}
public int ReceivedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Failed));
}
public int ReceivedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Processing));
}
public int ReceivedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Succeeded));
}
private int GetNumberOfMessage(IDbConnection connection, string tableName, string statusName)
{
var sqlQuery = statusName == StatusName.Processing
? $"select count(Id) from `{_prefix}.{tableName}` where StatusName in (N'Processing',N'Scheduled',N'Enqueued')"
: $"select count(Id) from `{_prefix}.{tableName}` where StatusName = @state";
var count = connection.ExecuteScalar<int>(sqlQuery, new {state = statusName});
return count;
}
private T UseConnection<T>(Func<IDbConnection, T> action)
{
return _storage.UseConnection(action);
}
private Dictionary<DateTime, int> GetHourlyTimelineStats(IDbConnection connection, string tableName,
string statusName)
{
var endDate = DateTime.Now;
var dates = new List<DateTime>();
for (var i = 0; i < 24; i++)
{
dates.Add(endDate);
endDate = endDate.AddHours(-1);
}
var keyMaps = dates.ToDictionary(x => x.ToString("yyyy-MM-dd-HH"), x => x);
return GetTimelineStats(connection, tableName, statusName, keyMaps);
}
private Dictionary<DateTime, int> GetTimelineStats(
IDbConnection connection,
string tableName,
string statusName,
IDictionary<string, DateTime> keyMaps)
{
var sqlQuery =
$@"
select aggr.* from (
select date_format(`Added`,'%Y-%m-%d-%H') as `Key`,
count(id) `Count`
from `{_prefix}.{tableName}`
where StatusName = @statusName
group by date_format(`Added`,'%Y-%m-%d-%H')
) aggr where `Key` in @keys;";
var valuesMap = connection.Query(
sqlQuery,
new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => (string) x.Key, x => (int) x.Count);
foreach (var key in keyMaps.Keys)
if (!valuesMap.ContainsKey(key)) valuesMap.Add(key, 0);
var result = new Dictionary<DateTime, int>();
for (var i = 0; i < keyMaps.Count; i++)
{
var value = valuesMap[keyMaps.ElementAt(i).Key];
result.Add(keyMaps.ElementAt(i).Value, value);
}
return result;
}
}
}
\ No newline at end of file
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Dashboard;
using Microsoft.Extensions.Logging;
using MySql.Data.MySqlClient;
......@@ -8,15 +11,30 @@ namespace DotNetCore.CAP.MySql
{
public class MySqlStorage : IStorage
{
private readonly MySqlOptions _options;
private readonly IDbConnection _existingConnection = null;
private readonly ILogger _logger;
private readonly MySqlOptions _options;
private readonly CapOptions _capOptions;
public MySqlStorage(ILogger<MySqlStorage> logger, MySqlOptions options)
public MySqlStorage(ILogger<MySqlStorage> logger,
MySqlOptions options,
CapOptions capOptions)
{
_options = options;
_capOptions = capOptions;
_logger = logger;
}
public IStorageConnection GetConnection()
{
return new MySqlStorageConnection(_options, _capOptions);
}
public IMonitoringApi GetMonitoringApi()
{
return new MySqlMonitoringApi(this, _options);
}
public async Task InitializeAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) return;
......@@ -62,5 +80,41 @@ CREATE TABLE IF NOT EXISTS `{prefix}.published` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
return batchSql;
}
internal T UseConnection<T>(Func<IDbConnection, T> func)
{
IDbConnection connection = null;
try
{
connection = CreateAndOpenConnection();
return func(connection);
}
finally
{
ReleaseConnection(connection);
}
}
internal IDbConnection CreateAndOpenConnection()
{
var connection = _existingConnection ?? new MySqlConnection(_options.ConnectionString);
if (connection.State == ConnectionState.Closed)
connection.Open();
return connection;
}
internal bool IsExistingConnection(IDbConnection connection)
{
return connection != null && ReferenceEquals(connection, _existingConnection);
}
internal void ReleaseConnection(IDbConnection connection)
{
if (connection != null && !IsExistingConnection(connection))
connection.Dispose();
}
}
}
\ No newline at end of file
......@@ -11,16 +11,19 @@ namespace DotNetCore.CAP.MySql
{
public class MySqlStorageConnection : IStorageConnection
{
private readonly MySqlOptions _options;
private readonly CapOptions _capOptions;
private readonly string _prefix;
public MySqlStorageConnection(MySqlOptions options)
private const string DateTimeMaxValue = "9999-12-31 23:59:59";
public MySqlStorageConnection(MySqlOptions options, CapOptions capOptions)
{
_options = options;
_prefix = _options.TableNamePrefix;
_capOptions = capOptions;
Options = options;
_prefix = Options.TableNamePrefix;
}
public MySqlOptions Options => _options;
public MySqlOptions Options { get; }
public IStorageTransaction CreateTransaction()
{
......@@ -31,7 +34,7 @@ namespace DotNetCore.CAP.MySql
{
var sql = $@"SELECT * FROM `{_prefix}.published` WHERE `Id`={id};";
using (var connection = new MySqlConnection(_options.ConnectionString))
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -39,26 +42,23 @@ namespace DotNetCore.CAP.MySql
public Task<IFetchedMessage> FetchNextMessageAsync()
{
//Last execute statement(FOR UPDATE to fix dirty read) :
//SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
//START TRANSACTION;
//SELECT MessageId,MessageType FROM `{_prefix}.queue` LIMIT 1 FOR UPDATE;
//DELETE FROM `{_prefix}.queue` LIMIT 1;
//COMMIT;
var sql = $@"
SELECT `MessageId`,`MessageType` FROM `{_prefix}.queue` LIMIT 1 FOR UPDATE;
DELETE FROM `{_prefix}.queue` LIMIT 1;";
// var sql = $@"
//SELECT @MId:=`MessageId` as MessageId, @MType:=`MessageType` as MessageType FROM `{_prefix}.queue` LIMIT 1;
//DELETE FROM `{_prefix}.queue` where `MessageId` = @MId AND `MessageType`=@MType;";
return FetchNextMessageCoreAsync(sql);
}
public async Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT * FROM `{_prefix}.published` WHERE `StatusName` = '{StatusName.Scheduled}' LIMIT 1;";
var sql = $@"
UPDATE `{_prefix}.published` SET Id=LAST_INSERT_ID(Id),ExpiresAt='{DateTimeMaxValue}' WHERE ExpiresAt IS NULL AND `StatusName` = '{StatusName.Scheduled}' LIMIT 1;
SELECT * FROM `{_prefix}.published` WHERE Id=LAST_INSERT_ID();";
using (var connection = new MySqlConnection(_options.ConnectionString))
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -66,16 +66,14 @@ DELETE FROM `{_prefix}.queue` LIMIT 1;";
public async Task<IEnumerable<CapPublishedMessage>> GetFailedPublishedMessages()
{
var sql = $"SELECT * FROM `{_prefix}.published` WHERE `StatusName` = '{StatusName.Failed}';";
var sql = $"SELECT * FROM `{_prefix}.published` WHERE `Retries`<{_capOptions.FailedRetryCount} AND `StatusName` = '{StatusName.Failed}' LIMIT 200;";
using (var connection = new MySqlConnection(_options.ConnectionString))
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<CapPublishedMessage>(sql);
}
}
// CapReceviedMessage
public async Task StoreReceivedMessageAsync(CapReceivedMessage message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
......@@ -84,7 +82,7 @@ DELETE FROM `{_prefix}.queue` LIMIT 1;";
INSERT INTO `{_prefix}.received`(`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
using (var connection = new MySqlConnection(_options.ConnectionString))
using (var connection = new MySqlConnection(Options.ConnectionString))
{
await connection.ExecuteAsync(sql, message);
}
......@@ -93,44 +91,82 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
{
var sql = $@"SELECT * FROM `{_prefix}.received` WHERE Id={id};";
using (var connection = new MySqlConnection(_options.ConnectionString))
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
}
}
public async Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync()
public async Task<CapReceivedMessage> GetNextReceivedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT * FROM `{_prefix}.received` WHERE `StatusName` = '{StatusName.Scheduled}' LIMIT 1;";
using (var connection = new MySqlConnection(_options.ConnectionString))
var sql = $@"
UPDATE `{_prefix}.received` SET Id=LAST_INSERT_ID(Id),ExpiresAt='{DateTimeMaxValue}' WHERE ExpiresAt IS NULL AND `StatusName` = '{StatusName.Scheduled}' LIMIT 1;
SELECT * FROM `{_prefix}.received` WHERE Id=LAST_INSERT_ID();";
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
}
}
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages()
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceivedMessages()
{
var sql = $"SELECT * FROM `{_prefix}.received` WHERE `StatusName` = '{StatusName.Failed}';";
using (var connection = new MySqlConnection(_options.ConnectionString))
var sql = $"SELECT * FROM `{_prefix}.received` WHERE `Retries`<{_capOptions.FailedRetryCount} AND `StatusName` = '{StatusName.Failed}' LIMIT 200;";
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<CapReceivedMessage>(sql);
}
}
public void Dispose()
{
}
public bool ChangePublishedState(int messageId, string state)
{
var sql =
$"UPDATE `{_prefix}.published` SET `Retries`=`Retries`+1,`StatusName` = '{state}' WHERE `Id`={messageId}";
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
public bool ChangeReceivedState(int messageId, string state)
{
var sql =
$"UPDATE `{_prefix}.received` SET `Retries`=`Retries`+1,`StatusName` = '{state}' WHERE `Id`={messageId}";
using (var connection = new MySqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
private async Task<IFetchedMessage> FetchNextMessageCoreAsync(string sql, object args = null)
{
//here don't use `using` to dispose
var connection = new MySqlConnection(_options.ConnectionString);
var connection = new MySqlConnection(Options.ConnectionString);
await connection.OpenAsync();
var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
FetchedMessage fetchedMessage = null;
try
{
fetchedMessage = await connection.QueryFirstOrDefaultAsync<FetchedMessage>(sql, args, transaction);
//fetchedMessage = await connection.QuerySingleOrDefaultAsync<FetchedMessage>(sql, args, transaction);
// An anomaly with unknown causes, sometimes QuerySingleOrDefaultAsync can't return expected result.
using (var reader = connection.ExecuteReader(sql, args, transaction))
{
while (reader.Read())
{
fetchedMessage = new FetchedMessage
{
MessageId = (int)reader.GetInt64(0),
MessageType = (MessageType)reader.GetInt64(1)
};
}
}
}
catch (MySqlException)
{
......@@ -146,7 +182,8 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
return null;
}
return new MySqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction);
return new MySqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection,
transaction);
}
}
}
\ No newline at end of file
......@@ -7,12 +7,12 @@ using MySql.Data.MySqlClient;
namespace DotNetCore.CAP.MySql
{
public class MySqlStorageTransaction : IStorageTransaction, IDisposable
public class MySqlStorageTransaction : IStorageTransaction
{
private readonly string _prefix;
private readonly IDbConnection _dbConnection;
private readonly IDbTransaction _dbTransaction;
private readonly IDbConnection _dbConnection;
private readonly string _prefix;
public MySqlStorageTransaction(MySqlStorageConnection connection)
{
......@@ -28,7 +28,8 @@ namespace DotNetCore.CAP.MySql
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"UPDATE `{_prefix}.published` SET `Retries` = @Retries,`ExpiresAt` = @ExpiresAt,`StatusName`=@StatusName WHERE `Id`=@Id;";
var sql =
$"UPDATE `{_prefix}.published` SET `Retries` = @Retries,`Content`= @Content,`ExpiresAt` = @ExpiresAt,`StatusName`=@StatusName WHERE `Id`=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -36,7 +37,8 @@ namespace DotNetCore.CAP.MySql
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"UPDATE `{_prefix}.received` SET `Retries` = @Retries,`ExpiresAt` = @ExpiresAt,`StatusName`=@StatusName WHERE `Id`=@Id;";
var sql =
$"UPDATE `{_prefix}.received` SET `Retries` = @Retries,`Content`= @Content,`ExpiresAt` = @ExpiresAt,`StatusName`=@StatusName WHERE `Id`=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -45,7 +47,8 @@ namespace DotNetCore.CAP.MySql
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"INSERT INTO `{_prefix}.queue` values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Publish }, _dbTransaction);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Publish},
_dbTransaction);
}
public void EnqueueMessage(CapReceivedMessage message)
......@@ -53,7 +56,8 @@ namespace DotNetCore.CAP.MySql
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"INSERT INTO `{_prefix}.queue` values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Subscribe }, _dbTransaction);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Subscribe},
_dbTransaction);
}
public Task CommitAsync()
......
......@@ -9,7 +9,7 @@ namespace DotNetCore.CAP
/// <summary>
/// Gets or sets the schema to use when creating database objects.
/// Default is <see cref="DefaultSchema"/>.
/// Default is <see cref="DefaultSchema" />.
/// </summary>
public string Schema { get; set; } = DefaultSchema;
......
......@@ -9,10 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static CapOptions UsePostgreSql(this CapOptions options, string connectionString)
{
return options.UsePostgreSql(opt =>
{
opt.ConnectionString = connectionString;
});
return options.UsePostgreSql(opt => { opt.ConnectionString = connectionString; });
}
public static CapOptions UsePostgreSql(this CapOptions options, Action<PostgreSqlOptions> configure)
......@@ -27,10 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static CapOptions UseEntityFramework<TContext>(this CapOptions options)
where TContext : DbContext
{
return options.UseEntityFramework<TContext>(opt =>
{
opt.DbContextType = typeof(TContext);
});
return options.UseEntityFramework<TContext>(opt => { opt.DbContextType = typeof(TContext); });
}
public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> configure)
......@@ -38,7 +32,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
var efOptions = new EFOptions { DbContextType = typeof(TContext) };
var efOptions = new EFOptions {DbContextType = typeof(TContext)};
configure(efOptions);
options.RegisterExtension(new PostgreSqlCapOptionsExtension(configure));
......
......@@ -18,17 +18,17 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, PostgreSqlStorage>();
services.AddScoped<IStorageConnection, PostgreSqlStorageConnection>();
services.AddSingleton<IStorageConnection, PostgreSqlStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddTransient<ICallbackPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();
services.AddTransient<IAdditionalProcessor, DefaultAdditionalProcessor>();
var postgreSqlOptions = new PostgreSqlOptions();
_configure(postgreSqlOptions);
if (postgreSqlOptions.DbContextType != null)
{
services.AddSingleton(x =>
{
using (var scope = x.CreateScope())
......@@ -39,11 +39,8 @@ namespace DotNetCore.CAP
return postgreSqlOptions;
}
});
}
else
{
services.AddSingleton(postgreSqlOptions);
}
}
}
}
\ No newline at end of file
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class PostgreSqlOptions : EFOptions
......
......@@ -13,9 +13,9 @@ namespace DotNetCore.CAP.PostgreSql
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly ILogger _logger;
private readonly PostgreSqlOptions _options;
private readonly DbContext _dbContext;
public CapPublisher(IServiceProvider provider,
ILogger<CapPublisher> logger,
......@@ -28,7 +28,15 @@ namespace DotNetCore.CAP.PostgreSql
if (_options.DbContextType != null)
{
IsUsingEF = true;
_dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType);
_dbContext = (DbContext) ServiceProvider.GetService(_options.DbContextType);
}
}
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new NpgsqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
}
......@@ -45,36 +53,33 @@ namespace DotNetCore.CAP.PostgreSql
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}
DbTranasaction = dbTrans;
DbTransaction = dbTrans;
}
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
dbConnection.Execute(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
}
protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
}
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new NpgsqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
return Task.CompletedTask;
}
#region private methods
private string PrepareSql()
{
return $"INSERT INTO \"{_options.Schema}\".\"published\" (\"Name\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
return
$"INSERT INTO \"{_options.Schema}\".\"published\" (\"Name\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
}
#endregion private methods
......
......@@ -9,26 +9,22 @@ namespace DotNetCore.CAP.PostgreSql
{
internal class DefaultAdditionalProcessor : IAdditionalProcessor
{
private readonly IServiceProvider _provider;
private readonly ILogger _logger;
private readonly PostgreSqlOptions _options;
private const int MaxBatch = 1000;
private readonly TimeSpan _delay = TimeSpan.FromSeconds(1);
private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5);
private static readonly string[] Tables =
{
"published","received"
"published", "received"
};
public DefaultAdditionalProcessor(
IServiceProvider provider,
ILogger<DefaultAdditionalProcessor> logger,
private readonly TimeSpan _delay = TimeSpan.FromSeconds(1);
private readonly ILogger _logger;
private readonly PostgreSqlOptions _options;
private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5);
public DefaultAdditionalProcessor(ILogger<DefaultAdditionalProcessor> logger,
PostgreSqlOptions sqlServerOptions)
{
_logger = logger;
_provider = provider;
_options = sqlServerOptions;
}
......@@ -43,8 +39,9 @@ namespace DotNetCore.CAP.PostgreSql
{
using (var connection = new NpgsqlConnection(_options.ConnectionString))
{
removedCount = await connection.ExecuteAsync($"DELETE FROM \"{_options.Schema}\".\"{table}\" WHERE \"ExpiresAt\" < @now AND \"Id\" IN (SELECT \"Id\" FROM \"{_options.Schema}\".\"{table}\" LIMIT @count);",
new { now = DateTime.Now, count = MaxBatch });
removedCount = await connection.ExecuteAsync(
$"DELETE FROM \"{_options.Schema}\".\"{table}\" WHERE \"ExpiresAt\" < @now AND \"Id\" IN (SELECT \"Id\" FROM \"{_options.Schema}\".\"{table}\" LIMIT @count);",
new {now = DateTime.Now, count = MaxBatch});
}
if (removedCount != 0)
......
......@@ -8,11 +8,11 @@ namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlFetchedMessage : IFetchedMessage
{
private readonly IDbConnection _connection;
private readonly IDbTransaction _transaction;
private readonly Timer _timer;
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromMinutes(1);
private readonly IDbConnection _connection;
private readonly object _lockObject = new object();
private readonly Timer _timer;
private readonly IDbTransaction _transaction;
public PostgreSqlFetchedMessage(int messageId,
MessageType type,
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.Monitoring;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlMonitoringApi : IMonitoringApi
{
private readonly PostgreSqlOptions _options;
private readonly PostgreSqlStorage _storage;
public PostgreSqlMonitoringApi(IStorage storage, PostgreSqlOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_storage = storage as PostgreSqlStorage ?? throw new ArgumentNullException(nameof(storage));
}
public StatisticsDto GetStatistics()
{
var sql = string.Format(@"
select count(""Id"") from ""{0}"".""published"" where ""StatusName"" = N'Succeeded';
select count(""Id"") from ""{0}"".""received"" where ""StatusName"" = N'Succeeded';
select count(""Id"") from ""{0}"".""published"" where ""StatusName"" = N'Failed';
select count(""Id"") from ""{0}"".""received"" where ""StatusName"" = N'Failed';
select count(""Id"") from ""{0}"".""published"" where ""StatusName"" in (N'Processing',N'Scheduled',N'Enqueued');
select count(""Id"") from ""{0}"".""received"" where ""StatusName"" in (N'Processing',N'Scheduled',N'Enqueued');",
_options.Schema);
var statistics = UseConnection(connection =>
{
var stats = new StatisticsDto();
using (var multi = connection.QueryMultiple(sql))
{
stats.PublishedSucceeded = multi.ReadSingle<int>();
stats.ReceivedSucceeded = multi.ReadSingle<int>();
stats.PublishedFailed = multi.ReadSingle<int>();
stats.ReceivedFailed = multi.ReadSingle<int>();
stats.PublishedProcessing = multi.ReadSingle<int>();
stats.ReceivedProcessing = multi.ReadSingle<int>();
}
return stats;
});
return statistics;
}
public IList<MessageDto> Messages(MessageQueryDto queryDto)
{
var tableName = queryDto.MessageType == MessageType.Publish ? "published" : "received";
var where = string.Empty;
if (!string.IsNullOrEmpty(queryDto.StatusName))
if (string.Equals(queryDto.StatusName, StatusName.Processing,
StringComparison.CurrentCultureIgnoreCase))
where += " and \"StatusName\" in (N'Processing',N'Scheduled',N'Enqueued')";
else
where += " and Lower(\"StatusName\") = Lower(@StatusName)";
if (!string.IsNullOrEmpty(queryDto.Name))
where += " and Lower(\"Name\") = Lower(@Name)";
if (!string.IsNullOrEmpty(queryDto.Group))
where += " and Lower(\"Group\") = Lower(@Group)";
if (!string.IsNullOrEmpty(queryDto.Content))
where += " and \"Content\" ILike '%@Content%'";
var sqlQuery =
$"select * from \"{_options.Schema}\".\"{tableName}\" where 1=1 {where} order by \"Added\" desc offset @Offset limit @Limit";
return UseConnection(conn => conn.Query<MessageDto>(sqlQuery, new
{
queryDto.StatusName,
queryDto.Group,
queryDto.Name,
queryDto.Content,
Offset = queryDto.CurrentPage * queryDto.PageSize,
Limit = queryDto.PageSize
}).ToList());
}
public int PublishedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Failed));
}
public int PublishedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Processing));
}
public int PublishedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "published", StatusName.Succeeded));
}
public int ReceivedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Failed));
}
public int ReceivedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Processing));
}
public int ReceivedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "received", StatusName.Succeeded));
}
public IDictionary<DateTime, int> HourlySucceededJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "published" : "received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Succeeded));
}
public IDictionary<DateTime, int> HourlyFailedJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "published" : "received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Failed));
}
private int GetNumberOfMessage(IDbConnection connection, string tableName, string statusName)
{
var sqlQuery = statusName == StatusName.Processing
? $"select count(\"Id\") from \"{_options.Schema}\".\"{tableName}\" where \"StatusName\" in (N'Processing',N'Scheduled',N'Enqueued')"
: $"select count(\"Id\") from \"{_options.Schema}\".\"{tableName}\" where Lower(\"StatusName\") = Lower(@state)";
var count = connection.ExecuteScalar<int>(sqlQuery, new { state = statusName });
return count;
}
private T UseConnection<T>(Func<IDbConnection, T> action)
{
return _storage.UseConnection(action);
}
private Dictionary<DateTime, int> GetHourlyTimelineStats(IDbConnection connection, string tableName,
string statusName)
{
var endDate = DateTime.Now;
var dates = new List<DateTime>();
for (var i = 0; i < 24; i++)
{
dates.Add(endDate);
endDate = endDate.AddHours(-1);
}
var keyMaps = dates.ToDictionary(x => x.ToString("yyyy-MM-dd-HH"), x => x);
return GetTimelineStats(connection, tableName, statusName, keyMaps);
}
private Dictionary<DateTime, int> GetTimelineStats(
IDbConnection connection,
string tableName,
string statusName,
IDictionary<string, DateTime> keyMaps)
{
var sqlQuery =
$@"
with aggr as (
select to_char(""Added"",'yyyy-MM-dd-HH') as ""Key"",
count(""Id"") as ""Count""
from ""{_options.Schema}"".""{tableName}""
where ""StatusName"" = @statusName
group by to_char(""Added"", 'yyyy-MM-dd-HH')
)
select ""Key"",""Count"" from aggr where ""Key""= Any(@keys);";
var valuesMap = connection.Query(sqlQuery,new { keys = keyMaps.Keys.ToList(), statusName })
.ToList()
.ToDictionary(x => (string)x.Key, x => (int)x.Count);
foreach (var key in keyMaps.Keys)
if (!valuesMap.ContainsKey(key)) valuesMap.Add(key, 0);
var result = new Dictionary<DateTime, int>();
for (var i = 0; i < keyMaps.Count; i++)
{
var value = valuesMap[keyMaps.ElementAt(i).Key];
result.Add(keyMaps.ElementAt(i).Value, value);
}
return result;
}
}
}
\ No newline at end of file
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Microsoft.EntityFrameworkCore;
using DotNetCore.CAP.Dashboard;
using Microsoft.Extensions.Logging;
using Npgsql;
......@@ -9,13 +11,28 @@ namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlStorage : IStorage
{
private readonly PostgreSqlOptions _options;
private readonly IDbConnection _existingConnection = null;
private readonly ILogger _logger;
private readonly CapOptions _capOptions;
private readonly PostgreSqlOptions _options;
public PostgreSqlStorage(ILogger<PostgreSqlStorage> logger, PostgreSqlOptions options)
public PostgreSqlStorage(ILogger<PostgreSqlStorage> logger,
CapOptions capOptions,
PostgreSqlOptions options)
{
_options = options;
_logger = logger;
_capOptions = capOptions;
}
public IStorageConnection GetConnection()
{
return new PostgreSqlStorageConnection(_options, _capOptions);
}
public IMonitoringApi GetMonitoringApi()
{
return new PostgreSqlMonitoringApi(this, _options);
}
public async Task InitializeAsync(CancellationToken cancellationToken)
......@@ -31,6 +48,42 @@ namespace DotNetCore.CAP.PostgreSql
_logger.LogDebug("Ensuring all create database tables script are applied.");
}
internal T UseConnection<T>(Func<IDbConnection, T> func)
{
IDbConnection connection = null;
try
{
connection = CreateAndOpenConnection();
return func(connection);
}
finally
{
ReleaseConnection(connection);
}
}
internal IDbConnection CreateAndOpenConnection()
{
var connection = _existingConnection ?? new NpgsqlConnection(_options.ConnectionString);
if (connection.State == ConnectionState.Closed)
connection.Open();
return connection;
}
internal bool IsExistingConnection(IDbConnection connection)
{
return connection != null && ReferenceEquals(connection, _existingConnection);
}
internal void ReleaseConnection(IDbConnection connection)
{
if (connection != null && !IsExistingConnection(connection))
connection.Dispose();
}
protected virtual string CreateDbTablesScript(string schema)
{
var batchSql = $@"
......
......@@ -11,14 +11,15 @@ namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlStorageConnection : IStorageConnection
{
private readonly PostgreSqlOptions _options;
private readonly CapOptions _capOptions;
public PostgreSqlStorageConnection(PostgreSqlOptions options)
public PostgreSqlStorageConnection(PostgreSqlOptions options, CapOptions capOptions)
{
_options = options;
_capOptions = capOptions;
Options = options;
}
public PostgreSqlOptions Options => _options;
public PostgreSqlOptions Options { get; }
public IStorageTransaction CreateTransaction()
{
......@@ -27,9 +28,9 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id)
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"Id\"={id}";
var sql = $"SELECT * FROM \"{Options.Schema}\".\"published\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -37,15 +38,16 @@ namespace DotNetCore.CAP.PostgreSql
public Task<IFetchedMessage> FetchNextMessageAsync()
{
var sql = $@"DELETE FROM ""{_options.Schema}"".""queue"" WHERE ""MessageId"" = (SELECT ""MessageId"" FROM ""{_options.Schema}"".""queue"" FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *;";
var sql = $@"DELETE FROM ""{Options.Schema}"".""queue"" WHERE ""MessageId"" = (SELECT ""MessageId"" FROM ""{Options.Schema}"".""queue"" FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *;";
return FetchNextMessageCoreAsync(sql);
}
public async Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;";
var sql =
$"SELECT * FROM \"{Options.Schema}\".\"published\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -53,23 +55,23 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<IEnumerable<CapPublishedMessage>> GetFailedPublishedMessages()
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;";
var sql =
$"SELECT * FROM \"{Options.Schema}\".\"published\" WHERE \"Retries\"<{_capOptions.FailedRetryCount} AND \"StatusName\"='{StatusName.Failed}' LIMIT 200;";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<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);";
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 NpgsqlConnection(_options.ConnectionString))
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
await connection.ExecuteAsync(sql, message);
}
......@@ -77,26 +79,28 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"Id\"={id}";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
var sql = $"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
}
}
public async Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync()
public async Task<CapReceivedMessage> GetNextReceivedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
var sql =
$"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
}
}
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages()
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceivedMessages()
{
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;";
using (var connection = new NpgsqlConnection(_options.ConnectionString))
var sql =
$"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"Retries\"<{_capOptions.FailedRetryCount} AND \"StatusName\"='{StatusName.Failed}' LIMIT 200;";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<CapReceivedMessage>(sql);
}
......@@ -106,13 +110,35 @@ namespace DotNetCore.CAP.PostgreSql
{
}
public bool ChangePublishedState(int messageId, string state)
{
var sql =
$"UPDATE \"{Options.Schema}\".\"published\" SET \"Retries\"=\"Retries\"+1,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
public bool ChangeReceivedState(int messageId, string state)
{
var sql =
$"UPDATE \"{Options.Schema}\".\"received\" SET \"Retries\"=\"Retries\"+1,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
private async Task<IFetchedMessage> FetchNextMessageCoreAsync(string sql, object args = null)
{
//here don't use `using` to dispose
var connection = new NpgsqlConnection(_options.ConnectionString);
var connection = new NpgsqlConnection(Options.ConnectionString);
await connection.OpenAsync();
var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
FetchedMessage fetchedMessage = null;
FetchedMessage fetchedMessage;
try
{
fetchedMessage = await connection.QueryFirstOrDefaultAsync<FetchedMessage>(sql, args, transaction);
......@@ -131,7 +157,8 @@ namespace DotNetCore.CAP.PostgreSql
return null;
}
return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction);
return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection,
transaction);
}
}
}
\ No newline at end of file
......@@ -7,12 +7,12 @@ using Npgsql;
namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlStorageTransaction : IStorageTransaction, IDisposable
public class PostgreSqlStorageTransaction : IStorageTransaction
{
private readonly string _schema;
private readonly IDbConnection _dbConnection;
private readonly IDbTransaction _dbTransaction;
private readonly IDbConnection _dbConnection;
private readonly string _schema;
public PostgreSqlStorageTransaction(PostgreSqlStorageConnection connection)
{
......@@ -28,7 +28,10 @@ namespace DotNetCore.CAP.PostgreSql
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $@"UPDATE ""{_schema}"".""published"" SET ""Retries""=@Retries,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
var sql =
$@"UPDATE ""{
_schema
}"".""published"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -36,7 +39,10 @@ namespace DotNetCore.CAP.PostgreSql
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $@"UPDATE ""{_schema}"".""received"" SET ""Retries""=@Retries,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
var sql =
$@"UPDATE ""{
_schema
}"".""received"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -45,7 +51,8 @@ namespace DotNetCore.CAP.PostgreSql
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);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Publish},
_dbTransaction);
}
public void EnqueueMessage(CapReceivedMessage message)
......@@ -53,7 +60,8 @@ namespace DotNetCore.CAP.PostgreSql
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);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Subscribe},
_dbTransaction);
}
public Task CommitAsync()
......
......@@ -8,10 +8,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static CapOptions UseRabbitMQ(this CapOptions options, string hostName)
{
return options.UseRabbitMQ(opt =>
{
opt.HostName = hostName;
});
return options.UseRabbitMQ(opt => { opt.HostName = hostName; });
}
public static CapOptions UseRabbitMQ(this CapOptions options, Action<RabbitMQOptions> configure)
......
using System;
// ReSharper disable once CheckNamespace
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class RabbitMQOptions
......
......@@ -16,15 +16,16 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapMessageQueueMakerService>();
var options = new RabbitMQOptions();
_configure?.Invoke(options);
services.AddSingleton(options);
services.AddSingleton<IConsumerClientFactory, RabbitMQConsumerClientFactory>();
services.AddSingleton<ConnectionPool>();
services.AddSingleton<IConnectionChannelPool, ConnectionChannelPool>();
services.AddSingleton<IQueueExecutor, PublishQueueExecutor>();
services.AddSingleton<IPublishExecutor, PublishQueueExecutor>();
}
}
}
\ No newline at end of file
......@@ -2,31 +2,61 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
namespace DotNetCore.CAP.RabbitMQ
{
public class ConnectionPool : IConnectionPool, IDisposable
public class ConnectionChannelPool : IConnectionChannelPool, IDisposable
{
private const int DefaultPoolSize = 15;
private readonly Func<IConnection> _connectionActivator;
private readonly ILogger<ConnectionChannelPool> _logger;
private readonly ConcurrentQueue<IModel> _pool = new ConcurrentQueue<IModel>();
private IConnection _connection;
private readonly ConcurrentQueue<IConnection> _pool = new ConcurrentQueue<IConnection>();
private readonly Func<IConnection> _activator;
private int _maxSize;
private int _count;
private int _maxSize;
public ConnectionPool(RabbitMQOptions options)
public ConnectionChannelPool(ILogger<ConnectionChannelPool> logger,
RabbitMQOptions options)
{
_logger = logger;
_maxSize = DefaultPoolSize;
_activator = CreateActivator(options);
_connectionActivator = CreateConnection(options);
}
IModel IConnectionChannelPool.Rent()
{
return Rent();
}
bool IConnectionChannelPool.Return(IModel connection)
{
return Return(connection);
}
public IConnection GetConnection()
{
if (_connection != null && _connection.IsOpen)
return _connection;
_connection = _connectionActivator();
_connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
return _connection;
}
public void Dispose()
{
_maxSize = 0;
while (_pool.TryDequeue(out var context))
context.Dispose();
}
private static Func<IConnection> CreateActivator(RabbitMQOptions options)
private static Func<IConnection> CreateConnection(RabbitMQOptions options)
{
var factory = new ConnectionFactory()
var factory = new ConnectionFactory
{
HostName = options.HostName,
UserName = options.UserName,
......@@ -41,23 +71,28 @@ namespace DotNetCore.CAP.RabbitMQ
return () => factory.CreateConnection();
}
public virtual IConnection Rent()
private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e)
{
if (_pool.TryDequeue(out IConnection connection))
_logger.LogWarning($"RabbitMQ client connection closed! {e}");
}
public virtual IModel Rent()
{
if (_pool.TryDequeue(out var model))
{
Interlocked.Decrement(ref _count);
Debug.Assert(_count >= 0);
return connection;
return model;
}
connection = _activator();
model = GetConnection().CreateModel();
return connection;
return model;
}
public virtual bool Return(IConnection connection)
public virtual bool Return(IModel connection)
{
if (Interlocked.Increment(ref _count) <= _maxSize)
{
......@@ -72,20 +107,5 @@ namespace DotNetCore.CAP.RabbitMQ
return false;
}
IConnection IConnectionPool.Rent() => Rent();
bool IConnectionPool.Return(IConnection connection) => Return(connection);
public void Dispose()
{
_maxSize = 0;
IConnection context;
while (_pool.TryDequeue(out context))
{
context.Dispose();
}
}
}
}
\ No newline at end of file
using RabbitMQ.Client;
namespace DotNetCore.CAP.RabbitMQ
{
public interface IConnectionChannelPool
{
IConnection GetConnection();
IModel Rent();
bool Return(IModel context);
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Text;
using RabbitMQ.Client;
namespace DotNetCore.CAP.RabbitMQ
{
public interface IConnectionPool
{
IConnection Rent();
bool Return(IConnection context);
}
}
......@@ -9,49 +9,43 @@ namespace DotNetCore.CAP.RabbitMQ
{
internal sealed class PublishQueueExecutor : BasePublishQueueExecutor
{
private readonly IConnectionChannelPool _connectionChannelPool;
private readonly ILogger _logger;
private readonly ConnectionPool _connectionPool;
private readonly RabbitMQOptions _rabbitMQOptions;
public PublishQueueExecutor(
CapOptions options,
IStateChanger stateChanger,
ConnectionPool connectionPool,
RabbitMQOptions rabbitMQOptions,
ILogger<PublishQueueExecutor> logger)
public PublishQueueExecutor(ILogger<PublishQueueExecutor> logger, CapOptions options,
RabbitMQOptions rabbitMQOptions, IConnectionChannelPool connectionChannelPool, IStateChanger stateChanger)
: base(options, stateChanger, logger)
{
_logger = logger;
_connectionPool = connectionPool;
_connectionChannelPool = connectionChannelPool;
_rabbitMQOptions = rabbitMQOptions;
}
public override Task<OperateResult> PublishAsync(string keyName, string content)
{
var connection = _connectionPool.Rent();
var channel = _connectionChannelPool.Rent();
try
{
using (var channel = connection.CreateModel())
{
var body = Encoding.UTF8.GetBytes(content);
channel.ExchangeDeclare(_rabbitMQOptions.TopicExchangeName, RabbitMQOptions.ExchangeType, durable: true);
channel.BasicPublish(exchange: _rabbitMQOptions.TopicExchangeName,
routingKey: keyName,
basicProperties: null,
body: body);
channel.ExchangeDeclare(_rabbitMQOptions.TopicExchangeName, RabbitMQOptions.ExchangeType, true);
channel.BasicPublish(_rabbitMQOptions.TopicExchangeName,
keyName,
null,
body);
_logger.LogDebug($"RabbitMQ topic message [{keyName}] has been published.");
_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}");
_logger.LogError(
$"RabbitMQ topic message [{keyName}] has been raised an exception of sending. the exception is: {ex.Message}");
return Task.FromResult(OperateResult.Failed(ex,
new OperateError()
new OperateError
{
Code = ex.HResult.ToString(),
Description = ex.Message
......@@ -59,7 +53,9 @@ namespace DotNetCore.CAP.RabbitMQ
}
finally
{
_connectionPool.Return(connection);
var returned = _connectionChannelPool.Return(channel);
if (!returned)
channel.Dispose();
}
}
}
......
......@@ -10,60 +10,37 @@ namespace DotNetCore.CAP.RabbitMQ
{
internal sealed class RabbitMQConsumerClient : IConsumerClient
{
private readonly IConnectionChannelPool _connectionChannelPool;
private readonly string _exchageName;
private readonly string _queueName;
private readonly RabbitMQOptions _rabbitMQOptions;
private ConnectionPool _connectionPool;
private IModel _channel;
private ulong _deliveryTag;
public event EventHandler<MessageContext> OnMessageReceieved;
public event EventHandler<string> OnError;
public RabbitMQConsumerClient(string queueName,
ConnectionPool connectionPool,
IConnectionChannelPool connectionChannelPool,
RabbitMQOptions options)
{
_queueName = queueName;
_connectionPool = connectionPool;
_connectionChannelPool = connectionChannelPool;
_rabbitMQOptions = options;
_exchageName = options.TopicExchangeName;
InitClient();
}
private void InitClient()
{
var connection = _connectionPool.Rent();
_channel = connection.CreateModel();
public event EventHandler<MessageContext> OnMessageReceived;
_channel.ExchangeDeclare(
exchange: _exchageName,
type: RabbitMQOptions.ExchangeType,
durable: true);
var arguments = new Dictionary<string, object> { { "x-message-ttl", (int)_rabbitMQOptions.QueueMessageExpires } };
_channel.QueueDeclare(_queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: arguments);
_connectionPool.Return(connection);
}
public event EventHandler<string> OnError;
public void Subscribe(IEnumerable<string> topics)
{
if (topics == null) throw new ArgumentNullException(nameof(topics));
foreach (var topic in topics)
{
_channel.QueueBind(_queueName, _exchageName, topic);
}
}
public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
{
......@@ -72,21 +49,41 @@ namespace DotNetCore.CAP.RabbitMQ
consumer.Shutdown += OnConsumerShutdown;
_channel.BasicConsume(_queueName, false, consumer);
while (true)
{
Task.Delay(timeout, cancellationToken).GetAwaiter().GetResult();
}
}
public void Commit()
{
_channel.BasicAck(_deliveryTag, false);
}
public void Reject()
{
_channel.BasicReject(_deliveryTag, true);
}
public void Dispose()
{
_channel.Dispose();
}
private void InitClient()
{
var connection = _connectionChannelPool.GetConnection();
_channel = connection.CreateModel();
_channel.ExchangeDeclare(
_exchageName,
RabbitMQOptions.ExchangeType,
true);
var arguments = new Dictionary<string, object> {
{ "x-message-ttl", _rabbitMQOptions.QueueMessageExpires }
};
_channel.QueueDeclare(_queueName, true, false, false, arguments);
}
private void OnConsumerReceived(object sender, BasicDeliverEventArgs e)
{
_deliveryTag = e.DeliveryTag;
......@@ -96,7 +93,7 @@ namespace DotNetCore.CAP.RabbitMQ
Name = e.RoutingKey,
Content = Encoding.UTF8.GetString(e.Body)
};
OnMessageReceieved?.Invoke(sender, message);
OnMessageReceived?.Invoke(sender, message);
}
private void OnConsumerShutdown(object sender, ShutdownEventArgs e)
......
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
namespace DotNetCore.CAP.RabbitMQ
namespace DotNetCore.CAP.RabbitMQ
{
internal sealed class RabbitMQConsumerClientFactory : IConsumerClientFactory
{
private readonly IConnectionChannelPool _connectionChannelPool;
private readonly RabbitMQOptions _rabbitMQOptions;
private readonly ConnectionPool _connectionPool;
public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions, ConnectionPool pool)
public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions, IConnectionChannelPool channelPool)
{
_rabbitMQOptions = rabbitMQOptions;
_connectionPool = pool;
_connectionChannelPool = channelPool;
}
public IConsumerClient Create(string groupId)
{
return new RabbitMQConsumerClient(groupId, _connectionPool, _rabbitMQOptions);
return new RabbitMQConsumerClient(groupId, _connectionChannelPool, _rabbitMQOptions);
}
}
}
\ No newline at end of file
......@@ -9,7 +9,7 @@ namespace DotNetCore.CAP
/// <summary>
/// Gets or sets the schema to use when creating database objects.
/// Default is <see cref="DefaultSchema"/>.
/// Default is <see cref="DefaultSchema" />.
/// </summary>
public string Schema { get; set; } = DefaultSchema;
......
......@@ -9,10 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static CapOptions UseSqlServer(this CapOptions options, string connectionString)
{
return options.UseSqlServer(opt =>
{
opt.ConnectionString = connectionString;
});
return options.UseSqlServer(opt => { opt.ConnectionString = connectionString; });
}
public static CapOptions UseSqlServer(this CapOptions options, Action<SqlServerOptions> configure)
......@@ -27,10 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static CapOptions UseEntityFramework<TContext>(this CapOptions options)
where TContext : DbContext
{
return options.UseEntityFramework<TContext>(opt =>
{
opt.DbContextType = typeof(TContext);
});
return options.UseEntityFramework<TContext>(opt => { opt.DbContextType = typeof(TContext); });
}
public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> configure)
......@@ -38,7 +32,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
if (configure == null) throw new ArgumentNullException(nameof(configure));
var efOptions = new EFOptions { DbContextType = typeof(TContext) };
var efOptions = new EFOptions {DbContextType = typeof(TContext)};
configure(efOptions);
options.RegisterExtension(new SqlServerCapOptionsExtension(configure));
......
......@@ -18,10 +18,11 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, SqlServerStorage>();
services.AddSingleton<IStorageConnection, SqlServerStorageConnection>();
services.AddTransient<ICapPublisher, CapPublisher>();
services.AddTransient<ICallbackPublisher, CapPublisher>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();
services.AddTransient<IAdditionalProcessor, DefaultAdditionalProcessor>();
AddSqlServerOptions(services);
}
......@@ -33,22 +34,18 @@ namespace DotNetCore.CAP
_configure(sqlServerOptions);
if (sqlServerOptions.DbContextType != null)
{
services.AddSingleton(x =>
{
using (var scope = x.CreateScope())
{
var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(sqlServerOptions.DbContextType);
var dbContext = (DbContext) provider.GetService(sqlServerOptions.DbContextType);
sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return sqlServerOptions;
}
});
}
else
{
services.AddSingleton(sqlServerOptions);
}
}
}
}
\ No newline at end of file
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class SqlServerOptions : EFOptions
......
......@@ -13,9 +13,9 @@ namespace DotNetCore.CAP.SqlServer
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly ILogger _logger;
private readonly SqlServerOptions _options;
private readonly DbContext _dbContext;
public CapPublisher(IServiceProvider provider,
ILogger<CapPublisher> logger,
......@@ -25,11 +25,18 @@ namespace DotNetCore.CAP.SqlServer
_logger = logger;
_options = options;
if (_options.DbContextType != null)
{
if (_options.DbContextType == null) return;
IsUsingEF = true;
_dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType);
}
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new SqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
}
protected override void PrepareConnectionForEF()
......@@ -45,36 +52,33 @@ namespace DotNetCore.CAP.SqlServer
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}
DbTranasaction = dbTrans;
DbTransaction = dbTrans;
}
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
dbConnection.Execute(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
}
protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message)
protected override Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction);
_logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString());
}
_logger.LogInformation("Published Message has been persisted in the database. name:" + message);
public async Task PublishAsync(CapPublishedMessage message)
{
using (var conn = new SqlConnection(_options.ConnectionString))
{
await conn.ExecuteAsync(PrepareSql(), message);
}
return Task.CompletedTask;
}
#region private methods
private string PrepareSql()
{
return $"INSERT INTO {_options.Schema}.[Published] ([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName])VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
return
$"INSERT INTO {_options.Schema}.[Published] ([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName])VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)";
}
#endregion private methods
......
......@@ -9,26 +9,22 @@ 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.FromMinutes(5);
private static readonly string[] Tables =
{
"Published","Received"
"Published", "Received"
};
public DefaultAdditionalProcessor(
IServiceProvider provider,
ILogger<DefaultAdditionalProcessor> logger,
private readonly TimeSpan _delay = TimeSpan.FromSeconds(1);
private readonly ILogger _logger;
private readonly SqlServerOptions _options;
private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5);
public DefaultAdditionalProcessor(ILogger<DefaultAdditionalProcessor> logger,
SqlServerOptions sqlServerOptions)
{
_logger = logger;
_provider = provider;
_options = sqlServerOptions;
}
......@@ -38,7 +34,7 @@ namespace DotNetCore.CAP.SqlServer
foreach (var table in Tables)
{
var removedCount = 0;
int removedCount;
do
{
using (var connection = new SqlConnection(_options.ConnectionString))
......@@ -46,7 +42,7 @@ namespace DotNetCore.CAP.SqlServer
removedCount = await connection.ExecuteAsync($@"
DELETE TOP (@count)
FROM [{_options.Schema}].[{table}] WITH (readpast)
WHERE ExpiresAt < @now;", new { now = DateTime.Now, count = MaxBatch });
WHERE ExpiresAt < @now;", new {now = DateTime.Now, count = MaxBatch});
}
if (removedCount != 0)
......
......@@ -8,11 +8,11 @@ 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 IDbConnection _connection;
private readonly object _lockObject = new object();
private readonly Timer _timer;
private readonly IDbTransaction _transaction;
public SqlServerFetchedMessage(int messageId,
MessageType type,
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.Monitoring;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.SqlServer
{
internal class SqlServerMonitoringApi : IMonitoringApi
{
private readonly SqlServerOptions _options;
private readonly SqlServerStorage _storage;
public SqlServerMonitoringApi(IStorage storage, SqlServerOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_storage = storage as SqlServerStorage ?? throw new ArgumentNullException(nameof(storage));
}
public StatisticsDto GetStatistics()
{
var sql = string.Format(@"
set transaction isolation level read committed;
select count(Id) from [{0}].Published with (nolock) where StatusName = N'Succeeded';
select count(Id) from [{0}].Received with (nolock) where StatusName = N'Succeeded';
select count(Id) from [{0}].Published with (nolock) where StatusName = N'Failed';
select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
select count(Id) from [{0}].Published with (nolock) where StatusName in (N'Processing',N'Scheduled',N'Enqueued');
select count(Id) from [{0}].Received with (nolock) where StatusName in (N'Processing',N'Scheduled',N'Enqueued');",
_options.Schema);
var statistics = UseConnection(connection =>
{
var stats = new StatisticsDto();
using (var multi = connection.QueryMultiple(sql))
{
stats.PublishedSucceeded = multi.ReadSingle<int>();
stats.ReceivedSucceeded = multi.ReadSingle<int>();
stats.PublishedFailed = multi.ReadSingle<int>();
stats.ReceivedFailed = multi.ReadSingle<int>();
stats.PublishedProcessing = multi.ReadSingle<int>();
stats.ReceivedProcessing = multi.ReadSingle<int>();
}
return stats;
});
return statistics;
}
public IDictionary<DateTime, int> HourlyFailedJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "Published" : "Received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Failed));
}
public IDictionary<DateTime, int> HourlySucceededJobs(MessageType type)
{
var tableName = type == MessageType.Publish ? "Published" : "Received";
return UseConnection(connection =>
GetHourlyTimelineStats(connection, tableName, StatusName.Succeeded));
}
public IList<MessageDto> Messages(MessageQueryDto queryDto)
{
var tableName = queryDto.MessageType == MessageType.Publish ? "Published" : "Received";
var where = string.Empty;
if (!string.IsNullOrEmpty(queryDto.StatusName))
if (string.Equals(queryDto.StatusName, StatusName.Processing,
StringComparison.CurrentCultureIgnoreCase))
where += " and statusname in (N'Processing',N'Scheduled',N'Enqueued')";
else
where += " and statusname=@StatusName";
if (!string.IsNullOrEmpty(queryDto.Name))
where += " and name=@Name";
if (!string.IsNullOrEmpty(queryDto.Group))
where += " and group=@Group";
if (!string.IsNullOrEmpty(queryDto.Content))
where += " and content like '%@Content%'";
var sqlQuery =
$"select * from [{_options.Schema}].{tableName} where 1=1 {where} order by Added desc offset @Offset rows fetch next @Limit rows only";
return UseConnection(conn => conn.Query<MessageDto>(sqlQuery, new
{
queryDto.StatusName,
queryDto.Group,
queryDto.Name,
queryDto.Content,
Offset = queryDto.CurrentPage * queryDto.PageSize,
Limit = queryDto.PageSize
}).ToList());
}
public int PublishedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Published", StatusName.Failed));
}
public int PublishedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Published", StatusName.Processing));
}
public int PublishedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Published", StatusName.Succeeded));
}
public int ReceivedFailedCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Received", StatusName.Failed));
}
public int ReceivedProcessingCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Received", StatusName.Processing));
}
public int ReceivedSucceededCount()
{
return UseConnection(conn => GetNumberOfMessage(conn, "Received", StatusName.Succeeded));
}
private int GetNumberOfMessage(IDbConnection connection, string tableName, string statusName)
{
var sqlQuery = statusName == StatusName.Processing
? $"select count(Id) from [{_options.Schema}].{tableName} with (nolock) where StatusName in (N'Processing',N'Scheduled',N'Enqueued')"
: $"select count(Id) from [{_options.Schema}].{tableName} with (nolock) where StatusName = @state";
var count = connection.ExecuteScalar<int>(sqlQuery, new {state = statusName});
return count;
}
private T UseConnection<T>(Func<IDbConnection, T> action)
{
return _storage.UseConnection(action);
}
private Dictionary<DateTime, int> GetHourlyTimelineStats(IDbConnection connection, string tableName,
string statusName)
{
var endDate = DateTime.Now;
var dates = new List<DateTime>();
for (var i = 0; i < 24; i++)
{
dates.Add(endDate);
endDate = endDate.AddHours(-1);
}
var keyMaps = dates.ToDictionary(x => x.ToString("yyyy-MM-dd-HH"), x => x);
return GetTimelineStats(connection, tableName, statusName, keyMaps);
}
private Dictionary<DateTime, int> GetTimelineStats(
IDbConnection connection,
string tableName,
string statusName,
IDictionary<string, DateTime> keyMaps)
{
//SQL Server 2012+
var sqlQuery =
$@"
with aggr as (
select FORMAT(Added,'yyyy-MM-dd-HH') as [Key],
count(id) [Count]
from [{_options.Schema}].{tableName}
where StatusName = @statusName
group by FORMAT(Added,'yyyy-MM-dd-HH')
)
select [Key], [Count] from aggr with (nolock) where [Key] in @keys;";
var valuesMap = connection.Query(
sqlQuery,
new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => (string) x.Key, x => (int) x.Count);
foreach (var key in keyMaps.Keys)
if (!valuesMap.ContainsKey(key)) valuesMap.Add(key, 0);
var result = new Dictionary<DateTime, int>();
for (var i = 0; i < keyMaps.Count; i++)
{
var value = valuesMap[keyMaps.ElementAt(i).Key];
result.Add(keyMaps.ElementAt(i).Value, value);
}
return result;
}
}
}
\ No newline at end of file
using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Dashboard;
using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP.SqlServer
{
public class SqlServerStorage : IStorage
{
private readonly SqlServerOptions _options;
private readonly IDbConnection _existingConnection = null;
private readonly ILogger _logger;
private readonly CapOptions _capOptions;
private readonly SqlServerOptions _options;
public SqlServerStorage(ILogger<SqlServerStorage> logger, SqlServerOptions options)
public SqlServerStorage(ILogger<SqlServerStorage> logger,
CapOptions capOptions,
SqlServerOptions options)
{
_options = options;
_logger = logger;
_capOptions = capOptions;
}
public IStorageConnection GetConnection()
{
return new SqlServerStorageConnection(_options, _capOptions);
}
public IMonitoringApi GetMonitoringApi()
{
return new SqlServerMonitoringApi(this, _options);
}
public async Task InitializeAsync(CancellationToken cancellationToken)
......@@ -83,5 +101,41 @@ CREATE TABLE [{schema}].[Published](
END;";
return batchSql;
}
internal T UseConnection<T>(Func<IDbConnection, T> func)
{
IDbConnection connection = null;
try
{
connection = CreateAndOpenConnection();
return func(connection);
}
finally
{
ReleaseConnection(connection);
}
}
internal IDbConnection CreateAndOpenConnection()
{
var connection = _existingConnection ?? new SqlConnection(_options.ConnectionString);
if (connection.State == ConnectionState.Closed)
connection.Open();
return connection;
}
internal bool IsExistingConnection(IDbConnection connection)
{
return connection != null && ReferenceEquals(connection, _existingConnection);
}
internal void ReleaseConnection(IDbConnection connection)
{
if (connection != null && !IsExistingConnection(connection))
connection.Dispose();
}
}
}
\ No newline at end of file
......@@ -11,14 +11,15 @@ namespace DotNetCore.CAP.SqlServer
{
public class SqlServerStorageConnection : IStorageConnection
{
private readonly SqlServerOptions _options;
private readonly CapOptions _capOptions;
public SqlServerStorageConnection(SqlServerOptions options)
public SqlServerStorageConnection(SqlServerOptions options, CapOptions capOptions)
{
_options = options;
_capOptions = capOptions;
Options = options;
}
public SqlServerOptions Options => _options;
public SqlServerOptions Options { get; }
public IStorageTransaction CreateTransaction()
{
......@@ -27,9 +28,9 @@ namespace DotNetCore.CAP.SqlServer
public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id)
{
var sql = $@"SELECT * FROM [{_options.Schema}].[Published] WITH (readpast) WHERE Id={id}";
var sql = $@"SELECT * FROM [{Options.Schema}].[Published] WITH (readpast) WHERE Id={id}";
using (var connection = new SqlConnection(_options.ConnectionString))
using (var connection = new SqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -39,7 +40,7 @@ namespace DotNetCore.CAP.SqlServer
{
var sql = $@"
DELETE TOP (1)
FROM [{_options.Schema}].[Queue] WITH (readpast, updlock, rowlock)
FROM [{Options.Schema}].[Queue] WITH (readpast, updlock, rowlock)
OUTPUT DELETED.MessageId,DELETED.[MessageType];";
return FetchNextMessageCoreAsync(sql);
......@@ -47,9 +48,10 @@ OUTPUT DELETED.MessageId,DELETED.[MessageType];";
public async Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT TOP (1) * FROM [{_options.Schema}].[Published] WITH (readpast) WHERE StatusName = '{StatusName.Scheduled}'";
var sql =
$"SELECT TOP (1) * FROM [{Options.Schema}].[Published] WITH (readpast) WHERE StatusName = '{StatusName.Scheduled}'";
using (var connection = new SqlConnection(_options.ConnectionString))
using (var connection = new SqlConnection(Options.ConnectionString))
{
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
}
......@@ -57,25 +59,37 @@ OUTPUT DELETED.MessageId,DELETED.[MessageType];";
public async Task<IEnumerable<CapPublishedMessage>> GetFailedPublishedMessages()
{
var sql = $"SELECT * FROM [{_options.Schema}].[Published] WITH (readpast) WHERE StatusName = '{StatusName.Failed}'";
var sql =
$"SELECT TOP (200) * FROM [{Options.Schema}].[Published] WITH (readpast) WHERE Retries<{_capOptions.FailedRetryCount} AND StatusName = '{StatusName.Failed}'";
using (var connection = new SqlConnection(_options.ConnectionString))
using (var connection = new SqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<CapPublishedMessage>(sql);
}
}
// CapReceviedMessage
public bool ChangePublishedState(int messageId, string state)
{
var sql =
$"UPDATE [{Options.Schema}].[Published] SET Retries=Retries+1,StatusName = '{state}' WHERE Id={messageId}";
using (var connection = new SqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
// CapReceivedMessage
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])
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))
using (var connection = new SqlConnection(Options.ConnectionString))
{
await connection.ExecuteAsync(sql, message);
}
......@@ -83,31 +97,44 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
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))
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()
public async Task<CapReceivedMessage> GetNextReceivedMessageToBeEnqueuedAsync()
{
var sql = $"SELECT TOP (1) * FROM [{_options.Schema}].[Received] WITH (readpast) WHERE StatusName = '{StatusName.Scheduled}'";
using (var connection = new SqlConnection(_options.ConnectionString))
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 async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages()
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceivedMessages()
{
var sql = $"SELECT * FROM [{_options.Schema}].[Received] WITH (readpast) WHERE StatusName = '{StatusName.Failed}'";
using (var connection = new SqlConnection(_options.ConnectionString))
var sql =
$"SELECT TOP (200) * FROM [{Options.Schema}].[Received] WITH (readpast) WHERE Retries<{_capOptions.FailedRetryCount} AND StatusName = '{StatusName.Failed}'";
using (var connection = new SqlConnection(Options.ConnectionString))
{
return await connection.QueryAsync<CapReceivedMessage>(sql);
}
}
public bool ChangeReceivedState(int messageId, string state)
{
var sql =
$"UPDATE [{Options.Schema}].[Received] SET Retries=Retries+1,StatusName = '{state}' WHERE Id={messageId}";
using (var connection = new SqlConnection(Options.ConnectionString))
{
return connection.Execute(sql) > 0;
}
}
public void Dispose()
{
}
......@@ -115,10 +142,10 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
private async Task<IFetchedMessage> FetchNextMessageCoreAsync(string sql, object args = null)
{
//here don't use `using` to dispose
var connection = new SqlConnection(_options.ConnectionString);
var connection = new SqlConnection(Options.ConnectionString);
await connection.OpenAsync();
var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
FetchedMessage fetchedMessage = null;
FetchedMessage fetchedMessage;
try
{
fetchedMessage = await connection.QueryFirstOrDefaultAsync<FetchedMessage>(sql, args, transaction);
......@@ -137,7 +164,8 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
return null;
}
return new SqlServerFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction);
return new SqlServerFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection,
transaction);
}
}
}
\ No newline at end of file
......@@ -7,12 +7,12 @@ using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.SqlServer
{
public class SqlServerStorageTransaction : IStorageTransaction, IDisposable
public class SqlServerStorageTransaction : IStorageTransaction
{
private readonly string _schema;
private readonly IDbConnection _dbConnection;
private readonly IDbTransaction _dbTransaction;
private readonly IDbConnection _dbConnection;
private readonly string _schema;
public SqlServerStorageTransaction(SqlServerStorageConnection connection)
{
......@@ -28,7 +28,8 @@ namespace DotNetCore.CAP.SqlServer
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"UPDATE [{_schema}].[Published] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
var sql =
$"UPDATE [{_schema}].[Published] SET [Retries] = @Retries,[Content] = @Content,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -36,7 +37,8 @@ namespace DotNetCore.CAP.SqlServer
{
if (message == null) throw new ArgumentNullException(nameof(message));
var sql = $"UPDATE [{_schema}].[Received] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
var sql =
$"UPDATE [{_schema}].[Received] SET [Retries] = @Retries,[Content] = @Content,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}
......@@ -45,7 +47,8 @@ namespace DotNetCore.CAP.SqlServer
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);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Publish},
_dbTransaction);
}
public void EnqueueMessage(CapReceivedMessage message)
......@@ -53,7 +56,8 @@ namespace DotNetCore.CAP.SqlServer
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);
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Subscribe},
_dbTransaction);
}
public Task CommitAsync()
......
......@@ -10,7 +10,7 @@ namespace DotNetCore.CAP.Abstractions
public abstract class CapPublisherBase : ICapPublisher, IDisposable
{
protected IDbConnection DbConnection { get; set; }
protected IDbTransaction DbTranasaction { get; set; }
protected IDbTransaction DbTransaction { get; set; }
protected bool IsCapOpenedTrans { get; set; }
protected bool IsCapOpenedConn { get; set; }
protected bool IsUsingEF { get; set; }
......@@ -36,22 +36,20 @@ namespace DotNetCore.CAP.Abstractions
return PublishWithTransAsync(name, content);
}
public void Publish<T>(string name, T contentObj, IDbConnection dbConnection,
string callbackName = null, IDbTransaction dbTransaction = null)
public void Publish<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{
CheckIsAdoNet(name);
PrepareConnectionForAdo(dbConnection, dbTransaction);
PrepareConnectionForAdo(dbTransaction);
var content = Serialize(contentObj, callbackName);
PublishWithTrans(name, content);
}
public Task PublishAsync<T>(string name, T contentObj, IDbConnection dbConnection,
string callbackName = null, IDbTransaction dbTransaction = null)
public Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{
CheckIsAdoNet(name);
PrepareConnectionForAdo(dbConnection, dbTransaction);
PrepareConnectionForAdo(dbTransaction);
var content = Serialize(contentObj, callbackName);
......@@ -60,43 +58,60 @@ namespace DotNetCore.CAP.Abstractions
protected abstract void PrepareConnectionForEF();
protected abstract void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message);
protected abstract void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message);
protected abstract Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message);
protected abstract Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message);
#region private methods
private string Serialize<T>(T obj, string callbackName = null)
protected virtual string Serialize<T>(T obj, string callbackName = null)
{
var packer = (IMessagePacker)ServiceProvider.GetService(typeof(IMessagePacker));
string content;
if (obj != null)
{
if (Helper.IsComplexType(obj.GetType()))
{
var serializer = (IContentSerializer)ServiceProvider.GetService(typeof(IContentSerializer));
content = serializer.Serialize(obj);
}
else
{
content = obj.ToString();
}
}
else
{
var message = new Message(obj)
content = string.Empty;
}
var message = new CapMessageDto(content)
{
CallbackName = callbackName
};
return Helper.ToJson(message);
return packer.Pack(message);
}
private void PrepareConnectionForAdo(IDbConnection dbConnection, IDbTransaction dbTransaction)
#region private methods
private void PrepareConnectionForAdo(IDbTransaction dbTransaction)
{
DbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection));
DbTransaction = dbTransaction ?? throw new ArgumentNullException(nameof(dbTransaction));
DbConnection = DbTransaction.Connection;
if (DbConnection.State != ConnectionState.Open)
{
IsCapOpenedConn = true;
DbConnection.Open();
}
DbTranasaction = dbTransaction;
if (DbTranasaction == null)
{
IsCapOpenedTrans = true;
DbTranasaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted);
}
}
private void CheckIsUsingEF(string name)
{
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." +
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.");
}
......@@ -104,10 +119,11 @@ namespace DotNetCore.CAP.Abstractions
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (IsUsingEF)
throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded.");
throw new InvalidOperationException(
"If you are using the EntityFramework, you do not need to use this overloaded.");
}
private async Task PublishWithTransAsync(string name, string content)
private Task PublishWithTransAsync(string name, string content)
{
var message = new CapPublishedMessage
{
......@@ -116,11 +132,13 @@ namespace DotNetCore.CAP.Abstractions
StatusName = StatusName.Scheduled
};
await ExecuteAsync(DbConnection, DbTranasaction, message);
ExecuteAsync(DbConnection, DbTransaction, message);
ClosedCap();
PublishQueuer.PulseEvent.Set();
return Task.CompletedTask;
}
private void PublishWithTrans(string name, string content)
......@@ -132,7 +150,7 @@ namespace DotNetCore.CAP.Abstractions
StatusName = StatusName.Scheduled
};
Execute(DbConnection, DbTranasaction, message);
Execute(DbConnection, DbTransaction, message);
ClosedCap();
......@@ -143,18 +161,16 @@ namespace DotNetCore.CAP.Abstractions
{
if (IsCapOpenedTrans)
{
DbTranasaction.Commit();
DbTranasaction.Dispose();
DbTransaction.Commit();
DbTransaction.Dispose();
}
if (IsCapOpenedConn)
{
DbConnection.Dispose();
}
}
public void Dispose()
{
DbTranasaction?.Dispose();
DbTransaction?.Dispose();
DbConnection?.Dispose();
}
......
using System;
namespace DotNetCore.CAP.Abstractions
{
/// <summary>
/// a context of consumer invoker.
/// </summary>
public class ConsumerInvokerContext
{
public ConsumerInvokerContext(ConsumerContext consumerContext)
{
ConsumerContext = consumerContext ??
throw new ArgumentNullException(nameof(consumerContext));
}
public ConsumerContext ConsumerContext { get; set; }
public IConsumerInvoker Result { get; set; }
}
}
\ No newline at end of file
using System;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.Abstractions
{
/// <summary>
/// Message content serializer.
/// <para>By default, CAP will use Json as a serializer, and you can customize this interface to achieve serialization of other methods.</para>
/// </summary>
public interface IContentSerializer
{
/// <summary>
/// Serializes the specified object to a string.
/// </summary>
/// <typeparam name="T"> The type of the value being serialized.</typeparam>
/// <param name="value">The object to serialize.</param>
/// <returns>A string representation of the object.</returns>
string Serialize<T>(T value);
/// <summary>
/// Deserializes the string to the specified .NET type.
/// </summary>
/// <typeparam name="T">The type of the object to deserialize to.</typeparam>
/// <param name="value">The content string to deserialize.</param>
/// <returns>The deserialized object from the string.</returns>
T DeSerialize<T>(string value);
/// <summary>
/// Deserializes the string to the specified .NET type.
/// </summary>
/// <param name="value">The string to deserialize.</param>
/// <param name="type">The type of the object to deserialize to.</param>
/// <returns>The deserialized object from the string.</returns>
object DeSerialize(string value, Type type);
}
/// <summary>
/// CAP message content wapper.
/// <para>You can customize the message body filed name of the wrapper or add fields that you interested.</para>
/// </summary>
/// <remarks>
/// We use the wrapper to provide some additional information for the message content,which is important for CAP。
/// Typically, we may need to customize the field display name of the message,
/// which includes interacting with other message components, which can be adapted in this manner
/// </remarks>
public interface IMessagePacker
{
/// <summary>
/// Package a message object
/// </summary>
/// <param name="obj">The obj message to be packed.</param>
string Pack(CapMessage obj);
/// <summary>
/// Unpack a message strings to <see cref="CapMessage"/> object.
/// </summary>
/// <param name="packingMessage">The string of packed message.</param>
CapMessage UnPack(string packingMessage);
}
}
\ No newline at end of file
using System.Reflection;
using DotNetCore.CAP.Abstractions.ModelBinding;
namespace DotNetCore.CAP.Internal
namespace DotNetCore.CAP.Abstractions
{
/// <summary>
/// Model binder factory.
/// </summary>
public interface IModelBinderFactory
{
/// <summary>
/// Create a model binder by parameter.
/// </summary>
/// <param name="parameter">The method parameter info</param>
/// <returns>A model binder instance.</returns>
IModelBinder CreateBinder(ParameterInfo parameter);
}
}
\ No newline at end of file
using System.Threading.Tasks;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP.Abstractions
{
/// <summary>
/// Consumer method executor.
/// </summary>
public interface ISubscriberExecutor
{
/// <summary>
/// Execute the consumer method.
/// </summary>
/// <param name="receivedMessage">The received message.</param>
Task<OperateResult> ExecuteAsync(CapReceivedMessage receivedMessage);
}
}
......@@ -8,22 +8,22 @@ namespace DotNetCore.CAP.Abstractions.ModelBinding
public struct ModelBindingResult
{
/// <summary>
/// Creates a <see cref="ModelBindingResult"/> representing a failed model binding operation.
/// Creates a <see cref="ModelBindingResult" /> representing a failed model binding operation.
/// </summary>
/// <returns>A <see cref="ModelBindingResult"/> representing a failed model binding operation.</returns>
/// <returns>A <see cref="ModelBindingResult" /> representing a failed model binding operation.</returns>
public static ModelBindingResult Failed()
{
return new ModelBindingResult(model: null, isSuccess: false);
return new ModelBindingResult(null, false);
}
/// <summary>
/// Creates a <see cref="ModelBindingResult"/> representing a successful model binding operation.
/// Creates a <see cref="ModelBindingResult" /> representing a successful model binding operation.
/// </summary>
/// <param name="model">The model value. May be <c>null.</c></param>
/// <returns>A <see cref="ModelBindingResult"/> representing a successful model bind.</returns>
/// <returns>A <see cref="ModelBindingResult" /> representing a successful model bind.</returns>
public static ModelBindingResult Success(object model)
{
return new ModelBindingResult(model, isSuccess: true);
return new ModelBindingResult(model, true);
}
private ModelBindingResult(object model, bool isSuccess)
......@@ -42,27 +42,17 @@ namespace DotNetCore.CAP.Abstractions.ModelBinding
public override string ToString()
{
if (IsSuccess)
{
return $"Success '{Model}'";
}
else
{
return $"Failed";
}
return "Failed";
}
public override bool Equals(object obj)
{
var other = obj as ModelBindingResult?;
if (other == null)
{
return false;
}
else
{
return Equals(other.Value);
}
}
public override int GetHashCode()
{
......@@ -77,14 +67,14 @@ namespace DotNetCore.CAP.Abstractions.ModelBinding
{
return
IsSuccess == other.IsSuccess &&
object.Equals(Model, other.Model);
Equals(Model, other.Model);
}
/// <summary>
/// Compares <see cref="ModelBindingResult"/> objects for equality.
/// Compares <see cref="ModelBindingResult" /> objects for equality.
/// </summary>
/// <param name="x">A <see cref="ModelBindingResult"/>.</param>
/// <param name="y">A <see cref="ModelBindingResult"/>.</param>
/// <param name="x">A <see cref="ModelBindingResult" />.</param>
/// <param name="y">A <see cref="ModelBindingResult" />.</param>
/// <returns><c>true</c> if the objects are equal, otherwise <c>false</c>.</returns>
public static bool operator ==(ModelBindingResult x, ModelBindingResult y)
{
......@@ -92,10 +82,10 @@ namespace DotNetCore.CAP.Abstractions.ModelBinding
}
/// <summary>
/// Compares <see cref="ModelBindingResult"/> objects for inequality.
/// Compares <see cref="ModelBindingResult" /> objects for inequality.
/// </summary>
/// <param name="x">A <see cref="ModelBindingResult"/>.</param>
/// <param name="y">A <see cref="ModelBindingResult"/>.</param>
/// <param name="x">A <see cref="ModelBindingResult" />.</param>
/// <param name="y">A <see cref="ModelBindingResult" />.</param>
/// <returns><c>true</c> if the objects are not equal, otherwise <c>false</c>.</returns>
public static bool operator !=(ModelBindingResult x, ModelBindingResult y)
{
......
......@@ -2,8 +2,9 @@
namespace DotNetCore.CAP.Abstractions
{
/// <inheritdoc />
/// <summary>
/// An abstract attribute that for kafka attribute or rabbitmq attribute
/// An abstract attribute that for kafka attribute or rabbit mq attribute
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public abstract class TopicAttribute : Attribute
......@@ -14,19 +15,14 @@ namespace DotNetCore.CAP.Abstractions
}
/// <summary>
/// topic or exchange route key name.
/// Topic or exchange route key name.
/// </summary>
public string Name { get; }
/// <summary>
/// kafak --> groups.id
/// rabbitmq --> queue.name
/// kafka --> groups.id
/// rabbit MQ --> queue.name
/// </summary>
public string Group { get; set; } = "cap.default.group";
/// <summary>
/// unused now
/// </summary>
public bool IsOneWay { get; set; }
}
}
\ No newline at end of file
using System;
using DotNetCore.CAP;
using DotNetCore.CAP.Dashboard.GatewayProxy;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// app extensions for <see cref="IApplicationBuilder"/>
/// app extensions for <see cref="IApplicationBuilder" />
/// </summary>
public static class AppBuilderExtensions
{
///<summary>
/// <summary>
/// Enables cap for the current application
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance this method extends.</returns>
/// <param name="app">The <see cref="IApplicationBuilder" /> instance this method extends.</param>
/// <returns>The <see cref="IApplicationBuilder" /> instance this method extends.</returns>
public static IApplicationBuilder UseCap(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var marker = app.ApplicationServices.GetService<CapMarkerService>();
if (marker == null)
{
throw new InvalidOperationException("Add Cap must be called on the service collection.");
}
CheckRequirement(app);
var provider = app.ApplicationServices;
var bootstrapper = provider.GetRequiredService<IBootstrapper>();
bootstrapper.BootstrapAsync();
if (provider.GetService<DashboardOptions>() != null)
{
if (provider.GetService<DiscoveryOptions>() != null)
app.UseMiddleware<GatewayProxyMiddleware>();
app.UseMiddleware<DashboardMiddleware>();
}
return app;
}
private static void CheckRequirement(IApplicationBuilder app)
{
var marker = app.ApplicationServices.GetService<CapMarkerService>();
if (marker == null)
throw new InvalidOperationException(
"AddCap() must be called on the service collection. eg: services.AddCap(...)");
var messageQueueMarker = app.ApplicationServices.GetService<CapMessageQueueMakerService>();
if (messageQueueMarker == null)
throw new InvalidOperationException(
"You must be config used message queue provider at AddCap() options! eg: services.AddCap(options=>{ options.UseKafka(...) })");
var databaseMarker = app.ApplicationServices.GetService<CapDatabaseStorageMarkerService>();
if (databaseMarker == null)
throw new InvalidOperationException(
"You must be config used database provider at AddCap() options! eg: services.AddCap(options=>{ options.UseSqlServer(...) })");
}
}
}
\ No newline at end of file
using System;
using DotNetCore.CAP.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP
......@@ -10,10 +11,24 @@ namespace DotNetCore.CAP
{
}
/// <summary>
/// Used to verify cap database storage extension was added on a ServiceCollection
/// </summary>
public class CapDatabaseStorageMarkerService
{
}
/// <summary>
/// Used to verify cap message queue extension was added on a ServiceCollection
/// </summary>
public class CapMessageQueueMakerService
{
}
/// <summary>
/// Allows fine grained configuration of CAP services.
/// </summary>
public class CapBuilder
public sealed class CapBuilder
{
public CapBuilder(IServiceCollection services)
{
......@@ -21,9 +36,39 @@ namespace DotNetCore.CAP
}
/// <summary>
/// Gets the <see cref="IServiceCollection"/> where MVC services are configured.
/// Gets the <see cref="IServiceCollection" /> where MVC services are configured.
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Add an <see cref="ICapPublisher" />.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
public CapBuilder AddProducerService<T>()
where T : class, ICapPublisher
{
return AddScoped(typeof(ICapPublisher), typeof(T));
}
/// <summary>
/// Add a custom content serializer
/// </summary>
public IServiceCollection Services { get; private set; }
/// <typeparam name="T">The type of the service.</typeparam>
public CapBuilder AddContentSerializer<T>()
where T : class, IContentSerializer
{
return AddSingleton(typeof(IContentSerializer), typeof(T));
}
/// <summary>
/// Add a custom message wapper
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
public CapBuilder AddMessagePacker<T>()
where T : class, IMessagePacker
{
return AddSingleton(typeof(IMessagePacker), typeof(T));
}
/// <summary>
/// Adds a scoped service of the type specified in serviceType with an implementation
......@@ -35,13 +80,12 @@ namespace DotNetCore.CAP
}
/// <summary>
/// Add an <see cref="ICapPublisher"/>.
/// Adds a singleton service of the type specified in serviceType with an implementation
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
public virtual CapBuilder AddProducerService<T>()
where T : class, ICapPublisher
private CapBuilder AddSingleton(Type serviceType, Type concreteType)
{
return AddScoped(typeof(ICapPublisher), typeof(T));
Services.AddSingleton(serviceType, concreteType);
return this;
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using DotNetCore.CAP.Models;
namespace DotNetCore.CAP
{
......@@ -8,8 +9,6 @@ namespace DotNetCore.CAP
/// </summary>
public class CapOptions
{
internal IList<ICapOptionsExtension> Extensions { get; private set; }
/// <summary>
/// Default value for polling delay timeout, in seconds.
/// </summary>
......@@ -21,26 +20,34 @@ namespace DotNetCore.CAP
public const int DefaultQueueProcessorCount = 2;
/// <summary>
/// Default successed message expriation timespan, in seconds.
/// Default succeeded message expiration time span, in seconds.
/// </summary>
public const int DefaultSuccessMessageExpirationAfter = 3600;
public const int DefaultSucceedMessageExpirationAfter = 24 * 3600;
/// <summary>
/// Failed message retry waiting interval.
/// </summary>
public const int DefaultFailedMessageWaitingInterval = 180;
public const int DefaultFailedMessageWaitingInterval = 600;
/// <summary>
/// Failed message retry count.
/// </summary>
public const int DefaultFailedRetryCount = 100;
public CapOptions()
{
PollingDelay = DefaultPollingDelay;
QueueProcessorCount = DefaultQueueProcessorCount;
SuccessedMessageExpiredAfter = DefaultSuccessMessageExpirationAfter;
FailedMessageWaitingInterval = DefaultFailedMessageWaitingInterval;
SucceedMessageExpiredAfter = DefaultSucceedMessageExpirationAfter;
FailedRetryInterval = DefaultFailedMessageWaitingInterval;
FailedRetryCount = DefaultFailedRetryCount;
Extensions = new List<ICapOptionsExtension>();
}
internal IList<ICapOptionsExtension> Extensions { get; }
/// <summary>
/// Productor job polling delay time.
/// Producer job polling delay time.
/// Default is 15 sec.
/// </summary>
public int PollingDelay { get; set; }
......@@ -52,21 +59,27 @@ namespace DotNetCore.CAP
public int QueueProcessorCount { get; set; }
/// <summary>
/// Sent or received successed message after timespan of due, then the message will be deleted at due time.
/// Dafault is 3600 seconds.
/// Sent or received succeed message after time span of due, then the message will be deleted at due time.
/// Default is 24*3600 seconds.
/// </summary>
public int SuccessedMessageExpiredAfter { get; set; }
public int SucceedMessageExpiredAfter { get; set; }
/// <summary>
/// Failed messages polling delay time.
/// Default is 180 seconds.
/// Default is 600 seconds.
/// </summary>
public int FailedMessageWaitingInterval { get; set; }
public int FailedRetryInterval { get; set; }
/// <summary>
/// We’ll invoke this call-back with message type,name,content when requeue failed message.
/// </summary>
public Action<Models.MessageType, string, string> FailedCallback { get; set; }
public Action<MessageType, string, string> FailedCallback { get; set; }
/// <summary>
/// The number of message retries, the retry will stop when the threshold is reached.
/// Default is 100 times.
/// </summary>
public int FailedRetryCount { get; set; }
/// <summary>
/// Registers an extension that will be executed when building services.
......
using System;
using System.Collections.Generic;
using System.Reflection;
using DotNetCore.CAP;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Processor;
using DotNetCore.CAP.Processor.States;
using Microsoft.Extensions.DependencyInjection.Extensions;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Contains extension methods to <see cref="IServiceCollection"/> for configuring consistence services.
/// Contains extension methods to <see cref="IServiceCollection" /> for configuring consistence services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds and configures the consistence services for the consitence.
/// Adds and configures the consistence services for the consistency.
/// </summary>
/// <param name="services">The services available in the application.</param>
/// <param name="setupAction">An action to configure the <see cref="CapOptions"/>.</param>
/// <returns>An <see cref="CapBuilder"/> for application services.</returns>
/// <param name="setupAction">An action to configure the <see cref="CapOptions" />.</param>
/// <returns>An <see cref="CapBuilder" /> for application services.</returns>
public static CapBuilder AddCap(
this IServiceCollection services,
Action<CapOptions> setupAction)
......@@ -33,33 +32,38 @@ namespace Microsoft.Extensions.DependencyInjection
AddSubscribeServices(services);
//Serializer and model binder
services.TryAddSingleton<IContentSerializer, JsonContentSerializer>();
services.TryAddSingleton<IMessagePacker, DefaultMessagePacker>();
services.TryAddSingleton<IConsumerServiceSelector, DefaultConsumerServiceSelector>();
services.TryAddSingleton<IModelBinderFactory, ModelBinderFactory>();
services.TryAddSingleton<ICallbackMessageSender, CallbackMessageSender>();
services.TryAddSingleton<IConsumerInvokerFactory, ConsumerInvokerFactory>();
services.TryAddSingleton<MethodMatcherCache>();
//Bootstrapper and Processors
services.AddSingleton<IProcessingServer, ConsumerHandler>();
services.AddSingleton<IProcessingServer, CapProcessingServer>();
services.AddSingleton<IBootstrapper, DefaultBootstrapper>();
services.AddSingleton<IStateChanger, StateChanger>();
//Processors
//Queue's message processor
services.AddTransient<PublishQueuer>();
services.AddTransient<SubscribeQueuer>();
services.AddTransient<FailedJobProcessor>();
services.AddTransient<FailedProcessor>();
services.AddTransient<IDispatcher, DefaultDispatcher>();
//Executors
services.AddSingleton<IQueueExecutorFactory, QueueExecutorFactory>();
services.AddSingleton<IQueueExecutor, SubscibeQueueExecutor>();
services.AddSingleton<IQueueExecutor, SubscribeQueueExecutor>();
services.TryAddSingleton<ISubscriberExecutor, DefaultSubscriberExecutor>();
//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);
......@@ -69,19 +73,13 @@ namespace Microsoft.Extensions.DependencyInjection
{
var consumerListenerServices = new List<KeyValuePair<Type, Type>>();
foreach (var rejectedServices in services)
{
if (rejectedServices.ImplementationType != null
&& typeof(ICapSubscribe).IsAssignableFrom(rejectedServices.ImplementationType))
{
consumerListenerServices.Add(new KeyValuePair<Type, Type>(typeof(ICapSubscribe),
rejectedServices.ImplementationType));
}
}
foreach (var service in consumerListenerServices)
{
services.AddTransient(service.Key, service.Value);
}
}
}
}
\ No newline at end of file
using System;
using System.Net;
using System.Threading.Tasks;
namespace DotNetCore.CAP.Dashboard
{
internal class BatchCommandDispatcher : IDashboardDispatcher
{
private readonly Action<DashboardContext, int> _command;
public BatchCommandDispatcher(Action<DashboardContext, int> command)
{
_command = command;
}
public async Task Dispatch(DashboardContext context)
{
var messageIds = await context.Request.GetFormValuesAsync("messages[]");
if (messageIds.Count == 0)
{
context.Response.StatusCode = 422;
return;
}
foreach (var messageId in messageIds)
{
var id = int.Parse(messageId);
_command(context, id);
}
context.Response.StatusCode = (int) HttpStatusCode.NoContent;
}
}
}
\ No newline at end of file
This diff is collapsed.
using System.Collections.Generic;
using DotNetCore.CAP.Dashboard;
// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class DashboardOptions
{
public DashboardOptions()
{
AppPath = "/";
PathMatch = "/cap";
Authorization = new[] {new LocalRequestsOnlyAuthorizationFilter()};
StatsPollingInterval = 2000;
}
/// <summary>
/// The path for the Back To Site link. Set to <see langword="null" /> in order to hide the Back To Site link.
/// </summary>
public string AppPath { get; set; }
public string PathMatch { get; set; }
public IEnumerable<IDashboardAuthorizationFilter> Authorization { get; set; }
/// <summary>
/// The interval the /stats endpoint should be polled with.
/// </summary>
public int StatsPollingInterval { get; set; }
}
}
\ No newline at end of file
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.
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.
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.
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.
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.
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.
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