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

Hidden Gems

jeg2
September 05, 2008

Hidden Gems

Some ways to coax some extra speed out of Ruby processes.

jeg2

September 05, 2008
Tweet

More Decks by jeg2

Other Decks in Technology

Transcript

  1. Back at Lone Star ‣I am James Edward Gray II

    ‣Created the Ruby Quiz ‣released FasterCSV, HighLine, and Elif
  2. Back at Lone Star ‣I am James Edward Gray II

    ‣Created the Ruby Quiz ‣released FasterCSV, HighLine, and Elif ‣Wrote a couple of Pragmatic Programmer books with a lot of Ruby in them
  3. Back at Lone Star ‣I am James Edward Gray II

    ‣Created the Ruby Quiz ‣released FasterCSV, HighLine, and Elif ‣Wrote a couple of Pragmatic Programmer books with a lot of Ruby in them ‣I’ve given a talk at every LSRC so far
  4. Why BSG? ‣The old ship that was to be decommissioned

    becomes the only thing keeping the human race alive
  5. Why BSG? ‣The old ship that was to be decommissioned

    becomes the only thing keeping the human race alive ‣Those are my kind of odds
  6. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions
  7. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions ‣This is rarely actually needed though
  8. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions ‣This is rarely actually needed though ‣We can make use of libraries, some written in C, that help with our problem
  9. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions ‣This is rarely actually needed though ‣We can make use of libraries, some written in C, that help with our problem ‣We can add more processing power
  10. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions ‣This is rarely actually needed though ‣We can make use of libraries, some written in C, that help with our problem ‣We can add more processing power ‣We can rework our data structures to better support the task at hand
  11. Ruby is as Fast as we Want her to Be!

    ‣We can always write C extensions for speed-critical portions ‣This is rarely actually needed though ‣We can make use of libraries, some written in C, that help with our problem ‣We can add more processing power ‣We can rework our data structures to better support the task at hand ‣Always the big win, in my opinion
  12. Ruby in the Fast Lane ‣Let’s see how fast Ruby

    can run using: ‣NArray ‣SQLite
  13. Ruby in the Fast Lane ‣Let’s see how fast Ruby

    can run using: ‣NArray ‣SQLite ‣RBTree
  14. Ruby in the Fast Lane ‣Let’s see how fast Ruby

    can run using: ‣NArray ‣SQLite ‣RBTree ‣FSDB
  15. Ruby in the Fast Lane ‣Let’s see how fast Ruby

    can run using: ‣NArray ‣SQLite ‣RBTree ‣FSDB ‣Rinda
  16. Ruby in the Fast Lane ‣Let’s see how fast Ruby

    can run using: ‣NArray ‣SQLite ‣RBTree ‣FSDB ‣Rinda ‣Thinking outside the box
  17. When the Numbers Count ‣Ruby’s Numeric family of objects were

    built for ease of use ‣This makes them a bit slower
  18. When the Numbers Count ‣Ruby’s Numeric family of objects were

    built for ease of use ‣This makes them a bit slower ‣C’s numbers were built for speed
  19. When the Numbers Count ‣Ruby’s Numeric family of objects were

    built for ease of use ‣This makes them a bit slower ‣C’s numbers were built for speed ‣Ruby can borrow them with NArray
  20. Problem: Faster Imaging ‣I use a trivial PPM library to

    generate images for one project ‣The PPM code was taking about 1.3 seconds for a 400 by 200 pixel image
  21. Problem: Faster Imaging ‣I use a trivial PPM library to

    generate images for one project ‣The PPM code was taking about 1.3 seconds for a 400 by 200 pixel image ‣I replaced a two dimensional Array of Color objects with a 3D NArray
  22. Problem: Faster Imaging ‣I use a trivial PPM library to

    generate images for one project ‣The PPM code was taking about 1.3 seconds for a 400 by 200 pixel image ‣I replaced a two dimensional Array of Color objects with a 3D NArray ‣I changed less than ten lines of code
  23. Problem: Faster Imaging ‣I use a trivial PPM library to

    generate images for one project ‣The PPM code was taking about 1.3 seconds for a 400 by 200 pixel image ‣I replaced a two dimensional Array of Color objects with a 3D NArray ‣I changed less than ten lines of code ‣The speed on the same image dropped to about 1/100th of a second
  24. Creating The Canvas def initialize(options = Hash.new) options = DEFAULT_OPTIONS.merge(options)

    @width = options[:width] @height = options[:height] @background = options[:background] @foreground = options[:foreground] @mode = options[:mode] @canvas = Array.new(@height) { Array.new(@width) { @background } } end
  25. Creating The Canvas def initialize(options = Hash.new) options = DEFAULT_OPTIONS.merge(options)

    @width = options[:width] @height = options[:height] @background = options[:background] @foreground = options[:foreground] @mode = options[:mode] @canvas = Array.new(@height) { Array.new(@width) { @background } } end
  26. Creating The Canvas def initialize(options = Hash.new) require "rubygems" require

    "narray" ! options = DEFAULT_OPTIONS.merge(options) @width = options[:width] @height = options[:height] @background = options[:background] @foreground = options[:foreground] @mode = options[:mode] @canvas = NArray.byte(@width, @height, 3) end
  27. Creating The Canvas def initialize(options = Hash.new) require "rubygems" require

    "narray" ! options = DEFAULT_OPTIONS.merge(options) @width = options[:width] @height = options[:height] @background = options[:background] @foreground = options[:foreground] @mode = options[:mode] @canvas = NArray.byte(@width, @height, 3) end
  28. Marking Pixels def draw_point(x, y, color = @foreground) return unless

    x.between? 0, @width - 1 return unless y.between? 0, @height - 1 ! @canvas[y][x] = color end
  29. Marking Pixels def draw_point(x, y, color = @foreground) return unless

    x.between? 0, @width - 1 return unless y.between? 0, @height - 1 ! @canvas[y][x] = color end
  30. Marking Pixels def draw_point(x, y, color = @foreground) return unless

    x.between? 0, @width - 1 return unless y.between? 0, @height - 1 ! @canvas[x, y, 0..2] = color.to_a end
  31. Marking Pixels def draw_point(x, y, color = @foreground) return unless

    x.between? 0, @width - 1 return unless y.between? 0, @height - 1 ! @canvas[x, y, 0..2] = color.to_a end
  32. Drawing an Image def save(file) File.open(file.sub(/\.ppm$/i, "") + ".ppm", "w")

    do |image| image.puts @mode image.puts "#{@width} #{@height} 255" @canvas.each do |row| pixels = row.map { |pixel| pixel.to_s(@mode) } image.send( @mode == "P6" ? :print : :puts, pixels.join(@mode == "P6" ? "" : " ") ) end end end
  33. Drawing an Image def save(file) File.open(file.sub(/\.ppm$/i, "") + ".ppm", "w")

    do |image| image.puts @mode image.puts "#{@width} #{@height} 255" @canvas.each do |row| pixels = row.map { |pixel| pixel.to_s(@mode) } image.send( @mode == "P6" ? :print : :puts, pixels.join(@mode == "P6" ? "" : " ") ) end end end
  34. Drawing an Image def save(file) File.open(file.sub(/\.ppm$/i, "") + ".ppm", "w")

    do |image| image.puts @mode image.puts "#{@width} #{@height} 255" 0.upto(@height - 1) do |y| row = @canvas[0...@width, y, 0..2].transpose(-1, 0) image.send( @mode == "P6" ? :print : :puts, @mode == "P6" ? row.to_s : row.to_a.join(" ") ) end end end
  35. Drawing an Image def save(file) File.open(file.sub(/\.ppm$/i, "") + ".ppm", "w")

    do |image| image.puts @mode image.puts "#{@width} #{@height} 255" 0.upto(@height - 1) do |y| row = @canvas[0...@width, y, 0..2].transpose(-1, 0) image.send( @mode == "P6" ? :print : :puts, @mode == "P6" ? row.to_s : row.to_a.join(" ") ) end end end
  36. Other Nice Features ‣NArray supports integers, floats, and even complex

    numbers in various sizes ‣Aside from indexing and iteration, NArray supports data generation, arithmetic operations, comparisons, bitwise manipulations, statistic calculations, and more
  37. Other Nice Features ‣NArray supports integers, floats, and even complex

    numbers in various sizes ‣Aside from indexing and iteration, NArray supports data generation, arithmetic operations, comparisons, bitwise manipulations, statistic calculations, and more ‣View the large API with examples At: http://narray.rubyforge.org/
  38. Conway’s Game of Life #!/usr/bin/env ruby -wKU ! require "rubygems"

    require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  39. Conway’s Game of Life #!/usr/bin/env ruby -wKU ! require "rubygems"

    require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  40. Conway’s Game of Life NArray.byte(5,5): [ [ 0, 0, 0,

    0, 0 ], [ 0, 1, 0, 1, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 1, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] #!/usr/bin/env ruby -wKU ! require "rubygems" require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  41. Conway’s Game of Life NArray.byte(5,5): [ [ 0, 0, 0,

    0, 0 ], [ 0, 1, 0, 1, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 1, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] #!/usr/bin/env ruby -wKU ! require "rubygems" require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  42. Conway’s Game of Life NArray.byte(5,5): [ [ 0, 0, 0,

    0, 0 ], [ 0, 1, 0, 1, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 1, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] NArray.byte(5,5): [ [ 0, 0, 0, 0, 0 ], [ 0, 0, 2, 0, 0 ], [ 0, 3, 4, 2, 0 ], [ 0, 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0 ] ] #!/usr/bin/env ruby -wKU ! require "rubygems" require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  43. Conway’s Game of Life NArray.byte(5,5): [ [ 0, 0, 0,

    0, 0 ], [ 0, 1, 0, 1, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 1, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] NArray.byte(5,5): [ [ 0, 0, 0, 0, 0 ], [ 0, 0, 2, 0, 0 ], [ 0, 3, 4, 2, 0 ], [ 0, 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0 ] ] #!/usr/bin/env ruby -wKU ! require "rubygems" require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  44. Conway’s Game of Life NArray.byte(5,5): [ [ 0, 0, 0,

    0, 0 ], [ 0, 1, 0, 1, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 1, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] NArray.byte(5,5): [ [ 0, 0, 0, 0, 0 ], [ 0, 0, 2, 0, 0 ], [ 0, 3, 4, 2, 0 ], [ 0, 1, 1, 1, 0 ], [ 0, 0, 0, 0, 0 ] ] NArray.byte(5,5): [ [ 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 1, 0, 0, 0 ], [ 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0 ] ] #!/usr/bin/env ruby -wKU ! require "rubygems" require "narray" ! # build cells life = NArray.byte(5, 5) life[1, 1] = NArray.byte(3, 3).random!(2) p life ! # count neighbors counts = NArray.byte(*life.shape) counts[1..-2, 1..-2] = life[0..-3, 0..-3] + life[0..-3, 1..-2] + life[0..-3, 2..-1] + life[1..-2, 0..-3] + life[1..-2, 2..-1] + life[2..-1, 0..-3] + life[2..-1, 1..-2] + life[2..-1, 2..-1] p counts ! # one step of the game life[] = counts.eq(3) | (counts.eq(2) & life) p life
  45. Thinking About Data can be Hard ‣SQLite has already solved

    many hard problems for data storage and retrieval
  46. Thinking About Data can be Hard ‣SQLite has already solved

    many hard problems for data storage and retrieval ‣It gives you an entire language to express your data needs
  47. Problem: IP to Country ‣Given an IP address, return the

    country for that IP ‣this was a Ruby Quiz
  48. Problem: IP to Country ‣Given an IP address, return the

    country for that IP ‣this was a Ruby Quiz ‣Solutions were also expected to be efficient in memory and speed
  49. Problem: IP to Country ‣Given an IP address, return the

    country for that IP ‣this was a Ruby Quiz ‣Solutions were also expected to be efficient in memory and speed ‣This is a real world task I’ve had to do for my job
  50. The Data # © 2002-2008 Webnet77.com # # # #

    "0","16777215","IANA","410227200","ZZ","ZZZ","RESERVED" "50331648","67108863","ARIN","572572800","US","USA","UNITED STATES" "67108864","83886079","ARIN","0","US","USA","UNITED STATES" "100663296","117440511","ARIN","0","US","USA","UNITED STATES" "117440512","134217727","ARIN","880329600","US","USA","UNITED STATES"
  51. The Data # © 2002-2008 Webnet77.com # # # #

    "0","16777215","IANA","410227200","ZZ","ZZZ","RESERVED" "50331648","67108863","ARIN","572572800","US","USA","UNITED STATES" "67108864","83886079","ARIN","0","US","USA","UNITED STATES" "100663296","117440511","ARIN","0","US","USA","UNITED STATES" "117440512","134217727","ARIN","880329600","US","USA","UNITED STATES"
  52. Solutions ‣Many solved the problem with a binary search on

    the file ‣Most of those prepossessed the file to make that search easier
  53. Solutions ‣Many solved the problem with a binary search on

    the file ‣Most of those prepossessed the file to make that search easier ‣I’m going to show a SQLite solution
  54. Solutions ‣Many solved the problem with a binary search on

    the file ‣Most of those prepossessed the file to make that search easier ‣I’m going to show a SQLite solution ‣It’s very close to the same speed (about 1/3rd of a second to lookup an IP)
  55. Solutions ‣Many solved the problem with a binary search on

    the file ‣Most of those prepossessed the file to make that search easier ‣I’m going to show a SQLite solution ‣It’s very close to the same speed (about 1/3rd of a second to lookup an IP) ‣I didn’t have to be clever or even add an index
  56. Solutions ‣Many solved the problem with a binary search on

    the file ‣Most of those prepossessed the file to make that search easier ‣I’m going to show a SQLite solution ‣It’s very close to the same speed (about 1/3rd of a second to lookup an IP) ‣I didn’t have to be clever or even add an index ‣It was easier for me to use full country names
  57. Setup #!/usr/bin/env ruby -KU ! require "open-uri" require "zlib" !

    require "rubygems" require "faster_csv" require "sqlite3" ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" LOCAL_DB = "country_ips.sqlite" ! File.unlink(LOCAL_DB) if ARGV.delete("-r") and File.exist? LOCAL_DB ! # ...
  58. Build The Database # ... ! unless File.exist? LOCAL_DB db

    = SQLite3::Database.new(LOCAL_DB) db.execute(<<-END_TABLE.strip) CREATE TABLE ips ( low_ip INTEGER, high_ip INTEGER, country TEXT ) END_TABLE open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ args = FCSV.parse_line(line).values_at(0..1, 6) db.execute(<<-END_INSERT.strip, *args) INSERT INTO ips( low_ip, high_ip, country) VALUES( ?, ?, ?) END_INSERT end end db.close end ! # ...
  59. Build The Database # ... ! unless File.exist? LOCAL_DB db

    = SQLite3::Database.new(LOCAL_DB) db.execute(<<-END_TABLE.strip) CREATE TABLE ips ( low_ip INTEGER, high_ip INTEGER, country TEXT ) END_TABLE open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ args = FCSV.parse_line(line).values_at(0..1, 6) db.execute(<<-END_INSERT.strip, *args) INSERT INTO ips( low_ip, high_ip, country) VALUES( ?, ?, ?) END_INSERT end end db.close end ! # ...
  60. Build The Database # ... ! unless File.exist? LOCAL_DB db

    = SQLite3::Database.new(LOCAL_DB) db.execute(<<-END_TABLE.strip) CREATE TABLE ips ( low_ip INTEGER, high_ip INTEGER, country TEXT ) END_TABLE open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ args = FCSV.parse_line(line).values_at(0..1, 6) db.execute(<<-END_INSERT.strip, *args) INSERT INTO ips( low_ip, high_ip, country) VALUES( ?, ?, ?) END_INSERT end end db.close end ! # ...
  61. Query # ... ! ip = ARGV.shift or abort "Usage:

    #{File.basename($PROGRAM_NAME)} IP" ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first db = SQLite3::Database.new(LOCAL_DB) ! puts db.get_first_value(<<-END_SELECT.strip, :ip => ip_int) || "Unknown" SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT
  62. Query # ... ! ip = ARGV.shift or abort "Usage:

    #{File.basename($PROGRAM_NAME)} IP" ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first db = SQLite3::Database.new(LOCAL_DB) ! puts db.get_first_value(<<-END_SELECT.strip, :ip => ip_int) || "Unknown" SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT
  63. Did You Know? ‣SQLite is totally free ‣You can receive

    query results in a Hash to index by column name and/or convert column values to Ruby objects based on type
  64. Did You Know? ‣SQLite is totally free ‣You can receive

    query results in a Hash to index by column name and/or convert column values to Ruby objects based on type ‣It can work with in-memory databases
  65. Did You Know? ‣SQLite is totally free ‣You can receive

    query results in a Hash to index by column name and/or convert column values to Ruby objects based on type ‣It can work with in-memory databases ‣It can run queries across tables in multiple database files
  66. Did You Know? ‣SQLite is totally free ‣You can receive

    query results in a Hash to index by column name and/or convert column values to Ruby objects based on type ‣It can work with in-memory databases ‣It can run queries across tables in multiple database files ‣You can define SQL functions and aggregates for it in Ruby code
  67. Ruby Friendly Data #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! country = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} COUNTRY" db = SQLite3::Database.new("country_ips.sqlite") db.results_as_hash = true db.type_translation = true ! db.execute( "SELECT * FROM ips WHERE country LIKE ?", "%#{country}%" ) do |match| low, high = match.values_at("low_ip", "high_ip"). map { |i| [i].pack("N").unpack("C*").join(".") } puts "%s: %15s - %15s" % [match["country"], low, high] end
  68. Ruby Friendly Data #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! country = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} COUNTRY" db = SQLite3::Database.new("country_ips.sqlite") db.results_as_hash = true db.type_translation = true ! db.execute( "SELECT * FROM ips WHERE country LIKE ?", "%#{country}%" ) do |match| low, high = match.values_at("low_ip", "high_ip"). map { |i| [i].pack("N").unpack("C*").join(".") } puts "%s: %15s - %15s" % [match["country"], low, high] end
  69. Ruby Friendly Data #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! country = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} COUNTRY" db = SQLite3::Database.new("country_ips.sqlite") db.results_as_hash = true db.type_translation = true ! db.execute( "SELECT * FROM ips WHERE country LIKE ?", "%#{country}%" ) do |match| low, high = match.values_at("low_ip", "high_ip"). map { |i| [i].pack("N").unpack("C*").join(".") } puts "%s: %15s - %15s" % [match["country"], low, high] end
  70. In-Memory #!/usr/bin/env ruby -KU ! # ... requires unchanged ...

    ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" ! db = SQLite3::Database.new(":memory:") ! # ... database loading unchanged ... ! stmt = db.prepare(<<-END_SELECT.strip) SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first puts stmt.execute(:ip => ip_int).each { |c| break c[0] } || "Unknown" else break end end
  71. In-Memory #!/usr/bin/env ruby -KU ! # ... requires unchanged ...

    ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" ! db = SQLite3::Database.new(":memory:") ! # ... database loading unchanged ... ! stmt = db.prepare(<<-END_SELECT.strip) SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first puts stmt.execute(:ip => ip_int).each { |c| break c[0] } || "Unknown" else break end end
  72. In-Memory #!/usr/bin/env ruby -KU ! # ... requires unchanged ...

    ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" ! db = SQLite3::Database.new(":memory:") ! # ... database loading unchanged ... ! stmt = db.prepare(<<-END_SELECT.strip) SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first puts stmt.execute(:ip => ip_int).each { |c| break c[0] } || "Unknown" else break end end
  73. In-Memory #!/usr/bin/env ruby -KU ! # ... requires unchanged ...

    ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" ! db = SQLite3::Database.new(":memory:") ! # ... database loading unchanged ... ! stmt = db.prepare(<<-END_SELECT.strip) SELECT country FROM ips WHERE low_ip <= :ip AND :ip <= high_ip END_SELECT loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first puts stmt.execute(:ip => ip_int).each { |c| break c[0] } || "Unknown" else break end end
  74. Attach and Functions #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! user = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} USER" db = SQLite3::Database.new("users.sqlite") db.execute("ATTACH DATABASE 'country_ips.sqlite' AS country_ips") ! db.create_function("IP2INT", 1) do |func, ip| func.result = ip.to_s.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first end ! sql = <<END_SQL SELECT users.name, users.ip, ips.country FROM users INNER JOIN ips ON ips.low_ip <= IP2INT(users.ip) AND IP2INT(users.ip) <= ips.high_ip WHERE users.name LIKE ? LIMIT 1 END_SQL puts db.get_first_row(sql, "%#{user}%").join(", ")
  75. Attach and Functions #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! user = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} USER" db = SQLite3::Database.new("users.sqlite") db.execute("ATTACH DATABASE 'country_ips.sqlite' AS country_ips") ! db.create_function("IP2INT", 1) do |func, ip| func.result = ip.to_s.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first end ! sql = <<END_SQL SELECT users.name, users.ip, ips.country FROM users INNER JOIN ips ON ips.low_ip <= IP2INT(users.ip) AND IP2INT(users.ip) <= ips.high_ip WHERE users.name LIKE ? LIMIT 1 END_SQL puts db.get_first_row(sql, "%#{user}%").join(", ")
  76. Attach and Functions #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! user = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} USER" db = SQLite3::Database.new("users.sqlite") db.execute("ATTACH DATABASE 'country_ips.sqlite' AS country_ips") ! db.create_function("IP2INT", 1) do |func, ip| func.result = ip.to_s.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first end ! sql = <<END_SQL SELECT users.name, users.ip, ips.country FROM users INNER JOIN ips ON ips.low_ip <= IP2INT(users.ip) AND IP2INT(users.ip) <= ips.high_ip WHERE users.name LIKE ? LIMIT 1 END_SQL puts db.get_first_row(sql, "%#{user}%").join(", ")
  77. Attach and Functions #!/usr/bin/env ruby -KU ! require "rubygems" require

    "sqlite3" ! user = ARGV.shift or abort "Usage: #{File.basename($PROGRAM_NAME)} USER" db = SQLite3::Database.new("users.sqlite") db.execute("ATTACH DATABASE 'country_ips.sqlite' AS country_ips") ! db.create_function("IP2INT", 1) do |func, ip| func.result = ip.to_s.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first end ! sql = <<END_SQL SELECT users.name, users.ip, ips.country FROM users INNER JOIN ips ON ips.low_ip <= IP2INT(users.ip) AND IP2INT(users.ip) <= ips.high_ip WHERE users.name LIKE ? LIMIT 1 END_SQL puts db.get_first_row(sql, "%#{user}%").join(", ")
  78. Sometimes you Just Need the big Guns ‣Binary search and

    binary trees are pretty big guns in computing
  79. Sometimes you Just Need the big Guns ‣Binary search and

    binary trees are pretty big guns in computing ‣RBTree provides a super efficient binary tree implementation
  80. Sometimes you Just Need the big Guns ‣Binary search and

    binary trees are pretty big guns in computing ‣RBTree provides a super efficient binary tree implementation ‣It’s written in C
  81. Another Solution: IP to Country ‣This time we will use

    a real binary search ‣But we don’t have to write it
  82. Another Solution: IP to Country ‣This time we will use

    a real binary search ‣But we don’t have to write it ‣This drops the search time below 1/1,000th of a second
  83. Another Solution: IP to Country ‣This time we will use

    a real binary search ‣But we don’t have to write it ‣This drops the search time below 1/1,000th of a second ‣We will Marshal RBTree to build our persistent database
  84. Another Solution: IP to Country ‣This time we will use

    a real binary search ‣But we don’t have to write it ‣This drops the search time below 1/1,000th of a second ‣We will Marshal RBTree to build our persistent database ‣We will use RBTree’s bounds search methods to perform the search
  85. The Same Setup #!/usr/bin/env ruby -wKU ! require "open-uri" require

    "zlib" ! require "rubygems" require "faster_csv" require "rbtree" ! REMOTE_DB = "http://software77.net/cgi-bin/" + "ip-country/geo-ip.pl?action=download" LOCAL_DB = "country_ips.marshal" ! File.unlink(LOCAL_DB) if ARGV.delete("-r") and File.exist? LOCAL_DB ! # ...
  86. An Easier Load # ... ! unless File.exist? LOCAL_DB ips

    = RBTree.new open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ low, high, country = FCSV.parse_line(line).values_at(0..1, 6) ips[Integer(low)] = [Integer(high), country] end end File.open(LOCAL_DB, "wb") { |file| Marshal.dump(ips, file) } end ! # ...
  87. An Easier Load # ... ! unless File.exist? LOCAL_DB ips

    = RBTree.new open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ low, high, country = FCSV.parse_line(line).values_at(0..1, 6) ips[Integer(low)] = [Integer(high), country] end end File.open(LOCAL_DB, "wb") { |file| Marshal.dump(ips, file) } end ! # ...
  88. An Easier Load # ... ! unless File.exist? LOCAL_DB ips

    = RBTree.new open(REMOTE_DB) do |url| Zlib::GzipReader.new(url).each do |line| next if line =~ /\A\s*(?:#|\z)/ low, high, country = FCSV.parse_line(line).values_at(0..1, 6) ips[Integer(low)] = [Integer(high), country] end end File.open(LOCAL_DB, "wb") { |file| Marshal.dump(ips, file) } end ! # ...
  89. A Much Faster Search # ... ! ips = File.open(LOCAL_DB,

    "rb") { |file| Marshal.load(file) } loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first match = ips.upper_bound(ip_int) puts match && ip_int <= match.last.first ? match.last.last : "Unknown" else break end end
  90. A Much Faster Search # ... ! ips = File.open(LOCAL_DB,

    "rb") { |file| Marshal.load(file) } loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first match = ips.upper_bound(ip_int) puts match && ip_int <= match.last.first ? match.last.last : "Unknown" else break end end
  91. A Much Faster Search # ... ! ips = File.open(LOCAL_DB,

    "rb") { |file| Marshal.load(file) } loop do print "IP address? " ip = gets.to_s.strip if ip =~ /\S/ ip_int = ip.split(".").map { |n| Integer(n) }. pack("C*").unpack("N").first match = ips.upper_bound(ip_int) puts match && ip_int <= match.last.first ? match.last.last : "Unknown" else break end end
  92. Other Nice Features ‣RBTree is pretty much a drop in

    replacement for a Hash you want to keep ordered by keys
  93. Other Nice Features ‣RBTree is pretty much a drop in

    replacement for a Hash you want to keep ordered by keys ‣Just having RBTree available magically speeds up Ruby SortedSet (over 15 times faster for simple iteration) in the standard library
  94. An Ordered Hash #!/usr/bin/env ruby -wKU ! require "rubygems" require

    "rbtree" ! ordered_hash = RBTree.new ! ordered_hash[2] = "two" ordered_hash[1] = "one" ordered_hash[3] = "three" ! ordered_hash.each do |key, value| puts "#{key}: #{value}" end # >> 1: one # >> 2: two # >> 3: three
  95. Magically Improving SortedSet #!/usr/bin/env ruby -wKU ! # pass an

    argument, like --rbtree, and this sets up the load require "rubygems" unless ARGV.empty? ! require "set" dictionary = SortedSet.new File.foreach("/usr/share/dict/words") do |word| dictionary << word.strip if word =~ /\S/ end ! start = Time.now dictionary.to_a # force the set into order puts "Time to order: #{Time.now - start}"
  96. Stay Flexible ‣Data can be in many different formats and

    related in many different ways ‣FSDB gives you a lot of flexibility in these areas
  97. Stay Flexible ‣Data can be in many different formats and

    related in many different ways ‣FSDB gives you a lot of flexibility in these areas ‣Get from: http:// redshift.sourcefor ge.net/fsdb/
  98. Problem: Server Monitoring ‣At my job we do a lot

    of server monitoring ‣We collect various statistics from servers at regular intervals
  99. Problem: Server Monitoring ‣At my job we do a lot

    of server monitoring ‣We collect various statistics from servers at regular intervals ‣We later analyze this data for spikes and trends
  100. Problem: Server Monitoring ‣At my job we do a lot

    of server monitoring ‣We collect various statistics from servers at regular intervals ‣We later analyze this data for spikes and trends ‣Time Series data is one thing RDBMS don’t do well
  101. A Solution ‣Store the data so it is easy to

    focus in on the parts that matter to you now
  102. A Solution ‣Store the data so it is easy to

    focus in on the parts that matter to you now ‣FSDB is essentially a Hash backed by the filesystem
  103. A Solution ‣Store the data so it is easy to

    focus in on the parts that matter to you now ‣FSDB is essentially a Hash backed by the filesystem ‣This allows you to use paths to drill down to subsets of the data
  104. A Solution ‣Store the data so it is easy to

    focus in on the parts that matter to you now ‣FSDB is essentially a Hash backed by the filesystem ‣This allows you to use paths to drill down to subsets of the data ‣It avoids irrelevant data and even the need for an index in some cases
  105. A Solution ‣Store the data so it is easy to

    focus in on the parts that matter to you now ‣FSDB is essentially a Hash backed by the filesystem ‣This allows you to use paths to drill down to subsets of the data ‣It avoids irrelevant data and even the need for an index in some cases ‣Techniques like this improved our graphing speed from almost four seconds to well under one
  106. Creating a Database #!/usr/bin/env ruby -wKU ! require "fsdb" !

    module TimeSeries DB = FSDB::Database.new("server_stats/") ! module_function def record(data, time = Time.now) DB[time.strftime("%Y/%m/%d/%H/%M.obj")] = data end # ...
  107. Creating a Database #!/usr/bin/env ruby -wKU ! require "fsdb" !

    module TimeSeries DB = FSDB::Database.new("server_stats/") ! module_function def record(data, time = Time.now) DB[time.strftime("%Y/%m/%d/%H/%M.obj")] = data end # ...
  108. Creating a Database #!/usr/bin/env ruby -wKU ! require "fsdb" !

    module TimeSeries DB = FSDB::Database.new("server_stats/") ! module_function def record(data, time = Time.now) DB[time.strftime("%Y/%m/%d/%H/%M.obj")] = data end # ...
  109. Query # ... def sum(year, *args, &block) total = 0

    path = [year, *args][0..5].join("/").gsub(/\b\d\b/, '0\0') if File.extname(path) == ".obj" total += block[DB[path]] else (DB[path] || []).each do |new_path| total += sum(File.join(path, new_path), &block) end end total end def average(*args) count = 0 sum(*args) { |data| count += 1; yield data } / count.to_f end end ! # ...
  110. Query # ... def sum(year, *args, &block) total = 0

    path = [year, *args][0..5].join("/").gsub(/\b\d\b/, '0\0') if File.extname(path) == ".obj" total += block[DB[path]] else (DB[path] || []).each do |new_path| total += sum(File.join(path, new_path), &block) end end total end def average(*args) count = 0 sum(*args) { |data| count += 1; yield data } / count.to_f end end ! # ...
  111. Query # ... def sum(year, *args, &block) total = 0

    path = [year, *args][0..5].join("/").gsub(/\b\d\b/, '0\0') if File.extname(path) == ".obj" total += block[DB[path]] else (DB[path] || []).each do |new_path| total += sum(File.join(path, new_path), &block) end end total end def average(*args) count = 0 sum(*args) { |data| count += 1; yield data } / count.to_f end end ! # ...
  112. Query # ... def sum(year, *args, &block) total = 0

    path = [year, *args][0..5].join("/").gsub(/\b\d\b/, '0\0') if File.extname(path) == ".obj" total += block[DB[path]] else (DB[path] || []).each do |new_path| total += sum(File.join(path, new_path), &block) end end total end def average(*args) count = 0 sum(*args) { |data| count += 1; yield data } / count.to_f end end ! # ...
  113. sample Usage # ... ! if __FILE__ == $PROGRAM_NAME include

    TimeSeries record( { :load_average => 70, :disk_free => 50 }, Time.local(2008, 9, 4, 12, 0) ) record( { :load_average => 10, :disk_free => 50 }, Time.local(2008, 9, 4, 12, 30) ) record( { :load_average => 20, :disk_free => 51 }, Time.local(2008, 9, 4, 13, 0) ) p average(2008, 9, 4) { |data| data[:load_average] } # >> 33.3333333333333 p average(2008, 9, 4, 12) { |data| data[:load_average] } # >> 40.0 end
  114. Other Nice Features ‣FSDB is multi-thread and multi-process safe on

    most platforms ‣It supports read only and read/write transactions and they can even be nested
  115. Other Nice Features ‣FSDB is multi-thread and multi-process safe on

    most platforms ‣It supports read only and read/write transactions and they can even be nested ‣You can define your own formats for files
  116. Transactions #!/usr/bin/env ruby -wKU ! require "fsdb" db = FSDB::Database.new("server_stats/")

    ! # a read only transaction (shared lock) db.browse "2008/09/04/12/00.obj" do |data| p data[:load_average] # >> 70 p data[:disk_free] # >> 50 end ! # a read/write transaction (exclusive lock) db.replace "2008/09/04/12/00.obj" do |data| data.merge(:uptime => 21 * 60) end ! p db["2008/09/04/12/00.obj"][:uptime] # >> 1260
  117. Transactions #!/usr/bin/env ruby -wKU ! require "fsdb" db = FSDB::Database.new("server_stats/")

    ! # a read only transaction (shared lock) db.browse "2008/09/04/12/00.obj" do |data| p data[:load_average] # >> 70 p data[:disk_free] # >> 50 end ! # a read/write transaction (exclusive lock) db.replace "2008/09/04/12/00.obj" do |data| data.merge(:uptime => 21 * 60) end ! p db["2008/09/04/12/00.obj"][:uptime] # >> 1260
  118. Transactions #!/usr/bin/env ruby -wKU ! require "fsdb" db = FSDB::Database.new("server_stats/")

    ! # a read only transaction (shared lock) db.browse "2008/09/04/12/00.obj" do |data| p data[:load_average] # >> 70 p data[:disk_free] # >> 50 end ! # a read/write transaction (exclusive lock) db.replace "2008/09/04/12/00.obj" do |data| data.merge(:uptime => 21 * 60) end ! p db["2008/09/04/12/00.obj"][:uptime] # >> 1260
  119. A Custom Format #!/usr/bin/env ruby -wKU ! require "fsdb" db

    = FSDB::Database.new("images/") ! PNG_FORMAT = FSDB::Format.new( /\.png\z/i, :binary, :name => "PNG_FORMAT", :load => lambda { |f| f.seek(16) w, h = f.read(8).unpack("N2") {:width => w, :height => h} }, :dump => lambda { raise "Read only format." } ) db.formats = [PNG_FORMAT] ! db.browse_each_child "/" do |image, details| puts "%s: %p" % [image, details] end # >> /fsdb_tree.png: {:width=>727, :height=>249} # >> /ip_to_country_quiz.png: {:width=>798, :height=>732} # >> /scout.png: {:width=>865, :height=>713}
  120. A Custom Format #!/usr/bin/env ruby -wKU ! require "fsdb" db

    = FSDB::Database.new("images/") ! PNG_FORMAT = FSDB::Format.new( /\.png\z/i, :binary, :name => "PNG_FORMAT", :load => lambda { |f| f.seek(16) w, h = f.read(8).unpack("N2") {:width => w, :height => h} }, :dump => lambda { raise "Read only format." } ) db.formats = [PNG_FORMAT] ! db.browse_each_child "/" do |image, details| puts "%s: %p" % [image, details] end # >> /fsdb_tree.png: {:width=>727, :height=>249} # >> /ip_to_country_quiz.png: {:width=>798, :height=>732} # >> /scout.png: {:width=>865, :height=>713}
  121. A Custom Format #!/usr/bin/env ruby -wKU ! require "fsdb" db

    = FSDB::Database.new("images/") ! PNG_FORMAT = FSDB::Format.new( /\.png\z/i, :binary, :name => "PNG_FORMAT", :load => lambda { |f| f.seek(16) w, h = f.read(8).unpack("N2") {:width => w, :height => h} }, :dump => lambda { raise "Read only format." } ) db.formats = [PNG_FORMAT] ! db.browse_each_child "/" do |image, details| puts "%s: %p" % [image, details] end # >> /fsdb_tree.png: {:width=>727, :height=>249} # >> /ip_to_country_quiz.png: {:width=>798, :height=>732} # >> /scout.png: {:width=>865, :height=>713}
  122. The Importance of Networking ‣When you need more processing power,

    you have to start hooking CPU’s together ‣Rinda can make the communication between processes a snap
  123. Problem: Descrambling ‣Given some scrambled letters, find all dictionary words

    that can be formed by rearranging those letters ‣This task represents any task that just needs some processing time to sort out
  124. Problem: Descrambling ‣Given some scrambled letters, find all dictionary words

    that can be formed by rearranging those letters ‣This task represents any task that just needs some processing time to sort out ‣All this requires is some I/O and simple comparisons
  125. The Trivial Solution #!/usr/bin/env ruby -wKU ! class String def

    signature strip.downcase.delete("^a-z").split("").sort.join end end ! pattern = ARGV.shift.signature descrambled = [ ] ! File.foreach("/usr/share/dict/words") do |word| descrambled << word if word.signature == pattern end ! puts descrambled
  126. The Trivial Solution #!/usr/bin/env ruby -wKU ! class String def

    signature strip.downcase.delete("^a-z").split("").sort.join end end ! pattern = ARGV.shift.signature descrambled = [ ] ! File.foreach("/usr/share/dict/words") do |word| descrambled << word if word.signature == pattern end ! puts descrambled
  127. Dividing the Work ‣Heavy processing almost always benefits from more

    processes doing the work ‣This is true multiprocessing, unlike Ruby’s thread model
  128. Dividing the Work ‣Heavy processing almost always benefits from more

    processes doing the work ‣This is true multiprocessing, unlike Ruby’s thread model ‣Rinda’s TupleSpace makes the Inter- Process Communication super easy
  129. Dividing the Work ‣Heavy processing almost always benefits from more

    processes doing the work ‣This is true multiprocessing, unlike Ruby’s thread model ‣Rinda’s TupleSpace makes the Inter- Process Communication super easy ‣This task is I/O bound, but it still halved the time to put four processes on it
  130. Setup #!/usr/bin/env ruby -wKU ! require "rinda/tuplespace" ! class String

    def signature strip.downcase.delete("^a-z").split("").sort.join end end ! DICT = "/usr/share/dict/words" workers = ARGV.first =~ /\A\d+\z/ ? ARGV.shift.to_i : 4 pattern = ARGV.shift.signature chunk_size = File.stat(DICT).size / workers ! # ...
  131. Spawn Workers # ... ! workers.times do |n| fork do

    descrambled = [ ] File.open(DICT) do |words| my_start = chunk_size * n my_end = my_start + chunk_size words.seek(my_start) words.gets unless my_start.zero? words.each do |word| descrambled << word if word.signature == pattern break if words.pos > my_end end end ! results = Rinda::TupleSpaceProxy.new( DRbObject.new_with_uri("druby://localhost:61676") ) results.write([pattern, descrambled]) end end ! # ...
  132. Spawn Workers # ... ! workers.times do |n| fork do

    descrambled = [ ] File.open(DICT) do |words| my_start = chunk_size * n my_end = my_start + chunk_size words.seek(my_start) words.gets unless my_start.zero? words.each do |word| descrambled << word if word.signature == pattern break if words.pos > my_end end end ! results = Rinda::TupleSpaceProxy.new( DRbObject.new_with_uri("druby://localhost:61676") ) results.write([pattern, descrambled]) end end ! # ...
  133. Collect Results # ... ! results = Rinda::TupleSpace.new DRb.start_service("druby://localhost:61676", results)

    workers.times do descrambled = results.take([/\b#{Regexp.escape(pattern)}\b/, Array]) puts descrambled.last end Process.waitall
  134. Collect Results # ... ! results = Rinda::TupleSpace.new DRb.start_service("druby://localhost:61676", results)

    workers.times do descrambled = results.take([/\b#{Regexp.escape(pattern)}\b/, Array]) puts descrambled.last end Process.waitall
  135. Collect Results # ... ! results = Rinda::TupleSpace.new DRb.start_service("druby://localhost:61676", results)

    workers.times do descrambled = results.take([/\b#{Regexp.escape(pattern)}\b/, Array]) puts descrambled.last end Process.waitall
  136. Other Nice Features ‣You can set expiration times for tuples

    added to a TupleSpace ‣Rinda also comes with a RingServer for zero configuration networking
  137. Using RingServer #!/usr/bin/env ruby -wKU ! require "rinda/ring" # for

    RingServer require "rinda/tuplespace" # for TupleSpace ! # start a RingServer DRb.start_service Rinda::RingServer.new(Rinda::TupleSpace.new) ! # ...
  138. Using RingServer #!/usr/bin/env ruby -wKU ! require "rinda/ring" # for

    RingServer require "rinda/tuplespace" # for TupleSpace ! # start a RingServer DRb.start_service Rinda::RingServer.new(Rinda::TupleSpace.new) ! # ...
  139. Using RingServer #!/usr/bin/env ruby -wKU ! require "rinda/ring" # for

    RingServer require "rinda/tuplespace" # for TupleSpace ! # start a RingServer DRb.start_service Rinda::RingServer.new(Rinda::TupleSpace.new) ! # ... #!/usr/bin/env ruby -wKU ! require "rinda/ring" # for RingFinger require "rinda/tuplespace" # for TupleSpace ! # find a RingServer DRb.start_service ring_server = Rinda::RingFinger.primary ! # ...