Skip to content

Commit aa63479

Browse files
committed
Optimizes EF Core upsert operations
Refactors the upsert logic in several data stores to leverage EF Core's change tracking more efficiently. Instead of creating a new entity and then calling Update, the code now fetches the existing entity (if any) and modifies its properties directly. This reduces the overhead and potential issues associated with detached entities. The RecoverabilityIngestionUnitOfWork is also updated to use change tracking for FailedMessageEntity updates. This commit was made on the `john/more_interfaces` branch.
1 parent 44859be commit aa63479

File tree

5 files changed

+78
-66
lines changed

5 files changed

+78
-66
lines changed

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,20 @@ public Task UpdateEndpointSettings(EndpointSettings settings, CancellationToken
3737
{
3838
return ExecuteWithDbContext(async dbContext =>
3939
{
40-
var entity = new EndpointSettingsEntity
41-
{
42-
Name = settings.Name,
43-
TrackInstances = settings.TrackInstances
44-
};
45-
4640
// Use EF's change tracking for upsert
47-
var existing = await dbContext.EndpointSettings.FindAsync([entity.Name], cancellationToken);
41+
var existing = await dbContext.EndpointSettings.FindAsync([settings.Name], cancellationToken);
4842
if (existing == null)
4943
{
44+
var entity = new EndpointSettingsEntity
45+
{
46+
Name = settings.Name,
47+
TrackInstances = settings.TrackInstances
48+
};
5049
dbContext.EndpointSettings.Add(entity);
5150
}
5251
else
5352
{
54-
dbContext.EndpointSettings.Update(entity);
53+
existing.TrackInstances = settings.TrackInstances;
5554
}
5655

5756
await dbContext.SaveChangesAsync(cancellationToken);

src/ServiceControl.Persistence.Sql.Core/Implementation/ErrorMessageDataStore.FailureGroups.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,23 @@ public Task EditComment(string groupId, string comment)
199199
{
200200
return ExecuteWithDbContext(async dbContext =>
201201
{
202-
var commentEntity = new GroupCommentEntity
203-
{
204-
Id = Guid.Parse(groupId),
205-
GroupId = groupId,
206-
Comment = comment
207-
};
202+
var id = Guid.Parse(groupId);
208203

209204
// Use EF's change tracking for upsert
210-
var existing = await dbContext.GroupComments.FindAsync(commentEntity.Id);
205+
var existing = await dbContext.GroupComments.FindAsync(id);
211206
if (existing == null)
212207
{
208+
var commentEntity = new GroupCommentEntity
209+
{
210+
Id = id,
211+
GroupId = groupId,
212+
Comment = comment
213+
};
213214
dbContext.GroupComments.Add(commentEntity);
214215
}
215216
else
216217
{
217-
dbContext.GroupComments.Update(commentEntity);
218+
existing.Comment = comment;
218219
}
219220

220221
await dbContext.SaveChangesAsync();

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,26 @@ public Task Save(MessageRedirectsCollection redirects)
5050
var newETag = Guid.NewGuid().ToString();
5151
var newLastModified = DateTime.UtcNow;
5252

53-
var entity = new MessageRedirectsEntity
54-
{
55-
Id = Guid.Parse(MessageRedirectsCollection.DefaultId),
56-
ETag = newETag,
57-
LastModified = newLastModified,
58-
RedirectsJson = redirectsJson
59-
};
53+
var id = Guid.Parse(MessageRedirectsCollection.DefaultId);
6054

6155
// Use EF's change tracking for upsert
62-
var existing = await dbContext.MessageRedirects.FindAsync(entity.Id);
56+
var existing = await dbContext.MessageRedirects.FindAsync(id);
6357
if (existing == null)
6458
{
59+
var entity = new MessageRedirectsEntity
60+
{
61+
Id = id,
62+
ETag = newETag,
63+
LastModified = newLastModified,
64+
RedirectsJson = redirectsJson
65+
};
6566
dbContext.MessageRedirects.Add(entity);
6667
}
6768
else
6869
{
69-
dbContext.MessageRedirects.Update(entity);
70+
existing.ETag = newETag;
71+
existing.LastModified = newLastModified;
72+
existing.RedirectsJson = redirectsJson;
7073
}
7174

7275
await dbContext.SaveChangesAsync();

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,20 @@ public Task StoreTrialEndDate(DateOnly trialEndDate, CancellationToken cancellat
2828
{
2929
return ExecuteWithDbContext(async dbContext =>
3030
{
31-
var entity = new TrialLicenseEntity
32-
{
33-
Id = SingletonId,
34-
TrialEndDate = trialEndDate
35-
};
36-
3731
// Use EF's change tracking for upsert
3832
var existing = await dbContext.TrialLicenses.FindAsync([SingletonId], cancellationToken);
3933
if (existing == null)
4034
{
35+
var entity = new TrialLicenseEntity
36+
{
37+
Id = SingletonId,
38+
TrialEndDate = trialEndDate
39+
};
4140
dbContext.TrialLicenses.Add(entity);
4241
}
4342
else
4443
{
45-
dbContext.TrialLicenses.Update(entity);
44+
existing.TrialEndDate = trialEndDate;
4645
}
4746

4847
await dbContext.SaveChangesAsync(cancellationToken);

src/ServiceControl.Persistence.Sql.Core/Implementation/UnitOfWork/RecoverabilityIngestionUnitOfWork.cs

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public async Task RecordFailedProcessingAttempt(MessageContext context, FailedMe
5757

5858
// Load existing message to merge attempts list
5959
var existingMessage = await parent.DbContext.FailedMessages
60-
.AsNoTracking()
6160
.FirstOrDefaultAsync(fm => fm.UniqueMessageId == uniqueMessageId);
6261

6362
List<FailedMessage.ProcessingAttempt> attempts;
@@ -77,45 +76,56 @@ public async Task RecordFailedProcessingAttempt(MessageContext context, FailedMe
7776
attempts = [.. attempts
7877
.OrderBy(a => a.AttemptedAt)
7978
.TakeLast(MaxProcessingAttempts)];
79+
80+
// Update the tracked entity
81+
existingMessage.Status = FailedMessageStatus.Unresolved;
82+
existingMessage.ProcessingAttemptsJson = JsonSerializer.Serialize(attempts);
83+
existingMessage.FailureGroupsJson = JsonSerializer.Serialize(groups);
84+
existingMessage.PrimaryFailureGroupId = groups.Count > 0 ? groups[0].Id : null;
85+
existingMessage.MessageId = processingAttempt.MessageId;
86+
existingMessage.MessageType = messageType;
87+
existingMessage.TimeSent = timeSent;
88+
existingMessage.SendingEndpointName = sendingEndpoint?.Name;
89+
existingMessage.ReceivingEndpointName = receivingEndpoint?.Name;
90+
existingMessage.ExceptionType = processingAttempt.FailureDetails?.Exception?.ExceptionType;
91+
existingMessage.ExceptionMessage = processingAttempt.FailureDetails?.Exception?.Message;
92+
existingMessage.QueueAddress = queueAddress;
93+
existingMessage.NumberOfProcessingAttempts = attempts.Count;
94+
existingMessage.LastProcessedAt = processingAttempt.AttemptedAt;
95+
existingMessage.ConversationId = conversationId;
96+
existingMessage.CriticalTime = criticalTime;
97+
existingMessage.ProcessingTime = processingTime;
98+
existingMessage.DeliveryTime = deliveryTime;
8099
}
81100
else
82101
{
83102
// First attempt for this message
84103
attempts = [processingAttempt];
85-
}
86104

87-
// Build the complete entity with all fields
88-
var failedMessageEntity = new FailedMessageEntity
89-
{
90-
Id = SequentialGuidGenerator.NewSequentialGuid(),
91-
UniqueMessageId = uniqueMessageId,
92-
Status = FailedMessageStatus.Unresolved,
93-
ProcessingAttemptsJson = JsonSerializer.Serialize(attempts),
94-
FailureGroupsJson = JsonSerializer.Serialize(groups),
95-
PrimaryFailureGroupId = groups.Count > 0 ? groups[0].Id : null,
96-
MessageId = processingAttempt.MessageId,
97-
MessageType = messageType,
98-
TimeSent = timeSent,
99-
SendingEndpointName = sendingEndpoint?.Name,
100-
ReceivingEndpointName = receivingEndpoint?.Name,
101-
ExceptionType = processingAttempt.FailureDetails?.Exception?.ExceptionType,
102-
ExceptionMessage = processingAttempt.FailureDetails?.Exception?.Message,
103-
QueueAddress = queueAddress,
104-
NumberOfProcessingAttempts = attempts.Count,
105-
LastProcessedAt = processingAttempt.AttemptedAt,
106-
ConversationId = conversationId,
107-
CriticalTime = criticalTime,
108-
ProcessingTime = processingTime,
109-
DeliveryTime = deliveryTime
110-
};
111-
112-
// Use EF's change tracking for upsert
113-
if (existingMessage != null)
114-
{
115-
parent.DbContext.FailedMessages.Update(failedMessageEntity);
116-
}
117-
else
118-
{
105+
// Build the complete entity with all fields
106+
var failedMessageEntity = new FailedMessageEntity
107+
{
108+
Id = SequentialGuidGenerator.NewSequentialGuid(),
109+
UniqueMessageId = uniqueMessageId,
110+
Status = FailedMessageStatus.Unresolved,
111+
ProcessingAttemptsJson = JsonSerializer.Serialize(attempts),
112+
FailureGroupsJson = JsonSerializer.Serialize(groups),
113+
PrimaryFailureGroupId = groups.Count > 0 ? groups[0].Id : null,
114+
MessageId = processingAttempt.MessageId,
115+
MessageType = messageType,
116+
TimeSent = timeSent,
117+
SendingEndpointName = sendingEndpoint?.Name,
118+
ReceivingEndpointName = receivingEndpoint?.Name,
119+
ExceptionType = processingAttempt.FailureDetails?.Exception?.ExceptionType,
120+
ExceptionMessage = processingAttempt.FailureDetails?.Exception?.Message,
121+
QueueAddress = queueAddress,
122+
NumberOfProcessingAttempts = attempts.Count,
123+
LastProcessedAt = processingAttempt.AttemptedAt,
124+
ConversationId = conversationId,
125+
CriticalTime = criticalTime,
126+
ProcessingTime = processingTime,
127+
DeliveryTime = deliveryTime
128+
};
119129
parent.DbContext.FailedMessages.Add(failedMessageEntity);
120130
}
121131

0 commit comments

Comments
 (0)