-
-
Notifications
You must be signed in to change notification settings - Fork 632
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
Connection pool and context switching #774
Comments
@udovenko Hydration of the redux store is per request, and is not long lived. |
@justin808 Sorry but it seems you did not understand my question. I'm talking about pool behaviour within the single request. Ruby can switch context at any moment. And I can't see any code that prevents js_context form checking in back to the pool during single request. So another parallel request in multithreading environment can chek the same js_context out and change it before request one is finshed. |
It is pretty sad that issue was closed before I even had a chance to explain what I mean... |
@udovenko Very easy to reopen the issue. We use the same connection pool algorithm of react-rails. Thread will get control and return the object when done. If there was a real issue, somebody would probably have seen it by now. Do you agree? In other words, if we didn't use a connection pool, we would have issues. |
@justin808 Thank you for response! I did not use react-rails and I'm not sure it this gem offers an ability to hydrate store in a separate |
@udovenko can you try to see if you can reproduce the issue? Maybe put in sleeps? Print statements? |
@justin808 Ok, I will try to do it this week. I'll start from pure ConnectionPool with dummy objects since I do not see any special logic added by ReactOnRails gem around it. |
@justin808 Ok, here is a quick and dirty test for ConnectionPool behaviour:
An output from this snippet is:
Imagine these simple threads are our parallel requests. First request takes one second to handle logic between helper method calls while second request works without delay. The last print in output shows that React component in the first request will be rendered in wrong context, changed by concurent second request. I have no idea on how to test it with real Rails app yet, but I do not see any reason for it to behave differently. |
@udovenko what do you recommend as the fix? |
@justin808 First of all - create falling tests for this particular case within Rails stack. I have no experience wih testing concurent requests with RSpec, but I think controller specs is the first place to try. Then, if issue is confirmed by falling tests, all helper methods should be moved inside the same
|
@udovenko I think we should do something so that if a thread picks up a transaction that's in process, due to a call to redux_store before calling redux_component, then the thread should return the connection and keep trying to get a new connection until it gets one that is not in use. The tricky part is going to be that we'll some sort of rack middleware or controller after hook to clear out this static flag, and if the controller does hit a ruby error, etc., then we need to be sure that we still clear out the flag. Thoughts? |
@justin808 If I understand you correctly, the procedure you've described is already implemented by ConnectionPool by |
@udovenko But don't parallel requests get different connection_pool instances (not threads, the entire pool is different) anyway, so there's no way one request could corrupt the connection pool of a different request? |
@robwise As I can see pool object is a class variable: https://github.com/shakacode/react_on_rails/blob/master/lib/react_on_rails/server_rendering_pool/exec.rb So it should be shared across the threads. |
@robwise ReactOnRails team recommended Puma as a server in the boilerplate project. This is a multithreading server. It loads application in a parent process and launches new thread for each new request. Please correct me if i'm wrong. Parent process (Puma worker) stores single class object of |
As I can remember I've even experienced an issue when store state from previous request was affecting next request. At least I thought that was the reason for my bug that time... |
It is easy to check by runing app on Puma with |
@udovenko No I think you're right, I thought we were creating a new instance of connection pool per request instead of keeping it in a class var like that. I'm trying to back and read through the source to figure it out. |
But I'm still getting lost as to how a new request can affect the store data of a different request? Isn't that held in an instance variable in |
I mean store data that is in |
When server rendering, we put each Then each time you call In other words, we re-initialize the store data each time for each component. |
@udovenko You're 100% right on the issue and I considered this in the original design a long time ago: The redux store is set at the beginning of each component rendering. The only the that we should consider is clearing the memory of the redux stores. # Returns Array [0]: html, [1]: script to console log
# NOTE, these are NOT html_safe!
def server_rendered_react_component_html(
props, react_component_name, dom_id,
prerender: required("prerender"),
trace: required("trace"),
raise_on_prerender_error: required("raise_on_prerender_error")
)
return { "html" => "", "consoleReplayScript" => "" } unless prerender
# On server `location` option is added (`location = request.fullpath`)
# React Router needs this to match the current route
# Make sure that we use up-to-date bundle file used for server rendering, which is defined
# by config file value for config.server_bundle_js_file
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
# Since this code is not inserted on a web page, we don't need to escape props
#
# However, as JSON (returned from `props_string(props)`) isn't JavaScript,
# but we want treat it as such, we need to compensate for the difference.
#
# \u2028 and \u2029 are valid characters in strings in JSON, but are treated
# as newline separators in JavaScript. As no newlines are allowed in
# strings in JavaScript, this causes an exception.
#
# We fix this by replacing these unicode characters with their escaped versions.
# This should be safe, as the only place they can appear is in strings anyway.
#
# Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
wrapper_js = <<-JS
(function() {
var railsContext = #{rails_context(server_side: true).to_json};
#{initialize_redux_stores}
var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
return ReactOnRails.serverRenderReactComponent({
name: '#{react_component_name}',
domNodeId: '#{dom_id}',
props: props,
trace: #{trace},
railsContext: railsContext
});
})()
JS
result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
if result["hasErrors"] && raise_on_prerender_error
# We caught this exception on our backtrace handler
# rubocop:disable Style/RaiseArgs
raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
# Sanitize as this might be browser logged
props: sanitized_props_string(props),
err: nil,
js_code: wrapper_js,
console_messages: result["consoleReplayScript"])
# rubocop:enable Style/RaiseArgs
end
result
rescue ExecJS::ProgramError => err
# This error came from execJs
# rubocop:disable Style/RaiseArgs
raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
# Sanitize as this might be browser logged
props: sanitized_props_string(props),
err: err,
js_code: wrapper_js)
# rubocop:enable Style/RaiseArgs
end
def initialize_redux_stores
return "" unless @registered_stores.present? || @registered_stores_defer_render.present?
declarations = "var reduxProps, store, storeGenerator;\n"
all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
result = all_stores.each_with_object(declarations) do |redux_store_data, memo|
store_name = redux_store_data[:store_name]
props = props_string(redux_store_data[:props])
memo << <<-JS
reduxProps = #{props};
storeGenerator = ReactOnRails.getStoreGenerator('#{store_name}');
store = storeGenerator(reduxProps, railsContext);
ReactOnRails.setStore('#{store_name}', store);
JS
end
result
end |
@udovenko given my last response, I'm going to close this issue with your permission. However, I'll open a new issue and suggest that we prepend a call to clear all stores here, just to be sure we don't have any stores from other requests sitting around. |
@robwise Now I see what I was missing. Store rehydrates on each |
Yes, this makes the issue resolved... Sorry if I was wasting your time with my doubts. |
Will try. |
@udovenko Please send me your email, and I'll add you to our slack room. BTW, it was some serious work building the separate JS package to interact with the generated JS code. 😃 |
I've looked at ConnectionPool and ReactOnRails codebase, but still did not find an answer. ReactOnRails allows to separately initialize Redux store and React components with redux_store and react_component helpers on server side. My question is: is there possibility of Ruby context switching between these helpers calls which potentially can lead to rehydration of Redux store by another thread before react component is rendered?
For example, in my template I have someting like this:
So template logic checks out js_context from the pool, hydrates store there, then renders React component assuming that we're in the same js_context with properly hydrated store, then retrieves meta tags rendered by DocumentMeta package during component rendering.
This approach works fine without concurrency. But is the following scenario possible in multithreading environment:
Sorry if this is a stupid question and the answer obvious...
The text was updated successfully, but these errors were encountered: