inertiajs / inertia-rails

The Rails adapter for Inertia.js.

Home Page:https://inertiajs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

inertia_share doesn't always seem to run

snaptopixel opened this issue · comments

We're using Inertia for a new web app and loving things so far. We do currently have an issue where the inertia_share doesn't seem to run consistently on initial page load?

The rendered props are there and correct in the data-page attribute but the shared props are nowhere to be seen. A reload typically fixes this but it's pretty bad in our instance since we share a layout prop which renders an application shell

@ledermann this seems related to your PR #17, any advice?

Yes, maybe it's related to #17. I suggest you test your app with my fork of inertia-rails by placing this into your Gemfile:

gem 'inertia_rails', github: 'ledermann/inertia-rails', branch: 'patched'

If this fixes your issue, we should discuss merging the PR again.

Thanks @ledermann pushing a production build with your fork now, will let you know how it goes!

@snaptopixel can you share any of your code, or a non-proprietary summary/explanation? trying @ledermann s fork is a good idea, but even if it works i'd like to have code or a test case so that we can understand why things aren't working for you

I'm not sure what would help most @bknoles, we're doing pretty bog standard Inertia stuff (from the docs) in Rails. We are using our own frontend (Web components that wrap the Inertia client) but those seem to be working as they should.

class ApplicationController < BaseController
  before_action :authenticate_user!
  inertia_share layout: lambda {
                  {
                    component: 'app-layout',
                    props: {
                      logoutUrl: destroy_user_session_path,
                      flash: flash.map { |instance| { type: instance[0], text: instance[1] } },
                    }
                  }
                }
   inertia_share env: lambda {
                  { RAILS_ENV: Rails.env }
                }

  inertia_share currentUser: -> { current_user }
end

Then our controllers are simple (thanks Inertia!)

