読者です 読者をやめる 読者になる 読者になる

odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

hatena blogに移行しました。

hatena dialy → hatena blog

重い腰あげました。

コンバート機能よくできてました。

 

android4アプリを6(marshmallow)対応に

いろいろ調べたことのメモ書きです。

snackbar等のandroid5で導入された部品を使うにはいろいろ最新化が必要

snackbar等のandroid5で導入された使うには
com.android.support:designの導入が必要。

関連していろいろ最新化しないといけない。

play-serviceを最新化

com.google.android.gms:play-serviceを最新にしたら
Unable to execute dex: method ID not in [0, 0xffff]: 65536
とでたのでググったら下記のサイトが見つかったけど、これに従っても解決できずでした。

この場合は下記のサイトが正解。

app compat対応

app compatの対応が必要なのでandroid support library v7対応が必要。

v7はv4を拡張してるっぽいので。あまり詳しい文献は見つけられずだけどv4(1.6)サポートは必要ないだろうから気にしない。
といっても一部はv4のままのクラスを使うっぽい。両方使っていくスタイルっぽい。

app compat関連で修正した箇所は下記

  • FragmentActivity → AppCompatActivityにした。
    • ActionBarActivityへの移行と書いてあるがこれも古くて今はAppCompatActivityを使う。
  • ThemeをTheme.AppCompatにした。
  • ActionBarDrawerToggle を android.support.v7.app のものに変える
  • ロゴがでない。

actionBar.setLogo(R.drawable.ic_launcher)
をやめて
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setIcon(R.drawable.ic_launcher);
を呼ぶ。

最後に

時代が変わっていくのついてくの大変。

androidアプリをビルドしてバージョン別フォルダでサーバーへアップロードするスクリプト

aaptコマンドはパスを通す必要あり。
[androidSDKのパス]/build-tools/[version]/aapt

# ビルド
./gradlew assembleStaging
./gradlew assembleRelease

# バージョン名取得
versionName=`aapt dump badging app/build/outputs/apk/app-*-staging.apk | sed -n 's/.*versionName='\''\(.*\)'\'' .*/\1/gp'`

# リモートにバージョンフォルダ作成。2回目ディレクトリ作成済みでエラーになるけどきにしない。-pは怖いのでやめた。
ssh 転送先ホスト mkdir 設置先パス/$versionName/

# リモートにアップロード
scp -r app/build/outputs/apk/app-*-staging.apk 転送先ホスト:設置先パス/$versionName/
scp -r app/build/outputs/apk/app-*-release.apk 転送先ホスト:設置先パス/$versionName/

バージョンチェック参考

http://qiita.com/nofrmm/items/9bdc0d7af2c018b49593

sed memo

シングルクォートのエスケープは'\''
http://d.hatena.ne.jp/tanakaBox/20070729/1185709167

最長一致になるのでスペースいれたりでマッチしないように工夫必要
http://techtipshoge.blogspot.jp/2011/10/sed.html

sedコマンド
http://bi.biopapyrus.net/linux/sed.html
https://hydrocul.github.io/wiki/commands/sed.html

実行環境

android studio 1.4.1
buildToolsVersion '23.0.1'
com.android.tools.build:gradle:1.3.1

android google analyticsでbuild flavor対応。

ラッキングIDである設定xmlファイル中のga_trackingIdの値をbuild flavorごとに切り替えたい。
当たり前にできると思ったが標準でできなそうで結局hack地味たことした。
android開発って罠多すぎるんだけど大丈夫かねgoogleさん。。

対処方法

            Tracker t = analytics.newTracker(R.xml.app_tracker);
            t.set("&tid", getString(R.string.ga_trackingId));

試みてだめだったこと

  • xml中に@string/ga_trackingIdとリソース参照しようとしても参照されずに@string/ga_trackingIdという文字列で認識される。
  • build flavorごとのフォルダにxmlの差分置けばマージされるだろと思ったが、マージされずにbuild flavorのxmlのみが適用されるだけでした。

便利メモ

xml
verbose
true
っていれると詳細なログでるのでわかりやすい。

実行環境

google analytics v4
android 4.3
android studio 0.8.9

sqlでlastIndexOf, rindexを行う


下記でできた。

LENGTH(col1) - INSTR(REVERSE(col1), '#')

指定文字列の前か後の文字列を抜き出す場合

対象文字列 : hoge#hoge2#end

endを抜く (最後の#より後を抜くSQL) (簡単)

SELECT
  SUBSTRING_INDEX(col1, '#', -1)
FROM
  (SELECT 'hoge#hoge2#end' col1) t

hoge#hoge2を抜く (最後の#より後を抜くSQL) (めんどいが下記でできる)

SELECT
  SUBSTRING(col1, 1, LENGTH(col1) - INSTR(REVERSE(col1), '#'))
FROM
  (SELECT 'hoge#hoge2#end' col1) t

実行環境

mysql4.1

rubyでmemcachedのキーの一覧を表示する。また特定データを削除する等。

なんで作ろうと思ったか

C#memcachedセッションライブラリがバグってて無期限データ入れやがったー。
そのまま放置して130万件にもなりやがった。
くそー 消したい。flush_allすると全データ消えて既存ユーザーが強制timeout扱いなるからそれは避けたい。
なので無期限データのみ消したいんだー 絶対ー!!

アイテム一覧表示 実行結果

$ util.print_items

stats
{"pid"=>"16348",
 "uptime"=>"585047",
 "time"=>"1361858174",
 "version"=>"1.4.7",
 "libevent"=>"1.4.13-stable",
 "pointer_size"=>"64",
 "rusage_user"=>"1.210815",
 "rusage_system"=>"5.016237",
 "curr_connections"=>"24",
 "total_connections"=>"1554",
 "connection_structures"=>"34",
 "cmd_get"=>"16466",
 "cmd_set"=>"25559",
 "cmd_flush"=>"0",
 "get_hits"=>"16424",
 "get_misses"=>"42",
 "delete_misses"=>"0",
 "delete_hits"=>"4",
 "incr_misses"=>"0",
 "incr_hits"=>"0",
 "decr_misses"=>"0",
 "decr_hits"=>"0",
 "cas_misses"=>"0",
 "cas_hits"=>"0",
 "cas_badval"=>"0",
 "auth_cmds"=>"0",
 "auth_errors"=>"0",
 "bytes_read"=>"783051598",
 "bytes_written"=>"396680001",
 "limit_maxbytes"=>"67108864",
 "accepting_conns"=>"1",
 "listen_disabled_num"=>"0",
 "threads"=>"4",
 "conn_yields"=>"0",
 "bytes"=>"111905",
 "curr_items"=>"7",
 "total_items"=>"25559",
 "evictions"=>"0",
 "reclaimed"=>"27"}
stats items
{5=>
  {"number"=>"2",
   "age"=>"583060",
   "evicted"=>"0",
   "evicted_nonzero"=>"0",
   "evicted_time"=>"0",
   "outofmemory"=>"0",
   "tailrepairs"=>"0",
   "reclaimed"=>"11"},
 6=>
  {"number"=>"1",
   "age"=>"582577",
   "evicted"=>"0",
   "evicted_nonzero"=>"0",
   "evicted_time"=>"0",
   "outofmemory"=>"0",
   "tailrepairs"=>"0",
   "reclaimed"=>"14"},
 7=>
  {"number"=>"1",
   "age"=>"502636",
   "evicted"=>"0",
   "evicted_nonzero"=>"0",
   "evicted_time"=>"0",
   "outofmemory"=>"0",
   "tailrepairs"=>"0",
   "reclaimed"=>"0"},
 28=>
  {"number"=>"3",
   "age"=>"505667",
   "evicted"=>"0",
   "evicted_nonzero"=>"0",
   "evicted_time"=>"0",
   "outofmemory"=>"0",
   "tailrepairs"=>"0",
   "reclaimed"=>"1"}}
server_start_time:2013-02-19 20:25:27 +0900
item_number:5 key:foo_development:_session_id:f0df468194448c505e26b99143e5d65c bytes:107 time:2013-02-26 20:55:22 +0900
item_number:5 key:foo_development:_session_id:f7450243f80ca59fbb9ffc18049fedc0 bytes:107 time:2013-02-26 20:23:07 +0900
item_number:6 key:foo_development:_session_id:e2674611e09369f8b5bc62ba95c59b92 bytes:165 time:2013-02-26 20:15:04 +0900
item_number:7 key:hoge_e15atqhotfpm4sbnbm3dwa3g bytes:269 time:2013-02-26 16:02:41 +0900
item_number:28 key:hoge_qmeysujdltvz22s2ahtpmrye bytes:36546 time:2013-02-27 14:55:44 +0900
item_number:28 key:hoge_3nsu2trne4j0vyrfqg202lqe bytes:37387 time:2013-02-26 21:04:37 +0900
item_number:28 key:hoge_lwbr2fcdzx21wgbroczjtghq bytes:36536 time:2013-02-26 16:53:29 +0900
2013-02-26 14:54:50 +0900 infinity_count:0 normal_count:7 expire_count:0

ソース

# coding: utf-8

STDOUT.sync = true

require 'rubygems'
require 'pp'

gem 'memcache_do', '>=0.1.1'
gem 'dalli', '>=2.6.2'

require 'memcache_do'
require 'dalli'

class MemcacheStats
  def initialize(host='localhost', port='11211')
    @host = host
    @port = port
    @dc = Dalli::Client.new("#{host}:#{port}")
  end

  def list
    stats_items_hash.each do |no, item|
      stats_cachedump(no).lines do |line|
        next if line.chomp == 'END'

        hoge, key, bytes, second = line.match(/^ITEM ([^ ]*) \[(\d*) b; (\d*)/).to_a
        second = second.to_i
        time = Time.at(second)

        data = {:item_number => no, :key => key, :bytes => bytes, :time => time}
        yield(data)
      end
    end
  end

  def exec(cmd)
    MemcacheDo.exec(cmd, @host, @port)
  end

  def stats
    exec 'stats'
  end

  def stats_hash
    raw = stats
    hash = {}
    raw.lines do |line|
      # p line
      line.chomp!
      res = line.match(/^STAT (\w+) ([^ ]+)/)
      if res
        hash[res[1]] = res[2]
      end
    end

    hash
  end

  def stats_items
    exec 'stats items'
  end

  def stats_cachedump(item_number)
    exec "stats cachedump #{item_number} 0"
  end

  def stats_items_hash
    raw = stats_items
    hash = {}
    raw.lines do |line|
    # p line
      line.chomp!
      res = line.match(/^STAT items:(\d+):(\w+) ([^ ]+)/)
      if res
        key_num = res[1].to_i
        hash[key_num] = {} unless hash.has_key? key_num
        hash[key_num][res[2]] = res[3]
      end
    end

    hash
  end

  def delete(key)
    @dc.delete key
  end

  def dalli(key)
    @dc
  end
end

if $0 == __FILE__
  class MemcacheUtil
    def initialize(host='localhost')
      @m = MemcacheStats.new(host)
    end

    # アイテム一覧表示
    def print_items
      m = @m

      infinity_count = 0
      normal_count = 0
      expire_count = 0

      # stats取得
      # puts m.stats
      stats = m.stats_hash
      puts "stats"
      pp stats

      # stats_items取得
      #puts m.stats_items
      stats_items = m.stats_items_hash
      puts "stats items"
      pp stats_items

      # サーバー開始時間を計算
      server_start_time = Time.at(stats["time"].to_i - stats["uptime"].to_i)
      puts "server_start_time:#{server_start_time}"

      # データ一覧取得
      m.list do |data|
        item_number = data[:item_number]
        key = data[:key]
        bytes = data[:bytes]
        time = data[:time]

        puts "item_number:#{item_number} key:#{key} bytes:#{bytes} time:#{time}"

        if time == server_start_time
          # 無期限キャッシュの有効期限はサーバー開始時間と一緒
          infinity_count += 1
        elsif time < Time.now
          # 期限切れデータ。どうもstats cachedumpに期限切れてるのがでてくる
          expire_count += 1
        else
          normal_count += 1
        end

        if key == "hogehoge"
          # 簡易データ表示(先頭にヘッダーぽいのついてるのを除去してないです)
          p m.exec("get #{key}")
          # rubyのデータなら下記のようにm.dalliでDalliClientのインスタンス返すのでご自由にどぞ。
          #m.dalli.get(key)
        end
      end

      puts "#{Time.now} infinity_count:#{infinity_count} normal_count:#{normal_count} expire_count:#{expire_count}"
    end

    # 無期限データ削除&有効期限切れデータ削除
    def delete_infinity_and_expire_data
      m = @m

      puts "#{Time.now} start process"
      loop do
        infinity_count = 0
        normal_count = 0
        expire_count = 0

        # stats取得
        # puts m.stats
        stats = m.stats_hash
        puts "stats"
        pp stats

        # stats_items取得
        #puts m.stats_items
        stats_items = m.stats_items_hash
        puts "stats items"
        pp stats_items

        # サーバー開始時間を計算
        server_start_time = Time.at(stats["time"].to_i - stats["uptime"].to_i)
        puts "server_start_time:#{server_start_time}"

        # データ一覧取得
        m.list do |data|
          item_number = data[:item_number]
          key = data[:key]
          bytes = data[:bytes]
          time = data[:time]

          puts "item_number:#{item_number} key:#{key} bytes:#{bytes} time:#{time}"

          if (time == server_start_time)
          # 無期限キャッシュの有効期限はサーバー開始時間と一緒
            infinity_count += 1
            m.delete key
          elsif time < Time.now
            # 期限切れデータ。どうもstats cachedumpに期限切れてるのがでてくる
            expire_count += 1
            m.delete key
          else
            normal_count += 1
          end
        end

        puts "#{Time.now} infinity_count:#{infinity_count} normal_count:#{normal_count} expire_count:#{expire_count}"
        # 削除対象が無くなるまで繰り返す。
        # stats cachedumpは1MBしか返さない制限あるのでゴミデータは削除しないと消したいデータが出てこない場合がある。なので期限切れデータを消した後もループで再実行している。
        if infinity_count == 0 && expire_count == 0
          break
        end
      end
      puts "#{Time.now} end process"
    end
  end

  util = MemcacheUtil.new('192.168.xxx.xxx')
  util.print_items

  # コメント外して使ってください。
  # 無期限データ削除&有効期限切れデータ削除
  #util.delete_infinity_and_expire_data
end
実行結果

delete_infinity_and_expire_dataを実行して
5,6時間経過で約130万件が約300件に!
アクティブセッション少なっ!

意地で作ったけど、memcached再起動すれば簡単かつ安全なのが泣ける。
そもそもmemcaechedのデータは消えていいようなキャッシュをいれる目的なんだから良いinterfaceもutilもないんだろうなー。
もしかしたらmemcached互換サーバーな永続性あるやつのほうがutil優れてたりして。

注意点

stats cachedumpだとキー一覧等のデータが1MBまでしか返ってこないとか。もしアクティブなデータがいっぱい入っている場合は無理そう。
また有効期限切れデータも返ってくるので、今回は有効期限切れデータがあった場合は削除するような処理にした。

参考
http://blog.elijaa.org/index.php?post/2010/12/24/Understanding-Memcached-stats-cachedump-command
http://lzone.de/dump+memcache+keys

そのうちコマンドstats cachedumpが消えるかもとかもいってるみたい。まー微妙な仕様なので新しいのができると期待。

rubyメモ書き

  • memcache_doのexecでdeleteはできなかった。ソース見たらレスポンスにENDが返ってくるデータ用らしく恐らくsetも無理。
  • dalliのgetを使うとrubyのデシリアライズしようとするのでruby専用になっている、恐らくdalliのsetもだめだろう。プレーンテキストをやりとりするserializer作ればいいのかも。

実行環境

ruby 1.9.3p374
memcached version 1.4.7

rails3.1で大きいデータ(csv等)をストリーミングで出力する方法

はじめに

管理画面等で膨大なデータをcsvで落としたい時等に
そのままDBから文字列を作って返すと反応がないままタイムアウトになると思います。(なんせ全部メモリに落とすのだから)


こういう場合の回避方法(2パターン)

  1. db等にリクエストをキューイングしておきバッチで処理してzipに圧縮しておく。
    • サーバーにはお行儀がいいお作法。ただし工数がかかる。
  2. 1000件等の細かい単位にしてsqlを発行してストリーミングで出力する。常にブラウザにデータが流れるためタイムアウトしない。
    • 工数少なめ。ただしサーバーに通信しっぱなしになるのでサーバーの同時リクエスト数を食いつぶす。管理画面等の一部利用者のみなら問題ないでしょう。

今回2の方法をrails3.1で行う文献が見つからなかったので調べました。

サンプル

1秒おきにtest1,test2..test5のように出力されます。

class TestController < ApplicationController
  def index

    headers["Cache-Control"] ||= "no-cache"
    headers["Transfer-Encoding"] = "chunked"

    # 上記のheadersを書くかもしくは下記のrenderでもOK。headersのほうが余計な処理が入らないのでお勧めかな??
    #render :text=>"", :stream => true

    # DLにする場合はここのコメントを外せばOK.ブラウザで順次出力されるのを確認できるようにあえて外しておきます。
    # headers.merge!(
      # 'Content-Type' => "text/csv; charset=Shift_JIS",
      # 'Content-Disposition' => "attachment; filename=\"test.csv\""
    # )

    self.response_body = Rack::Chunked::Body.new(Enumerator.new do |y|
      5.times do |i|
        y << "test#{i} \n"
        sleep 1
      end
    end)

  end
end

大抵はDBから1000件ずつ等ちょこちょこ取得して出力するでしょう。その際はfind_eachを使うと便利です。
http://wiki.usagee.co.jp/ruby/rails/RailsGuides%E3%82%92%E3%82%86%E3%81%A3%E3%81%8F%E3%82%8A%E5%92%8C%E8%A8%B3%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F%E3%82%88/Active%20Record%20Query%20Interface#i803e697

注意点

webrickではストリーミングになりません。
unicornを使ってください。
また古いバージョンのunicornだと設定に下記を指定しないといけないそうです。(現在はデフォでこの設定になってる)

# unicorn.config.rb
listen 3000, :tcp_nopush => false

参考

rails3.1の新機能のhttpストリーミング部分のソースを読みました。(実際はrailsのbody部分の出力の流れまで追ってしまいました。。unicornのc部分まで行きそうな所で力尽きましたw)
actionpack-3.1.3/lib/action_controller/metal/streaming.rb

実行環境

ruby (1.9.2p290)
rails (3.1.3)
unicorn (4.1.1)