Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **A cross-database lock now names the database the contended object actually lives in, from both blocking collectors** ([#1893]) - [#1876] made the two collectors' `contentious_object` labels agree at the incident identity, and corrected the `blocked_process_report` sentinel to name the lock RESOURCE's database rather than the blocked session's. The DMV snapshot side could not follow: its normalization runs in C# against a stored row whose only database name is `database_name`, which the collector writes as the blocked SESSION's database. A cross-database lock is held in one database by a session running in another, so for exactly that case the two sides still disagreed - the report row said `database: StackOverflow2013` where the snapshot row said `database: master` - and one contended object still raised two alerts.

**The resource's database id comes from `sys.dm_os_waiting_tasks.resource_description`, not from the wait resource.** MS Learn documents every lock resource type's description as carrying a `dbid=<db-id>` token - `keylock`, `pagelock`, `ridlock`, `objectlock`, `databaselock`, `filelock`, `extentlock`, `applicationlock`, `metadatalock`, `hobtlock` and `allocunitlock` - so a single parse covers every lock shape, where splitting `wait_resource` positionally would need a branch per resource type and would still miss the ones whose layout differs. `DB_NAME()` then runs in the same query, which is the whole reason this had to move server-side: the id was always sitting in the row, but nothing downstream could turn it into a name. Verified live on SQL Server 2022 against a genuine cross-database KEY lock, whose description reads `keylock hobtid=72057594045726720 dbid=11 id=lock... mode=X associatedObjectId=...`; the same blocking pair produced `Unresolved: key lock, database: pm1893_res` from both collectors, byte for byte, where before the fix the snapshot row carried the raw `KEY: 11:72057594045726720 (8194443284a0)` and could only ever have been named with the session's database.

**The snapshot sweep deliberately still does NOT resolve the object behind a KEY, PAGE or RID lock.** That is the per-database lookup the report side needs a server-side cursor and [#1865]'s permission screen to do safely, and this collector runs on a far tighter cadence - naming the database is a string parse, resolving the object is a cross-database metadata read per row. Only the naming changed. The parse is restricted to `LCK_%` waits that have a wait resource, so latch and `RESOURCE_SEMAPHORE` rows - whose `resource_description` is a bare `<db-id>:<file-id>:<page>` or a latch class, with no `dbid=` to read - keep byte-identically what they had; and where no `dbid=` is found the row keeps its old raw value rather than being relabelled with a database nobody verified.

**Query text only - no schema moved**, so no upgrade-folder work and no store migration in any of the three stores; the collector writes the same 19 columns with a better value in one of them, pinned by a test. The deprecated Dashboard's hand-maintained twin (`install/56_collect_dmv_blocking_snapshot.sql`) takes the identical change, guarded by a test that reads the file, so the SQL Server store cannot drift from Lite's and Darling's. This is the **third and final** blocking-fingerprint transition in this release, after [#1865] and [#1876]: DMV-sourced incidents for cross-database locks re-fire once against their new key. Consolidating all three into one upgrade was deliberate. One narrower difference remains and is filed rather than folded in ([#1898]): [#1865]'s trailing reason (`(no metadata access)`, `(page reallocated)`) exists only on the report side, so when it is present the two labels still differ by that suffix - dropping it from the identity while keeping it on the row is a product decision, not a tweak.

- **The same blocked object no longer raises two different alerts depending on which collector saw it** ([#1876]) - two collectors write `contentious_object` and they never agreed on how. `blocked_process_report` writes a plain `schema.object`, or an `Unresolved: key lock, database: Foo` sentinel when the lookup failed; `dmv_blocking_snapshots` writes a `QUOTENAME`'d `[schema].[object]` and, for the KEY, PAGE and RID locks it does not resolve at all, the **raw wait resource**. The alert engine merges both into one list and hashes that label into the incident's dedup key, so `dbo.Users` and `[dbo].[Users]` were two incidents for one table - and the DMV fallback, which exists to stand in for the XE session when it captured nothing (AWS RDS, or an unset blocked-process threshold), could never dedup against the reports it is standing in for. **The raw wait resource was the worse half**: `KEY: 6:72057594041991168 (8194443284a0)` carries a hobt id and a per-VALUE lock hash, so it was not merely a second identity for the object, it was a NEW identity on almost every sample - one recurring unresolvable lock rendered as an unbounded stream of one-occurrence incidents, each with its own cooldown. Those rows now collapse to the report side's sentinel and group by (lock type, database) exactly as report rows do.

**Normalized at the incident identity - not at collection, and deliberately not where the two collectors' rows are merged.** Doing it on the read side at all is what fixes rows **already in the store**; a collector-side fix would only have helped rows gathered after the upgrade, leaving every historical DMV row still fingerprinting by lock hash. Doing it at the grouper specifically buys two things the obvious placement does not. **Coverage:** two independent producers raise blocking incidents - the live alert builders, which go through the merge, and the `top_blocking_chains` drill-down behind the analysis alerts, whose SQL `UNION`s the two collectors before anything downstream can tell them apart - and the grouper is the one point neither can bypass, which also makes a future third producer correct by default. **Not destroying evidence:** `dmv_blocking_snapshots` stores no `wait_resource` column at all, so for a lock it could not resolve, `contentious_object` IS the raw resource, hobt id and lock hash included - and that merge feeds the blocking grids and both `get_blocking` MCP tools as well as the alert path, so rewriting rows there would have fixed the fingerprint by deleting the only copy of the evidence an operator needs to chase the lock. **Grids, MCP payloads and the store keep exactly what the collector wrote; only what the ALERT groups on and names as the contended object is normalized.**
Expand Down Expand Up @@ -2145,3 +2153,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1893]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/1893
[#1873]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/1873
[#1896]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/1896
[#1898]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/1898
Loading
Loading