Skip to content

BUG: ensure thread safety in set_temp_(cache|config) context managers - #19559

Merged
neutrinoceros merged 2 commits into
astropy:mainfrom
neutrinoceros:bug/config-paths-races
Apr 24, 2026
Merged

neutrinoceros merged 2 commits into
astropy:mainfrom
neutrinoceros:bug/config-paths-races

Conversation

@neutrinoceros

@neutrinoceros neutrinoceros commented Apr 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

While working on #18964, I realized that these context managers where obviously not thread safe as they relying on mutations to global state.
Because they are public API, I don't think it's possible to put the genie back in the bottle now and so we must compose with this global+mutable state, and the only thing to do is hence to add locking to it.
Furthermore, because we have to allow any number of threads to call any combination of these context managers in any order, they need to share a single lock (otherwise deadlocks would be possible). In particular, the possibility of nesting more than one instance of each context imposes the use of a re-entrant lock (RLock).
I've added multiple tests that all fail seemingly 100% of the time on my machine so I'm confident they capture the bugs and don't think we need special jobs to run them as stress tests.

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@neutrinoceros neutrinoceros added the backport-v7.2.x on-merge: backport to v7.2.x label Apr 10, 2026
@neutrinoceros neutrinoceros changed the title BUG: ensure thread safety in \set_temp_(cache|config)\ context managers BUG: ensure thread safety in set_temp_(cache|config) context managers Apr 10, 2026
@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch 3 times, most recently from ebe3d88 to 47a2295 Compare April 10, 2026 18:01
@neutrinoceros neutrinoceros added the Extra CI Run cron CI as part of PR label Apr 10, 2026
@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch from 47a2295 to a4b4f53 Compare April 10, 2026 18:07
@neutrinoceros
neutrinoceros marked this pull request as ready for review April 10, 2026 19:21
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Interesting. It looks like I deadlocked the double-test job. Let's see if I can fix that quickly.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

indeed I can easily reproduce the deadlock locally. It's hit when my new tests are run for the second time. Interestingly, running just these tests multiple times in a single session doesn't seem to be problematic, so what seems most likely is that some other test is polluting the global state somehow. I don't have a clear idea what to look for just yet but that's what I'll need to figure out next.

@neutrinoceros
neutrinoceros marked this pull request as draft April 10, 2026 21:18
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

another note while I'm thinking about this problem: locking the classes isn't sufficient to provide complete safety; if two threads set a temporary cache to the same location concurrently, the second thread will inherit the first's cache. This is generally to be avoided as it creates a clear vector of attack, though I need to ponder a bit more about how to address it. I'm considering the only thing to do might be to document this as an unsafe pattern, and promote the use of owned uuids to set temporary directories.

@neutrinoceros neutrinoceros removed the Extra CI Run cron CI as part of PR label Apr 11, 2026
@neutrinoceros

neutrinoceros commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor Author
uv run --extra test pytest --keep-duplicates astropy/config/tests/test_concurrency.py astropy/config/tests/test_concurrency.py

completes nicely

so does

uv run --extra test pytest --keep-duplicates astropy/config/tests/test_concurrency.py astropy/config/tests

but

uv run --extra test pytest --keep-duplicates astropy/config/tests astropy/config/tests

halts (deadlock)

and so does

uv run --extra test pytest --keep-duplicates astropy/config/tests astropy/config/tests/test_concurrency.py

@neutrinoceros

neutrinoceros commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor Author

In fact, I can deadlock with just

uv run --extra test pytest astropy/config/tests/test_configs.py astropy/config/tests/test_concurrency.py

which means these test modules just happen to pass normal jobs because their names are alphabetical sorted in the only order that doesn't cause a deadlock.
The good thing is that I wrote all these tests, so I'm uniquely well positioned to sort it out (I hope).

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

test_set_temp_cache_resets_on_exception is the one that's polluting the global state.

@neutrinoceros

Copy link
Copy Markdown
Contributor Author

Problem found: I forgot to release the lock when an exception was raised. I expect clean CI this time.

@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch 2 times, most recently from b9b92b6 to 8184cc2 Compare April 11, 2026 16:09
@neutrinoceros neutrinoceros added the Extra CI Run cron CI as part of PR label Apr 11, 2026
@neutrinoceros
neutrinoceros marked this pull request as ready for review April 11, 2026 16:31
@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch 4 times, most recently from 9a7d2a8 to 9d68832 Compare April 12, 2026 11:05

@astrofrog astrofrog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is looking good though I have a few comments below, after which we can merge

Comment thread astropy/config/paths.py Outdated
@@ -134,11 +140,17 @@ def __init__(
self._prev_path = self.__class__._temp_path

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This line should probably be in __enter__ after the acquire, otherwise if a thread reaches here while another has already executed __enter__, self._prev_path will get overridden and set to self._path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good point

Comment thread astropy/config/paths.py Outdated
_cfgobjs.clear()
return super().__enter__()
except Exception:
_PATHS_MUTEX.release()

@astrofrog astrofrog Apr 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably restore _cfgobjs at this point, otherwise if an exception happens during super().__enter__() _cfgobjs will be left empty?

Comment thread docs/changes/config/19559.bugfix.rst Outdated
@@ -0,0 +1,2 @@
Ensure thread safety in ``set_temp_cache`` and ``set_temp_config``
context managers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe worth mentioning that concurrency is now explicitly disabled inside these contexts?

Comment thread astropy/config/paths.py
self.__class__._temp_path = self._path
_PATHS_MUTEX.acquire()
try:
self.__class__._temp_path = self._path

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible for this to raise an exception? Can we at least combine this try...except with the next one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's indeed very unlikely to raise. The only failure mode I see is if self._path isn't defined, which is something linters and type-checkers should already be warning about. Maybe I over-corrected after my first attempt didn't have enough release hooks, but I agree this one can be dropped.

Comment thread astropy/config/paths.py Outdated
# in any order. Hence they need to share a single lock (otherwise deadlocks would be possible).
# In particular, the possibility of nesting more than one instance of each context imposes
# the use of a re-entrant lock (RLock).
_PATHS_MUTEX = RLock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about naming it _PATHS_LOCK?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure. I just like "mutex" a bit more, just because it sounds slightly more ominous to me, but really, not good enough of a reason to break the convention

@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch from 684a6de to 0ee7b1a Compare April 24, 2026 07:13
@neutrinoceros

Copy link
Copy Markdown
Contributor Author

thanks a lot @astrofrog, I baked all your suggestions into a rebased commit !

@astrofrog astrofrog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good, thanks! One small remaining minor potential issue below, but I'll leave it up to you as to whether to ignore.

Comment thread astropy/config/paths.py
_cfgobjs.clear()
return super().__enter__()
except Exception:
_cfgobjs.update(self._cfgobjs_copy)

@astrofrog astrofrog Apr 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unlikely to happen in practice but if _cfgobjs.copy() was to ever raise an exception, self._cfgobjs_copy would be unassigned. One way around this would be to set self._cfgobjs_copy = None and checking that it isn't None here. This is just minor as unlikely, so I'll leave it up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good point. I'm going to force push again, but for ease of review, the concrete patch I'm pushing is

diff --git a/astropy/config/paths.py b/astropy/config/paths.py
index 06054e1e6d..1e313fb3c5 100644
--- a/astropy/config/paths.py
+++ b/astropy/config/paths.py
@@ -311,7 +311,14 @@ class set_temp_config(_SetTempPath):
         _PATHS_LOCK.acquire()

         try:
-            self._cfgobjs_copy = _cfgobjs.copy()
+            _cfgobjs_copy = _cfgobjs.copy()
+        except Exception:
+            _PATHS_LOCK.release()
+            raise
+
+        self._cfgobjs_copy = _cfgobjs_copy
+
+        try:
             _cfgobjs.clear()
             return super().__enter__()
         except Exception:

@neutrinoceros
neutrinoceros force-pushed the bug/config-paths-races branch from 0ee7b1a to ff26994 Compare April 24, 2026 08:36
@neutrinoceros
neutrinoceros enabled auto-merge April 24, 2026 08:37
@neutrinoceros
neutrinoceros merged commit d690055 into astropy:main Apr 24, 2026
65 of 68 checks passed
neutrinoceros added a commit that referenced this pull request Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-v7.2.x on-merge: backport to v7.2.x Bug config Extra CI Run cron CI as part of PR multi-threading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants