Skip to content

Commit 1766644

Browse files
author
Ovan Crone
committed
Add MachineAlreadyExistsException and resolve race conditions in tests
1 parent d9e226c commit 1766644

File tree

26 files changed

+300
-267
lines changed

26 files changed

+300
-267
lines changed

src/REstate.Engine.Repositories.EntityFrameworkCore/EntityFrameworkCoreEngineRepositoryContext.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Data.SqlClient;
34
using System.Linq;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -151,7 +152,15 @@ public async Task<MachineStatus<TState, TInput>> CreateMachineAsync(
151152

152153
DbContext.Machines.Add(record);
153154

154-
await DbContext.SaveChangesAsync(cancellationToken);
155+
try
156+
{
157+
await DbContext.SaveChangesAsync(cancellationToken);
158+
}
159+
catch(DbUpdateException dbEx)
160+
when (dbEx.InnerException is SqlException sqlEx && sqlEx.Number == 2627)
161+
{
162+
throw new MachineAlreadyExistException(record.MachineId);
163+
}
155164

156165
return new MachineStatus<TState, TInput>
157166
{

src/REstate.Engine.Repositories.EntityFrameworkCore/REstate.Engine.Repositories.EntityFrameworkCore.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@
3030
<ProjectReference Include="..\REstate\REstate.csproj" />
3131
</ItemGroup>
3232

33+
<ItemGroup>
34+
<Reference Include="System.Data.SqlClient">
35+
<HintPath>..\..\..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\system.data.sqlclient\4.4.0\ref\netstandard2.0\System.Data.SqlClient.dll</HintPath>
36+
</Reference>
37+
</ItemGroup>
38+
3339
</Project>

src/REstate.Engine.Repositories.Redis/RedisEngineRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public async Task<MachineStatus<TState, TInput>> CreateMachineAsync(
136136

137137
var recordBytes = MessagePackSerializer.Serialize(record);
138138

139-
await _restateDatabase.StringSetAsync($"{MachinesKeyPrefix}/{machineId}", recordBytes).ConfigureAwait(false);
139+
await _restateDatabase.StringSetAsync($"{MachinesKeyPrefix}/{machineId}", recordBytes, null, When.NotExists).ConfigureAwait(false);
140140

141141
return new MachineStatus<TState, TInput>
142142
{

src/REstate/Engine/EventListeners/LoggingEventListener.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.Threading;
54
using System.Threading.Tasks;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
3+
namespace REstate.Engine
4+
{
5+
/// <summary>
6+
/// Indicates a request for a machine was made with an existing machineId
7+
/// </summary>
8+
public class MachineAlreadyExistException
9+
: Exception
10+
{
11+
public string RequestedMachineId { get; }
12+
13+
public MachineAlreadyExistException(string machineId)
14+
: this(machineId, $"A Machine already exists with MachineId matching {machineId}.")
15+
{
16+
}
17+
18+
public MachineAlreadyExistException(string machineId, string message)
19+
: base(message)
20+
{
21+
RequestedMachineId = machineId;
22+
}
23+
24+
public MachineAlreadyExistException(string machineId, string message, Exception innerException)
25+
: base(message, innerException)
26+
{
27+
RequestedMachineId = machineId;
28+
}
29+
}
30+
}

src/REstate/Engine/Repositories/InMemory/EngineRepository.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
4+
using System.Diagnostics;
35
using System.Linq;
46
using System.Threading;
57
using System.Threading.Tasks;
@@ -13,10 +15,9 @@ public class EngineRepository<TState, TInput>
1315
: ISchematicRepository<TState, TInput>
1416
, IMachineRepository<TState, TInput>
1517
{
16-
private IDictionary<string, Schematic<TState, TInput>> Schematics { get; } =
17-
new Dictionary<string, Schematic<TState, TInput>>();
18-
private IDictionary<string, (MachineStatus<TState, TInput> MachineStatus, Metadata Metadata)> Machines { get; } =
19-
new Dictionary<string, (MachineStatus<TState, TInput>, Metadata)>();
18+
private ConcurrentDictionary<string, Schematic<TState, TInput>> Schematics { get; } = new ConcurrentDictionary<string, Schematic<TState, TInput>>();
19+
private ConcurrentDictionary<string, (MachineStatus<TState, TInput> MachineStatus, Metadata Metadata)> Machines { get; } =
20+
new ConcurrentDictionary<string, (MachineStatus<TState, TInput>, Metadata)>();
2021

2122
/// <summary>
2223
/// Retrieves a previously stored Schematic by name.
@@ -51,7 +52,7 @@ public Task<Schematic<TState, TInput>> StoreSchematicAsync(
5152
if (schematic == null) throw new ArgumentNullException(nameof(schematic));
5253
if (schematic.SchematicName == null) throw new ArgumentException("Schematic must have a name to be stored.", nameof(schematic));
5354

54-
Schematics.Add(schematic.SchematicName, schematic);
55+
Schematics.AddOrUpdate(schematic.SchematicName, (key) => schematic, (key, old) => schematic);
5556

5657
var storedSchematic = Schematics[schematic.SchematicName];
5758

@@ -109,16 +110,21 @@ public Task<MachineStatus<TState, TInput>> CreateMachineAsync(
109110
Metadata = metadata
110111
};
111112

112-
Machines.Add(id, (record, metadata));
113+
if(Machines.TryAdd(id, (record, metadata)))
114+
return Task.FromResult(record);
113115

114-
return Task.FromResult(record);
116+
throw new MachineAlreadyExistException(id);
115117
}
116118

117119
public Task<ICollection<MachineStatus<TState, TInput>>> BulkCreateMachinesAsync(
118120
Schematic<TState, TInput> schematic,
119121
IEnumerable<Metadata> metadata,
120122
CancellationToken cancellationToken = new CancellationToken())
121123
{
124+
var exceptions = new Queue<Exception>();
125+
126+
var aggregateException = new AggregateException(exceptions);
127+
122128
List<(MachineStatus<TState, TInput> MachineStatus, IDictionary<string, string> Metadata)> machineRecords =
123129
metadata.Select(meta =>
124130
(new MachineStatus<TState, TInput>
@@ -134,9 +140,13 @@ public Task<ICollection<MachineStatus<TState, TInput>>> BulkCreateMachinesAsync(
134140

135141
foreach (var machineRecord in machineRecords)
136142
{
137-
Machines.Add(machineRecord.MachineStatus.MachineId, machineRecord);
143+
if (!Machines.TryAdd(machineRecord.MachineStatus.MachineId, machineRecord))
144+
exceptions.Enqueue(new MachineAlreadyExistException(machineRecord.MachineStatus.MachineId));
138145
}
139146

147+
if(exceptions.Count > 0)
148+
throw aggregateException;
149+
140150
return Task.FromResult<ICollection<MachineStatus<TState, TInput>>>(
141151
machineRecords.Select(r => r.MachineStatus).ToList());
142152
}
@@ -166,7 +176,7 @@ public Task DeleteMachineAsync(
166176
{
167177
if (machineId == null) throw new ArgumentNullException(nameof(machineId));
168178

169-
Machines.Remove(machineId);
179+
Machines.TryRemove(machineId, out var _);
170180

171181
#if NET45
172182
return Task.FromResult(0);

src/REstate/Engine/Repositories/InMemory/RepositoryContextFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class InMemoryRepositoryContextFactory<TState, TInput>
99
{
1010
private Lazy<IEngineRepositoryContext<TState, TInput>> repositoryContextLazy
1111
= new Lazy<IEngineRepositoryContext<TState, TInput>>(()
12-
=> new EngineRepositoryContext<TState, TInput>());
12+
=> new EngineRepositoryContext<TState, TInput>(), true);
1313

1414
public Task<IEngineRepositoryContext<TState, TInput>> OpenContextAsync(CancellationToken cancellationToken = default)
1515
{

test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineCreation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using LightBDD.Framework.Scenarios.Contextual;
44
using LightBDD.Framework.Scenarios.Extended;
55
using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context;
6-
using REstate.Tests.Features.Templates;
6+
using REstate.Tests.Features;
77

88
// ReSharper disable InconsistentNaming
99

test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineDeletion.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using LightBDD.Framework.Scenarios.Contextual;
44
using LightBDD.Framework.Scenarios.Extended;
55
using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context;
6-
using REstate.Tests.Features.Templates;
6+
using REstate.Tests.Features;
77

88
// ReSharper disable InconsistentNaming
99

test/REstate.Engine.Repositories.EntityFrameworkCore.Tests/Features/MachineRetrieval.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using LightBDD.Framework.Scenarios.Contextual;
44
using LightBDD.Framework.Scenarios.Extended;
55
using REstate.Engine.Repositories.EntityFrameworkCore.Tests.Features.Context;
6-
using REstate.Tests.Features.Templates;
6+
using REstate.Tests.Features;
77

88
// ReSharper disable InconsistentNaming
99

0 commit comments

Comments
 (0)