Skip to content

Commit 14a7617

Browse files
Merge pull request #5555 from Particular/patch721
Patch 7.2.1
2 parents 87e18a5 + 0476890 commit 14a7617

File tree

8 files changed

+230
-135
lines changed

8 files changed

+230
-135
lines changed

src/NServiceBus.Core.Tests/MessageMapper/MessageMapperTests.cs

+9-12
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,13 @@ public void Should_map_structs()
156156
}
157157

158158
[Test]
159-
public void Should_fail_for_interfaces_with_invalid_attributes()
159+
public void Should_handle_interfaces_that_have_attributes_with_nullable_properties()
160160
{
161-
var ex = Assert.Throws<ArgumentException>(() =>
162-
new MessageMapper().Initialize(new[]
163-
{
164-
typeof(MessageInterfaceWithPropertyWithIllegalAttribute)
165-
}));
161+
var mapper = new MessageMapper();
166162

167-
StringAssert.Contains("An invalid type was used as a custom attribute constructor argument, field or property.", ex.Message);
163+
var messageInstance = mapper.CreateInstance<MessageInterfaceWithNullablePropertyAttribute>();
164+
165+
Assert.IsNotNull(messageInstance);
168166
}
169167

170168
public class SampleMessageClass
@@ -229,20 +227,19 @@ interface IPrivateInterfaceMessage
229227
}
230228

231229
[AttributeUsage(AttributeTargets.Property)]
232-
public class IllegalAttribute : Attribute
230+
public class NullablePropertyAttribute : Attribute
233231
{
234-
// our mapper does not support custom attributes with nullable properties
235232
public int? IntKey { get; set; }
236233

237-
public IllegalAttribute(int x)
234+
public NullablePropertyAttribute(int x)
238235
{
239236
IntKey = x;
240237
}
241238
}
242239

243-
public interface MessageInterfaceWithPropertyWithIllegalAttribute
240+
public interface MessageInterfaceWithNullablePropertyAttribute
244241
{
245-
[Illegal(0)]
242+
[NullableProperty(0)]
246243
object Value { get; set; }
247244
}
248245

src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
<Reference Include="System.Transactions" />
1717
</ItemGroup>
1818

19+
<!-- Workaround reference because we can't use nullable reference types yet -->
20+
<ItemGroup>
21+
<Reference Include="WithDodgyNullable" HintPath="TestDlls\WithDodgyNullable.dll" />
22+
</ItemGroup>
23+
<!-- End workaround -->
24+
1925
<ItemGroup>
2026
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
2127
<PackageReference Include="Mono.Cecil" Version="0.10.4" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace NServiceBus.Persistence.InMemory.Tests
2+
{
3+
using NUnit.Framework;
4+
5+
[TestFixture]
6+
class ClientIdStorageTests
7+
{
8+
[Test]
9+
public void Should_detect_duplicates()
10+
{
11+
var clientIdStorage = new ClientIdStorage(10);
12+
13+
clientIdStorage.RegisterClientId("A");
14+
clientIdStorage.RegisterClientId("B");
15+
16+
Assert.True(clientIdStorage.IsDuplicate("A"));
17+
Assert.False(clientIdStorage.IsDuplicate("C"));
18+
}
19+
20+
[Test]
21+
public void Should_evict_oldest_entry_when_LRU_reaches_limit()
22+
{
23+
var clientIdStorage = new ClientIdStorage(2);
24+
25+
clientIdStorage.RegisterClientId("A");
26+
clientIdStorage.RegisterClientId("B");
27+
clientIdStorage.RegisterClientId("C");
28+
29+
Assert.False(clientIdStorage.IsDuplicate("A"));
30+
}
31+
32+
[Test]
33+
public void Should_reset_time_added_for_existing_IDs_when_checked()
34+
{
35+
var clientIdStorage = new ClientIdStorage(2);
36+
37+
clientIdStorage.RegisterClientId("A");
38+
clientIdStorage.RegisterClientId("B");
39+
40+
Assert.True(clientIdStorage.IsDuplicate("A"));
41+
42+
clientIdStorage.RegisterClientId("C");
43+
44+
Assert.False(clientIdStorage.IsDuplicate("B"));
45+
Assert.True(clientIdStorage.IsDuplicate("A"));
46+
}
47+
}
48+
}

src/NServiceBus.Core.Tests/Persistence/InMemory/InMemoryGatewayDeduplicationTests.cs

+40-27
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,72 @@
22
{
33
using System;
44
using System.Threading.Tasks;
5+
using System.Transactions;
56
using Extensibility;
67
using Features;
7-
using Gateway.Deduplication;
88
using NUnit.Framework;
99
using Settings;
1010

1111
[TestFixture]
1212
class InMemoryGatewayDeduplicationTests
1313
{
1414
[Test]
15-
public async Task Should_return_true_on_first_unique_test()
15+
public void Should_have_configured_storage_maxsize()
1616
{
17-
var storage = new InMemoryGatewayDeduplication(2);
17+
var settings = new SettingsHolder();
18+
var persistenceSettings = new PersistenceExtensions<InMemoryPersistence>(settings);
1819

19-
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
20-
Assert.True(await storage.DeduplicateMessage("B", DateTime.UtcNow, new ContextBag()));
21-
}
20+
persistenceSettings.GatewayDeduplicationCacheSize(42);
21+
22+
Assert.AreEqual(42, settings.Get<int>(InMemoryGatewayPersistence.MaxSizeKey));
23+
}
2224

2325
[Test]
24-
public async Task Should_return_false_on_second_test()
26+
public async Task Should_add_on_transaction_scope_commit()
2527
{
26-
var storage = new InMemoryGatewayDeduplication(2);
28+
var storage = CreateInMemoryGatewayDeduplication();
29+
30+
using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, TransactionScopeAsyncFlowOption.Enabled))
31+
{
32+
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
33+
34+
scope.Complete();
35+
}
2736

28-
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
2937
Assert.False(await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag()));
30-
}
31-
38+
}
39+
3240
[Test]
33-
public async Task Should_return_true_if_LRU_reaches_limit()
41+
public async Task Should_not_add_on_transaction_scope_abort()
3442
{
35-
var storage = new InMemoryGatewayDeduplication(2);
43+
var storage = CreateInMemoryGatewayDeduplication();
44+
45+
using (new TransactionScope(TransactionScopeOption.RequiresNew, TransactionScopeAsyncFlowOption.Enabled))
46+
{
47+
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
48+
49+
// no commit
50+
}
3651

37-
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
38-
await storage.DeduplicateMessage("B", DateTime.UtcNow, new ContextBag());
39-
await storage.DeduplicateMessage("C", DateTime.UtcNow, new ContextBag());
4052
Assert.True(await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag()));
4153
}
4254

4355
[Test]
44-
public void Should_have_configured_maxsize()
56+
// With the gateway persistence seam v1 design it's only safe to deduplicate when there is a tx scope present since the check happens before
57+
// the messages have been pushed to the transport. If we add entries earlier they would be considered duplicate when retrying after something went wrong.
58+
// Note: The gateway will always wrap the v1 seam invocation in a transaction scope
59+
public async Task Should_only_deduplicate_when_scope_is_present()
4560
{
46-
var feature = new InMemoryGatewayPersistence();
47-
var settings = new SettingsHolder();
48-
var container = new CommonObjectBuilder(new LightInjectObjectBuilder());
61+
var storage = CreateInMemoryGatewayDeduplication();
4962

50-
var persistenceSettings = new PersistenceExtensions<InMemoryPersistence>(settings);
51-
persistenceSettings.GatewayDeduplicationCacheSize(42);
52-
53-
feature.Setup(new FeatureConfigurationContext(settings, container, null, null, null));
63+
await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag());
5464

55-
var implementation = (InMemoryGatewayDeduplication)container.Build<IDeduplicateMessages>();
56-
Assert.AreEqual(42, implementation.maxSize);
65+
Assert.True(await storage.DeduplicateMessage("A", DateTime.UtcNow, new ContextBag()));
5766
}
5867

68+
InMemoryGatewayDeduplication CreateInMemoryGatewayDeduplication()
69+
{
70+
return new InMemoryGatewayDeduplication(new ClientIdStorage(10));
71+
}
5972
}
60-
}
73+
}

src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs

+16-71
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace NServiceBus
22
{
33
using System;
44
using System.Collections.Generic;
5-
using System.ComponentModel;
65
using System.Linq;
76
using System.Reflection;
87
using System.Reflection.Emit;
@@ -43,7 +42,7 @@ public Type CreateTypeFrom(Type type)
4342
propertyType,
4443
null);
4544

46-
foreach (var customAttribute in prop.GetCustomAttributes(true))
45+
foreach (var customAttribute in prop.GetCustomAttributesData())
4746
{
4847
AddCustomAttributeToProperty(customAttribute, propBuilder);
4948
}
@@ -93,85 +92,31 @@ public Type CreateTypeFrom(Type type)
9392
}
9493

9594
/// <summary>
96-
/// Given a custom attribute and property builder, adds an instance of custom attribute
95+
/// Given a custom attribute and property builder, adds an instance of the custom attribute
9796
/// to the property builder
9897
/// </summary>
99-
void AddCustomAttributeToProperty(object customAttribute, PropertyBuilder propBuilder)
98+
static void AddCustomAttributeToProperty(CustomAttributeData attributeData, PropertyBuilder propBuilder)
10099
{
101-
var customAttributeBuilder = BuildCustomAttribute(customAttribute);
102-
if (customAttributeBuilder != null)
103-
{
104-
propBuilder.SetCustomAttribute(customAttributeBuilder);
105-
}
106-
}
100+
var namedArguments = attributeData.NamedArguments;
107101

108-
static CustomAttributeBuilder BuildCustomAttribute(object customAttribute)
109-
{
110-
ConstructorInfo longestCtor = null;
111-
// Get constructor with the largest number of parameters
112-
foreach (var cInfo in customAttribute.GetType().GetConstructors().
113-
Where(cInfo => longestCtor == null || longestCtor.GetParameters().Length < cInfo.GetParameters().Length))
102+
if (namedArguments == null)
114103
{
115-
longestCtor = cInfo;
116-
}
104+
var attributeBuilder = new CustomAttributeBuilder(
105+
attributeData.Constructor,
106+
attributeData.ConstructorArguments.Select(x => x.Value).ToArray());
117107

118-
if (longestCtor == null)
119-
{
120-
return null;
108+
propBuilder.SetCustomAttribute(attributeBuilder);
121109
}
122-
123-
// For each constructor parameter, get corresponding (by name similarity) property and get its value
124-
var args = new object[longestCtor.GetParameters().Length];
125-
var position = 0;
126-
foreach (var consParamInfo in longestCtor.GetParameters())
110+
else
127111
{
128-
var attrPropInfo = customAttribute.GetType().GetProperty(consParamInfo.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
129-
if (attrPropInfo != null)
130-
{
131-
args[position] = attrPropInfo.GetValue(customAttribute, null);
132-
}
133-
else
134-
{
135-
args[position] = null;
136-
var attrFieldInfo = customAttribute.GetType().GetField(consParamInfo.Name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
137-
if (attrFieldInfo == null)
138-
{
139-
if (consParamInfo.ParameterType.IsValueType)
140-
{
141-
args[position] = Activator.CreateInstance(consParamInfo.ParameterType);
142-
}
143-
}
144-
else
145-
{
146-
args[position] = attrFieldInfo.GetValue(customAttribute);
147-
}
148-
}
149-
++position;
150-
}
112+
var attributeBuilder = new CustomAttributeBuilder(
113+
attributeData.Constructor,
114+
attributeData.ConstructorArguments.Select(x => x.Value).ToArray(),
115+
namedArguments.Select(x => (PropertyInfo)x.MemberInfo).ToArray(),
116+
namedArguments.Select(x => x.TypedValue.Value).ToArray());
151117

152-
var propList = new List<PropertyInfo>();
153-
var propValueList = new List<object>();
154-
foreach (var attrPropInfo in customAttribute.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
155-
{
156-
if (!attrPropInfo.CanWrite)
157-
{
158-
continue;
159-
}
160-
object defaultValue = null;
161-
var defaultValueAttribute = attrPropInfo.GetCustomAttribute<DefaultValueAttribute>(true);
162-
if (defaultValueAttribute != null)
163-
{
164-
defaultValue = defaultValueAttribute.Value;
165-
}
166-
var value = attrPropInfo.GetValue(customAttribute, null);
167-
if (value == defaultValue)
168-
{
169-
continue;
170-
}
171-
propList.Add(attrPropInfo);
172-
propValueList.Add(value);
118+
propBuilder.SetCustomAttribute(attributeBuilder);
173119
}
174-
return new CustomAttributeBuilder(longestCtor, args, propList.ToArray(), propValueList.ToArray());
175120
}
176121

177122
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace NServiceBus
2+
{
3+
using System.Collections.Generic;
4+
5+
class ClientIdStorage
6+
{
7+
public ClientIdStorage(int maxSize)
8+
{
9+
this.maxSize = maxSize;
10+
}
11+
12+
public bool IsDuplicate(string clientId)
13+
{
14+
lock (clientIdSet)
15+
{
16+
// This implementation is not 100% consistent since there is a race condition here where another thread might consider the same message as a non-duplicate.
17+
// We consider this good enough since this is the inmemory persister which will not be consistent when the endpoint is scaled out anyway.
18+
if (clientIdSet.TryGetValue(clientId, out var existingNode)) // O(1)
19+
{
20+
clientIdList.Remove(existingNode); // O(1) operation, because we got the node reference
21+
clientIdList.AddLast(existingNode); // O(1) operation
22+
23+
return true;
24+
}
25+
}
26+
27+
return false;
28+
}
29+
30+
public void RegisterClientId(string clientId)
31+
{
32+
lock (clientIdSet)
33+
{
34+
//another thread might already have added the ID since we checked the last time
35+
if (clientIdSet.ContainsKey(clientId))
36+
{
37+
// another thread has proceed this ID already and there is a potential duplicate message but there is nothing we can do about it at this stage so just return.
38+
// Throwing would just cause unessessary retries for the client.
39+
return;
40+
}
41+
42+
if (clientIdSet.Count == maxSize)
43+
{
44+
var id = clientIdList.First.Value;
45+
clientIdSet.Remove(id); // O(1)
46+
clientIdList.RemoveFirst(); // O(1)
47+
}
48+
49+
var node = clientIdList.AddLast(clientId); // O(1)
50+
clientIdSet.Add(clientId, node); // O(1)
51+
}
52+
}
53+
54+
readonly int maxSize;
55+
readonly LinkedList<string> clientIdList = new LinkedList<string>();
56+
readonly Dictionary<string, LinkedListNode<string>> clientIdSet = new Dictionary<string, LinkedListNode<string>>();
57+
}
58+
}

0 commit comments

Comments
 (0)