Author Archives: Adam Z

Agile Estimation Tools

What’s the best online story estimation tool you ask? Well, Story Estimate sounds like one of the best options out there. Sure, I may be a little biased, since I wrote it, but still, it’s pretty good.

Nonetheless, I’ll pretend to be impartial and give a “fair” overview of several options.

  • Story Estimate – free, presents a modern UI, has no known vulnerabilities. It allows to use multiple estimation techniques (planning poker, T-shirt sizes…), including custom ones (specify whatever you want for your votes). Registered users can see additional statistics, including how their vote compare to the rest of the team. Simple to use. Best of all, it is written by me. What not to like?
  • Pointing Poker – a little outdated UI, but a pretty nice tool, also free. Unfortunately, it is open to JavaScript injection. Which, in a way, can be thought of as a feature (you can inject some fun JavaScript scripts), as long as you can trust your teammates not to inject any harmful scripts.
  • Planning Poker – the original planning poker. Free tier is limited (up to 5 players), paid version has some nice integrations with other tools (JIRA, TFS…). It’s a little more complicated, takes a little longer to setup, requires a user account.
  • PlanITPoker – Doesn’t require an account, although you can create one. It is a little more involved to setup, but not bad in terms of functionality.
  • Firepoker – Another free and open source tool. Extra points for being open source on GitHub. It’s a little limited in terms of the points you can select (offers two presets – powers of 2 and a slightly modified Fibonacci sequence). Also open to JavaScript injection.

RSpec, Action Cable, and Capybara (As of Rails 5.1.2)

Gems in Gemfile:

group :test, :development do
  gem 'database_cleaner'
  gem "rspec-rails", "~> 3.6.0"
  gem 'selenium-webdriver'

group :test do
  gem "capybara", "~> 2.14.0"


config.use_transactional_fixtures = false

  config.before(:suite) do

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction

  config.before(:each, js: true) do
    DatabaseCleaner.strategy = :truncation

  config.before(:each) do

  config.after(:each) do

  #Puma makes it possible to run RSpec with ActionCable
  Capybara.server = :puma

  Capybara.register_driver :selenium_chrome do |app|, browser: :chrome)
  Capybara.javascript_driver = :selenium_chrome

And, the driver:

brew install chromedriver

Setting up a cron job on Mac

In this example, we’re setting up a job, which will run at 08:00 am daily and clean rails logs

Step 1 – create a file with what we want to run (


cd ~/code/rails_project
/Users/username/.rbenv/shims/bundle exec rake log:clear

Setp 2 – set up the cron scheduler:

$ env EDITOR=vim crontab -e

# Add the line to run the script at 8am daily:

0 8 * * * sh ~/scripts/ >/tmp/stdout.log 2>/tmp/stderr.log

Configuring ESLint and Prettier in Atom

Install eslint and optional plugins:

npm install -g eslint eslint-plugin-react

Install atom plugins:


Create .eslintrc either in the root of your application (or your root path if you want it to be global)

Add simple configuration rules:

    "plugins": [
    "parserOptions": {
        "ecmaVersion": 6,
        "sourceType": "module",
        "ecmaFeatures": {
            "jsx": true
    "env": {
        "es6":     true,
        "browser": true,
        "node":    true,
        "mocha":   true
    "extends": [
    "rules": {

Optionally, update atom prettier-atom to format on save (in plugin settings)

Deploying Rails Action Cable to Linux

Selected VPS: Linode, 1GB Ram, 20 GB SSD, 1 TB transfer
OS: Ubuntu 18.04 LTS
App server: Passenger
Web Server: Ngnix

If you’d like to try Linode, I would greatly appreciate using this referral link – Linode: SSD Cloud Hosting & Linux Servers


First step – boot your server from Linode Manager Dashboard and ssh into it

General Server Updates

apt-get update && apt-get upgrade

#If desired to run auto-updates
apt-get install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

Set timezone:

dpkg-reconfigure tzdata
sudo service rsyslog restart



sudo apt-get install fail2ban

awk '{ printf "# "; print; }' /etc/fail2ban/jail.conf | sudo tee /etc/fail2ban/jail.local
vim /etc/fail2ban/jail.conf

uncomment sshd section and add
enabled = true

sudo apt-get install sendmail iptables-persistent
sudo service fail2ban start

Firewall ( allow established connections, traffic generated by the server itself, traffic destined for our SSH and web server ports. We will drop all other traffic):

sudo service fail2ban stop
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
sudo iptables -A INPUT -j DROP

#easy way to rate-limit ssh with ufw:
# technically, we could do all of the iptables stuff with ufw
ufw enable
ufw limit ssh

If using IPv6:

ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT # (replace with your undisclosed port)
ip6tables -A INPUT -p icmpv6 -j ACCEPT
ip6tables -A INPUT -j REJECT
ip6tables -A FORWARD -j REJECT

View iptables rules:

sudo iptables -S

Save iptables rules:

sudo dpkg-reconfigure iptables-persistent
sudo service fail2ban start

Create a deploy user

sudo adduser deploy
sudo adduser deploy sudo
su deploy

Installing Ruby (2.4)


sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev python-software-properties libffi-dev nodejs

git clone ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL

git clone ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
exec $SHELL

rbenv install 2.4.0
rbenv global 2.4.0
ruby -v

gem install bundler


sudo apt-key adv --keyserver hkp:// --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

# Add Passenger APT repository
sudo sh -c 'echo deb xenial main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

# Install Passenger & Nginx
sudo apt-get install -y nginx-extras passenger

Nginx update for Ubuntu 17.10 zesty

sudo apt-key adv --keyserver hkp:// --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

# Add our APT repository
sudo sh -c 'echo deb zesty main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update
# Install Passenger + Nginx module
sudo apt-get install -y libnginx-mod-http-passenger libnginx-mod-http-headers-more-filter nginx

Nginx update for Ubuntu 18.04 bionic

sudo sh -c 'echo deb bionic main > /etc/apt/sources.list.d/passenger.list'

sudo apt-get update
sudo apt-get install -y libnginx-mod-http-passenger

Block anything you don’t want in Nginx

vim /etc/nginx/sites-enabled/default
location ~ ^/(wp-admin|wp-content|wp-login) {
 deny all;

Set Cache headers

location ^~ /assets/ {
        gzip_static on;
        expires 1d;
        add_header Cache-Control "public";

location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
  expires 30d;
  access_log off;
  add_header Cache-Control "public";

Prevent access by IP

vim /etc/nginx/sites-enabled/default
server {
        listen 80;
        listen 443;
        server_name _put_server_ip_here;
        return 404;

Start Nginx:
sudo service nginx start

Nginx config:
sudo vim /etc/nginx/nginx.conf
Uncomment Phusion config – include /etc/nginx/passenger.conf;

Update passenger config:
sudo vim /etc/nginx/passenger.conf

passenger_ruby /home/deploy/.rbenv/shims/ruby;
passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
#passenger_ruby /usr/bin/passenger_free_ruby;

Restart Nginx
sudo service nginx restart

Test Nginx Config with gixy

We will need python-pip in order to install gixy:

apt-get install python-pip
pip install --upgrade pip
pip -V

pip install gixy



sudo apt-get install postgresql postgresql-contrib libpq-dev

Postgres update for Ubuntu 17.10

sudo apt-get install postgresql-9.6
sudo apt-get install python-psycopg2
sudo apt-get install libpq-dev

Postgres update for Ubuntu 18.04

touch /etc/apt/sources.list.d/pgdg.list
sudo sh -c 'echo deb bionic-pgdg main > /etc/apt/sources.list.d/pgdg.list'

wget --quiet -O - | sudo apt-key add -
sudo apt-get update

apt-get install postgresql-10

#Migrate Postresql from 9.6 to 10
sudo pg_dropcluster 10 main --stop
sudo systemctl stop postgresql 
sudo pg_upgradecluster  9.6 main
sudo pg_dropcluster 9.6 main --stop

Ensure UTF-8 support before creating a new database
vim /etc/profile.d/
Add the following if not already there:

export LANGUAGE="en_US.UTF-8"
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"

Create a Database with a user (Make sure to change the app name):

sudo su - postgres
createuser --pwprompt deploy
sudo su - deploy
create database your_db_name_production_new with owner=deploy encoding='UTF-8';


Creating dumps

pg_dump db_name_production -U deploy -h localhost > db_name_production_backup
# To restore:
psql -d db_name_production -f db_name_production_backup

#if you want to scp it from somewhere else(assuming dumps are in ~/dumps):
scp -r [email protected]_ip:~/dumps ~/destination_dumps


On your local machine add to your Gemfile:

gem 'capistrano', '~> 3.7', '>= 3.7.1'
gem 'capistrano-passenger', '~> 0.2.0'
gem 'capistrano-rails', '~> 1.2'

group :production do
  gem 'capistrano-rbenv', '~> 2.1'

Generate configs:

cap install STAGES=production

Update Capfile:

# If you are using rbenv add these lines:
# require 'capistrano/rbenv'
# set :rbenv_type, :user
# set :rbenv_ruby, '2.4.0'

Update deploy.rb:

set :application, "my_app_name"
set :repo_url, "[email protected]:me/my_repo.git"

set :deploy_to, '/home/deploy/my_app_name'

append :linked_files, "config/database.yml", "config/secrets.yml"
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "vendor/bundle", "public/system", "public/uploads"

Update production.rb with the server IP:
server ‘’, user: ‘deploy’, roles: %w{app db web}

Had to manually create database.yml and secrets.yml for now in /home/deploy/my_app_name/shared/config and copy connection data. This should be automated later

Update /etc/nginx/sites-enabled/default

server {
        listen 80;
        listen [::]:80 ipv6only=on;

        passenger_enabled on;
        rails_env    production;
        root         /home/deploy/my_app_name/current/public;
#action cable config:
        location /cable {
                 passenger_app_group_name app_name_websocket;
                 passenger_force_max_concurrent_requests_per_process 0;


sudo apt-get install redis-server

sudo service nginx restart

Time Synchronization

sudo apt-get update
sudo apt-get install ntp

systemctl start ntp.service
sudo ntpq -p

Let’s Encrypt (optional step)

Install certbot

apt-get install software-properties-common
add-apt-repository ppa:certbot/certbot
apt-get update
apt-get install certbot

Add to /etc/nginx/sites-available/default (this may be necessary for the Webroot plugin, which we’ll use to generate the certs)

location ^~ /.well-known/acme-challenge {
  root /var/www/html;
  default_type "text/plain";
  try_files $uri =404;

Verify config and restart:

nginx -t		
systemctl restart nginx

Get your webroot path from /etc/nginx/sites-available/default

Generate certs(replace domain an dweb-root-path):

certbot certonly --webroot --webroot-path=/var/www/html -d -d

Add a snippet:
vim /etc/nginx/snippets/
Paste the following:

ssl_certificate /etc/letsencrypt/live/;
ssl_certificate_key /etc/letsencrypt/live/;

And another one for ssl params:
vim /etc/nginx/snippets/ssl-params.conf

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver valid=300s;
resolver_timeout 5s;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

Update nginx config (this will allow both http and https):
vim /etc/nginx/sites-available/default

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;

    include snippets/;
    include snippets/ssl-params.conf;

Automate certificate renewal:
crontab -e

15 1 * * * /usr/bin/certbot renew --quiet --renew-hook "/bin/systemctl reload nginx"
20 1 * * * nginx -s reloadv

The above will run at 1:15 am every day, then reloads nginx config at 1:20 am.

To view certs expiration dates:

apt-get install  ssl-cert-check
ssl-cert-check -c /etc/letsencrypt/live/

Additional Mac Setup

brew install ssh-copy-id
ssh-copy-id [email protected]
This will allow you to ssh without a password the next time you ssh.

Useful logs

cat /var/log/redis/redis-server.log
sudo cat /var/log/fail2ban.log
sudo cat /var/log/nginx/error.log

fail2ban grouped by IP:

awk '($(NF-1) = /Ban/){print $NF}' /var/log/fail2ban.log | sort | uniq -c | sort -n

Useful monitoring commands

Ram usage:
free -m

df -ah
du -hs * | sort -h

Another way to get space info
apt-get install ncdu
ncdu /

Network activity:
netstat -tulpn

Linode ipv6 issue

If you can’t get apt-get to work, try to update /etc/gai.conf and uncomment line 54: precedence ::ffff:0:0/96 100

Linode Monitoring – Longview

Linode also provides Longview – web UI to access your server stats. The free version is limited to the last 24 hours.
Here is the official guide
I had to update iptables rules and restart the longview service:

iptables -I INPUT -s -j ACCEPT
iptables -I OUTPUT -d -j ACCEPT 
sudo service longview restart

Useful links:

Now, while it’s fun figuring these things out, git push heroku master sounds a lot easier.

Setting up Personal VPN Server

This is my second attempt at setting up a VPN service. This this time with lessons learned.
Server provider: RamNode
Price: $3/month
Virtualization: KVM
Specs: 512 GB RAM, 10 GB Storage, 1000 GB Bandwidth
Chosen OS: CentOS 7

After you set up your server, ssh into it.

Optionally, install and enable firewall for better security. Better to do before you run the vpn script. Otherwise, will have to manually update firewall rules.

yum update
# If you want vim:
yum search vim
yum install vim-enhanced.x86_64
yum install firewalld

systemctl enable firewalld
systemctl start firewalld

#Show configuration for the given zone:
firewall-cmd --zone=public --list-all

# add a port for your ssh console. Recommended to change from default 12
firewall-cmd --add-port 1234/tcp
firewall-cmd --add-port 1234/tcp --permanent
vim /etc/ssh/sshd_config
# update Port to 1234 (or whatever you picked above)
# Uncomment Protocol 2 - it provides better security
service sshd restart

# this retrieves the vpn script - see
wget -O && bash

# if you plan to run VPN on port 443
firewall-cmd --zone=public --add-port=443/udp
firewall-cmd --permanent --zone=public --add-port=443/udp
vim /etc/openvpn/server.conf
#add "port 443" or change to 443 if there is already an entry

#in the generated .ovpn file make the same change for the port
# should look something like this: remote your_server_ip 443

# additional security feature (not necessary for VPN)
yum install epel-release
yum install fail2ban
yum install fail2ban-systemd
cp -pf /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
systemctl enable fail2ban
vim /etc/fail2ban/jail.local
#set to somehting like this
# ban hosts for 300 seconds:
bantime = 300
maxretry = 2

port = your_ssh_port
enabled = true

#Override /etc/fail2ban/jail.d/00-firewalld.conf:
banaction = iptables-multiport

#if SELinux is enabled (check with sestatus)
port     = ssh
logpath  = %(auditd_log)s
enabled = true

#Check logs:
systemctl restart fail2ban

# if you need more users, just execute the script again

This generates a single some_name.ovpn file that you can download and use with an OpenVPN client. Hopefully, you now have a personal VPN server for roughly $30 a year.

Disabling ipv6
If you’re not planning on using ipv6, may as well disable it. Unfortunately fail2ban doesn’t support it yet.

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1

Multiple instances of OpenVPN

If you want to run multiple instances of vpn (say a typical 1194 udp and another one on 443 tcp to avoid blocking)
Add a new config in /etc/openvpn
For simplicity, you can copy existing server.conf and lets name it server2.conf.

Update server2.conf to use a different port and change the server ip (let’s say if original server.conf has ip of, let’s give the new config

port 443
proto tcp

Start your new openvpn instance:

systemctl start [email protected]_443_tcp

If using firewalld, add new rules:

firewall-cmd --zone=trusted --add-source=
firewall-cmd --permanent --zone=trusted --add-source=
firewall-cmd --direct --add-rule ipv4 nat POSTROUTING 0 -s -j SNAT --to your_server_ip
firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -s -j SNAT --to your_server_ip

Restart your new openvpn server:

systemctl restart [email protected]_443_tcp

If you’d like to use the same .ovpn config on the client side to connect to both ports, add this to client config:

server-poll-timeout 4
remote your_ip 1194 udp
remote your_ip 443 tcp
remote your_ip 1194 udp
remote your_ip 1194 udp

This will try to connect to 1194 udp first, then if unsuccessful, it’ll try 443 tcp, then 1194
In order for this to work, both configs on the server must have the same settings (except for IP’s of course)

If using OpenDNS head to and set it up

Auto-update all CentOS packages

yum -y install yum-cron

vim /etc/yum/yum-cron.conf
# set:
# download_updates = yes
# apply_updates = yes
sudo systemctl enable yum-cron.service
systemctl start yum-cron

Harden SSH
vim /etc/ssh/sshd_config

Ciphers aes128-ctr,aes192-ctr,aes256-ctr
KexAlgorithms ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256
MACs hmac-sha2-256,hmac-sha2-512

A nice resource to test the speed:

Phoenix and Ecto. First Steps

Valid as of Phoenix 1.2

Show routes:
mix phoenix.routes

Generating resources:
mix phoenix.gen.html Post posts –no-model
mix phoenix.gen.json Post posts


  • :string
  • :integer
  • :map
  • :binary_id
  • :float
  • :boolean

Writing Queries
Two ways

  1. Query
    import Ecto.Query
    from p in context.Posts
    where p.Title.Contains("Stuff")
    select p;
  2. Expression
    |> where(titlle: "Stuff")
    |> limit(1)

Making changes –
changeset = Post.changeset(post, %{title: “updated”})

Generate migration:
mix ecto.gen.migration [migration_name] -r [repo]

Generate schema:
mix phoenix.gen.model [Schema] [table] [fields] -r [repo]

Run/Rollback migration
mix ecto.migrate -r [repo]
mix ecto.rollback -r [repo]

Generate migration:
Does not generate schema module:
mix ecto.gen.migration [migration_name] -r [repo]

Generates both schema model and a migration:
mix phoenix.gen.model [Schema] [table] [fields] -r [repo]

To avoid specifying repo all the time:
config :my_app, ecto_repos: [MyApp.Repo]

Starting with Phoenix. Installation.

Prerequisites (valid as of Phoenix 1.2)


  • Elixir – brew install elixir
  • node – brew install node
  • Postgres –

For Windows, can use Chocolatey to install node and Elixir. Postgres seems to be outdated as of time of this post. Or download from source and install:

Add to Windows path PostgreSQL (dependent on PostgreSQL version), so we can use psql on command line –
C:\Program Files\PostgreSQL\9.6\bin
C:\Program Files\PostgreSQL\9.6\lib

Install phoenix:

$ mix archive.install

Alternatively, download a specific version from

Generate and run a new project:

$ mix my_first_app
$ mix ecto.create
$ npm install
$ iex -S mix phoenix.server
iex> :observer.start

Developing a Simple app with Action Cable and Devise on Heroku

As I continue to learn Rails, this is my experience developing a Rails application using Action Cable (allows communication using web sockets)

Finished product: Story Estimate

Assuming we already generated a new Rails app, lets start with authentication using Devise gem.
Add “gem ‘devise'” to your gem file.

Run a few commands to generate your users:
$ rails generate devise:install
$ rails generate devise User
$ rails generate devise:views
$ rails db:migrate

For the sake of this application, I want to enable guest users. Lets allow that:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def current_or_guest_user
    current_user || guest_user

  # find guest_user object associated with the current session,
  # creating one as needed
  def guest_user(with_retry = true)
    # Cache the value the first time it's gotten.
    @cached_guest_user ||= User.find(session[:guest_user_id] ||=

  rescue ActiveRecord::RecordNotFound # if session[:guest_user_id] invalid
    session[:guest_user_id] = nil
    guest_user if with_retry


  def create_guest_user
    u = User.create(email: "guest_#{}#{rand(100)}")!(validate: false)
    session[:guest_user_id] =

Now, if we want to sign in a user as a guest, we’d call sign_in(current_or_guest_user)

Next thing, we want to authorize our Action Cable connections with Devise users. In order to do that, we need to add some hooks in the config/initialazers. This will add current user id to user’s cookie:


Warden::Manager.after_set_user do |user,auth,opts|
  scope = opts[:scope]
  auth.cookies.signed["#{scope}.id"] =

Warden::Manager.before_logout do |user, auth, opts|
  scope = opts[:scope]
  auth.cookies.signed["#{scope}.id"] = nil

With this, we can configure rails connection:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user

      def find_verified_user
        if verified_user = User.find_by(id: cookies.signed[''])

Now, time to generate a new channel:
rails g channel channel_name optional_method
It may look someting like this:

class CoolChannel < ApplicationCable::Channel
  BASE_CHANNEL = 'BaseChannel'.freeze

  def subscribed
    stream_from "#{BASE_CHANNEL}#{params['some_key']}"

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed

  def some_action(data)
    value_from_client = data['value_0']

This will create a new custom-named channel based on whatever we want to send in params from the client. some_action is the method that will be called by the clients to send a message. In this case we just rebroadcast the message to all clients.

To make a call from the client to the server, go to the .js or .coffee file for the earlier generated channel (should be in javascripts/channels). We can call back to the server:

jQuery(document).on 'turbolinks:load', ->
  session_slug = $('#session_slug').val()
  player_name = $('#player_id').val()
  $stats =$('#statistics')

  App.estimation_session = App.cable.subscriptions.create {
    channel: "CoolChannel",
    connected: ->

    disconnected: ->
      # Called when the subscription has been terminated by the server

    received: (data) ->
      value_from_server = data.value_0

    some_action: (value_0, value_1)->
      @perform 'some_action',
               value_0: value_0,
               value_1: value_1

When deploying to Heroku, we need to Add Redis To Go add-in (or any other Redis add-in such as Heroku Redi. Then update cable.yml to use redis:
adapter: redis

To find Redis connection URL, run:
heroku config | grep redis

Also, we need to add redis gem:
gem ‘redis’, ‘~> 3.2’

git push heroku master

Some useful resources:

Ruby Inheritance Rules Simplified

A simple principle to understand Ruby inheritance is LIFO (Last In First Out).


  1. Ruby will search the object you’re calling it from
  2. It will search it in any included modules, starting with the last loaded module
  3. It will go in the superclass (technically, modules get in the inheritance hierarchy as well, but whatever)

When the method is found, the search stops.


A Note About Calling super.
The same rule applies if you use super. If you have the same method defined in both module and a parent class, the module method will be called.
Here’s a simple example:

module Test
  def abc
    puts 'abc from Module'

class TestClassParent
  def abc
    puts 'abc from parent'

class TestClassChild &amp;lt; TestClassParent
  include Test

  def abc

Running this will result in:

abc from Module