Skip to content

Commit 512c316

Browse files
Fix race condition inside DataCollectionAttachmentManager
1 parent 44feee0 commit 512c316

File tree

6 files changed

+155
-30
lines changed

6 files changed

+155
-30
lines changed

src/Microsoft.TestPlatform.Common/DataCollection/DataCollectionAttachmentManager.cs

+27-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace Microsoft.VisualStudio.TestPlatform.Common.DataCollector;
55

66
using System;
7+
using System.Collections.Concurrent;
78
using System.Collections.Generic;
89
using System.ComponentModel;
910
using System.Diagnostics;
@@ -14,18 +15,31 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.DataCollector;
1415
using System.Threading.Tasks;
1516

1617
using Interfaces;
17-
using ObjectModel;
18+
1819
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
1920
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
2021
using Microsoft.VisualStudio.TestPlatform.Utilities;
2122
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;
2223

24+
using ObjectModel;
25+
2326
/// <summary>
2427
/// Manages file transfer from data collector to test runner service.
28+
///
29+
/// Events are handled sequentially so it's not possible have parallel AddAttachment/GetAttachments for the same DataCollectionContext.
30+
/// DataCollectionContext can be a session context(session start/end) or a test case context(test case start/end).
31+
///
32+
/// We have two type of events that will fire a collection of files "session end" and "test case end".
33+
/// File are sent and copied/moved in parallel using async tasks, for these reason we need to use an async structure ConcurrentDictionary
34+
/// to be able to handle parallel test case start/end events(if host run tests in parallel).
35+
///
36+
/// We could avoid to use ConcurrentDictionary for the list of the attachment sets of a specific DataCollectionContext, but
37+
/// we don't know how the user will implement the datacollector and they could send file out of events(wrong usage, no more expected sequential access AddAttachment->GetAttachments),
38+
/// so we prefer protect every collection. This not means that outcome will be "always correct"(file attached in a correct way) but at least we avoid exceptions.
2539
/// </summary>
2640
internal class DataCollectionAttachmentManager : IDataCollectionAttachmentManager
2741
{
28-
private static readonly object AttachmentTaskLock = new();
42+
private readonly object _attachmentTaskLock = new();
2943

3044
#region Fields
3145

@@ -42,7 +56,7 @@ internal class DataCollectionAttachmentManager : IDataCollectionAttachmentManage
4256
/// <summary>
4357
/// Attachment transfer tasks associated with a given datacollection context.
4458
/// </summary>
45-
private readonly Dictionary<DataCollectionContext, List<Task>> _attachmentTasks;
59+
private readonly ConcurrentDictionary<DataCollectionContext, List<Task>> _attachmentTasks;
4660

4761
/// <summary>
4862
/// Use to cancel attachment transfers if test run is canceled.
@@ -74,8 +88,8 @@ protected DataCollectionAttachmentManager(IFileHelper fileHelper)
7488
{
7589
_fileHelper = fileHelper;
7690
_cancellationTokenSource = new CancellationTokenSource();
77-
_attachmentTasks = new Dictionary<DataCollectionContext, List<Task>>();
78-
AttachmentSets = new Dictionary<DataCollectionContext, Dictionary<Uri, AttachmentSet>>();
91+
_attachmentTasks = new ConcurrentDictionary<DataCollectionContext, List<Task>>();
92+
AttachmentSets = new ConcurrentDictionary<DataCollectionContext, ConcurrentDictionary<Uri, AttachmentSet>>();
7993
}
8094

8195
#endregion
@@ -90,7 +104,7 @@ protected DataCollectionAttachmentManager(IFileHelper fileHelper)
90104
/// <summary>
91105
/// Gets the attachment sets for the given datacollection context.
92106
/// </summary>
93-
internal Dictionary<DataCollectionContext, Dictionary<Uri, AttachmentSet>> AttachmentSets
107+
internal ConcurrentDictionary<DataCollectionContext, ConcurrentDictionary<Uri, AttachmentSet>> AttachmentSets
94108
{
95109
get; private set;
96110
}
@@ -155,8 +169,8 @@ public List<AttachmentSet> GetAttachments(DataCollectionContext dataCollectionCo
155169
if (AttachmentSets.TryGetValue(dataCollectionContext, out var uriAttachmentSetMap))
156170
{
157171
attachments = uriAttachmentSetMap.Values.ToList();
158-
_attachmentTasks.Remove(dataCollectionContext);
159-
AttachmentSets.Remove(dataCollectionContext);
172+
_attachmentTasks.TryRemove(dataCollectionContext, out _);
173+
AttachmentSets.TryRemove(dataCollectionContext, out _);
160174
}
161175

162176
return attachments;
@@ -180,14 +194,14 @@ public void AddAttachment(FileTransferInformation fileTransferInfo, AsyncComplet
180194

181195
if (!AttachmentSets.ContainsKey(fileTransferInfo.Context))
182196
{
183-
var uriAttachmentSetMap = new Dictionary<Uri, AttachmentSet>();
184-
AttachmentSets.Add(fileTransferInfo.Context, uriAttachmentSetMap);
185-
_attachmentTasks.Add(fileTransferInfo.Context, new List<Task>());
197+
var uriAttachmentSetMap = new ConcurrentDictionary<Uri, AttachmentSet>();
198+
AttachmentSets.TryAdd(fileTransferInfo.Context, uriAttachmentSetMap);
199+
_attachmentTasks.TryAdd(fileTransferInfo.Context, new List<Task>());
186200
}
187201

188202
if (!AttachmentSets[fileTransferInfo.Context].ContainsKey(uri))
189203
{
190-
AttachmentSets[fileTransferInfo.Context].Add(uri, new AttachmentSet(uri, friendlyName));
204+
AttachmentSets[fileTransferInfo.Context].TryAdd(uri, new AttachmentSet(uri, friendlyName));
191205
}
192206

193207
AddNewFileTransfer(fileTransferInfo, sendFileCompletedCallback, uri, friendlyName);
@@ -327,7 +341,7 @@ private void AddNewFileTransfer(FileTransferInformation fileTransferInfo, AsyncC
327341
{
328342
if (t.Exception == null)
329343
{
330-
lock (AttachmentTaskLock)
344+
lock (_attachmentTaskLock)
331345
{
332346
AttachmentSets[fileTransferInfo.Context][uri].Attachments.Add(UriDataAttachment.CreateFrom(localFilePath, fileTransferInfo.Description));
333347
}

src/Microsoft.TestPlatform.Common/DataCollection/DataCollectionManager.cs

+18
Original file line numberDiff line numberDiff line change
@@ -768,10 +768,28 @@ private void RemoveDataCollectors(IReadOnlyCollection<DataCollectorInformation>
768768

769769
private void LogAttachments(List<AttachmentSet> attachmentSets)
770770
{
771+
if (attachmentSets is null)
772+
{
773+
EqtTrace.Error("DataCollectionManager.LogAttachments: Unexpected null attachmentSets.");
774+
return;
775+
}
776+
771777
foreach (var entry in attachmentSets)
772778
{
779+
if (entry is null)
780+
{
781+
EqtTrace.Error("DataCollectionManager.LogAttachments: Unexpected null entry inside attachmentSets.");
782+
continue;
783+
}
784+
773785
foreach (var file in entry.Attachments)
774786
{
787+
if (file is null)
788+
{
789+
EqtTrace.Error("DataCollectionManager.LogAttachments: Unexpected null file inside entry attachments.");
790+
continue;
791+
}
792+
775793
EqtTrace.Verbose(
776794
"Test Attachment Description: Collector:'{0}' Uri:'{1}' Description:'{2}' Uri:'{3}' ",
777795
entry.DisplayName,

src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionRequestHandler.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected DataCollectionRequestHandler(IMessageSink messageSink, IRequestData re
7676
new SocketCommunicationManager(),
7777
messageSink,
7878
DataCollectionManager.Create(messageSink, requestData),
79-
new DataCollectionTestCaseEventHandler(),
79+
new DataCollectionTestCaseEventHandler(messageSink),
8080
JsonDataSerializer.Instance,
8181
new FileHelper(),
8282
requestData)
@@ -162,7 +162,7 @@ public static DataCollectionRequestHandler Create(
162162
communicationManager,
163163
messageSink,
164164
DataCollectionManager.Create(messageSink, requestData),
165-
new DataCollectionTestCaseEventHandler(),
165+
new DataCollectionTestCaseEventHandler(messageSink),
166166
JsonDataSerializer.Instance,
167167
new FileHelper(),
168168
requestData);
@@ -362,7 +362,7 @@ private void HandleBeforeTestRunStart(Message message)
362362
}
363363
catch (Exception e)
364364
{
365-
EqtTrace.Error("DataCollectionRequestHandler.HandleBeforeTestRunStart : Error occurred during initialization of TestHost : {0}", e);
365+
EqtTrace.Error("DataCollectionRequestHandler.HandleBeforeTestRunStart : Error occurred during test case events handling: {0}.", e);
366366
}
367367
},
368368
_cancellationTokenSource.Token);

src/Microsoft.TestPlatform.CommunicationUtilities/DataCollectionTestCaseEventHandler.cs

+45-9
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33

44
namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.DataCollection;
55

6+
using System;
7+
using System.Collections.ObjectModel;
68
using System.Net;
79

810
using Common.DataCollector;
11+
912
using Microsoft.VisualStudio.TestPlatform.Common.DataCollector.Interfaces;
1013
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;
11-
using ObjectModel;
1214
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
1315
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
16+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
17+
18+
using ObjectModel;
1419

1520
/// <summary>
1621
/// The test case data collection request handler.
@@ -20,26 +25,27 @@ internal class DataCollectionTestCaseEventHandler : IDataCollectionTestCaseEvent
2025
private readonly ICommunicationManager _communicationManager;
2126
private readonly IDataCollectionManager _dataCollectionManager;
2227
private readonly IDataSerializer _dataSerializer;
28+
private readonly IMessageSink _messageSink;
2329

2430
/// <summary>
2531
/// Initializes a new instance of the <see cref="DataCollectionTestCaseEventHandler"/> class.
2632
/// </summary>
27-
internal DataCollectionTestCaseEventHandler()
28-
: this(new SocketCommunicationManager(), DataCollectionManager.Instance, JsonDataSerializer.Instance)
29-
{
30-
}
33+
internal DataCollectionTestCaseEventHandler(IMessageSink messageSink)
34+
: this(messageSink, new SocketCommunicationManager(), DataCollectionManager.Instance, JsonDataSerializer.Instance)
35+
{ }
3136

3237
/// <summary>
3338
/// Initializes a new instance of the <see cref="DataCollectionTestCaseEventHandler"/> class.
3439
/// </summary>
3540
/// <param name="communicationManager">Communication manager implementation.</param>
3641
/// <param name="dataCollectionManager">Data collection manager implementation.</param>
3742
/// <param name="dataSerializer">Serializer for serialization and deserialization of the messages.</param>
38-
internal DataCollectionTestCaseEventHandler(ICommunicationManager communicationManager, IDataCollectionManager dataCollectionManager, IDataSerializer dataSerializer)
43+
internal DataCollectionTestCaseEventHandler(IMessageSink messageSink, ICommunicationManager communicationManager, IDataCollectionManager dataCollectionManager, IDataSerializer dataSerializer)
3944
{
4045
_communicationManager = communicationManager;
4146
_dataCollectionManager = dataCollectionManager;
4247
_dataSerializer = dataSerializer;
48+
_messageSink = messageSink;
4349
}
4450

4551
/// <inheritdoc />
@@ -79,7 +85,17 @@ public void ProcessRequests()
7985
}
8086

8187
var testCaseStartEventArgs = _dataSerializer.DeserializePayload<TestCaseStartEventArgs>(message);
82-
_dataCollectionManager.TestCaseStarted(testCaseStartEventArgs);
88+
89+
try
90+
{
91+
_dataCollectionManager.TestCaseStarted(testCaseStartEventArgs);
92+
}
93+
catch (Exception ex)
94+
{
95+
_messageSink.SendMessage(new DataCollectionMessageEventArgs(TestMessageLevel.Error, $"Error occurred during TestCaseStarted event handling: {ex}"));
96+
EqtTrace.Error($"DataCollectionTestCaseEventHandler.ProcessRequests: Error occurred during TestCaseStarted event handling: {ex}");
97+
}
98+
8399
_communicationManager.SendMessage(MessageType.DataCollectionTestStartAck);
84100

85101
if (EqtTrace.IsInfoEnabled)
@@ -96,7 +112,19 @@ public void ProcessRequests()
96112
}
97113

98114
var testCaseEndEventArgs = _dataSerializer.DeserializePayload<TestCaseEndEventArgs>(message);
99-
var attachmentSets = _dataCollectionManager.TestCaseEnded(testCaseEndEventArgs);
115+
116+
Collection<AttachmentSet> attachmentSets;
117+
try
118+
{
119+
attachmentSets = _dataCollectionManager.TestCaseEnded(testCaseEndEventArgs);
120+
}
121+
catch (Exception ex)
122+
{
123+
_messageSink.SendMessage(new DataCollectionMessageEventArgs(TestMessageLevel.Error, $"Error occurred during DataCollectionTestEnd event handling: {ex}"));
124+
EqtTrace.Error($"DataCollectionTestCaseEventHandler.ProcessRequests: Error occurred during DataCollectionTestEnd event handling: {ex}");
125+
attachmentSets = new Collection<AttachmentSet>();
126+
}
127+
100128
_communicationManager.SendMessage(MessageType.DataCollectionTestEndResult, attachmentSets);
101129

102130
if (EqtTrace.IsInfoEnabled)
@@ -114,7 +142,15 @@ public void ProcessRequests()
114142
EqtTrace.Info("DataCollectionTestCaseEventHandler: Test session ended");
115143
}
116144

117-
Close();
145+
try
146+
{
147+
Close();
148+
}
149+
catch (Exception ex)
150+
{
151+
_messageSink.SendMessage(new DataCollectionMessageEventArgs(TestMessageLevel.Error, $"Error occurred during SessionEnd event handling: {ex}"));
152+
EqtTrace.Error($"DataCollectionTestCaseEventHandler.ProcessRequests: Error occurred during SessionEnd event handling: {ex}");
153+
}
118154

119155
break;
120156

test/Microsoft.TestPlatform.CommunicationUtilities.UnitTests/DataCollectionTestCaseEventHandlerTests.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ public class DataCollectionTestCaseEventHandlerTests
2727
private readonly Mock<IDataCollectionManager> _mockDataCollectionManager;
2828
private readonly DataCollectionTestCaseEventHandler _requestHandler;
2929
private readonly Mock<IDataSerializer> _dataSerializer;
30+
private readonly Mock<IMessageSink> _messageSink;
3031

3132
public DataCollectionTestCaseEventHandlerTests()
3233
{
3334
_mockCommunicationManager = new Mock<ICommunicationManager>();
3435
_mockDataCollectionManager = new Mock<IDataCollectionManager>();
3536
_dataSerializer = new Mock<IDataSerializer>();
36-
_requestHandler = new DataCollectionTestCaseEventHandler(_mockCommunicationManager.Object, new Mock<IDataCollectionManager>().Object, _dataSerializer.Object);
37+
_messageSink = new Mock<IMessageSink>();
38+
_requestHandler = new DataCollectionTestCaseEventHandler(_messageSink.Object, _mockCommunicationManager.Object, new Mock<IDataCollectionManager>().Object, _dataSerializer.Object);
3739
}
3840

3941
[TestMethod]
@@ -91,7 +93,7 @@ public void CloseShouldThrowExceptionIfThrownByCommunicationManager()
9193
[TestMethod]
9294
public void CloseShouldNotThrowExceptionIfCommunicationManagerIsNull()
9395
{
94-
var requestHandler = new DataCollectionTestCaseEventHandler(null, new Mock<IDataCollectionManager>().Object, _dataSerializer.Object);
96+
var requestHandler = new DataCollectionTestCaseEventHandler(_messageSink.Object, null, new Mock<IDataCollectionManager>().Object, _dataSerializer.Object);
9597

9698
requestHandler.Close();
9799

@@ -107,7 +109,7 @@ public void ProcessRequestsShouldProcessBeforeTestCaseStartEvent()
107109

108110
_mockCommunicationManager.SetupSequence(x => x.ReceiveMessage()).Returns(message).Returns(new Message() { MessageType = MessageType.SessionEnd, Payload = "false" });
109111

110-
var requestHandler = new DataCollectionTestCaseEventHandler(_mockCommunicationManager.Object, _mockDataCollectionManager.Object, _dataSerializer.Object);
112+
var requestHandler = new DataCollectionTestCaseEventHandler(_messageSink.Object, _mockCommunicationManager.Object, _mockDataCollectionManager.Object, _dataSerializer.Object);
111113

112114
requestHandler.ProcessRequests();
113115

@@ -124,7 +126,7 @@ public void ProcessRequestsShouldProcessAfterTestCaseCompleteEvent()
124126

125127
_mockCommunicationManager.SetupSequence(x => x.ReceiveMessage()).Returns(message).Returns(new Message() { MessageType = MessageType.SessionEnd, Payload = "false" });
126128

127-
var requestHandler = new DataCollectionTestCaseEventHandler(_mockCommunicationManager.Object, _mockDataCollectionManager.Object, _dataSerializer.Object);
129+
var requestHandler = new DataCollectionTestCaseEventHandler(_messageSink.Object, _mockCommunicationManager.Object, _mockDataCollectionManager.Object, _dataSerializer.Object);
128130

129131
requestHandler.ProcessRequests();
130132

0 commit comments

Comments
 (0)