Skip to content

fix(database): fix non-UTC log partitions and cut index and JIT overhead - #2157

Merged
Quick104 merged 8 commits into
mainfrom
fix/postgres-best-practices
Oct 8, 2026
Merged

Quick104 merged 8 commits into
mainfrom
fix/postgres-best-practices

Conversation

@Quick104

@Quick104 Quick104 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Related issue: N/A
Validation tasks: none

A review of Silo's PostgreSQL use against Microsoft's
postgresql-best-practices guidance found one bug that
breaks log partitioning on some fresh installs, and several costs every deployment pays:

  • Fresh installs whose PostgreSQL server is not set to UTC get broken log partitions. Migration
    028 creates the first operational_logs and activity_log partitions at the server's local
    midnight, and internal/partman later adds partitions at UTC midnight. The next one overlaps
    (SQLSTATE 42P17) or leaves a gap, EnsureFuturePartitions fails at startup and on every cleanup
    run, and log rows pile into the default partition until retention removes the partitions 028 made,
    about a week later. The Docker image runs PostgreSQL in UTC; distro packages usually use the host's
    timezone.
  • Thirteen indexes cost writes and serve nothing. Ten duplicate a primary key or unique
    constraint, or are a leading prefix of another index on the same table. Three are never read: the
    episodes full-text indexes left behind when episode search moved to episode_catalog_entries,
    and an HNSW index on user_taste_clusters, which is only read by profile.
  • Six older CREATE INDEX CONCURRENTLY IF NOT EXISTS migrations can leave an index unusable for
    good.
    If a build was interrupted, the retry skipped the invalid leftover and goose recorded the
    migration as applied.
  • normalize_search_number_token cannot be inlined because its body is a CTE, so
    normalize_search_text plans a subquery for every token of every title write and search.
  • JIT is on by default. Silo's short catalog queries often cross jit_above_cost and spend
    longer compiling than running; two call sites already turn it off per transaction.
  • Operational-log cursor pages scan every daily partition, because PostgreSQL cannot prune on
    the (timestamp, id) < (...) row comparison.

Approach

  • Migration 028 sets TimeZone to UTC for its own transaction. Only fresh installs run it;
    editing an applied legacy migration for fresh-install behavior follows fix(playback): align default transcode directory #573.
  • A new migration drops the 13 indexes concurrently and names each one's replacement.
    idx_user_watch_progress_profile and idx_media_files_folder stay even though a superset could
    serve them: deduplication keeps them several times smaller, so profile-wide progress reads and
    per-folder file counts touch far fewer pages.
  • A Go migration rebuilds whichever of the six indexes is invalid or missing, with
    DROP INDEX CONCURRENTLY and CREATE INDEX CONCURRENTLY, so serving replicas keep reading and
    writing those tables; valid indexes are left alone. It is Go because SQL cannot choose per index
    outside a transaction, and a plain DROP INDEX would take an ACCESS EXCLUSIVE lock.
  • normalize_search_number_token becomes a single CASE expression with the same output, so
    stored columns and indexes stay valid without a rebuild.
  • Each new pool connection turns JIT off unless the server configuration, ALTER DATABASE or
    ALTER ROLE, or DATABASE_URL already set jit; it reads pg_settings.source rather than
    parsing the URL. The opt-in tuner recommends the same value.
  • The ops-log cursor adds a redundant timestamp <= $cursor bound so PostgreSQL prunes newer
    partitions.
  • DEVELOPMENT.md documents the migration lock-safety rules these changes follow, and
    .env.example notes that POSTGRES_TUNE_CONNECTIONS must cover every Silo process's pool in a
    cluster.

The review also found that hnsw.ef_search can exceed pgvector's limit of 1000; #2095 fixes that,
so this PR leaves it out. Commits are split by concern.

Validation

  • Go build, gofmt, vet, and make lint-changed: pass. Every commit builds on its own.
  • make test-go: pass, except internal/mediasample TestRunStatsWithRealFFmpeg, which runs the
    host's ffmpeg build and depends on no changed package.
  • Web gate and all verify-* targets: pass.
  • DB pins (5 new) and DB contracts on a freshly migrated PostgreSQL 18.6 with pgvector 0.8.6: pass.
    Each new test fails on the code it replaces.
  • On a shared development deployment with a real catalog (434k items, 2.2M episodes, 1.7M files),
    recomputing every stored normalized title with the new function gave 0 mismatches, and the access
    paths that lose an index ran equal or faster. That deployment ran an earlier revision of these
    migrations; its schema was then brought in line with this one.
  • Validation tasks: Calendar and Person browsing read tables that lose indexes. Their queries keep
    explicit ORDER BY, and dropping an index does not change results, so their passed cases are
    unaffected. No other validated feature reaches this change.
Measurements
Check Where Result
Stored normalizations vs. new function Development catalog 0 mismatches across 434,474 items (title, original, sort), 365,152 aliases, 1,185,140 episode entries
New vs. migration 138 function 200,075 synthetic tokens 0 mismatches with standard_conforming_strings on and off
normalize_search_text over 50,000 titles Local PostgreSQL 18.6 1,782 ms → 612 ms
Ops-log page five days back Development catalog 12 partitions in 2.9 ms → 4 in 0.13 ms
Unused episodes full-text indexes Development catalog 346 MB + 64 MB, 0 scans
Profile-wide progress count, idx_user_watch_progress_profile dropped Development catalog 192 → 724 pages read, so the index stays

Evidence

Evidence: none, no user-visible change

Risks

  • The index drops were measured against one development catalog, not production. Comparing
    idx_scan for these indexes on production before merge would confirm none is still hot. Down
    recreates them concurrently.
  • With JIT off by default, a query that benefits from JIT gets slower. None is known, and jit=on in
    DATABASE_URL or the server configuration restores it.
  • Where one of the six repaired indexes is invalid, the first startup rebuilds it
    concurrently. That takes time on a large table but does not block other sessions.
  • The JIT default and cluster connection sizing affect the PostgreSQL pages on
    siloserver.org. They are added to the open manual issue that covers those pages,
    docs: add the Docker deployment details the server guide is dropping siloserver.org#38, and the site update waits for this PR to merge.

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern.
  • The Evidence section shows every change a user can see, or says there is none.

AI Disclosure

  • Harness: T3 Code through the Claude Code harness
  • Tool(s): Claude Code; Codex (adversarial review)
  • Model(s): claude-opus-5-5; gpt-6.1-sol (adversarial review)
  • Involvement: Fully AI-generated, human verification pending
  • Adversarial review: Three read-only rounds by Codex (gpt-6.1-sol, high reasoning) against the diff
    and a scratch PostgreSQL 18.6 cluster, covering migration parsing and locking, index redundancy,
    function equivalence, JIT precedence, and the tests. They found a regex backreference that broke
    under standard_conforming_strings=off, a repair that took ACCESS EXCLUSIVE locks and later one
    that skipped an index after an interrupted drop, an index worth keeping, and URL-based JIT
    detection that misread options. Each is fixed with a test that fails on the earlier code; JIT
    detection now reads pg_settings.source. A finding that the Go migration starves on a
    one-connection pool was dismissed: migrations always run on the 20-connection bootstrap pool.

🤖 Generated with Claude Code

Quick104 and others added 7 commits October 8, 2026 14:50
On a server whose TimeZone is not UTC, migration 028 cut the first
operational_logs and activity_log partitions at local midnight while
partman extends them at UTC midnight, so the next partition overlapped or
left a gap and log writes landed in the default partition. Only fresh
installs run 028.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CTE body kept PostgreSQL from inlining the function, so
normalize_search_text planned a subquery for every token of every title
write and search. The single CASE body returns the same output, so stored
columns and indexes stay valid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… builds

Six migrations ran CREATE INDEX CONCURRENTLY IF NOT EXISTS without first
dropping an invalid copy, so an interrupted build left an index the
planner never uses. A Go migration rebuilds whichever of them is invalid
or missing with DROP and CREATE INDEX CONCURRENTLY, so serving replicas
keep reading and writing those tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ten indexes duplicate a primary key or unique constraint or are a leading
prefix of another index on the same table; three are never read. Each
one only costs writes on tables that scans, metadata refreshes and
progress reports rewrite constantly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silo's short catalog queries often cross jit_above_cost and take longer
to compile than to run. Each new connection turns JIT off unless the
server configuration, the database or role, or DATABASE_URL already set
it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostgreSQL cannot prune partitions on the (timestamp, id) row comparison,
so every cursor page scanned every daily partition. A redundant bare bound
on timestamp lets it skip the ones newer than the cursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 19 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ed65e43b-1487-4766-b727-6d7b0d4ceeb5
📥 Commits

Reviewing files that changed from the base of the PR and between 63f8bcb and 67b9048.

📒 Files selected for processing (17)
  • .env.example
  • DEVELOPMENT.md
  • internal/database/invalid_index_rebuild.go
  • internal/database/invalid_index_rebuild_test.go
  • internal/database/log_partition_timezone_test.go
  • internal/database/migrate.go
  • internal/database/postgres.go
  • internal/database/postgres_jit_test.go
  • internal/database/postgres_tune.go
  • internal/opslog/repo.go
  • internal/opslog/repo_db_test.go
  • internal/userstore/pgstore/progress.go
  • migrations/search_number_token_test.go
  • migrations/sql/028_log_partitioning.sql
  • migrations/sql/20261008165511_inline_search_number_token.sql
  • migrations/sql/20261008170456_drop_redundant_indexes.sql
  • scripts/ci/db-pins.txt
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T19:06:57.476419Z 67b9048 New commits
🔒 Security Review ✅ Completed 2026-10-08T19:07:14.776564Z 67b9048 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Quick104 Quick104 added priority: P2 Limited scope, workaround exists, or polish impact: compat Breaks a supported client or server path labels Oct 8, 2026 — with Cursor

Quick104 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Note

Grok commenting on Quick's behalf.

Ranking: impact: compat, priority: P2. The headline fix is broken log partitioning on fresh installs whose PostgreSQL isn't in UTC (a supported distro-package path), but it clears itself after about a week of retention. The rest is performance work that every deployment benefits from.

Scope: this bundles six separable database changes (partition timezone fix, 13 index drops, invalid-index rebuild, search-function inlining, JIT off by default, ops-log pruning), and the "one concern" box is unchecked. Consider splitting the partition fix out from the riskier index-drop and JIT-default changes so each can merge on its own.

Comment thread scripts/ci/db-pins.txt Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d41bdfaf16

ℹ️ 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 thread scripts/ci/db-pins.txt Outdated
Splitting the branch into commits appended the earlier pin blocks again in each later commit, and the pin runner rejects duplicate entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌
⚠️ 1 Kody Rule(s) were not evaluated

These rules declare that they need repository context beyond this diff, and the review could not retrieve it, so they were not judged on this pull request:

  • Preserve optional store capabilities through production wrappers

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67b9048702

ℹ️ 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 thread .env.example
@Quick104
Quick104 merged commit 3eb7645 into main Oct 8, 2026
43 of 45 checks passed
@Quick104
Quick104 deleted the fix/postgres-best-practices branch October 8, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact: compat Breaks a supported client or server path priority: P2 Limited scope, workaround exists, or polish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant