Doug Bryant

Tech thought and notes

Fun With Ruby Hashes - Initializing With a Block

The alternate title for this post was Poor man’s email load balancer.

A less common method for creating and using Ruby hashes is to initialize the hash with a block. This allows you to return and/or assign a value for a missing key. You can think of this as a parallel to using method_missing in Ruby classes and modules.

The documentation for Hash.new with a block suggests

If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block’s responsibility to store the value in the hash if required.

Most often for the case where I wanted a default value returned, I would use fetch and set the default value for individual keys being looked for.

1
2
3
4
5
6
7
my_hash = {}
my_hash.fetch(:foo, "Not Set")
=> "Not Set"

my_hash[:foo] = 'bar'
my_hash.fetch(:foo, "Not Set")
=> "bar"

The same result can be achieved by initializing the hash with a block – any unknown keys requested from the hash will be initialized with a default value.

1
2
3
4
5
6
my_hash = Hash.new{|hash,key| hash[key] = "Not Set"}
my_hash[:foo]
=> "Not Set"

my_hash[:foo] = "bar"
=> "bar"

An alternative to initializing Hash with a block is to set the default_proc= on an existing hash.

1
2
3
4
5
6
7
8
my_hash = {}
my_hash[:foo]
=> nil

my_proc = proc{|hash,key| hash[key] = "Not Set"}
my_hash.default_proc = my_proc
my_hash[:foo]
=> "Not Set"

So now we have a way of always returning a default value for a hash. Not very interesting in and of itself. What else could we do with our block backed hash?

A simple loadbalancer…

This is one of my favorite hacks. At MileMeter, we were using our Google Apps account to send email (reminders, confirmation emails, etc). At the time (not sure what the limit is now) we were limited to 500 emails per day from a single account. At some point, we started bumping up against the 500 email per day limit.

It was easy enough to create another email account to send emails through, but ActionMailer only provided a single configuration and we didn’t want to upgrade Google Apps for all MileMeter users because of a single account limitation.

John Riney and I looked at our options went in search of a solution which would allows us to send more emails without spending any additional money.

John found the Hash documentation and realized we had a solution in what he deemed “Untrustworty Ruby Hashes”

It was easy enough to create additional emails accounts. Armed with a Hash initialized with a block, we can now tell ActionMailer to pick a random user_name from a list of accounts each time it sent an email, thus effectively load-balancing our email.

A typical ActionMailer configuration is

1
2
3
4
5
6
7
8
9
10
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address:              'smtp.gmail.com',
  port:                 587,
  domain:               'example.com',
  user_name:            '<username>',
  password:             '<password>',
  authentication:       'plain',
  enable_starttls_auto: true
}

Our new load-balanced ActionMailer config is initialize with a block and configured to return a different user_name each time the user_name key is accessed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# list of email accounts to send from
ACCOUNTS = %w{system1 system2 system3 system4}.map{|name| "#{name}@example.com"}
# => ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]

# NOTE: user_name key not set
email_config = {
  address:              'smtp.gmail.com',
  port:                 587,
  domain:               'example.com',
  password:             '<password>',
  authentication:       'plain',
  enable_starttls_auto: true
}

config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = Hash.new do |hash,key|
  ACCOUNTS[rand(ACCOUNTS.size)] if :user_name == key
end.merge!(email_config)

Assuming the password is set the same for all accounts you want to use, when an email is sent, the user_name for the account is looked up and set to a different value every time, effectively a simple load-balancing technique.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
irb(main):037:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):038:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):039:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):040:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):041:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):042:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):043:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):044:0> smtp_settings[:user_name]
=> "[email protected]"
irb(main):045:0> smtp_settings[:user_name]
=> "[email protected]"