Skip to content

feat: rework persistence for async sqlx backends and PostgreSQL support#1695

Open
DamnCrab wants to merge 8 commits intotorrust:developfrom
DamnCrab:codex/pg-adaptation-rework
Open

feat: rework persistence for async sqlx backends and PostgreSQL support#1695
DamnCrab wants to merge 8 commits intotorrust:developfrom
DamnCrab:codex/pg-adaptation-rework

Conversation

@DamnCrab
Copy link
Copy Markdown

Summary

This PR reworks the tracker persistence layer to make PostgreSQL support fit the current async architecture, instead of adding PostgreSQL as a special case on top of the previous synchronous database abstraction.

The main goals were:

  • remove the previously rejected synchronous PostgreSQL execution model
  • split the monolithic database interface into narrower persistence capabilities
  • move the SQL backends to a shared async substrate
  • add PostgreSQL as a first-class backend
  • widen persisted download counters to 64-bit internally while preserving BitTorrent protocol compatibility at the encoding boundary

This branch also includes the follow-up fixes from review and the regressions found while running the full local test suite.

Why

The previous PostgreSQL attempt worked around the sync/async mismatch with a blocking execution model that is not a good fit for the rest of the project.

This rework addresses that first:

  • persistence is now async-native
  • SQLite, MySQL, and PostgreSQL follow the same structure
  • schema ownership is centralized in migrations
  • PostgreSQL is added on top of the same persistence model as the existing backends

Main Changes

Persistence redesign

  • replaced the old monolithic Database trait with narrower async traits:
    • SchemaMigrator
    • TorrentMetricsStore
    • WhitelistStore
    • AuthKeyStore
  • added a Persistence facade so the rest of the codebase can move to the new structure without introducing a single “god object” again

Async SQL backends

  • migrated SQLite and MySQL from the previous synchronous stack to sqlx
  • added a PostgreSQL backend using the same async approach
  • removed the old blocking PostgreSQL workaround

Schema and migrations

  • migrations are now the source of truth for schema changes
  • added PostgreSQL migrations
  • added the matching no-op SQLite migration for the counter widening step so migration history stays aligned across backends

Counter widening and protocol boundaries

  • widened persisted download counters from u32 to u64
  • kept peer counts as 32-bit where that still matches their meaning
  • preserved protocol compatibility by saturating only at the BitTorrent protocol boundary where 32-bit fields are required

Follow-up fixes included in this branch

  • removed a UDP health-check panic path caused by an internal unwrap()
  • fixed async persistence timing in tests by waiting for eventual DB writes when needed
  • hardened database fault-injection tests after the lazy schema initialization changes
  • documented the drop_database_tables() contract used by tests
  • improved diagnostics for persisted-download polling failures

Validation

I ran the following locally:

  • cargo test --workspace --all-targets
  • database compatibility matrix across:
    • SQLite 3.51.0
    • MySQL 8.0, 8.4
    • PostgreSQL 14, 15, 16, 17

I also ran real qBittorrent end-to-end tests against the tracker:

  • SQLite + UDP
  • MySQL 8.0 + HTTP
  • PostgreSQL 16 + HTTP

Those runs completed real seeder/leecher transfers and ended with the expected tracker scrape state:

  • complete = 2
  • downloaded = 1
  • incomplete = 0

Benchmark Notes

I also compared the previous implementation and this branch with the same black-box workloads.

High-level results:

  • SQLite is mostly neutral overall, although reload paths are slower
  • MySQL is modestly better on announce and sequential write-heavy paths, with slightly slower reloads
  • PostgreSQL shows the clearest benefit from the rework

Representative PostgreSQL results from the before/after comparison:

  • whitelist_add_seq: about +61%
  • auth_key_add_seq: about +30%
  • auth_key_reload: about +38%

Notes

This PR is intentionally broader than “just add a PostgreSQL driver”, because the required PostgreSQL support is tied to the persistence redesign itself.

If you would prefer, I can split this into smaller follow-up PRs after initial review, but I kept the branch together because the persistence trait split, async SQL migration, and PostgreSQL backend are tightly connected.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Reworks tracker persistence to be async-native via sqlx, adds PostgreSQL as a first-class backend, and widens persisted download counters to 64-bit while clamping at protocol encoding boundaries.

Changes:

  • Replaces the monolithic database abstraction with async, domain-focused store traits behind a Persistence facade.
  • Migrates SQLite/MySQL to sqlx, adds a new PostgreSQL driver, and centralizes schema ownership in SQL migrations.
  • Widens persisted download counters to 64-bit and updates UDP/HTTP scrape encoding to saturate where protocol fields are narrower.

Reviewed changes

Copilot reviewed 68 out of 70 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
share/default/config/tracker.container.postgresql.toml Adds a default container config for PostgreSQL deployments.
share/container/entry_script_sh Adds PostgreSQL driver option and selects the PostgreSQL default config.
packages/udp-tracker-server/src/handlers/scrape.rs Clamps scrape counters at the UDP protocol boundary; adds saturation tests.
packages/udp-tracker-server/src/handlers/mod.rs Updates tests to use the new torrent-metrics store handle.
packages/udp-tracker-server/src/handlers/announce.rs Updates tests to use the new torrent-metrics store handle.
packages/tracker-core/tests/integration.rs Awaits the now-async torrent load call in integration coverage.
packages/tracker-core/tests/common/test_env.rs Adds an eventual-consistency wait helper for persisted download counters.
packages/tracker-core/src/whitelist/test_helpers.rs Updates whitelist test wiring to use whitelist_store().
packages/tracker-core/src/whitelist/setup.rs Refactors whitelist setup to accept a WhitelistStore handle.
packages/tracker-core/src/whitelist/repository/persisted.rs Makes persisted whitelist operations async and store-trait based; updates tests.
packages/tracker-core/src/whitelist/manager.rs Awaits persisted whitelist operations; updates related tests.
packages/tracker-core/src/torrent/manager.rs Makes torrent loading from persistence async and store-backed.
packages/tracker-core/src/test_helpers.rs Updates helper wiring to use the torrent-metrics store handle.
packages/tracker-core/src/statistics/persisted/mod.rs Loads persisted global downloads asynchronously and treats them as u64.
packages/tracker-core/src/statistics/persisted/downloads.rs Refactors downloads repository to async + TorrentMetricsStore; updates tests for u64.
packages/tracker-core/src/statistics/event/handler.rs Awaits async persistence updates when handling completion events.
packages/tracker-core/src/databases/setup.rs Makes database initialization return Persistence and adds PostgreSQL driver selection.
packages/tracker-core/src/databases/mod.rs Introduces Persistence and async store traits (SchemaMigrator, TorrentMetricsStore, WhitelistStore, AuthKeyStore).
packages/tracker-core/src/databases/error.rs Reworks persistence error type to support sqlx and migration errors.
packages/tracker-core/src/databases/driver/sqlite.rs Reimplements SQLite backend using sqlx + migrations and async store traits.
packages/tracker-core/src/databases/driver/postgres.rs Adds a new PostgreSQL backend using sqlx + migrations and async store traits.
packages/tracker-core/src/databases/driver/mysql.rs Reimplements MySQL backend using sqlx + migrations and async store traits.
packages/tracker-core/src/databases/driver/mod.rs Updates driver factory to return Persistence and refactors driver integration tests to async.
packages/tracker-core/src/container.rs Wires TrackerCoreContainer to the new Persistence facade and store handles.
packages/tracker-core/src/authentication/mod.rs Updates auth wiring to use auth_key_store().
packages/tracker-core/src/authentication/key/repository/persisted.rs Refactors persisted key repository to async + AuthKeyStore; updates tests.
packages/tracker-core/src/authentication/key/mod.rs Updates key error conversion to map from the new persistence error type.
packages/tracker-core/src/authentication/handler.rs Awaits async persisted key operations.
packages/tracker-core/src/announce_handler.rs Makes DB download-metric loading async in announce path.
packages/tracker-core/migrations/sqlite/20260409120000_torrust_tracker_widen_download_counters.sql Adds a no-op SQLite migration to keep migration history aligned.
packages/tracker-core/migrations/sqlite/20240730183000_torrust_tracker_create_all_tables.sql Fixes comment syntax to SQL comment style in SQLite migration file.
packages/tracker-core/migrations/postgresql/20260409120000_torrust_tracker_widen_download_counters.sql Widens PostgreSQL counter columns to BIGINT.
packages/tracker-core/migrations/postgresql/20250527093000_torrust_tracker_new_torrent_aggregate_metrics_table.sql Adds PostgreSQL torrent aggregate metrics table migration.
packages/tracker-core/migrations/postgresql/20240730183500_torrust_tracker_keys_valid_until_nullable.sql Makes PostgreSQL valid_until nullable.
packages/tracker-core/migrations/postgresql/20240730183000_torrust_tracker_create_all_tables.sql Adds PostgreSQL base schema migration.
packages/tracker-core/migrations/mysql/20260409120000_torrust_tracker_widen_download_counters.sql Widens MySQL counter columns to BIGINT.
packages/tracker-core/migrations/mysql/20240730183000_torrust_tracker_create_all_tables.sql Fixes comment syntax to SQL comment style in MySQL migration file.
packages/tracker-core/migrations/README.md Updates documentation to reflect automatic migrations and backend split.
packages/tracker-core/Cargo.toml Switches persistence dependencies to sqlx + async-trait and removes r2d2 drivers.
packages/tracker-client/src/udp/client.rs Removes a panic path in UDP health-check by handling receive errors.
packages/torrent-repository-benchmarking/tests/repository/mod.rs Updates metrics/download count types to u64.
packages/torrent-repository-benchmarking/src/repository/skip_map_mutex_std.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_tokio_mutex_tokio.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_tokio_mutex_std.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_tokio.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_std_mutex_tokio.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_std_mutex_std.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/rw_lock_std.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/repository/dash_map_mutex_std.rs Updates metrics/download count aggregation to use u64 directly.
packages/torrent-repository-benchmarking/src/entry/mod.rs Widens torrent entry downloaded field to NumberOfDownloads.
packages/swarm-coordination-registry/src/swarm/registry.rs Updates aggregate downloaded metric math to use u64 directly.
packages/swarm-coordination-registry/src/swarm/coordinator.rs Widens coordinator constructor downloaded parameter to NumberOfDownloads.
packages/primitives/src/swarm_metadata.rs Widens downloaded in SwarmMetadata to NumberOfDownloads and documents rationale.
packages/primitives/src/lib.rs Widens NumberOfDownloads from u32 to u64.
packages/http-tracker-core/src/services/scrape.rs Updates tests to use the torrent-metrics store handle.
packages/http-tracker-core/src/services/announce.rs Updates tests to use the torrent-metrics store handle.
packages/http-tracker-core/benches/helpers/util.rs Updates benchmark setup to use the torrent-metrics store handle.
packages/http-protocol/src/v1/responses/scrape.rs Saturates large download counts when bencoding scrape responses; adds tests.
packages/configuration/src/v2_0_0/database.rs Adds PostgreSQL to config driver enum and URL secret masking coverage.
packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/whitelist.rs Updates fault-injection helper usage to be async.
packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/auth_key.rs Updates fault-injection helper usage to be async.
packages/axum-rest-tracker-api-server/tests/server/mod.rs Makes force_database_error async and uses schema_migrator() handles.
packages/axum-http-tracker-server/src/v1/handlers/announce.rs Updates tests to use the torrent-metrics store handle.
packages/axum-health-check-api-server/tests/server/contract.rs Makes UDP health-check assertions tolerant of non-timeout socket errors.
docs/postgresql-adaptation-rework.md Adds design/benchmark write-up for the PostgreSQL + persistence redesign.
contrib/dev-tools/qa/run-qbittorrent-e2e.py Adds an end-to-end qBittorrent QA runner for HTTP/UDP across DB backends.
contrib/dev-tools/qa/run-db-compatibility-matrix.sh Adds a script to run a DB version compatibility matrix across MySQL/PostgreSQL.
contrib/dev-tools/qa/run-before-after-db-benchmark.py Adds a before/after benchmark harness for persistence-related workloads.
.gitignore Ignores Python bytecode caches for the new QA scripts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 229 to 231
// code-review: should we return a friendly error instead of the DB
// constrain error when the key already exist? For now, it's returning
// the specif error for each DB driver when a UNIQUE constrain fails.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Spelling/grammar in this comment makes it harder to read (e.g. “constrain”→“constraint”, “exist”→“exists”, “specif”→“specific”). Consider correcting it since this area was touched in the refactor.

Copilot uses AI. Check for mistakes.
pub type DurationSinceUnixEpoch = Duration;

pub type NumberOfDownloads = u32;
pub type NumberOfDownloads = u64;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

NumberOfDownloads is defined as u64, but SQL backends store counters in signed 64-bit columns (BIGINT) and the drivers encode via i64::try_from(...). This means values > i64::MAX cannot be persisted even though the public type suggests they can. Consider switching NumberOfDownloads to i64 (or a newtype wrapping i64) across the codebase, or explicitly documenting and saturating/clamping at i64::MAX when persisting/loading to keep the effective range consistent across SQLite/MySQL/PostgreSQL.

Suggested change
pub type NumberOfDownloads = u64;
pub type NumberOfDownloads = i64;

Copilot uses AI. Check for mistakes.
Comment on lines +74 to 80
fn decode_counter_i64(&self, value: i64) -> Result<NumberOfDownloads, Error> {
u64::try_from(value).map_err(|err| Error::invalid_query(DRIVER, err))
}

Ok(())
fn encode_counter(&self, value: NumberOfDownloads) -> Result<i64, Error> {
i64::try_from(value).map_err(|err| Error::invalid_query(DRIVER, err))
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

encode_counter() converts NumberOfDownloads (u64) to i64 and maps overflow to an InvalidQuery error. With the counters widened to u64, this makes persistence fail once the value exceeds i64::MAX (the actual range of the BIGINT columns). Either clamp/saturate before converting (and treat BIGINT as the true max) or change the counter type to a signed 64-bit representation so the type-level contract matches what the DB can store.

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 79.75207% with 196 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.29%. Comparing base (d178b2b) to head (d50ce7d).

Files with missing lines Patch % Lines
...ages/tracker-core/src/databases/driver/postgres.rs 69.81% 43 Missing and 21 partials ⚠️
...ackages/tracker-core/src/databases/driver/mysql.rs 60.62% 31 Missing and 19 partials ⚠️
...ckages/tracker-core/src/databases/driver/sqlite.rs 66.66% 21 Missing and 20 partials ⚠️
packages/tracker-core/src/databases/error.rs 70.51% 19 Missing and 4 partials ⚠️
packages/tracker-core/src/databases/driver/mod.rs 96.84% 3 Missing and 3 partials ⚠️
...tracker-core/src/statistics/persisted/downloads.rs 87.87% 2 Missing and 2 partials ⚠️
packages/tracker-core/src/announce_handler.rs 33.33% 0 Missing and 2 partials ⚠️
...tracker-core/src/whitelist/repository/persisted.rs 92.30% 0 Missing and 2 partials ⚠️
packages/tracker-client/src/udp/client.rs 66.66% 1 Missing ⚠️
packages/tracker-core/src/databases/setup.rs 75.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1695      +/-   ##
===========================================
+ Coverage    86.50%   87.29%   +0.78%     
===========================================
  Files          288      289       +1     
  Lines        22672    22948     +276     
  Branches     22672    22948     +276     
===========================================
+ Hits         19613    20033     +420     
+ Misses        2833     2668     -165     
- Partials       226      247      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DamnCrab DamnCrab requested a review from a team as a code owner April 17, 2026 07:23
Copy link
Copy Markdown
Member

@josecelano josecelano left a comment

Choose a reason for hiding this comment

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

Hi @DamnCrab, thank you for your PR. It looks good, and I think it's a very valuable feature for the community.

I'm going to start a detailed review (I will write comments on code blocks and maybe another set of questions). These are my first impressions:

  1. Things to fix before merging:
  • Sign commits
  • Pass all PR checks
  1. Things I would recommend doing:

This is a big PR. As you mentioned, it would have been better to implement it progressively. However, this PR give us a good understanding of all things we need to change to support PostgreSQL. Now that we have learnt a lot I think it would be convenient to do an implementation plan with two main parts:

  • Refactor plan: Refactor the current state to make the new feature easy to implement.
  • Add support for PostgreSQL

“Make the change easy, then make the easy change”.

Steps could be like something like this:

  • Add more E2E tests (as you did). For example using a real client like you do. Everything that add tests should be included as soon as possible.
  • Align Rust types with DB types (fix problems with misalignment between max u64 and the representation in the DB)
  • Split the Database trait
  • Migrate current drivers to sqlx crate (without migrations)
  • Introduce automatic migrations
  • Add new driver for postgres

Notice that all steps could be merged independently from the new feature.

I understand that doing that now can be a big effort for you, so if you prefer to keep it all in one I will try to merge it as it's because It's a nice feature to have, but it can take me longer to carefully review it.

  1. I have some initial questions (I haven't checked the code yet)

A) Are all migrations safe to execute in old databases?
B) Why the postgresql driver has more migrations? Should not all the drivers have the same?
C) I think it's a good idea to use a real client. I have to review that code. However I would prefer to use Rust instead of python. We are already using container for E2E testing in other parts. On the other hand, I wonder if the tracker client would be enough for the tests you are doing (I have not checked them yet). I'm planning tho change the tracker client to accept more arguments so we can simulate to be a bittorrent client. That should be simpler and faster for testing.

That's it for now. I may add some more comments.

@josecelano
Copy link
Copy Markdown
Member

Hi @DamnCrab, I prefer not to have python code:

  • contrib/dev-tools/qa/run-qbittorrent-e2e.py
  • contrib/dev-tools/qa/run-before-after-db-benchmark.py

Those are valuable tests but:

  • We should port them to Rust (I do not want to maintain code in two languages for testing if there is no reason for it)
  • They have some duplicate code
  • Ideally we should run them in CI with workflows (if they are not too expensive)

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.

3 participants