class HomeController < ApplicationController
  def index
    render inertia: 'page-home', props: {
      googleSigninUrl: user_google_oauth2_omniauth_authorize_path,
      

Everything works, but every now and then the shared_props aren't merged in, it seems random. This only happens in production with model caching and we haven't seen it in dev

@snaptopixel out of curiosity, what's your deployment setup? we use docker-compose on a single VPS to run our production Inertia app without any issues. Earlier this year, we attempted to migrate it over to AWS on ECS with Fargate and were seeing the same issue you are describing... we could never figure out why and ended up abandoning the migration for other reasons

@hoffoo care to elaborate on the question above? This issue/thread is in regards to the undefined issue we've been having...

Also, @bknoles the issue does seem to be resolved with @ledermann's fork, we are not having heavy usage right now so I'll give it a few more days to be sure.

@hoffoo for clarification, i would just like to know what you use to deploy / host the app that is running Inertia

commented

@bknoles the app is ran in docker. in your VPC docker environment do you use session affinity (i don't see how that would be relevant here but its my only guess). It might explain why we aren't able to reproduce it locally where we only use a single instance of the app.

what do you use to manage the Docker containers?

We use google cloud build @bknoles not sure if that's what you're looking for.

Also, fwiw, after a few days of decent usage we haven't seen the issue while using @ledermann's fork, everything has been running v nice

@snaptopixel: Great to hear that my fork works. Currently I have not the time to check this in detail, but: Maybe the current implementation of inertia_share is not thread-safe? And the re-implementation in my fork is thread-safe?

that's kinda what it sounds like @ledermann, but i want to make sure it's not something weird related to Docker.

we saw the issue running Docker via AWS's ECS/Fargate orchestration.. hoping to dig in a bit more this week and determine what's going on

a quick google search informed me that mattr_accessor (used to store shared Inertia data) is definitely not thread safe.

the Rails team was kind enough to create a threadsafe helper method years and years ago. thread_mattr_accessor.

#38 uses that method... the included test fails if you switch the method definition back to mattr_accessor.

According to rails/rails#29233, class_attribute, which is used in the @ledermann branch, should be threadsafe. However, even if class_attribute is not threadsafe, it would pass the equivalent test in #38 because shared inertia data is scoped to controllers in that branch. The "class level" data store design is just more robust to multithreaded usage since

  • controllers cannot "collide" by sharing the same store
  • shared plain data defined at the class level will be cached and should not change between requests
  • shared blocks evaluate at render time, and not when the class is cached

Thinking more about the code, if class_attribute is not threadsafe, we'd only see a bug in this kind of situation:

class SomeController < ApplicationController
  inertia_share name: 'Tom'

  def first_route
    # first_route takes a long time to complete
    sleep 10
    render inertia: 'SomeComponent'
  end

  def second_route
    # Atypical usage, but I think this should technically work?
    self.class.inertia_share(name: 'Not Tom')
    render inertia: 'SomeOtherComponent'
  end
end

# Handled in thread 1
first_response = get "/first_route"
# Handled in thread 2
get "/second_route"

# if class_attribute is not threadsafe, then the second request would modify
# the class variable such that the first response has the incorrect value for :name
first_response.body => { name: 'Not Tom' }

@ledermann would you be able to create test on your branch to see if class_attribute is indeed threadsafe? I believe the fix in #38 will resolve this specific issue, but I'd still like to keep your PR open. I think we may eventually see something else that definitively pushes us in one direction or the other for a module level data store vs. controller level data stores. Also, your comments on the new PR are welcome.

@BrandonShar can you take a look at #38 as well?

@bknoles: In addition to my comment in #38:

Your example (the "atypical use" calling self.class.inertia_share in a controller method) should never be made, it's like calling MyModel.has_many in a controller method. So, currently I see no need to add thread safety tests to #17.

I've played around with this in #17: If a second request changes the global shared data within a controller action, the first request will be affected too, which is correct from my point of view. IMHO, the concept of "shared data" should be a global thing. It's nothing to be changed on every request - because for this we have props.

Thanks @ledermann, I would have to disagree about "It's nothing to be changed on every request" …in our case we use inertia_share for flash messages and other things that occur in the application's "layout" or "shell" like modals, etc. These potentially change on each/every request.

@snaptopixel can you share a code snippet? do you mean something like:

inertia_share do
  {
    flash: flash
  }
end

or are you doing something different?

@snaptopixel To clarify my statement "It's nothing to be changed on every request":

The definition of shared data should only be processed once. Because a definition can be a block, of course it allows rendering different content on every request.

Example:

inertia_share flash: -> {
  {
    notice: flash.notice,
    alert: flash.alert
  }
}

Placing this into the ApplicationController is the definition. This shared data example is a block, so it's not static. The block is executed for every request and merges the current flash messages into the response. Of course this is fine.

The current implementation resets the definition on every request. IMHO this is not the right way so I changed this in #17.

@bknoles we do things a bit differently, I've built a custom frontend adapter using web-components and I've added feature for using a global "layout" component/configuration like this:

inertia_share layout: -> {
  {
    component: 'app-layout',
    props: {
      logoutUrl: destroy_user_session_path,
      flash: flash.map { |instance| { type: instance[0], text: instance[1] } },
      showHeader: RequestStore.store[:hide_header].blank?
    }
  }
}

Ideally we could do things like modals with a similar approach to flash. Where the individual controllers can trigger them without worrying about the inertia_share that's happening behind the scenes. We're using RequestStore currently which works but isn't very elegant. We use that in a controller like so:

RequestStore.store[:hide_header] = true

@snaptopixel gotcha... yea, there's no disagreement between what @ledermann was saying and how you guys are using Inertia share. he was just referring to the internal way that the gem stores the data. in both versions (current and the version in #17), lambdas and blocks are evaluated at runtime, resulting in dynamic final props.

i only asked to make sure that you aren't using the gem in a way we hadn't envisioned. if you were, we might want to look at the design and make sure the use case was captured. in the end, your example fits within the current design so we are all good here!

would you mind testing the new version of the gem for us?

gem 'inertia_rails', github: 'inertiajs/inertia-rails', branch: 'make-inertia-share-threadsafe'

i will cut a new release once i get a final PR review from brandon

@snaptopixel we just released a new version with this fix in it... 1.4.0, or pull the latest from Rubygems.

please report here if it solves the issue for you! would be nice to have confirmation. thanks for reporting this!

Thanks again @bknoles sorry for the delay. I'll post back here once we test it to confirm, but I'm sure it's good.