Category: Cloud Computing

JLine is not loading in maven-scala-plugin with scala-console

For unknown reason maven-scala-plugin is not loading properly scala:console. After compile my app and run

mvn scala:console

It raises

Failed to created JLineReader: java.lang.NoClassDefFoundError: scala/tools/jline/console/completer/Completer
Falling back to SimpleReader.
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_29).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

Even though application context works, the collateral effect for this issue is you cannot neither get typed commands by up-arrow key nor get autocomple working by tab key.

I’ve fixed just including in my pom.xml the jline dependency

<dependency>
  <groupId>org.scala-lang</groupId>
  <artifactId>jline</artifactId>
  <version>2.9.0-1</version>
</dependency>

It’s not elegant but works. I’m looking for an environmental fix for this instead of a fix per project.

Indexing files with Scala and Elasticsearch

I was doing load testing in Elasticsearch., I’ve created a simple code in Scala to fetch files recursively and index them to Elasticsearch.

The code uses Java Mime Magic Library as a helper to get file description.

So let’s get started installing Elasticsearch

Installing and start Elasticsearch

curl -O http://cloud.github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.18.7.tar.gz
tar zxf elasticsearch-0.18.7.tar.gz
cd elasticsearch-0.18.7
bin/elasticsearch -f

Remove -f out if you don’t want start it in foreground.

Scala code

Our Scala code have 3 functions, one to list all files

def fetchFiles(path:String)(op:File => Unit){
  for (file <- new File(path).listFiles if !file.isHidden){
    op(file)
    if (file.isDirectory){
      fetchFiles(file.getAbsolutePath)(op)
    }
  }
}

A function to create the JSON.

def document(file:File) = {

  val json = jsonBuilder.startObject
  .field("name",file.getName)
  .field("parent",file.getParentFile.getAbsolutePath)
  .field("path",file.getAbsolutePath)
  .field("last_modified",new Date(file.lastModified))
  .field("size",file.length)
  .field("is_directory", file.isDirectory)

  if (file.isFile) {
    try{
      val m = Magic.getMagicMatch(file, true)
      json.field("description",m.getDescription)
      .field("extension",m.getExtension)
      .field("mimetype",m.getMimeType)
    }catch {
      case _ => json.field("description","unknown")
        .field("extension",file.getName.split("\\.").last.toLowerCase)
        .field("mimetype","application/octet-stream")
    }
  }
  json.endObject
}

Only files will be passed to Magic detection, there’s a treatment in case detector gets issue parsing the file. It’ll generate the final format to be indexed.

  {
      "name": "pragmatic-guide-to-git_p1_0.pdf",
      "parent": "/Users/shairon/Reference",
      "path": "/Users/shairon/Reference/pragmatic-guide-to-git_p1_0.pdf",
      "last_modified": "2010-11-26T18:55:43.000Z",
      "size": 1358963,
      "is_directory": false,
      "description": "PDF document",
      "extension": "pdf",
      "mimetype": "application/pdf"
  }

And finally the main

def main(args: Array[String]) = {
    val dir = new File(args(0))
    if (!dir.exists || dir.isFile || dir.isHidden) {
      printf("Directory not found %s\n",dir)
      System.exit(1)
    }

    val client = new TransportClient()
    client addTransportAddress(
      new InetSocketTransportAddress("0.0.0.0",9300)
    )
    fetchFiles( dir.getAbsolutePath){
      file => {
        printf("Indexing %s\n",file)
      client.prepareIndex("files", "file", DigestUtils.md5Hex(file.getAbsolutePath))
        .setSource(document(file))
        .execute.actionGet
      }
    }
    client.close
}

As you may notice, we’re running Elasticsearch and the program in the same machine(0.0.0.0), if you want to run Elasticsearch in other machine, change the ip/hostname at

client addTransportAddress(new InetSocketTransportAddress("ip/host-name-here",9300))

The index name and type is in the line

client.prepareIndex("files", "file", DigestUtils.md5Hex(file.getAbsolutePath))

it’s equivalent of a curl call

curl -XPUT 'http://0.0.0.0:9200/files/file/4cdb168a80e2adc397f44353b3223494' -d '...'

The only difference is port 9300, it’s used by Java Transport Client and 9200 is used straightforward by others clients.

Indexing

Indexing files is also simple. All we have to do is get this code put together so clone it https://github.com/shairontoledo/elasticsearch-filesystem-indexer

git clone git://github.com/shairontoledo/elasticsearch-filesystem-indexer.git
cd elasticsearch-filesystem-indexer

Install dependencies and compile it by maven

mvn install

Running

mvn exec:java -Dexec.mainClass=net.hashcode.fsindexer.Main -Dexec.args=/Users/me/directory/path

Set exec.args to a directory that you want to index.

Searching

After to index some files, you can search by

curl -XGET 'http://0.0.0.0:9200/files/file/_search?q=pdf&pretty=true'

You should see a response similar to

"took": 86,
"timed_out": false,
"_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
},
"hits": {
    "total": 767,
    "max_score": 0.34046465,
    "hits": [
        {
            "_index": "files",
            "_type": "file",
            "_id": "a277bda1f97f8ffa6885347b1c76b8d3",
            "_score": 0.34046465,
            "_source": {
                "name": "agile-web-development-with-rails_p1_0.pdf",
                "parent": "/Users/shairon/Reference",
                "path": "/Users/shairon/Reference/agile-web-development-with-rails_p1_0.pdf",
                "last_modified": "2011-11-03T09:59:09.000Z",
                "size": 6700177,
                "is_directory": false,
                "description": "PDF document",
                "extension": "pdf",
                "mimetype": "application/pdf"
            }
        },

      ...

Now you have data, you can improve your queries and get started with Elasticsearch in your Scala application.

Language detection using pdf documents

This is a set of test for detect tika extracted documents.. It uses Language Detection Library for Java (langdetect.jar). Add more files to /docs/{lang}, the test will load them automatically.

Current support

  • Portuguese
  • English
  • French
  • German
  • Dutch
  • Italian
  • Spanish
  • Korean
  • Simplified Chinese
  • Traditional Chinese
  • Japanese

Install

Install Language Detection Library for Java manually

mvn install:install-file -Dfile=lib/langdetect.jar -DgroupId=com.cybozu.labs.langdetect -DartifactId=langdetect -Dversion=1.0 -Dpackaging=jar

The others dependencies will be installed by Maven.

Two steps to add your language

Create a directory under docs/{langX}, add some document to it and then create a new test method.

@Test
public void LanguageX() throws Exception {
  genericTest("Language X", "lang-x");
}

Running

export JAVA_OPTS=-Djava.awt.headless=true
mvn test

Log Output

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running TestSuite
[INFO ][LangMatcherTest] Setup
[INFO ][LangMatcherTest] Loading lang detector
[INFO ][LangMatcherTest] Loading tika
[INFO ][LangMatcherTest] Language Dutch(nl): loading
[INFO ][LangMatcherTest] Language Dutch(nl): 9 files
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/bereikbaarkaart-nl_print.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/de_bodem_onder_amsterdam.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/Economische verkenningen MRA2011_tcm14-228419.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/How To Reach Amsterdam RAI.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/IBM_Amsterdam_HDK.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/kwaliteitswijzer_291111_web_def.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/mas0.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/route-oracle-amsterdam1-157087-nl.pdf
[INFO ][LangMatcherTest] Language Dutch(nl): docs/nl/{00C1E905-406C-490B-8079-5B9DCA9927BA}_C_NED.pdf
[INFO ][LangMatcherTest] Language English(en): loading
[INFO ][LangMatcherTest] Language English(en): 8 files
[INFO ][LangMatcherTest] Language English(en): docs/en/188741.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/Article33.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/BOSTmap.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/ff-boston_tcm7-4572.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/file84471.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/InformingTheDebate_Final.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/preven_code_tcm3-4039.pdf
[INFO ][LangMatcherTest] Language English(en): docs/en/STATEOFBLACKBOSTON_000.pdf
[INFO ][LangMatcherTest] Language French(fr): loading

Dump files

When the test passes to the language you can get extracted text in dump directory in the root of the project.

dump/nl
dump/nl/bereikbaarkaart-nl_print.pdf.txt
dump/nl/de_bodem_onder_amsterdam.pdf.txt
dump/nl/Economische verkenningen MRA2011_tcm14-228419.pdf.txt
dump/nl/How To Reach Amsterdam RAI.pdf.txt
dump/nl/IBM_Amsterdam_HDK.pdf.txt
dump/nl/kwaliteitswijzer_291111_web_def.pdf.txt
dump/nl/mas0.pdf.txt
dump/nl/route-oracle-amsterdam1-157087-nl.pdf.txt
dump/nl/{00C1E905-406C-490B-8079-5B9DCA9927BA}_C_NED.pdf.txt
dump/en
dump/en/188741.pdf.txt
dump/en/Article33.pdf.txt
dump/en/BOSTmap.pdf.txt
dump/en/ff-boston_tcm7-4572.pdf.txt
dump/en/file84471.pdf.txt
dump/en/InformingTheDebate_Final.pdf.txt
dump/en/preven_code_tcm3-4039.pdf.txt
dump/en/STATEOFBLACKBOSTON_000.pdf.txt

Scala in a Mavenized Netbeans project

I like Netbeans development environment, with it I can handle Java, C++, Ruby and now Scala. I found Scala NetBeans Plugin, in my research about how to bring netbeans working with Scala I came across some outdated documentation, in some of those the plugin working only with Scala 2.8.x + Netbeans 6.9, issues with OSX Lion and so on.
Another problem(no problem for you but for me) is the Scala NetBeans Plugin is based on Apache Ant build project, I prefer to use Apache Maven instead. I’ve got some karma by this liking. To save your time, I’ve written some steps to put Java, Scala, Maven and Netbeans working together.

Scala 2.9.1

To install Scala just download it, uncompress it to a directory, in this example, I’m going to use /Application/CustomApps so my SCALA_HOME is

/Applications/CustomApps/scala-2.9.1.final

Download Netbeans 7.1

Download Netbeans 7.1

Install it, probably, the installer will place it to /Applications/NetBeans/NetBeans7.1.app, let’s bind SCALA_HOME to be accessible in Netbeans. Edit Netbeans start up script

/Applications/NetBeans/NetBeans\ 7.1.app/Contents/Resources/NetBeans/bin/netbeans

After commented lines export SCALA_HOME environment variable, this way

export SCALA_HOME=/Applications/CustomApps/scala-2.9.1.final

Ps. There was a way to bind environment variables by adding a .plist in ~/.MacOSX/environment.plist. it’s deprecated in OSX Lion then you can edit Netbeans start up script that will work for all OSX versions.

Scala NetBeans Plugin(nbscala)

Download Scala plugin for Netbeans 7.1 and Scala 2.9.x. Uncompress it anywhere, but don’t forget the path :)

Installing the plugin

Open Netbeans, menu Tools -> Plugins -> Downloaded, button Add Plugins, select *.nbm files of nbscala plugin. Say yes/agree/allow for every dialogs.

Maven vs Ant

The Netbeans Scala plugin uses ant as builder if you don’t care about using ant, this tutorial ends up here. If you want to Maven(yes! Maven!) go ahead in the next topic.

Maven based Scala project

Let’s create a new ‘mavenized’ project, File -> New Project -> Maven -> Java Application go there and place the project in where do you want to. There is the maven-scala-plugin we’re going to use it, it is not needed download anything, Maven takes care about that, do you need to add some lines to your pom.xml.

The Maven repository for Scala

<repositories>
  <repository>
      <id>scala-tools.org</id>
      <name>Scala-tools Maven2 Repository</name>
      <url>http://scala-tools.org/repo-releases</url>
    </repository>
</repositories>

The build entry

<build>
    <plugins>
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <version>2.15.2</version>
        <executions>
          <execution>
            <phase>process-resources</phase>
            <goals>
              <goal>add-source</goal>
              <goal>compile</goal>
            </goals>
          </execution>
          <execution>
            <id>scala-test-compile</id>
            <phase>process-test-resources</phase>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>${scala.version}</scalaVersion>
          <sendJavaToScalac>true</sendJavaToScalac>
          <args>
            <arg>-target:jvm-1.5</arg>
            <arg>-g:vars</arg>
            <arg>-deprecation</arg>
            <arg>-dependencyfile</arg>
            <arg>${project.build.directory}/.scala_dependencies</arg>
          </args>
        </configuration>
      </plugin>
    </plugins>
</build>

And scala-lang dependency

<dependency>
     <groupId>org.scala-lang</groupId>
     <artifactId>scala-library</artifactId>
     <version>2.9.0</version>
 </dependency>

Before run the project, you may create scala source directory src/main/scala and also the test src/test/scala.

A peace of scala code

Under src/main/scala create a directory mytest it will be our package. Now theMain.scala object in src/main/scala/mytest/Main.scala with content:

package mytest
import java.io.File

object Main {

  def main(args: Array[String]):Unit = {
    println("Hello this is my /tmp")
    for(file <- new File("/tmp").listFiles){
      println(file)
    }
  }
}

By default Netbeans continues point to App.java, you may change the main class in right-click in Project -> Properties -> Run in the Main Class type mytest.Main.

F6 and be happy.

ElasticSearch & Tika – the mapper-attachment plugin

ElasticSearch supports text extraction at indexing time, it’s called mapper-attachment plugin, basically it gets a base 64 encoded field, decoded it and so invokes Apache Tika to get its content. I had some troubles to get it running by current documentation then I’ve created a simple test project to get more sense about the mapping. I’m going to describe here.

Mapping

ElasticSearch uses JSON to indexing and searching, it also uses JSON in its configuration. In the ElasticSearch java client, you have a bunch of builders they facilitate a lot JSON creation in a DSL. In few words a mapping an index layout configuration, to the test I created the following structure:

String idxName = "test";
String idxType = "attachment";
XContentBuilder map = jsonBuilder().startObject()
        .startObject(idxType)
          .startObject("properties")
            .startObject("file")
              .field("type", "attachment")
              .startObject("fields")
                .startObject("title")
                  .field("store", "yes")
                .endObject()
                .startObject("file")
                  .field("term_vector","with_positions_offsets")
                  .field("store","yes")
                .endObject()
              .endObject()
            .endObject()
          .endObject()
     .endObject();

It just hold the mapping in the object map, you can to create the index and mapping it in the same time.

CreateIndexResponse resp = client.admin().indices().prepareCreate(idxName).setSettings(
            ImmutableSettings.settingsBuilder()
            .put("number_of_shards", 1)
            .put("index.numberOfReplicas", 1))
            .addMapping("attachment", map).execute().actionGet();

assertThat(resp.acknowledged(), equalTo(true));

As you may see above, CreateIndexResponse provides a method acknowledged to verify the answer that you got.

Indexing

The next step is to index a PDF document encoded in base64. I’m going to index a document with id 80.

String pdfPath = ClassLoader.getSystemResource("fn6742.pdf").getPath();
String data64 = org.elasticsearch.common.Base64.encodeFromFile(pdfPath);
XContentBuilder source = jsonBuilder().startObject()
        .field("file", data64).endObject();

IndexResponse idxResp = client.prepareIndex().setIndex(idxName).setType(idxType).setId("80")
        .setSource(source).setRefresh(true).execute().actionGet();

Searching

Now, you can perform search on it

QueryBuilder query = QueryBuilders.queryString("amplifier");

SearchRequestBuilder searchBuilder = client.prepareSearch().setQuery(query)
        .addField("title")
        .addHighlightedField("file");
SearchResponse search = searchBuilder.execute().actionGet();

Highlights

In the search result set you can get the hits and the highlights.

    assertThat(search.hits().totalHits(), equalTo(1L));
    assertThat(search.hits().hits().length, equalTo(1));
    assertThat(search.hits().getAt(0).highlightFields().get("file"), notNullValue());
    assertThat(search.hits().getAt(0).highlightFields().get("file").toString(), containsString("<em>Amplifier</em>"));

It is just a parsed search result for:

{
  "took" : 136,
  "timed_out" : false,
  "_shards" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.005872132,
    "hits" : [ {
      "_index" : "test",
      "_type" : "attachment",
      "_id" : "80",
      "_score" : 0.005872132,
      "fields" : {
        "file.title" : "ISL99201"
      },
      "highlight" : {
        "file" : [ "\nMono "<em>Amplifier</em>"\nThe ISL99201 is a fully integrated high efficiency class-D \nmono "<em>amplifier</em>". It is designed" ]
      }
    } ]
  }
}

Very simple to get searching done in your system, files such as PDF and MS documents without previous steps to extract documents.

Replicando Memcached com Rails 3

Um dos fatores de qualidade de SaaS é eliminar os SPOFs ganhando assim alta disponibilidade do serviço para o usuário final. A regra é simples, se tem algo no sistema que é único, tenha uma backup desse serviço, a.k.a redundância. Exemplo dois ou mais replicas de banco de dados, mais de um servidor http, mais de um worker e etc.
Na modalidade de negócios Saas isso significa credibilidade e deve ser mantida no mais alto nível quanto possível. Quando há uma falha crítica o cliente não quer saber se o correu falha humana, falha no banco, falha no hosting, no storage ou qualquer outro componente da solução, o cliente quer a informação no momento do acesso. Quaisquer tipo de transtorno causados no acesso dessa informação custa caro para empresa, é um chamado a mais para um atendente, é um ticket a mais para você solucionar.

E a sorte nunca esta do lado executivo-da-força, se tem algo pra dar problema… vai acontecer hoje ou outro dia. Isso não é pessimismo, isso é realidade do dia-a-dia em tentar deixar uma big aplicação Always on no cloud.

Falando em cloud e indo para um nível baixo, na comunidade Ruby on Rails existe muito barulho na parte de escalabilidade, aquela velha história do “O Rails não escala”, não continuarei esse briga aqui… o intuito desse post é compartilhar uma estrutura que estamos usando aqui na OfficeDrop para manter a sessão do usuário mesmo se houver um erro tanto no memcached master ou se houver uma sobrecarga no uso do mesmo.

O workflow de sessão

O fluxo de utilização do memcached pelo Rails é bem simples. O memcache-client instanciado no Rails estabelece um conexão TCP/IP(pode utilizar UDP também) com o memcached, considerando uma conexão ativa ente eles, você pode utilizar vários comandos disponíveis no memcache-client que na verdade é um wrapper facilitador. Se você for curioso o bastante inicie o memcached e teste-o com o telnet.

Inicie com very verbose mode pra ver as mensagens sendo trocadas pelo protocolo

 memcached -vv

Obs: estou considerando o memcached instalado no seu sistema.

A porta padrão do memcached é 11211

telnet localhost 11211

Com a conexão ativa você pode executar comandos como o set para armazenar uma valor utilizando uma chave como indexado.

 set msg 0 0 7
 diga oi

As duas linhas acima estão dizendo “utilize a chave “msg” com flag 0 (o primeiro), sem tempo de expiração (o segundo 0 ) com o valor ‘diga oi’ com o comprimento 7 (length)”. Para obter o valor, use get com a chave e comprimento do valor.

get msg 7

ficando assim seu shell

telnet localhost 11221
Trying ::1...
Connected to localhost.
Escape character is '^]'.
set msg 0 0 7
diga oi
STORED
get msg 7
VALUE msg 0 7
diga oi
END

Dê uma olhada aqui para saber sobre mais comandos.

Na Prática

É bem popular a utilização do memcached no rails mas tem um problema na utilização padrão. Mesmo configurando um failover em um array de servidores ["mem1:11211", "mem2:11212"] isso não garante, por exemplo, que a sessão do usuário iniciada no mem1 será mantida no servidor mem2 se houver uma queda no mem1. Então, falhando mem1 o usuário terá que logar novamente, mas dessa vez usando o mem2.

Repcached

Para evitar que o usuário tenha esse desprazer de logar novamente utilizamos o Repcached.

O Repcached é um patch para lidar com replicação multi-master assíncrona, multi-master aqui quer dizer, mem1 replicará para mem2, se mem1 falhar mem2 receberá as novas requisições e quando o mem1 tonar ativo o mem2 enviará os dados para o mem1.

Cenários

Cenário 1 – Apenas uma VM
Ideal se o seu sistema só tem um servidor.

Não é tão ideal assim, se seu sistema só tem um servidor … repcached não resolve muito o seu caso. Você usaria repcached apenas para redundar o servidor master na mesmo host.

Cenário 2 – Dois ou mais VMs

Ideal para sistema médio ou grande. Com essa configuração cada instância do Rails utilizará o memcached na máquina local e o failover para o servidor central. Então quando o usuário inicia a sessão no repcached1 e por acaso venha a falhar a sessão pode ser retomada usando o repcached2.

Para os dois cenários a configuração é a mesma, mudando apenas os IPs.

Compilando

A instalação é bem simples apenas com libevent como dependência, a compilação abaixo é para MacOS, se você esta usando linux verifique no seu gerenciador de pacote como instalar libevent no seu sistema.

Instale libevent

sudo port install libevent

Baixe, descompacte e entre no diretório que foi criado

wget http://sourceforge.net/projects/repcached/files/repcached/2.2-1.2.8/memcached-1.2.8-repcached-2.2.tar.gz/download
tar zxf memcached-1.2.8-repcached-2.2.tar.gz
cd memcached-1.2.8-repcached-2.2

No configure, passe o flag --enable-replication

./configure --prefix=/opt/local --enable-replication

Compile e instale

sudo make install

Observe o destino em que o repcached foi instalado, observe também que o nome do executável é o mesmo do memcached. Lembrando… repcached é o memcached com patch para replicação.

/usr/bin/install -c 'memcached' '/opt/local/bin/memcached'
/usr/bin/install -c 'memcached-debug' '/opt/local/bin/memcached-debug'

Se você achar melhor, crie um alias

alias repcached=/opt/local/bin/memcached

Após a instalação execute o repcached com -h, nas duas primeiras linhas o memcached original e o repcached.

/opt/local/bin/memcached -h
memcached 1.2.8
repcached 2.2

E nas últimas linhas o -x host e -X port para a replicação

-x   hostname or IP address of peer repcached
-X       TCP port number for replication (default: 11212)

Veja que a porta de replicação padrão é 11212, então nenhum memcached poderá ser iniciado na mesma.
Configure /etc/hosts para ambas as VMs

192.168.20  mem1
192.168.20  mem2

Iniciando os servidores

Vamos iniciar o primeiro servidor, o mem1, -p(TCP port 11220) e -U (UDP port 11220) são parâmetros para o memcached local, -x (ip mem2) e -X (port 11230) são parâmetros para replicação. Então, no mem1 o comando é.

memcached -vv -p 11220 -U 11220 -x mem2 -X 11230

Observe que na inicialização aparecerá o servidor de replicação utilizado

replication: connect (peer=mem2:11230)
replication: marugoto copying
replication: close
<5 connection closed.

Mas com a replicação inativa.
Agora na mem2, inicie o memcached apontando pra mem1

memcached -vv -p 11220 -U 11220 -x mem1 -X 11230

Agora, o mem2 ativa a replicação

replication: connect (peer=mem1:11230)
<4 new client connection
replication: marugoto copying
<4 marugoto_end
<7 server listening
<8 server listening
<9 send buffer was 9216, now 3728270
<9 server listening (udp)
<10 send buffer was 9216, now 3728270
<10 server listening (udp)
replication: start

Se você der uma olhada no mem1 depois de iniciar a replicação no mem2 o handshake começa.

<4 server listening (replication)
<5 server listening (replication)
replication: listen
<6 server listening
<7 server listening
<8 send buffer was 9216, now 3728270
<8 server listening (udp)
<9 send buffer was 9216, now 3728270
<9 server listening (udp)
replication: accept

Configurando o Rails 3.0.3

Dado que os servidores estão configurados vamos para o Rails. Utilizaremos o Rails 3 para o teste. Então no Gemfile use

gem "memcache-client"
gem 'memcached-northscale', :require => 'memcached'

Execute o bundle

bundle install

Configure os servidores no config/environment.rb, com opções essenciais, habilitando multi-thread, compressão para grande quantidade de dados e os servidores. Usei o nome App para a aplicação então o arquivo config/environment.rb fica parecido com:

require File.expand_path('../application', __FILE__)

RAILS_CACHE  = Memcached::Rails.new {
  :compression => true,
  :multithread => true,
  :servers => ['mem1:11211','mem2:11211']
}

# Initialize the rails application
App::Application.initialize!
App::Application.config.cache_store = :mem_cache_store
App::Application.config.session_store(:mem_cache_store, :cache => RAILS_CACHE)

O teste

Você pode testar criando um scaffold usando session[] mas aqui vou usar o console pra demonstrar a replicação. Um simples teste,

1. Usar Rails.cache
2. Armazenar um valor
3. Fechar o mem1
4. Ler o valor

Então abra o console e verifique se o Rails.cache esta usando o memcache-client

rails console
Loading development environment (Rails 3.0.3)
>> Rails.cache.class
=> Memcached::Rails

Armazenando um valor

>> Rails.cache.set "message", "oi tudo bem?"
=> true

No STDOUT do mem1 você verá algo como

<11 set mytestmessage 0 604800 16
>11 STORED

e no mem2

<10 rep mytestmessage 0 1292878695 16 2
REP>10 STORED

Agora feche o mem1, volte no console do rails e leia o valor

>> Rails.cache.get "message"
=> "oi tudo bem?"

Hmm então funcionou!

No mem2 você verá que o repcached está tentando replicar para mem1. Como o memcached é volátil quando o servidor memcached é fechado todas as chaves são perdidas mas como a replicação esta ativa e o mem2 tem as chaves, no ato de iniciar o mem1 novamente, mem2 sincronizará as chaves e os valores com mem1.

replication: marugoto copyinga
<4 rep mytestmessage 0 1292878695 16 2
REP>4 STORED
<4 marugoto_end

Então é isso, se você precisar de esteroides na sua sessão da sua aplicação... repcached é uma boa pedida.

A gem memcached-northscale tem várias opções de configuração do memcached no rails, recomendo a leitura da documentação que esta em rdoc.

Outra opção seria o Redis talvez em outro momento eu posto algo sobre ele.