Skip to content

Fix Rails 8 deadlock in KML feature import#51

Merged
njakobsen merged 2 commits intomasterfrom
fix-rails-8-kml-thread-deadlock
May 5, 2026
Merged

Fix Rails 8 deadlock in KML feature import#51
njakobsen merged 2 commits intomasterfrom
fix-rails-8-kml-thread-deadlock

Conversation

@njakobsen
Copy link
Copy Markdown
Member

Summary

Importers::KML#geom_from_kml ran each ST_GeomFromKML SQL in a freshly-spawned thread, manually checking out a separate connection from ActiveRecord::Base.connection_pool for fault isolation: a malformed KML feature would raise StatementInvalid on the worker connection, which Postgres aborts; on the test/main connection that abort would poison the surrounding transaction. The thread trick worked on Rails 7.x.

Rails 7.1 introduced per-test connection pinning under use_transactional_fixtures = true, and Rails 8.1 made the pin stricter. The spawned worker thread now blocks indefinitely at ConnectionPool#checkout, and the main thread blocks on Thread#join. Every KMZ-import controller spec hangs forever.

Replace the thread+separate-connection pattern with a transaction(requires_new: true) SAVEPOINT on the existing connection. A StatementInvalid from a malformed feature rolls back only the savepoint; the surrounding transaction is untouched. Same fault containment, no thread crossing.

The savepoint approach is backwards-compatible to every Rails version this gem supports — transaction(requires_new: true) has issued SAVEPOINTs since Rails 3.0.

Bumped patch version to 3.10.1.

Test plan

  • KMZ-import controller specs in a host running Rails 8.1 + use_transactional_fixtures finish in <1s instead of hanging forever
  • Manually upload a KMZ with malformed geometry — outer transaction stays alive, malformed feature is silently skipped (same behaviour as before the fix)
  • Existing spatial_features test suite passes

🤖 Generated with Claude Code

njakobsen and others added 2 commits May 4, 2026 23:31
… Rails 8 deadlock

`Importers::KML#geom_from_kml` ran each `ST_GeomFromKML` SQL in a freshly-spawned thread, manually checking out a separate connection from `ActiveRecord::Base.connection_pool`. The intent was fault isolation: if a single feature's KML was malformed and Postgres raised, the failure shouldn't poison the surrounding transaction. On Rails 7.x this worked because the test's transaction wasn't connection-pinned.

Rails 7.1+ introduced per-test connection pinning under `use_transactional_fixtures = true`, and Rails 8.1 made the pin stricter. Now the spawned worker thread blocks indefinitely at `ConnectionPool#checkout` (it can't get a connection because the test thread has effectively claimed pool access), and the main thread blocks on `Thread#join`. Every KMZ-import controller spec hangs for as long as the test runner allows.

Instead of crossing thread boundaries for fault containment, run the query inside a `transaction(requires_new: true)` SAVEPOINT on the existing connection. A `StatementInvalid` from a malformed feature rolls back only the savepoint; the surrounding transaction is unaffected. No second connection, no `Thread.new`, and no `.join` to deadlock on. KMZ specs now run in well under a second instead of timing out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@njakobsen njakobsen merged commit 988af10 into master May 5, 2026
1 of 2 checks passed
@njakobsen njakobsen deleted the fix-rails-8-kml-thread-deadlock branch May 5, 2026 06:44
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