Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Breaking Nil to Fix Bugs - An experimental appr...

Breaking Nil to Fix Bugs - An experimental approach

In Ruby, encountering `nil` is inevitable and often leads to frustrating bugs and NoMethodError exceptions. While `nil` is a crucial part of Ruby's design, it can be the source of elusive and hard-to-diagnose issues in your codebase. This talk explores an unconventional and experimental approach to debugging by "breaking" the nil object.
How will we debug bugs?
- extending the NilClass
- customizing method_missing
- creating methods dynamically with the previous options

This is probably the wrong solution. Don't try this in production. However, we'll find bugs and it will be fun!

Enrique Carlos Mogollan

December 03, 2024
Tweet

More Decks by Enrique Carlos Mogollan

Other Decks in Programming

Transcript

  1. Breaking Nil to fix bugs An experimental approach Enrique Mogollan

    @mogox Software Engineer - Salesforce Trailhead 1
  2. Breaking Nil to fix bugs An experimental approach Enrique Mogollan

    @mogox Software Engineer - Salesforce Trailhead 2
  3. Breaking Nil to fix bugs An experimental approach Enrique Mogollan

    @mogox Software Engineer - Salesforce Trailhead 3 Do not use it in production!
  4. Breaking Nil to fix bugs An experimental approach Enrique Mogollan

    @mogox Software Engineer - Salesforce Trailhead 4 Do not use it in production!
  5. params = { id: 1234, lesson_id: 2024 } def mark_lesson_complete(params)

    user = User.find_by(params[:user_id]) user&.complete(params[:lesson_id]) end mark_lesson_complete(params) 6 Intro
  6. What is the issue? 1. Code will raise error Id

    “not found” 2. Invalid parameters on User.find_by 3.Wrong implementation of `complete` method 7 Intro
  7. What is the issue? 1. Code will raise error Id

    “not found” 2. Invalid parameters on User.find_by 3.Wrong implementation of `complete` method 8 Intro
  8. 9 params = { id: 1234, lesson_id: 2024 } def

    mark_lesson_complete(params) user = User.find_by(params[:user_id]) user&.complete(params[:lesson_id]) end mark_lesson_complete(params) # Result: User with ID 1 was marked as complete (user<id: 1>).complete(lesson_id: 2024) Intro
  9. Breaking Nil to fix bugs An experimental approach Enrique Mogollan

    @mogox Software Engineer - Salesforce Trailhead 14 Do not use it in production!
  10. 15 About Enrique: • From San Miguel de Allende, in

    the ❤ of Mexico • CS in Mexico and MS CS in Valencia, Spain • Spanish is my first language, ruby is my favorite Intro
  11. 17 Love is all you need - Words that express

    “love” in English: LOVE Section 1.
  12. 18 Love is all you need - Words that express

    “love” in English: LOVE Section 1.
  13. 19 In Spanish: we have more love - Amar -

    Querer - Encariñarse - Estimar - Apreciar - Cariño - Adorar Section 1.
  14. 20 Love is all you need We use context to

    express meaning: - I love you as a friend. - I Love Rock n' Roll - Love Bites Section 1.
  15. 22 Describe ideas with more context or create new concepts

    MINASWAN Section 1. Matz is nice and so we are nice We [the ruby community] are driven by love and passion - Matz during Rubyconf 2024
  16. 23 params = { id: 1234, lesson_id: 2024 } def

    mark_lesson_complete(params) user = User.find_by(params[:user_id]) user&.complete(params[:lesson_id]) end mark_lesson_complete(params) What is the missing concept? Section 1.
  17. 24 There is a description we need for nil parameters

    Let’s do a experiment Section 1.
  18. 25 Experiment No. 1 Let’s do a experiment data =

    nil data.help # =====> Trying to call method `help` # from a nil instance” experiment_1.rb:28
  19. 26 Experiment No. 1 Let’s do a experiment data =

    nil data.help(user_id: 123, lesson_id: 456) # =====> Trying to call method `help` # from a nil instance” experiment_1.rb:28
  20. 27 Experiment No. 1 class NilClass def method_missing(method, *args, &block)

    error_message = "===> Trying to call method `#{method}`" puts error_message puts caller.take(5).join("\n") puts "=====> args: #{args}" if args&.size > 0 puts "=====> block: #{block.source}" if block puts "=====> NoMethodError not raised" end def respond_to_missing?(method_name, include_private) false end end
  21. 29 Experiment No. 1 [1] pry(main)> ls nil NilClass#methods: &

    =~ inspect pretty_print_cycle to_a to_f to_i to_s === ^ nil? rationalize to_c to_h to_r | NilClass # nil is a singleton
  22. 30 Experiment No. 1 Monkey Patched NilClass [1] pry(main)> ls

    nil NilClass#methods: & =~ inspect nil? rationalize to_ary to_f to_hash to_r | === ^ method_missing pretty_print_cycle to_a to_c to_h to_i to_s
  23. 31 Experiment No. 1 We know a bit more about

    the context - Where are we calling nil - What parameters & block - Not configurable, always on - We probably bypass other nil
  24. In languages there are words that mean completely different things

    but they are spelled the same In English they are called: Homographs 33 Section 2
  25. An example is: I left my phone on the left

    side of the table 34 Section 2
  26. Same word different meaning. In general this can happen: in

    the same language across countries, or across regions. 35 Section 2
  27. Doesn’t work in staging 44 undefined method `zero?’ for an

    instance of String (NoMethodError) Section 2
  28. My code doesn’t work in staging But it’s kind of

    urgent, so I test with staging2 45 Section 2
  29. My code doesn’t work in staging But it’s kind of

    urgent, so I test with staging2 And it works! 46 Section 2
  30. Debug time: 49 class LoginHelper def login(organization_id, user_id, user_profile) profile

    = user_profile.login_data client.user_info(organization_id, user_id, profile) end def client @client ||= HttpHelper.new end end
  31. Debug time (App code): 50 class Client::HttpHelper def user_info(organization_id, user_id,

    profile) params = { organization_id:, user_id:, profile: } request_auth(LOGIN_URL, timeout, ) end private def timeout if development? DEFAULT_TIMEOUT else ENV['TIMEOUT_SECONDS'].to_s end end def development? @host != PRODUCTION && @host != STAGING end def request_auth(url, timeout, params) uri = URI.parse(url) Net::HTTP.start(uri.hostname, uri.port, read_timeout: timeout, use_ssl: true) do |http| request = Net::HTTP::Post.new(uri.request_uri) request['authorization'] = "#{jwt}" http.request(request) # HERE IS THE ISSUE end rescue StandardError => re re.backtrace.join("\n") raise re end end
  32. Debug time (Net::Http Code): 51 module Net class HTTP <

    Protocol def request(req, body = nil, &block) # :yield: +response+ unless started? start { req['connection'] ||= 'close' return request(req, body, &block) } end if proxy_user() req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? end req.set_body_internal body res = transport_request(req, &block) if sspi_auth?(res) sspi_auth(req) res = transport_request(req, &block) end res end def transport_request(req) begin begin_transport req res = catch(:response) { ... res = HTTPResponse.read_new(@socket) ... } rescue Net::OpenTimeout raise rescue Net::ReadTimeout, IOError, EOFError, .... end ... rescue => exception D "Conn close because of error #{exception}" @socket.close if @socket raise exception end end def begin_transport(req) if @socket.closed? connect ... end def connect D "opening connection to #{conn_addr}:#{conn_port}..." s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { begin TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) rescue => e raise e, "Failed to open TCP connection to “... end end end end class Timeout def timeout(sec, klass = nil, message = nil) return yield(sec) if sec == nil or sec.zero? ... end end
  33. Let’s simplify the error: 53 Timeout.timeout(timeout, Timeout::Error, msg) do #

    more code end `timeout': undefined method `zero?' for an instance of String (NoMethodError)
  34. Let’s simplify the error: 54 def development? false # hostname

    == (“staging|prod”) end timeout = development? ? DEFAULT_TIMEOUT : ENV[‘TIMEOUT_SECONDS'].to_s
  35. Let’s simplify the error: 55 def development? false # hostname

    == (“staging|prod”) end timeout = development? ? DEFAULT_TIMEOUT : ENV[‘TIMEOUT_SECONDS'].to_s ENV[‘TIMEOUT_SECONDS’] => nil timeout = “”
  36. The focus of today 57 nil.to_s => "" "".zero? `timeout':

    undefined method `zero?' for an instance of String (NoMethodError)
  37. App Code: • LoginHelper#login • HttpHelper#user_info 58 Net::HTTP •Net::HTTP#request •

    #transport request • #begin_transport • #connect • Timeout#timeout Section 2
  38. App Code: • LoginHelper#login • HttpHelper#user_info • HttpHelper#timeout 59 Net::HTTP

    •Net::HTTP#request • #transport request • #begin_transport • #connect • Timeout#timeout Section 2
  39. 60 Experiment No. 1 class NilClass # EXPERIMENT 1 def

    method_missing(method, *args, &block) error_message = "===> Trying to call method `#{method}`" puts error_message puts caller.take(5).join("\n") puts "=====> args: #{args}" if args&.size > 0 puts "=====> block: #{block.source}" if block puts "=====> NoMethodError not raised" end def respond_to_missing?(method_name, include_private) false end end
  40. 61 Experiment No. 2 module NilTracker def method_missing(method, *args, &block)

    ... end def respond_to_missing?(method_name, false) false end end
  41. 62 Experiment No. 2 module NilTracker def method_missing(method, *args, &block)

    ... end def respond_to_missing?(method_name, false) false end end nil.extend(NilTracker)
  42. 63 Experiment No. 2 module NilTracker def method_missing(method, *args, &block)

    ... if raise_exception? puts "====> Raising NoMethodError <=====" raise NoMethodError.new(error_message) else puts "=====> NoMethodError ignored" puts "================================\n\n" end end def raise_exception? tracker_helper.raise_exception end end
  43. 66 Experiment No. 2 [1] pry(main)> ls nil NilTracker#methods: method_missing

    raise_exception= raise_exception? to_ary to_hash to_str tracker_helper NilClass#methods: & =~ inspect pretty_print_cycle to_a to_f to_i to_s === ^ nil? rationalize to_c to_h to_r |
  44. 70 For example: Section 3 • self-portrait • selfie In

    Spanish: • auto-retrato • selfie
  45. 71 Ruby Language evolution Section 3 Ruby 1.9 (December 25,

    2007) • Hash#fetch Ruby 2.3 (December 25, 2015) • & safe navigation operator • Hash#dig
  46. 72 Story time - Slow week at work Section 3

    - Rake task => development environment - Data Microservice => Web App - 10 minutes later
  47. 73 10 min later Section 3 - I login, go

    to the /info page - It’s broken
  48. 74 10 min later Section 3 - I login, go

    to the /info page - It’s broken “ActionView::Template::Error" "(undefined method `[]' for nil)"
  49. 75 10 min later Section 3 My good old friend

    nil, we meet again. “ActionView::Template::Error" "(undefined method `[]' for nil)"
  50. 76 What did I break? Section 3 - Localization task

    failed. - The tool to report the failure failed
  51. 77 How is the web app broken because of tags?

    Section 3 # Inside a view %span= @tags[:level][:label] # Inside a controller @tags = data[:tags] @labels = data[:labels]
  52. 78 How is the web app broken because of tags?

    Section 3 # Inside a view %span= @tags.dig(:level,:label) # Inside a controller @tags = data.fetch(:tags,{}) @labels = data.fetch(:labels,{})
  53. 79 What can we do about this? Section 3 module

    NilTracker def method_missing(method, *args, &block) nil_tracker.log(method, caller.take(5), args, block) end end
  54. 80 What can we do about this? Section 3 def

    log(method, caller_lines, *args, &block) error_message = "=====> Trying to call method ...” puts error_message puts "=====> Caller: (stacktrace)" puts caller.take(5).join("\n") puts "=====> args: #{args}" if args&.size > 0 puts "=====> block: #{block.source}" if block create_method(method, args, block) store_method_info(method, caller_lines, args, block) end
  55. Let’s wrap all this in a Gem 81 What would

    be a good name for Gem that - The gem defeats nil by creating dynamically methods - It’s like a self healing super power for the Ruby apps
  56. Let’s wrap all this in a Gem 82 What would

    be a good name for Gem that - The gem defeats nil by creating dynamically methods - It’s like a self healing super power for the Ruby apps Wolvernil
  57. 83 Wolvernil Section 3 def create_method(method, *args, &block) return if

    methods_list[method] Wolvernil.define_method(method) do |*args, &block| Puts "---> Calling a method #{method} in the nil class" end end def store_method_info(method, caller_lines, *args, &block) methods_list[method] = { args:, block:, caller_lines:, timestamp: Time.now } end
  58. 85 Experiment No. 3 Let’s run the experiment - Wolvernil

    [1] pry(#<RubyConf>)> ls nil Wolvernil#methods: []= help1 help2 help3 ]permitted? raise_exception= call method_missing process_wait raise_exception? to_ary to_hash to_str NilClass#methods: & =~ inspect pretty_print_cycle to_a to_f to_i to_s === ^ nil? rationalize to_c to_h to_r |
  59. Additional data: 86 { :help3 => { # method name

    :args => [ [0] [true], [1] #<Proc:0x000000011bd838f0 experiment_3.rb:121> ], :block => “{ puts \"Help is on the way\" }\n", :caller_lines => "experiment_3.rb:121:in `break_nil'\nexperiment_3.rb:129:in `<main>'", :timestamp => 2024-11-10 16:34:43.744649 -0800 } }
  60. params = { id: 1234, lesson_id: 2024 } def mark_lesson_complete(params)

    user = User.find_by(params[:user_id]) user&.complete(params[:lesson_id]) end mark_lesson_complete(params) 91 Intro
  61. 92 Experiment No. 3 Wolvernil + AI def mark_lesson_complete(user_id:, lesson_id:)

    user = User.find_by(id: user_id) user&.complete(lesson_id) end # Called with: mark_lesson_complete( user_id: params[:user_id], lesson_id: params[:lesson_id] )
  62. 95 Section 3 Dry::Struct require 'dry-struct' class User < Dry::Struct

    attribute :name, Types::String.optional attribute :age, Types::Coercible::Integer end user = User.new(name: nil, age: '21') user.name # nil user.age # 21
  63. extend T::Sig sig {params(x: String).void} def must_be_given_string(x) puts "Got string:

    #{x}" end sig {params(x: T.nilable(String)).void} def foo(x) must_be_given_string(x) # error: Expected `String` but found `T.nilable(String)` for argument `x` if x must_be_given_string(x) # ok end end 96 Section 3 Sorbet
  64. 100 Resources Section 3 1. Nothing is something: https://www.youtube.com/watch?v=OMPfEXIlTVE&ab_channel=Confreaks 2.

    The case of the missing method: https://www.youtube.com/watch?v=mn2D_k-X-es&ab_channel=Confreaks 3. Sorbet - https://sorbet.org/ 4. Dry::Struct - https://dry-rb.org/gems/dry-struct/1.0/recipes/ 5. Experimental approach: https://github.com/mogox/wolvernil 6. The rest of the code: https://github.com/mogox/talk_breaking_nil 7. Sugar cane picture (permission from the photographer granted for this presentation only): https://www.karemrodriguez.com/zafra-in-veracruz Contact info: @mogox