Skip to content

fix: release the GIL while covering a polygon - #34

Merged
mailletf merged 1 commit into
fix/hop-count-guardfrom
fix/release-gil-polygon
Sep 16, 2026
Merged

mailletf merged 1 commit into
fix/hop-count-guardfrom
fix/release-gil-polygon

Conversation

@mailletf

@mailletf mailletf commented Aug 25, 2026 •

Copy link
Copy Markdown
Member

Problem

polygon_to_geohashes took _py: Python and never used it, so the entire cover walk ran with the GIL held. The walk touches no Python objects once __geo_interface__ has been read, and at fine precisions it runs for a long time — verdun at precision 10 takes minutes — blocking every other thread in the interpreter for the duration.

Every other heavy function in this module already detaches.

Fix

Read the geometry under the GIL as before, then py.detach for the walk.

Test

Two concurrent calls against one. Without the fix the pair reports 2.06× — fully serialised.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the polygon_to_geohashes PyO3 binding to release the Python GIL during the (potentially long-running) polygon cover walk, and adds a regression test intended to detect accidental GIL retention by checking that two concurrent calls overlap rather than serialize.

Changes:

  • Release the GIL for the polygons_to_geohashes cover walk by running it inside py.detach(...) after reading __geo_interface__.
  • Add a new pytest that times one call vs two concurrent calls to validate that the cover walk does not hold the GIL.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/lib.rs Detaches from the GIL during the cover walk to avoid blocking other Python threads.
tests/test_geohasher.py Adds a timing-based concurrency test intended to verify GIL release behavior.
Suppressed comments (1)

tests/test_geohasher.py:241

  • Exceptions raised inside the worker threads won't currently fail the test (they're swallowed by threading.Thread). Also, starting threads one-by-one can skew the concurrency timing. Consider synchronizing the start and capturing any thread exceptions explicitly.
    def work():
        geohash_polygon.polygon_to_geohashes(polygon_verdun, precision, False)

    threads = [threading.Thread(target=work) for _ in range(2)]
    start = time.perf_counter()

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_geohasher.py
Comment on lines +225 to +228
import threading
import time

precision = 9

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.

Fixed in the amended 5ca65fb, along with two more problems in the same test:

  • Added @pytest.mark.skipif((os.cpu_count() or 1) < 2) — you are right that two CPU-bound threads on one core take ~2x whatever the GIL does.
  • The workers now start behind a threading.Barrier. They were started one at a time, so the first had a head start and the pair looked more overlapped than it was.
  • Worker exceptions are collected and asserted on. threading.Thread swallowed them, so a call that raised inside a worker would have passed on its (fast) timing.

The barrier tightened the measurement enough to expose a thin margin, so each timing now repeats the walk 10 times. Measured on this fixture: held GIL 1.97-2.21x, released 1.16-1.26x, against the 1.7x threshold. Verified the held-GIL numbers by building with the py.detach removed.

Comment thread tests/test_geohasher.py
Comment on lines +228 to +235
precision = 9

# Warm up so neither timing pays one-off costs.
geohash_polygon.polygon_to_geohashes(polygon_verdun, precision, False)

start = time.perf_counter()
geohash_polygon.polygon_to_geohashes(polygon_verdun, precision, False)
single = time.perf_counter() - start

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.

Precision drops to 8 later in the stack (#40), for a different reason — above PARALLEL_COVER_MIN_CELLS one call already saturates every core and the test cannot tell a held GIL from a released one. The runtime concern lands in the same place. Full pytest run on this branch is ~10s.

@mailletf
mailletf force-pushed the fix/hop-count-guard branch from 33d98fc to ece4a59 Compare August 25, 2026 14:45
@mailletf
mailletf force-pushed the fix/release-gil-polygon branch from 86f9cb8 to 5ca65fb Compare August 25, 2026 14:45
@mailletf
mailletf force-pushed the fix/hop-count-guard branch from ece4a59 to 2256bc4 Compare September 1, 2026 18:19
@mailletf
mailletf force-pushed the fix/release-gil-polygon branch 2 times, most recently from 61106b2 to 6d1578c Compare September 1, 2026 18:33
@mailletf
mailletf force-pushed the fix/hop-count-guard branch from 2256bc4 to 129071d Compare September 1, 2026 18:33
polygon_to_geohashes took `_py: Python` and never used it, so the entire
cover walk ran with the GIL held. The walk touches no Python objects once
__geo_interface__ has been read, and at fine precisions it runs for a long
time — verdun at precision 10 takes minutes — blocking every other thread in
the interpreter for the duration. Every other heavy function in this module
already detaches.

Read the geometry under the GIL as before, then detach for the walk.

The test runs two concurrent calls and compares against one. Without the fix
it reports 2.06x, i.e. fully serialised.

Squashed with:
- test: make the GIL test robust off a multi-core runner

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mailletf
mailletf force-pushed the fix/release-gil-polygon branch from 6d1578c to 32ab316 Compare September 1, 2026 18:54
@mailletf
mailletf force-pushed the fix/hop-count-guard branch from 129071d to 0e25cfe Compare September 1, 2026 18:54
@mailletf
mailletf merged commit 19f4c4b into fix/hop-count-guard Sep 16, 2026
32 checks passed
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.

2 participants