Skip to content

Modernize to .NET 10, fix latent thread synchronization related Race conditions - #182

Open
Amir Burbea (amirburbea) wants to merge 38 commits into
microsoft:masterfrom
amirburbea:modernize-net10
Open

Amir Burbea (amirburbea) wants to merge 38 commits into
microsoft:masterfrom
amirburbea:modernize-net10

Conversation

@amirburbea

@amirburbea Amir Burbea (amirburbea) commented Mar 24, 2026

Copy link
Copy Markdown

Using Adam Cohen (@adam-dot-cohen)'s PR as a base, update all libraries to net10.0, but remove the dotnet standard library references (i.e. Microsoft.CSharp).

Fixed locking in ConfigModifier to support re-entry
Fixed race condition in BinaryPipe
Use newer Assert methods in test libraries

@microsoft-github-policy-service agree

2. Workaround to windows only thread affinity in Native32 for Linux

3. Solution updated to target Net7, Net6, Net472, and NetCoreapp3.1
- Upgraded MSTest packages with security vulerabilites
- Reverted StyleCop package update from previous commit
- Target net8.0 across all projects, dropping net6.0/net7.0/net472
- Remove legacy .NET Standard packages (System.Linq.Expressions 4.3.0,
  Microsoft.CSharp 4.7.0, System.Diagnostics.*, etc.) now built into the runtime
- Upgrade Microsoft.CodeAnalysis.Scripting 4.4.0 -> 5.3.0
- Upgrade MSTest 3.x -> 4.x, System.Reactive 5.x -> 6.x, and other packages
- Fix ConfigModifier gate: replace Monitor (thread-affine) with SemaphoreSlim +
  AsyncLocal depth counter, making it safe for async tests that release from a
  different thread while remaining re-entrant for nested Modify() calls
- Make T4 template import conditional so dotnet CLI builds work without VS
- Add trill.runsettings with StopRunOnFirstFailure for faster test diagnosis
- All 3185 tests pass on .NET 8
…er concurrent scheduling

When both inputs complete concurrently, ProcessPendingBatches() uses Monitor.TryEnter
and silently returns if another thread holds the lock. The subsequent Monitor.Enter block
then called base.OnCompleted() immediately, propagating completion downstream before
queued batches were processed. Fixed by calling ProcessPendingBatches() inside the
Monitor.Enter block before base.OnCompleted() - Monitor is reentrant so TryEnter
succeeds for the owning thread, guaranteeing all pending batches are flushed first.
…on tree compat

- Target net10.0 across all projects
- Remove StyleCop from core library (was never running on net8 due to netstandard2.0 condition)
- Standardize test projects on StyleCop 1.1.118
- Suppress SA1137 in test ruleset (T4-generated files have non-standard indentation)
- Fix AdHocTests: string.Join with params object[] instead of params ReadOnlySpan<object>
  to avoid CS8640/CS9226 in expression trees under .NET 10
@amirburbea

Copy link
Copy Markdown
Author

Amir Burbea (@amirburbea) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

BinaryDecoder:
- DecodeFloat, DecodeDouble, DecodeGuid, ReadIntFixed: replace new byte[]
  with stackalloc Span<byte>; add Span<byte> overload of ReadAllRequiredBytes
- DecodeString: bypass DecodeByteArray, rent from ArrayPool<byte>, decode
  string, return buffer — avoids a heap allocation per string field

BinaryEncoder:
- Encode(float): replace BitConverter.GetBytes (allocates) with stackalloc +
  BitConverter.TryWriteBytes
- Encode(Guid): replace ToByteArray() (allocates) with stackalloc +
  TryWriteBytes
- Encode(string): replace Encoding.UTF8.GetBytes(string) (allocates) with
  ArrayPool<byte> rent, encode into span, delegate to Encode(ReadOnlySpan<byte>)
The codegen cache (EquiJoinStreamable.cachedPipes) persisted across tests
in the same process, causing JoinTestWithException to silently succeed
(no exception thrown) when a compatible join had already been compiled
by an earlier test.

Fix: add internal Clear() to SafeConcurrentDictionary, expose cachedPipes
as internal on EquiJoinStreamable, and clear the exact cache instance in
[TestInitialize] before each run. The test now uses a named JoinResult
struct so the closed generic type can be referenced at compile time, and
asserts the StreamProcessingException is always thrown.
@adam-dot-cohen

Copy link
Copy Markdown

Great! Thanks

…plemented with - instead of +. SimpleTesting does not have a test for this.
…tackalloc

Begin to modernize codebase by replacing lambda expressions with method group references in ForEachAsync, Subscribe, and similar calls. Update collection initializations to use C# 12 collection expressions for improved readability. Use targeted new in several places. Remove StyleCop.Analyzers references from project files, and update Main method and array initializations in Program.cs.  Checkpoint related version headers are now sent/received via stack allocated spans as opposed to byte[].
Configures Microsoft.StreamProcessing to pack as Trill.StreamProcessing,
bundling both the core and Provider assemblies into a single package.
Fixes broken version calculation in Directory.Build.props. Adds package
README, license, and repository metadata pointing to the amirburbea/trill
fork. Marks Microsoft.StreamProcessing.Provider as non-packable since it
is bundled into the main package.
- Use targeted new
- Use System.Threading.Lock
- Use ArgumentNullException.ThrowIfNull/ArgumentOutOfRangeException.ThrowIf*
CanRepresentAsColumnar incorrectly reported several payload shapes as
columnar-representable when Trill's columnar reconstitution (which assigns each
field/autoprop by plain assignment) cannot actually construct them:

- a get-only autoprop (no setter at all) was skipped entirely instead of
  disqualifying the type
- an init-only setter (`{ get; init; }`) was accepted because SetMethod.IsPublic
  is true for init same as for a normal setter - the IsExternalInit modifier was
  never checked
- a public readonly instance field (as in a readonly struct, or a hand-written
  immutable type) was never inspected at all; only non-public fields were scanned

All three previously fell through to "true" and then failed for real the first
time a query tried to generate the columnar batch, silently degrading to
row-based execution (or throwing, under DontFallBackToRowBasedExecution).

Every consumer of CanRepresentAsColumnar (ingress, Group, EquiJoin, Shuffle x3)
already treats it as a pre-filter for its own CanGenerateColumnar and falls back
to a row-based pipe on failure, so tightening it cannot regress a case that
works today - confirmed by the full SimpleTesting suite (3200 tests) passing
unchanged.

Also flips StreamProperties<TKey,TPayload>.IsColumnar from internal to a public
getter (setter stays internal) so a consumer can check whether a given payload
type actually got the columnar path without reflection.

Added CanRepresentAsColumnarTests covering both fixes plus the pre-existing
correctly-rejected shapes (no nullary ctor, non-public setter, manual backing
field, abstract type) as non-regression checks.
- Microsoft.CodeAnalysis.Scripting 5.3.0 -> 5.9.0 (Core + Provider) - several
  minor releases behind, including allocation-reduction work in Roslyn's
  general-purpose hot paths (e.g. GreenNode.To(Full)String,
  GetEscapedMetadataName) that the runtime codegen in this repo exercises on
  every columnar compile
- System.Numerics.Tensors 10.0.0 -> 10.0.12
- System.Reactive 6.1.0 -> 7.0.0 (SimpleTesting, PerformanceTesting) -
  System.Reactive.Linq stays at 6.1.0, already latest
- Microsoft.NET.Test.Sdk 18.3.0 -> 18.10.0, MSTest.TestAdapter/TestFramework
  4.1.0 -> 4.4.0 (SimpleTesting)

Verified: full solution builds clean, full SimpleTesting suite (3200 tests,
including the CanRepresentAsColumnarTests from the previous commit) passes
unchanged.

Bump to 10.2.1.
System.Reactive.Linq stopped receiving releases at 6.1.0 - its contents were
folded back into the main System.Reactive package starting with v7 (the
split Core/Linq/Interfaces/PlatformServices packages were consolidated).
Both test projects already reference System.Reactive 7.0.0, which already
contains everything System.Reactive.Linq provided; the separate reference
was dead weight.

Test/perf projects only - the actual Microsoft.StreamProcessing package has
no Rx dependency at all, so this doesn't touch anything already published
as 10.2.1.

Verified: full solution builds clean, full SimpleTesting suite (3200 tests)
passes unchanged.
The get-only-autoprop fix in the previous commit (setMethod == null -> return
false, instead of continue) was correct for a genuine user-defined get-only
property, but every `record` (not `record struct`) also synthesizes a
compiler-generated, get-only, non-public `protected virtual Type
EqualityContract => typeof(T);` property. That property now tripped the same
check, so as of 10.2.1 EVERY record class - including a fully mutable one
with plain `{ get; set; }` properties - was incorrectly reported non-columnar.
Worse than the original bug, which accidentally tolerated this shape.

EqualityContract has no independent storage (it's always re-derived from the
runtime type), so there's nothing for columnar reconstitution to lose by not
touching it. The fix only skips the get-only case when the getter itself is
non-public - a genuinely non-public autoprop with a real setter (protected
get+set, say) still disqualifies the type exactly as before; only the
'non-public AND no setter at all' combination unique to EqualityContract is
exempted.

Added regression tests: a plain mutable record class must stay columnar
despite EqualityContract, and a class with a real non-public get+set autoprop
must still be rejected (proving the fix didn't over-broaden).

Verified: full solution builds clean, full SimpleTesting suite (3202 tests,
now including both new regression tests) passes.

Bump to 10.2.2. 10.2.1 should be treated as broken for any record class
payload and superseded by this version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants