Skip to content

Commit efe561f

Browse files
committed
Improves licensing data handling in SQL persistence
Refactors endpoint user indicator updates to handle sanitized names efficiently, preventing redundant queries and ensuring consistent data across related endpoints. Adds `AsNoTracking()` to queries to improve performance and avoid unnecessary change tracking. Also includes initial database migrations for MySql and PostgreSQL.
1 parent ddbb137 commit efe561f

19 files changed

+1209
-188
lines changed

src/ServiceControl.Persistence.Sql.Core/Implementation/LicensingDataStore.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public async Task<IDictionary<string, IEnumerable<ThroughputData>>> GetEndpointT
2424

2525
var cutOff = DefaultCutOff();
2626

27-
var data = await dbContext.Throughput.Where(x => queueNames.Contains(x.EndpointName) && x.Date >= cutOff)
27+
var data = await dbContext.Throughput
28+
.AsNoTracking()
29+
.Where(x => queueNames.Contains(x.EndpointName) && x.Date >= cutOff)
2830
.ToListAsync(cancellationToken);
2931

3032
var lookup = data.ToLookup(x => x.EndpointName);
@@ -170,28 +172,43 @@ public async Task UpdateUserIndicatorOnEndpoints(List<UpdateUserIndicator> userI
170172
using var scope = serviceProvider.CreateScope();
171173
using var dbContext = scope.ServiceProvider.GetRequiredService<ServiceControlDbContextBase>();
172174

175+
// Get all relevant sanitized names from endpoints matched by name
176+
var sanitizedNames = await dbContext.Endpoints
177+
.Where(e => updates.Keys.Contains(e.EndpointName) && e.SanitizedEndpointName != null)
178+
.Select(e => e.SanitizedEndpointName)
179+
.Distinct()
180+
.ToListAsync(cancellationToken);
181+
182+
// Get all endpoints that match either by name or sanitized name in a single query
173183
var endpoints = await dbContext.Endpoints
174-
.Where(e => updates.Keys.Contains(e.EndpointName) || (e.SanitizedEndpointName != null && updates.Keys.Contains(e.SanitizedEndpointName)))
184+
.Where(e => updates.Keys.Contains(e.EndpointName)
185+
|| (e.SanitizedEndpointName != null && updates.Keys.Contains(e.SanitizedEndpointName))
186+
|| (e.SanitizedEndpointName != null && sanitizedNames.Contains(e.SanitizedEndpointName)))
175187
.ToListAsync(cancellationToken) ?? [];
176188

177189
foreach (var endpoint in endpoints)
178190
{
179191
if (endpoint.SanitizedEndpointName is not null && updates.TryGetValue(endpoint.SanitizedEndpointName, out var newValueFromSanitizedName))
180192
{
193+
// Direct match by sanitized name
181194
endpoint.UserIndicator = newValueFromSanitizedName;
182195
}
183196
else if (updates.TryGetValue(endpoint.EndpointName, out var newValueFromEndpoint))
184197
{
198+
// Direct match by endpoint name - this should also update all endpoints with the same sanitized name
185199
endpoint.UserIndicator = newValueFromEndpoint;
186-
//update all that match this sanitized name
187-
var sanitizedMatchingEndpoints = await dbContext.Endpoints
188-
.Where(e => e.SanitizedEndpointName == endpoint.SanitizedEndpointName && e.EndpointName != endpoint.EndpointName)
189-
.ToListAsync(cancellationToken) ?? [];
200+
}
201+
else if (endpoint.SanitizedEndpointName != null && sanitizedNames.Contains(endpoint.SanitizedEndpointName))
202+
{
203+
// This endpoint shares a sanitized name with an endpoint that was matched by name
204+
// Find the update value from the endpoint that has this sanitized name
205+
var matchingEndpoint = endpoints.FirstOrDefault(e =>
206+
e.SanitizedEndpointName == endpoint.SanitizedEndpointName &&
207+
updates.ContainsKey(e.EndpointName));
190208

191-
foreach (var matchingEndpointOnSanitizedName in sanitizedMatchingEndpoints)
209+
if (matchingEndpoint != null && updates.TryGetValue(matchingEndpoint.EndpointName, out var cascadedValue))
192210
{
193-
matchingEndpointOnSanitizedName.UserIndicator = newValueFromEndpoint;
194-
_ = dbContext.Endpoints.Update(matchingEndpointOnSanitizedName);
211+
endpoint.UserIndicator = cascadedValue;
195212
}
196213
}
197214
_ = dbContext.Endpoints.Update(endpoint);
@@ -263,7 +280,9 @@ public async Task<BrokerMetadata> GetBrokerMetadata(CancellationToken cancellati
263280
{
264281
using var scope = serviceProvider.CreateScope();
265282
await using var dbContext = scope.ServiceProvider.GetRequiredService<ServiceControlDbContextBase>();
266-
var existing = await dbContext.LicensingMetadata.SingleOrDefaultAsync(m => m.Key == key, cancellationToken);
283+
var existing = await dbContext.LicensingMetadata
284+
.AsNoTracking()
285+
.SingleOrDefaultAsync(m => m.Key == key, cancellationToken);
267286
if (existing is null)
268287
{
269288
return default;

src/ServiceControl.Persistence.Sql.MySQL/Migrations/20241208000000_InitialCreate.cs

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/ServiceControl.Persistence.Sql.MySQL/Migrations/20251210040740_InitialCreate.Designer.cs

Lines changed: 143 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#nullable disable
2+
3+
namespace ServiceControl.Persistence.Sql.MySQL.Migrations
4+
{
5+
using System;
6+
using Microsoft.EntityFrameworkCore.Metadata;
7+
using Microsoft.EntityFrameworkCore.Migrations;
8+
9+
/// <inheritdoc />
10+
public partial class InitialCreate : Migration
11+
{
12+
/// <inheritdoc />
13+
protected override void Up(MigrationBuilder migrationBuilder)
14+
{
15+
migrationBuilder.AlterDatabase()
16+
.Annotation("MySql:CharSet", "utf8mb4");
17+
18+
migrationBuilder.CreateTable(
19+
name: "DailyThroughput",
20+
columns: table => new
21+
{
22+
Id = table.Column<int>(type: "int", nullable: false)
23+
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
24+
EndpointName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
25+
.Annotation("MySql:CharSet", "utf8mb4"),
26+
ThroughputSource = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
27+
.Annotation("MySql:CharSet", "utf8mb4"),
28+
Date = table.Column<DateOnly>(type: "date", nullable: false),
29+
MessageCount = table.Column<long>(type: "bigint", nullable: false)
30+
},
31+
constraints: table =>
32+
{
33+
table.PrimaryKey("PK_DailyThroughput", x => x.Id);
34+
})
35+
.Annotation("MySql:CharSet", "utf8mb4");
36+
37+
migrationBuilder.CreateTable(
38+
name: "LicensingMetadata",
39+
columns: table => new
40+
{
41+
Id = table.Column<int>(type: "int", nullable: false)
42+
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
43+
Key = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
44+
.Annotation("MySql:CharSet", "utf8mb4"),
45+
Data = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: false)
46+
.Annotation("MySql:CharSet", "utf8mb4")
47+
},
48+
constraints: table =>
49+
{
50+
table.PrimaryKey("PK_LicensingMetadata", x => x.Id);
51+
})
52+
.Annotation("MySql:CharSet", "utf8mb4");
53+
54+
migrationBuilder.CreateTable(
55+
name: "ThroughputEndpoint",
56+
columns: table => new
57+
{
58+
Id = table.Column<int>(type: "int", nullable: false)
59+
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
60+
EndpointName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
61+
.Annotation("MySql:CharSet", "utf8mb4"),
62+
ThroughputSource = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
63+
.Annotation("MySql:CharSet", "utf8mb4"),
64+
SanitizedEndpointName = table.Column<string>(type: "longtext", nullable: true)
65+
.Annotation("MySql:CharSet", "utf8mb4"),
66+
EndpointIndicators = table.Column<string>(type: "longtext", nullable: true)
67+
.Annotation("MySql:CharSet", "utf8mb4"),
68+
UserIndicator = table.Column<string>(type: "longtext", nullable: true)
69+
.Annotation("MySql:CharSet", "utf8mb4"),
70+
Scope = table.Column<string>(type: "longtext", nullable: true)
71+
.Annotation("MySql:CharSet", "utf8mb4"),
72+
LastCollectedData = table.Column<DateOnly>(type: "date", nullable: false)
73+
},
74+
constraints: table =>
75+
{
76+
table.PrimaryKey("PK_ThroughputEndpoint", x => x.Id);
77+
})
78+
.Annotation("MySql:CharSet", "utf8mb4");
79+
80+
migrationBuilder.CreateTable(
81+
name: "TrialLicense",
82+
columns: table => new
83+
{
84+
Id = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
85+
TrialEndDate = table.Column<DateOnly>(type: "date", nullable: false)
86+
},
87+
constraints: table =>
88+
{
89+
table.PrimaryKey("PK_TrialLicense", x => x.Id);
90+
})
91+
.Annotation("MySql:CharSet", "utf8mb4");
92+
93+
migrationBuilder.CreateIndex(
94+
name: "UC_DailyThroughput_EndpointName_ThroughputSource_Date",
95+
table: "DailyThroughput",
96+
columns: new[] { "EndpointName", "ThroughputSource", "Date" },
97+
unique: true);
98+
99+
migrationBuilder.CreateIndex(
100+
name: "IX_LicensingMetadata_Key",
101+
table: "LicensingMetadata",
102+
column: "Key",
103+
unique: true);
104+
105+
migrationBuilder.CreateIndex(
106+
name: "UC_ThroughputEndpoint_EndpointName_ThroughputSource",
107+
table: "ThroughputEndpoint",
108+
columns: new[] { "EndpointName", "ThroughputSource" },
109+
unique: true);
110+
}
111+
112+
/// <inheritdoc />
113+
protected override void Down(MigrationBuilder migrationBuilder)
114+
{
115+
migrationBuilder.DropTable(
116+
name: "DailyThroughput");
117+
118+
migrationBuilder.DropTable(
119+
name: "LicensingMetadata");
120+
121+
migrationBuilder.DropTable(
122+
name: "ThroughputEndpoint");
123+
124+
migrationBuilder.DropTable(
125+
name: "TrialLicense");
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)