Skip to content

fix: locking query shape coverage & AsyncLocal timing bug#11

Merged
mnbuhl merged 13 commits intomainfrom
fix/locking-query-shape-coverage
Apr 25, 2026
Merged

fix: locking query shape coverage & AsyncLocal timing bug#11
mnbuhl merged 13 commits intomainfrom
fix/locking-query-shape-coverage

Conversation

@mnbuhl
Copy link
Copy Markdown
Owner

@mnbuhl mnbuhl commented Apr 25, 2026

Summary

  • Fix AsyncLocal timing bug: All three provider SQL generators and LockingValidationInterceptor now parse LockOptions from the SQL tag instead of reading LockContext.Current. This eliminates silent missing-lock behaviour when a query is built in one async context and executed in another (deferred execution, stored queryables).
  • Extend unsafe-shape detection: UnsafeShapeDetector now rejects DISTINCT and GROUP BY queries with a typed LockingConfigurationException instead of letting them reach the database and produce cryptic driver errors.
  • Unify exception types: InvalidOperationException (no transaction) and ArgumentException (invalid distributed lock key) replaced with LockingConfigurationException so callers can catch LockingException as a unified base.
  • Documentation: XML-doc on ForUpdate/ForShare and all distributed lock extension methods now lists every thrown exception; README gains an "Unsupported query shapes" update and a new "Limitations" section for FromSqlRaw, compiled queries, and bulk operations.

mnbuhl and others added 7 commits April 25, 2026 23:49
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing queries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l timing bug)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lidOperationException with LockingConfigurationException

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…distributed lock key validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d lock extensions; add Limitations section to README

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mnbuhl mnbuhl force-pushed the fix/locking-query-shape-coverage branch from fff6019 to 9c30a25 Compare April 25, 2026 22:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fff6019076

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +36 to +38
var lockTag = selectExpression.Tags.FirstOrDefault(t =>
t.StartsWith(LockTagConstants.Prefix, StringComparison.Ordinal)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Select the most recent locking tag, not the first

Using FirstOrDefault here makes lock resolution depend on tag ordering when multiple locking tags exist on the same query (for example, chaining lock extensions or composing helpers that apply them twice). That can apply a stale mode/timeout instead of the latest lock request, whereas the previous LockContext.Current behavior followed the most recent call. This can emit the wrong lock clause in production queries (e.g., FOR UPDATE instead of an intended later FOR SHARE/timeout).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 39ef779. Changed to in all three provider SQL generators so the most-recently-applied locking tag wins — matching the previous last-write-wins behavior.

Comment on lines +33 to +41
parts[2].Length > 0
&& double.TryParse(
parts[2],
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var ms
)
)
timeout = TimeSpan.FromMilliseconds(ms);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject malformed timeout values in lock tag parsing

If the timeout segment is present but invalid, this path still returns success and leaves Timeout as null, silently changing behavior to "wait indefinitely" instead of preserving the requested timeout. In addition, double.TryParse accepts values like NaN/Infinity, which can make TimeSpan.FromMilliseconds throw even though TryParse is expected to be non-throwing. A malformed or corrupted __efcore_locking tag can therefore cause hangs or unexpected runtime exceptions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 39ef779. TryParse now returns false when the timeout segment is non-empty but unparseable (instead of silently defaulting to null). Also rejects NaN, Infinity, and negative values via !double.IsFinite(ms) || ms < 0 — all of which would cause TimeSpan.FromMilliseconds to throw or produce a nonsensical timeout. Four new unit tests cover these cases.

mnbuhl and others added 6 commits April 26, 2026 00:12
… and chained Where shapes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ushes GroupBy into subqueries

GroupBy with entity-typed results never appears on the outer SelectExpression that carries the lock
tag, so the guard was dead code that produced false test failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e, join shapes, compiled queries, and FromSql composition

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…query tests; fix SQL Server container on ARM64

- UnsafeShapeDetector: walk expression tree recursively to find aggregate
  SqlFunctionExpression wrapped in CAST or COALESCE (fixes CountAsync/SumAsync/etc.)
- QueryShapeTestsBase: use List<int> instead of int[] for Contains (fixes net10 span constraint)
- Remove compiled query tests: EF.CompileAsyncQuery cannot translate ForUpdate() extension
- SqlServerFixture: switch to azure-sql-edge (ARM64 native) + custom TCP/login wait strategy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…formed/NaN timeout in TryParse; update README with discovered limitations

- SQL generators: use LastOrDefault instead of FirstOrDefault when scanning
  locking tags, so the most-recently-applied lock options win (matches
  LockContext.Current last-write-wins semantics)
- LockTagConstants.TryParse: reject non-empty timeout segments that fail
  double.TryParse, and reject NaN/Infinity/negative values; previously these
  silently fell back to null (wait indefinitely)
- README: correct EF.CompileAsyncQuery entry (throws at compile time, not
  just missing pre-statement SQL), add Concat/aggregate/GroupBy shape docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mnbuhl mnbuhl merged commit fbcc6e7 into main Apr 25, 2026
12 checks passed
@mnbuhl mnbuhl deleted the fix/locking-query-shape-coverage branch April 25, 2026 23:29
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.

1 participant