Skip to content

Commit 4a55f7c

Browse files
committed
Simplify GlobalTypeMapper locking and fix races (#6538)
(cherry picked from commit 44f1394)
1 parent e124680 commit 4a55f7c

1 file changed

Lines changed: 75 additions & 143 deletions

File tree

src/Npgsql/TypeMapping/GlobalTypeMapper.cs

Lines changed: 75 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.Linq;
55
using System.Text.Json;
6-
using System.Text.Json.Nodes;
7-
using System.Threading;
86
using Npgsql.Internal;
97
using Npgsql.Internal.Postgres;
108
using Npgsql.Internal.ResolverFactories;
@@ -16,87 +14,67 @@ namespace Npgsql.TypeMapping;
1614
sealed class GlobalTypeMapper : INpgsqlTypeMapper
1715
{
1816
readonly UserTypeMapper _userTypeMapper = new();
19-
readonly List<PgTypeInfoResolverFactory> _pluginResolverFactories = new();
20-
readonly ReaderWriterLockSlim _lock = new();
21-
PgTypeInfoResolverFactory[] _typeMappingResolvers = Array.Empty<PgTypeInfoResolverFactory>();
17+
readonly List<PgTypeInfoResolverFactory> _pluginResolverFactories = [];
18+
readonly object _sync = new();
19+
PgTypeInfoResolverFactory[] _typeMappingResolvers = [];
2220

2321
internal List<HackyEnumTypeMapping> HackyEnumTypeMappings { get; } = new();
2422

2523
internal IEnumerable<PgTypeInfoResolverFactory> GetPluginResolverFactories()
2624
{
27-
var resolvers = new List<PgTypeInfoResolverFactory>();
28-
_lock.EnterReadLock();
29-
try
30-
{
31-
resolvers.AddRange(_pluginResolverFactories);
32-
}
33-
finally
34-
{
35-
_lock.ExitReadLock();
36-
}
37-
38-
return resolvers;
25+
lock (_sync)
26+
return new List<PgTypeInfoResolverFactory>(_pluginResolverFactories);
3927
}
4028

4129
internal PgTypeInfoResolverFactory? GetUserMappingsResolverFactory()
4230
{
43-
_lock.EnterReadLock();
44-
try
45-
{
31+
lock (_sync)
4632
return _userTypeMapper.Items.Count > 0 ? _userTypeMapper : null;
47-
}
48-
finally
49-
{
50-
_lock.ExitReadLock();
51-
}
5233
}
5334

5435
internal void AddGlobalTypeMappingResolvers(PgTypeInfoResolverFactory[] factories, Func<PgTypeInfoResolverChainBuilder>? builderFactory = null, bool overwrite = false)
5536
{
56-
// Good enough logic to prevent SlimBuilder overriding the normal Builder.
57-
if (overwrite || factories.Length > _typeMappingResolvers.Length)
37+
lock (_sync)
5838
{
59-
_builderFactory = builderFactory;
60-
_typeMappingResolvers = factories;
61-
ResetTypeMappingCache();
39+
// Good enough logic to prevent SlimBuilder overriding the normal Builder.
40+
if (overwrite || factories.Length > _typeMappingResolvers.Length)
41+
{
42+
_builderFactory = builderFactory;
43+
_typeMappingResolvers = factories;
44+
_typeMappingOptions = null;
45+
}
6246
}
6347
}
6448

65-
void ResetTypeMappingCache() => _typeMappingOptions = null;
66-
6749
PgSerializerOptions? _typeMappingOptions;
6850
Func<PgTypeInfoResolverChainBuilder>? _builderFactory;
6951
JsonSerializerOptions? _jsonSerializerOptions;
7052

71-
PgSerializerOptions TypeMappingOptions
53+
PgSerializerOptions TypeMappingOptions => _typeMappingOptions ?? BuildTypeMappingOptions();
54+
55+
PgSerializerOptions BuildTypeMappingOptions()
7256
{
73-
get
57+
lock (_sync)
7458
{
75-
if (_typeMappingOptions is not null)
76-
return _typeMappingOptions;
77-
78-
_lock.EnterReadLock();
79-
try
59+
if (_typeMappingOptions is { } existing)
60+
return existing;
61+
62+
var builder = _builderFactory?.Invoke() ?? new();
63+
builder.AppendResolverFactory(_userTypeMapper);
64+
foreach (var factory in _pluginResolverFactories)
65+
builder.AppendResolverFactory(factory);
66+
foreach (var factory in _typeMappingResolvers)
67+
builder.AppendResolverFactory(factory);
68+
var chain = builder.Build();
69+
var options = new PgSerializerOptions(PostgresMinimalDatabaseInfo.DefaultTypeCatalog, chain)
8070
{
81-
var builder = _builderFactory?.Invoke() ?? new();
82-
builder.AppendResolverFactory(_userTypeMapper);
83-
foreach (var factory in _pluginResolverFactories)
84-
builder.AppendResolverFactory(factory);
85-
foreach (var factory in _typeMappingResolvers)
86-
builder.AppendResolverFactory(factory);
87-
var chain = builder.Build();
88-
return _typeMappingOptions = new(PostgresMinimalDatabaseInfo.DefaultTypeCatalog, chain)
89-
{
90-
// This means we don't ever have a missing oid for a datatypename as our canonical format is datatypenames.
91-
PortableTypeIds = true,
92-
// Don't throw if our catalog doesn't know the datatypename.
93-
IntrospectionMode = true
94-
};
95-
}
96-
finally
97-
{
98-
_lock.ExitReadLock();
99-
}
71+
// This means we don't ever have a missing oid for a datatypename as our canonical format is datatypenames.
72+
PortableTypeIds = true,
73+
// Don't throw if our catalog doesn't know the datatypename.
74+
IntrospectionMode = true
75+
};
76+
_typeMappingOptions = options;
77+
return options;
10078
}
10179
}
10280

@@ -126,8 +104,7 @@ static GlobalTypeMapper()
126104
/// <inheritdoc />
127105
public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory)
128106
{
129-
_lock.EnterWriteLock();
130-
try
107+
lock (_sync)
131108
{
132109
var type = factory.GetType();
133110

@@ -145,51 +122,20 @@ public void AddTypeInfoResolverFactory(PgTypeInfoResolverFactory factory)
145122
}
146123

147124
_pluginResolverFactories.Insert(0, factory);
148-
ResetTypeMappingCache();
149-
}
150-
finally
151-
{
152-
_lock.ExitWriteLock();
125+
_typeMappingOptions = null;
153126
}
154127
}
155128

156-
void ReplaceTypeInfoResolverFactory(PgTypeInfoResolverFactory factory)
157-
{
158-
_lock.EnterWriteLock();
159-
try
160-
{
161-
var type = factory.GetType();
162-
163-
for (var i = 0; i < _pluginResolverFactories.Count; i++)
164-
{
165-
if (_pluginResolverFactories[i].GetType() == type)
166-
{
167-
_pluginResolverFactories[i] = factory;
168-
break;
169-
}
170-
}
171-
172-
ResetTypeMappingCache();
173-
}
174-
finally
175-
{
176-
_lock.ExitWriteLock();
177-
}
178-
}
179129

180130
/// <inheritdoc />
181131
public void Reset()
182132
{
183-
_lock.EnterWriteLock();
184-
try
133+
lock (_sync)
185134
{
186135
_pluginResolverFactories.Clear();
187136
_userTypeMapper.Items.Clear();
188137
HackyEnumTypeMappings.Clear();
189-
}
190-
finally
191-
{
192-
_lock.ExitWriteLock();
138+
_typeMappingOptions = null;
193139
}
194140
}
195141

@@ -203,9 +149,25 @@ public INpgsqlNameTranslator DefaultNameTranslator
203149
/// <inheritdoc />
204150
public INpgsqlTypeMapper ConfigureJsonOptions(JsonSerializerOptions serializerOptions)
205151
{
206-
_jsonSerializerOptions = serializerOptions;
207-
// If JsonTypeInfoResolverFactory exists we replace it with a configured instance on the same index of the array.
208-
ReplaceTypeInfoResolverFactory(new JsonTypeInfoResolverFactory(serializerOptions));
152+
lock (_sync)
153+
{
154+
_jsonSerializerOptions = serializerOptions;
155+
156+
// If JsonTypeInfoResolverFactory exists we replace it with a configured instance on the same index of the array.
157+
var factory = new JsonTypeInfoResolverFactory(serializerOptions);
158+
var type = factory.GetType();
159+
160+
for (var i = 0; i < _pluginResolverFactories.Count; i++)
161+
{
162+
if (_pluginResolverFactories[i].GetType() == type)
163+
{
164+
_pluginResolverFactories[i] = factory;
165+
break;
166+
}
167+
}
168+
169+
_typeMappingOptions = null;
170+
}
209171
return this;
210172
}
211173

@@ -216,7 +178,9 @@ public INpgsqlTypeMapper EnableDynamicJson(
216178
Type[]? jsonbClrTypes = null,
217179
Type[]? jsonClrTypes = null)
218180
{
219-
AddTypeInfoResolverFactory(new JsonDynamicTypeInfoResolverFactory(jsonbClrTypes, jsonClrTypes, _jsonSerializerOptions));
181+
// Use a re-entered lock to add the read of _jsonSerializerOptions to the total scope.
182+
lock (_sync)
183+
AddTypeInfoResolverFactory(new JsonDynamicTypeInfoResolverFactory(jsonbClrTypes, jsonClrTypes, _jsonSerializerOptions));
220184
return this;
221185
}
222186

@@ -241,90 +205,68 @@ public INpgsqlTypeMapper EnableUnmappedTypes()
241205
/// <inheritdoc />
242206
public INpgsqlTypeMapper MapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum
243207
{
244-
_lock.EnterWriteLock();
245-
try
208+
lock (_sync)
246209
{
247210
_userTypeMapper.MapEnum<TEnum>(pgName, nameTranslator);
248211

249-
// Temporary hack for EFCore.PG enum mapping compat
212+
// Temporary hack for EFCore.PG enum mapping compat
250213
if (_userTypeMapper.Items.FirstOrDefault(i => i.ClrType == typeof(TEnum)) is UserTypeMapping userTypeMapping)
251214
HackyEnumTypeMappings.Add(new(typeof(TEnum), userTypeMapping.PgTypeName, nameTranslator ?? DefaultNameTranslator));
252215

253-
ResetTypeMappingCache();
254-
216+
_typeMappingOptions = null;
255217
return this;
256218
}
257-
finally
258-
{
259-
_lock.ExitWriteLock();
260-
}
261219
}
262220

263221
/// <inheritdoc />
264222
public bool UnmapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum
265223
{
266-
_lock.EnterWriteLock();
267-
try
224+
lock (_sync)
268225
{
269226
var removed = _userTypeMapper.UnmapEnum<TEnum>(pgName, nameTranslator);
270227

271228
// Temporary hack for EFCore.PG enum mapping compat
272229
if (removed && ((List<UserTypeMapping>)_userTypeMapper.Items).FindIndex(m => m.ClrType == typeof(TEnum)) is > -1 and var index)
273230
HackyEnumTypeMappings.RemoveAt(index);
274231

275-
ResetTypeMappingCache();
276-
232+
_typeMappingOptions = null;
277233
return removed;
278234
}
279-
finally
280-
{
281-
_lock.ExitWriteLock();
282-
}
283235
}
284236

285237
/// <inheritdoc />
286238
[RequiresDynamicCode("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")]
287239
public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
288240
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
289241
{
290-
_lock.EnterWriteLock();
291-
try
242+
lock (_sync)
292243
{
293244
_userTypeMapper.MapEnum(clrType, pgName, nameTranslator);
294245

295246
// Temporary hack for EFCore.PG enum mapping compat
296247
if (_userTypeMapper.Items.FirstOrDefault(i => i.ClrType == clrType) is UserTypeMapping userTypeMapping)
297248
HackyEnumTypeMappings.Add(new(clrType, userTypeMapping.PgTypeName, nameTranslator ?? DefaultNameTranslator));
298249

299-
ResetTypeMappingCache();
250+
_typeMappingOptions = null;
300251
return this;
301252
}
302-
finally
303-
{
304-
_lock.ExitWriteLock();
305-
}
306253
}
307254

308255
/// <inheritdoc />
309256
public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
310257
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
311258
{
312-
_lock.EnterWriteLock();
313-
try
259+
lock (_sync)
314260
{
315261
var removed = _userTypeMapper.UnmapEnum(clrType, pgName, nameTranslator);
316262

317263
// Temporary hack for EFCore.PG enum mapping compat
318264
if (removed && ((List<UserTypeMapping>)_userTypeMapper.Items).FindIndex(m => m.ClrType == clrType) is > -1 and var index)
319265
HackyEnumTypeMappings.RemoveAt(index);
320266

321-
ResetTypeMappingCache();
267+
_typeMappingOptions = null;
322268
return removed;
323269
}
324-
finally
325-
{
326-
_lock.ExitWriteLock();
327-
}
328270
}
329271

330272
/// <inheritdoc />
@@ -342,34 +284,24 @@ public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes
342284
public INpgsqlTypeMapper MapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)]
343285
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
344286
{
345-
_lock.EnterWriteLock();
346-
try
287+
lock (_sync)
347288
{
348289
_userTypeMapper.MapComposite(clrType, pgName, nameTranslator);
349-
ResetTypeMappingCache();
290+
_typeMappingOptions = null;
350291
return this;
351292
}
352-
finally
353-
{
354-
_lock.ExitWriteLock();
355-
}
356293
}
357294

358295
/// <inheritdoc />
359296
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types which can require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
360297
public bool UnmapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)]
361298
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
362299
{
363-
_lock.EnterWriteLock();
364-
try
300+
lock (_sync)
365301
{
366302
var result = _userTypeMapper.UnmapComposite(clrType, pgName, nameTranslator);
367-
ResetTypeMappingCache();
303+
_typeMappingOptions = null;
368304
return result;
369305
}
370-
finally
371-
{
372-
_lock.ExitWriteLock();
373-
}
374306
}
375307
}

0 commit comments

Comments
 (0)