Slide 1

Slide 1 text

Life on the Edge Introducing “Edge Case Thinking”

Slide 2

Slide 2 text

Who Am I?

Slide 3

Slide 3 text

James Edward Gray II I’ve been in the community for long time now I’ve written code and docs for Ruby I’m a Ruby Rogue

Slide 4

Slide 4 text

Edge Cases

Slide 5

Slide 5 text

The ultimate edge case

Slide 6

Slide 6 text

Stargate Universe The ultimate edge case

Slide 7

Slide 7 text

Figuring Out How to Survive Flung to the far end of the universe, the SGU crew has to go back to the basics of survival

Slide 8

Slide 8 text

Edge Cases are the Currency of Programmers Most of our development effort is spent on them If we didn’t have them, programming would be easy (maybe even automatic) How many bugs aren’t edge cases? This is important stuff!

Slide 9

Slide 9 text

Numbers are Behind Letters What are the minimal cards you have to flip to prove there’s an even behind each vowel?

Slide 10

Slide 10 text

Numbers are Behind Letters What are the minimal cards you have to flip to prove there’s an even behind each vowel?

Slide 11

Slide 11 text

Numbers are Behind Letters What are the minimal cards you have to flip to prove there’s an even behind each vowel?

Slide 12

Slide 12 text

Numbers are Behind Letters What are the minimal cards you have to flip to prove there’s an even behind each vowel?

Slide 13

Slide 13 text

Reprogramming Their Brains The crew had to rework their thoughts to match their new circumstances

Slide 14

Slide 14 text

Edge Case Thinking Edge cases are the important stuff The rest of our work is at least easier Given that, I simply prefer to focus on the edge cases first, when possible

Slide 15

Slide 15 text

A Stargate Safari As Destiny travels, Stargates allow the crew to explore many worlds

Slide 16

Slide 16 text

nil: The Edge of Nothing

Slide 17

Slide 17 text

The Trouble With nil It’s an edge case, by definition Given that, it’s best avoided as much as possible

Slide 18

Slide 18 text

Returning nil This is an example of a common, but pointless edge case module GooglePlus module_function def has_any_circles?(user) !!check_api_for_any_circles?(user) end def circles(user) if has_any_circles? user get_circles_from_api(user) else nil end end # ... end

Slide 19

Slide 19 text

Using nil We are forced into an edge case at this end as well if circles = GooglePlus.circles(user) circles.each do |circle| # use with circle here... end end

Slide 20

Slide 20 text

My Favorite De-edger This unusual core Ruby method can guarantee an Array (Enumerable) result Array([1, 2, 3]) # => [1, 2, 3] Array(:not_an_array) # => [:not_an_array] Array(nil) # => []

Slide 21

Slide 21 text

Dodging the Edge Case Remove the edge case instead of work with it, when possible Array(GooglePlus.circles(user)).each do |circle| # use with circle here... end

Slide 22

Slide 22 text

Avoiding Returning nil This method has a much better contract: I’ll return to you a list of circles module GooglePlus module_function def circles(user) Array(get_circles_from_api(user)) end # ... end

Slide 23

Slide 23 text

Ideal Usage Getting the method contract right automatically fixed this code too GooglePlus.circles(user).each do |circle| # use with circle here... end

Slide 24

Slide 24 text

We Haven’t Lost Anything We can still handle the no circles case, if needed circles = GooglePlus.circles(user) if circles.empty? # handle no circles here... else circles.each do |circle| # use with circle here... end end

Slide 25

Slide 25 text

Slurping Your Food Data

Slide 26

Slide 26 text

Too Much Heat? When Destiny begins flying straight into a sun, the crew assumes they are about to burn up

Slide 27

Slide 27 text

Data as a Stream Avoid “slurping” a ton of data in whenever possible Big data is a scary edge case Draw only what you need at once Come back for more as needed This applies to all data, including database queries

Slide 28

Slide 28 text

No Slurping Some exceptions: you produce and consume, you ensure a reasonable size, etc. File.read(path).each_line do |line| # use line here... end

Slide 29

Slide 29 text

No Slurping Some exceptions: you produce and consume, you ensure a reasonable size, etc. File.read(path).each_line do |line| # use line here... end

Slide 30

Slide 30 text

A Better Way It’s rarely any harder to work with data in chunks File.foreach(path) do |line| # use line here... end

Slide 31

Slide 31 text

The Rails Version In Rails, all() without a where() is generally the same as read() @comments = @post.comments.all

Slide 32

Slide 32 text

The Rails Version In Rails, all() without a where() is generally the same as read() @comments = @post.comments.all

Slide 33

Slide 33 text

Better It’s OK to grab subsets of data @comments = @post.comments .where("created_at >= ?", 1.day.ago) .all # ... or ... @comments = @post.comments.paginate(params[:page])

Slide 34

Slide 34 text

Best You can even walk all of the data in subsets @post.comments.find_each do |comment| # use comment here... end

Slide 35

Slide 35 text

Edgy Tests

Slide 36

Slide 36 text

Risk Management Tests are about managing concerns Edge cases are the primary concerns Focus your efforts there

Slide 37

Slide 37 text

Test Fodder Given a trivial method… class Post < ActiveRecord::Base def editable? created_at >= 1.hour.ago end # ... end

Slide 38

Slide 38 text

Boring Tests We often test it like this describe Post do it "is editable within an hour of creation" do Post.new(created_at: 10.minutes.ago).must_be :editable? end it "is not editable beyond an hour of creation" do Post.new(created_at: 1.day.ago).wont_be :editable? end end

Slide 39

Slide 39 text

