RabbitMQ Ruby client example (AMQP)

Tagged amqp, bunny, rabbitmq, thread-safe, ruby  Languages ruby
require 'bunny'

# See http://rubybunny.info/articles/concurrency.html
class AMQP
  # Bunny::Session
  def self.session
    Thread.current[:bunny_session] ||= Bunny.new(uri).start
  end
  class << self
    alias connect session
  end

  # Bunny::Channel
  def self.channel
    Thread.current[:bunny_channel] ||= session.create_channel
  end

  # Bunny::Exchange
  def self.exchange
    channel.topic(EXCHANGE, auto_delete: false, durable: true)
  end

  def self.publish(message, queue:)
    exchange.publish(message, routing_key: queue)
  end

  def self.disconnect
    session&.close
  end

  # amqp://user:pass@localhost:5672
  def self.uri
    ENV.fetch('AMQP_URI')
  end
end
  • All Bunny methods are not thread safe, e.g., channel
  • Call AMQP.connect/disconnect in Puma/Unicorn/Sneakers/Sidekiq’s after/before_fork callbacks.

Escaping strings in PostgreSQL queries

Tagged sql, escape, postgres  Languages sql

PostgreSQL queries containing, for example, single quotes or semicolons need to be escaped.

This won’t work:

UPDATE jobs SET work=':'';

To make it work, escape the strings using C-style escapes (E’’) and replacing single-quotes with ‘’:

UPDATE jobs SET work=E':''';

See section “4.1.2.2. String Constants with C-style Escapes” in the PostgreSQL documentation https://www.postgresql.org/docs/10/sql-syntax-lexical.html for details.

How to use private Github repositories with Bundler

Tagged repository, private, github, git, bundler  Languages bash

Does your Gemfile reference private Github repositories?

Option 1: ENV variable

export BUNDLE_GITHUB__COM=username:password
export BUNDLE_GITHUB__COM=<personal-oauth-token>:x-oauth-basic

Option 2: bundle config

bundle config https://github.com/bundler/bundler.git username:password

Option 3: ~/.git-credentials

echo 'https://user:pass@example.com' >> ~/.git-credentials

References https://github.com/rubygems/bundler/pull/3898 https://git-scm.com/docs/git-credential-store

RabbitMQ Publisher Confirms with Bunny

Tagged confirms, rabbitmq, publisher, bunny  Languages ruby

Source https://github.com/ruby-amqp/bunny/blob/master/examples/guides/extensions/publisher_confirms.rb:

require "bunny"

puts "=> Demonstrating publisher confirms"
puts

conn = Bunny.new
conn.start

ch   = conn.create_channel
x    = ch.fanout("amq.fanout")
q    = ch.queue("", :exclusive => true).bind(x)

ch.confirm_select
1000.times do
  x.publish("")
end
ch.wait_for_confirms # blocks calling thread until all acks are received

sleep 0.2
puts "Received acks for all published messages. #{q.name} now has #{q.message_count} messages."

sleep 0.7
puts "Disconnecting..."
conn.close

How to call JavaScript code from Ruby

Tagged execjs, therubyracer, javascript, ruby  Languages js, ruby, javascript

Gemfile:

source 'https://rubygems.org'
gem "therubyracer"
# commonjs.rb provides 'require' needed to import modules
gem 'commonjs'

add_with_javascript.rb:

require 'v8'
require 'commonjs'
env = CommonJS::Environment.new(V8::Context.new, path: './node_modules')
env.require('add.js').add(2, 2)

node_modules/add.js:

function add(a, b) {
  // Require works, if the package is available in node_modules
  // require('xyz');
  // Console is not available by default
  // console.log(msg);
  return a + b;
}

exports.add = add;
$ bundle exec ruby add_with_javascript.rb

Tested with:

  • commonjs (0.2.7)
  • libv8 (3.16.14.19)
  • therubyracer (0.12.3)

Set the accept-language header in Capybara / Selenium WebDriver tests

Tagged selenium, capybara, ruby  Languages ruby

To change the accept-language header that Chrome sends with each request in Capybara tests, add the ‘selenium-webdriver (3.142.6)’ preference to the driver’s options:

  opts.add_preference('intl.accept_languages', 'sv')

Example:

Capybara.register_driver :chrome do |app|
  caps = Selenium::WebDriver::Remote::Capabilities.chrome(loggingPrefs: { browser: 'ALL' })
  opts = Selenium::WebDriver::Chrome::Options.new
  opts.add_preference('intl.accept_languages', 'sv')
  chrome_args = %w[--no-sandbox --disable-popup-blocking --enable-features=NetworkService,NetworkServiceInProcess --window-size=1920,1080]
  chrome_args.each { |arg| opts.add_argument(arg) }
  Capybara::Selenium::Driver.new(app, browser: :chrome, options: opts, desired_capabilities: caps)
end

Tested with:

  • capybara (3.30.3)
  • selenium-webdriver (3.142.6)

Which version of Ruby to use with Rails?

Tagged rails, ruby, version  Languages 

To know which version of Ruby is safe to use with Rails one option is to look at the Travis configuration, for example, Rails 5.2: https://github.com/rails/rails/blob/ee1278cc841a883835918fbedb70add96c84543b/.travis.yml#L64-L69

Note the allowed_failures configuration: https://github.com/rails/rails/blob/172bb64ca8d3f1cf88ca7daa4f6a58436e577c69/.travis.yml#L145-L148

This works for Rubygems too: https://github.com/bblimke/webmock/blob/a3c8de2550122d0998cb47292be0bbd2e3f203bf/.travis.yml#L4-L14

Optimistic locking in Rails

Tagged rails, edit conflict, optimistic lock, optimistic locking  Languages ruby, slim

This code is an adaptation of the code found in this Railscasts article: http://railscasts.com/episodes/59-optimistic-locking-revised?view=asciicast

app/models/concerns/optimistic_locking.rb

module OptimisticLocking
  extend ActiveSupport::Concern

  included do
    validate :handle_conflict, on: :update
  end

  def original_updated_at
    @original_updated_at || updated_at.to_f
  end
  attr_writer :original_updated_at

  def handle_conflict
    return unless @conflict || updated_at.to_f > original_updated_at.to_f

    @conflict = true
    # By setting original_updated_at to nil the edit conflict is ignored on a second form submit
    @original_updated_at = nil

    errors.add :base, I18n.t('activerecord.edit_conflict.message')
    changes.each do |attribute, values|
      errors.add(
        attribute,
        I18n.t('activerecord.edit_conflict.was', value: values.first)
      )
    end
  end
end

app/models/project.rb

class Project < ApplicationRecord
  include OptimisticLocking

app/views/_edit_conflict.slim

- object.errors[:base]&.each do |error|
  p.text-danger
    = icon('fas', 'exclamation-circle')
    =< error

app/views/projects/_form.slim

= form @project do |f|
  = render '/edit_conflict', object: @project
  = f.hidden_field :original_updated_at
  = f.text_field :name, class: 'form-control'
  = form_buttons(f, @project)

How to use Rails' form_for with a PostgreSQL array

Tagged array, postgresql, string, form_for  Languages ruby

Using Rails’ form_for with a PostgreSQL array, for example, user roles can be done as follows:

Migration:

class CreateProjectMembers < ActiveRecord::Migration[6.0]
  def change
    create_table :project_members, id: :uuid do |t|
      ...
      t.string :roles, array: true, default: []

Model:

class ProjectMember < ApplicationRecord
  validate :validate_roles
  ROLES = %w[administrator collaborator].freeze

  def validate_roles
    return if roles.is_a?(Array) && roles.all? { |d| ROLES.include?(d) }

    errors.add(:roles, :invalid)
  end
end

View (slim-lang):

= form @project_member do |f|
  .form-group
    = f.label :roles, 'Roles'
    = f.check_box :roles, { label: 'Administrator', multiple: true }, 'administrator', nil
    = f.check_box :roles, { label: 'Collaborator', multiple: true }, 'collaborator', nil

Controller:

class ProjectMembersController < ApplicationController
  ...
  def update
    if @project_member.update(project_member_params)
    ...
  end

  def project_member_params
    params.require(:project_member).permit(roles: [])
  end

Note that if you have a “has_many through” relationship, instead of a PostgreSQL array, you can simply use:

= f.collection_check_boxes(:locale_ids, Locale.all, :id, :language) do |b|
  b.check_box

Reference: https://apidock.com/rails/v6.0.0/ActionView/Helpers/FormOptionsHelper/collection_check_boxes