Skip to content

fix: run distributed-lock heartbeat on a dedicated connection (issue #79) - #8

Merged
tbbuck merged 1 commit into
masterfrom
fix/distributed-lock-heartbeat-connection
Jun 23, 2026
Merged

tbbuck merged 1 commit into
masterfrom
fix/distributed-lock-heartbeat-connection

Conversation

@tbbuck

@tbbuck tbbuck commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Fixes the AccessViolationException / storage corruption reported upstream in raisedapp/Hangfire.Storage.SQLite#79.

Root cause

SQLiteDistributedLock ran its heartbeat timer's UpdateExpiration on the consumer's HangfireDbContext, while the consumer used that same NoMutex (non-thread-safe) connection on another thread — e.g. RecurringJobScheduler releasing another lock, or ExpirationManager executing its delete. Two threads on one SQLite connection → memory corruption / AccessViolationException. (Same bug class already fixed for the sliding-timeout heartbeat in #3.)

Fix

The heartbeat now obtains its own dedicated connection from the storage pool (storage.CreateAndOpenConnection()), so the timer thread never touches the caller's connection. Storage is plumbed through the two internal acquisition paths (HangfireSQLiteConnection.AcquireDistributedLock, ExpirationManager.RemoveExpireRows). The public Acquire overload is preserved and falls back to the legacy shared-connection behaviour when no storage is supplied, so existing callers and tests are unaffected.

Tests

Adds SQLiteDistributedLockHeartbeatFacts: the heartbeat renews ExpireAt on a dedicated connection and releases cleanly, plus a file-backed (WAL) concurrency test hammering the consumer connection while the heartbeat fires. Full suite: 203 passing.

Bumps version to 0.5.1-beta.

…aisedapp#79)

SQLiteDistributedLock ran its heartbeat timer's UpdateExpiration on the
consumer's HangfireDbContext while the consumer used that same NoMutex
(non-thread-safe) connection on another thread, causing AccessViolationException
and storage corruption (upstream raisedapp#79).

The heartbeat now obtains its own dedicated connection from the storage pool
(storage.CreateAndOpenConnection()), so the timer thread never touches the
caller's connection. Storage is plumbed through the two internal acquisition
paths (HangfireSQLiteConnection.AcquireDistributedLock and
ExpirationManager.RemoveExpireRows); the existing public Acquire overload is
preserved and falls back to the legacy shared-connection behaviour when no
storage is supplied. Bumps version to 0.5.1-beta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xW2zhMvrJChpY58hfJBeZ
@tbbuck
tbbuck merged commit b467b53 into master Jun 23, 2026
1 of 3 checks passed
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