authors | categories | date | draft | short | title | |||
---|---|---|---|---|---|---|---|---|
|
|
2016-07-24 13:27:26 +1000 |
false |
A guide to configuring and deploying a Rails 5 Action Cable app to Cloud Foundry.
|
Using Action Cable With Cloud Foundry |
This post is going to show you how to set up a Rails 5 app that makes use of the new Action Cable feature to perform live reloads of part of a web page. Action Cable is based on Websockets and is one of the new features just released with Rails 5. This new library gives you the ability to easily create real time web applications without pulling in any third party gems to handle the websockets. Additionally the integration with other rails components and conventions means you can write very few lines of code to accomplish a great deal (this is why we use rails in the first place).
As well as showing you the basics of Action Cable this guide will show you how to configure your app so that it runs on Cloud Foundry. In this particular guide we will be deploying to PWS, which is Pivotal's consumer instance of Cloud Foundry, however most of the steps will be relevant to any Cloud Foundry instance.
All code used here can be found here.
Here is a demo of the running app syncing messages across tabs using websockets: {{< responsive-figure src="https://camo.githubusercontent.com/2aaf8beb1524062bbc58d4324b7aac141d960433/687474703a2f2f672e7265636f726469742e636f2f6b6e6e74444f335074642e676966" >}}
Firstly we create a new rails application the usual way with the rails 5.X.X gem installed:
rails new action-cable-test
We want our app to display a live list of messages so our model is just:
# app/models/message.rb
class Message < ApplicationRecord
end
And the migration is:
class CreateMessages < ActiveRecord::Migration[5.0]
def change
create_table :messages do |t|
t.text :message
t.timestamps
end
end
end
Now that we have a our model we want a view to create and show them. For now it will be a single input form and a list of messages underneath.
We must first add our routes to config/routes.rb
.
Rails.application.routes.draw do
root to: redirect('/messages')
resources :messages, only: [:index, :create]
end
Then we must add the app/controllers/messages_controller.rb
:
class MessagesController < ApplicationController
def index
@messages = Message.all
end
def create
@message = Message.create!(params.require(:message).permit(:message))
redirect_to messages_url
end
end
And now the views:
# app/views/messages/index.html.erb
<%= form_for Message.new do |f| %>
<%= f.text_field :message %>
<%= f.submit %>
<% end %>
<div id="messages">
<%= render 'message_list', messages: @messages %>
</div>
# app/views/messages/_message_list.html.erb
<ul>
<% messages.each do |message| %>
<li><%= message.message %></li>
<% end %>
</ul>
The reason we've split up the list into a separate view will become clearer in the next section.
Ok so at this point we have our app set up with a form and a list of messages but the problem is that we don't have the messages live updating when we have multiple clients with the page open simultaneously. In order to accomplish this we are going to use the new Action Cable feature in Rails 5.
Rails 5 comes with a generator to set up some of the boiler plate for your Action Cable channel:
$ bin/rails generate channel messages
create app/channels/messages_channel.rb
create app/assets/javascripts/cable.js
create app/assets/javascripts/channels/messages.coffee
The first generated file we're going to change is app/channels/messages_channel.rb
which is the backend code. The MessagesChannel
is a bit like a rails controller but for Action Cable.
We are going to make a couple of changes to this file.
class MessagesChannel < ApplicationCable::Channel
def self.broadcast
broadcast_to "messages",
messages: MessagesController.render(
partial: 'messages/message_list',
locals: { messages: Message.all }
)
end
def subscribed
stream_from "messages:messages"
end
def unsubscribed
end
end
The first change is the self.broadcast
method. This is a helper we're going to call later in order broadcast changes to the messages list. As you can see this renders the app/views/messages/_message_list.html.erb
partial and broadcasts this to all subscribers of "messages"
. Since this is the MessagesChannel
this then becomes the "messages:messages"
in the subscribed method.
The other change is to the subscribed
method. Here we add stream_from "messages:messages"
so that all subscribers are set up to stream from the "messages:messages"
topic.
Now that we have a helper to broadcast changes we need to call this whenever our messages list updates. To do this we add call to MessagesChannel.broadcast
in app/controllers/messages_controller.rb
:
class MessagesController < ApplicationController
def index
@messages = Message.all
end
def create
Message.create!(params.require(:message).permit(:message))
# Broadcast must be done after creating the new message so it's
# there when we render the template.
MessagesChannel.broadcast
redirect_to messages_url
end
end
Now we have the messages list being broadcast to all subscribers whenever there is a change, but we aren't doing anything with these messages yet. To ensure the changes are reflected live on the page we add a little bit of code to our app/assets/javascripts/channels/messages.coffee
.
App.messages = App.cable.subscriptions.create "MessagesChannel",
connected: ->
disconnected: ->
received: (data) ->
$('#messages').html(data.messages)
This snippet of jquery simply finds the existing messages list and swaps it out with the new messages list html that we sent via the websockets payload.
And that's really it for the Action Cable stuff. If we now open two browsers locally on the app we'll be able to see our changes synchronised across both browsers without even refreshing. It's also pretty quick, too.
Now that we have our messages app up and running locally we need to ship it to production!
In development we were just using an in memory async process for communicating with our websocket subscribers, but for production we need something externalized if we want to be able to run multiple instances of the application.
Action Cable by default uses Redis for publishing/subscribing changes in the production environment. The good news for us is that we already have a Redis tile on PWS that we can easily install. Additionally we are going to want add a database to our application for production. For that we'll use Postgres. We can set these services up easily using the CF CLI:
$ cf create-service rediscloud 30mb redis-actioncable && cf create-service elephantsql turtle postgres-actioncable
Creating service instance redis-actioncable in org myorg / space development as me@example.com...
OK
Creating service instance postgres-actioncable in org myorg / space development as me@example.com...
OK
Now that we've created our services we can bind them in our manifest.yml
. Mine looks like:
---
applications:
- name: actioncable
memory: 128M
command: bundle exec rake db:migrate && bundle exec rails s -p $PORT -e $RAILS_ENV
services:
- postgres-actioncable
- redis-actioncable
Since the defaults for Action Cable in Rails 5 are not quite right for PWS we'll need to configure a couple of things differently. Firstly we need some gems for production dependencies and configuration. We should add the following to our Gemfile
:
group :production do
# Used for extracting data from CF environment
gem 'cf-app-utils'
gem 'addressable'
gem 'redis'
gem 'pg'
end
Be sure to run bundle install
before deploying.
The default cable.yml
chooses to configure Redis using a single url parameter which we must construct from our CF credentials. We can connect to our CF Redis service if we update config/cable.yml
to the following:
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: <%= ENV["RAILS_ENV"] == "production" && (c = CF::App::Credentials.find_by_service_tag('redis')) && Addressable::URI.new(scheme: 'redis', host: c.fetch('hostname'), password: c.fetch('password'), port: c.fetch('port')).to_s %>
Since PWS uses port 4443 (rather than 443) for websocket connections we need to make a small change to our config/environments/production.rb
. To configure this port we add the following lines in the configure block:
application_uris = JSON.parse(ENV['VCAP_APPLICATION'])['application_uris']
# Be sure this host is able to route your requests on port 4443. This can be an
# issue if you have a proxy in front of your application that will not proxy port
# 4443 (eg. CloudFlare).
first_host = application_uris[0]
config.action_cable.url = "wss://#{first_host}:4443/cable"
config.action_cable.allowed_request_origins = application_uris.flat_map { |host| ["http://#{host}", "https://#{host}"] }
The last bit of configuration is to update the javascript to use port 4443 rather than the default 443. In order to not hardcode anything in the JS we add a snippet to app/views/layouts/application.html.erb
to pass the url to the frontend like so:
<!DOCTYPE html>
<html>
<head>
+ <script type="text/javascript">window.action_cable_url = '<%= Rails.configuration.action_cable.url %>'</script>
<title>ActionCableTest</title>
Then we must update the app/assets/javascripts/cable.js
to read this global variable before making the connection:
(function() {
this.App || (this.App = {});
- App.cable = ActionCable.createConsumer();
+ if(window.action_cable_url) {
+ App.cable = ActionCable.createConsumer(window.action_cable_url);
+ } else {
+ App.cable = ActionCable.createConsumer();
+ }
}).call(this);
Now that we've got the configuration out of the way we can deploy to production with an easy cf push
.
We were able to use the new Action Cable feature in Rails 5 to make a real time web app and then integrate that easily into Cloud Foundry using the Redis and Postgres services.
Action Cable does appear to support using Postgres for the pub/sub (rather than Redis) so we could have in theory avoided using two different services for this app but as Redis was the default and since it's such a commonly used service it made sense for choosing that in the example.