無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

Code Climate + Circle CI でRubyプロジェクトのコードカバレッジを計測する手順

Code Climate + Circle CI でRubyプロジェクトのコードカバレッジを計測する手順です。 試したのはCircle CIですが、テストが実行できる環境であればTravis CIやJenkinsでも同じ仕組みでできるはず。

概要

  1. テストに SimpleCov を仕込み、コードカバレッジを計測
  2. SimpleCov のレポーターに codeclimate-test-reporter を追加して、結果を Code Climate に送信
    Code Climateカバレッジが集計されるようになります。

  3. あとは、Circle CIなりTravis CIなりでテストを実行すればOK

簡単ですね。

1.テストに SimpleCov を仕込んでカバレッジを計測する

SimpleCov をbundlerでインストールします。

Gemfileに以下を追加して、

gem 'simplecov', :require => false, :group => :test

インストール。

$ bundle install

テストの先頭に、SimpleCovの設定を行うコードを追加します。

require 'simplecov'

# カバレッジレポートの出力先を指定
# Circle CIで実行する場合は、ビルド成果物置き場に、
# ローカルで実行する場合は、 ./build/coverage に作成します。
dir = File.join(ENV['CIRCLE_ARTIFACTS'] || 'build', 'coverage')
SimpleCov.coverage_dir(dir)

SimpleCov.start do
  # /vendor/,/spec/ を集計対象から除外
  add_filter '/vendor/'
  add_filter '/spec/'
end

テストを実行すれば、 ./build/coverage/index.html にカバレッジレポートが作成されるはず。

2. SimpleCov に codeclimate-test-reporter を追加する

Gemfileに一行追加して、bundler でインストール

gem 'codeclimate-test-reporter', :require => false, :group => :test
$ bundle install

SimpleCov の設定部分を以下のように変更。レポーターを追加します。

require 'simplecov'
require 'codeclimate-test-reporter'   # ★追加

dir = File.join(ENV['CIRCLE_ARTIFACTS'] || 'build', 'coverage')
SimpleCov.coverage_dir(dir)

SimpleCov.start do
  add_filter '/vendor/'
  add_filter '/spec/'

  # ★追加
  formatter SimpleCov::Formatter::MultiFormatter[
    SimpleCov::Formatter::HTMLFormatter,
    CodeClimate::TestReporter::Formatter
  ]
end

3. Circle CIに 環境変数を追加

最後に、環境変数 CODECLIMATE_REPO_TOKEN を設定します。 これをしないと、レポーターを追加しても 結果の送信はされません。

まず、CODECLIMATE_REPO_TOKEN の値を Code Climate の Settings ページから取得します。(プロジェクトごとに異なります。)

f:id:unageanu:20150322144620p:plain

Circle CIのページの右上にある、Project Settingsをクリック。

f:id:unageanu:20150322144617p:plain

Enviroment variables に移動し、

f:id:unageanu:20150322144618p:plain

CODECLIMATE_REPO_TOKEN を設定します。

f:id:unageanu:20150322144619p:plain

あとは、Circle CIでテストを実行すると、Code Climate のページでコードカバレッジが見られるようになるはず。

f:id:unageanu:20150322144621p:plain

参考: バッジを貼る

Code Climate の Settings ページに各種形式のURLがあるので、コピペして貼り付ければOK

f:id:unageanu:20150322144622p:plain

キャッシュしたデータが消える!?prefork型HTTPサーバーUnicornでドはまりしたメモ

HTTPサーバーUnicornを使っていてドはまりしたのでメモ。
主に、Unicornで動かすRackアプリでの変数スコープとpreload_appについて。

サマリ

  • Uncornは、RackアプリケーションのためのHTTPサーバーです。
  • HTTPリクエストを、メインプロセスからforkした子プロセスで処理するアーキテクチャが特徴。
    • スレッドとかは使いません。
  • forkした子プロセスは、それぞれ別のメモリ空間を持つので、
    • HTTPリクエストの処理間で、状態(変数の値)が共有されません。
      • ある処理で変数の値を変更しても、別の子プロセスで処理しているHTTPリクエストでは変数値は変わりません。
      • ただし、子プロセスが同じ場合は、状態が引き継がれます。(処理が終わった子プロセスは、次のHTTPリクエストで使いまわされます)
    • 状態が共有されないので、スレッドセーフティを考慮した実装にする必要もありません。
    • もし、メモリリークしても、子プロセスを再作成すればOK。

