Riding the Rails
@
DigitalOcean
• Shared business logic as an engine
• Form objects
• MySQL table as KV store
• Abusing ASN for cleanliness
gem 'core',
git: 'https://githubenterprise/digitalocean/core',
ref: '623ac1e785e092f7369d5cfa7e56ea2e98fb2e20'
“Core”
Our Rails Engine in Reverse
“Core”
Our Rails Engine in Reverse
gem 'core',
git: 'https://githubenterprise/digitalocean/core',
ref: '623ac1e785e092f7369d5cfa7e56ea2e98fb2e20'
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
config.to_prepare do
Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
require_dependency(c)
end
end
end
end
# MyApp/app/decorators/models/blorgh/post_decorator.rb
Blorgh::Post.class_eval do
def time_since_created
Time.current - created_at
end
end
:thumbsdown:
# MyApp/app/models/blorgh/post.rb
class Blorgh::Post < ActiveRecord::Base
include Blorgh::Concerns::Models::Post
def time_since_created
Time.current - created_at
end
def summary
"#{title} - #{truncate(text)}"
end
end
:thumbsdown:
# Blorgh/lib/concerns/models/post
module Blorgh::Concerns::Models::Post
extend ActiveSupport::Concern
# 'included do' causes the included code to be evaluated in the
# context where it is included (post.rb), rather than being
# executed in the module's context (blorgh/concerns/models/post).
included do
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_save :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
end
end
require_dependency Core::Engine.root.join('app', 'models', 'droplet').to_s
class Droplet
BACKUP_CHARGE_PERCENTAGE = 0.20
def monthly_backup_price
self.size.monthly_price * BACKUP_CHARGE_PERCENTAGE
end
end
:thumbsup:
Previously...
# app/controllers/events_controller.rb
event = Event.new(
:event_scope => 'droplet',
:event_type => params[:event_type], # from the route
:droplet_id => params[:droplet_id],
:image_id => params[:image_id],
:size_id => params[:size_id],
:user_id => current_user.id,
:name => name
)
# app/models/event.rb
# Validations Based on Event Type
case self.event_type
when 'resize'
errors.add(:size_id, "...") if size_id == droplet.size_id
errors.add(:size_id, "...") unless Size.active.include? size_id
end
class Resize
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
include Virtus
attribute :size, Size
attribute :user, User
attribute :droplet, Droplet
validates :size,
allowed_size: true,
presence: true
def save
if valid?
event.save!
else
false
end
end
def persisted?
false
end
def event
@_event ||= EventFactory.build(:resize, event_params)
end
end
# app/views/resizes/_form.erb
= form_for [@droplet, Resize.new] do |f|
= render 'sizes', sizes: @sizes_available_for_resize, form: f
= f.submit 'Resize'
class ResizesController < ApplicationController
def create
droplet = current_user.droplets.find(params[:droplet_id])
size = Size.active.where(id: params[:size_id]).first
resize = Resize.new(droplet: droplet, size: size)
if resize.save
redirect_to resize.droplet, notice: 'Your resize is processing'
else
redirect_to resize.droplet, alert: resize.errors.first
end
end
end
<3Virtus
• Validations are contextual
• Slims down god objects
• Can be tested without ActiveRecord
• Makes testing ActiveRecord classes easier
WARNING:
YOU
PROBABLY
SHOULD
NOT
DO
THIS
CREATE TABLE `user_properties` (
`user_id` int(11) NOT NULL,
`name` varchar(32) NOT NULL DEFAULT '',
`value` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`user_id`,`name`),
KEY `name` (`name`,`value`),
KEY `user_id` (`user_id`,`name`,`value`)
)
class User
include Propertable
property :marked_as_sketchy_at, nil, :date_time
property :marked_as_verified_at, nil, :date_time
property :marked_as_abuse_at, nil, :date_time
property :marked_as_hold_at, nil, :date_time
property :marked_as_suspended_at, nil, :date_time
property :marked_as_review_at, nil, :date_time
end
module Propertable
def property(name, default, type = :string)
key = name.to_s
props = class_variable_get(:@@properties)
props[key] = default
define_method(key) do
property = __read_property__(key)
if property.nil? || property == default
default
else
Propertable::Coercer.call(property.value, property.value.class, type)
end
end
define_method("#{key}?") do
!!public_send(key)
end
define_method("#{key}=") do |value|
begin
coerced_value = Propertable::Coercer.call(value, value.class, type)
rescue
coerced_value = default
end
coerced_value = Propertable::Coercer.call(coerced_value, type.class, :string)
property = __write_property__(key, coerced_value)
end
end
end
ASN = ActiveSupport::Notifications
INSTRUMENT
EVERYTHING!
$statsd = Statsd.new(statsd_ip).tap { |s| s.namespace = Rails.env }
# Request Times
ActiveSupport::Notifications.subscribe /process_action.action_controller/ do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
status = event.payload[:status]
key = "requests.cloud"
$statsd.timing "#{key}.time.total", event.duration
$statsd.timing "#{key}.time.db", event.payload[:db_runtime]
$statsd.timing "#{key}.time.view", event.payload[:view_runtime]
$statsd.increment "#{key}.status.#{status}"
end
# SQL Queries
ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
key = 'queries.cloud'
$statsd.increment key
$statsd.timing key, event.duration
end
module Instrumentable
extend ActiveSupport::Concern
included do
build_instrumentation
end
module ClassMethods
def build_instrumentation
key = name.underscore + '.callbacks'
after_commit(on: :create) do |record|
ASN.instrument key, attributes: record.attributes, action: 'create'
end
after_rollback do |record|
ASN.instrument key, attributes: record.attributes, action: 'rollback'
end
end
end
end
ActiveSupport::Notifications.subscribe 'event.callbacks' do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
type_id = event.payload[:attributes]['event_type_id']
action = event.payload[:action]
EventInstrumenter.perform_async(type_id, action)
end
after_commit(on: :update) do |record|
ASN.instrument key,
attributes: record.attributes,
changes: record.previous_changes,
action: 'update'
end
class TicketNotifier < BaseNotifier
setup :ticket
def created(ticket)
notify_about_admin_generated_ticket!(ticket) if ticket.opened_by_admin?
end
def updated(ticket, changes)
if category = modification(changes, 'category')
notify_networking(ticket) if category == 'networking'
notify_about_escalated_ticket(ticket) if category == 'engineering'
end
end
private
# TODO: move to base
def modification(changes, key)
changes.has_key?(key) && changes[key].last
end
end
ESB = ASN = ActiveSupport::Notifications
We’re Hiring!
module Rack
class Audit
def call(env)
audit = Thread.current[:audit] = {}
audit[:request] = Rack::Request.new(env)
@app.call(env)
end
end
end

Digital Ocean Presentation - Ruby Dev Stackup - The Flatiron School

  • 1.
  • 2.
    • Shared businesslogic as an engine • Form objects • MySQL table as KV store • Abusing ASN for cleanliness
  • 3.
    gem 'core', git: 'https://githubenterprise/digitalocean/core', ref:'623ac1e785e092f7369d5cfa7e56ea2e98fb2e20' “Core” Our Rails Engine in Reverse
  • 4.
    “Core” Our Rails Enginein Reverse gem 'core', git: 'https://githubenterprise/digitalocean/core', ref: '623ac1e785e092f7369d5cfa7e56ea2e98fb2e20'
  • 5.
    # lib/blorgh/engine.rb module Blorgh classEngine < ::Rails::Engine isolate_namespace Blorgh config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end end end # MyApp/app/decorators/models/blorgh/post_decorator.rb Blorgh::Post.class_eval do def time_since_created Time.current - created_at end end :thumbsdown:
  • 6.
    # MyApp/app/models/blorgh/post.rb class Blorgh::Post< ActiveRecord::Base include Blorgh::Concerns::Models::Post def time_since_created Time.current - created_at end def summary "#{title} - #{truncate(text)}" end end :thumbsdown:
  • 7.
    # Blorgh/lib/concerns/models/post module Blorgh::Concerns::Models::Post extendActiveSupport::Concern # 'included do' causes the included code to be evaluated in the # context where it is included (post.rb), rather than being # executed in the module's context (blorgh/concerns/models/post). included do attr_accessor :author_name belongs_to :author, class_name: "User" before_save :set_author private def set_author self.author = User.find_or_create_by(name: author_name) end end end
  • 8.
    require_dependency Core::Engine.root.join('app', 'models','droplet').to_s class Droplet BACKUP_CHARGE_PERCENTAGE = 0.20 def monthly_backup_price self.size.monthly_price * BACKUP_CHARGE_PERCENTAGE end end :thumbsup:
  • 9.
    Previously... # app/controllers/events_controller.rb event =Event.new( :event_scope => 'droplet', :event_type => params[:event_type], # from the route :droplet_id => params[:droplet_id], :image_id => params[:image_id], :size_id => params[:size_id], :user_id => current_user.id, :name => name ) # app/models/event.rb # Validations Based on Event Type case self.event_type when 'resize' errors.add(:size_id, "...") if size_id == droplet.size_id errors.add(:size_id, "...") unless Size.active.include? size_id end
  • 10.
    class Resize extend ActiveModel::Naming includeActiveModel::Conversion include ActiveModel::Validations include Virtus attribute :size, Size attribute :user, User attribute :droplet, Droplet validates :size, allowed_size: true, presence: true def save if valid? event.save! else false end end def persisted? false end def event @_event ||= EventFactory.build(:resize, event_params) end end
  • 11.
    # app/views/resizes/_form.erb = form_for[@droplet, Resize.new] do |f| = render 'sizes', sizes: @sizes_available_for_resize, form: f = f.submit 'Resize' class ResizesController < ApplicationController def create droplet = current_user.droplets.find(params[:droplet_id]) size = Size.active.where(id: params[:size_id]).first resize = Resize.new(droplet: droplet, size: size) if resize.save redirect_to resize.droplet, notice: 'Your resize is processing' else redirect_to resize.droplet, alert: resize.errors.first end end end
  • 12.
    <3Virtus • Validations arecontextual • Slims down god objects • Can be tested without ActiveRecord • Makes testing ActiveRecord classes easier
  • 13.
  • 14.
    CREATE TABLE `user_properties`( `user_id` int(11) NOT NULL, `name` varchar(32) NOT NULL DEFAULT '', `value` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`user_id`,`name`), KEY `name` (`name`,`value`), KEY `user_id` (`user_id`,`name`,`value`) ) class User include Propertable property :marked_as_sketchy_at, nil, :date_time property :marked_as_verified_at, nil, :date_time property :marked_as_abuse_at, nil, :date_time property :marked_as_hold_at, nil, :date_time property :marked_as_suspended_at, nil, :date_time property :marked_as_review_at, nil, :date_time end
  • 15.
    module Propertable def property(name,default, type = :string) key = name.to_s props = class_variable_get(:@@properties) props[key] = default define_method(key) do property = __read_property__(key) if property.nil? || property == default default else Propertable::Coercer.call(property.value, property.value.class, type) end end define_method("#{key}?") do !!public_send(key) end define_method("#{key}=") do |value| begin coerced_value = Propertable::Coercer.call(value, value.class, type) rescue coerced_value = default end coerced_value = Propertable::Coercer.call(coerced_value, type.class, :string) property = __write_property__(key, coerced_value) end end end
  • 16.
  • 17.
  • 18.
    $statsd = Statsd.new(statsd_ip).tap{ |s| s.namespace = Rails.env } # Request Times ActiveSupport::Notifications.subscribe /process_action.action_controller/ do |*args| event = ActiveSupport::Notifications::Event.new(*args) status = event.payload[:status] key = "requests.cloud" $statsd.timing "#{key}.time.total", event.duration $statsd.timing "#{key}.time.db", event.payload[:db_runtime] $statsd.timing "#{key}.time.view", event.payload[:view_runtime] $statsd.increment "#{key}.status.#{status}" end # SQL Queries ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args| event = ActiveSupport::Notifications::Event.new(*args) key = 'queries.cloud' $statsd.increment key $statsd.timing key, event.duration end
  • 20.
    module Instrumentable extend ActiveSupport::Concern includeddo build_instrumentation end module ClassMethods def build_instrumentation key = name.underscore + '.callbacks' after_commit(on: :create) do |record| ASN.instrument key, attributes: record.attributes, action: 'create' end after_rollback do |record| ASN.instrument key, attributes: record.attributes, action: 'rollback' end end end end
  • 21.
    ActiveSupport::Notifications.subscribe 'event.callbacks' do|*args| event = ActiveSupport::Notifications::Event.new(*args) type_id = event.payload[:attributes]['event_type_id'] action = event.payload[:action] EventInstrumenter.perform_async(type_id, action) end
  • 22.
    after_commit(on: :update) do|record| ASN.instrument key, attributes: record.attributes, changes: record.previous_changes, action: 'update' end
  • 23.
    class TicketNotifier <BaseNotifier setup :ticket def created(ticket) notify_about_admin_generated_ticket!(ticket) if ticket.opened_by_admin? end def updated(ticket, changes) if category = modification(changes, 'category') notify_networking(ticket) if category == 'networking' notify_about_escalated_ticket(ticket) if category == 'engineering' end end private # TODO: move to base def modification(changes, key) changes.has_key?(key) && changes[key].last end end
  • 24.
    ESB = ASN= ActiveSupport::Notifications
  • 25.
  • 26.
    module Rack class Audit defcall(env) audit = Thread.current[:audit] = {} audit[:request] = Rack::Request.new(env) @app.call(env) end end end