コピペコードで快適生活

明日使えるソースを自分のために

シェルでヒアドキュメントを使う

そういればやり方知らなかったのでメモ。

# これで標準出力できる。
cat << EOS
hoge
fuga
piyo
EOS

# ヒアドキュメント内で変数展開できる。
# 標準出力になるので変数に代入したいときはバッククォート使う。
params=`cat << EOS
{
    "login_id": "${LOGIN_ID}",
    "password": "${PASSWORD}"
}
EOS
`
res=`curl -X POST \
-H "Content-Type: application/json; charset=utf-8" \
-H "Authorization: ${ACCESS_TOKEN}" \
-d "${params}" "${API_HOST}/api/v1/users/auth"`

Javascriptのオブジェクト指向について

Javascript書くときに雰囲気でオブジェクト指向してたので復習。

function構文使う

// 関数オブジェクトはnew演算子でインスタンスを作ることができる。
// インスタンスは、this.xxxで定義したプロパティにアクセスできる。
const Human = function(name){
  this.name = name;
  this.say = function() {
    return "My name is " + this.name;
  }
};
const human = new Human('taro');
human.say(); // -> My name is taro

// 関数オブジェクトのprototype代入したオブジェクトのプロパティは、
// 生成したインスタンス同士で"共有"されて使われる。
const prototypeObject = {
    type: 'Human',
    toString: function() {
        return this.type + ': ' + this.name;
    }
}
Human.prototype = prototypeObject;
const jiro = new Human('jiro');
jiro.toString(); // -> Human: jiro
const hanako = new Human('hanako');
hanako.toString(); // -> Human: hanako

// 共有されているので書き換えると、すべてのインスタンスに影響がある。
Human.prototype.type = 'Homo sapiens';
jiro.toString(); // -> "Homo sapiens: jiro"
hanako.toString(); // -> "Homo sapiens: hanako"

// prototypeを使ってErrorオブジェクトを継承をする
const MyError = function(msg) {
    this.message = msg || 'Exception occured';
    this.name = 'MyError';
};
MyError.prototype = new Error();
e = new MyError();
e.toString(); // -> "MyError: Exception occured"

class構文使う

classはプロトタイプベース継承の糖衣構文。ECMAScript 2015 で導入。
今まで出来ていたことを、class構文を使っても書けるようになりましたよ的なもの。

// class構文を使って関数オブジェクトを定義。
// インスタンスの作成/プロパティへのアクセスは同じ。
class HumanKlass {
  constructor(name) {
    this.name = name;
  }
  say() {
     return "My name is " + this.name;
  }
}
tom = new HumanKlass('tom');
tom.say(); // -> My name is tom

// extendsを構文を使ってErrorオブジェクトを継承
class MyErrorKlass extends Error {
  constructor(msg) {
    super();
    this.message = msg || 'Exception occured';
    this.name = 'MyError';
  }
}
ek = new MyErrorKlass();
ek.toString(); // -> "MyError: Exception occured"
MyErrorKlass.prototype // -> Errorのインスタンスが入っている。

db:migrateで巨大なテーブルへadd_column+default値設定をする

Rails + PostgreSQL環境での話。
数千万行あるような巨大なテーブルに対して、add_column+default設定をまとめて設定すると、サービスを止めてしまうほどに長時間テーブルロックかかってしまう。AccessExclusiveLockなのでSELECTも通らない。
原因は、ALTER_TABLEのテーブルロックかかった中で、カラム追加+デフォルト設定に加えて全レコードに対してUPDATE(デフォルト値で上書き)が走るため。

こんな書き方すると発生する。

class AddXxxIdToBigRecords < ActiveRecord::Migration
  def change
    add_column :big_records, :xxx_id, :integer, null: false, default: 0
  end
end

対応方法としては下記となる。
ただ、テーブルサイズがでかいので、1行ごとUPDATEするのに相当の時間がかかる。

class AddXxxIdToBigRecords < ActiveRecord::Migration
  # まずこれを設定しないと、upメソッド内全部にトランザクションかかってしまう。
  disable_ddl_transaction!

  def up
    # 最初にカラムを足す。AccessExclusiveLockかかるけど一瞬で終わる。
    # (MySQLと違って一瞬で終わる。)
    add_column :big_records, :xxx_id, :integer

    # 次にデフォルト値を設定する。AccessExclusiveLockかかるけど一瞬で終わる。
    # カラムの値は更新されない。
    change_column :big_records, :xxx_id, :integer, default: true

    # 全レコードに対してUPDATEを実行する
    update_all_with_default_value

    # NOT NULLのフタをする
    change_column :big_records, :xxx_id, :integer, default: true, null: false
  end

  def down
    remove_column :big_records, :xxx_id
  end

  #
  # 全レコードにたいして一つずつUPDATEをかけていく。
  # Rails5以上であれば、
  # in_batchedとupdate_allで複数行をまとめて更新かけたほうがいいかも。
  #
  def update_all_with_default_value
    total = BigRecord.count
    return true if total == 0

    start_f = Time.zone.now.to_f
    BigRecord.find_each.with_index do |record, i|
      n = i + 1
      record.update_columns(xxx_id: 0)
      if n % 1000 == 0 || n == total
        STDOUT.puts "-- update_all_with_default_value (#{i+1} / #{total})"
      end
    end
    end_f = Time.zone.now.to_f

    STDOUT.puts "  -> #{end_f - start_f}s"
    return true
  end
end

Grape+Rspec環境でAPIをテストする

既存RailsアプリにRspecを入れる - コピペコードで快適生活
の続き。

まず、JSONのテストを簡単にするgemを入れる。

gem 'json_expressions'

テストコードはこんな感じで書ける。

require 'rails_helper'
require 'json_expressions/rspec'

describe "GET api/orders/:order_ids" do
  context '存在しないIDを指定するとき' do
    it '空の配列が返る' do
      order_ids = "aaaa,bbbb"
      get "/api/v1/orders/#{order_ids}"
      pattern = {
        orders: [],
        total_count: 0
      }
      expect(response.status).to eq 200
      expect(response.body).to match_json_expression(pattern)
    end
  end
end

describe 'PATCH api/orders/:order_id/cancel' do
  context '存在しないIDを指定するとき' do
    it '204が返る' do
      order_id = "aaaa"
      params = {xxx: 2}
      patch "/api/v1/orders/#{order_id}/cancel", params
      expect(response.status).to eq 204
    end
  end
end

Nodejsでのバイナリの取り扱い

よくわかっていなかったので整理した。

Bufferオブジェクト

Node.jsではバイナリデータはBufferオブジェクトで扱う。

ArrayBuffer

実際のメモリに格納されたバイナリデータを配列で表現したもの。読み込み専用。

Uint8Array

バイナリデータを8bitごとに区切って配列にしたもの。1つの要素には0~255の値が入る。変更可能。

// 128バイトのバッファを作成
// 初期化はされない
let buf = new Buffer(128);
console.log('定義だけ', buf);

// バッファに0を書き込む
buf.fill(0);
console.log('0で初期化', buf);

// 10バイト目から20バイト目まで2を書き込む
// (先頭は0と数える)
buf.fill(2, 10, 30);
console.log('2バイトで一部初期化', buf);

// 文字列をバイナリ化
buf = Buffer.from('テスト文字列', 'utf8');
console.log('文字列バイナリ', buf);
console.log('文字列のバイト数', buf.length);
console.log('元の文字列を出力', buf.toString('utf8'));

// bufferをArrayBufferにする
function toArrayBuffer(buffer) {
  var ab = new ArrayBuffer(buffer.length);
  var view = new Uint8Array(ab);
  for (var i = 0; i < buffer.length; ++i) {
    view[i] = buffer[i];
  }
  return ab;
}
buf = Buffer.from('TEST', 'utf8');
let arrayBuf = toArrayBuffer(buf);
console.log('ArrayBuffer', arrayBuf);

// arrayBufferからBufferを作る
buf = Buffer.from(arrayBuf);
console.log('arrayBufから作成したBuffer', buf);

// ファイルを読み込む
let fs = require('fs');
buf = fs.readFileSync('./buf.js');
console.log('ファイルのバイナリデータ', buf);

// axiosでバイナリデータを取得するときは
// responseTypeに arraybuffer を指定する
// そうすることで、arrayBufferオブジェクトとして受け取れる
// let client = axios.create({
//   'responseType': 'arraybuffer',
// });

// Bufferオブジェクトかどうかのチェック
console.log('Bufferかどうかチェック', Buffer.isBuffer(buf));

補足

html5のJSにはBufferはない。Blobで扱う。
https://www.sejuku.net/blog/67735#Blob-2

ArrayBufferをBlobに変換
http://var.blog.jp/archives/62330155.html

AWS NLBとコネクションプールについて

NLBの後ろに1台足してもそっちに全然クエリが流れてこなかった。
間違っているかもしれないけど、多分こういうことじゃないかな。

コネクションプールについて

接続確立は負荷を伴うので、一度確立した接続を維持して、その中でクエリ発行することで負荷を軽減する仕組み。

クライアントの実装によるが

・プール数上限設定があってそれ以上のクエリ発行をしようとすると待つ。
・接続断が発生したあと、再度クエリを発行すると再接続確立を試みる。
・一定時間使われていないコネクションがあると閉じる(ガベージコレクタに回収させたり)
・なので、クエリ発行が休みなく正常に続いているコネクションはずっと開いたまま。

AWS NLB

NLBは単にバランシングしているだけで、実際にはその後ろにあるサーバとコネクションが確立されている。
なので、
・クエリ発行が休みなく正常に続いているコネクションはずっと同じサーバとつながったまま。
・NLBの後ろに1台足しても、クエリ発行が休みなく続く限り、新しいサーバには通信はいかない。
・クライアント側を再起動してコネクションを再接続すると、新しいサーバにも通信がいくようになる。