ドはまりした状況を再現する

↓のような、オンメモリキャッシュを作っていました。

  • Singleton インスタンスにキャッシュデータを格納して共有。
  • Singleton なので、同じRuby VM上なら共有されるはず。
# coding: utf-8

require 'sinatra/base'
require 'singleton'

# キャッシュ
class Cache
  include Singleton
  
  def initialize
    @store = {}
    @mutex = Mutex.new
  end
  
  def create_or_get(key, &generator)
    @mutex.synchronize {
      unless @store.include? key
        puts "cache is not exists. key=#{key}"
        @store[key] = generator.call
      else
        puts "cache exists. key=#{key} value=#{@store[key]}"
      end
      @store[key]
    }
  end
  
end

class App < Sinatra::Base
  get '/' do
    Cache.instance.create_or_get(:date) { DateTime.now.iso8601 } 
  end
end

リクエストを何回か投げてみます。

cache is not exists. key=date
10.0.2.2 - - [14/Feb/2015 12:32:18] "GET / HTTP/1.1" 200 25 0.0587
cache exists. key=date value=2015-02-14T12:32:18+09:00
10.0.2.2 - - [14/Feb/2015 12:32:19] "GET / HTTP/1.1" 200 25 0.0021
cache exists. key=date value=2015-02-14T12:32:18+09:00
10.0.2.2 - - [14/Feb/2015 12:32:21] "GET / HTTP/1.1" 200 25 0.0084
cache is not exists. key=date ★
10.0.2.2 - - [14/Feb/2015 12:32:22] "GET / HTTP/1.1" 200 25 0.0234
cache exists. key=date value=2015-02-14T12:32:22+09:00
10.0.2.2 - - [14/Feb/2015 12:32:26] "GET / HTTP/1.1" 200 25 0.0071


キャッシュされたはずのデータが、、消えてる・・・!?・・・って、Unicornの仕組みを考慮すれば、そりゃそうなんですが。
恥ずかしながら、その辺の仕組みを知らずに何気なく使っていた私は、ドはまりしました。

  • 最初は、キャッシュへの投入がどこかで失敗してる?と思ってデバッグするも、そんなことはなく。
  • 次に、Cacheのインスタンスが違うものになっている可能性を考え、object_idを確認。しかし、格納時/参照時とも、同じインスタンスが使われていました。
    • 後述するprefork_app=trueにしてたので、forkした子プロセスはメインプロセスからコピーしたアドレス空間を共有します。
    • なので、object_idもまったく一緒になるのです。

そもそもforkとは何か

fork(フォーク)とは、プロセスのコピーを生成するものである。
...
forkが呼び出されると、子プロセスのためのアドレス空間が新たに作成される。子プロセスのアドレス空間には親プロセスが持っていた全セグメントのコピーがあるが、コピーオンライト機能によって実際の物理メモリの確保は遅延される

Wikipedia:fork より

  • forkとは、プロセスのコピーを生成するもの
  • forkしてできた子プロセスは、独自のアドレス空間を持つ。
  • また、子のアドレス空間には、親プロセスが持っていた全セグメントのコピーが作成される。
  • この時、メモリを全コピーするとコストが高いので、コピーオンライトの仕組み提供されている。
    • fork時に、実際にはメモリの値をコピーせず、親子で共有する。
    • 子プロセスがメモリを書き換えたときに、そのページだけコピーを作成して使用する。

ふーむ。

preload_app

さて、この仕組みだとすべてのプロセスがRuby VMをごっそり持つので、

    • メモリたくさん食うんじゃね?
    • 子プロセスごとにRackアプリのロードが走って遅いんじゃね?

とか思うわけですが、このあたりの問題を回避する仕組みとして、preload_app(の設定)があります。

preload_app=trueにすると、メインプロセス内でアプリのロードが行われ、fork時にまるまる複製されるようになります。なので、子プロセス側でのロードは発生しません。また、コピーオンライトの仕組みでメモリも大部分が共有されるので、実メモリの使用量も少なくて済みます。

ということで、検証。
ロードに時間がかかる状態をエミュレーションするアプリを作ります。

# coding: utf-8

require 'sinatra/base'
require 'singleton'


puts "start loading."
sleep 5
puts "end loading."

class LazyApp < Sinatra::Base
  get '/' do
    "Lazy"
  end
end

preload_app=true で起動すると、子プロセスのforkの前に1度だけロードが走ります。

I, [2015-02-14T14:42:14.329315 #8117]  INFO -- : Refreshing Gem list
start loading.
end loading.
I, [2015-02-14T14:42:19.411606 #8117]  INFO -- : listening on addr=0.0.0.0:5000 fd=9
I, [2015-02-14T14:42:19.411817 #8117]  INFO -- : worker=0 spawning...
I, [2015-02-14T14:42:19.413196 #8117]  INFO -- : worker=1 spawning...
I, [2015-02-14T14:42:19.413949 #8144]  INFO -- : worker=0 spawned pid=8144
I, [2015-02-14T14:42:19.415228 #8117]  INFO -- : worker=2 spawning...
I, [2015-02-14T14:42:19.416395 #8117]  INFO -- : master process ready
I, [2015-02-14T14:42:19.416847 #8144]  INFO -- : worker=0 ready
I, [2015-02-14T14:42:19.417557 #8149]  INFO -- : worker=2 spawned pid=8149
I, [2015-02-14T14:42:19.418379 #8147]  INFO -- : worker=1 spawned pid=8147
I, [2015-02-14T14:42:19.421307 #8149]  INFO -- : worker=2 ready
I, [2015-02-14T14:42:19.421429 #8147]  INFO -- : worker=1 ready

preload_app=false だと、forkされたプロセスの数だけ、最初にロードが走ります。

I, [2015-02-14T14:45:24.395364 #8154]  INFO -- : listening on addr=0.0.0.0:5000 fd=9
I, [2015-02-14T14:45:24.395553 #8154]  INFO -- : worker=0 spawning...
I, [2015-02-14T14:45:24.397392 #8154]  INFO -- : worker=1 spawning...
I, [2015-02-14T14:45:24.398156 #8181]  INFO -- : worker=0 spawned pid=8181
I, [2015-02-14T14:45:24.398442 #8181]  INFO -- : Refreshing Gem list
I, [2015-02-14T14:45:24.398412 #8154]  INFO -- : worker=2 spawning...
I, [2015-02-14T14:45:24.399575 #8184]  INFO -- : worker=1 spawned pid=8184
I, [2015-02-14T14:45:24.399857 #8184]  INFO -- : Refreshing Gem list
I, [2015-02-14T14:45:24.401218 #8154]  INFO -- : master process ready
I, [2015-02-14T14:45:24.401716 #8187]  INFO -- : worker=2 spawned pid=8187
I, [2015-02-14T14:45:24.402049 #8187]  INFO -- : Refreshing Gem list
start loading.
start loading.
start loading.
end loading.
end loading.
end loading.
I, [2015-02-14T14:45:29.529195 #8184]  INFO -- : worker=1 ready
I, [2015-02-14T14:45:29.533661 #8187]  INFO -- : worker=2 ready
I, [2015-02-14T14:45:29.541946 #8181]  INFO -- : worker=0 ready

JavaTomcatな世界しか知らないと、こんなところではまったりします。うーむ。

resqueで非同期分散処理を試す

resqueは非同期処理の仕組みを提供するライブラリです。

  • Webアプリで、画像変換とかメールの一括送信のような重たい処理を行うときに使ったりします。
    • Railsに組み込んで使うこともできる模様。
  • 複数ノードでの分散処理にも対応。
  • 管理コンソールがついているので、処理ノード(worker)の状態や、積まれた処理の数を簡単に確認できるもポイント。

サンプルのタスクを作って、複数ノードで分散処理するところまで試してみました。

構成

docker上に構築します。用意したコンテナは以下。

  • redis
    • resqueではバックエンドにredisを使います。
  • resque-manager
    • resqueの管理コンソール(webアプリ)を動かすコンテナ
  • resque-console
    • resqueで行わせる処理(job)を置くコンテナです。
    • resqueへの処理の登録もこのコンテナから行います。
  • resque-worker x2
    • 非同期処理を実行するコンテナです。2つ用意します。

図にするとこんな感じ。


  • すべてのコンテナからredisにアクセスできるようにします。
  • resque-consoleにjobを置き、さらにData Volume Containerにして、resque-workerからもjobを参照できるようにします。
    • jobは、タスクの登録時および実行時の両方で必要です。
    • workerの実行時に必要なRakefileも、resque-consoleに置いて共有します。

1. redis コンテナを作る

まず、redisコンテナを作ります。

$ mkdir -p ~/work/redis
$ vi ~/work/redis/Dockerfile

Dockerfileの中身は以下。

FROM centos:centos7

RUN yum -y update; yum clean all
RUN yum -y install epel-release; yum -y install redis; yum clean all

EXPOSE 6379
CMD ["redis-server"]
  • centos7をベースに使います。
  • redisをインストールして、起動します。

Dockerfileを保存したら、Dockere Imageをビルドして実行。

$ sudo docker build --rm -t='redis' redis
$ sudo docker run -d --name=redis redis
  • -d オプションを指定して、バックエンドで実行します。
  • redisへのアクセスはコンテナ内からのみできればOKなので、ホスト側のポートとのマッピングはしていません。

2. resque-manager コンテナを作る

次にresque-manager コンテナを作ります。

$ mkdir -p ~/work/resque-manager
$ vi ~/work/resque-manager/Dockerfile

Dockerfileは以下。

FROM centos:centos7

RUN yum -y update; yum clean all
RUN yum -y install ruby; yum clean all
RUN gem install resque

EXPOSE 3000

CMD resque-web -p 3000 -r redis -F
  • rubyとresqueのgemをインストールします。
  • コンテナ起動時にresqueの管理コンソールを起動するように指定します。
    • 「-p 3000」で使用するポートを3000に変更。
    • 「-r redis」でredisノードを指定します。
      • コンテナ起動時に、「--link redis:redis」を指定することで、resque-managerコンテナの /etc/hosts にredisコンテナのアドレスが追加され接続できるようになる仕組み。(詳しくはこちら)なお、上記の例ではportは省略しています。
    • 「-F」でフォアグラウンドで実行するようにします。

ビルドして実行。

$ sudo docker build --rm -t='resque-manager' resque-manager
$ sudo docker run -d -p 3000:3000 --link redis:redis --name=resque-manager resque-manager
  • 「-p 3000:3000」で、ホストのポート3000へのアクセスをゲストのポート3000にマッピングするようにしています。

起動したら、ブラウザから http://127.0.0.1:3000 にアクセス。resqueの管理コンソールが表示されるはず。


3. resque-console コンテナを作る

管理コンソールの起動が確認できたら、resque-console コンテナを作ります。

$ mkdir -p ~/work/resque-console
$ vi ~/work/resque-console/Dockerfile

Dockerfile:

FROM centos:centos7

RUN yum -y update; yum clean all
RUN yum -y install ruby; yum clean all
RUN gem install resque

VOLUME ["/resque-data"]

CMD ["/bin/bash"]
  • jobの登録時に必要になので、rubyとresqueをインストールします。
  • 「VOLUME ["/resque-data"]」で data volume を追加。
    • ここにjobの定義ファイルやwoker起動用Rakefileを置いて、他のコンテナと共有します。

ビルドして実行。

$ sudo docker build --rm -t='resque-console' resque-console
$ sudo docker run -ti --link redis:redis --name=resque-console resque-console


起動後のコンソールから、jobファイルを作成します。

# cd /resque-data 
# vi job.rb

job.rb:

class Job 
  @queue = :default

  def self.perform(time)
    puts "start #{time}"
    sleep time
    puts "end #{time}"
  end
end
  • 非同期で行う処理の定義です。
  • perform メソッドが処理の本体になります。
  • @queue でキューの名前を指定します。

jobが用意できたので、resqueに登録してみます。

add_jobs.rb:

require 'resque'
require_relative 'job'

Resque.redis="redis" # 接続先とするredisを指定

10.times {|n|
  Resque.enqueue( Job, n )
}

実行すると、jobが登録されます。

# ruby add_jobs.rb

workerはまだないので、実行はされません。

4. workerの起動時に使用するRakefileを作る

resque-workerコンテナをつくる前にworker起動用のRakefileを用意しておきます。

  • workerはRake Taskの形で提供されているので、Rakefileを作る必要があります。
    • といっても、job.rbとresque/tasksをrequireするだけですけど。
    • あと、接続先とするredisもこの中で指定します。
  • Rakefileはresque-consoleコンテナの /resque-data に置き、resque-worker x2 と共有します。
    • アクセスできるようにするための設定は後述。

Rakefile:

require_relative 'job'
require 'resque/tasks'

Resque.redis="redis"

5. resque-worker コンテナを作る

最後にworkerコンテナを作ります。

$ mkdir -p ~/work/resque-worker
$ vi ~/work/resque-worker/Dockerfile

Dockerfile:

FROM centos:centos7

RUN yum -y update; yum clean all
RUN yum -y install ruby; yum clean all
RUN yum -y install rake; yum clean all
RUN gem install resque

CMD TERM_CHILD=2 QUEUE=default rake -f /resque-data/Rakefile resque:work
  • ruby,resque と rake も必要なので追加でインストールします。
  • CMDで、起動時にresqueのworkerを起動します。

ビルドして実行。別の名前で2つ実行します。

$ sudo docker build --rm -t='resque-worker' resque-worker
$ sudo docker run -d --link redis:redis --volumes-from resque-console --name="resque-worker1" resque-worker
$ sudo docker run -d --link redis:redis --volumes-from resque-console --name="resque-worker2" resque-worker
  • 「--volumes-from resque-console」でresque-consoleに追加した「/resque-data」にアクセスできるようにしています。

管理コンソールをリロードすると、workerが2つ起動しているはず。

jobの実行も始まります。

Rakeの基本的な使い方まとめ

Rakeの基本的な使い方のまとめです。

  • インストールから、Rakefileの書き方、組み込みライブラリの使い方まで。
  • 忘れたときに見返す用に。

Rakeって何?

rubyで処理内容を定義できるビルドツール。「xmlでなくrubybuild.xmlを書くAnt」ですな。

  • ruby専用とかいうわけではなく、javaのビルドなんかにも(使おうと思えば)使えます。
  • 処理内容をrubyで書けるので、ループとか条件分岐も思いのまま。
    • ただし、組み込みのタスクはちょい少ないので、ちょっとした作業させるにもコーディングが必要そうかな・・・。便利なライブラリがどっかにあったりするのかもですが・・・。

rakeツールのインストールと使い方

インストール

rubyrubygems は事前にインストールされている前提で。

$ gem install rake

rakeのgemをインストールするとrakeコマンドが使えるようになります。

rakeコマンド

よく使いそうなものをいくつか。

$ rake <タスク名> # 指定のタスクを実行。
$ rake <パラメータキー>=<値> <タスク名> # パラメータ指定ありで、指定のタスクを実行。
$ rake -h # 使い方を表示。
$ rake -T # 定義されているタスク一覧を表示
Rakefile

rakeが実行する処理内容(タスク)を定義するファイルです。rakeコマンドの引数でファイル名を指定できるので、任意の名前をつけても問題ないですが、デフォルトは「Rakefile」なのでそれに合わせておくとちょい楽。

Rakefileの書き方

Task

Rakeで行う処理は「タスク」としてRakefileで定義されます。タスクは以下の要素から成ります。

  • 名前
    • タスクを識別するための名前。rakeコマンドで実行するタスクを指定する場合などに使用します。
  • アクション
    • タスクで行われる一連の処理です。省略可です。
  • 事前タスク
    • 前提として実行するタスクの一覧です。指定された事前タスクは、タスクの実行前に実行されていなければ実行されます。
  • パラメータ
    • タスクに渡されるパラメータ名です。rakeコマンドの引数で

タスクは「task」メソッドで定義できます。

## 書き方
# task <タスク名> , [<パラメータ名>, <パラメータ名> ... ] => [<前提タスク名>,<前提タスク名> ... ] do
#    # アクション
# end

# "hello" を表示するだけのタスク
task :hello do
  puts 'hello'
end

# "hello"の後に"world"を出力するタスク
task :hellow_world=>[:hello] do
  puts 'world'
end

# アクションなしのタスク
task :all=>[:hellow_world, :echo]

#パラメータを受け付けるタスク
task :echo, [:message] => [:hello] do |t, args|
  # ブロックへのパラメータとしてタスクオブジェクトとパラメータが渡される。
  puts args.message
end

タスクに説明を付ける

「desc」で、次に作成するタスクの説明を設定できます。

desc "タスクの説明です。"
task :hoge

説明を付与しておくと、「-T」オプションの一覧で表示されます。

$ rake -T
(in xx/Rakefile)
rake hoge  # タスクの説明です。

指定のファイル/ディレクトリがあれば実行しないタスクを作る

「file」メソッドでタスクを定義すると、タスク名が示すファイルが存在しない場合のみ実行するタスク(File Task)を作成できます。「.classを作ったら消されるまで再コンパイルしない」といった場合に使えます。

# foo.txtがない場合のみ実行されるタスク
file "foo.txt" do
  puts "create foo.txt"
  open("./foo.txt", "w"){|f| f << "test." }
end

同様に、ディレクトリがない場合のみ作成するタスクも作成できます。こちらはアクションは指定できません。

# var/hoge ディレクトリがない場合のみ、ディレクトリを作るタスク。
# アクションは指定できない。
directory "var/hoge"

名前に対応するタスクが存在しない場合のタスクを指定する

「rule」メソッドで、名前に対応するタスクが存在しない場合に実行されるタスクを定義できます。

# *.txtが存在しない場合、作成するタスク
rule /.*\.txt/ do |t|
  puts "create #{t.name}"
  open("#{t.name}", "w"){|f| f << "test." }
end

rubyの「method_missing」的な機能ですかね。ただ、ruleで定義したタスクはFile Task扱い(=タスク名が示すファイルが存在しない場合のみ実行される)のでご注意。

名前空間

「namespace」で、任意の名前空間の中にタスクを定義できます。

# ネームスペースの中にタスクを定義する。
namespace :test do
  task :hello do
    puts 'hello'
  end
  task :hello_world=>[:hello] do
    puts 'world'
  end
end

タスクをグループ化したいときに。実行時には「<ネームスペース>:<タスク名>」の形式で指定します。

$ rake test:hello_world
(in xx/Rakefile)
hello
world

組み込みタスクを使う

よく使うタスクがいくつか組み込みで用意されていて、さくっと使えるようになっています。

成果物を削除する

clean/clobberタスクは

require 'rake/clean'

で定義されるタスクです。

  • clean
    • Remove any temporary products. (ビルドの一時作成物を削除する?)
    • デフォルトでは["**/*~", "**/*.bak", "**/core"]を削除するので、他に消したいものがあれば、定数「CLEAN」に追加します。
require 'rake/clean'
CLEAN << "*.txt"
  • clobber
    • Remove any generated file.(ビルドの作成物を削除する?)
    • cleanで消されるファイル+定数「CLOBBER」のファイルを削除します。
    • cleanとの違いはいまいちわからん。
rdocを作る

RDocTaskでrdocを生成するタスクをさくっと定義できます。

# 「lib/**/*.rb」のrdocを生成するタスク
require 'rake/rdoctask'
Rake::RDocTask.new do |rd| # RDocTask 内で rdoc,rdoc_clobber,rerdocが定義される。
  rd.rdoc_dir = 'rdocs'
  rd.rdoc_files = FileList["lib/**/*.rb"]
  rd.options << '-charset=UTF-8 '
end

これで、

$ rake -T
(in xx/Rakefile)
rake clobber_rdoc  # Remove rdoc products
rake rdoc          # Build the rdoc HTML Files
rake rerdoc        # Force a rebuild of the RDOC files

が定義され、実行できるようになります。

$ rake rdoc
(in xx/Rakefile)
rm -r rdocs

                            test.rb: c....
Generating HTML...

Files:   1
Classes: 1
Modules: 0
Methods: 4
Elapsed: 0.717s
Gemを作る、UnitTestを実行する .. etc ..

その他、未確認ですが、gemを作ったりUnitTestを実行したりするタスクもrdocのようなユーティリティでさくっと定義できるみたいです。詳細はrdocで。

補足: 参考にしたサイトなど

一定期間経過するとログアウト状態になる問題に対応したsbiclientをプレリリース

tana さんよりご報告頂いた、「起動後一定期間経過するとレート情報が取得できなくなってしまう」問題に対応したsbiclientをプレリリースします。

Github - unageanu / sbiclient
Gemcutter.org - sbiclient

原因

ログイン後一定期間経過すると、強制的にログアウト状態にされることが原因のようです。明確な時間は不明ですが、こちらで試した限りでは1時間程度でログアウト状態になりました。定期的にレート情報を取得しているにもかかわらず、です。

対応

ログアウトによりレート情報が取得できない場合、再ログインする処理を追加して対応します。

ステータス

動作確認中ですが、試してみたい方は以下の手順で更新できます。

$ gem update sbiclient

jijiのrestartもお忘れなく。不具合等あればご報告いただければ幸いです。

Javaメソッドの呼び出し元をツリー表示するスクリプト

JavaClassを使って指定メソッドの呼び出し元を探索しツリー表示するスクリプトを書いてみました。

  • 引数で指定されたパス以下の*.class,および*.jarに含まれるクラスファイルを解析し、
  • メソッドの呼び出しコードを収集。
  • 収集した情報を再帰的に探索し、呼び出し元メソッドをツリー形式で出力します。

使い方

$ ./caller.rb <クラスが置かれたディレクトリorJarファイル> <呼び出し元を探すメソッド>
java.Util.ArrayList#indexOf(Ljava/lang/Object;)I
  • クラスが置かれたディレクトリ or Jarファイルは、「;」区切りで複数指定可能です。

具体例

↓のようなクラスがあったとして、

package com.example.caller;

public class CallSample {
    static class A {
        void aaa() {
            new B().bbb();
        }
        void aaa2() {
            new C().ccc();
        }
        void aaa3() {
            aaa2();
        }
    }
    static class B {
        void bbb() {
            new C().ccc();
        }
    }
    static class C {
        void ccc() {}
    }
}

C#ccc()」の呼び出し元を探索すると、

$ ./caller.rb "../lib;../classes" "com.example.caller.CallSample\$C#ccc()V"

以下が出力されます。

com.example.caller.CallSample$C#ccc()V
├com.example.caller.CallSample$A#aaa2()V
│└com.example.caller.CallSample$A#aaa3()V
└com.example.caller.CallSample$B#bbb()V
 └com.example.caller.CallSample$A#aaa()V

実装

次のとおりです。

#!/usr/bin/ruby

require "rubygems"
require "zip/zip"
require "javaclass"
require "kconv"

# クラスプール
class ClassPool
  def initialize
    @classes = {}
    @classes_r = {}
  end
  def <<( jc )
    jc.methods.each {|m|
      key = jc.name + "#" + m.name + m.descriptor
      @classes[key] ||= []
      next unless m.attributes.key? "Code"
      m.attributes["Code"].codes.each {|code|
        case code.opcode
          when 0xB6,0xB7,0xB8,0xB9
            m = jc.get_constant( code.operands[0].value )
            add_caller( key,
              m.class_name.name + "#" +
              m.name_and_type.name + m.name_and_type.descriptor
            )
          when 0xB2,0xB3,0xB4,0xB5
            m = jc.get_constant( code.operands[0].value )
            add_caller( key,
              m.class_name.name + "." + m.name_and_type.name
            )
        end
      }
    }
  end
  attr_reader :classes
  attr_reader :classes_r
private
  def add_caller( from, to )
    @classes[from] ||= []
    @classes[from] << to
    @classes_r[to] ||= []
    @classes_r[to] << from
  end
end

#ノード
class Node
  def initialize( name  )
    @name = name
    @children = []
  end
  def <<(child)
    @children << child
  end
  def to_s( indent="" )
    str = indent.dup
    str << name.to_s << "\n"
    child_indent = indent.gsub(//, "").gsub(//, " ")
    @children.each_index {|i|
       next if children[i] == nil
       tmp = child_indent + ( i >= children.length-1 ? "" : "" )
       str << children[i].to_s( tmp )
    }
    return str
  end
  attr :name, true
  attr :children, true
end

#呼び出し元を収集する。
def collect( pool, start, stop=[], node=nil, checked=[] )
  node ||= Node.new(start.to_s)
  return node unless pool.classes_r.key? start
  pool.classes_r[start].each {|caller|
    next if checked.find{|i| i==caller }
    next if stop && stop.find{|i| caller =~ i }
    checked.push start.to_s
    node << collect( pool, caller, stop, Node.new( caller ), checked ) 
    checked.pop
  }
  return node
end

# クラス情報を収集
pool = ClassPool.new
JavaClass::Utils.each_class( *ARGV[0].split(";") ){|jc|
  pool << jc
}
# 呼び出し元をツリー表示
puts collect(pool,  ARGV[1]).to_s

なお、「JavaClass」のGemはGemcutterにアップロードしたので、そちらから取得できます。同名のGemがすでにあったので「unageanu-javaclass」として登録しています。

$ gem install unageanu-javaclass

「require」するときは「require 'javaclass'」です。

require 'javaclass'

JSON-RPCで転送するJavaオブジェクトをさくっと作成するユーティリティ の続き

昨日作成したJSON-RPCで転送するJavaオブジェクトをさくっと作成するユーティリティですが、RubyJSONへのシリアライズだけじゃなく、JSONRubyへのデシリアライズもサポートしてればさらに便利じゃね?と思い立って実装してみた。API呼び出しの結果として返されたハッシュが定義済みのJavaクラスであればそのインスタンスに変換して返します。

require 'rubygems'
require 'uri'
require 'httpclient'
require 'json/lexer'
require 'json/objects'

module JsonRpc

  # クライアント
  class Client
    def initialize( name, host="http://localhost:8080", proxy=ENV["http_proxy"] )
      @client = HTTPClient.new( proxy, "JsonClientLib")
      @client.set_cookie_store("cookie.dat")
      @name = name
      @host = host
      @id = 0
    end
    def method_missing( name, *args )
      body = "{ 'id':#{@id+=1},method:#{(@name+"."+name.to_s).to_json},params:#{args.to_json}}"
      result = @client.post(@host, body )
      json = JSON::Lexer.new(result.content).nextvalue
      if json["error"]
        raise json["error"]["msg"]
      else
        JsonRpc.to_java_class(json["result"])
      end
    end
  end
  
  #JSON-RPCで送付するJavaクラスを定義する。
  #java_class:: 対応するJavaクラスのFQCN 
  #attrs:: Javaクラスの属性一覧
  #return:: Javaクラス
  def self.define( java_class, *attrs )
    cl = Class.new(JsonRpc::JavaClass) {|klass|
      @java_class = java_class
      @attrs = attrs
      def self.java_class ; @java_class; end
      def self.attrs; @attrs; end
      def initialize( *args )
        attrs = self.class.attrs
        args.each_index {|i|
          break if i >= attrs.length
          instance_variable_set("@" + attrs[i].to_s, args[i])
        }
      end
      attrs.each {|a| attr a,  true}
    }
    @@registory[java_class] = cl
    return cl
  end
  # 定義したJavaClass
  @@registory = {}
  
  # ハッシュをJavaClassにデシリアライズする。
  def self.to_java_class( obj )
    if obj.kind_of?( Hash )
      hash = obj.inject({}){|r,i| r[i[0]] = to_java_class(i[1]); r }
      if ( obj["javaClass"] && @@registory.key?(obj["javaClass"]) )
        java_class = @@registory[obj["javaClass"]]
        args = java_class.attrs.map {|a| hash[to_jar_attr_name( a )] }
        return java_class.new(*args)
      else
        return hash
      end
    elsif obj.kind_of?( Array )
      return obj.map {|i| to_java_class( i ) }
    else
      return obj
    end
  end
  def self.to_jar_attr_name( name )
    tmp = name.to_s.split(/_/)
    return ([tmp[0]] + tmp[1..-1].map{|i| i.capitalize}).join
  end
  
  # Javaクラスの基底クラス
  class JavaClass
    def to_hash
      h = {"javaClass"=>self.class.java_class}
      attrs = self.class.attrs
      attrs.each {|attr|
        java_attr_name = JsonRpc.to_jar_attr_name( attr )
        h[java_attr_name] = instance_variable_get("@" + attr.to_s)
      }
      return h
    end
    def to_json
      to_hash.to_json
    end
  end
end

利用例は以下。

# Kittenオブジェクトを定義
Kitten = JsonRpc::define( 
  "test.Kitten",
  :id, :name, :age
)
# クライアント作成
client = JsonRpc::Client.new( "dao", 
  "http://localhost:8888/json" )
# API呼び出し。
p client.put( Kitten.new( nil, "mii", 1 ))

実行結果です。

#<Kitten:0x7fd308f4 @name="mii", @age=1, @id={"kind"=>"Kitten", "javaClass"=>"com.google.appengine.api.datastore.Key", "id"=>4}>