Commit ffc33990 authored by Savorboard's avatar Savorboard

implements dashboard interface.

parent 2a1a908b
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 PostgreSqlStorage _storage;
private readonly PostgreSqlOptions _options;
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()
{
string 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"" = N'Processing';",
_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 \"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 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 \"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+
string sqlQuery =
$@"
with aggr as (
select to_char(""Added"",'yyyy-MM-dd-HH') as ""Key"",
count(""Id"") as ""Count""
from ""{_options.Schema}"".""{tableName}""
where ""StatusName"" = N'Processing'
group by to_char(""Added"", 'yyyy-MM-dd-HH')
)
select ""Key"",""Count"" from 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.Data.SqlClient;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
...@@ -11,6 +14,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -11,6 +14,7 @@ namespace DotNetCore.CAP.PostgreSql
{ {
private readonly PostgreSqlOptions _options; private readonly PostgreSqlOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDbConnection _existingConnection = null;
public PostgreSqlStorage(ILogger<PostgreSqlStorage> logger, PostgreSqlOptions options) public PostgreSqlStorage(ILogger<PostgreSqlStorage> logger, PostgreSqlOptions options)
{ {
...@@ -20,12 +24,12 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -20,12 +24,12 @@ namespace DotNetCore.CAP.PostgreSql
public IStorageConnection GetConnection() public IStorageConnection GetConnection()
{ {
throw new System.NotImplementedException(); return new PostgreSqlStorageConnection(_options);
} }
public IMonitoringApi GetMonitoringApi() public IMonitoringApi GetMonitoringApi()
{ {
throw new System.NotImplementedException(); return new PostgreSqlMonitoringApi(this, _options);
} }
public async Task InitializeAsync(CancellationToken cancellationToken) public async Task InitializeAsync(CancellationToken cancellationToken)
...@@ -41,6 +45,46 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -41,6 +45,46 @@ namespace DotNetCore.CAP.PostgreSql
_logger.LogDebug("Ensuring all create database tables script are applied."); _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 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();
}
}
protected virtual string CreateDbTablesScript(string schema) protected virtual string CreateDbTablesScript(string schema)
{ {
var batchSql = $@" var batchSql = $@"
......
...@@ -5,21 +5,18 @@ using System.Threading.Tasks; ...@@ -5,21 +5,18 @@ using System.Threading.Tasks;
using Dapper; using Dapper;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models; using DotNetCore.CAP.Models;
using DotNetCore.CAP.Processor.States;
using Npgsql; using Npgsql;
namespace DotNetCore.CAP.PostgreSql namespace DotNetCore.CAP.PostgreSql
{ {
public class PostgreSqlStorageConnection : IStorageConnection public class PostgreSqlStorageConnection : IStorageConnection
{ {
private readonly PostgreSqlOptions _options;
public PostgreSqlStorageConnection(PostgreSqlOptions options) public PostgreSqlStorageConnection(PostgreSqlOptions options)
{ {
_options = options; Options = options;
} }
public PostgreSqlOptions Options => _options; public PostgreSqlOptions Options { get; }
public IStorageTransaction CreateTransaction() public IStorageTransaction CreateTransaction()
{ {
...@@ -28,9 +25,9 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -28,9 +25,9 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id) 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); return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
} }
...@@ -38,15 +35,18 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -38,15 +35,18 @@ namespace DotNetCore.CAP.PostgreSql
public Task<IFetchedMessage> FetchNextMessageAsync() 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); return FetchNextMessageCoreAsync(sql);
} }
public async Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync() 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); return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql);
} }
...@@ -54,9 +54,10 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -54,9 +54,10 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<IEnumerable<CapPublishedMessage>> GetFailedPublishedMessages() 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 \"StatusName\"='{StatusName.Failed}' LIMIT 1000;";
using (var connection = new NpgsqlConnection(_options.ConnectionString)) using (var connection = new NpgsqlConnection(Options.ConnectionString))
{ {
return await connection.QueryAsync<CapPublishedMessage>(sql); return await connection.QueryAsync<CapPublishedMessage>(sql);
} }
...@@ -68,9 +69,10 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -68,9 +69,10 @@ namespace DotNetCore.CAP.PostgreSql
{ {
if (message == null) throw new ArgumentNullException(nameof(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); await connection.ExecuteAsync(sql, message);
} }
...@@ -78,8 +80,8 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -78,8 +80,8 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id) public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
{ {
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"Id\"={id}"; var sql = $"SELECT * FROM \"{Options.Schema}\".\"received\" 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<CapReceivedMessage>(sql); return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
} }
...@@ -87,8 +89,9 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -87,8 +89,9 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync() public async Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync()
{ {
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;"; var sql =
using (var connection = new NpgsqlConnection(_options.ConnectionString)) $"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); return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql);
} }
...@@ -96,8 +99,9 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -96,8 +99,9 @@ namespace DotNetCore.CAP.PostgreSql
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages() public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages()
{ {
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;"; var sql =
using (var connection = new NpgsqlConnection(_options.ConnectionString)) $"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
{ {
return await connection.QueryAsync<CapReceivedMessage>(sql); return await connection.QueryAsync<CapReceivedMessage>(sql);
} }
...@@ -107,10 +111,32 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -107,10 +111,32 @@ 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) private async Task<IFetchedMessage> FetchNextMessageCoreAsync(string sql, object args = null)
{ {
//here don't use `using` to dispose //here don't use `using` to dispose
var connection = new NpgsqlConnection(_options.ConnectionString); var connection = new NpgsqlConnection(Options.ConnectionString);
await connection.OpenAsync(); await connection.OpenAsync();
var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
FetchedMessage fetchedMessage; FetchedMessage fetchedMessage;
...@@ -132,22 +158,8 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -132,22 +158,8 @@ namespace DotNetCore.CAP.PostgreSql
return null; return null;
} }
return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction); return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection,
} transaction);
public List<string> GetRangeFromSet(string key, int startingFrom, int endingAt)
{
throw new NotImplementedException();
}
public bool ChangePublishedState(int messageId, IState state)
{
throw new NotImplementedException();
}
public bool ChangeReceivedState(int messageId, IState state)
{
throw new NotImplementedException();
} }
} }
} }
\ No newline at end of file
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