I have been trying to set up an action cable with ruby on rails version 7.0.4 and fail to do
In the app/channels/application_cable.rb my configuration file is as below. As per official documentation, every connection should be identified, so I decided to identify that via random string:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by: random_string
def random_string
return SecureRandom.base64()
end
end
end
I have only a single channel, called room_channel for learning purposes:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
in the app/javascript/channels/consumer.js I created below configuration and set up at least 1 consumer, so my app could work
import { createConsumer } from "#rails/actioncable"
export default createConsumer()
createConsumer(getWebSocketURL)
function getWebSocketURL() {
return "ws://159.61.241.9:8000/cable"
}
My config/environments/development.rb file is as below for action cable related settings:
config.action_cable.url = "ws://localhost:6379/cable"
config.action_cable.allowed_request_origins = ['http://159.65.241.9:8000']
config.action_cable.mount_path="ws://localhost:6379/cable"
my cable.yml file in the config as below:
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
test:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: action_cable_chat_app_production
I started up the redis server already.
The error I end up with is as below:
Can someone please help me why WebSocket tries to start at 8000 port even though I mount the server in 6379 in the config file ?
Problem is in your JS
function getWebSocketURL() {
return "ws://159.61.241.9:8000/cable"
}
It tries to connect to "ws://159.61.241.9:8000/cable", but you started at 6379 port. Change it to "ws://159.61.241.9:6379/cable". If 159.61.241.9 is not your public IP, than change to "ws://localhost:6379/cable"
Related
I am trying to build an app which will use one of latest Rails 6.1 features: multiple databases - you can read in sources at the bottom.
I would like to implement multitenancy by being able to switch between databases based on request domain.
Each club would have his own database, and one primary database would store app specific data like Users, Clubs.
I created two types of records: GlobalRecord using primary database and ShardRecord using horizontal shards databases.
And also tried to use around_action to select current club database.
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
development:
primary:
<<: *default
database: primary
migrations_paths: db/migrate
club_1:
<<: *default
database: club_1
migrations_paths: db/shard_migrate
club_2:
<<: *default
database: club_2
host: 1.1.1.1
username: deploy
password: pass
migrations_paths: db/shard_migrate
class Club < GlobalRecord; end
class GlobalRecord < ActiveRecord::Base
self.abstract_class = true
connects_to shards: {
club_1: { writing: :primary, reading: :primary },
club_2: { writing: :primary, reading: :primary }
}
end
class MemberRecord < ShardRecord; end
class ShardRecord < ActiveRecord::Base
self.abstract_class = true
connects_to shards: {
club_1: { writing: :club_1, reading: :club_1 },
club_2: { writing: :club_2, reading: :club_2 }
}
end
class ApplicationController < ActionController::API
before_action :set_club
around_action :connect_to_shard
private
def set_club
#club = SelectClubByDomain.new(request).slug
end
def connect_to_shard
ActiveRecord::Base.connected_to(role: :writing, shard: #club.slug) do
yield
end
end
end
I have few design questions/issues I would like to ask YOU about:
I would like to set GlobalRecord with connects_to database: { writing: :primary } but because I added around_action
I have to set it as above.
Is it bad design?
Can I refactor it so I could always call GlobalRecord without ActiveRecord::Base.connected_to(role: :writing, shard: :club_1) block?
Tried to use ActiveRecord::Base.connected_to_many(GlobalRecord, ShardRecord, role: :writing, shard: #club_slug.to_sym) in around_action but on request(clubs#index => [Club.all]) I get an error:
ActiveRecord::ConnectionNotEstablished (No connection pool for 'GlobalRecord' found for the 'club_1' shard.)
On production server on start I see this warning, how to fix it?
=> Run `bin/rails server --help` for more startup options
Failed to define attribute methods because of ActiveRecord::ConnectionNotEstablished: ActiveRecord::ConnectionNotEstablished
Sources:
https://guides.rubyonrails.org/active_record_multiple_databases.html
https://www.freshworks.com/horizontal-sharding-in-a-multi-tenant-app-with-rails-61-blog/
https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-connected_to_many
In general it's absolutely fine to switch connections with an around filter, we have been doing this for years (even before Rails 6) in my previous company.
Btw: did you see that the guide is mentioning your posted exception?
Note that connected_to with a role will look up an existing connection and switch using the connection specification name. This means that if you pass an unknown role like connected_to(role: :nonexistent) you will get an error that says ActiveRecord::ConnectionNotEstablished (No connection pool for 'ActiveRecord::Base' found for the 'nonexistent' role.)
https://guides.rubyonrails.org/active_record_multiple_databases.html#using-manual-connection-switching
I have an app hosted on Heroku, using Redis Cloud and ActionCable. Locally everything works but on Heroku the user is redirected to a localhost address after a form is submitted. I have tried most of the advice listed here (apart from resque settings since resque gem is not installed): Redis tries to connect to localhost on Heroku instead of REDIS_URL
Redis Cloud was added with
heroku addons:create rediscloud:30
This is in my cable.yml, I have commented out two other solutions that I tried that also did not work (in my actual code, redis url is set to the actual address rather than xxx999)
production:
adapter: redis
# url: <%= ENV.fetch('REDISCLOUD_URL') %>
# url: <%= ENV.fetch("REDISCLOUD_URL") { "redis://localhost:6379/1" } %>
url: <%= ENV.fetch("REDISCLOUD_URL") { "redis://default:xxx999#redis-13674.c77.eu-west-1-1.ec2.cloud.redislabs.com:13674" } %>
channel_prefix: art--collabs_production
In redis.rb:
if ENV["REDISCLOUD_URL"]
$redis = Redis.new(url: ENV["REDISCLOUD_URL"])
end
In production.rb I have the following (again, the commented out code has been tried and also failed)
config.action_cable.url = "ws://www.artcollabs.net/cable"
config.action_cable.allowed_request_origins = [ "http://www.artcollabs.net", "https://www.artcollabs.net" ]
# config.cache_store = :redis_cache_store, { url: ENV.fetch("REDISCLOUD_URL", "redis://localhost:6379/1") }
config.cache_store = :redis_cache_store, { url: ENV.fetch("REDISCLOUD_URL", "redis://default:xxx999#redis-13674.c77.eu-west-1-1.ec2.cloud.redislabs.com:13674") }
Any suggestions on how to solve this?
I'm working on a site someone else made. There is a production version.
I'm trying to send emails to users whose properties haven't been updated in 30 days.
Everything works until I try to use deliver_later. The reason I'm using deliver_later is because deliver_now results in an issue of sending too many emails per second. I'm currently using Mailtrap for testing, but I assume I will run into that sort of issue on production.
So I opted to wait 1 second for each email:
#testproperties = Property.has_not_updated.includes(:user)
#testproperties.each do |property|
UserMailer.with(property: property, user: property.user).check_listing.deliver_later(wait:1.seconds)
end
This results in IO::EINPROGRESSWaitWritable Operation now in progress - connect(2) would block
And nothing sends.
I'm not sure how to solve this issue.
Edit:
I can see on the production site that I can visit the route /sidekiq. The routes file has this block:
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
I can view the web interface and see all the jobs. It's all working there. But I need to access development version running on localhost:3000.
Trying to access this locally still results in:
Operation now in progress - connect(2) would block
# # Socket#connect
def connect_nonblock(addr, exception: true)
__connect_nonblock(addr, exception)
end
end
Sidekiq.rb:
require 'sidekiq'
unless Rails.env.test?
host = 'localhost'
port = '6379'
namespace = 'sitename'
Sidekiq.configure_server do |config|
config.redis = { url: "redis://#{host}:#{port}", namespace: namespace }
schedule_file = "config/schedule.yml"
if File.exists?(schedule_file)
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
end
config.server_middleware do |chain|
chain.add Sidekiq::Status::ServerMiddleware, expiration: 30.minutes
end
config.client_middleware do |chain|
chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
end
end
Sidekiq.configure_client do |config|
config.redis = { url: "redis://#{host}:#{port}", namespace: namespace }
config.client_middleware do |chain|
chain.add Sidekiq::Status::ClientMiddleware, expiration: 30.minutes
end
end
end
for cable.yml:
development:
adapter: async
url: redis://localhost:6379/1
channel_prefix: sitename_dev
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
channel_prefix: sitename_production
The production server is running Ubuntu and they already installed redis-server.
I had not installed that locally. (I'm using Ubuntu through Windows WSL)
sudo apt install redis-server
I can now access the web interface.
Make sure that Redis is started:
sudo service redis-server restart
I am trying to setup Recaptcha in my rails 5 application as it's described in the documentation but it fails.
I use this gem: recaptcha (4.6.6), ruby 2.5.0 and rails 5.1.4
In view form:
<%= flash[:recaptcha_error] %>
<%= recaptcha_tags %>
In devise registrations controller:
prepend_before_action :check_captcha, only: :create
private
def check_captcha
unless verify_recaptcha
self.resource = resource_class.new sign_up_params
resource.validate # Look for any other validation errors besides Recaptcha
respond_with_navigational(resource) { redirect_to new_user_registration_path }
end
end
In my initializers/recaptcha.rb
Recaptcha.configure do |config|
config.site_key = Rails.application.config_for(:recaptcha)['site_key']
config.secret_key = Rails.application.config_for(:recaptcha)['secret_key']
end
In my recaptcha.yml:
default: &default
site_key: <%= ENV["RECAPTCHA_SITE_KEY"] %>
secret_key: <%= ENV["RECAPTCHA_SECRET_KEY"] %>
development:
<<: *default
test:
<<: *default
staging:
<<: *default
production:
<<: *default
In /etc/environments:
# RECAPTCHA
RECAPTCHA_SITE_KEY=6Lfg3ksUAAAAABOD_OXCtPO60*******
RECAPTCHA_SECRET_KEY=6Lfg3ksUAAAAAOmFGdAxdo8*******
PROBLEM
After adding ENV variables to /etc/environments, I exported it with this command:
for line in $( cat /etc/environment ) ; do export $line ; done
Then I check that Recaptcha module is configured correctly:
/home/deploy/apps/app_name/current$ bundle exec rails c
Loading staging environment (Rails 5.1.4)
2.5.0 :001 > Recaptcha::Configuration.new
=> #<Recaptcha::Configuration:0x0000000006601908 #skip_verify_env=["test", "cucumber"], #handle_timeouts_gracefully=true, #secret_key="6Lfg3ksUAAAAAOmFGdAxdo8H*************", #site_key="6Lfg3ksUAAAAABOD_OXCtPO*************">
2.5.0 :002 > Recaptcha::Configuration.new.site_key!
=> "6Lfg3ksUAAAAABOD_OXCtPO*************"
Also, I see these ENV variables when I run printenv command (so it's really loaded)
After that, I restarted rails and got an error
No site key specified.
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/recaptcha-4.6.6/lib/recaptcha/configuration.rb:47:in `site_key!'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/recaptcha-4.6.6/lib/recaptcha/client_helper.rb:79:in `recaptcha_components'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/recaptcha-4.6.6/lib/recaptcha/client_helper.rb:15:in `recaptcha_tags'
/home/deploy/apps/app_name/releases/20180310222304/app/views/users/registrations/new.html.erb:27:in `block in _app_views_users_registrations_new_html_erb___216558772140569572_69973306795360'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/actionview-5.1.4/lib/action_view/helpers/capture_helper.rb:39:in `block in capture'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/actionview-5.1.4/lib/action_view/helpers/capture_helper.rb:203:in `with_output_buffer'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/actionview-5.1.4/lib/action_view/helpers/capture_helper.rb:39:in `capture'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/actionview-5.1.4/lib/action_view/helpers/form_helper.rb:450:in `form_for'
/home/deploy/apps/app_name/releases/20180310222304/app/views/users/registrations/new.html.erb:21:in `_app_views_users_registrations_new_html_erb___216558772140569572_69973306795360'
/home/deploy/apps/app_name/shared/bundle/ruby/2.5.0/gems/actionview-5.1.4/lib/action_view/template.rb:157:in `block in render'
I am posting here in case someone is looking for a Rails 5.2 solution to setting up Recaptcha keys. This solution utilizes the new config/master.key and config/credentials.yml.enc encryption file.
Add the Recaptcha keys to the credentials.yml.enc file by editing the file in your local terminal:
EDITOR="vim" rails credentials:edit
After adding the keys to the credentials file (see example below), save and exit the file. Upon exit, the credentials.yml.enc file will then be automatically encrypted. The master.key is necessary for decryption by your application. Before encryption:
recaptcha_site_key: 6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy
recaptcha_secret_key: 6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxx
3. Create a file named config/recaptcha.rb in your Rails application and add the following code to it:
Recaptcha.configure do |config|
config.site_key = Rails.application.credentials.dig(:recaptcha_site_key)
config.secret_key = Rails.application.credentials.dig(:recaptcha_secret_key)
end
This solution works locally and on Ubuntu/nginx in production. You won't need a gem or environment variables for it to work. If the master.key fails to decrypt, you may need to delete both the credentials.yml.enc file and possibly even the master.key file, then repeat this process locally (EDITOR="vim" rails credentials:edit, etc.) before copying over a new master.key to production and re-deploying.
I still don't know what is the cause of the "No site key specified" error.
I really don't like gem 'recapthca' works directly with ENV variables,
and also I spent too much time on investigations.
So, I decided to not use this gem and write my own code.
I use only Invisible Recaptcha in my application.
Config file (loads secret and site keys)
# /config/recaptcha.yml
default: &default
site_key: <%= ENV["RECAPTCHA_SITE_KEY"] %>
secret_key: <%= ENV["RECAPTCHA_SECRET_KEY"] %>
development:
<<: *default
test:
<<: *default
staging:
<<: *default
production:
<<: *default
Application helper (a button with Recaptcha helper)
# /app/helpers/application_helper.rb
module ApplicationHelper
def submit_with_recaptcha(text, custom_options)
unless custom_options[:data].has_key?(:form_id)
raise "Data Form Id option not found ('{data: {form_id: 'id_without_dash'}')."
end
options = {
type: 'button',
data: {
form_id: custom_options[:data][:form_id],
sitekey: recaptcha_site_key,
callback: "submit#{custom_options[:data][:form_id].camelize}#{Time.current.to_i}"
},
class: (custom_options[:class].split(' ') + ['g-recaptcha']).uniq.join(' ')
}
script_code = <<-SCRIPT
function #{options[:data][:callback]}() {
document.getElementById('#{options[:data][:form_id]}').submit();
}
SCRIPT
javascript_tag(script_code) + content_tag(:div, class: 'recaptcha_wrapper'){ submit_tag(text, options) }
end
private
def recaptcha_site_key
Rails.application.config_for(:recaptcha)['site_key']
end
end
Verification service (as it uses external API)
# app/services/google_recaptcha/verification.rb
module GoogleRecaptcha
# https://developers.google.com/recaptcha/docs/verify
class Verification
# response - params['g-recaptcha-response'])
def self.successful?(recaptcha_params, remoteip)
verify_url = URI.parse('https://www.google.com/recaptcha/api/siteverify')
verify_request = Net::HTTP::Post.new(verify_url.path)
verify_request.set_form_data(
response: recaptcha_params,
secret: secret_key,
remoteip: remoteip
)
connection = Net::HTTP.new(verify_url.host, verify_url.port)
connection.use_ssl = true
Rails.logger.info '[RECAPTCHA] Sending verification request.'
verify_response = connection.start { |http| http.request(verify_request) }
response_data = JSON.parse(verify_response.body)
Rails.logger.info "[RECAPTCHA] Verification response is#{' not' unless response_data['success']} successful."
response_data['success']
end
private
def self.secret_key
Rails.application.config_for(:recaptcha)['secret_key']
end
end
end
Controller Concern (Recaptcha verification in before_action)
# app/controllers/concerns/recaptchable.rb
module Recaptchable
extend ActiveSupport::Concern
included do
before_action :verify_recaptcha, only: [:create]
end
private
def verify_recaptcha
unless GoogleRecaptcha::Verification.successful?(recaptcha_params['g-recaptcha-response'], request.remote_ip)
render :new
return
end
end
def recaptcha_params
params.permit(:'g-recaptcha-response')
end
end
Usage
Add concern to your controller:
class MyController < ShopController
include Recaptchable
end
Add www.google.com/recaptcha/api.js javascript to your page
Add submit_with_recaptcha helper into your form
<%= form_for #delivery, url: users_delivery_path, method: 'post' do |f| %>
<%= submit_with_recaptcha t('order.deliver.to_confirmation'), data: {form_id: 'new_delivery'}, class: 'btn-round' %>
<% end %>
<%= javascript_include_tag "https://www.google.com/recaptcha/api.js?hl=#{I18n.locale}", 'data-turbolinks-track': 'reload' %>
That's it.
Note: I'm posting this answer here for people who may find this question. Here is how I solved the problem.
I use local_env.yml for my environment variables. I just started using the gem and added RECAPTCHA_SITE_KEY & RECAPTCHA_SECRET_KEY to local_env.yml. I got the same error.
It took a bit to figure out that the gem directly used the variables. I ended up putting the following statements in ~/.bashrc similar to what the documentation said but without the quotes around the values.
export RECAPTCHA_SITE_KEY=6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy
export RECAPTCHA_SECRET_KEY=6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx
I host my applications on Heroku. I executed the following terminal commands to set my environment variables in Heroku.
heroku config:set RECAPTCHA_SITE_KEY=‘6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy’
heroku config:set RECAPTCHA_SECRET_KEY=‘6LcGuI4U6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxxAAAAGAWMYRKFGfHUCSD0SPrMX2lfyl9’
Are you using nginx? Nginx removes ENV vars (except TZ) and it seems that the recaptcha gem is particularly sensitive to this. From experience, when using the dotenv gem other ENV vars work ok, recaptcha ENV vars are ignored.
You can solve the issue by adding the env vars to the top of your nginx.conf.
env RECAPTCHA_SITE_KEY=value1;
env RECAPTCHA_SECRET_KEY=value2;
Here's nginx's documentation on the matter.
I'm using Rails 5 in a docker environment and I can get Action Cable to broadcast on a Sidekiq worker perfectly fine, using worker.new.perform.
But for the life of me, I cannot get it to broadcast while using worker.perform_async.
Here is my cable.yml:
default: &default
adapter: redis
url: <%= ENV['REDIS_PROVIDER'] %>
development:
<<: *default
test:
<<: *default
production:
<<: *default
Here is my worker:
class AdminWorker
include Sidekiq::Worker
def perform
ActionCable.server.broadcast 'admin_channel', content: 'hello'
end
end
My Admin Channel:
class AdminChannel < ApplicationCable::Channel
def subscribed
stream_from "admin_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
As I mentioned earlier, this works just fine when calling AdminWorker.new.perform. As soon as I try to run AdminWorker.perform_async, the cable will not broadcast and nothing helpful regarding action cable in the logs. What am I missing here?
I had the same problem. Came across this answer: ActionCable.server.broadcast from the console - worked for me. Basically just changed cable.yml
development:
adapter: async
to
development:
adapter: redis
url: redis://localhost:6379/1
and I was able to broadcast from console, models, Sidekiq worker classes, etc.
For those who wants to dockerize rails and sidekiq, you should run action_cable server separately like
puma -p 28080 cable/config.ru
https://nickjanetakis.com/blog/dockerize-a-rails-5-postgres-redis-sidekiq-action-cable-app-with-docker-compose
https://github.com/kchrismucheke/dockersample/blob/da5923899fb17682fabed041bef5381ed3fd91ab/config/application.rb#L57-L62
In development environment
By default, ActionCable would run only on the rails server only. And it would not work in Sidekiq, console, cron jobs, etc.
Because in config/cable.yml development server adapter is set to async
Solution:
For ActionCable inter-process broadcasting, you have to set the adapter to Redis
In config/cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
channel_prefix: project_name_development