ActsAsInsertOrUpdate 5

Posted by aaron
on Tuesday, April 22

Problem

With high volume Rails applications, entities with unique constraints are expensive and error prone to create/update. ActsAsInsertOrUpdate helps solve that problem (if you're using MySQL), by leveraging the "INSERT ... ON DUPLICATE KEY UPDATE" functionality.

Scenario

Lets say you have a Person, and Entity, and a Rating. Each user can rate each entity only once, and if they re-rate the entity, it should update the value.

class Entity < ActiveRecord::Base
  has_many :ratings
end

class Person < ActiveRecord::Base
 has_many :ratings
end
  
class Rating < ActiveRecord::Base
 belongs_to :Person
 belongs_to :Entity
end  

Here is the table that back's Rating. Notice the Unique Key constraint on (entity_id, person_id).

CREATE TABLE `ratings` (
  `id` int(11) NOT NULL auto_increment,
  `rating` tinyint(4) default '0',
  `person_id` int(11) default NULL,
  `entity_id` int(11) default NULL,
  `created_at` datetime default NULL,
  `updated_at` datetime default NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `index_ratings_on_entity_id_and_person_id` (`entity_id`,`person_id`),
)

Previously, the logic would be something like:

  • 1) Check if a rating exists for the User + Entity
  • 2) If so, update
  • 3) If not, insert
  • 4) rescue the insert in case there is a unqiue constraint error
  • 5) retrieve the record (and/or update with the new rating)
  • If the table is MyISAM, Steps 1-5 aren't transactionally safe. If you're using InnoDB, and experience heavy volumes of traffic, you're prone to Deadlock's. This is even more of a concern is the unique entity is shared across multiple users, as seen with a recent client of ours.

    Solution:

    class Rating < ActiveRecord::Base
     belongs_to :Person
     belongs_to :Entity
     acts_as_insert_or_update :field_to_update => "rating"
    end  

    Now Steps 1-5 above become, just one. Rating.create(..)

    In the background, ActsAsInsertOrUpdate overwrites the implementation of ActionRecord:Base#create, to leverage an often unsed feature of MySQL called INSERT ... ON DUPLICATE KEY UPDATE. As configured above, if a duplicate record is found for the unique constraint, the rating field will be updated with the new value.

    Caution

    This is a brute force hack on ActiveRecord::Base#create. Use at your own risk.

    Code

    Waiting for a rubyforge account. Will post more info soon.

MySQL Stored Function: parsing a JSON encoded string 5

Posted by warren
on Monday, April 14
For analytics purposes, we ended up storing JSON-encoded data as a column in a mysql table. Although we don't often need to query it directly, from time to time, it makes things a bit easier/faster. Below is a MySQL stored function that takes two parameters (a JSON encoded string, and the name of a key) and returns the value associated with that key.
CREATE FUNCTION JSON(`json` TEXT, `search_key` VARCHAR(255)) RETURNS TEXT DETERMINISTIC BEGIN

  DECLARE i INT DEFAULT 1;
  DECLARE json_length INT DEFAULT LENGTH(json);
  DECLARE state ENUM('reading_key','done_reading_key','reading_string', 'reading_array');
  DECLARE tmp_key TEXT;
  DECLARE tmp_value TEXT;
  DECLARE current_char VARCHAR(1);

  WHILE i <= json_length DO
    SET current_char = SUBSTRING(json,i,1);

    IF state = 'reading_key' THEN
      IF current_char = '"' THEN
        SET state = 'done_reading_key';
      ELSE
        SET tmp_key = CONCAT(tmp_key, current_char);
      END IF;
    ELSEIF state = 'done_reading_key' THEN
      IF current_char = '"' THEN
        SET state = 'reading_string';
      ELSEIF current_char = '[' THEN
        SET state = 'reading_array';
      END IF;
    ELSEIF state = 'reading_string' OR state = 'reading_array' THEN
      IF current_char = '\\' THEN
        SET i = i + 1;
        SET tmp_value = CONCAT(tmp_value, SUBSTRING(json,i,1));
      ELSEIF (state = 'reading_string' AND current_char = '"') OR (state = 'reading_array' AND current_char = ']') THEN
        IF search_key = tmp_key THEN
          RETURN tmp_value;
        ELSE
          SET state = NULL;
        END IF;
      ELSE
        SET tmp_value = CONCAT(tmp_value, current_char);
      END IF;
    ELSE 
      IF current_char='"' THEN
        SET state = 'reading_key';
        SET tmp_key = '';
        SET tmp_value = '';
      END IF;
    END IF;

    SET i = i + 1;
  END WHILE;

  RETURN NULL;
END

Examples

Here's a few examples of how it can be used:

SELECT JSON('{"key1":"val\\"ue1","key2":"value2","key3":["array1","array2"],"key4":"value4"}', 'key1');
# returns 'val"ue1'

SELECT JSON('{"key1":"val\\"ue1","key2":"value2","key3":["array1","array2"],"key4":"value4"}', 'key2');
# returns 'value2'

SELECT JSON('{"key1":"val\\"ue1","key2":"value2","key3":["array1","array2"],"key4":"value4"}', 'key3');
# returns '"array1","array2"'

SELECT JSON('{"key1":"val\\"ue1","key2":"value2","key3":["array1","array2"],"key4":"value4"}', 'key4');
# returns 'value4'

SELECT JSON('{"key1":"val\\"ue1","key2":"value2","key3":["array1","array2"],"key4":"value4"}', 'key5');
# returns NULL

Notes:

If you're trying to run this in the MySQL console, you'll need to set the DELIMITER to be something other than a semi-colon. Before executing the above, run "DELIMITER $$" and after executing it, run "$$" and then "DELIMITER ;" to set your delimiter back to semi-colon.

If you're trying to run this in a Rails migration, don't forget to escape the back-slashes (i.e. '\\' should become '\\\\')

Although this handles several simple use cases of extracting JSON-encoded data, it is by no means comprehensive. There are many JSON-encoded structures that this will not work on. This will not work correctly with nested arrays, or with named hashes.

The performance of this is pretty slow. A better approach would be to create a UDF that plugs into MySQL. Here's a UDF to encode JSON data, but not decode: http://www.mysqludf.org/lib_mysqludf_json/index.php

Faster Implementation:

Here's a faster version, but it's not quite as robust:
CREATE FUNCTION JSON_FAST(`json` TEXT, `search_key` VARCHAR(255)) RETURNS TEXT DETERMINISTIC BEGIN
  IF INSTR(json, CONCAT('"', search_key, '":"')) THEN
    RETURN SUBSTRING_INDEX(SUBSTRING(json, INSTR(json, CONCAT('"', search_key, '":"')) +
           LENGTH(search_key) + 4), '"', 1);
  ELSEIF INSTR(json, CONCAT('"', search_key, '": "')) THEN
    RETURN SUBSTRING_INDEX(SUBSTRING(json, INSTR(json, CONCAT('"', search_key, '": "')) +
           LENGTH(search_key) + 5), '"', 1);
  ELSE
    RETURN NULL;
  END IF;
END
Here's some key differences:
SELECT JSON('{"key":"value \"plus quotes\""}', 'key');
# returns 'value "plus quotes"'
SELECT JSON_FAST('{"key":"value \"plus quotes\""}', 'key');
# returns 'value \'

SELECT JSON('{"key":["value1","value2"]}', 'key');
# returns '"value1","value2"'
SELECT JSON_FAST('{"key":["value1","value2"]}', 'key');
# returns NULL

Curb your Net::HTTP 0

Posted by aaron
on Tuesday, April 08
Curb is a ruby binding for libcurl. We've had sporadic issues with Net::HTTP, which this might aleviate via native dns, native timeouts, performance improvements, etc. It wouldnt be hard to re-implement ActiveResource, rfacebook, myspace-ruby, etc to use it instead. Anyone using this already?
sudo gem install curb
require 'rubygems'
require 'curb'
require "net/http"
require 'benchmark'

iterations = 40
Benchmark.bm do |x|
  x.report("curb") do
    iterations.times do
      c = Curl::Easy.perform("http://www.google.com")
      #puts c.body_str
    end
  end
  x.report("net/http")  do
    iterations.times do
      http = Net::HTTP.start("www.google.com")
      req = Net::HTTP::Get.new("/")
      res = http.request(req)
      #puts res.body
    end
  end
end
             user     system      total        real
curb      0.010000   0.030000   0.040000 (  4.019197)
net/http  0.140000   0.110000   0.250000 (  4.155106)

Agressive Timeouts On External API Calls 1

Posted by val
on Sunday, March 30

One of the challenges with writing a Facebook or Bebo application is staying within a limit it gives you to respond with data before it shows the Application Did Not Respond page to a user. Having a content reach application calling external APIs, like Amazon or YouTube, with response times beyond your control, forces you to keep such calls short to allow extra time for processing. We usually wrap them in aggressive timeouts with a retry. As an example is this code from the Ruby Amazon E-Commerce REST Service API gem rewritten to limit a single call attempt to two seconds with one more retry.

Original Code
module Amazon  
  class Ecs

    def self.send_request(opts)
      request_url = prepare_url(opts)

      res = Net::HTTP.get_response(URI::parse(request_url))
      unless res.kind_of? Net::HTTPSuccess
        raise Amazon::RequestError, "HTTP Response: #{res.code} #{res.message}"
      end
      Response.new(res.body)
    end

  end
end
Modified Code
module Amazon  
  class Ecs

    class EmptyResponse
      def items; []; end
      def total_pages; 0; end
    end

    def self.send_request(opts)

      res = timed_try(request_url, 2) do |url|

        uri = URI::parse(url)
        req = Net::HTTP.new(uri.host, uri.port)

        # Agressive timeouts
        req.open_timeout = 1
        req.read_timeout = 2

        req.start { |http| http.request_get(url) }

      end

      res.kind_of?(Net::HTTPSuccess) ? Response.new(res.body) : EmptyResponse.new

    end

private

     def timed_try(url, attempts, &block)

       attempt = 1
       begin
         block.call(url)
       rescue Timeout::Error
         if attempt >= attempts
           RAILS_DEFAULT_LOGGER.warn "[amazon_api] gave up after attempt ##{ attempt } to get data from #{ url }"
           nil
         else
           RAILS_DEFAULT_LOGGER.warn "[amazon_api] attempt ##{ attempt } timed out on getting data from #{ url }"
           attempt += 1
           retry
         end
       end

     end

  end
end

Reviewing Application Health with HAProxy Stats 0

Posted by val
on Thursday, March 27
One of the methods we use for checking the health of our applications is stats collected from HAProxy. We utilize it to see how many requests are scheduled for execution on mongrel instances. The graph is one indication of how our applications perform. When we launched the new version of the site three weeks ago, the graph for a single vertical (ReadingSocial) on a typical Tuesday looked like this:
So, between porting all verticals to Myspace, Orkut, Bebo, and enhancing the functionality, we spent some time on optimization. In addition to analyzing slow-query logs with mysqlsla, Aaron wrapped all external API calls (and we do a lot of them - to Amazon, Facebook, Myspace, etc) in slow monitoring so we could see where the latest external bottleneck was so we could fix it one by one. Three weeks later the graph became much more peaceful:

Viva LivingSocial! 0

Posted by val
on Saturday, March 15
You haven't heard from us for a while because we were working on a new project - LivingSocial - the web-site that spreads across all major social networks (Facebook, Myspace, Bebo, Orkut) allowing people to talk to each other about their interests without barriers. We are going to speak more about it in a dedicated blog while keeping this one for posts on Rails and other technologies.

Simple Google Pie Chart Graph in Rails 1

Posted by warren
on Saturday, February 16
Although there are many comprehensive libraries out there for google graphs in Rails, all we needed was a quick and simple pie chart. Here's what we came up with:

module ApplicationHelper
  def google_pie_chart(data, options = {})
    options[:width] ||= 250
    options[:height] ||= 100
    options[:colors] = %w(0DB2AC F5DD7E FC8D4D FC694D FABA32 704948 968144 C08FBC ADD97E)
    dt = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."
    options[:divisor] ||= 1
    
    while (data.map { |k,v| v }.max / options[:divisor] >= 4096) do
      options[:divisor] *= 10
    end
    
    opts = {
      :cht => "p",
      :chd => "e:#{data.map{|k,v|v=v/options[:divisor];dt[v/64..v/64]+dt[v%64..v%64]}}",
      :chl => "#{data.map { |k,v| CGI::escape(k + " (#{v})")}.join('|')}",
      :chs => "#{options[:width]}x#{options[:height]}",
      :chco => options[:colors].slice(0, data.length).join(',')
    }
    
    image_tag("http://chart.apis.google.com/chart?#{opts.map{|k,v|"#{k}=#{v}"}.join('&')}")
  rescue
  end
end
Here's an example of how to use it:

<%=google_pie_chart([["Very Liberal", 8], ["Liberal", 22], ["Moderate", 9], ["Conservative", 4], ["Very Conservative", 1]], :width => 626, :height => 300) %>
And what it renders:

Leverage Rails Resource Routes on Facebook 1

Posted by eddie
on Monday, January 07
Since all canvas page views are proxied through POSTs, resource routes were hopelessly broken. The Facebook platform team was kind enough to add a new feature just for us rails folks: a new signed parameter that indicates the original request type (i.e. POST v. GET) against canvas pages.

Here's a small patch you can stick at the end of environment.rb to restore REST-ful routes.

class ActionController::Routing::RouteSet
  def extract_request_environment(request)
    contents = request.body.read
    facebook_method_override = nil
    if contents =~ /fb_sig_request_method=([A-Z]+)/
      facebook_method_override = $1
    end
    request.body.string = contents
    { :method => facebook_method_override ? facebook_method_override.downcase.intern : request.method}
  end
end

UPDATE: Here's a slightly better implementation that allows the .get? and .post? helpers as well as a few other things to work (thanks Chris Nolan for reminding us to update this blog entry):

ActionController::AbstractRequest.class_eval do
  def request_method_with_facebook_overrides
    @request_method ||= begin
      case 
        when parameters[:_method]
          parameters[:_method].downcase.to_sym
        when parameters[:fb_sig_request_method]
          parameters[:fb_sig_request_method].downcase.to_sym
        else
          request_method_without_facebook_overrides
      end
    end
  end
  alias_method_chain :request_method, :facebook_overrides
end

Pretty SVN commit emails 1

Posted by aaron
on Monday, November 05

So i know it may be old school, but I like getting svn commit messages, especially when working on a small team. I found ElliotH's post about A better subversion post-commit hook than commit-email.pl, but it wasnt entirely working.

Most of the credit goes to him, I just fixed the colorization.

[UPDATED]: for some reason, new lines werent looking good in all emails
#!/usr/bin/ruby -w

# A Subversion post-commit hook. Edit the configurable stuff below, and
# copy into your repository's hooks/ directory as "post-commit". Don't
# forget to "chmod a+x post-commit".

# ------------------------------------------------------------------------

# You *will* need to change these.

address="FOO@SOME_DOMAIN.com"
sendmail="/usr/sbin/sendmail"
svnlook="/usr/bin/svnlook"

# ------------------------------------------------------------------------

require 'cgi'

# Subversion's commit-email.pl suggests that svnlook might create files.
Dir.chdir("/tmp")

# What revision in what repository?
repo = ARGV.shift()
rev = ARGV.shift()

# Get the overview information.
info=`#{svnlook} info #{repo} -r #{rev}`
info_lines=info.split("\n")
author=info_lines.shift
date=info_lines.shift
info_lines.shift
comment=info_lines

# Output the overview.
body = "<p><b>#{author}</b> #{date}</p>"
body << "<p>"
comment.each { |line|  body << "#{CGI.escapeHTML(line)}<br/>\n" }
body << "</p>"
body << "<hr noshade>"

# Get and output the patch.
changes=`#{svnlook} diff #{repo} -r #{rev}`
body << "<pre>"
changes.each do |top_line|
  top_line.split("\n").each do |line|
    color = case
      when line =~ /^Modified: / || line =~ /^=+$/ || line =~ /^@@ /: "gray"
      when line =~ /^-/: "red"
      when line =~ /^\+/: "blue"
      else "black"
    end
    body << %Q{<font style="color:#{color}">#{CGI.escapeHTML(line)}</font><br/>\n}
 end
end
body << "</pre>"

# Write the header.
header = ""
header << "To: #{address}\n"
header << "From: #{address}\n"
header << "Subject: [SVN] #{repo} revision #{rev}\n"
header << "Reply-to: #{address}\n"
header << "MIME-Version: 1.0\n"
header << "Content-Type: text/html; charset=UTF-8\n"
header << "Content-Transfer-Encoding: 8bit\n"
header << "\n"

# Send the mail.
begin
    fd = open("|#{sendmail} #{address}", "w")
    fd.print(header)
    fd.print(body)
rescue
    exit(1)
end
fd.close

# We're done.
exit(0)

JRuby trunk == better/faster Rails performance 0

Posted by aaron
on Saturday, November 03

So I remember a couple months ago playing with JRuby, and while fibonacci was super fast, Rails was way off.. ActiveRecord performance 6x-10x slower than MRI...

Looks like its getting better. Disclaimer: These are really really simple non-scientific tests.

Local mysql database, MYISAM, table people, with 2 columns, (id, name). 100k rows

> jruby -J-server -O script/console production
>> Benchmark.measure {10000.times {Person.find :first}}.total
=> 2.286
> ./script/console production
>>  Benchmark.measure {10000.times {Person.find :first}}.total
=> 1.7
Mongrel
> ab -n 1000 http://localhost:3001/people/1
Requests per second:    95.02 [#/sec] (mean)
On Glassfish v3 with: RAILS_ENV=production jruby -J-server -O -S glassfish_rails glass2, after a bit of warmup.
> ab -n 1000 http://localhost:8080/glass2/people/1
Requests per second:    48.25 [#/sec] (mean)

turning logging mostly off

> ab -n 1000 http://localhost:8080/glass2/people/1
Requests per second:    56.70 [#/sec] (mean)
All in all, thats impressive. Congrats to Charles, Ola, and the whole JRuby crew. I dont think i'll be putting this in production yet, but I'm very interested to hear from others that have.

OpenSocial is here! 3

Posted by eddie
on Wednesday, October 31

As you may have seen on TechCrunch and in the New York Times, Google is days away from the developer launch of Open Social.

As a trusted tester/launch partner, we've been busy porting our social shopping suite (such as Visual Bookshelf) to OpenSocial.

I'll follow up tomorrow with more thoughts, but I wanted to congratulate the OpenSocial team on the upcoming launch. We're definitely excited!

Super fast IP to lat/lng in Rails - Part 2 9

Posted by aaron
on Tuesday, October 30
In Super fast IP to lat/lng in Rails, I showed a solution for fast IP to lat/lng resolution in rails. I called it "Super fast" because it performed orders of magnitude faster the the RESTful interface, but it was also "Super fast" to implement. That being said, Kyle made a comment to check out the GeoIP gem. I had heard of MaxMind before, but I didnt want to spend hundreds of dollars to solve this problem. What I didnt know was they also have a free download of their "lite" datasource. They have a GeoLiteCountry and GeoLiteCity version, although only the City version has lat/lng info. They provide wrappers in most languages (including Ruby), and while Kyle's suggested geoip, I found geoip_city on RubyForge which I like a bit better.

[UPDATED]: I had forgotten to include the install instructions for getting the GeoIP C library. Install the C bindings, the gem (which isnt packaged as a gem for easy download) and get the data.
wget http://www.maxmind.com/download/geoip/api/c/GeoIP.tar.gz
tar -zxvf GeoIP.tar.gz
cd GeoIP
./configure && make && sudo make install

wget http://rubyforge.org/frs/download.php/27077/geoip_city-0.1.gem
sudo gem install geoip_city-0.1.gem
wget http://www.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz
gunzip GeoLiteCity.dat.gz
sudo mkdir /usr/local/share/GeoIP
sudo mv GeoLiteCity.dat /usr/local/share/GeoIP/GeoLiteCity.dat
Then the ruby part:
>> require 'geoip_city'
>> g = GeoIPCity::Database.new('/usr/local/share/GeoIP/GeoLiteCity.dat')
>> res = g.look_up('4.2.2.2')
>> puts "lat: #{res[:latitude]} lng: #{res[:longitude]}"
lat: 38.0 lng: -97.0
So the question is (although its probably obvious), which is faster?
>>  Benchmark.measure { 1000.times {Hostip.geocode('4.2.2.2')}}.total
=> 1.02
>>   Benchmark.measure { 100000.times {g.look_up('4.2.2.2')}}.total
=> 0.5
Conclusion? The C library is MUCH faster than my database version. 200k req/s vs. 1k req/s. As a side note, I also tested the "geoip" gem, and it was about 3x faster than my database version.

But all in all, for the 5 minutes it took me to write the original version, it was fast enough... but MaxMind GeoLiteCity + the geoip_city gem is Super fast-er.

Monitoring Rails Apps: Pulse + More 1

Posted by aaron
on Friday, October 26
Paul Gross from Thoughtworks recently created a pulse gem. The gem adds a simple action to your rails app, "/pulse", which acts as a heartbeat.

The pulse gem currently defines the method as:
def pulse
   render :text => "OK"
end
Then you can configure haproxy to monitor your application by hitting http://server/pulse and verifying the response is "OK".

We've had a similar action in our applications for quite some time, although I really like the idea of externalizing it to a gem. Our implementation is slightly different though. Since so many rails applications depend on a database, I instead added:
def pulse
    rows = ActiveRecord::Base.connection.execute("select 1 from dual").num_rows rescue 0
    render :text => rows == 1 ? "OK" : "Error!"
end
IIRC, "select 1 from dual" is the fastest query you can run against a database, in MySQL, Oracle, and Postgres. And if you have mutliple databases, you can add union in the one pulse request, or have multiple actions.

Now you are not only testing your application is live, but that it can connect to the database.

Facebook lets money flow 0

Posted by aaron
on Monday, October 22

This morning the Chicago Tribune had an article entitled “Facebook lets money flow”, which among other recent articles, outlines something Facebook got right. I spoke with the article’s author Eric Benderoff earlier in the week to discuss Hungry Machine’s monetization strategies on Facebook.

While MySpace has allowed third party companies to embed widgets for quite some time, Facebook took it a step further and allowed full featured applications to reside “within” the Facebook experience, like the Social Shopping experience in Visual Bookshelf. Most importantly, Facebook allowed these applications almost full control over the application experience.

Beyond selling our own display advertising across the social shopping suite and our 20+ other applications, we can provide advertisers the ability to leverage Social Data to target customers directly. Social Data Demographics includes what user’s watch, read, listen to, and use. Beyond the traditional demographics of age, sex, and location, social data takes that a step further.

For example, the Harry Potter Book 7 in Visual Bookshelf has 4x the number of book reviews as the same title on Amazon. Hundreds of thousands of users have added books from the Harry Potter series to their bookshelves. That community is active on Facebook. Why wouldn’t an advertiser want to target Harry Potter readers when the next Harry Potter movie comes out?

This is the next phase of advertising in the social application space.

Super fast IP to lat/lng in Rails 4

Posted by aaron
on Monday, October 22
In building the eye candy demo for the Graphing Social conference, I needed a quick way to geo-locate users by IP address. For Rails, GeoKit is an awesome plugin. It supports a list of providers, and overlays distance calculations, before_filter helpers, all sorts of good stuff.
It uses hostip.info to do IP to lat/lng, using their RESTful interface:
http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true
  Country: UNITED STATES (US)
  City: Sugar Grove, IL
  Latitude: 41.7696
  Longitude: -88.4588

This is great, but its just too darn slow to geolocate 12 users a second. All I needed was a FAST ip to lat/long lookup mechanism. Luckily, HostInfo provides the raw data. Here's how I built super fast GeoLoc by IP method.

Download, create a DB, and import the data.
wget http://hostip.ww.com/hostip_current.sql.gz
gunzip hostip_current.sql.gz 
mysqladmin -uroot create hostip
mysql -uroot hostip < hostip_current.sql


Create a simple HostIp class. Note: I need to dynamically build this query b/c the data is sharded across tables by the A class of the IP.
class Hostip < ActiveRecord::Base
  def self.geocode(ip)
    a,b,c,d = ip.split(".")
    self.set_table_name "hostip.ip4_#{a}"
    ip = find(:first, :select => "lat,lng",
                :joins => %Q{INNER JOIN hostip.cityByCountry 
                               ON hostip.ip4_#{a}.city = hostip.cityByCountry.city 
                               AND hostip.ip4_#{a}.country = hostip.cityByCountry.country},
                :conditions => ["b = ? and c = ?", b,c])
    if ip
      [ip.lat, ip.lng]            
    else 
      ["",""]
    end
  end
end 


Usage
>> Hostip.geocode("4.2.2.2")
=> ["39.944", "-105.062"]


Thats it! Now you can geolocate by IP in your own datacenter.
Thanks again to the guys at HostIp for sharing this data!