Skip to content

Commit 59dcc7a

Browse files
avishmsftCopilot
andcommitted
Fix ObjectDisposedException race in PublishTestResultsCommand
Concurrent ##vso[results.publish] commands caused sporadic ObjectDisposedException because async tasks continued reading shared singleton state after the command lock released and the next Execute() overwrote it. Changes: - Create VssConnection once in Execute() and share it between LoadFeatureFlagState and the async publish task. Previously LoadFeatureFlagState created its own connection in a using block that disposed immediately, poisoning the singleton FeatureFlagService's connection. Parsers (JUnitParser, etc.) later called GetService<IFeatureFlagService>() and hit the dead connection. The async task takes ownership and disposes the connection when done. - RC1/RC4: Use CreateService instead of GetService for ITestDataPublisher, ITestResultsServer, and IFeatureFlagService so each async publish gets its own service instances (modern publisher path). - RC2: Capture all mutable instance fields as locals in Execute() before the async continuation to prevent subsequent calls from corrupting in-flight state. - Pass captured state as parameters to PublishTestRunDataAsync and helper methods instead of reading instance fields. - TriggerCoverageMergeJob: use passed-in locals instead of instance fields but preserve original fire-and-forget behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 21eed20 commit 59dcc7a

2 files changed

Lines changed: 80 additions & 42 deletions

File tree

src/Agent.Worker/TestResults/ResultsCommandExtension.cs

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,42 @@ public void Execute(IExecutionContext context, Command command)
7272

7373
_telemetryProperties = new Dictionary<string, object>();
7474
PopulateTelemetryData();
75-
LoadFeatureFlagState();
75+
76+
// Create the connection once and share it between LoadFeatureFlagState and the
77+
// async publish task. Previously LoadFeatureFlagState created its own connection
78+
// in a using block that disposed immediately, poisoning the singleton
79+
// FeatureFlagService's _connection. Parsers (JUnitParser, etc.) later called
80+
// GetService<IFeatureFlagService>() and hit the dead connection.
81+
// The async task takes ownership of this connection and disposes it when done.
82+
var connection = WorkerUtilities.GetVssConnection(context);
83+
LoadFeatureFlagState(connection);
7684

7785
LoadPublishTestResultsInputs(context, eventProperties, data);
7886

7987
string teamProject = context.Variables.System_TeamProject;
8088

8189
TestRunContext runContext = CreateTestRunContext();
90+
91+
var executionContext = _executionContext;
92+
var testResultFiles = _testResultFiles;
93+
var testRunner = _testRunner;
94+
var testCaseResults = _testCaseResults;
95+
var testPlanId = _testPlanId;
96+
var runTitle = _runTitle;
97+
var publishTestResultsLibFeatureState = _publishTestResultsLibFeatureState;
98+
var triggerCoverageMergeJobFeatureState = _triggerCoverageMergeJobFeatureState;
99+
var failTaskOnFailedTests = _failTaskOnFailedTests;
100+
var publishOptions = GetPublishOptions();
101+
var telemetryProperties = _telemetryProperties;
102+
82103
var commandContext = context.GetHostContext().CreateService<IAsyncCommandContext>();
83104
commandContext.InitializeCommandContext(context, StringUtil.Loc("PublishTestResults"));
84-
commandContext.Task = PublishTestRunDataAsync(teamProject, runContext);
85-
_executionContext.AsyncCommands.Add(commandContext);
105+
commandContext.Task = PublishTestRunDataAsync(
106+
teamProject, runContext, executionContext, testResultFiles, testRunner,
107+
testCaseResults, testPlanId, runTitle, publishTestResultsLibFeatureState,
108+
triggerCoverageMergeJobFeatureState, failTaskOnFailedTests,
109+
publishOptions, telemetryProperties, connection);
110+
executionContext.AsyncCommands.Add(commandContext);
86111
}
87112

88113
private void LoadPublishTestResultsInputs(IExecutionContext context, Dictionary<string, string> eventProperties, string data)
@@ -303,47 +328,63 @@ private PublishOptions GetPublishOptions()
303328
return publishOptions;
304329
}
305330

