Skip to content

Apply SQLite connection settings to every connection - #1677

Merged
burma-shave merged 6 commits into
mainfrom
ab/1659-connection-pragmas
Nov 8, 2026
Merged

burma-shave merged 6 commits into
mainfrom
ab/1659-connection-pragmas

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Fixes #1659. Part of #1657: steady-state memory on a 512 MB instance. Stacked on #1676.

What it does

cache_size goes in the DSN for both drivers (mattn _cache_size, modernc _pragma=cache_size(...)), at 2 MB, SQLite's built-in default. temp_store=MEMORY is dropped, so large sorts spill to temp files. The pure-Go driver gets the same 5 s busy_timeout that mattn sets by default. journal_mode=WAL stays a PRAGMA because it is stored in the database file.

Root cause

configureSQLitePerformance set cache_size and temp_store with db.ExecContext, which runs on one pooled connection, and both settings are per connection. One connection had a 64 MB page cache and in-memory temp tables while the other 24 used SQLite's defaults; which one a query got was random; and after SetConnMaxLifetime (5 min) closed that connection, no connection had them.

Testing

  • Test failing on main before this change: TestSQLitePerformancePragmasApplied, run against the base branch (three pooled connections of a file database):
# maglev.onebusaway.org/gtfsdb.test
--- FAIL: TestSQLitePerformancePragmasApplied (0.01s)
    sqlite_performance_test.go:46: 
        	Error Trace:	/Users/aaron/repos/onebusaway/red-1677/gtfsdb/sqlite_performance_test.go:46
        	Error:      	Not equal: 
        	            	expected: -2000
        	            	actual  : -64000
        	Test:       	TestSQLitePerformancePragmasApplied
        	Messages:   	connection 0 cache size
    sqlite_performance_test.go:47: 
        	Error Trace:	/Users/aaron/repos/onebusaway/red-1677/gtfsdb/sqlite_performance_test.go:47
        	Error:      	Not equal: 
        	            	expected: 0
        	            	actual  : 2
        	Test:       	TestSQLitePerformancePragmasApplied
        	Messages:   	connection 0 keeps SQLite's default temp store
FAIL
FAIL	maglev.onebusaway.org/gtfsdb	0.207s
FAIL
  • Manual check: NJ Transit bus import, TestImportMemory, at the previous PR in the stack and at this branch:
previous PR (#1676) this PR
inserting_stop_times peak heap 63.15 MiB 67.08 MiB
building_block_layover_index peak heap 81.53 MiB 45.11 MiB
process peak RSS 324.19 MiB 185.66 MiB
import time 26.961s 25.979s

Full per-stage output is in the TestImportMemory log; the table shows the stages this PR moves.

Tests:

  • TestSQLitePerformancePragmasApplied: holds three pooled connections of a file database open at once and checks cache_size, busy_timeout, temp_store and journal_mode on each. The old version only passed because an in-memory database has a single connection.
  • TestDSNCacheSizeReachesEveryConnection: opens the DSN with a 3000 KiB cache and checks -3000 on three connections, so a dropped DSN parameter is caught (2000 KiB is also SQLite's default, which the first test cannot tell apart).
  • TestDSNAppendsConnectionSettingsWithCorrectSeparator: golden DSN strings for a plain path and for a path that already has a query string.
  • TestDSNWithExistingQueryOpens: the & form opens and the driver honours foreign_keys and busy_timeout.
  • TestMemoryDatabaseConnectionSettings: :memory: keeps foreign_keys, busy_timeout and journal_mode=memory.
  • TestForeignKeysEnabledOnEveryPooledConnection: unchanged, now through the shared holdConnections helper.
    The pure-Go busy_timeout assertion only discriminates under -tags purego, which CI does not run; it passes locally in both modes.

Checklist

  • This PR addresses one issue.
  • It is under ~200 changed lines, or the description says why it can't be split.
  • This PR is not a bug fix, or the new or changed test fails on main and passes on this branch.
  • Each commit is one logical change with a clear message.
  • I did not hand-edit sqlc-generated files under gtfsdb/ (db.go, models.go, query.sql.go) or testdata/openapi.yml (synced from upstream).
  • This PR changes no handler, or handlers pass r.Context() to DB/service calls, not context.Background().
  • The response comparison above is filled in, or this PR doesn't change a Java-served response.
  • This PR adds no endpoint, or each new endpoint matches testdata/openapi.yml, has an issue open on OneBusAway/sdk-config, or is outside the SDK and the description says why.
  • I read every line of this diff and can explain it if asked.

Notes for reviewers

Size. 156 added, 74 removed; 108 of the additions are tests, most of them holding several pooled connections open to check each one.

Why 2 MB. It bounds SQLite's page cache at about 50 MB across 25 connections; applying the old 64 MB to every connection could have held 1.6 GB. Earlier k6 runs against Sound Transit (up to 500 VUs) showed no latency gain from a bigger cache, because the OS already caches the database file.

Temp files. Without temp_store=MEMORY, SQLite writes temp files to SQLITE_TMPDIR/TMPDIR (/tmp in the image, writable by the maglev user). A deployment with a read-only root filesystem and no writable /tmp would need SQLITE_TMPDIR pointed at the data volume.

@coderabbitai

coderabbitai Bot commented Oct 20, 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 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ad6f9a47-cec7-4c02-9359-7c48fb4e6d75
Commits

Reviewing files that changed from the base of the PR and between 1ef7815 and d1f9795.

Files selected for processing: 6

  • gtfsdb/config.go
  • gtfsdb/driver_cgo.go
  • gtfsdb/driver_pure.go
  • gtfsdb/foreign_keys_test.go
  • gtfsdb/helpers.go
  • gtfsdb/sqlite_performance_test.go
  • Autofix · 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

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.0 ms
Error rate 0.00%
Total requests 336
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@aaronbrethorst
aaronbrethorst force-pushed the ab/1659-connection-pragmas branch from ac45350 to 811ebbd Compare October 20, 2026 07:39
@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.7 ms
Error rate 0.00%
Total requests 337
Req/sec 11.1

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.6 ms
Error rate 0.00%
Total requests 334
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.5 ms
Error rate 0.00%
Total requests 326
Req/sec 10.7

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

Comment thread gtfsdb/helpers.go Outdated
// concurrent readers and a single writer. journal_mode=WAL is stored in the
// database file, so one PRAGMA applies it to every connection. That is why it
// stays a PRAGMA, while the connection-scoped settings ride the DSN.
func configureSQLitePerformance(ctx context.Context, db *sql.DB) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With temp_store=MEMORY gone, SQLite sort and temp-table spills (for example CREATE INDEX during import) go to SQLITE_TMPDIR/TMPDIR, which defaults to /tmp. This stack's own downloadDir (internal/gtfs/feed_file.go) avoids /tmp because it "may be held in memory (tmpfs)". On a host where /tmp is tmpfs, the spill still counts against the 512 MB memory limit, so the saving is lost. On a read-only root filesystem, the import fails with SQLITE_IOERR or "disk full". Consider pointing SQLite's temp directory at the database's directory (for example, set SQLITE_TMPDIR to filepath.Dir(DBPath) at startup) rather than leaving it to deployment docs.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think this one holds. SQLite's unix VFS tries SQLITE_TMPDIR, TMPDIR, then /var/tmp before /tmp (sqlite3-binding.c around line 46460 in mattn v1.14.52), and /var/tmp is normally on disk. It also reads those env vars once at OS init, so setting them inside createDB wouldn't be reliable. And before this PR, 24 of the 25 connections already used file-backed temp storage. A read-only root is a deployment setting (TMPDIR in the Dockerfile), so I'd rather handle that separately.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Filed the read-only root case as #1972.

Comment thread gtfsdb/driver_cgo.go Resolved
Comment thread gtfsdb/config.go
// depend on how a driver's SQLite was compiled. A larger cache didn't lower
// latency under the k6 load test, because the operating system already caches
// the database file.
const connectionCacheKiB = 2000

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The 2 MB cache also applies to the connection that runs the static import: the bulk inserts and the index and layover builds. Before this change, that connection had a 64 MB cache whenever it happened to get the configured one. The PR measures one feed (NJ Transit bus). A larger feed with large indexes, such as a combined-agency feed, could get much slower to import with 2 MB, and import time isn't covered by k6. Consider a larger cache on the import transaction's connection only (PRAGMA cache_size on the *sql.Conn or *sql.Tx used for the import), or measuring import time on the largest feed you support.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I measured this on King County Metro (1.03M stop_times, 190k shape points), two runs each. 2 MB: 9.30 s and 9.51 s, peak RSS 128-129 MiB. 64 MB: 10.32 s and 9.53 s, peak RSS 467-541 MiB. The bigger cache isn't faster and costs about 4x the memory, so I'm keeping 2 MB.

Comment thread gtfsdb/sqlite_performance_test.go Resolved
Comment thread gtfsdb/driver_pure.go Resolved
Comment thread gtfsdb/helpers.go Resolved
Comment thread gtfsdb/config.go Resolved
Comment thread gtfsdb/foreign_keys_test.go Resolved
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 342
Req/sec 11.3

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.6 ms
Error rate 0.00%
Total requests 343
Req/sec 11.3

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.9 ms
Error rate 0.00%
Total requests 335
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from e65003d to 86b3c7e Compare November 7, 2026 22:03
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.7 ms
Error rate 0.00%
Total requests 327
Req/sec 10.7

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from 86b3c7e to 3151173 Compare November 7, 2026 23:26
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.6 ms
Error rate 0.00%
Total requests 335
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from 3151173 to d22498d Compare November 8, 2026 02:46
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 328
Req/sec 10.8

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from d22498d to 65c1dab Compare November 8, 2026 02:49
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 336
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from 65c1dab to 65a0da9 Compare November 8, 2026 02:54
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.0 ms
Error rate 0.00%
Total requests 340
Req/sec 11.1

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from 65a0da9 to 1af0361 Compare November 8, 2026 05:27
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 335
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

Base automatically changed from ab/1657-stream-import to main November 8, 2026 05:41
configureSQLitePerformance set cache_size and temp_store with
db.ExecContext, which runs on one pooled connection. Both settings are
per connection, so that one connection had a 64 MB page cache and
in-memory temp tables while the other 24 used SQLite's defaults, and
which settings a query or an import got depended on the connection it
drew. After SetConnMaxLifetime closed it, no connection had them.

cache_size now rides the DSN, as foreign_keys already does, so every
connection gets it. It is 2 MB, the SQLite default that most
connections already used: under the k6 load test against Sound
Transit, a 2 MB, 8 MB and the old mixed setup all gave p95 latencies
of 7-8 ms with no failures, because the operating system caches the
database file. 25 connections can then hold at most about 50 MB of
page cache, where applying 64 MB to every connection could have held
1.6 GB.

temp_store=MEMORY is dropped, so every connection keeps SQLite's
default and the import's large sorts don't build temp tables in
memory. The pure-Go driver also gets the 5 s busy_timeout the CGo
driver sets by default. journal_mode=WAL stays a PRAGMA, since it is
stored in the database file.

The pragma test now checks several pooled connections of a file
database; it only passed before because an in-memory database has a
single connection.

Closes #1659
DSN and dsn were identical in driver_cgo.go and driver_pure.go, apart
from the parameter string, and both copied the "?"/"&" separator
logic. They now live once in config.go, and each driver file supplies
only connectionParams, its spelling of the connection settings. A new
connection setting then changes one line per driver.
connectionBusyTimeoutMs reached only the pure-Go DSN. The CGo driver
got 5000 ms only because mattn/go-sqlite3 defaults busyTimeout to
5000, so the busy_timeout check on CGo tested that default, not the
constant. If either changed, the two drivers would drift apart with no
test failing. The CGo DSN now passes _busy_timeout from the constant.

The DSN test builds the cache size from connectionCacheKiB too, rather
than hard-coding -2000, so changing the cache size doesn't break a test
about separators.
The connectionCacheKiB comment repeated the pool size of 25 from
configureConnectionPool and gave the resulting page cache bound in MB,
so a change to the pool limit would leave the comment wrong. The limit
is now the maxOpenConnections constant, which configureConnectionPool
uses and the comment names, and the comment no longer states a
derived MB figure.
The function now only sets journal_mode=WAL, since the
connection-scoped settings moved to the DSN, so its old name promised
more than it does. createDB's error message and comment say WAL mode
too.
TestSQLitePerformancePragmasApplied opened and held its connections
with its own Conn call and a defer in the loop, while the other two
pooled-connection tests use holdConnections. It now uses the helper
too. The client closes through t.Cleanup, so it closes after the held
connections rather than before them.
@burma-shave
burma-shave force-pushed the ab/1659-connection-pragmas branch from 1af0361 to d1f9795 Compare November 8, 2026 05:41
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 339
Req/sec 11.1

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@burma-shave
burma-shave merged commit ae9a2ca into main Nov 8, 2026
13 checks passed
@aaronbrethorst
aaronbrethorst deleted the ab/1659-connection-pragmas branch November 8, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-template-check Skip the PR template check (small fixes, or PRs opened before the check existed)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite cache_size and temp_store apply to only one pooled connection

2 participants