Skip to content

Expired cache rows are never evicted, causing app.db to reach ~100 MB and sustained multi-MB/s reads at idle #7331

Description

@LouisGue

Please confirm the following.

  • I checked the existing issues for duplicate problems
  • I have tried resolving the issue using the support portal
  • I have ensured my Modrinth App installation is up to date

What version of the Modrinth App are you using?

0.18.2

What operating systems are you seeing the problem on?

Linux

Describe the bug

The cache table in app.db grows without bound. The expires column is populated correctly, but nothing ever deletes rows once they are past it, so every entry the app has written stays in the file permanently.

This matters because most cached types are short lived. Measuring the remaining TTL on my install:

data_type          rows    remaining TTL
version            1114    3 to 11 minutes
user                 74    3 to 11 minutes
team                 60    3 to 12 minutes
project              54    3 to 11 minutes
version_v3           54    3 to 12 minutes
file_hash            69    ~30 days
file                 54    ~30 days

So version entries live about half an hour, yet after a few days of ordinary use my cache table held 16792 version rows totalling 74.4 MB. Almost all of it was data that had expired long before.

Resulting state

app.db had reached 97 MB, of which 85 MB was the cache table:

data_type            rows      size
version              16792     74.4 MB
project_versions        20      4.5 MB
search_results_v3       48      3.8 MB
project                113      1.0 MB
loader_manifest          4      0.8 MB

4796 rows (16.6 MB) were already past their expires timestamp.

app.db-wal was also sitting at 54 MB, and was still 54 MB after a clean application shutdown, so it does not appear to be checkpointed on exit.

Clearing it

I closed the app, ran DELETE FROM cache, PRAGMA wal_checkpoint(TRUNCATE) and VACUUM. app.db went from 97 MB to 639 kB. PRAGMA integrity_check returned ok before and after, and no user data was affected: instances, content entries, settings and Java versions all survived, and the cache repopulated from the network normally.

It refills quickly. Within about an hour of ordinary use it was back to roughly 4000 rows and 10 MB.

Scale context

This is not a heavy install. 3 instances, 134 total content entries. The 16792 cached version rows came from ordinary catalogue browsing over a few days.

Suggested fixes

  1. Evict rows where expires has passed, on startup or periodically.
  2. Cap the cache table, for example by row count or total size per data_type, evicting least recently used entries.
  3. Checkpoint the WAL on shutdown so it does not persist at tens of MB.

Scope

To be clear about what this issue is and is not: I am reporting unbounded growth of an on-disk cache. I separately see high CPU and multi-GB memory growth in the app while browsing (#7276), but I measured the cache table growing by only about 5 MB during a window where RSS grew 4.3 GB, so I have no evidence the two are connected and I am not claiming they are.

Steps to reproduce

1. Note the starting state

sqlite3 ~/.local/share/ModrinthApp/app.db \
  "select count(*), sum(length(data))/1048576.0 from cache;"

2. Use the app normally for a few minutes. Open Discover, search for a few mods, open some project pages. Nothing unusual is needed.

3. Check the TTLs.

sqlite3 ~/.local/share/ModrinthApp/app.db \
  "select data_type, count(*), min(expires - strftime('%s','now'))/60 as min_ttl_minutes
   from cache group by data_type order by count(*) desc;"

Most data_type values are short lived. On my install version, user, team, project and version_v3 entries expire about half an hour after being written.

4. Wait until those TTLs have passed, then count the expired rows.

sqlite3 ~/.local/share/ModrinthApp/app.db \
  "select count(*), sum(length(data))/1048576.0 from cache
   where expires < strftime('%s','now');"

They are all still present. Nothing removes them. This is the bug: the count only ever goes up, so the table accumulates every short lived entry the app has ever written. After a few days of ordinary use mine held 16792 version rows totalling 74.4 MB, of which 4796 rows (16.6 MB) were already expired.

5. Observe the idle read amplification. With the app open and doing nothing, watch the backend process:

watch -n1 'grep rchar /proc/$(pgrep -x ModrinthApp)/io'

With a ~100 MB app.db this climbed at 24 to 42 MB/s continuously. After clearing the cache it dropped to 1 to 2 MB/s.

Note on how fast this refills. After I cleared the cache completely, it was back to 1578 rows and 6.4 MB in under half an hour, with app.db going from 639 kB to 8.0 MB. I am quoting that as an upper bound on elapsed time rather than a controlled measurement, since the app was not running for that entire window.

Expected behavior

Expired rows should be removed from the cache table, either on startup, periodically, or lazily on read. app.db should stay bounded in size regardless of how long the app has been in use.

Idle disk reads should be close to zero when the app is open with no interaction and no install running, rather than scaling with the accumulated cache size.

app.db-wal should be checkpointed on a clean shutdown rather than persisting at tens of MB.

Additional context

Hardware: Intel Core Ultra 9 185H + RTX 4060, 30 GB RAM, Fedora 44, KDE Plasma on Wayland. Native RPM package.

The expires values are unix epoch seconds, so expired rows can be counted with:

sqlite3 ~/.local/share/ModrinthApp/app.db \
  "select count(*), sum(length(data))/1048576.0 from cache
   where expires < strftime('%s','now');"

On my install that returned 4796 rows and 16.6 MB before I cleared the table. The count only ever goes up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    appRelates to Modrinth App

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions