Migrate FindItemsFromPackages to IMultiThreadableTask#53952
Migrate FindItemsFromPackages to IMultiThreadableTask#53952SimaTian wants to merge 2 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Migrates FindItemsFromPackages to support MSBuild multithreaded execution by implementing IMultiThreadableTask and adding dedicated concurrency-focused unit tests.
Changes:
- Update
FindItemsFromPackagesto implementIMultiThreadableTaskand expose aTaskEnvironmentproperty. - Add a new unit test suite validating interface implementation plus concurrent execution behavior via a start-gate pattern.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/Tasks/Microsoft.NET.Build.Tasks/FindItemsFromPackages.cs |
Implements IMultiThreadableTask and adds TaskEnvironment plumbing for multithreaded execution. |
src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFindItemsFromPackagesMultiThreading.cs |
Adds behavioral + concurrency tests to validate correctness under parallel execution. |
|
|
||
| var multiThreadable = task as IMultiThreadableTask; | ||
| multiThreadable.Should().NotBeNull(); | ||
|
|
||
| var property = typeof(FindItemsFromPackages).GetProperty(nameof(IMultiThreadableTask.TaskEnvironment)); | ||
| property.Should().NotBeNull(); | ||
| property.CanRead.Should().BeTrue(); | ||
| property.CanWrite.Should().BeTrue(); |
There was a problem hiding this comment.
This test name says the TaskEnvironment property "can be set", but the test only checks via reflection that the property exists and is readable/writable. Consider actually setting TaskEnvironment on the task instance and asserting that the getter returns the same value (or that it can be accessed without throwing).
| var multiThreadable = task as IMultiThreadableTask; | |
| multiThreadable.Should().NotBeNull(); | |
| var property = typeof(FindItemsFromPackages).GetProperty(nameof(IMultiThreadableTask.TaskEnvironment)); | |
| property.Should().NotBeNull(); | |
| property.CanRead.Should().BeTrue(); | |
| property.CanWrite.Should().BeTrue(); | |
| var multiThreadable = task as IMultiThreadableTask; | |
| multiThreadable.Should().NotBeNull(); | |
| var property = typeof(FindItemsFromPackages).GetProperty(nameof(IMultiThreadableTask.TaskEnvironment)); | |
| property.Should().NotBeNull(); | |
| property.CanRead.Should().BeTrue(); | |
| property.CanWrite.Should().BeTrue(); | |
| var taskEnvironment = Activator.CreateInstance(property.PropertyType); | |
| taskEnvironment.Should().NotBeNull(); | |
| property.SetValue(task, taskEnvironment); | |
| property.GetValue(task).Should().BeSameAs(taskEnvironment); |
| // Start-gate pattern to ensure maximum concurrency | ||
| var startGate = new ManualResetEventSlim(false); | ||
| var taskList = new List<Task>(); | ||
|
|
||
| for (int i = 0; i < threadCount; i++) | ||
| { | ||
| int taskIndex = i; | ||
| taskList.Add(Task.Run(() => | ||
| { | ||
| // Wait for start signal | ||
| startGate.Wait(); | ||
|
|
||
| try | ||
| { | ||
| bool success = tasks[taskIndex].Execute(); | ||
| success.Should().BeTrue($"Task {taskIndex} should succeed"); | ||
| results.Add(tasks[taskIndex].ItemsFromPackages); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| exceptions.Add(ex); | ||
| } | ||
| })); | ||
| } | ||
|
|
||
| // Release all threads simultaneously | ||
| startGate.Set(); | ||
|
|
||
| // Wait for all tasks to complete | ||
| Task.WaitAll(taskList.ToArray()); | ||
|
|
There was a problem hiding this comment.
These concurrency tests use startGate.Wait() and Task.WaitAll() without any timeout. If a task deadlocks or a thread never starts, this can hang the test run indefinitely. Consider adding timeouts (and failing the test with a clear message) and disposing the ManualResetEventSlim (e.g., via using) to avoid CI hangs/leaks.
| var startGate = new ManualResetEventSlim(false); | ||
| var taskList = new List<Task>(); | ||
|
|
||
| for (int i = 0; i < threadCount; i++) | ||
| { | ||
| int taskIndex = i; | ||
| taskList.Add(Task.Run(() => | ||
| { | ||
| startGate.Wait(); | ||
|
|
||
| bool success = tasks[taskIndex].Execute(); | ||
| success.Should().BeTrue(); | ||
| results[taskIndex] = tasks[taskIndex].ItemsFromPackages; | ||
| })); | ||
| } | ||
|
|
||
| startGate.Set(); | ||
| Task.WaitAll(taskList.ToArray()); | ||
|
|
There was a problem hiding this comment.
This test uses startGate.Wait() and Task.WaitAll() without a timeout. To avoid rare but costly CI hangs, consider adding timeouts (with a clear assertion message) and disposing startGate (ManualResetEventSlim implements IDisposable).
| var startGate = new ManualResetEventSlim(false); | ||
| var taskList = new List<Task>(); | ||
| var results = new ConcurrentBag<ITaskItem[]>(); | ||
|
|
||
| for (int i = 0; i < threadCount; i++) | ||
| { | ||
| int taskIndex = i; | ||
| taskList.Add(Task.Run(() => | ||
| { | ||
| startGate.Wait(); | ||
|
|
||
| bool success = tasks[taskIndex].Execute(); | ||
| success.Should().BeTrue(); | ||
| results.Add(tasks[taskIndex].ItemsFromPackages); | ||
| })); | ||
| } | ||
|
|
||
| startGate.Set(); | ||
| Task.WaitAll(taskList.ToArray()); | ||
|
|
There was a problem hiding this comment.
This test uses startGate.Wait() and Task.WaitAll() without any timeout. Adding timeouts (and disposing the ManualResetEventSlim) would prevent indefinite hangs if something goes wrong during concurrent execution.
| var startGate = new ManualResetEventSlim(false); | ||
| var taskList = new List<Task>(); | ||
|
|
||
| for (int i = 0; i < threadCount; i++) | ||
| { | ||
| int taskIndex = i; | ||
| taskList.Add(Task.Run(() => | ||
| { | ||
| startGate.Wait(); | ||
|
|
||
| bool success = tasks[taskIndex].Execute(); | ||
| success.Should().BeTrue(); | ||
| results.Add(tasks[taskIndex].ItemsFromPackages); | ||
| })); | ||
| } | ||
|
|
||
| startGate.Set(); | ||
| Task.WaitAll(taskList.ToArray()); | ||
|
|
There was a problem hiding this comment.
This test uses startGate.Wait() and Task.WaitAll() without a timeout. Consider adding timeouts (and disposing ManualResetEventSlim) to avoid indefinite hangs in CI if a task never completes.
| var startGate = new ManualResetEventSlim(false); | ||
| var taskList = new List<Task>(); | ||
|
|
||
| for (int i = 0; i < threadCount; i++) | ||
| { | ||
| int taskIndex = i; | ||
| taskList.Add(Task.Run(() => | ||
| { | ||
| startGate.Wait(); | ||
|
|
||
| bool success = tasks[taskIndex].Execute(); | ||
| success.Should().BeTrue(); | ||
| results.Add(tasks[taskIndex].ItemsFromPackages); | ||
| })); | ||
| } | ||
|
|
||
| startGate.Set(); | ||
| Task.WaitAll(taskList.ToArray()); | ||
|
|
There was a problem hiding this comment.
This test uses startGate.Wait() and Task.WaitAll() without any timeout. Adding timeouts (and disposing the ManualResetEventSlim) would help prevent the test suite from hanging indefinitely under failure conditions.
| get => _taskEnvironment ??= new TaskEnvironment(new ProcessTaskEnvironmentDriver(Directory.GetCurrentDirectory())); | ||
| set => _taskEnvironment = value; | ||
| } |
There was a problem hiding this comment.
On NETFRAMEWORK, the TaskEnvironment getter lazily initializes using Directory.GetCurrentDirectory(), which is process-global and may not match the project directory when tasks are executed concurrently for multiple projects. Since TaskEnvironment is intended to provide the per-project execution context, consider removing this CWD-based fallback and relying on MSBuild to set TaskEnvironment, or derive the directory from BuildEngine.ProjectFileOfTaskNode (when available) instead of Directory.GetCurrentDirectory().
| get => _taskEnvironment ??= new TaskEnvironment(new ProcessTaskEnvironmentDriver(Directory.GetCurrentDirectory())); | |
| set => _taskEnvironment = value; | |
| } | |
| get => _taskEnvironment ??= CreateTaskEnvironment(); | |
| set => _taskEnvironment = value; | |
| } | |
| private TaskEnvironment CreateTaskEnvironment() | |
| { | |
| string projectFile = BuildEngine?.ProjectFileOfTaskNode; | |
| if (string.IsNullOrEmpty(projectFile)) | |
| { | |
| throw new InvalidOperationException("TaskEnvironment was not set and the project file path is unavailable from the build engine."); | |
| } | |
| string projectDirectory = System.IO.Path.GetDirectoryName(projectFile); | |
| if (string.IsNullOrEmpty(projectDirectory)) | |
| { | |
| throw new InvalidOperationException("TaskEnvironment was not set and the project directory could not be determined from the build engine project file path."); | |
| } | |
| return new TaskEnvironment(new ProcessTaskEnvironmentDriver(projectDirectory)); | |
| } |
- Add IMultiThreadableTask interface implementation - Add TaskEnvironment property stub (pure in-memory task) - Create comprehensive multithreading test suite: * Interface implementation verification * Concurrent execution with start-gate pattern * Independent package filtering per thread * Empty input handling * Metadata preservation * Version matching correctness - All 7 tests passed This task is pure in-memory (no file system or environment access), so only attribute + stub property are needed per Pattern A. Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
daec033 to
0d26a88
Compare
|
The task is already enlightened, the tests looks to me having very limited value. |
Migrates \FindItemsFromPackages\ to support \IMultiThreadableTask.
Changes
Addresses review feedback from #53116
Supersedes
Part of the split of stuck merge-group PR #53116. This is the 3-of-5 split for \FindItemsFromPackages.