Skip to content

WIP: Maintain thread context in spawned threads for nested sideloads #496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

jkeen
Copy link
Collaborator

@jkeen jkeen commented May 15, 2025

When there are multi-level sideloads, the thread context is not maintained when sideloads spawn sideloads, i.e. when a sideload of "permissions.user" is requested. In practice this makes the context object available in resource methods (used to access controller methods) unavailable.

In my app this was causing intermittent 500 errors when it couldn't access the controller to grab the pundit context.
@MattFenelon Removing these lines fixes the issue, but I'm not sure if this will cause other unintended problems?

…ontext when the execution context changes loses context for nested sideloads that are spawned by other sideloads
@MattFenelon
Copy link
Contributor

I think that may cause memory leaks as any data added in a thread won’t be cleaned down at the end of the thread. I can’t think why removing those lines fixes the issue you’re seeing as the context should be set back up at the start of a new thread (execution context).

@jkeen
Copy link
Collaborator Author

jkeen commented May 16, 2025

@MattFenelon It only seems to happen when the includes are nested and it looked like when I was debugging that in my example of my request with a handful of sideloads plus permissions.user, the permissions sideload thread would execute, then another sideload would execute and then a user sideload would execute with the parent resource being permissions…but the thread context would be lost.

@MattFenelon
Copy link
Contributor

I wonder if it’s because the execution context hasn’t changed, causing the if statement body to not execute, which would copy the context to Thread.current again. In other words,

  1. Thread 1 runs
  2. Sets context
  3. Loads scope
  4. Clears context
  5. Thread 1 runs next scope
  6. Context hasn’t changed so doesn’t set it again
  7. Loads scope
  8. Fails because the code relying on the context causes an exception.

@jkeen
Copy link
Collaborator Author

jkeen commented May 17, 2025

Hmmm, this sounds like a possibility. Any idea how to handle that?

@MattFenelon
Copy link
Contributor

I think there'd need to be two changes. The first is to always set the thread variables at the start of the work and remove the if execution_context_changed conditional. That will fix the issue you're seeing of the disappearing context.

The second thing is to fix the clearing of the thread/fiber storage, before more work is scheduled on that thread, while removing execution_context_changed (as it doesn't work). I think what we want here is to compare what was in the thread when the promise was started and only remove (set to nil) those keys that we set at the start of the promise, not those that were already there. We should probably move that to an ensure as well.

What do you think?

@jkeen
Copy link
Collaborator Author

jkeen commented May 19, 2025

@MattFenelon Yeah that sounds like a good plan

MattFenelon added a commit to MattFenelon/graphiti that referenced this pull request May 20, 2025
Thread and fiber locals are being cleared on completion of every future.
If the promise ran on the same thread as the request, or the same thread
as the last promise, the locals are then not available. The execution
context hasn't changed in those circumstances so the code that sets the
locals at the start of a promise doesn't run.

1. Thread 1 runs
2. Sets context
3. Loads scope
4. Clears context
5. Thread 1 runs next scope
6. Context hasn’t changed so doesn’t set it again
7. Loads scope
8. Fails because the code relying on the context causes an exception
   because the locals aren't available.

Instead always set the thread and fiber locals and only clear those that
we know are new. This should avoid the lost locals and also avoid
memory leaks between promises.

See: graphiti-api#496
@MattFenelon
Copy link
Contributor

I've had a go at fixing it over in #497, @jkeen.

jkeen pushed a commit that referenced this pull request May 20, 2025
…497)

* Fix lost thread and fiber locals

Thread and fiber locals are being cleared on completion of every future.
If the promise ran on the same thread as the request, or the same thread
as the last promise, the locals are then not available. The execution
context hasn't changed in those circumstances so the code that sets the
locals at the start of a promise doesn't run.

1. Thread 1 runs
2. Sets context
3. Loads scope
4. Clears context
5. Thread 1 runs next scope
6. Context hasn’t changed so doesn’t set it again
7. Loads scope
8. Fails because the code relying on the context causes an exception
   because the locals aren't available.

Instead always set the thread and fiber locals and only clear those that
we know are new. This should avoid the lost locals and also avoid
memory leaks between promises.

See: #496

* Fix account for Fiber.current.storage not being available

* Fix account for Fiber.current.storage not being available 2/
@jkeen
Copy link
Collaborator Author

jkeen commented May 20, 2025

Closed in favor of #497

@jkeen jkeen closed this May 20, 2025
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