Skip to content

Tracking of sunpy context managers is not thread safe #8498

@dgarciabriseno

Description

@dgarciabriseno

Describe the bug

global _ignore_sun_motion
old_ignore_sun_motion = _ignore_sun_motion # nominally False
if not old_ignore_sun_motion:
log.debug("Ignoring the motion of the center of the Sun for transformations")
_ignore_sun_motion = True
yield

We set up an HTTP front end for coordinate transformations which uses sunpy for the transformations. We started randomly hitting this error:

File "/home/nonroot/.conda/envs/coordinator/lib/python3.13/site-packages/sunpy/coordinates/screens.py", line 42, in __exit__
    raise RuntimeError(f"Cannot remove {self._context_name} from tracking stack because {removed} is last active.")
RuntimeError: Cannot remove sunpy.coordinates.screens.SphericalScreen from tracking stack because sunpy.coordinates._transformations.transform_with_sun_center is last active.

After investigating it seems that since each request is running on a thread, the state of the context manager ends up shared between threads since it's tracked at the module level, and so entering/exiting the context managers out of order causes this error.

To Reproduce

This script demonstrates the issue by starting 2 threads that use context managers but exit at different times.

"""
Reproduction script for SunPy coordinate tracking stack error using threading.

This script demonstrates the RuntimeError that occurs when multiple threads
interleave their use of SunPy coordinate context managers, causing the
tracking stack to become corrupted.
"""

import threading
import time
import astropy.units as u
from astropy.coordinates import SkyCoord
from sunpy.coordinates import HeliographicStonyhurst
from sunpy.coordinates.screens import SphericalScreen
from sunpy.coordinates import transform_with_sun_center


def run_spherical_screen():
    """
    Thread 1: Enter SphericalScreen, delay, then exit.
    """
    print("[Thread 1] Starting...")

    # Create a coordinate
    coord = SkyCoord(
        0 * u.deg,
        0 * u.deg,
        1 * u.AU,
        frame=HeliographicStonyhurst,
        obstime='2023-01-01'
    )

    print("[Thread 1] Entering SphericalScreen")
    with SphericalScreen(coord):
        print("[Thread 1] Inside SphericalScreen")
        time.sleep(0.5)  # Delay to allow thread 2 to enter transform_with_sun_center
        print("[Thread 1] About to exit SphericalScreen")
    print("[Thread 1] Exited SphericalScreen")


def run_transform_with_sun_center():
    """
    Thread 2: Wait a bit, enter transform_with_sun_center, delay, then exit.
    This will interleave with thread 1's operation.
    """
    print("[Thread 2] Starting...")
    time.sleep(0.2)  # Wait for thread 1 to enter SphericalScreen first

    print("[Thread 2] Entering transform_with_sun_center")
    with transform_with_sun_center():
        print("[Thread 2] Inside transform_with_sun_center")
        time.sleep(0.5)  # Stay inside while thread 1 tries to exit
        print("[Thread 2] About to exit transform_with_sun_center")
    print("[Thread 2] Exited transform_with_sun_center")


def reproduce_error():
    """
    Run two threads that will interleave their context manager usage,
    causing the tracking stack error.
    """
    print("Starting threads to reproduce tracking stack error...\n")

    t1 = threading.Thread(target=run_spherical_screen, name="SphericalScreen")
    t2 = threading.Thread(target=run_transform_with_sun_center, name="TransformWithSunCenter")

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print("\nThreads completed")


if __name__ == "__main__":
    print("Attempting to reproduce SunPy coordinate tracking stack error...")
    print("This uses threading to create a race condition in the context manager stack.\n")
    print("=" * 70)
    print()

    try:
        reproduce_error()
    except RuntimeError as e:
        print()
        print("=" * 70)
        print("\nSuccessfully reproduced the error!")
        print(f"Error message: {e}")

Screenshots

No response

System Details

==============================
sunpy Installation Information
==============================

General
#######
OS: Mac OS 26.2
Arch: 64bit, (arm)
sunpy: 7.1.0
Installation path: /Users/dgarciab/PostMabel/fastapi-test/venv/lib/python3.14/site-packages/sunpy-7.1.0.dist-info

Required Dependencies
#####################
astropy: 7.2.0
fsspec: 2026.1.0
numpy: 2.4.2
packaging: 26.0
parfive: 2.3.1
pyerfa: 2.0.1.5
requests: 2.32.5

Optional Dependencies
#####################
asdf-astropy: Missing asdf-astropy>=0.5.0; extra == "asdf" or "asdf" or "asdf" or "asdf" or "asdf"
beautifulsoup4: Missing beautifulsoup4>=4.13.1; extra == "net" or "net" or "net" or "net" or "net"
cdflib: Missing cdflib>=1.3.2; extra == "timeseries" or "timeseries" or "timeseries" or "timeseries" or "timeseries"
contourpy: Missing contourpy>=1.1.0; extra == "map" or "map" or "map" or "map" or "map"
drms: Missing drms>=0.7.1; extra == "net" or "net" or "net" or "net" or "net"
glymur: Missing glymur>=0.13.0; extra == "jpeg2000" or "jpeg2000" or "jpeg2000" or "jpeg2000"
h5netcdf: Missing h5netcdf>=1.2.0; extra == "timeseries" or "timeseries" or "timeseries" or "timeseries" or "timeseries"
h5py: Missing h5py>=3.10.0; extra == "timeseries" or "timeseries" or "timeseries" or "timeseries" or "timeseries"
lxml: Missing lxml>=5.0.1; extra == "jpeg2000" or "jpeg2000" or "jpeg2000" or "jpeg2000"
matplotlib: Missing matplotlib>=3.8.0; extra == "map" or "timeseries" or "visualization" or "timeseries" or "timeseries" or "timeseries" or "timeseries"
mpl-animators: Missing mpl-animators>=1.2.0; extra == "map" or "visualization" or "map" or "map" or "map" or "map"
opencv-python: Missing opencv-python>=4.8.0.74; extra == "opencv" or "opencv" or "opencv" or "opencv"
pandas: Missing pandas>=2.2.0; extra == "timeseries" or "timeseries" or "timeseries" or "timeseries" or "timeseries"
python-dateutil: Missing python-dateutil>=2.9.0; extra == "net" or "net" or "net" or "net" or "net"
reproject: Missing reproject>=0.13.0; extra == "map" or "map" or "map" or "map" or "map"
scipy: Missing scipy>=1.12.0; extra == "image" or "map" or "image" or "image" or "image" or "image"
spiceypy: Missing spiceypy>=6.0.0; extra == "spice" or "spice" or "spice" or "spice"
tqdm: 4.67.2
zeep: Missing zeep>=4.3.0; extra == "net" or "net" or "net" or "net" or "net"

Installation method

pip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions