Rails ActionCable and Ember CLI app - Resource Bottlenecks - ruby-on-rails

We've successfully implemented real time updates in our app using ActionCable in Rails and implemented the consumer as a client service in Ember CLI, but am looking for a better, less-expensive approach.
app/models/myobj.rb
has_many :child_objs
def after_commit
ActionCable.server.broadcast("obj_#{self.id}", model: "myobj", id: self.id)
self.child_objs.update_all foo: bar
end
app/models/child_obj.rb
belongs_to :myobj
def change_job
self.job = 'foo'
self.save
ActionCable.server.broadcast("obj_#{self.myobj.id}", model: "child_obj", id: self.id)
end
frontend/app/services/stream.js
Here we're taking the model and id data from the broadcast and using it to reload from the server.
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service(),
subscribe(visitId) {
let store = this.get("store")
MyActionCable.cable.subscriptions.create(
{channel: "ObjChannel", id: objId}, {
received(data) {
store.findRecord(data.model, data.id, {reload: true});
}
}
);
},
});
This approach "works" but feels naïve and is resource intensive, hitting our server again for each update, which requires re-authenticating the request, grabbing data from the database, re-serializing the object (which could have additional database pulls), and sending it across the wire. This does in fact cause pool and throttling issues if the number of requests are high.
I'm thinking we could potentially send the model, id, and changeset (self.changes) in the Rails broadcast, and have the Ember side handle setting the appropriate model properties. Is this the correct approach, or is there something else anyone recommends?

You should be fine with sending whole entity payload with your change event via sockets. Later you can push payload to the store - create new records or update existing. This way you'll avoid additional server requests.

Related

Commit a nested transaction

Let's say I have a method that provides access to an API client in the scope of a user and the API client will automatically update the users OAuth tokens when they expire.
class User < ActiveRecord::Base
def api
ApiClient.new access_token: oauth_access_token,
refresh_token: oauth_refresh_token,
on_oauth_refresh: -> (tokens) {
# This proc will be called by the API client when an
# OAuth refresh occurs
update_attributes({
oauth_access_token: tokens[:access_token],
oauth_refresh_token: tokens[:refresh_token]
})
}
end
end
If I consume this API within a Rails transaction and a refresh occurs and then an error occurs - I can't persist the new OAuth tokens (because the proc above is also treated as part of the transaction):
u = User.first
User.transaction {
local_info = Info.create!
# My tokens are expired so the client automatically
# refreshes them and calls the proc that updates them locally.
external_info = u.api.get_external_info(local_info.id)
# Now when I try to locally save the info returned by the API an exception
# occurs (for example due to validation). This rolls back the entire
# transaction (including the update of the user's new tokens.)
local_info.info = external_info
local_info.save!
}
I'm simplifying the example but basically the consuming of the API and the persistence of data returned by the API need to happen within a transaction. How can I ensure the update to the user's tokens gets committed even if the parent transaction fails.
Have you tried opening a new db connection inside new thread, and in this thread execute the update
u = User.first
User.transaction {
local_info = Info.create!
# My tokens are expired so the client automatically
# refreshes them and calls the proc that updates them locally.
external_info = u.api.get_external_info(local_info.id)
# Now when I try to locally save the info returned by the API an exception
# occurs (for example due to validation). This rolls back the entire
# transaction (including the update of the user's new tokens.)
local_info.info = external_info
local_info.save!
# Now open new thread
# In the new thread open new db connection, separate from the one already opened
# In the new connection execute update only for the tokens
# Close new connection and new thread
Thread.new do
ActiveRecord::Base.connection_pool.with_connection do |connection|
connection.execute("Your SQL statement that will update the user tokens")
end
end.join
}
I hope this helps
Nermin's (the accepted) answer is correct. Here's an update for Rails >= 5.0
Thread.new do
Rails.application.executor.wrap do
record.save
end
# Note: record probably won't be updated here yet since it's async
end
Documented here: Rails guides threading and concurrency
This discussion from a previous question might help you. It looks like you can set a requires_new: true flag and essentially mark the child transaction as a sub transaction.
User.transaction {
User.transaction(requires_new: true) {
u.update_attribute(:name, 'test')
};
u.update_attribute(:name, 'test2');
raise 'boom'
}

Using Actioncable and Rails 5 API mode together

I am creating a very basic chat app. My goal is to have a Rails API backend and then build an IOS, Android, web, and desktop client. This is purely to explore Websockets and mobile development.
I've never used Actioncable, and my knowledge of Websockets is very limited. What I'd like to know is if I can set up Actioncable on my Rails API and have it communicate with Node (for instance).
Does Actioncable act like any other Websocket? Could I connect to it from my Node app through ws://<host>/cable and have a functional pub-sub system between whatever client and Rails?
I'm sorry if that doesn't make sense, I'm having a hard time wording it :)
Thank you!
Indeed you can!
Just like you create any api app, use generator
rails new my_app --api
Create your_channel
rails generate channel your_channel
Add mount path in routes.rb
mount ActionCable.server => '/cable'
Allow stream on subscribe method in /app/channels/your_channel.rb
class YourChannel < ApplicationCable::Channel
def subscribed
stream_from 'messages' # <----- Stream Name Here
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Call ActionCable.server.broadcast from any other part of your app to stream
ActionCable.server.broadcast 'messages', message: 'ping'
Now use your front-end to test it. Since you told you want iOS Android and also mentioned about node, I assume you are using (or would choose to use) react-native
import ActionCable from 'react-native-actioncable';
const cable = ActionCable.createConsumer("ws://localhost:3000/cable");
class YourReactClass extends React.Component {
# add or update the following
componentDidMount = () => {
console.log("componentDidMount executed");
this.subscription = cable.subscriptions.create("OrderChannel", {
connected: function() { console.log("connected: action cable") },
disconnected: function() { console.log("disconnected: action cable") },
received: function (data) { console.log(data) }
}
)
};
componentWillUnmount () {
this.subscription &&
cable.subscriptions.remove(this.subscription)
}
}
And you're good to go, build your logic on top of this... If you got any problems, let me know.

How to receive data in ActionCable Channel without JS?

I'm writing a Rails application that uses WebSockets to communicate with other machines (no browser and client side logic in this process). I have a channel:
class MachinesChannel < ApplicationCable::Channel
def subscribed
...
end
def unsubscribed
...
end
def handle_messages
...
end
end
To receive the data the only way I know about is the JavaScript client:
ActionCable.createConsumer('/cable').subscriptions.create 'MachinesChannel',
received: (message) ->
#perform('handle_messages')
I can call server side methods from JS via #perform() method.
Is there any way to omit the JS part and somehow directly handle the incoming data in MachinesChannel?
The ideal situation would be to have the handle_messages method accept a data argument and have this metod called on incoming data.
After looking into ActionCable source code I got the following solution. You just have to create a method in MachinesChannel that you want to be called, e.g. handle_messages(data). Then, in the client that connects to your websocket, you need to send a message in the following format (example in ruby):
id = { channel: 'MachinesChannel' }
ws = WebSocket::Client::Simple.connect(url)
ws.send(JSON.generate(command: 'message', identifier: JSON.generate(id), data: JSON.generate(action: 'handle_messages', foo: 'bar', biz: 'baz')))
action has to be the name of the method you want to be called in MachinesChannel. The rest of key-values are whatever you want. This the date you can receive in the ActionCable channel.
Recently a gem action_cable_client has been release which seems exactly perfect for this kind of usage. I haven't used it, so I don't know how it really works.
Instead of:
def handle_messages
...
end
This works for me:
def receive(data)
puts data
...
end

The stratigy of build a talk-to-talk system using em-websocket in rails?

Maybe it is a good example for server push system. There are many users in the system, and users can talk with each other. It can be accomplished like this: one user sends message(through websocket) to the server, then the server forward the message to the other user. The key is to find the binding between the ws(websocket object) and the user. The example code like below:
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
# extract the user id from handshake and store the binding between user and ws
}
ws.onmessage { |msg|
# extract the text and receiver id from msg
# extract the ws_receiver from the binding
ws_receiver.send(text)
}
end
}
I want to figure out following issues:
The ws object can be serialized so it can be stored into disk or database? Otherwise I can only store the binding into memory.
What the differences between em-websocket and websocket-rails?
Which gem do you recommend for websocket?
You're approaching a use case that websockets are pretty good for, so you're on the right track.
You could serialize the ws object with Marshal, but think of websocket objects as being a bit like http request objects in that they are abstractions for a type of communication. You are probably best off marshaling/storing the data.
em-websocket is a lower(ish) lever websocket library built more or less directly on web-machine. websocket-rails is a higher level abstraction on websockets, with a lot of nice tools built in and pretty ok docs. It is built on top of faye-websocket-rails which is itself built on web machine. *Note, action cable which is the new websocket library for Rails 5 is built on faye.
I've use websocket-rails in the past and rather like it. It will take care of a lot for you. However, if you can use Rails 5 and Action Cable, do that, its the future.
The following is in addition to Chase Gilliam's succinct answer which included references to em-websocket, websocket-rails (which hadn't been maintained in a long while), faye-websocket-rails and ActionCable.
I would recommend the Plezi framework. It works both as an independent application framework as well as a Rails Websocket enhancement.
I would consider the following points as well:
do you need the message to persist between connections (i.e. if the other user if offline, should the message wait in a "message box"? for how long should the message wait?)...?
Do you wish to preserve message history?
These points would help yo decide if to use a persistent storage (i.e. a database) for the messages or not.
i.e., to use Plezi with Rails, create an init_plezi.rb in your application's config/initializers folder. use (as an example) the following code:
class ChatDemo
# use JSON events instead of raw websockets
#auto_dispatch = true
protected #protected functions are hidden from regular Http requests
def auth msg
#user = User.auth_token(msg['token'])
return close unless #user
# creates a websocket "mailbox" that will remain open for 9 hours.
register_as #user.id, lifetime: 60*60*9, max_connections: 5
end
def chat msg, received = false
unless #user # require authentication first
close
return false
end
if received
# this is only true when we sent the message
# using the `broadcast` or `notify` methods
write msg # writes to the client websocket
end
msg['from'] = #user.id
msg['time'] = Plezi.time # an existing time object
unless msg['to'] && registered?(msg['to'])
# send an error message event
return {event: :err, data: 'No recipient or recipient invalid'}.to_json
end
# everything was good, let's send the message and inform
# this will invoke the `chat` event on the other websocket
# notice the `true` is setting the `received` flag.
notify msg['to'], :chat, msg, true
# returning a String will send it to the client
# when using the auto-dispatch feature
{event: 'message_sent', msg: msg}.to_json
end
end
# remember our route for websocket connections.
route '/ws_chat', ChatDemo
# a route to the Javascript client (optional)
route '/ws/client.js', :client
Plezi sets up it's own server (Iodine, a Ruby server), so remember to remove from your application any references to puma, thin or any other custom server.
On the client side you might want to use the Javascript helper provided by Plezi (it's optional)... add:
<script src='/es/client.js' />
<script>
TOKEN = <%= #user.token %>;
c = new PleziClient(PleziClient.origin + "/ws_chat") // the client helper
c.log_events = true // debug
c.chat = function(event) {
// do what you need to print a received message to the screen
// `event` is the JSON data. i.e.: event.event == 'chat'
}
c.error = function(event) {
// do what you need to print a received message to the screen
alert(event.data);
}
c.message_sent = function(event) {
// invoked after the message was sent
}
// authenticate once connection is established
c.onopen = function(event) {
c.emit({event: 'auth', token: TOKEN});
}
// // to send a chat message:
// c.emit{event: 'chat', to: 8, data: "my chat message"}
</script>
I didn't test the actual message code because it's just a skeleton and also it requires a Rails app with a User model and a token that I didn't want to edit just to answer a question (no offense).

Manually invoking stream action in server sent event

How do I send update notifications to clients using server sent event?
What I want to accomplish is that when a client ajax calls an action, the server then would send relevant data to all connecting clients through my stream action.
I'm trying to know if this would be possible without websockets or pub/subs.
From what I can gather, you're looking for a generalized approach, rather than specific code?
--
SSE's
Server Sent Events are an HTML5 technology, meaning that if you do it correctly, it shouldn't matter whether you use Rails or another framework -- they should just work
One drawback to SSE's is they act very similar to Ajax long-polling, meaning they send constant "pings" / requests to your server, relaying back any response they find. And they'll still use the pub/sub pattern too
-
Simply, SSE's are when you have a Javascript "event listener", which will listen to an "endpoint" (URL). The endpoint, in the case of Rails, will be a controller#action, from which you can send the relevant text/event-stream updates, which is what ActionController::Live::SSE is there to do
--
Setup
#config/routes.rb
resources :your_controller do
collection do
get :endpoint
end
end
#app/assets/javascripts/application.js
var source = new EventSource('your_controller/endpoint');
source.addEventListener('message', function(e) {
console.log(e.data);
}, false);
#app/controllers/your_controller.rb
Class YourController < ActionController::Base
include ActionController::Live
def endpoint
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
sse.write({ name: 'John'})
ensure
sse.close
end
end
This will send the relevant updates for you every time

Resources