306-
private async Task PublishTestRunDataAsync(string teamProject, TestRunContext testRunContext)
331+
private async Task PublishTestRunDataAsync(
332+
string teamProject,
333+
TestRunContext testRunContext,
334+
IExecutionContext executionContext,
335+
List<string> testResultFiles,
336+
string testRunner,
337+
TestCaseResult[] testCaseResults,
338+
string testPlanId,
339+
string runTitle,
340+
bool publishTestResultsLibFeatureState,
341+
bool triggerCoverageMergeJobFeatureState,
342+
bool failTaskOnFailedTests,
343+
PublishOptions publishOptions,
344+
Dictionary<string, object> telemetryProperties,
345+
VssConnection connection)
307346
{
308347
bool isTestRunOutcomeFailed = false;
309348

310-
_telemetryProperties.Add("UsePublishTestResultsLib", _publishTestResultsLibFeatureState);
311-
using (var connection = WorkerUtilities.GetVssConnection(_executionContext))
349+
telemetryProperties.Add("UsePublishTestResultsLib", publishTestResultsLibFeatureState);
350+
using (connection)
312351
{
313-
314-
//This check is to determine to use "Microsoft.TeamFoundation.PublishTestResults" Library or the agent code to parse and publish the test results.
315-
if (_publishTestResultsLibFeatureState)
352+
// Use CreateService instead of GetService to get per-invocation instances.
353+
// GetService returns a cached singleton, so a second Execute() call would
354+
// reinitialize the same instance while the first async task is still in-flight,
355+
// causing ObjectDisposedException when the second using block disposes the connection.
356+
if (publishTestResultsLibFeatureState)
316357
{
317-
var publisher = _executionContext.GetHostContext().GetService<ITestDataPublisher>();
318-
publisher.InitializePublisher(_executionContext, teamProject, connection, _testRunner);
358+
var publisher = executionContext.GetHostContext().CreateService<ITestDataPublisher>();
359+
publisher.InitializePublisher(executionContext, teamProject, connection, testRunner);
319360

320-
if (!_testCaseResults.IsNullOrEmpty() && !_testPlanId.IsNullOrEmpty())
361+
if (!testCaseResults.IsNullOrEmpty() && !testPlanId.IsNullOrEmpty())
321362
{
322-
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, _testResultFiles, _testCaseResults, GetPublishOptions(), _executionContext.CancellationToken);
363+
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, testResultFiles, testCaseResults, publishOptions, executionContext.CancellationToken);
323364
}
324365
else
325366
{
326-
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, _testResultFiles, GetPublishOptions(), _executionContext.CancellationToken);
367+
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, testResultFiles, publishOptions, executionContext.CancellationToken);
327368
}
328369
}
329370
else
330371
{
331-
var publisher = _executionContext.GetHostContext().GetService<ILegacyTestRunDataPublisher>();
332-
publisher.InitializePublisher(_executionContext, teamProject, connection, _testRunner, _publishRunLevelAttachments);
372+
var publisher = executionContext.GetHostContext().CreateService<ILegacyTestRunDataPublisher>();
373+
publisher.InitializePublisher(executionContext, teamProject, connection, testRunner, publishOptions.IsAddTestRunAttachments);
333374

334-
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, _testResultFiles, _runTitle, _executionContext.Variables.Build_BuildId, _mergeResults);
375+
isTestRunOutcomeFailed = await publisher.PublishAsync(testRunContext, testResultFiles, runTitle, executionContext.Variables.Build_BuildId, publishOptions.IsMergeTestResultsToSingleRun);
335376
}
336377

337-
if (isTestRunOutcomeFailed && _failTaskOnFailedTests)
378+
if (isTestRunOutcomeFailed && failTaskOnFailedTests)
338379
{
339-
_executionContext.Result = TaskResult.Failed;
340-
_executionContext.Error(StringUtil.Loc("FailedTestsInResults"));
380+
executionContext.Result = TaskResult.Failed;
381+
executionContext.Error(StringUtil.Loc("FailedTestsInResults"));
341382
}
342383

343-
await PublishEventsAsync(connection);
344-
if (_triggerCoverageMergeJobFeatureState)
384+
await PublishEventsAsync(connection, executionContext, telemetryProperties);
385+
if (triggerCoverageMergeJobFeatureState)
345386
{
346-
TriggerCoverageMergeJob(_testResultFiles, _executionContext);
387+
TriggerCoverageMergeJob(testResultFiles, executionContext);
347388
}
348389
}
349390
}
@@ -354,7 +395,7 @@ private void TriggerCoverageMergeJob(List<string> resultFilesInput, IExecutionCo
354395
try
355396
{
356397
ITestResultsServer _testResultsServer = context.GetHostContext().GetService<ITestResultsServer>();
357-
using (var connection = WorkerUtilities.GetVssConnection(_executionContext))
398+
using (var connection = WorkerUtilities.GetVssConnection(context))
358399
{
359400
foreach (var resultFile in resultFilesInput)
360401
{
@@ -374,14 +415,14 @@ private void TriggerCoverageMergeJob(List<string> resultFilesInput, IExecutionCo
374415
Path.GetExtension(file).Equals(".coverage", StringComparison.OrdinalIgnoreCase)
375416
)
376417
{
377-
_testResultsServer.InitializeServer(connection, _executionContext);
418+
_testResultsServer.InitializeServer(connection, context);
378419
try
379420
{
380-
var codeCoverageResults = _testResultsServer.UpdateCodeCoverageSummaryAsync(connection, _executionContext.Variables.System_TeamProjectId.ToString(), _executionContext.Variables.Build_BuildId.GetValueOrDefault());
421+
var codeCoverageResults = _testResultsServer.UpdateCodeCoverageSummaryAsync(connection, context.Variables.System_TeamProjectId.ToString(), context.Variables.Build_BuildId.GetValueOrDefault());
381422
}
382423
catch (Exception e)
383424
{
384-
_executionContext.Section($"Could not queue code coverage merge:{e}");
425+
context.Section($"Could not queue code coverage merge:{e}");
385426
}
386427
}
387428
}
@@ -391,28 +432,28 @@ private void TriggerCoverageMergeJob(List<string> resultFilesInput, IExecutionCo
391432
}
392433
catch (Exception e)
393434
{
394-
_executionContext.Debug($"Exception in Method:{e.Message}");
435+
context.Debug($"Exception in Method:{e.Message}");
395436
}
396437
}
397438

398-
private async Task PublishEventsAsync(VssConnection connection)
439+
private async Task PublishEventsAsync(VssConnection connection, IExecutionContext executionContext, Dictionary<string, object> telemetryProperties)
399440
{
400441
try
401442
{
402443
CustomerIntelligenceEvent ciEvent = new CustomerIntelligenceEvent()
403444
{
404445
Area = _telemetryArea,
405446
Feature = _telemetryFeature,
406-
Properties = _telemetryProperties
447+
Properties = telemetryProperties
407448
};
408449

409-
var ciService = _executionContext.GetHostContext().GetService<ICustomerIntelligenceServer>();
450+
var ciService = executionContext.GetHostContext().GetService<ICustomerIntelligenceServer>();
410451
ciService.Initialize(connection);
411452
await ciService.PublishEventsAsync(new CustomerIntelligenceEvent[] { ciEvent });
412453
}
413454
catch (Exception ex)
414455
{
415-
_executionContext.Debug(StringUtil.Loc("TelemetryCommandFailed", ex.Message));
456+
executionContext.Debug(StringUtil.Loc("TelemetryCommandFailed", ex.Message));
416457
}
417458
}
418459

@@ -432,15 +473,12 @@ private void PopulateTelemetryData()
432473
}
433474
}
434475

435-
private void LoadFeatureFlagState()
476+
private void LoadFeatureFlagState(VssConnection connection)
436477
{
437-
using (var connection = WorkerUtilities.GetVssConnection(_executionContext))
438-
{
439-
var featureFlagService = _executionContext.GetHostContext().GetService<IFeatureFlagService>();
440-
featureFlagService.InitializeFeatureService(_executionContext, connection);
441-
_publishTestResultsLibFeatureState = featureFlagService.GetFeatureFlagState(TestResultsConstants.UsePublishTestResultsLibFeatureFlag, TestResultsConstants.TFSServiceInstanceGuid);
442-
_triggerCoverageMergeJobFeatureState = featureFlagService.GetFeatureFlagState(CodeCoverageConstants.TriggerCoverageMergeJobFF, TestResultsConstants.TFSServiceInstanceGuid);
443-
}
478+
var featureFlagService = _executionContext.GetHostContext().GetService<IFeatureFlagService>();
479+
featureFlagService.InitializeFeatureService(_executionContext, connection);
480+
_publishTestResultsLibFeatureState = featureFlagService.GetFeatureFlagState(TestResultsConstants.UsePublishTestResultsLibFeatureFlag, TestResultsConstants.TFSServiceInstanceGuid);
481+
_triggerCoverageMergeJobFeatureState = featureFlagService.GetFeatureFlagState(CodeCoverageConstants.TriggerCoverageMergeJobFF, TestResultsConstants.TFSServiceInstanceGuid);
444482
}
445483
}
446484

src/Agent.Worker/TestResults/TestDataPublisher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ public void InitializePublisher(IExecutionContext context, string projectName, V
5454
_testRunner = testRunner;
5555
_testRunPublisher = new TestRunPublisher(connection, new CommandTraceListener(context));
5656
_testLogStore = new TestLogStore(connection, new CommandTraceListener(context));
57-
_testResultsServer = HostContext.GetService<ITestResultsServer>();
57+
_testResultsServer = HostContext.CreateService<ITestResultsServer>();
5858
_testResultsServer.InitializeServer(connection, _executionContext);
5959
var extensionManager = HostContext.GetService<IExtensionManager>();
60-
_featureFlagService = HostContext.GetService<IFeatureFlagService>();
60+
_featureFlagService = HostContext.CreateService<IFeatureFlagService>();
6161
_featureFlagService.InitializeFeatureService(_executionContext, connection);
6262
_calculateTestRunSummary = _featureFlagService.GetFeatureFlagState(TestResultsConstants.CalculateTestRunSummaryFeatureFlag, TestResultsConstants.TFSServiceInstanceGuid);
6363
_isFlakyCheckEnabled = _featureFlagService.GetFeatureFlagState(TestResultsConstants.EnableFlakyCheckInAgentFeatureFlag, TestResultsConstants.TCMServiceInstanceGuid); ;

0 commit comments

Comments
 (0)