Skip to content

Commit 3fa5acd

Browse files
fixup! Recompute hashcodes on deserialization
1 parent f289ba0 commit 3fa5acd

File tree

10 files changed

+306
-168
lines changed

10 files changed

+306
-168
lines changed

src/NHibernate/Cache/CacheKey.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class CacheKey : IDeserializationCallback
1818
private readonly string entityOrRoleName;
1919
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
2020
[NonSerialized]
21-
private Lazy<int> _hashCode;
21+
private int? _hashCode;
2222
private readonly ISessionFactoryImplementor _factory;
2323

2424
/// <summary>
@@ -37,8 +37,7 @@ public CacheKey(object id, IType type, string entityOrRoleName, ISessionFactoryI
3737
this.entityOrRoleName = entityOrRoleName;
3838
_factory = factory;
3939

40-
// No need to delay computation here, but we need the lazy for the deserialization case.
41-
_hashCode = new Lazy<int>(GenerateHashCode);
40+
_hashCode = GenerateHashCode();
4241
}
4342

4443
//Mainly for SysCache and Memcache
@@ -57,22 +56,22 @@ public override bool Equals(object obj)
5756

5857
public override int GetHashCode()
5958
{
60-
// If the object is put in a set or dictionary during deserialization, the lazy will be null, and
61-
// we have to compute the hashcode on the fly.
62-
return _hashCode?.Value ?? GenerateHashCode();
59+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
60+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
61+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
62+
// deserialization callback is used.
63+
return _hashCode ?? GenerateHashCode();
6364
}
6465

65-
private int GenerateHashCode()
66+
/// <inheritdoc />
67+
public void OnDeserialization(object sender)
6668
{
67-
return type.GetHashCode(key, _factory);
69+
_hashCode = GenerateHashCode();
6870
}
6971

70-
/// <inheritdoc />
71-
public void OnDeserialization(object sender)
72+
private int GenerateHashCode()
7273
{
73-
// No attempt to compute directly here: the computation depends on another object which may not be
74-
// ready at this stage, if it itself or one of its dependency has an OnDeserialization.
75-
_hashCode = new Lazy<int>(GenerateHashCode);
74+
return type.GetHashCode(key, _factory);
7675
}
7776

7877
public object Key

src/NHibernate/Cache/FilterKey.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.Serialization;
35
using NHibernate.Engine;
46
using NHibernate.Impl;
57
using NHibernate.Type;
@@ -8,10 +10,16 @@
810
namespace NHibernate.Cache
911
{
1012
[Serializable]
11-
public class FilterKey
13+
public class FilterKey : IDeserializationCallback
1214
{
1315
private readonly string _filterName;
14-
private readonly Dictionary<string, TypedValue> _filterParameters = new Dictionary<string, TypedValue>();
16+
17+
// Sets and dictionaries are populated last during deserialization, causing them to be potentially empty
18+
// during the deserialization callback. This causes them to be unreliable when used in hashcode computations.
19+
// So better serialize them as other structures and use those other structures in hashcode computation.
20+
[NonSerialized]
21+
private Dictionary<string, TypedValue> _filterParameters = new Dictionary<string, TypedValue>();
22+
private IEnumerable<KeyValuePair<string, TypedValue>> _serializedFilterParameters;
1523

1624
// Since v5.2
1725
[Obsolete("Use overload taking a FilterImpl")]
@@ -40,7 +48,11 @@ public override int GetHashCode()
4048
{
4149
var result = 13;
4250
result = 37 * result + _filterName.GetHashCode();
43-
result = 37 * result + CollectionHelper.GetHashCode(_filterParameters, NamedParameterComparer.Instance);
51+
52+
// On deserialization, _filterParameters may be not yet populated. Use _serializedFilterParameters
53+
// if it is defined.
54+
var filterParameters = _serializedFilterParameters ?? _filterParameters;
55+
result = 37 * result + CollectionHelper.GetHashCode(filterParameters, NamedParameterComparer.Instance);
4456
return result;
4557
}
4658

@@ -53,7 +65,24 @@ public override bool Equals(object other)
5365
}
5466
if (!that._filterName.Equals(_filterName))
5567
return false;
56-
if (!CollectionHelper.DictionaryEquals<string, TypedValue>(that._filterParameters, _filterParameters))
68+
69+
// On deserialization, _filterParameters may be not yet populated. Use _serializedFilterParameters
70+
// if it is defined.
71+
var filterParameters = _filterParameters;
72+
if (_serializedFilterParameters != null)
73+
{
74+
filterParameters = new Dictionary<string, TypedValue>(_serializedFilterParameters.Count());
75+
foreach (var param in _serializedFilterParameters)
76+
filterParameters.Add(param.Key, param.Value);
77+
}
78+
var thatFilterParameters = that._filterParameters;
79+
if (that._serializedFilterParameters != null)
80+
{
81+
thatFilterParameters = new Dictionary<string, TypedValue>(that._serializedFilterParameters.Count());
82+
foreach (var param in that._serializedFilterParameters)
83+
thatFilterParameters.Add(param.Key, param.Value);
84+
}
85+
if (!CollectionHelper.DictionaryEquals<string, TypedValue>(thatFilterParameters, filterParameters))
5786
return false;
5887
return true;
5988
}
@@ -76,5 +105,21 @@ public static ISet<FilterKey> CreateFilterKeys(IDictionary<string, IFilter> enab
76105
}
77106
return result;
78107
}
108+
109+
[OnSerializing]
110+
private void OnSerializing(StreamingContext context)
111+
{
112+
_serializedFilterParameters = _filterParameters.ToArray();
113+
}
114+
115+
/// <inheritdoc />
116+
public void OnDeserialization(object sender)
117+
{
118+
_filterParameters = new Dictionary<string, TypedValue>(_serializedFilterParameters.Count());
119+
foreach (var param in _serializedFilterParameters)
120+
_filterParameters.Add(param.Key, param.Value);
121+
122+
_serializedFilterParameters = null;
123+
}
79124
}
80125
}

src/NHibernate/Cache/QueryKey.cs

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Runtime.Serialization;
45
using System.Text;
56
using NHibernate.Engine;
@@ -20,12 +21,21 @@ public class QueryKey : IDeserializationCallback
2021
private readonly object[] _values;
2122
private readonly int _firstRow = RowSelection.NoValue;
2223
private readonly int _maxRows = RowSelection.NoValue;
24+
25+
// Sets and dictionaries are populated last during deserialization, causing them to be potentially empty
26+
// during the deserialization callback. This causes them to be unreliable when used in hashcode computations.
27+
// So better also serialize them as other structures and use those other structures in hashcode computation.
28+
// We nonetheless need to also serialize them, because they are externally supplied and may use a custom
29+
// comparer, implementation, ...
2330
private readonly IDictionary<string, TypedValue> _namedParameters;
2431
private readonly ISet<FilterKey> _filters;
32+
private IEnumerable<KeyValuePair<string, TypedValue>> _serializedNamedParameters;
33+
private IEnumerable<FilterKey> _serializedFilters;
34+
2535
private readonly CacheableResultTransformer _customTransformer;
2636
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
2737
[NonSerialized]
28-
private Lazy<int> _hashCode;
38+
private int? _hashCode;
2939

3040
private int[] _multiQueriesFirstRows;
3141
private int[] _multiQueriesMaxRows;
@@ -61,8 +71,7 @@ public QueryKey(ISessionFactoryImplementor factory, SqlString queryString, Query
6171
_filters = filters;
6272
_customTransformer = customTransformer;
6373

64-
// No need to delay computation here, but we need the lazy for the deserialization case.
65-
_hashCode = new Lazy<int>(ComputeHashCode);
74+
_hashCode = ComputeHashCode();
6675
}
6776

6877
public CacheableResultTransformer ResultTransformer
@@ -131,12 +140,32 @@ public override bool Equals(object other)
131140
}
132141
}
133142

134-
if (!CollectionHelper.SetEquals(_filters, that._filters))
143+
// On deserialization, _namedParameters and _filters may be not yet populated. Use their _serialization
144+
// counterparts if they are defined.
145+
if (_serializedFilters != null || that._serializedFilters != null)
146+
{
147+
var filters = _serializedFilters ?? _filters;
148+
var thatFilters = that._serializedFilters ?? that._filters;
149+
// BagEquals is less efficient than a SetEquals, but if the sets are using a custom comparer, it
150+
// will cause less issues than rebuilding sets without the custom comparer.
151+
if (!CollectionHelper.BagEquals(filters, thatFilters))
152+
return false;
153+
}
154+
else if (!CollectionHelper.SetEquals(_filters, that._filters))
135155
{
136156
return false;
137157
}
138158

139-
if (!CollectionHelper.DictionaryEquals(_namedParameters, that._namedParameters))
159+
if (_serializedNamedParameters != null || that._serializedNamedParameters != null)
160+
{
161+
var namedParameters = _serializedNamedParameters ?? _namedParameters;
162+
var thatNamedParameters = that._serializedNamedParameters ?? that._namedParameters;
163+
// BagEquals is less efficient than a DictionaryEquals, but if the dictionaries are using a custom
164+
// comparer, it will cause less issues than rebuilding the dictionaries without the custom comparer.
165+
if (!CollectionHelper.BagEquals(namedParameters, thatNamedParameters, NamedParameterComparer.Instance))
166+
return false;
167+
}
168+
else if (!CollectionHelper.DictionaryEquals(_namedParameters, that._namedParameters))
140169
{
141170
return false;
142171
}
@@ -154,16 +183,27 @@ public override bool Equals(object other)
154183

155184
public override int GetHashCode()
156185
{
157-
// If the object is put in a set or dictionary during deserialization, the lazy will be null, and
158-
// we have to compute the hashcode on the fly.
159-
return _hashCode?.Value ?? ComputeHashCodeOnDeserialization();
186+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
187+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
188+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
189+
// deserialization callback is used.
190+
return _hashCode ?? ComputeHashCode();
160191
}
161192

162-
private int ComputeHashCodeOnDeserialization()
193+
/// <inheritdoc />
194+
public void OnDeserialization(object sender)
163195
{
164-
(_filters as IDeserializationCallback).OnDeserialization(this);
165-
(_namedParameters as IDeserializationCallback).OnDeserialization(this);
166-
return ComputeHashCode();
196+
_hashCode = ComputeHashCode();
197+
198+
_serializedNamedParameters = null;
199+
_serializedFilters = null;
200+
}
201+
202+
[OnSerializing]
203+
private void OnSerializing(StreamingContext context)
204+
{
205+
_serializedNamedParameters = _namedParameters?.ToArray();
206+
_serializedFilters = _filters?.ToArray();
167207
}
168208

169209
public int ComputeHashCode()
@@ -174,7 +214,10 @@ public int ComputeHashCode()
174214
result = 37 * result + _firstRow.GetHashCode();
175215
result = 37 * result + _maxRows.GetHashCode();
176216

177-
result = 37 * result + (_namedParameters == null ? 0 : CollectionHelper.GetHashCode(_namedParameters, NamedParameterComparer.Instance));
217+
// On deserialization, _namedParameters may be not yet populated. Use _serializedNamedParameters
218+
// if it is defined.
219+
var namedParameters = _serializedNamedParameters ?? _namedParameters;
220+
result = 37 * result + (namedParameters == null ? 0 : CollectionHelper.GetHashCode(namedParameters, NamedParameterComparer.Instance));
178221

179222
for (int i = 0; i < _types.Length; i++)
180223
{
@@ -201,12 +244,12 @@ public int ComputeHashCode()
201244
}
202245
}
203246

204-
if (_filters != null)
247+
// On deserialization, _filters may be not yet populated. Use _serializedFilters
248+
// if it is defined.
249+
var filters = _serializedFilters ?? _filters;
250+
if (filters != null)
205251
{
206-
foreach (var filter in _filters)
207-
{
208-
result = 37 * result + filter.GetHashCode();
209-
}
252+
result = 37 * result + CollectionHelper.GetHashCode(filters);
210253
}
211254

212255
result = 37 * result + (_customTransformer == null ? 0 : _customTransformer.GetHashCode());
@@ -215,16 +258,6 @@ public int ComputeHashCode()
215258
}
216259
}
217260

218-
/// <inheritdoc />
219-
public void OnDeserialization(object sender)
220-
{
221-
// No attempt to compute directly here: the computation depends on a complex graph of objects,
222-
// including hashsets and dictionaries, which are not yet populated. We could force them to get
223-
// populated by explicitly calling OnDeserialization on them, but then, we would also have to iterate
224-
// them and do the same on any dictionary or set contained by their elements (like FilterKey).
225-
_hashCode = new Lazy<int>(ComputeHashCode);
226-
}
227-
228261
public override string ToString()
229262
{
230263
StringBuilder buf = new StringBuilder()

src/NHibernate/Engine/AssociationKey.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ internal sealed class AssociationKey : IDeserializationCallback
1515
private readonly string propertyName;
1616
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
1717
[NonSerialized]
18-
private Lazy<int> _hashCode;
18+
private int? _hashCode;
1919

2020
public AssociationKey(EntityKey ownerKey, string propertyName)
2121
{
2222
this.ownerKey = ownerKey;
2323
this.propertyName = propertyName;
2424

25-
// No need to delay computation here, but we need the lazy for the deserialization case.
26-
_hashCode = new Lazy<int>(GenerateHashCode);
25+
_hashCode = GenerateHashCode();
2726
}
2827

2928
public override bool Equals(object that)
@@ -45,17 +44,17 @@ public override bool Equals(object that)
4544

4645
public override int GetHashCode()
4746
{
48-
// If the object is put in a set or dictionary during deserialization, the lazy will be null, and
49-
// we have to compute the hashcode on the fly.
50-
return _hashCode?.Value ?? GenerateHashCode();
47+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
48+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
49+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
50+
// deserialization callback is used.
51+
return _hashCode ?? GenerateHashCode();
5152
}
5253

5354
/// <inheritdoc />
5455
public void OnDeserialization(object sender)
5556
{
56-
// No attempt to compute directly here: the computation depends on another object which may not be
57-
// ready at this stage, if it itself or one of its dependency has an OnDeserialization.
58-
_hashCode = new Lazy<int>(GenerateHashCode);
57+
_hashCode = GenerateHashCode();
5958
}
6059

6160
private int GenerateHashCode()

src/NHibernate/Engine/CollectionKey.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Runtime.Serialization;
3-
using NHibernate.Engine;
43
using NHibernate.Impl;
54
using NHibernate.Persister.Collection;
65
using NHibernate.Type;
@@ -19,7 +18,7 @@ public sealed class CollectionKey : IDeserializationCallback
1918
private readonly ISessionFactoryImplementor factory;
2019
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
2120
[NonSerialized]
22-
private Lazy<int> _hashCode;
21+
private int? _hashCode;
2322

2423
public CollectionKey(ICollectionPersister persister, object key)
2524
: this(persister.Role, key, persister.KeyType, persister.Factory)
@@ -33,8 +32,7 @@ private CollectionKey(string role, object key, IType keyType, ISessionFactoryImp
3332
this.keyType = keyType;
3433
this.factory = factory;
3534

36-
// No need to delay computation here, but we need the lazy for the deserialization case.
37-
_hashCode = new Lazy<int>(GenerateHashCode);
35+
_hashCode = GenerateHashCode();
3836
}
3937

4038
public override bool Equals(object obj)
@@ -45,17 +43,17 @@ public override bool Equals(object obj)
4543

4644
public override int GetHashCode()
4745
{
48-
// If the object is put in a set or dictionary during deserialization, the lazy will be null, and
49-
// we have to compute the hashcode on the fly.
50-
return _hashCode?.Value ?? GenerateHashCode();
46+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
47+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
48+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
49+
// deserialization callback is used.
50+
return _hashCode ?? GenerateHashCode();
5151
}
5252

5353
/// <inheritdoc />
5454
public void OnDeserialization(object sender)
5555
{
56-
// No attempt to compute directly here: the computation depends on another object which may not be
57-
// ready at this stage, if it itself or one of its dependency has an OnDeserialization.
58-
_hashCode = new Lazy<int>(GenerateHashCode);
56+
_hashCode = GenerateHashCode();
5957
}
6058

6159
private int GenerateHashCode()

0 commit comments

Comments
 (0)