Chris Morris
Sr. Engineer
@the_chrismo
4,000-plus-person company
Almost $400 million in revenue
About $1 billion in sales last year
Hundreds of millions in the b...
4 PACK OF LIGHTNING TALKS
COBBLER’S PRODUCTION
CONSOLE HAS NO SHOES
LIVINGSOCIAL PAYMENTS

•

Credit Card, PayPal, V.me,
MasterPass, International
(Adyen, iPay88)

•

1.5M API calls a day

•...
LIVINGSOCIAL PAYMENTS

•

Whole Foods - 1,000,000

•

Starbucks - 1,500,000
irb(main):004:0> Transaction.find_all_by_person_id 2
=> [#<SaleTransaction id: 5, credit_card_id: 47523892,
reference_tran...
[159] pry(main)> b=PB.from_people_ids 2
=> +----+-------------------------+------------+
| id |
created_at
| aasm_state |
...
irb(main):080:0> b.options
.=> [{"country"=>"US", "profile"=>nil,
"requires_cvv"=>nil, "order"=>nil,
"priority"=>nil, "use...
[33] pry(main)> b.options
=> [{"country"=>"US",
"profile"=>nil,
"requires_cvv"=>nil,
"order"=>nil,
"priority"=>nil,
"use_d...
class Batch < Array
def initialize(ary)
self << ary
flatten!
end
!
!

def method_missing(meth_id, *args)
self.map do |t|
t...
[14] pry(main)> b.map { |t| t.aasm_state }
=> ["processed", "processed"]
[15] pry(main)> b.map(&:type)
=> ["SaleTransactio...
[11] pry(main)> b.aasm_state
=> ["processed", "processed"]
[12] pry(main)> b.type
=> ["SaleTransaction", "RefundTransactio...
class PaymentsBatch < Batch
def self.from_people_ids(ary)
transactions = [ary].flatten.collect do |person_id|
Transaction....
class PaymentsBatch < Batch
def report
puts self.map do |t|
[t.id.to_s.ljust(5),
t.created_at.to_s.ljust(25),
t.aasm_state...
class PaymentsBatch < Batch
def report
self.map do |t|
[t.id, t.created_at, t.aasm_state]
end.to_text_table
end
end
!

[22...
class PaymentsBatch < Batch
def report
cols = [:id, :created_at, :aasm_state]
h = serializable_hash(only: cols)
rows = ([h...
[155] pry(main)> b=PB.from_people_ids 2
=> [#<SaleTransaction id: 5, credit_card_id: 47523892,
reference_transaction_id: n...
class Batch < Array
def pretty_inspect
report.to_s
end
end
[155] pry(main)> b=PB.from_people_ids 2
=> +----+-------------------------+------------+
| id |
created_at
| aasm_state |
...
class PaymentsBatch
def email
addr = 'you@re.awesome.com'
Mail::Message.new(to: addr,
from: addr,
subject: 'PaymentsBatch'...
LOG SEARCH
chris.morris@support01:$ RAILS_ENV=production bundle exec thor grep:payments -p '(PCI|SPT)
environment' -d 1
********** su...
chris.morris@support01:$ RAILS_ENV=production bundle exec thor grep:payments -p '(PCI|SPT)
environment' -d 1 -c --pry
****...
From: script/grep.payments.thor @ line 81 Thor::Sandbox::Grep#payments:

!

!

76:
77:
78:
79:
80:
=> 81:
82:
83:
84:
85:
...
[1] pry(#<Thor::Sandbox::Grep>)> chunks
=> [#<RequestChunk:0x00000004f39540
@action="POST",
@completed=true,
@controller="...
THE END
DIY MOCKS AND FIXTURES
RAVE REVIEWS

"You either get an F for
logic or an A+ for balls.”
!
!

— J. B. Rainsberger
author of JUnit Recipes
[~]$ curl -s http://www.linkedin.com/in/chrismo | grep Relevance
[~]$
NOT GLENN VANDERBURG =>
NOT GLENN VANDERBURG =>

!

CODMONGER
!
FactoryGirl.define do
factory :customer do
external_record_type 'person'
sequence(:external_id)
email Faker::Internet.emai...
describe Collection do
it "should not return old exported collection with end status" do
[:collected, :canceled, :refused]...
describe Collection do
it "should not return old exported collection with end status" do
[:collected, :canceled, :refused]...
class Collection < ActiveRecord::Base
def customer
purchase.customer
end
end

NoMethodError: undefined method `customer' f...
FactoryGirl.define do
...
# YOU HAD ONE JOB
factory :collection do
purchase
end
end
require 'machinist/active_record'
require 'faker'

!

Sham.sham_id { |i| i }

!

Customer.blueprint do
external_record_typ...
describe Collection, 'scope :problem_old_exports (machinist)' do
it "should not return old exported collection with end st...
describe Collection, 'scope :problem_old_exports (machinist)' do
it "should not return old exported collection with end st...
!

# ONE. JOB.
Collection.blueprint do
purchase
end
require 'faker'

!

class CodmongerFixture
def self.create(opts={})
record = self.new(opts); record.save!; record
end
end
...
describe Collection do
it "should not return old exported collection with end status" do
[:collected, :canceled, :refused]...
FactoryGirl::Proxy::ObjectWrapper

def object
@object ||= @klass.new
assign_object_attributes
@object
end
class Collection < ActiveRecord::Base
state_machine :status, :initial => lambda{ |collection|
collection.customer.new? ? :...
ACTIVE RECORD
IS
HARD ENOUGH
!
!

I DON’T NEED YOUR
FREEKING
DSL ON TOP OF IT
require 'faker'

!

class CodmongerFixture
def self.create(opts={})
record = self.new(opts); record.save!; record
end
end
...
FactoryGirl.define do
factory :customer do
external_record_type 'person'
sequence(:external_id)
email Faker::Internet.emai...
require 'faker'
!
class CodmongerFixture
def self.create(opts={})
record = self.new(opts); record.save!; record
end
end
!
...
class PurchaseFixture < CodmongerFixture
@@purchase_id = Purchase.last.external_id + 1 rescue 1
def self.new(opts={})
cust...
WHEN A DSL POOPS
OUT ON YOU
!

YOU CAN'T LOOK
AT ITS SOURCE CODE
!

BECAUSE
IT'S ALL META AND STUFF.
MOCK FRAMEWORKS
NOT ALL EXPECTATIONS WERE SATISFIED
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: Purchase(id: integ...
unexpected invocation: #<Mock:0x1022aff50>.reload()
satisfied expectations:
- allowed any number of times, not yet invoked...
purchase = stub()
purchase.stubs(:id).returns(123)
purchase.stubs(:quantity).returns(2)
!
deal = stub()
deal.stubs(:title)...
PurchaseStub = Struct.new(:id, :quantity, :deal, :person)
DealStub = Struct.new(:title, :price, :currency_unit)
PersonStub...
person = OpenStruct.new(id: 8675309,
email: 'chrismo@ls.com')
!

deal = OpenStruct.new(title: "Mr. Bones's Deal'",
price: ...
require 'ostruct'
!
person = OpenStruct.new(id: 8675309, email: 'chrismo@clabs.org',
available_credit: {'USD'=>5,'MYR'=>0}...
module PaymentProviders
module DoubleFake
class Payment
@@db = {}

!

!

!

!

def self.load_payment(id, params)
@@db[id]
...
DIY
—
Gem
BUILD
—
BUY
BUY
trades in

FAMILIARITY
TRANSPARENCY
THE END
TRACK YER BIG STUFF
No tengo ni idea de lo
que estoy haciendo
•

2500 translation keys

•

1/2 in use
dozens of translate calls / request
module Humperdink
class DirtySet
attr_reader :dirty, :clean, :config

!

!

!
!

!

!

def initialize(initial_contents=[],...
describe Humperdink::DirtySet do
let(:set) { Humperdink::DirtySet.new }
!

it 'should only append to dirty' do
set << ‘foo...
it 'should only append to dirty if not in clean' do
set << ‘foo'
set.dirty.should == ['foo'].to_set
set.clean.should == []...
it 'should initialize clean with initialize' do
set = Humperdink::DirtySet.new(['a', ‘b'])
set.dirty.should == [].to_set
s...
module Humperdink
class RedisDirtySet < DirtySet
def initialize(initial_content=[], config={})
super(initial_content, conf...
module Humperdink
class Tracker
attr_reader :set, :config

!

!

!

!

def initialize(set=Set.new, config={})
@set = set
@...
class KeyTracker
def initialize(redis, key)
redis_set = Humperdink::RedisDirtySet.new(:redis => redis,
:key => key,
:max_d...
module KeyTrackerBackend
def key_tracker
@key_tracker
end

!

!

def key_tracker=(value)
@key_tracker = value
end

def tra...
CONFIGURATION
module Humperdink
class DirtySet
!

def <<(value)
@dirty << value if !@clean.include?(value)
clean! if getting_messy?
end
...
module Humperdink
class DirtySet
!

def getting_messy?
if @config[:max_dirty_items] &&
@dirty.length > @config[:max_dirty_...
module Humperdink
class DirtySet
def initialize(initial_contents=[], config={})
@clean = Set.new(initial_contents)
@dirty ...
CONFIGURATION
RESQUE
module Resque
class Worker
def work(interval = 5.0, &block)
loop do
break if shutdown?
!
if not paused? and job = reserve
...
module Humperdink::ForkPiping
class KeyTracker
def initialize(redis, key)
redis_set = Humperdink::RedisDirtySet.new(:redis => redis,
:key => key,
:max_d...
def on_translate(locale, key, options = {})
begin
if @tracker.tracker_enabled
requested_key = normalize_requested_key(key,...
def on_translate(locale, key, options = {})
begin
if @tracker.not_forked_or_parent_process
if @tracker.tracker_enabled
req...
COVERBAND
HTTPS://GITHUB.COM/DANMAYER/COVERBAND
use Coverband::Middleware
module Coverband
class Middleware
!

def call(env)
configure_sampling
record_coverage
results = @app.call(env)
report_cove...
def configure_sampling
if (rand * 100.0) > @sample_percentage
@enabled = false
else
@enabled = true
end
end
def record_coverage
if @enabled
unless @function_set
set_trace_func proc { |event, file, line, id, binding, classname|
add...
module Coverband
class Middleware
!

def call(env)
configure_sampling
record_coverage
results = @app.call(env)
report_cove...
<= NOT DAN MAYER
THE END
ALL THE ANALOGIES
META
THE END
Chris Morris
Sr. Engineer
@the_chrismo
CREDITS
http://www.flickr.com/photos/tracy_olson/61056391/
http://imgs.xkcd.com/comics/the_general_problem.png
http://uploa...
Chris Morris
Sr. Engineer
@the_chrismo
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Big Ruby 2014: A 4 Pack of Lightning Talks
Upcoming SlideShare
Loading in …5
×

Big Ruby 2014: A 4 Pack of Lightning Talks

865 views

Published on

Make your production console a nice place to work.
DIY Fixtures and Mocks.
Runtime Analysis with Humperdink and Coverband.
ALL THE ANALOGIES.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Big Ruby 2014: A 4 Pack of Lightning Talks

  1. 1. Chris Morris Sr. Engineer @the_chrismo
  2. 2. 4,000-plus-person company Almost $400 million in revenue About $1 billion in sales last year Hundreds of millions in the bank — TIM O'SHAUGHNESSY, CEO
 CNNMONEY - JAN 2014
  3. 3. 4 PACK OF LIGHTNING TALKS
  4. 4. COBBLER’S PRODUCTION CONSOLE HAS NO SHOES
  5. 5. LIVINGSOCIAL PAYMENTS • Credit Card, PayPal, V.me, MasterPass, International (Adyen, iPay88) • 1.5M API calls a day • 70k-150k credit card transactions a day
  6. 6. LIVINGSOCIAL PAYMENTS • Whole Foods - 1,000,000 • Starbucks - 1,500,000
  7. 7. irb(main):004:0> Transaction.find_all_by_person_id 2 => [#<SaleTransaction id: 5, credit_card_id: 47523892, reference_transaction_id: nil, amount: #<BigDecimal: 7fb60bc0ad78,'0.2E2',9(18)>, response: nil, transaction_type: "sale", aasm_state: "processed", created_at: "2014-02-04 19:42:57", updated_at: "2014-02-04 19:43:05", type: "SaleTransaction", callback_queue_name: "medium", callback_queue_application: "pry", options: "{"country":"US","profile":null,"requires_cvv ":null,...", processor_authorization_code: "illoearumi", gateway_transaction_id: "excepturiq", success: true, failure_reason: nil, avs_response_code: "M", cvv_response_code: "M", scope_identifier: nil, person_id: 2>, #<SaleTransaction id: 6, credit_card_id: 47523892, reference_transaction_id: nil, amount: #<BigDecimal:7fb60bc0a828,'0.2E2',9(18)>, response: nil, transaction_type: "sale", aasm_state: "processed", created_at: "2014-02-04 19:43:11", updated_at: "2014-02-04 19:43:20", type: "SaleTransaction", callback_queue_name: "medium", callback_queue_application: "pry", options: "{"country":"US", "profile":null,"requires_cvv":null,...", processor_authorization_code: "errorconse", gateway_transaction_id: "eumauttene", success: true, failure_reason: nil, avs_response_code: "M", cvv_response_code: "M", scope_identifier: nil, person_id: 2>]
  8. 8. [159] pry(main)> b=PB.from_people_ids 2 => +----+-------------------------+------------+ | id | created_at | aasm_state | +----+-------------------------+------------+ | 5 | 2014-02-04 19:42:57 UTC | processed | | 6 | 2014-02-04 19:43:11 UTC | processed | +----+-------------------------+------------+
  9. 9. irb(main):080:0> b.options .=> [{"country"=>"US", "profile"=>nil, "requires_cvv"=>nil, "order"=>nil, "priority"=>nil, "use_deal_bucks"=>nil, "requires_avs"=>nil, "charge_distribution_strategy"=>nil, "device_data"=>nil}, {"country"=>"US", "profile"=>nil, "requires_cvv"=>nil, "order"=>nil, "priority"=>nil, "use_deal_bucks"=>nil, "requires_avs"=>nil, "charge_distribution_strategy"=>nil, "device_data"=>nil}]
  10. 10. [33] pry(main)> b.options => [{"country"=>"US", "profile"=>nil, "requires_cvv"=>nil, "order"=>nil, "priority"=>nil, "use_deal_bucks"=>nil, "requires_avs"=>nil, "charge_distribution_strategy"=>nil, "device_data"=>nil}, {"country"=>"US", "profile"=>nil, "requires_cvv"=>nil, "order"=>nil, "priority"=>nil, "use_deal_bucks"=>nil, "requires_avs"=>nil, "charge_distribution_strategy"=>nil, "device_data"=>nil}]
  11. 11. class Batch < Array def initialize(ary) self << ary flatten! end ! ! def method_missing(meth_id, *args) self.map do |t| t.send(meth_id, *args) end end end
  12. 12. [14] pry(main)> b.map { |t| t.aasm_state } => ["processed", "processed"] [15] pry(main)> b.map(&:type) => ["SaleTransaction", "RefundTransaction"]
  13. 13. [11] pry(main)> b.aasm_state => ["processed", "processed"] [12] pry(main)> b.type => ["SaleTransaction", "RefundTransaction"]
  14. 14. class PaymentsBatch < Batch def self.from_people_ids(ary) transactions = [ary].flatten.collect do |person_id| Transaction.find_all_by_person_id(person_id) end PaymentsBatch.new(transactions) end end ! PB = PaymentsBatch ! ! [50] pry(main)> b=PB.from_people_ids 2 => #<PaymentsBatch:0x3fd8eab40048>
  15. 15. class PaymentsBatch < Batch def report puts self.map do |t| [t.id.to_s.ljust(5), t.created_at.to_s.ljust(25), t.aasm_state.to_s.ljust(15)].join end.join("n") end end ! ! [75] pry(main)> b.report 5 2014-02-04 19:42:57 UTC 6 2014-02-04 19:43:11 UTC => nil processed processed
  16. 16. class PaymentsBatch < Batch def report self.map do |t| [t.id, t.created_at, t.aasm_state] end.to_text_table end end ! [22] pry(main)> b.report => +---+-------------------------+-----------+ | 5 | 2014-02-04 19:42:57 UTC | processed | | 6 | 2014-02-04 19:43:11 UTC | processed | | 7 | 2014-02-04 19:43:42 UTC | processed | +---+-------------------------+-----------+
  17. 17. class PaymentsBatch < Batch def report cols = [:id, :created_at, :aasm_state] h = serializable_hash(only: cols) rows = ([h.first.keys] + h.map(&:values)) rows.to_table(:first_row_is_head => true) end end ! ! [87] pry(main)> b.report => +------------+-------------------------+----+ | aasm_state | created_at | id | +------------+-------------------------+----+ | processed | 2014-02-04 19:42:57 UTC | 5 | | processed | 2014-02-04 19:43:11 UTC | 6 | | processed | 2014-02-04 19:43:42 UTC | 7 | +------------+-------------------------+----+
  18. 18. [155] pry(main)> b=PB.from_people_ids 2 => [#<SaleTransaction id: 5, credit_card_id: 47523892, reference_transaction_id: nil, amount: #<BigDecimal:7ff71ecc9b48,'0.2E2', 9(18)>, response: nil, transaction_type: "sale", aasm_state: "processed", created_at: "2014-02-04 19:42:57", updated_at: "2014-02-04 19:43:05", type: "SaleTransaction", callback_queue_name: "medium", callback_queue_application: "pry", options: "{"country":"US","profile ":null,"requires_cvv":null,...", processor_authorization_code: "illoearumi", gateway_transaction_id: "excepturiq", success: true, failure_reason: nil, avs_response_code: "M", cvv_response_code: "M", scope_identifier: nil, person_id: 2>, #<SaleTransaction id: 6, credit_card_id: 47523892, reference_transaction_id: nil, amount: #<BigDecimal:7ff71ecc9620,'0.2E2', 9(18)>, response: nil, transaction_type: "sale", aasm_state: "processed", created_at: "2014-02-04 19:43:11", updated_at: "2014-02-04 19:43:20", type: "SaleTransaction", callback_queue_name: "medium", callback_queue_application: "pry", options: "{"country":"US","profile ":null,"requires_cvv":null,...", processor_authorization_code: "errorconse", gateway_transaction_id: "eumauttene", success: true, failure_reason: nil, avs_response_code: "M", cvv_response_code: "M", scope_identifier: nil, person_id: 2>] [156] pry(main)> b.report .=> +----+-------------------------+------------+ | id | created_at | aasm_state | +----+-------------------------+------------+ | 5 | 2014-02-04 19:42:57 UTC | processed | | 6 | 2014-02-04 19:43:11 UTC | processed | +----+-------------------------+------------+ LOL
  19. 19. class Batch < Array def pretty_inspect report.to_s end end
  20. 20. [155] pry(main)> b=PB.from_people_ids 2 => +----+-------------------------+------------+ | id | created_at | aasm_state | +----+-------------------------+------------+ | 5 | 2014-02-04 19:42:57 UTC | processed | | 6 | 2014-02-04 19:43:11 UTC | processed | +----+-------------------------+------------+
  21. 21. class PaymentsBatch def email addr = 'you@re.awesome.com' Mail::Message.new(to: addr, from: addr, subject: 'PaymentsBatch', body: report.to_s).deliver end end
  22. 22. LOG SEARCH
  23. 23. chris.morris@support01:$ RAILS_ENV=production bundle exec thor grep:payments -p '(PCI|SPT) environment' -d 1 ********** support01 ********** ssh support01 'grep -P "(PCI|SPT) environment" log/production.log-20140218' 14-02-18T02:48:20 - [INFO] (#21648) SPT environment forwarding #create to PCI environment 14-02-18T02:49:22 - [INFO] (#21651) SPT environment forwarding #create to PCI environment 14-02-18T03:00:04 - [INFO] (#21648) SPT environment forwarding #create to PCI environment ********** support02 ********** ssh support02 'grep -P "(PCI|SPT) environment" log/production.log-20140218' 14-02-17T15:00:50 - [INFO] (#3844) SPT environment forwarding #create to PCI environment 14-02-17T19:18:37 - [INFO] (#3841) SPT environment forwarding #create to PCI environment 14-02-17T22:12:38 - [INFO] (#3844) SPT environment forwarding #create to PCI environment 14-02-18T01:40:56 - [INFO] (#3844) SPT environment forwarding #create to PCI environment ********** boxen01 ********** ssh boxen01 'grep -P "(PCI|SPT) environment" log/production.log-20140218' 14-02-17T19:18:37 - [INFO] (#24618) PCI environment handling #create 14-02-18T01:40:56 - [INFO] (#24618) PCI environment handling #create 14-02-18T02:49:22 - [INFO] (#24615) PCI environment handling #create ********** boxen02 ********** ssh boxen02 'grep -P "(PCI|SPT) environment" log/production.log-20140218' 14-02-17T15:00:50 - [INFO] (#8119) PCI environment handling #create 14-02-17T22:12:38 - [INFO] (#8129) PCI environment handling #create 14-02-18T02:48:20 - [INFO] (#8125) PCI environment handling #create 14-02-18T03:00:04 - [INFO] (#8122) PCI environment handling #create
  24. 24. chris.morris@support01:$ RAILS_ENV=production bundle exec thor grep:payments -p '(PCI|SPT) environment' -d 1 -c --pry ********** support01 ********** ssh support01 'grep -P -C 15 "(PCI|SPT) environment" log/production.log-20140218' ********** support02 ********** ssh support02 'grep -P -C 15 "(PCI|SPT) environment" log/production.log-20140218' ********** boxen01 ********** ssh boxen01 'grep -P -C 15 "(PCI|SPT) environment" log/production.log-20140218' ********** boxen02 ********** ssh boxen02 'grep -P -C 15 "(PCI|SPT) environment" log/production.log-20140218' +-------+---------------------+--------+-------------+---------------+ | pid | timestamp | action | status_code | ip_addr | +-------+---------------------+--------+-------------+---------------+ | 3844 | 2014-02-17T15:00:50 | POST | 201 | x.x.x.x | | 8119 | 2014-02-17T15:00:50 | POST | 201 | x.x.x.x | | 3841 | 2014-02-17T19:18:36 | POST | 500 | x.x.x.x | | 24618 | 2014-02-17T19:18:37 | POST | 500 | x.x.x.x | | 8129 | 2014-02-17T22:12:38 | POST | 201 | x.x.x.x | | 3844 | 2014-02-17T22:12:38 | POST | 201 | x.x.x.x | | 24618 | 2014-02-18T01:40:56 | POST | 201 | x.x.x.x | | 21648 | 2014-02-18T02:48:20 | POST | 201 | x.x.x.x | | 8125 | 2014-02-18T02:48:20 | POST | 201 | x.x.x.x | | 24615 | 2014-02-18T02:49:22 | POST | 400 | x.x.x.x | | 21651 | 2014-02-18T02:49:22 | POST | 500 | x.x.x.x | | 21648 | 2014-02-18T03:00:04 | POST | 201 | x.x.x.x | | 8122 | 2014-02-18T03:00:04 | POST | 201 | x.x.x.x | +-------+---------------------+--------+-------------+---------------+
  25. 25. From: script/grep.payments.thor @ line 81 Thor::Sandbox::Grep#payments: ! ! 76: 77: 78: 79: 80: => 81: 82: 83: 84: 85: 86: wheelhouse = Wheelhouse.new(sio) chunks = wheelhouse.chunk_it(:grep => options[:pattern], :filename => options[:save_chunks], :range_threshold_in_seconds => options[:timestamp_range_in_seconds]) puts wheelhouse.to_text_table(chunks) binding.pry if options[:pry] end end private [1] pry(#<Thor::Sandbox::Grep>)>
  26. 26. [1] pry(#<Thor::Sandbox::Grep>)> chunks => [#<RequestChunk:0x00000004f39540 @action="POST", @completed=true, @controller="Api::V1::CreditCardsController#create", @ip_addr="x.x.x.x", @lines= ["14-02-18T02:48:20 - [INFO] (#21648) Started POST ... for x.x.x.x with ...", "14-02-18T02:48:20 - [INFO] (#21648) Processing by Api::V1::CreditCards...", "14-02-18T02:48:20 - [INFO] (#21648) Parameters: {"..."", "14-02-18T02:48:20 - [INFO] (#21648) SPT environment forwarding #create...", "14-02-18T02:48:22 - [INFO] (#21648) Completed 201 Created in 1279.5ms ..."] @parameters= {"foo"=> {"token"=>"e4facf2d83cf58bd78b05485e21d0923", "checkout_resource_url"=>"https://foobar.com", "return_url"=>"https://www.livingsocial.com", "country_code"=>"AU"}, "person_id"=>"12345678"}, @pid="21648", @range_threshold_in_seconds=2, @referrer="", @route="http://...", @status_code="201", @timestamp="2014-02-18T02:48:20", @user_agent="Ruby">,
  27. 27. THE END
  28. 28. DIY MOCKS AND FIXTURES
  29. 29. RAVE REVIEWS "You either get an F for logic or an A+ for balls.” ! ! — J. B. Rainsberger author of JUnit Recipes
  30. 30. [~]$ curl -s http://www.linkedin.com/in/chrismo | grep Relevance [~]$
  31. 31. NOT GLENN VANDERBURG =>
  32. 32. NOT GLENN VANDERBURG => ! CODMONGER !
  33. 33. FactoryGirl.define do factory :customer do external_record_type 'person' sequence(:external_id) email Faker::Internet.email end ! ! ! ! factory :address do name Faker::Name.name customer end sequence :purchase_external_id sequence :purchase_deal_id factory :purchase do currency 'USD' external_record_type 'purchase' external_id { FactoryGirl.generate(:purchase_external_id) } deal_id { FactoryGirl.generate(:purchase_deal_id) } price { rand(50) + 1 } customer association :address, :method => :build quantity { rand(4) + 1 } title Faker::Company.catch_phrase end factory :collection do purchase end end
  34. 34. describe Collection do it "should not return old exported collection with end status" do [:collected, :canceled, :refused].each do |status| collection = Factory.build(:collection, :exported_at => 4.days.ago, :status => status.to_s) assert !Collection.problem_old_exports.include?(collection) end end
  35. 35. describe Collection do it "should not return old exported collection with end status" do [:collected, :canceled, :refused].each do |status| collection = Factory.build(:collection, :exported_at => 4.days.ago, :status => status.to_s) assert !Collection.problem_old_exports.include?(collection) end end end NoMethodError: undefined method `customer' for nil:NilClass ./app/models/collection.rb:90:in `customer' ./app/models/collection.rb:14:in `block in <class:Collection>' ./spec/models/collection_spec.rb:43:in `block (3 levels) in <top (required)>' ./spec/models/collection_spec.rb:42:in `each' ./spec/models/collection_spec.rb:42:in `block (2 levels) in <top (required)>'
  36. 36. class Collection < ActiveRecord::Base def customer purchase.customer end end NoMethodError: undefined method `customer' for nil:NilClass ./app/models/collection.rb:90:in `customer' ./app/models/collection.rb:14:in `block in <class:Collection>' ./spec/models/collection_spec.rb:43:in `block (3 levels) in <top (required)>' ./spec/models/collection_spec.rb:42:in `each' ./spec/models/collection_spec.rb:42:in `block (2 levels) in <top (required)>'
  37. 37. FactoryGirl.define do ... # YOU HAD ONE JOB factory :collection do purchase end end
  38. 38. require 'machinist/active_record' require 'faker' ! Sham.sham_id { |i| i } ! Customer.blueprint do external_record_type { 'person' } external_id { Sham.sham_id } email { Faker::Internet.email } end ! Address.blueprint do name Faker::Name.name customer purchase end ! Purchase.blueprint do currency {'USD'} external_record_type {'purchase'} external_id { Sham.sham_id } deal_id { Sham.sham_id } price { rand(50) + 1 } customer quantity { rand(4) + 1 } title { Faker::Company.catch_phrase } end ! Collection.blueprint do purchase end
  39. 39. describe Collection, 'scope :problem_old_exports (machinist)' do it "should not return old exported collection with end status" do [:collected, :canceled, :refused].each do |status| collection = Collection.make_unsaved(:exported_at => 4.days.ago, :status => status.to_s) assert !Collection.problem_old_exports.include?(collection) end end end
  40. 40. describe Collection, 'scope :problem_old_exports (machinist)' do it "should not return old exported collection with end status" do [:collected, :canceled, :refused].each do |status| collection = Collection.make_unsaved(:exported_at => 4.days.ago, :status => status.to_s) assert !Collection.problem_old_exports.include?(collection) end end end ! NoMethodError: undefined method `customer' for nil:NilClass ./app/models/collection.rb:90:in `customer' ./app/models/collection.rb:14:in `block in <class:Collection>' ./spec/models/collection_spec.rb:61:in `block (3 levels) in <top (required)>' ./spec/models/collection_spec.rb:60:in `each' ./spec/models/collection_spec.rb:60:in `block (2 levels) in <top (required)>'
  41. 41. ! # ONE. JOB. Collection.blueprint do purchase end
  42. 42. require 'faker' ! class CodmongerFixture def self.create(opts={}) record = self.new(opts); record.save!; record end end ! class AddressFixture < CodmongerFixture def self.new(opts={}) Address.new({:name => Faker::Name.name}.merge(opts)) end end ! class CustomerFixture < CodmongerFixture @@customer_id = Customer.last.external_id + 1 rescue 1 def self.new(opts={}) Customer.new({:external_record_type => 'person', :external_id => @@customer_id += 1, :email => Faker::Internet.email}.merge(opts)) end end ! class PurchaseFixture < CodmongerFixture @@purchase_id = Purchase.last.external_id + 1 rescue 1 def self.new(opts={}) customer = CustomerFixture.new Purchase.new({:currency => 'USD', :deal_id => 1, :external_record_type => 'purchase', :external_id => @@purchase_id += 1, :price => rand(50) + 1, :quantity => rand(4) + 1, :title => Faker::Company.catch_phrase, :address => AddressFixture.new(:customer => customer), :customer => customer}.merge(opts)) end end ! class CollectionFixture < CodmongerFixture def self.new(opts={}) Collection.new({:purchase => PurchaseFixture.new}.merge(opts)) end end
  43. 43. describe Collection do it "should not return old exported collection with end status" do [:collected, :canceled, :refused].each do |status| collection = CollectionFixture.new(:exported_at => 4.days.ago, :status => status.to_s) assert !Collection.problem_old_exports.include?(collection) end end end 1 example, 0 failures, 1 passed
  44. 44. FactoryGirl::Proxy::ObjectWrapper def object @object ||= @klass.new assign_object_attributes @object end
  45. 45. class Collection < ActiveRecord::Base state_machine :status, :initial => lambda{ |collection| collection.customer.new? ? :received : :customer_verified} do ... end end
  46. 46. ACTIVE RECORD IS HARD ENOUGH ! ! I DON’T NEED YOUR FREEKING DSL ON TOP OF IT
  47. 47. require 'faker' ! class CodmongerFixture def self.create(opts={}) record = self.new(opts); record.save!; record end end ! class AddressFixture < CodmongerFixture def self.new(opts={}) Address.new({:name => Faker::Name.name}.merge(opts)) end end ! class CustomerFixture < CodmongerFixture @@customer_id = Customer.last.external_id + 1 rescue 1 def self.new(opts={}) Customer.new({:external_record_type => 'person', :external_id => @@customer_id += 1, :email => Faker::Internet.email}.merge(opts)) end end ! class PurchaseFixture < CodmongerFixture @@purchase_id = Purchase.last.external_id + 1 rescue 1 def self.new(opts={}) customer = CustomerFixture.new Purchase.new({:currency => 'USD', :deal_id => 1, :external_record_type => 'purchase', :external_id => @@purchase_id += 1, :price => rand(50) + 1, :quantity => rand(4) + 1, :title => Faker::Company.catch_phrase, :address => AddressFixture.new(:customer => customer), :customer => customer}.merge(opts)) end end ! class CollectionFixture < CodmongerFixture def self.new(opts={}) Collection.new({:purchase => PurchaseFixture.new}.merge(opts)) end end
  48. 48. FactoryGirl.define do factory :customer do external_record_type 'person' sequence(:external_id) email Faker::Internet.email end ! ! ! ! factory :address do name Faker::Name.name customer end sequence :purchase_external_id sequence :purchase_deal_id factory :purchase do currency 'USD' external_record_type 'purchase' external_id { FactoryGirl.generate(:purchase_external_id) } deal_id { FactoryGirl.generate(:purchase_deal_id) } price { rand(50) + 1 } customer association :address, :method => :build quantity { rand(4) + 1 } title Faker::Company.catch_phrase end factory :collection do purchase end end
  49. 49. require 'faker' ! class CodmongerFixture def self.create(opts={}) record = self.new(opts); record.save!; record end end ! class AddressFixture < CodmongerFixture def self.new(opts={}) Address.new({:name => Faker::Name.name}.merge(opts)) end end ! class CustomerFixture < CodmongerFixture @@customer_id = Customer.last.external_id + 1 rescue 1 def self.new(opts={}) Customer.new({:external_record_type => 'person', :external_id => @@customer_id += 1, :email => Faker::Internet.email}.merge(opts)) end end
  50. 50. class PurchaseFixture < CodmongerFixture @@purchase_id = Purchase.last.external_id + 1 rescue 1 def self.new(opts={}) customer = CustomerFixture.new Purchase.new({:currency => 'USD', :deal_id => 1, :external_record_type => 'purchase', :external_id => @@purchase_id += 1, :price => rand(50) + 1, :quantity => rand(4) + 1, :title => Faker::Company.catch_phrase, :address => AddressFixture.new(:customer => customer), :customer => customer}.merge(opts)) end end ! class CollectionFixture < CodmongerFixture def self.new(opts={}) Collection.new({:purchase => PurchaseFixture.new}.merge(opts)) end end
  51. 51. WHEN A DSL POOPS OUT ON YOU ! YOU CAN'T LOOK AT ITS SOURCE CODE ! BECAUSE IT'S ALL META AND STUFF.
  52. 52. MOCK FRAMEWORKS NOT ALL EXPECTATIONS WERE SATISFIED
  53. 53. not all expectations were satisfied unsatisfied expectations: - expected exactly once, not yet invoked: Purchase(id: integer, person_id: integer, deal_id: integer, created_at: datetime, updated_at: datetime, credit_card_id: integer, ref_code: string, charged_at: datetime, coupons_count: integer, aasm_state: string, approval_code: string, referring_purchase_id: integer, referring_purchases_count: integer, discount: decimal, referring_user_id: integer, last_transaction_id: string, collapsed: boolean, blasted_to_feed_at: datetime, fb_fan_incentive: decimal, deleted_at: datetime, latitude: float, longitude: float, ip_address: string, visa_discount: decimal, purchaser_id: integer, cheerios_discount: decimal, city_id: integer).find('1234567')
  54. 54. unexpected invocation: #<Mock:0x1022aff50>.reload() satisfied expectations: - allowed any number of times, not yet invoked: #<Mock:0x1022b6df0>.available_credit(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022b6df0>.available_credit(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022b6df0>.Person(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022b57e8>.LocalDeal(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022aff50>.Purchase(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022aff50>.total_price(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022aff50>.alternate_payment(any_parameters) - allowed any number of times, invoked once: #<Mock:0x1022aff50>.person(any_parameters) - allowed any number of times, invoked once: #<Mock:0x1022aff50>.deal(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022aff50>.aasm_state(any_parameters) - allowed any number of times, not yet invoked: #<Mock:0x1022aff50>.id(any_parameters) - allowed any number of times, invoked once: #<Mock:0x1022aff50>.referred_purchases(any_parameters)
  55. 55. purchase = stub() purchase.stubs(:id).returns(123) purchase.stubs(:quantity).returns(2) ! deal = stub() deal.stubs(:title).returns("Mr. Bones's Deal'") deal.stubs(:price).returns(12) deal.stubs(:currency_unit).returns('$') ! person = stub() person.stubs(:email).returns('chris.morris@livingsocial.com') person.stubs(:id).returns(8675309) ! purchase.stubs(:person).returns(person) purchase.stubs(:deal).returns(deal)
  56. 56. PurchaseStub = Struct.new(:id, :quantity, :deal, :person) DealStub = Struct.new(:title, :price, :currency_unit) PersonStub = Struct.new(:id, :email) ! person = PersonStub.new(8675309, 'chrismo@ls.com') deal = DealStub.new("Mr. Bones's Deal'", 12, '$') purchase = PurchaseStub.new(123, 2, deal, person)
  57. 57. person = OpenStruct.new(id: 8675309, email: 'chrismo@ls.com') ! deal = OpenStruct.new(title: "Mr. Bones's Deal'", price: 12, currency_unit: '$') ! purchase = OpenStruct.new(id: 123, quantity: 2, deal: deal, person: person)
  58. 58. require 'ostruct' ! person = OpenStruct.new(id: 8675309, email: 'chrismo@clabs.org', available_credit: {'USD'=>5,'MYR'=>0}) ! def person.available_credit_by_currency(currency) self.available_credit[currency] end
  59. 59. module PaymentProviders module DoubleFake class Payment @@db = {} ! ! ! ! def self.load_payment(id, params) @@db[id] end def initialize(purchase, hash, all_params={}) @hash = hash end def address @hash[:address] end def save @@db[@hash[:id]] = self end end end end
  60. 60. DIY — Gem
  61. 61. BUILD — BUY
  62. 62. BUY trades in FAMILIARITY TRANSPARENCY
  63. 63. THE END
  64. 64. TRACK YER BIG STUFF
  65. 65. No tengo ni idea de lo que estoy haciendo
  66. 66. • 2500 translation keys • 1/2 in use
  67. 67. dozens of translate calls / request
  68. 68. module Humperdink class DirtySet attr_reader :dirty, :clean, :config ! ! ! ! ! ! def initialize(initial_contents=[], config={}) @clean = Set.new(initial_contents) @dirty = Set.new @config = config end def <<(value) @dirty << value if !@clean.include?(value) clean! if getting_messy? end def getting_messy? ... end def clean! @clean.merge(@dirty) cleaned = @dirty.to_a.dup @dirty.clear cleaned end def length # wonky -> doesn't include @dirty @clean.length end def clear @clean.clear @dirty.clear end end end
  69. 69. describe Humperdink::DirtySet do let(:set) { Humperdink::DirtySet.new } ! it 'should only append to dirty' do set << ‘foo' set.dirty.should == ['foo'].to_set set.clean.should == [].to_set end ! it 'should clean! dirty things to clean' do set << 'foo' wuz_dirty = set.clean! wuz_dirty.should == [‘foo'] set.dirty.should == [].to_set set.clean.should == ['foo'].to_set end end
  70. 70. it 'should only append to dirty if not in clean' do set << ‘foo' set.dirty.should == ['foo'].to_set set.clean.should == [].to_set ! set.clean! set.dirty.should == [].to_set set.clean.should == ['foo'].to_set ! end set << ‘foo' set.dirty.should == [].to_set set.clean.should == ['foo'].to_set
  71. 71. it 'should initialize clean with initialize' do set = Humperdink::DirtySet.new(['a', ‘b']) set.dirty.should == [].to_set set.clean.should == ['a', 'b'].to_set end ! it 'should only append to dirty if not in clean' do set = Humperdink::DirtySet.new(['foo']) set << ‘foo' set.dirty.should == [].to_set set.clean.should == ['foo'].to_set end
  72. 72. module Humperdink class RedisDirtySet < DirtySet def initialize(initial_content=[], config={}) super(initial_content, config) end ! def clean! to_save = super save(to_save) end ! def load @clean.merge(redis.smembers(@config[:key])) end ! def save(to_save) redis.sadd(@config[:key], to_save.to_a) end end end
  73. 73. module Humperdink class Tracker attr_reader :set, :config ! ! ! ! def initialize(set=Set.new, config={}) @set = set @config = config end def track(data) @set << data end def tracker_enabled @config[:enabled] end # Anytime an exception happens, we want to skedaddle out of the way # and let life roll on without any tracking in the loop. def shutdown(exception) begin on_event(:shutdown, "#{exception.message}") rescue => e $stderr.puts([e.message, e.backtrace].join("n")) rescue nil end @config[:enabled] = false end end end
  74. 74. class KeyTracker def initialize(redis, key) redis_set = Humperdink::RedisDirtySet.new(:redis => redis, :key => key, :max_dirty_items => 9) @tracker = Humperdink::Tracker.new(redis_set, :enabled => true) end ! def on_translate(locale, key, options = {}) begin if @tracker.tracker_enabled requested_key = normalize_requested_key(key, options) @tracker.track(requested_key) end rescue => e @tracker.shutdown(e) end end end
  75. 75. module KeyTrackerBackend def key_tracker @key_tracker end ! ! def key_tracker=(value) @key_tracker = value end def translate(locale, key, options = {}) @key_tracker.on_translate(locale, key, options) if @key_tracker super end end ! def setup @redis = Redis.connect(:url => 'redis://127.0.0.1:6379/8') @redis_key = 'humperdink:example:i18n' ! tracker = KeyTracker.new(@redis, @redis_key) I18n.backend.class.class_eval { include KeyTrackerBackend } I18n.backend.key_tracker = tracker end
  76. 76. CONFIGURATION
  77. 77. module Humperdink class DirtySet ! def <<(value) @dirty << value if !@clean.include?(value) clean! if getting_messy? end ! end end
  78. 78. module Humperdink class DirtySet ! def getting_messy? if @config[:max_dirty_items] && @dirty.length > @config[:max_dirty_items] return true end ! if @config[:clean_timeout] && Time.now > @time_to_clean return true end end ! end end
  79. 79. module Humperdink class DirtySet def initialize(initial_contents=[], config={}) @clean = Set.new(initial_contents) @dirty = Set.new @config = config ... at_exit { clean! if @config[:clean_at_exit] } end end end
  80. 80. CONFIGURATION RESQUE
  81. 81. module Resque class Worker def work(interval = 5.0, &block) loop do break if shutdown? ! if not paused? and job = reserve if @child = fork(job) ... else perform(job, &block) exit!(true) if will_fork? end ! done_working @child = nil else sleep interval end end end end end
  82. 82. module Humperdink::ForkPiping
  83. 83. class KeyTracker def initialize(redis, key) redis_set = Humperdink::RedisDirtySet.new(:redis => redis, :key => key, :max_dirty_items => 9) @tracker = Humperdink::Tracker.new(redis_set, :enabled => true) @tracker.include Humperdink::ForkPiping end end
  84. 84. def on_translate(locale, key, options = {}) begin if @tracker.tracker_enabled requested_key = normalize_requested_key(key, options) @tracker.track(requested_key) end rescue => e @tracker.shutdown(e) end end
  85. 85. def on_translate(locale, key, options = {}) begin if @tracker.not_forked_or_parent_process if @tracker.tracker_enabled requested_key = normalize_requested_key(key, options) @tracker.track(requested_key) end else if @tracker.config[:enabled] requested_key = normalize_requested_key(key, options) @tracker.write_to_child_pipe(requested_key) end end rescue => e @tracker.shutdown(e) end end
  86. 86. COVERBAND HTTPS://GITHUB.COM/DANMAYER/COVERBAND
  87. 87. use Coverband::Middleware
  88. 88. module Coverband class Middleware ! def call(env) configure_sampling record_coverage results = @app.call(env) report_coverage results end ! end end
  89. 89. def configure_sampling if (rand * 100.0) > @sample_percentage @enabled = false else @enabled = true end end
  90. 90. def record_coverage if @enabled unless @function_set set_trace_func proc { |event, file, line, id, binding, classname| add_file(file, line) } @function_set = true end else if @function_set set_trace_func(nil) @function_set = false end end end
  91. 91. module Coverband class Middleware ! def call(env) configure_sampling record_coverage results = @app.call(env) report_coverage results end ! end end
  92. 92. <= NOT DAN MAYER
  93. 93. THE END
  94. 94. ALL THE ANALOGIES
  95. 95. META
  96. 96. THE END
  97. 97. Chris Morris Sr. Engineer @the_chrismo
  98. 98. CREDITS http://www.flickr.com/photos/tracy_olson/61056391/ http://imgs.xkcd.com/comics/the_general_problem.png http://upload.wikimedia.org/wikipedia/commons/a/a3/PikePlaceMarket2006.jpg http://pixabay.com/en/why-question-gorilla-monkey-234596/ http://www.flickr.com/photos/liquidator/3450997601/in/photostream/ http://upload.wikimedia.org/wikipedia/commons/c/c2/EWM_shop_2007.jpg http://upload.wikimedia.org/wikipedia/commons/5/52/Vader_2011_in_Rostock.jpg http://upload.wikimedia.org/wikipedia/commons/4/4e/Construction_in_Toronto_May_2012.jpg http://upload.wikimedia.org/wikipedia/commons/9/9a/SF_Japanese_Garden.JPG http://upload.wikimedia.org/wikipedia/commons/3/32/Climbing_in_Joshua_Tree_NP.jpg http://www.flickr.com/photos/pagedooley/7899921242/ http://upload.wikimedia.org/wikipedia/commons/f/f3/Symfonicky_orchestr_hl._m._Prahy_FOK.jpg http://upload.wikimedia.org/wikipedia/commons/5/57/Chess-king.JPG http://upload.wikimedia.org/wikipedia/commons/6/62/ElBulliKitchen.jpg http://farm7.staticflickr.com/6020/5994637447_01d1e6107a_o.jpg http://static3.wikia.nocookie.net/__cb20100721202821/muppet/images/5/52/ Bobby_Benson_and_the_Babies.jpg http://upload.wikimedia.org/wikipedia/commons/f/fd/Detail_Lewis_%26_Clark_at_Three_Forks.jpg http://upload.wikimedia.org/wikipedia/commons/e/e3/Lewis_and_Clark_track_map_published_1814_LoC.jpg http://www.fhwa.dot.gov/byways/Uploads/asset_files/000/006/078/Rugged_Point.jpg http://www.geograph.org.uk/photo/2415063 http://www.flickr.com/photos/18203311@N08/4405479417/ http://upload.wikimedia.org/wikipedia/commons/4/45/Thomas_Eakins,_The_Agnew_Clinic_1889.jpg http://www.flickr.com/photos/theslowlane/6111718072/
  99. 99. Chris Morris Sr. Engineer @the_chrismo

×