Skip to content

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.

3 participants