Skip to content

Commit cbff896

Browse files
committed
Background jobs should not get deadlocks.
Context properties will now display HTML as code
1 parent 3202b68 commit cbff896

File tree

16 files changed

+252
-128
lines changed

16 files changed

+252
-128
lines changed

src/Server/Coderr.Server.App/Core/Incidents/Jobs/DeleteEmptyIncidents.cs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
2+
using System.Data;
3+
using System.Diagnostics;
24
using System.Threading.Tasks;
35
using Coderr.Server.Abstractions.Boot;
46
using Coderr.Server.Abstractions.Config;
57
using Coderr.Server.App.Core.Reports.Config;
68
using Griffin.ApplicationServices;
79
using Griffin.Data;
10+
using Griffin.Data.Mapper;
811
using log4net;
912

1013
namespace Coderr.Server.App.Core.Incidents.Jobs
@@ -23,28 +26,58 @@ namespace Coderr.Server.App.Core.Incidents.Jobs
2326
internal class DeleteEmptyIncidents : IBackgroundJobAsync
2427
{
2528
private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteEmptyIncidents));
26-
private readonly IAdoNetUnitOfWork _unitOfWork;
29+
private readonly IDbConnection _connection;
2730
private readonly IConfiguration<ReportConfig> _reportConfiguration;
2831

2932
/// <summary>
3033
/// Creates a new instance of <see cref="DeleteEmptyIncidents" />.
3134
/// </summary>
32-
/// <param name="unitOfWork">Used for SQL queries</param>
33-
public DeleteEmptyIncidents(IAdoNetUnitOfWork unitOfWork, IConfiguration<ReportConfig> reportConfiguration)
35+
/// <param name="connection">Used for SQL queries</param>
36+
public DeleteEmptyIncidents(IDbConnection connection, IConfiguration<ReportConfig> reportConfiguration)
3437
{
35-
if (unitOfWork == null) throw new ArgumentNullException("unitOfWork");
36-
_unitOfWork = unitOfWork;
37-
this._reportConfiguration = reportConfiguration;
38+
_connection = connection;
39+
_reportConfiguration = reportConfiguration;
3840
}
3941

4042
/// <inheritdoc />
4143
public async Task ExecuteAsync()
4244
{
43-
using (var cmd = _unitOfWork.CreateDbCommand())
45+
using (var cmd = _connection.CreateDbCommand())
4446
{
4547
cmd.CommandText =
46-
$@"DELETE TOP(500) Incidents
47-
WHERE LastReportAtUtc < @retentionDays";
48+
$@"CREATE TABLE #ItemsToDelete
49+
(
50+
Id int NOT NULL PRIMARY KEY
51+
)
52+
53+
INSERT #ItemsToDelete (Id)
54+
SELECT TOP(500) Id
55+
FROM Incidents WITH (ReadUncommitted)
56+
WHERE LastReportAtUtc < @retentionDays
57+
declare @counter int = 0;
58+
59+
IF @@ROWCOUNT <> 0
60+
BEGIN
61+
DECLARE ItemsToDeleteCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
62+
FOR SELECT Id FROM #ItemsToDelete
63+
set @counter = 1
64+
65+
DECLARE @IdToDelete int
66+
OPEN ItemsToDeleteCursor
67+
FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete
68+
69+
WHILE @@FETCH_STATUS = 0
70+
BEGIN
71+
set @counter = @counter + 1
72+
DELETE FROM Incidents WHERE Id = @IdToDelete
73+
FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete
74+
END
75+
76+
CLOSE ItemsToDeleteCursor
77+
DEALLOCATE ItemsToDeleteCursor
78+
END
79+
DROP TABLE #ItemsToDelete
80+
select @counter;";
4881

4982
// Wait until no reports have been received for the specified report save time
5083
// and then make sure during another period that no new reports have been received.
Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
2+
using System.Data;
3+
using System.Diagnostics;
24
using System.Threading.Tasks;
35
using Coderr.Server.Abstractions.Boot;
46
using Coderr.Server.Abstractions.Config;
57
using Coderr.Server.App.Core.Reports.Config;
68
using Griffin.ApplicationServices;
79
using Griffin.Data;
10+
using Griffin.Data.Mapper;
811
using log4net;
912

1013
namespace Coderr.Server.App.Core.Reports.Jobs
@@ -17,56 +20,89 @@ namespace Coderr.Server.App.Core.Reports.Jobs
1720
public class DeleteOldReports : IBackgroundJobAsync
1821
{
1922
private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteOldReports));
20-
private readonly IAdoNetUnitOfWork _unitOfWork;
23+
private readonly IDbConnection _connection;
2124
private readonly IConfiguration<ReportConfig> _reportConfig;
2225

2326
/// <summary>
2427
/// Creates a new instance of <see cref="DeleteOldReports" />.
2528
/// </summary>
26-
/// <param name="unitOfWork">Used for SQL queries</param>
29+
/// <param name="connection">Used for SQL queries</param>
2730
/// <param name="reportConfig"></param>
2831
/// <exception cref="ArgumentNullException"></exception>
29-
public DeleteOldReports(IAdoNetUnitOfWork unitOfWork, IConfiguration<ReportConfig> reportConfig)
32+
public DeleteOldReports(IDbConnection connection, IConfiguration<ReportConfig> reportConfig)
3033
{
31-
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
34+
_connection = connection;
3235
_reportConfig = reportConfig;
3336
}
3437

35-
/// <summary>
36-
/// Number of reports which can be stored per incident.
37-
/// </summary>
38-
public int MaxReportsPerIncident
39-
{
40-
get { return _reportConfig.Value.MaxReportsPerIncident; }
41-
}
42-
4338
/// <summary>
4439
/// Number of days to keep old reports.
4540
/// </summary>
46-
public int RetentionDays
47-
{
48-
get
49-
{
50-
return _reportConfig.Value.RetentionDays;
51-
}
52-
}
41+
public int RetentionDays => _reportConfig.Value.RetentionDays;
5342

5443
/// <inheritdoc />
5544
public async Task ExecuteAsync()
5645
{
57-
using (var cmd = _unitOfWork.CreateDbCommand())
46+
using (var cmd = _connection.CreateDbCommand())
5847
{
59-
var sql = @"DELETE TOP(1000) FROM ErrorReports WHERE CreatedAtUtc < @date";
48+
var sql = @"CREATE TABLE #OldReports
49+
(
50+
ReportId int NOT NULL PRIMARY KEY
51+
)
52+
53+
INSERT #OldReports (ReportId)
54+
SELECT TOP(1000) Id
55+
FROM ErrorReports WITH (READUNCOMMITTED)
56+
WHERE CreatedAtUtc < @date
57+
58+
declare @counter int = 0;
59+
60+
IF @@ROWCOUNT <> 0
61+
BEGIN
62+
DECLARE OldReportsCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
63+
FOR SELECT ReportId FROM #OldReports
64+
set @counter = 1
65+
66+
DECLARE @ReportId int
67+
68+
OPEN OldReportsCursor
69+
70+
FETCH NEXT FROM OldReportsCursor INTO @ReportId
71+
72+
WHILE @@FETCH_STATUS = 0
73+
BEGIN
74+
set @counter = @counter + 1
75+
DELETE FROM ErrorReports WHERE Id = @ReportId
76+
FETCH NEXT FROM OldReportsCursor INTO @ReportId
77+
END
78+
79+
CLOSE OldReportsCursor
80+
81+
DEALLOCATE OldReportsCursor
82+
83+
END
84+
85+
DROP TABLE #OldReports
86+
select @counter;
87+
";
6088
cmd.CommandText = sql;
6189
cmd.AddParameter("date", DateTime.UtcNow.AddDays(-RetentionDays));
6290
cmd.CommandTimeout = 90;
63-
var rows = await cmd.ExecuteNonQueryAsync();
64-
if (rows > 0)
91+
try
6592
{
66-
_logger.Debug("Deleted the oldest " + rows + " reports.");
93+
var rows = await cmd.ExecuteNonQueryAsync();
94+
if (rows > 0)
95+
{
96+
_logger.Debug("Deleted the oldest " + rows + " reports.");
97+
}
98+
}
99+
catch (Exception ex)
100+
{
101+
_logger.Error("Failed on connection " + cmd.Connection.ConnectionString, ex);
102+
throw;
67103
}
68104
}
69105
}
70-
106+
71107
}
72108
}

src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
2+
using System.Data;
3+
using System.Diagnostics;
34
using Coderr.Server.Abstractions.Boot;
45
using Coderr.Server.Abstractions.Config;
56
using Coderr.Server.App.Core.Reports.Config;
6-
77
using Griffin.ApplicationServices;
88
using Griffin.Data;
99
using log4net;
@@ -22,18 +22,17 @@ namespace Coderr.Server.App.Core.Reports.Jobs
2222
public class DeleteReportsBelowReportLimit : IBackgroundJob
2323
{
2424
private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteReportsBelowReportLimit));
25-
private readonly IAdoNetUnitOfWork _unitOfWork;
26-
private ConfigurationStore _configStore;
25+
private readonly IDbConnection _connection;
26+
private readonly IConfiguration<ReportConfig> _reportConfig;
2727

2828
/// <summary>
2929
/// Creates a new instance of <see cref="DeleteReportsBelowReportLimit" />.
3030
/// </summary>
31-
/// <param name="unitOfWork">Used for SQL queries</param>
32-
public DeleteReportsBelowReportLimit(IAdoNetUnitOfWork unitOfWork, ConfigurationStore configStore)
31+
/// <param name="connection">Used for SQL queries</param>
32+
public DeleteReportsBelowReportLimit(IDbConnection connection, IConfiguration<ReportConfig> reportConfig)
3333
{
34-
if (unitOfWork == null) throw new ArgumentNullException("unitOfWork");
35-
_unitOfWork = unitOfWork;
36-
_configStore = configStore;
34+
_connection = connection;
35+
_reportConfig = reportConfig;
3736
}
3837

3938
/// <summary>
@@ -43,57 +42,73 @@ public int MaxReportsPerIncident
4342
{
4443
get
4544
{
46-
var config = _configStore.Load<ReportConfig>();
47-
if (config == null)
48-
return 100;
49-
return config.MaxReportsPerIncident;
45+
return _reportConfig?.Value?.MaxReportsPerIncident ?? 100;
5046
}
5147
}
5248

5349
/// <inheritdoc />
5450
public void Execute()
5551
{
56-
// find incidents with too many reports.
57-
var incidentsToTruncate = new List<Tuple<int, int>>();
58-
using (var cmd = _unitOfWork.CreateCommand())
52+
using (var cmd = _connection.CreateCommand())
5953
{
60-
cmd.CommandText =
61-
@"SELECT TOP(5) IncidentId, count(Id)
62-
FROM ErrorReports WITH (ReadPast)
63-
GROUP BY IncidentId
64-
HAVING Count(IncidentId) > @max
65-
ORDER BY count(Id) DESC";
54+
var sql = $@"CREATE TABLE #Incidents (Id int NOT NULL PRIMARY KEY, NumberOfItems int)
55+
INSERT #Incidents (Id, NumberOfItems)
56+
SELECT TOP(100) IncidentId, Count(Id) - @max
57+
FROM ErrorReports WITH (READUNCOMMITTED)
58+
GROUP BY IncidentId
59+
HAVING Count(Id) > @max
60+
ORDER BY count(Id) DESC
61+
62+
CREATE TABLE #ReportsToDelete (Id int not null primary key)
63+
declare @counter int = 0;
64+
65+
DECLARE IncidentCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
66+
FOR SELECT Id, NumberOfItems FROM #Incidents
67+
DECLARE @IncidentId int
68+
DECLARE @NumberOfItems int
69+
OPEN IncidentCursor
70+
FETCH NEXT FROM IncidentCursor INTO @IncidentId, @NumberOfItems
71+
WHILE @@FETCH_STATUS = 0
72+
BEGIN
73+
INSERT INTO #ReportsToDelete (Id)
74+
SELECT TOP(@NumberOfItems) Id
75+
FROM ErrorReports WITH (READUNCOMMITTED)
76+
WHERE IncidentId = @IncidentId
77+
ORDER BY Id asc
78+
FETCH NEXT FROM IncidentCursor INTO @IncidentId, @NumberOfItems
79+
END
80+
CLOSE IncidentCursor
81+
DEALLOCATE IncidentCursor
82+
DROP TABLE #Incidents
83+
84+
DECLARE ItemsToDeleteCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
85+
FOR SELECT Id FROM #ReportsToDelete
86+
87+
DECLARE @IdToDelete int
88+
OPEN ItemsToDeleteCursor
89+
FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete
90+
91+
WHILE @@FETCH_STATUS = 0
92+
BEGIN
93+
set @counter = @counter + 1
94+
DELETE FROM ErrorReports WHERE Id = @IdToDelete
95+
FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete
96+
END
97+
98+
CLOSE ItemsToDeleteCursor
99+
DEALLOCATE ItemsToDeleteCursor
100+
DROP TABLE #ReportsToDelete
101+
select @counter;";
102+
103+
cmd.CommandText = sql;
104+
cmd.CommandTimeout = 90;
66105
cmd.AddParameter("max", MaxReportsPerIncident);
67-
using (var reader = cmd.ExecuteReader())
106+
var rows = (int)cmd.ExecuteScalar();
107+
if (rows > 0)
68108
{
69-
while (reader.Read())
70-
{
71-
incidentsToTruncate.Add(new Tuple<int, int>((int)reader[0], (int)reader[1]));
72-
}
109+
_logger.Debug("Deleted the oldest " + rows + " reports.");
73110
}
74-
}
75111

76-
foreach (var incidentIdAndCount in incidentsToTruncate)
77-
{
78-
//do not delete more then 500 at a time.
79-
var rowsToDelete = Math.Min(500, incidentIdAndCount.Item2 - MaxReportsPerIncident);
80-
using (var cmd = _unitOfWork.CreateCommand())
81-
{
82-
var sql = $@"With RowsToDelete AS
83-
(
84-
SELECT TOP {rowsToDelete} Id
85-
FROM ErrorReports WITH (ReadPast)
86-
WHERE IncidentId = {incidentIdAndCount.Item1}
87-
)
88-
DELETE FROM RowsToDelete";
89-
cmd.CommandText = sql;
90-
cmd.CommandTimeout = 90;
91-
var rows = cmd.ExecuteNonQuery();
92-
if (rows > 0)
93-
{
94-
_logger.Debug("Deleted the oldest " + rows + " reports for incident " + incidentIdAndCount);
95-
}
96-
}
97112
}
98113
}
99114
}

src/Server/Coderr.Server.ReportAnalyzer/ApplicationVersions/Handlers/GetVersionFromReport.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3+
using Coderr.Client;
34
using Coderr.Server.Domain.Modules.ApplicationVersions;
45
using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
56
using DotNetCqs;
@@ -29,6 +30,13 @@ public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
2930
if (version == null)
3031
return;
3132

33+
if (version.Length > 20)
34+
{
35+
Err.ReportLogicError("Application version is too large.", new {version, e.Incident.ApplicationName},
36+
"AppVersionLength");
37+
return;
38+
}
39+
3240
var isNewIncident = e.Incident.ReportCount <= 1;
3341
var versionEntity = await _repository.FindVersionAsync(e.Incident.ApplicationId, version)
3442
?? new ApplicationVersion(e.Incident.ApplicationId, e.Incident.ApplicationName,

src/Server/Coderr.Server.Web/Areas/Installation/Controllers/BootController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public ActionResult Index()
1919
public ActionResult NoInstallation()
2020
{
2121
if (Request.Path.Value.EndsWith("/setup/activate", StringComparison.OrdinalIgnoreCase))
22-
return Redirect("~/?#/welcome/admin/");
22+
return Redirect("~/");
2323
return View();
2424
}
2525

0 commit comments

Comments
 (0)