Skip to content

fix(WebAPI): Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.…#620

Merged
JasonLandbridge merged 18 commits into
devfrom
stable-fixes
Jul 20, 2026
Merged

fix(WebAPI): Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.…#620
JasonLandbridge merged 18 commits into
devfrom
stable-fixes

Conversation

@JasonLandbridge

@JasonLandbridge JasonLandbridge commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

…NET connections

Quartz's AdoJobStore opens raw ADO.NET SqliteConnection objects that
bypass the EF Core SqliteConnectionEnhancer interceptor pipeline. Without
busy_timeout, concurrent writes fail instantly with SQLITE_BUSY
('database is locked') during MisfireHandler.Manage().

Add QuartzSqliteConnectionProvider — a custom IDbProvider that hooks
StateChange to call SqliteConnectionEnhancer.ApplyRuntimePragmas() on
every connection Open(), applying the same PRAGMAs (WAL, busy_timeout,
synchronous, cache_size, etc.) as EF Core connections.

Summary by CodeRabbit

  • New Features
    • Added configurable timeout support for Plex server status requests and connection-check operations.
  • Bug Fixes
    • Improved Quartz SQLite stability by applying per-connection settings (including busy timeout).
    • Continued inspecting remaining Plex servers even if one server’s connection check fails.
  • Performance
    • Streamlined actor/genre/country metadata synchronization using batched insert-or-ignore upserts.
  • Refactor
    • Plex server inspection now uses a fresh database context per server for parallel runs.
  • Tests
    • Expanded unit tests for timeouts, retry/progress completion signaling, parallel inspection, and partial failure handling.

…NET connections

Quartz's AdoJobStore opens raw ADO.NET SqliteConnection objects that
bypass the EF Core SqliteConnectionEnhancer interceptor pipeline. Without
busy_timeout, concurrent writes fail instantly with SQLITE_BUSY
('database is locked') during MisfireHandler.Manage().

Add QuartzSqliteConnectionProvider — a custom IDbProvider that hooks
StateChange to call SqliteConnectionEnhancer.ApplyRuntimePragmas() on
every connection Open(), applying the same PRAGMAs (WAL, busy_timeout,
synchronous, cache_size, etc.) as EF Core connections.
Set BindByName=true, ParameterNamePrefix="@", and
UseParameterNamePrefixInParameterCollection=true so that
AdoUtil.AddCommandParameter does not substitute SQL parameter
references with empty strings.

Without this, DbMetadata defaults (BindByName=false,
ParameterNamePrefix=null) caused the !BindByName branch to
replace every @paramName reference with null (empty string),
corrupting the SQL text. On fresh databases, this produced:

  UPDATE QRTZ_TRIGGERS SET TRIGGER_STATE =  WHERE ...

which SQLite rejects with "near WHERE: syntax error".

Also simplifies the StateChange PRAGMA to a synchronous inline
busy_timeout=30000 command, replacing the earlier async
ApplyRuntimePragmas call to avoid fire-and-forget issues.
… telling the user their credentials are invalid
…a Plex server finished when connectable or all fail
@JasonLandbridge

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/Application/_Shared/Config/Quartz/QuartzSqliteConnectionProvider.cs (1)

79-82: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Apply all intended SQLite PRAGMAs.

The PR summary states that this provider configures WAL, busy_timeout, synchronous, and cache_size. However, the code currently only sets busy_timeout = 30000;. Omitting the other PRAGMAs, especially synchronous = NORMAL, causes SQLite to fall back to its default behavior, which can result in excessive disk I/O and degrade concurrent performance.

⚡ Proposed fix
-                using var pragmaCmd = conn.CreateCommand();
-                pragmaCmd.CommandText = "PRAGMA busy_timeout = 30000;";
-                pragmaCmd.ExecuteNonQuery();
+                using var pragmaCmd = conn.CreateCommand();
+                pragmaCmd.CommandText = @"
+                    PRAGMA busy_timeout = 30000;
+                    PRAGMA journal_mode = WAL;
+                    PRAGMA synchronous = NORMAL;
+                    PRAGMA cache_size = -20000;";
+                pragmaCmd.ExecuteNonQuery();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Application/_Shared/Config/Quartz/QuartzSqliteConnectionProvider.cs`
around lines 79 - 82, Update the SQLite initialization in the connection
provider to apply all intended PRAGMAs: enable WAL, retain the 30-second busy
timeout, set synchronous mode to NORMAL, and configure the intended cache_size.
Ensure each setting is executed on the created connection alongside the existing
pragma command.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@tests/UnitTests/Application.UnitTests/PlexServerConnection/CheckAllConnectionsByServer/CheckAllConnectionsStatusByPlexServerCommand.UnitTests.cs`:
- Around line 140-144: Update the test around
CheckAllConnectionsStatusByPlexServerCommand to construct the request with a
non-default timeout such as 30, then change the ICommandExecutor verification
predicate to require CheckConnectionStatusByIdCommand.Timeout == 30 while
preserving the existing invocation count and cancellation-token checks.

---

Duplicate comments:
In `@src/Application/_Shared/Config/Quartz/QuartzSqliteConnectionProvider.cs`:
- Around line 79-82: Update the SQLite initialization in the connection provider
to apply all intended PRAGMAs: enable WAL, retain the 30-second busy timeout,
set synchronous mode to NORMAL, and configure the intended cache_size. Ensure
each setting is executed on the created connection alongside the existing pragma
command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7e5d28a0-5c4f-4b6c-83a9-b1284a0f8175

📥 Commits

Reviewing files that changed from the base of the PR and between 6101d69 and 0a202d0.

⛔ Files ignored due to path filters (51)
  • src/AppHost/AppHost.csproj is excluded by !**/*.csproj and included by none
  • src/AppHost/ClientApp/cypress/e2e/pages/dialogs/check-server-connections-dialog.cy.ts is excluded by none and included by none
  • src/AppHost/ClientApp/cypress/e2e/pages/settings/accounts/account-create.cy.ts is excluded by none and included by none
  • src/AppHost/ClientApp/src/components/Dialogs/AccountDialog/AccountTokenValidateDialog.vue is excluded by none and included by none
  • src/AppHost/ClientApp/src/components/Dialogs/AccountDialog/AccountVerificationCodeDialog.vue is excluded by none and included by none
  • src/AppHost/ClientApp/src/components/Dialogs/CheckServerConnectionsDialog.vue is excluded by none and included by none
  • src/AppHost/ClientApp/src/lang/de-DE.json is excluded by none and included by none
  • src/AppHost/ClientApp/src/lang/en-US.json is excluded by none and included by none
  • src/AppHost/ClientApp/src/lang/fr-FR.json is excluded by none and included by none
  • src/AppHost/ClientApp/src/lang/pl-PL.json is excluded by none and included by none
  • src/AppHost/ClientApp/src/store/accountDialogStore.ts is excluded by none and included by none
  • src/AppHost/ClientApp/tests/nuxt/stores/account-dialog-store/validate-plex-account.test.ts is excluded by none and included by none
  • src/AppHost/ClientApp/tests/nuxt/stores/account-dialog-store/validate-plex-token.test.ts is excluded by none and included by none
  • src/AppHost/ClientApp/tests/nuxt/stores/account-dialog-store/validate-verification-code.test.ts is excluded by none and included by none
  • src/AppHost/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Application.Contracts/Application.Contracts.csproj is excluded by !**/*.csproj and included by none
  • src/Application.Contracts/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Application/Application.csproj is excluded by !**/*.csproj and included by none
  • src/Application/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/BackgroundJobs.Contracts/BackgroundJobs.Contracts.csproj is excluded by !**/*.csproj and included by none
  • src/BackgroundJobs.Contracts/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/BackgroundJobs/BackgroundJobs.csproj is excluded by !**/*.csproj and included by none
  • src/BackgroundJobs/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Data.Contracts/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Data/Data.csproj is excluded by !**/*.csproj and included by none
  • src/Data/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/External/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/FileSystem/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Identity.Contracts/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/Identity/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/PlexApi.Contracts/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/PlexApi/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/PublicAPI/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • src/SignalR/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/BaseTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/IntegrationTests/IntegrationTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/AppHost.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Application.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/BackgroundJobs.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/BaseTests.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Build.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Data.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Domain.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Environment.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/External.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/FileSystem.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/FluentResultExtension.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Logging.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/PlexApi.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/PublicApi.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
  • tests/UnitTests/Settings.UnitTests/packages.lock.json is excluded by !**/packages.lock.json and included by none
📒 Files selected for processing (14)
  • src/Application/PlexServerConnection/CheckAllConnectionsByServer/CheckAllConnectionsStatusByPlexServerCommand.cs
  • src/Application/PlexServerConnection/CheckConnectionStatus/CheckConnectionStatusByIdCommand.cs
  • src/Application/PlexServers/InspectPlexServer/InspectPlexServerJob.cs
  • src/Application/_Shared/Config/Autofac/QuartzModule.cs
  • src/Application/_Shared/Config/Quartz/QuartzSqliteConnectionProvider.cs
  • src/BackgroundJobs/LibrarySync/Commands/RefreshLibraryMedia/CRUD/InsertMediaMetaDataCommand.cs
  • src/Data.Contracts/Extensions/DbContext/DbContextExtensions.PlexMetaData.cs
  • src/PlexApi.Contracts/PlexServer/GetServerStatus/GetServerStatusCommand.cs
  • src/PlexApi/PlexAPI/PlexAPIClient/PlexApiClient.cs
  • src/PlexApi/PlexServer/GetServerStatus/GetServerStatusCommandHandler.cs
  • tests/UnitTests/Application.UnitTests/BackgroundServices/Listeners/DownloadJobListener.UnitTests.cs
  • tests/UnitTests/Application.UnitTests/PlexServerConnection/CheckAllConnectionsByServer/CheckAllConnectionsStatusByPlexServerCommand.UnitTests.cs
  • tests/UnitTests/Application.UnitTests/PlexServers/InspectPlexServer/InspectPlexServerJob.UnitTests.cs
  • tests/UnitTests/PlexApi.UnitTests/PlexApiClient/PlexApiClient.UnitTests.cs

Comment on lines +140 to +144
Mock.Mock<ICommandExecutor>()
.Verify(
x => x.Send(It.Is<CheckConnectionStatusByIdCommand>(command => command.Timeout == 10), It.IsAny<CancellationToken>()),
Times.AtLeastOnce()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test a non-default timeout to cover propagation.

Because the request uses the default value, this assertion would still pass if the handler hardcoded 10. Construct the request with a distinct timeout, such as 30, and assert that CheckConnectionStatusByIdCommand.Timeout == 30.

Proposed test adjustment
-var request = new CheckAllConnectionsStatusByPlexServerCommand(1);
+var request = new CheckAllConnectionsStatusByPlexServerCommand(1, 30);

- command => command.Timeout == 10
+ command => command.Timeout == 30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/UnitTests/Application.UnitTests/PlexServerConnection/CheckAllConnectionsByServer/CheckAllConnectionsStatusByPlexServerCommand.UnitTests.cs`
around lines 140 - 144, Update the test around
CheckAllConnectionsStatusByPlexServerCommand to construct the request with a
non-default timeout such as 30, then change the ICommandExecutor verification
predicate to require CheckConnectionStatusByIdCommand.Timeout == 30 while
preserving the existing invocation count and cancellation-token checks.

@JasonLandbridge
JasonLandbridge merged commit fcebb9a into dev Jul 20, 2026
1 check was pending
@JasonLandbridge
JasonLandbridge deleted the stable-fixes branch July 20, 2026 22:17
JasonLandbridge added a commit that referenced this pull request Jul 20, 2026
## [0.38.3-dev.1](v0.38.2...v0.38.3-dev.1) (2026-07-20)

### Bug Fixes

* **WebAPI:** Add correct DbMetadata to QuartzSqliteConnectionProvider ([b553ed9](b553ed9))
* **WebAPI:** Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.… ([#620](#620)) ([fcebb9a](fcebb9a))
* **WebAPI:** Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.NET connections ([35e087e](35e087e))
* **Web-UI:** Fix 2FA verification code pop-up not showing and instead telling the user their credentials are invalid ([a811e1d](a811e1d))
* **WebAPI:** Fix connections not completing when they time-out ([68f91c7](68f91c7))
* **Web-UI:** Fix the Plex server connectable count, it will now show a Plex server finished when connectable or all fail ([1b27097](1b27097))
* **Web-UI:** Fixed the invalid verification code input for 2FA not showing the validity ([7d80657](7d80657))
* **WebAPI:** Prevent metadata insert races ([f1d0b4e](f1d0b4e))
* **WebAPI:** Use DbContext factory for Plex inspection ([0bb1c87](0bb1c87))
JasonLandbridge added a commit that referenced this pull request Jul 20, 2026
## [0.38.3](v0.38.2...v0.38.3) (2026-07-20)

### Bug Fixes

* **WebAPI:** Add correct DbMetadata to QuartzSqliteConnectionProvider ([b553ed9](b553ed9))
* **WebAPI:** Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.… ([#620](#620)) ([fcebb9a](fcebb9a))
* **WebAPI:** Apply SQLite WAL and busy_timeout PRAGMAs to Quartz ADO.NET connections ([35e087e](35e087e))
* **Web-UI:** Fix 2FA verification code pop-up not showing and instead telling the user their credentials are invalid ([a811e1d](a811e1d))
* **WebAPI:** Fix connections not completing when they time-out ([68f91c7](68f91c7))
* **Web-UI:** Fix the Plex server connectable count, it will now show a Plex server finished when connectable or all fail ([1b27097](1b27097))
* **Web-UI:** Fixed the invalid verification code input for 2FA not showing the validity ([7d80657](7d80657))
* **WebAPI:** Prevent metadata insert races ([f1d0b4e](f1d0b4e))
* **WebAPI:** Use DbContext factory for Plex inspection ([0bb1c87](0bb1c87))
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