Skip to content

fix: make BsdiffDiffer thread-safe for concurrent pipeline use - #450

Merged
JusterZhu merged 3 commits into
masterfrom
fix/bsdiff-thread-safety
May 26, 2026
Merged

fix: make BsdiffDiffer thread-safe for concurrent pipeline use#450
JusterZhu merged 3 commits into
masterfrom
fix/bsdiff-thread-safety

Conversation

@JusterZhu

Copy link
Copy Markdown
Collaborator

Summary

  • Remove _oldfilePath, _newfilePath, _patchPath instance fields from BsdiffDiffer that caused race conditions under concurrent calls
  • ValidationParameters() now accepts parameters directly instead of reading shared mutable state
  • Clean() and Dirty() lambdas capture file paths via closure — each invocation gets its own copy
  • BsdiffDiffer is now safe to use with DiffPipelineBuilder.WithParallelism(>1)

Root Cause

When DiffPipeline runs with parallelism > 1, concurrent tasks share the same BsdiffDiffer instance. The old code wrote per-call state (_oldfilePath, _newfilePath, _patchPath) to instance fields inside Task.Run, making them subject to race conditions. Thread A could overwrite thread B's fields between assignment and use.

Test Plan

  • All 94 existing + new tests pass (70 original + 24 comprehensive)
  • BsdiffDiffer with WithParallelism(4) in DiffPipeline produces correct results
  • Single-file round-trip: text, binary, empty, identical, large files (both BsdiffDiffer and StreamingHdiffDiffer)
  • Multi-file parallel: 30-50 files with mixed operations (modify/add/delete)
  • Bug fix: Bump DiffPipeline_CustomDiffer_BsdiffDiffer_Works from parallelism=1 to parallelism=4, confirms fix

🤖 Generated with Claude Code

Remove per-call mutable instance fields (_oldfilePath, _newfilePath, _patchPath)
that caused race conditions when a single BsdiffDiffer instance was shared
across parallel DiffPipeline tasks. The Task.Run lambdas now capture parameters
directly via closure, making each invocation independently thread-safe.

Also add missing GeneralUpdate.Core project reference to DifferentialTest,
and add comprehensive single/multi-file differential integration tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 12:38
@JusterZhu JusterZhu added the fix Fix some existing issues label May 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes BsdiffDiffer safe to reuse concurrently (e.g., when DiffPipeline is configured with parallelism > 1) by removing per-call mutable instance state and passing file paths through the call stack. It also adds a comprehensive test suite that exercises single-file round-trips and multi-file parallel pipeline scenarios.

Changes:

  • Refactor BsdiffDiffer to remove per-invocation instance fields and make parameter validation purely argument-based.
  • Add a new comprehensive differential test suite covering bsdiff/hdiff round-trips, pipeline parallelism, delete handling, progress, and cancellation.
  • Add GeneralUpdate.Core project reference to the src/c#/DifferentialTest test project.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
tests/DifferentialTest/ComprehensiveDifferentialTests.cs Adds broad single-/multi-file and parallelism test coverage for differ + pipeline behavior.
src/c#/GeneralUpdate.Differential/Differ/BsdiffDiffer.cs Removes shared mutable state and updates docs/validation to enable safe concurrent use.
src/c#/DifferentialTest/DifferentialTest.csproj Adds a direct reference to GeneralUpdate.Core for tests that use Core pipeline/models.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Also add some new files in target (no old version)
File.WriteAllBytes(Path.Combine(tgt, "new_features.dll"), [1, 2, 3, 4, 5]);

var completedFiles = new ConcurrentBag<string>();
Comment on lines +399 to +402
var progress = new SyncProgress<DiffProgress>();
var pipeline = new DiffPipelineBuilder()
.WithParallelism(Environment.ProcessorCount)
.Build();
Comment on lines +409 to +412
// Assert
Assert.True(progress.LastValue.IsComplete);
_output.WriteLine($"Progress: {progress.LastValue.Completed}/{progress.LastValue.Total}");

Comment on lines +810 to +812
var pipeline = new DiffPipelineBuilder()
.WithParallelism(Environment.ProcessorCount)
.Build();
Comment on lines +18 to +19
/// Thread-safety: this class is stateless beyond the compression provider.
/// A single instance is safe for concurrent calls.
JusterZhu and others added 2 commits May 26, 2026 20:43
- Remove unused ConcurrentBag variable
- Cap Environment.ProcessorCount in tests for stable CI
- Strengthen DiffProgress assertions (check Total > 0 and Completed == Total)
- Clarify BsdiffDiffer thread-safety doc to mention compression provider contract

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@JusterZhu
JusterZhu merged commit 0bdfe24 into master May 26, 2026
2 of 3 checks passed
@JusterZhu
JusterZhu deleted the fix/bsdiff-thread-safety branch May 26, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix some existing issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants