Skip to content

Commit ba8b2f8

Browse files
authored
fix(ShadowNodeRegistry): thread safety issue in ShadowNodeRegistry (microsoft#1337)
Various reports of an InvalidOperationException occurring sporadically on app start have been filed (microsoft#1326, microsoft#266). It seems that this is related to the enumeration of root nodes tags in a dictionary from a KeyCollection while the dictionary is being updated. This PR copies the keys to a list (we reuse the list to reduce heap impact) to get around the exception (at least until a better architecture for this shadow node registry is proposed). Fixes microsoft#1326 Fixes microsoft#266
1 parent 5c2dd98 commit ba8b2f8

File tree

2 files changed

+65
-4
lines changed

2 files changed

+65
-4
lines changed

ReactWindows/ReactNative.Shared.Tests/UIManager/ShadowNodeRegistryTests.cs

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
68

79
namespace ReactNative.Tests.UIManager
810
{
@@ -82,5 +84,36 @@ public void ShadowRegistryNode_AddNode()
8284
AssertEx.Throws<KeyNotFoundException>(() => registry.GetNode(i));
8385
}
8486
}
87+
88+
[Test]
89+
public async Task ShadowRegistryNode_AddRootNode_WhileEnumerating()
90+
{
91+
var n1 = new ReactShadowNode { ReactTag = 1 };
92+
var n2 = new ReactShadowNode { ReactTag = 2 };
93+
94+
var enter = new AutoResetEvent(false);
95+
var exit = new AutoResetEvent(false);
96+
97+
var registry = new ShadowNodeRegistry();
98+
99+
var task = Task.Run(() =>
100+
{
101+
enter.WaitOne();
102+
registry.AddRootNode(n2);
103+
exit.Set();
104+
});
105+
106+
registry.AddRootNode(n1);
107+
108+
foreach (var tag in registry.RootNodeTags)
109+
{
110+
enter.Set();
111+
exit.WaitOne();
112+
}
113+
114+
await task;
115+
116+
Assert.AreEqual(2, registry.RootNodeTags.Count);
117+
}
85118
}
86119
}

ReactWindows/ReactNative.Shared/UIManager/ShadowNodeRegistry.cs

+32-4
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,40 @@ namespace ReactNative.UIManager
1010
/// </summary>
1111
public class ShadowNodeRegistry
1212
{
13+
private readonly object _gate = new object();
14+
1315
private readonly IDictionary<int, ReactShadowNode> _tagsToCssNodes =
1416
new Dictionary<int, ReactShadowNode>();
1517

1618
private readonly IDictionary<int, bool> _rootTags =
1719
new Dictionary<int, bool>();
1820

21+
// The RootNodeTags API is called from UI thread instead of the
22+
// layout thread. Occasionally, we would get an exception related to
23+
// the enumeration of the Keys collection being disrupted by an
24+
// AddRootNode operation. This was especially likely in Debug mode, but
25+
// also could occur during a reload of the application from CodePush.
26+
// To get around this, we copy the key collection into this list.
27+
private List<int> _rootNodeTags = new List<int>();
28+
1929
/// <summary>
2030
/// The collection of root node tags.
2131
/// </summary>
22-
public ICollection<int> RootNodeTags
32+
public IReadOnlyList<int> RootNodeTags
2333
{
2434
get
2535
{
26-
return _rootTags.Keys;
36+
_rootNodeTags.Clear();
37+
38+
lock (_gate)
39+
{
40+
foreach (var tag in _rootTags.Keys)
41+
{
42+
_rootNodeTags.Add(tag);
43+
}
44+
}
45+
46+
return _rootNodeTags;
2747
}
2848
}
2949

@@ -38,7 +58,11 @@ public void AddRootNode(ReactShadowNode node)
3858

3959
var tag = node.ReactTag;
4060
_tagsToCssNodes[tag] = node;
41-
_rootTags[tag] = true;
61+
62+
lock (_gate)
63+
{
64+
_rootTags[tag] = true;
65+
}
4266
}
4367

4468
/// <summary>
@@ -54,7 +78,11 @@ public void RemoveRootNode(int tag)
5478
}
5579

5680
_tagsToCssNodes.Remove(tag);
57-
_rootTags.Remove(tag);
81+
82+
lock (_gate)
83+
{
84+
_rootTags.Remove(tag);
85+
}
5886
}
5987

6088
/// <summary>

0 commit comments

Comments
 (0)