Skip to content

fix(builder): make model declaration check-and-create atomic - #8005

Open
aqeelat wants to merge 1 commit into
microsoft:mainfrom
aqeelat:fix/idempotency-race-condition
Open

fix(builder): make model declaration check-and-create atomic#8005
aqeelat wants to merge 1 commit into
microsoft:mainfrom
aqeelat:fix/idempotency-race-condition

Conversation

@aqeelat

@aqeelat aqeelat commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Intermittent idempotency test failures with the Twitter API spec: two consecutive generations from the same spec sometimes produced different output. CI logs showed model files missing in one run (Models/Analytics_metrics.cs) and content differences in others (Models/Analytics.cs, Models/User.cs, etc.).

Root cause

AddModelClass checked for an existing class implicitly via AddModelDeclarationIfDoesntExist's GetExistingDeclaration call, then created and added a class stub via currentNamespace.AddClass() — in separate, non-atomic steps. When two parallel threads processing different URL tree nodes both referenced the same component schema, both could pass the check and both create class stubs. The existing classLifecycles mechanism prevented duplicate property building but not duplicate class creation.

Fix

Added a narrow lock (lifecycle) inside AddModelClass, covering ONLY the GetExistingDeclaration re-check and AddClass call — not property building. This makes check-and-add atomic per class name.

Why not lock the entire method? Property building (CreatePropertiesForModelClass) recursively creates dependent models via AddModelDeclarationIfDoesntExist. If the lock were held during property building, mutually-referencing models (A → B → A) processed by different threads would deadlock: Thread 1 holds A's lock waiting for B's, Thread 2 holds B's waiting for A's.

The narrow lock avoids this because:

  • Lock is held only for microseconds (check + add stub), no recursion inside
  • Property building happens outside the lock, coordinated by the existing classLifecycles mechanism
  • Different classes use different lock objects — no unnecessary contention

Verification

  • No deadlock: 10/10 runs on a mutually-referencing fixture (ModelA.b → ModelB, ModelB.a → ModelA)
  • Reduced race: 29/30 idempotency iterations pass with the Twitter API spec (was ~20% failure rate before fix)
  • Unit tests: 2190 passed (2 pre-existing skips)
  • Pre-existing: race reproduces on main (2/10 failures without fix)

Copilot AI review requested due to automatic review settings July 29, 2026 13:10
@aqeelat
aqeelat requested a review from a team as a code owner July 29, 2026 13:10

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aqeelat
aqeelat marked this pull request as draft July 29, 2026 19:06
@aqeelat
aqeelat force-pushed the fix/idempotency-race-condition branch from 80a5910 to aa8c343 Compare July 29, 2026 19:11
Copilot AI review requested due to automatic review settings July 29, 2026 19:11

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

AddModelDeclarationIfDoesntExist checked for an existing class and created
a new one in separate non-atomic steps. When two parallel threads both saw
'doesn't exist', both created class stubs — one with properties, one empty.
The empty stub could be returned as a type definition, causing missing or
different model files between runs (idempotency test failures).

Fix: lock on the classLifecycles entry (keyed by namespace + class name) for
the entire check-and-create. The lock is reentrant (recursive parent-schema
and self-reference calls on the same thread don't deadlock) and per-class-name
(different classes don't contend).
@aqeelat
aqeelat force-pushed the fix/idempotency-race-condition branch from aa8c343 to 2cc8059 Compare July 29, 2026 20:57
Copilot AI review requested due to automatic review settings July 29, 2026 20:57

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aqeelat
aqeelat marked this pull request as ready for review July 29, 2026 21:15
@aqeelat

aqeelat commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@baywet can you please run the tests?

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