ふと思って、Ruby でデータを memcached に入れて、それを Perl から取り出せるのかな〜というのが気になったので試してみました。データの形式として、文字列、ActiveRecord、ハッシュをそれぞれ試してみます。予想では、ActiveRecord のデータはさすがに Perl では読み込めないだろうけど、それ以外は読み込めるんじゃないかなぁと。

まずは Ruby を使って memcached にデータを set します。

#!/usr/bin/ruby

require 'rubygems'
require 'activerecord'
require 'memcache'

class Hoge < ActiveRecord::Base
  establish_connection(
    'adapter'  => 'mysql',
    'host'     => 'localhost',
    'database' => 'test',
    'username' => 'USER',
    'password' => 'PASS',
    'encoding' => 'utf8'
  )
  set_table_name :hoge
end

cache = MemCache::new '127.0.0.1:11211'

# 文字列
cache['hogehoge'] = 'fugafuga'    

# ActiveRecord で取得したデータ
hoge = Hoge.find(1)
cache['activerecord'] = hoge

# ハッシュ
hash = {}
hash['hoge'] = 100
hash['fuga'] = 200
cache['foo'] = hash

で、これを Perl で読み込んでみます。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;
use Data::Dumper;

my $cache = Cache::Memcached->new({
    servers => ['127.0.0.1:11211']    
});

warn Dumper $cache->get('hogehoge'); # fugafuga';

warn Dumper $cache->get('activerecord'); # $VAR1 = o:      Hoge:@attributes{"      name"\\"id"1:@attributes_cache{';

warn Dumper $cache->get('foo'); # $VAR1 = {"      fugai   hogeii';

・・・何か変です。データが受け取れてはいるもののぐちゃっとしています。。文字列なんかは正しく受け取れているように見えますが、これも微妙に違います。こんなコードで検証してみました。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;
use Data::Dumper;

my $cache = Cache::Memcached->new({
    servers => ['127.0.0.1:11211']    
});

my $hash = {
    fugafuga => 1
};
my $hogehoge = $cache->get('hogehoge');
warn Dumper $hash->{$hogehoge}; # undef

データが正しく受け取れていれば(つまり 'fugafuga' が正しく受け取れていれば)、undef ではなく、1 が得られるはずです。この辺の原因を探るために、Ruby の memcache-client のソースコードをチェックしてみたところ、理由がわかりました。

# /usr/lib/ruby/gems/1.8/gems/memcache-client-1.7.5/lib/memcache.rb の一部
alias [] get    
      
def []=(key, value)
  set key, value
end

# set
def set(key, value, expiry = 0, raw = false)
  raise MemCacheError, "Update of readonly cache" if @readonly

  value = Marshal.dump value unless raw
  with_server(key) do |server, cache_key|
    logger.debug { "set #{key} to #{server.inspect}: #{value.to_s.size}" } if logger

    if @check_size && value.to_s.size > ONE_MB
      raise MemCacheError, "Value too large, memcached can only store 1MB of data per key"
    end

    command = "set #{cache_key} 0 #{expiry} #{value.to_s.size}#{noreply}\r\n#{value}\r\n"

    with_socket_management(server) do |socket|
      socket.write command
      break nil if @no_reply
      result = socket.gets
      raise_on_error_response! result

      if result.nil?
        server.close
        raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
      end

      result
    end
  end
end

#get
def get(key, raw = false)
  with_server(key) do |server, cache_key|
    logger.debug { "get #{key} from #{server.inspect}" } if logger
    value = cache_get server, cache_key
    return nil if value.nil?
    value = Marshal.load value unless raw
    return value
  end
rescue TypeError => err
  handle_error nil, err
end


まず、[] を set にエイリアスしていますが、その set の中で、Marshal.dump してシリアライズしています( raw が false のとき)。そしてデータを取り出すときには get しますが、その中で Marshal.load してデシリアライズしているようです( raw が false のとき)。なので、Ruby で set したものを Perl で普通に受け取ってもそれはシリアライズされたデータなのでそのままでは利用できません。かといって、Perl で Storable モジュールを使えば復元出来そうですが、うまく出来ませんでした。。微妙に形式が違うのかも?しれません。

そこで、そもそもシリアライズしないようにしてみます。これは raw に true を代入してあげればおkです。こうすれば、文字列の場合にはデータの set, get どちらもきちんと行うことが出来ました。でも ActiveRecord やハッシュの場合にはデータの set でエラーになってしまいます。。まぁ、、そのためのシリアライズですよね。(´Д⊂)

#!/usr/bin/ruby

require 'rubygems'
require 'activerecord'
require 'memcache'

class Hoge < ActiveRecord::Base
  establish_connection(
    'adapter'  => 'mysql',
    'host'     => 'localhost',
    'database' => 'test',
    'username' => 'USER',
    'password' => 'PASS',
    'encoding' => 'utf8'
  )
  set_table_name :hoge
end

cache = MemCache::new '127.0.0.1:11211'

cache.set('hogehoge', 'fugafuga', 0, true)

hash = {}
hash['hoge'] = 100
hash['fuga'] = 200
cache.set('foo', hash, 0, true) # エラー

hoge = Hoge.find(1)
cache.set('activerecord', hoge, 0, true) # エラー

さて、このような場合には、汎用のデータ形式を使うことで解決できます。XML や JSON といった形式です。例えば JSON 形式を利用してみましょう。

このように Ruby で データを作成し、それを JSON 形式に変換します。そのデータをmemcached に set します。このとき、第4引数 (raw) を true にして、シリアライズを行わないようにしていることに注意してください。ちなみに第3引数は expires ですが、これを 0 に設定した場合にはそのデータが消えることはありません(データはサーバが起動している限りずっと保持される)。

#!/usr/bin/ruby

require 'rubygems'
require 'activerecord'
require 'memcache'

class Hoge < ActiveRecord::Base
  establish_connection(
    'adapter'  => 'mysql',
    'host'     => 'localhost',
    'database' => 'test',
    'username' => 'USER',
    'password' => 'PASS',
    'encoding' => 'utf8'
  )
  set_table_name :hoge
end

cache = MemCache::new '127.0.0.1:11211'

hash = { 
  'key1' => 'data1', 
  'key2' => 'data2'
}
cache.set('json', hash.to_json, 0, true)


その後、Perl でデータを受け取って、JSON からハッシュへと変換することが出来ました。このようにすれば異なる言語間でもデータのやり取りをすることが出来ますね。スバラシス。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;
use JSON;
use Data::Dumper;

my $cache = Cache::Memcached->new({
    servers => ['127.0.0.1:11211']    
});

my $json = $cache->get('json');

warn Dumper from_json($json); # $VAR1 = {
                              #     'key2' => 'data2',
                              #     'key1' => 'data1'
                              # };