Boring Tests We often test it like this describe Post do it "is editable within an hour of creation" do Post.new(created_at: 10.minutes.ago).must_be :editable? end it "is not editable beyond an hour of creation" do Post.new(created_at: 1.day.ago).wont_be :editable? end end

Slide 40

Slide 40 text

Moving to the Edge Spend your paranoia on the edges instead describe Post do it "is editable within an hour of creation" do Post.new(created_at: 1.hour.ago).must_be :editable? end it "is not editable beyond an hour of creation" do Post.new(created_at: (1.hour + 1.minute).ago).wont_be :editable? end end

Slide 41

Slide 41 text

Moving to the Edge Spend your paranoia on the edges instead describe Post do it "is editable within an hour of creation" do Post.new(created_at: 1.hour.ago).must_be :editable? end it "is not editable beyond an hour of creation" do Post.new(created_at: (1.hour + 1.minute).ago).wont_be :editable? end end

Slide 42

Slide 42 text

Edges Everywhere There are so many edges in programming The relationships between objects Interactions with frameworks Networks and other systems So much more…

Slide 43

Slide 43 text

It Pays to be Prepared When investigating alien technology, the crew protects themselves from the environment

Slide 44

Slide 44 text

The Hardline Make edge cases impossible, when you can # guarantee we do not touch API's class Geokit::Geocoders::MultiGeocoder def self.geocode(*args) fail "Tried to contact Google" end end class Boomerang::APICall def initialize(*args) fail "Tried to contact Amazon" end end module SendGridDisabler def make_api_call(*args) fail "Tried to contact SendGrid" end private :make_api_call end SendGrid.extend(SendGridDisabler)

Slide 45

Slide 45 text

All at Once But in Order

Slide 46

Slide 46 text

Eli: An Edge Case Thinker As the crew’s boy genius, Eli is always thinking his way around their edge cases

Slide 47

Slide 47 text

Don’t Do Extra Work! I’m not advocating heavy planning here There are so many great answers to edge cases besides hard work: It won’t affect us We accept the limitations There’s a easier way Etc.

Slide 48

Slide 48 text

Linear Requests (Part 1) Faraday allows us to abstract away the process of making requests and use middleware require "faraday_middleware" # load Faraday plus extra middleware ua = Faraday.new("https://api.github.com/") do |builder| builder.use Faraday::Response::ParseJson # parse JSON responses builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses builder.adapter Faraday.default_adapter end # ...

Slide 49

Slide 49 text

Linear Requests (Part 1) Faraday allows us to abstract away the process of making requests and use middleware require "faraday_middleware" # load Faraday plus extra middleware ua = Faraday.new("https://api.github.com/") do |builder| builder.use Faraday::Response::ParseJson # parse JSON responses builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses builder.adapter Faraday.default_adapter end # ...

Slide 50

Slide 50 text

Linear Requests (Part 2) We request the list of repos, then fetch each repo in turn for an issue count # ... repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end

Slide 51

Slide 51 text

Linear Requests (Part 2) We request the list of repos, then fetch each repo in turn for an issue count # ... repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end

Slide 52

Slide 52 text

Linear Requests (Part 2) We request the list of repos, then fetch each repo in turn for an issue count # ... repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end

Slide 53

Slide 53 text

Linear Requests (Part 2) We request the list of repos, then fetch each repo in turn for an issue count # ... repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end

Slide 54

Slide 54 text

Linear Requests (Part 2) We request the list of repos, then fetch each repo in turn for an issue count # ... repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end

Slide 55

Slide 55 text

The Request Tree After fetching the list of repos, the remaining requests are independent

Slide 56

Slide 56 text

Parallel Requests (Part 1) Thanks to the abstraction, switching HTTP clients is trivial require "faraday_middleware" # load Faraday plus extra middleware ua = Faraday.new("https://api.github.com/") do |builder| builder.use Faraday::Response::ParseJson # parse JSON responses builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses builder.adapter :typhoeus end # ...

Slide 57

Slide 57 text

Parallel Requests (Part 1) Thanks to the abstraction, switching HTTP clients is trivial require "faraday_middleware" # load Faraday plus extra middleware ua = Faraday.new("https://api.github.com/") do |builder| builder.use Faraday::Response::ParseJson # parse JSON responses builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses builder.adapter :typhoeus end # ...

Slide 58

Slide 58 text

Parallel Requests (Part 2) We can now fetch the list, then handle the repo requests in parallel # ... ua.in_parallel(Typhoeus::Hydra.hydra) do repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end end

Slide 59

Slide 59 text

Parallel Requests (Part 2) We can now fetch the list, then handle the repo requests in parallel # ... ua.in_parallel(Typhoeus::Hydra.hydra) do repos = ua.get("/users/JEG2/repos") repos.on_complete do repos.body.each do |repo| name = repo["name"] project = ua.get("/repos/JEG2/#{name}") project.on_complete do issues = project.body["open_issues"] puts "#{name}: #{issues}" if issues.nonzero? end end end end

Slide 60

Slide 60 text

The Parallel Advantage We’ve cut the time for these requests down by about 84% $ time ruby linear_requests.rb highline: 1 faster_csv: 1 real 0m11.087s user 0m0.466s sys 0m0.121s $ time ruby parallel_requests.rb highline: 1 faster_csv: 1 real 0m1.721s user 0m0.403s sys 0m0.104s

Slide 61

Slide 61 text

The Parallel Advantage We’ve cut the time for these requests down by about 84% $ time ruby linear_requests.rb highline: 1 faster_csv: 1 real 0m11.087s user 0m0.466s sys 0m0.121s $ time ruby parallel_requests.rb highline: 1 faster_csv: 1 real 0m1.721s user 0m0.403s sys 0m0.104s

Slide 62

Slide 62 text

Thanks