admin のすべての投稿

StringBufferとStringBuilderの違い

javaには文字列を可変的に扱うためのクラスとして StringBuffer 及び StringBuilder という2つのクラスが用意されています。
この2つのクラスは文字をバッファとして取り扱い、任意のタイミングで任意の文字列を詰め込むことができ、非常に使用頻度の高いクラスとなっています。
されとてこの2つのクラス同じような事できるのですが具体的に、どう異なるかご存知でしょうか?
本記事ではこの2つのクラスの差について調査します。

なお対象としては java8 の API を対象とします。

調査する

公式の java8 API を参照してみましょう。参考リンクについては本記事の末尾に記載しますので参照してください。

と、いきなり回答にたどり着いてしまうのですが StringBuilder のマニュアルを確認すると、以下のような記載があります。

文字の可変シーケンスです。このクラスは、StringBufferと互換性があるAPIを提供しますが、同期化は保証されません。このクラスは、文字列バッファが単一のスレッド(一般的なケース)により使用されていた場合のStringBufferの簡単な代替として使用されるよう設計されています。このクラスは、ほとんどの実装で高速に実行されるので、可能な場合は、StringBufferよりも優先して使用することをお薦めします。

つまり StringBuilder はスレッドセーフではないシンプルなケースに適用でき、それ以外の場合は StringBuffer を使用すると良い、ということです。

シンプルに使い所をまとめると

StringBuffer

スレッドセーフであるので複数スレッドから参照されるような場合に適切
スレッド間の排他制御を実装している。

StringBuilder

シングルスレッドで排他処理の必要がないシンプルな処理の場合に適切
使用できる場合はこちらのクラスを使用したほうがStringBuilderよりも高速に処理が行える

というところですね。

実験してみる

実装レベルでの検証のために下記のようなサンプルコードを用意しました。
ちなみに groovy コードとなりますのでご留意ください。

同じ文字列バッファを共有したスレッドを10個作成してそれぞれのスレッドから100回書き込みを行います。
最終的にバッファの内容がどうなっているかを確認するシンプルなコードです。
これを文字列バッファをStringBufferとStringBuilderそれぞれに切り替えて結果を確認してみます。

import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class WriteThread implements Runnable {
  int num
  StringBuffer sb

  public WriteThread(int num, StringBuffer sb) {
    this.num = num
    this.sb = sb
  }

  @Override
  public void run() {
    for (int i=0; i<100; i++) {
      this.sb.append("I am thread ${num}.\n")
    }
  }
}

StringBuffer sharedBuilder = new StringBuffer()
ExecutorService executor = Executors.newFixedThreadPool(10)
for (int i=0; i<10; i++) {
  executor.submit(new WriteThread(i, sharedBuilder))
}
executor.shutdown()
executor.awaitTermination(10, TimeUnit.SECONDS)

println sharedBuilder.toString()

StringBuffer

結果の一部のみを抜粋しますが、きれいに文字列が出力できていることが確認できます。

...
I am thread 6.
I am thread 1.
I am thread 3.
I am thread 7.
I am thread 3.
I am thread 1.
I am thread 6.
...

StringBuilder

対して StringBuilder の方は下記のように破損している箇所が見られます。
(※見れられないこともあります)
書き込みが衝突してしまったということです。

...
I am thread 7.
I am thread 9.
ead 8.
thread 9.
ad 1.
ad 0.
I am thread 5.
...

まとめ

とりあえずマルチスレッドなケースが求められる場面では StringBuffer を使いましょう。
にしても名前がややこしいな・・・どっちがどっちだかわからなくなる。

参考

StringBuilder
StringBuffer


GASでスプレッドシートのセルの書式を文字列に設定する

GAS 便利ですよね。
スクリプトでサクッとかける上にもともと拡張性の高いスプレッドシートなのでちょっとした実装でも良いものが作れます。
スプレッドシートなんかのインタフェースはエンジニア以外の人にも馴染み深いのでコストをかけて画面系を実装するよりも適している場合がよくあります。

なのですがGASを利用する際に正しくAPIやその動作を理解していないと、非常に重い動作になってしまうことがあります。
テストしてみたらループ処理なんかでずーっと時間がかかってとまらないスクリプトなんてこともあります。

今回GASでスプレッドシート上のセルの書式を文字列にできないかというところを試行錯誤にして改善してみたので、一つのパフォーマンス改善のアプローチとして参考にしてもらえればと思います。

やりたいこと

具体的にやりたいことはシンプルで、セルの値の先頭に + を入力したいということです。しかしそのままやろうとするとこれがうまくいかない。
デフォルトの状態ではスプレッドシートは先頭に+を認識すると値を数式であると認識して、展開しようとします。
実際には文字列ですので式の値が不正でありエラーとなります。

エクセルですと書式設定で テキスト なんていう書式があるようですが、現在のスプレッドシートの書式にはこのケースを許容してくれる書式は無いようでした。

ではどうするかというと先頭にシングルクォート()を打ち込みます。
こうすることでセルに入力された値をテキストだと認識してくれます。

問題はこれを大量のセルに対して実施したいということ。
ちまちま一つ一つのセルを手作業で編集していたのでは気が遠くなります。

GASを使って解決してみた・・・

始めに単純な思いつきで実装してみます。
レンジに関してはあまり深く考えないで良いです。

var valueRange = sheet.getRange('A1:A'); // A列に存在するデータに対して実施
for (var i=0; i<valueRange.getNumRows(); i++) {
  var cell = valueRange.getCell(i, 0);
  var value = cell.getValue();
  if (value == "") {
    continue;
  }
  cell.setValue("'" + values);
}

余裕のある人は一度実装してみてください。
動かしてみるとわかるのですがこの実装、めちゃめちゃ遅いです。

何故かと言うと getValue, setValue というAPIをforループ内で毎回呼び出しているからなんですね。

スプレッドシートは基本的にサーバ側で反映されたデータがブラウザに反映されます。
ですので上記のスクリプトでデータを書き換えている箇所は、ブラウザ上に表示されているデータを書き換えているのではなく実際にはGoogleのサーバ上にあるスプレッドシートのデータを編集しているような設計になっています。
で、ブラウザは逐次その反映されたデータを更新して表示しているということになります。
なのでループ毎に getValue, setValue を呼び出すことがものすごい遅延につながります。

ここまでの話で想像できるかと思いますが、GASの動作を高速化する際にまず考えることは GASのAPI呼び出しをなるべく少なくする ということです。
スプレッドシートを取り上げますがその他のGServiceでも同じことだと思います。
まーRDBでもなんでもバッチ処理にする。ということは基本ですね。。

この点について留意して最適化したコードを実装してみましょう。

処理を最適化する

下記が最適化されたコードになります。

var valueRange = sheet.getRange('A1:A');
var values = valueRange.getValues();
for (var i=0; i<valueRange.getNumRows(); i++) {
  if (values[i][0] == "") {
    continue;
  }
  values[i][0] = "'" + values[i][0];
}
valueRange.setValues(values);

こちらも実行していただけるとわかるのですが、一瞬で実行が完了します。
見ての通り API呼び出しを getValues, setValues にすることでたったの二回に減らすことができています。

APIマニュアルを良く読んで自分がやりたいことが他の方法で実現できないか?とよく考えると意外と様々なAPIが存在しているのでぜひ確認してみてください。
今回はこの方法で目的を達成する事ができました。
それでは。


改めてphpのerror_reportingは心もとないなと感じた

久しぶりに他人が実装したphpコードを改修する機会があり、改めてphpのエラーレポートレベルに関して思うところがありました。

コードがあったほうが話がわかりやすいと思いますので用意しましょう。

はい。例えば、下記のようなコードが合ったとしましょう
fetchに関しては適当なレコードが連想配列で返却されるとイメージしてください。

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL);

class Car {
	public $engine;
	public $wheel;

	public static function apply($record) {
		$car = new Car();
		$car->engine = $record['engine'];
		$car->wheel = $record['wheel'];
		return $car;
	}
}

$carRecord = $pdo->fetch(PDO::FETCH_ASSOC);
$car = Car::apply($carRecord);

これは問題なく動きます。
ところが開発途中でRDBのテーブルの属性名がイケてないなと思い wheel という属性名を wheels と複数形にしたとします。

ソースコードに関しては変更するのを忘れてしまいました。
だとしても当然スクリプト言語ですから動きます。これは。そういう設計ですから。

Notice errorを表示する設定の場合は、下記のようなエラーを吐いてくれます。

Notice: Undefined index: wheel in hoge.php on line 12

これは文字通り連想配列に存在しないインデックスにアクセスしているというエラーなのですが、エラーレベルってNoticeなんですよね。
これもうアプリケーション動かないじゃないですか
しばらくコンパイラ言語で開発していると少々このギャップに戸惑ったという話。

さらに公式によると error_reporting の初期値は E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED なんですよね。
初期値にNoticeが含まれていないんですよね
これがまた学習レベルの浅いphpユーザがなんたるかと言われることにつながっているような気がしなくもないです。

開発中はできればNoticeエラーをonにすることで、Noticeエラーは積極的に潰していきましょう。

さらに別の側面から見るために下記のようなコードを用意しました。

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL);

class Car {
	public $engine;
	public $wheel;

	public static function apply($record) {
		$car = new Car();
		$car->engine = $record['engine'];
		$car->wheel = $record['wheel'];
		return $car;
	}
}

$car = new Car();
$car->status = "stop";

phpは未定義のメンバ変数を動的に追加できます。
つまり上記のコードはエラーでもなんでもなくphpでは正しいコードなのです。そういう設計ですから。

これもなかなかしんどいですよ。例えば意図的ではなくタイポなどが原因だったらバグですから。
こういった事象をUTなどでカバーするっていうのは、なんかちょっと無駄なような気がしますし、できたとしてもそれに追従するコストが結構かかります。
ようするに無駄なコストをかけざるをえないんじゃないかと思います。

コンパイラ言語などでコンパイル時に検出できるたぐいの不具合であれば、そちらを使うに越したことはないじゃないか。という結論。


ハレノヒムービー サービスリリースのお知らせ

この度 ハレノヒムービーという新しいウェブサービスをリリースしました。

コンセプトは 誰でも簡単にムービーが作成できる です。

fig1


主に結婚式や二次会、パーティなどで会場に流すような形式のムービーをターゲットとしています。

特に結婚式などでは式場に依頼すると非常に高額につくのでうんざり・・・
シンプルなものなら自分でやってしまいたい。なんていう方に最適です。

ゆくゆくは有償化も考えていますが、現在はベータ版ということで全て無料でご使用いただけます。

使い方はごくごくシンプルで

  1. ユーザ登録
  2. 素材を選択して作成依頼

だけで完了です。

またこのサービスのこだわったところは、動画の作成を行う前にプレビューが確認できることです。
素早く出来上がりを確認しながら何度でも手直しができます。
満足の行く設定が出来上がれば、動画の作成を依頼するだけです。

素材に制限はなく、自分の好きな写真などを選択していただけます。

fig2


現在テンプレートは1種類のみで、シンプルな結婚式や二次会の退場シーンなどを想定したエンドロールのみとなっております
今後テンプレートを増やしていきたいと思います。
出来上がった動画はマイページからいつでもダウンロードすることができます。

fig3


近々結婚式やパーティのご予定のある方、興味のある方は是非使用してみてください。


yumの設定ファイルを確認する

概要

yumのグローバル設定やリポジトリ設定ファイルなどの各種設定ファイルをおさらいします。対象バージョンとしてCentOS6系に存在するファイルを元に調査をしていきます。
なお本記事は参考サイトとして挙げているサイトの情報要約していますので正確な情報を求めている方は参考サイト(特に公式の情報)に目を通すことを推奨します。

まず/etc/に存在するyum関連のファイルを確認してみます

始めに実環境に存在する yum に関連する設定ファイルと思われるものを確認します。
簡単に /etc/ 配下を調べると下記のようなファイルやディレクトリを発見しました。

  • /etc/yum.conf ファイル
  • /etc/yum.repos.d/ ディレクトリ
  • /etc/yum/ ディレクトリ

本記事ではこれらについてそれぞれどういう役割を持っているのかをまとめることを目的とします。

/etc/yum.conf ファイルの設定

それではまず /etc/yum.conf ファイルを確認してみましょう。
調査環境では下記のようなファイルが定義されていました。ひとつひとつ見ていきます。

[main]
cachedir=/var/cache/yum/$basearch/$releasever
keepcache=0
debuglevel=2
logfile=/var/log/yum.log
exactarch=1
obsoletes=1
gpgcheck=1
plugins=1
installonly_limit=5
bugtracker_url=http://bugs.centos.org/set_project.php?project_id=19&ref=http://bugs.centos.org/bug_report_page.php?category=yum
distroverpkg=centos-release

cachedir

Yumがキャッシュとデータベースを格納するディレクトリへの絶対パス< $basearch と $releasever という変数を利用しています。Yum変数のリストを確認しましょう。
すると $basearch はシステムのベースアーキテクチャを参照できます。最近のマシンですと64ビットが多いと思いますので x86_64 という変数に解決されます。
また $releasever は Red Hat のリリースバージョンを参照できるとのことですね。同じファイルの distroverpkg=centos-release の値を元にバージョンを取得します。
ここでは centos-release パッケージを元にバージョンを取得しているということになります。
centos-release パッケージのバージョンを調べてみます。

yum info centos-release
...
Installed Packages
Name        : centos-release
Arch        : x86_64
Version     : 6
...

すると Version 6 という記述があるので、cachedirに関しては /var/cache/yum/x86_64/6 というディレクトリに解決されていることがわかりました。
ローカルキャッシュはこのディレクトリに保存されることになります。

keepcache

インストールに成功した後にヘッダーとパッケージのキャッシュを保持するかどうかをコントロールします。
デフォルトではキャッシュをしない設定になっています。これはパッケージの移り変わりが早いので常に最新の情報を参照してほしいということかなと感じます。
0 – キャッシュを保持しません。デフォルト設定です
1 – キャッシュを保持します

debuglevel

1から10までの整数値を取ります。文字通りdebugのレベルをコントロールします。0を設定した場合はデバッグ出力を無効にします。
こちらはログファイルへの出力ではなく、yumコマンド実行時の標準出力の内容が対象になります。

logfile

ログ出力を行うファイルへの絶対パスです。
出力される内容には日時とアクション(どのパッケージが インストール/削除/更新 された)という情報が書き込まれています。

exactarch

OSがサポートするアーキテクチャを考慮してパッケージの更新を行うかどうかをコントロールします。
アーキテクチャを入れない場合、例えばi386のパッケージを更新するためにi686パッケージをインストールするなどの挙動を許可することになります。
0 – 正しいアーキテクチャを考慮に入れません
1 – 正しいアーキテクチャを考慮に入れます。デフォルト設定です

obsoletes

更新実行時に obsoletes処理ロジックを有効にするか無効にするかをコントロールします。

obsoletes処理と言われても何のことだかピンとこないですよね。
例えば、あるパッケージAがもともとパッケージBに依存していたとしましょう。
パッケージAが更新されパッケージBに依存しなくなったとします。このときにパッケージBを obsoletes 扱いにするというような言い方をします。
するとパッケージAを更新する際に、パッケージBは obsoletes 扱いですよ、という記述があれば更新時に依存していたパッケージBを排除します。
というようなロジックを適用するかどうか、という設定項目になります。
0 – obsoletes処理ロジックを無効にします
1 – obsoletes処理ロジックを有効にします。デフォルト設定です

gpgcheck

GPG署名の確認をコントロールします。
GPG署名に関しては こちら のページに書いてあります。
GPGというツールを利用してパッケージに対して署名したり、パッケージの署名を検証することでパッケージが改ざんされていないことを確認することができます。
この項目は yum.conf に設定された項目を各リポジトリごとの .repo ファイルで設定をオーバーライドすることができます。
0 – 全リポジトリの全パッケージ上でのGPG署名確認を無効にします
1 – 全リポジトリの全パッケージ上でのGPG署名確認を有効にします。デフォルト設定です

plugins

Yumのプラグインについては こちら にまとまっています。
インストールされたプラグインは /etc/yum/pluginconf.d/ のディレクトリに設定ファイルを持ち、この項目で設定されたプラグインの有効/無効の設定を各種プラグインの設定ファイルでオーバーライドすることができます。
0 – Yumのプラグインを無効にします
1 – Yumのプラグインを有効にします。デフォルト設定です

installonly_limit

単一のパッケージに同時にインストール可能なバージョンの最大数を表す整数を設定します。
複数のバージョンをインストールすることがあるのか?と思いますが、カーネルパッケージなんかではそういうことがあるようで、2以下に設定してしまうことは推奨されていません。

bugtracker_url

こちらに関しては参考ページには記載はありませんでしたが、centosのディストリにてバグを管理しているページへのリンクを記載しているようです。

上記に見てきたようにこの [main] セクションを定義することでyumのグローバル設定を定義できます。
またファイル内にリポジトリ用のセクション [repository] を作成して各リポジトリの設定を定義することもできますが、これは推奨されません。
各リポジトリの設定は /etc/yum.repos.d/ ディレクトリ内に .repoファイル を作成することで定義することが推奨されます。
この方が各リポジトリと設定がどこに集約されているかということがファイルを見るだけで読み取れ、管理が容易に行えるからです。

/etc/yum.repo.d/*.repo ファイルの設定

こちらも設定ファイルを参考に一つ一つ確認していきましょう。
デフォルトでは下記のようなファイルが存在するようですが、一つ例に取ってみていきます。

CentOS-Base.repo
CentOS-Debuginfo.repo
CentOS-Media.repo
CentOS-Vault.repo
CentOS-fasttrack.repo

CentOS-Base.repo を見ていきましょう

[base]
name=CentOS-$releasever - Base
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6

#released updates
[updates]
name=CentOS-$releasever - Updates
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/centosplus/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6

#contrib - packages by Centos Users
[contrib]
name=CentOS-$releasever - Contrib
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=contrib&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/contrib/$basearch/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6

いろいろなセクション(=リポジトリ)を持っていることが見て取れます。
それぞれを見ていきます。

セクション名

まずセクション名称はidとして機能するため重複することのない文字列でなければなりません。おそらく任意に定義できるでしょう。

name

nameはリポジトリの名称で人間が読み取れる形式で記述します。

baseurl または mirrorlist

リポジトリのrepodataディレクトリがあるディレクトリへのURLです。
centosのディストリビューションでは baseurl か mirrorlist のどちらかがあれば問題ないようです。

試しに mirrorurl に記載のある変数を解決したページ http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=contrib&infra= を叩いてみると
以下のようなリポジトリのミラーリストが返却されました。

http://mirror.fairway.ne.jp/centos/6.9/contrib/x86_64/
http://ftp.iij.ad.jp/pub/linux/centos/6.9/contrib/x86_64/
http://www.ftp.ne.jp/Linux/packages/CentOS/6.9/contrib/x86_64/
http://ftp.riken.jp/Linux/centos/6.9/contrib/x86_64/
http://ftp.jaist.ac.jp/pub/Linux/CentOS/6.9/contrib/x86_64/
http://ftp.nara.wide.ad.jp/pub/Linux/centos/6.9/contrib/x86_64/
http://ftp.yz.yamagata-u.ac.jp/pub/linux/centos/6.9/contrib/x86_64/
http://mirror.vodien.com/centos/6.9/contrib/x86_64/
http://mirror.vastspace.net/centos/6.9/contrib/x86_64/
http://mirror.nus.edu.sg/centos/6.9/contrib/x86_64/

ユーザのアクセス元IPや負荷状況に応じてこの中のどれかを利用する。ということですね。

gpgcheck

こちらは先程のグローバル設定項目に出てきましたね。
gpgcheckを実行するかどうかの設定です。先に説明したようにこの項目は各種リポジトリの設定項目でオーバーライドすることができます。

gpgkey

こちらは先程のグローバール設定の項目でも取り上げましたが、GnuPGキーファイルを指す項目です。
このリポジトリのパッケージは開発者によりGnuPG秘密鍵により署名されています。
インストールするパッケージが改ざんされていないことをローカルに保存されている公開鍵 file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 を用いて検証するということになります。

/etc/yum ディレクトリのファイル

こちらのディレクトリにはグローバル設定やリポジトリ設定以外のファイルが存在します。
具体的にはプラグイン設定、パッケージの保護設定、変数の定義などが存在するのでそれぞれの設定を確認してみます。

/etc/yum/pluginconf.d のファイルの設定

こちらのディレクトリにはプラグインごとの設定ファイルが存在しているようです。
fastestmirror というプラグインを例にとって /etc/yum/pluginconf.d/fastestmirror.conf というファイルを見てみます。

[main]
enabled=1
verbose=0
always_print_best_host = true
socket_timeout=3
hostfilepath=timedhosts.txt
maxhostfileage=10
maxthreads=15

定義としては見たところプラグイン固有の設定がされているようです。
共通項目としては enabled くらいのものだと思うので説明は割愛します。

/etc/yum/protected.d のファイルの設定

こちらのディレクトリにはパッケージが削除されないように保護を行う設定を行うことができます。
デフォルトの設定では保護のためのファイルは存在していませんでしたが、こちらに保護対象のパッケージを定義することで yum erase を行う際にチェックが入るようになります。

/etc/yum/vars のファイルの設定

こちらは確認したところリポジトリのmirrorurlなどの設定で使用されていた $infra 変数の定義を行っていました。
同様にして変数名をファイル名にし、ファイルの中身を変数値に展開するように様々な変数の定義を行うようです。

cat /etc/yum/vars/infra
stock

まとめ

以上で設定ファイルの全体概要が見えてきたのではないかと思います。
リポジトリ上でのパッケージ解決や内部実装などについても今後取り上げていければと思います。

参考

Red Hat Enterprise Linux 導入ガイド
mainオプションの設定
repositoryオプションの設定
What is the difference between base URL and mirrorlist in Yum?


mysqlのネクストキーロックと挿入インテンションギャップロックのデッドロックを確認する

概要

先日mysqlを利用したアプリケーションにおいてデッドロックが発生しました。
あちゃぱーと思いつつもせっかくなので自分の中で消化しきれいなかった部分をこれを機に再確認してみることに。

この記事ではmysqlのデフォルトの分離レベル(Repeatable Read)においての レコードロック / ネクストキーロック / ギャップロック / 挿入インテンションギャップロック というハイカラな単語と結びつけながら自分なりに解釈したものを解説します。
と、赤字で書きましたが、始めに詫びを入れておきます。
ロックの解釈はドキュメントを読むだけで詳細に把握するのは非常に難しく、もしかしたら間違っていることを言っているかもしれません。そしたら本当に申し訳ありません。

解決したい問題

はじめに問題を共有したほうがやる気もでるしわかりやすい。
というわけで具体的に問題をものすごく単純にしたテーブルを用意しました。
こちらをつかって問題を再確認したいと思います。

テーブル

はい。めちゃくちゃ単純化しました。主キーとさらにインデックスを付けた属性だけが存在するテーブルです。わかりやすい。

CREATE TABLE test (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  secondaryId bigint(20) unsigned NOT NULL,
  PRIMARY KEY (id),
  KEY idxSecondaryId (secondaryId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

データ

データを用意します。一応ギャップロックとかいう話も出てくるのでギャップが存在するように飛び飛びのデータを入れてみます。これまたわかりやすい。

INSERT INTO test (id, secondaryId) VALUES (10, 10), (20, 20), (30, 30), (40, 40);

デッドロック

さてここで用意ができましたので、問題を再現してみたいと思います。
ここでトランザクション1(Trx1)とトランザクション2(Trx2)という二人のお友達に協力してもらおうと思います。
上から順に時系列で記載します。左側にどちらのトランザクションが実施したかという情報を記載します。

Trx1: begin;
Trx2: begin;
Trx1: DELETE FROM test WHERE secondaryId = 80;
Trx2: DELETE FROM test WHERE secondaryId = 90;
Trx1: INSERT INTO test VALUES (80, 80); // ここでTrx1がロックが取得できずに待ち状態になります
Trx2: INSERT INTO test VALUES (90, 90); // ここでTrx2もロックを取得できずに待ち状態となり、Trx1とのデッドロック状態となります

ということで

これがどういう仕組でデッドロックするか理解している方、これ以上この記事を見る必要はないです。

さておき、問題が確認できました。
実際にはアプリケーションで特定のキーによって管理されたデータを一回削除して再度データを入れ直す、というような処理を実装していました。
データ構造は全く違いますが、問題の本質はこれと一緒になります。
さて、この状況で中で一体何が起こっているのか見ていきましょう。

基本

まずここに上げる公式マニュアルを合わせてくまなく目を通しましょう。
合わせてよくまとまっているサイトなどもあると思うので、嘘か本当か見極めつつ検索して参考にしましょう。
なお本記事では各種ロックがどういうものかという基本的なことは一切説明しません。

さらに言葉の定義をちゃんとイメージできた方がいいと思うのでいくつかここで補足します。

トランザクションとロック

「ロックした」とかいう表現が意外と通じるのであるのであたかも「ロック」というのが動作とか状態のように解釈されがちだと感じますが、自分なりに解釈した結果ですと「ロック」とはアクセスする権利とイメージするとしっくり来ると思います。

例えばあるトランザクションのsql実行が別のトランザクションによって待ち状態になったとしましょう。
この時まさに「ロックした」といってもいいんですが、もっと正確に言うと「ロックを取得しようとしたが取得できずに待ち状態になった」というのが正しいです。

つまりロックというのは単なるアクセス権なのです。
ものすごく単純にいうと、この辺の話はトランザクションという登場人物がいて、レコードを操作する際にロック(アクセス権)を確保したり開放したりして自分の作業の影響範囲を定義しているのです。
でロックが先に取得されていたら、ロックが解放されるまで終わるまで待つ。ロックが開放されたら自分がロックを取得してレコードを操作する。シンプルにそれだけの話です。

ロックとは

でロックにも3つほど種類が存在します。冒頭にも挙げた レコードロック / ギャップロック / ネクストキーロック です。
マニュアルを読むとなるほどこれらの性質はなんとなくわかります。ですが どういうときにどのロックが使用されるのか というところがいまいちマニュアルだけだと分からない。
というわけでここが本記事の肝になります。

調査

というわけでじゃあ具体的にどういうことが起こっているの?という調査をします。

調査環境は osx で MariaDB を使用します。バージョンは下記。

MariaDB [test]> select version();
+-----------------+
| version()       |
+-----------------+
| 10.1.21-MariaDB |
+-----------------+

そして状態を確認するために下記のコマンドを実施しておきます。
これによってトランザクションの状態を出力する際にロックの詳細も一緒に出力してくれるようになります。

set global innodb_status_output_locks = ON;

どういうロックがかかっているのか

では先の例に沿って実行していきます。下記のdelete文を発行した時点で各トランザクションがどういうロックの取得を行っているのか確認してみます。

Trx1: begin;
Trx1: DELETE FROM test WHERE secondaryId = 80;

この時点で下記のコマンドによってトランザクションの状態を確認します。
モニターテーブルを作成してそちらを参照しても良いです。

show engine innodb status\G

するといろいろ出力されるのですが、トランザクションのロック取得状況を表す箇所を抜粋します。

---TRANSACTION 82897, ACTIVE 101 sec
2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 899, OS thread handle 0x700010b45000, query id 4569881 localhost root cleaning up
Trx #rec lock waits 7 #table lock waits 0
Trx total rec lock wait time 28 SEC
Trx total table lock wait time 0 SEC
TABLE LOCK table test.test trx id 82897 lock mode IX lock hold time 101 wait time before grant 0
RECORD LOCKS space id 5931 page no 4 n bits 72 index idxSecondaryId of table test.test trx table locks 1 total table locks 1  trx id 82897 lock_mode X lock hold time 101 wait time before grant 0

確認すると

  1. テーブルに対するインテンション排他ロック
  2. idxSecondaryIdインデックスに対する排他ネクストキーロック

が取得されているようです。テーブルに対するインテンションなロックはこの場合にはデッドロックを引き起こさないので無視します。
では続いてトランザクション2にも同じ手順に沿って実施してもらいましょう。

Trx2: begin;
Trx2: DELETE FROM test WHERE secondaryId = 90;

どうように show engin innodb status を実施すると

---TRANSACTION 82898, ACTIVE 1 sec
2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 945, OS thread handle 0x700010e57000, query id 4569884 localhost root cleaning up
Trx #rec lock waits 2 #table lock waits 0
Trx total rec lock wait time 0 SEC
Trx total table lock wait time 0 SEC
TABLE LOCK table test.test trx id 82898 lock mode IX lock hold time 1 wait time before grant 0
RECORD LOCKS space id 5931 page no 4 n bits 72 index idxSecondaryId of table test.test trx table locks 1 total table locks 2  trx id 82898 lock_mode X lock hold time 1 wait time before grant 0

トランザクション1と同様のロックを取得していることが確認できます。

ここで一つ注目したいのはこれら2つのステートメントの実施はロック待ち状態が発生することなく実施することができました。
つまり delete文によって取得した2つの異なるインデックス値に対するネクストキーロックの範囲は重なっていない ということがわかりました。

ネクストキーロックとは何なのだろう

これに気づいた時結構驚きました。
だってネクストキーロックって、指定の値に対してレコードロックとさらにその前のインデックスとのギャップを含むと説明があるではありませんか。
テストデータの末尾の idxSeconaryId は 40 になっています。
すなわち Trx1 の delete文は (40, 80] の範囲のネクストキーロックを取得し、 Trx2 の delete文は (40, 90] の範囲のネクストキーロックを取得するのではないかと考えました。

わかりやすいように図を用意しましょう。力作です。
横軸が idxSecondaryId のインデックスの並びで、ボックスが取得しようとしているロックの範囲を表しています。

fig1

しかしながら双方のロックが取得しているということは、この仮定が誤っているという結論になります。
この解釈は後で述べることにして、とりあえず先に進みます。

デッドロックの確認

ではロック取得待ちを引き起こすトランザクション1のステートメントを実施してみましょう

Trx1: INSERT INTO test VALUES (80, 80); // ここでTrx1がロックが取得できずに待ち状態になります

ここでロック取得待ちが発生します。トランザクション1のロック取得状態を確認してみましょう。

---TRANSACTION 82897, ACTIVE 2059 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1
MySQL thread id 899, OS thread handle 0x700010b45000, query id 4569893 localhost root update
INSERT INTO test VALUES (80, 80)
Trx #rec lock waits 8 #table lock waits 0
Trx total rec lock wait time 28 SEC
Trx total table lock wait time 0 SEC
------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5931 page no 4 n bits 72 index idxSecondaryId of table test.test trx table locks 1 total table locks 2  trx id 82897 lock_mode X insert intention waiting lock hold time 2 wait time before grant 0
------------------
TABLE LOCK table test.test trx id 82897 lock mode IX lock hold time 2059 wait time before grant 0
RECORD LOCKS space id 5931 page no 4 n bits 72 index idxSecondaryId of table test.test trx table locks 1 total table locks 2  trx id 82897 lock_mode X lock hold time 2059 wait time before grant 0
RECORD LOCKS space id 5931 page no 4 n bits 72 index idxSecondaryId of table test.test trx table locks 1 total table locks 2  trx id 82897 lock_mode X insert intention waiting lock hold time 2 wait time before grant 0

一つロックが増えていますね。これは挿入インテンションギャップロックというやつです。
そしてこれがトランザクション2の取得している idxSecondaryId = 90 に対するネクストキーロックによりブロックされているという状況になります。

そして最後にトランザクション2の方でも insert を実行してみましょう。

Trx2: INSERT INTO test VALUES (90, 90); // ここでTrx2もロックを取得できずに待ち状態となり、Trx1とのデッドロック状態となります
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

デッドロックが起こったことが確認できました。

考察

状況を整理して客観的に見たときに、下記のような条件が成り立っています。

  1. 2つのトランザクションがはじめにdelete文により取得したネクストキーロックはお互いのロック領域に重なっていない
  2. その後発行するinsert文が取得しようとする挿入インテンションギャップロックはお互いのネクストキーロックに重なっている

逆にこれが成り立つような状況を想像できればネクストキーロック、挿入インテンションギャップロックが取得しようとするロックの範囲を推測できます。

で自分なりに解釈した結論が下記のようになります

ネクストキーロックについて

ネクストキーロックはインデックス値の前のギャップも取得することもあるが、ギャップを取得しないこともある。つまりこの場合はレコードロックの範囲と等しい
これは具体的には Trx1 の発行する delete文は idxSecondaryId = 80 のレコードロックの範囲に等しく、 Trx2 の発行する delete文は idxSecondaryId = 90 のレコードロックの範囲に等しい

挿入インテンションギャップロックについて

挿入インテンションギャップロックが取得しようとするギャップは、挿入するレコードが該当するギャップに等しい範囲が対象となる。つまりこの場合は末尾のレコードである idxSecondaryId = 40 以降に存在する無限のギャップが対象となる
これは具体的には Trx1, Trx2 の発行する insert文の idxSecondaryId がそれぞれ 80, 90であり、Trx1, Trx2ともに idxSecondaryId = 40 以降のギャップロックを取得しようとする

わかりにくいと思うので図にします。これまた会心の出来です。
矢印はレコードロックを表しています。

fig2

こんな形になるんではなかろうかと。
記事に記載されていること以外にも色々試行錯誤したのですが、すべての結果を説明できるので自分としてはこんな形であろうと解釈しました。
(本当は実装レベルで把握できれば良いんだろうけれども。)

対応

今回の対応としては削除時に主キーのレコードロックのみを取得して削除するような実装に変更しました。

参考

InnoDB のレコード、ギャップ、およびネクストキーロック
InnoDB のロックモード
InnoDB のさまざまな SQL ステートメントで設定されたロック


時間が立つとGoogle APIのOAuth認証に失敗する

概要

Googleが提供するツール。便利ですよね。
公式ではよくG Suitesと呼ばれているスプレッドシートや、ドキュメントなどなどのことを指します。
アプリケーションからG Suitesを操作するためにはOauth認証を利用するのですが、今回はその際時間が立つと認証時にエラーが出てアプリケーションからの操作ができなくなる問題に遭遇したのでまとめておきます。

エラー詳細

今回のアプリケーションはG Suitesの操作をアプリケーションで実行しています。
挙動としてはまずOAuthクライアントを作成したユーザで 認証を行い、アクセストークンを取得します。
その後アクセストークンを用いてG Suitesの操作を行うようなごくごくシンプルなものです。

当初はアプリケーションが正常に動作するのですが、一定時間が経過した上で再度実行するとG Suitesの操作を行う際にトークンが正常でないエラーが発生するような現象が起こりました。

もう少し細かい実装について述べましょう。
今回はgoogle側で用意してくれているjavaのライブラリを使用しています。
かなりライブラリが充実しているので普通に考えてこれを使わない手はありません。

認証には公式ガイドに沿った典型的な実装だと思います。

GoogleAuthorizationCodeFlow を利用して認証しアクセストークンを取得します。
また認証した情報は FileDataStoreFactory を用いてローカルディスクに保存します。
一度認証した後はセッションを発行して、二度目以降は保存したトークンを用いてアプリケーションの処理を行います。

原因

一定時間が経過すると再度実行できなくなるということから、おそらくトークンが期限切れになった際にトークンのリフレッシュができていないんじゃないかと当たりをつけました。
ちょうどはじめに返却されるアクセストークンの生存期間が1時間でしたので、期限が切れたタイミングで実施するとやはり、エラーが再現することが確認できました。
更に認証時に返却される情報をよくよく確認してみると、リフレッシュトークンが返却されていません。
なのでFileDataStoreにはアクセストークンなどは保存はされているんですが、当然リフレッシュトークンがないのでアクセストークンの期限が切れた瞬間に再発行ができずにエラーとなっているようです。

今回はリフレッシュトークンが空っぽいという点に気づくのに時間がかかりました。
あとはデバッグする際にはアプリケーションを再起動して確認していたので、初回のアクセストークンが切れる前に再度アクセストークンが新しいものに変わるんですよね。なのでエラーが出るまでに時間がかかるので気づきにくかったです。

調べてみますとこちらに情報があり、リフレッシュトークンは初回の認証した時点でしか返却されないそうです。
なんですが approval_promt=force を指定することで毎回認可ダイアログに飛び、それによってリフレッシュトークンが返却されるようになります。

いろいろ試してみたところ、途中から認可ダイアログが表示されなくなるユーザ(この場合リフレッシュトークンが帰ってこなくなります)もいれば、ずっと認可ダイアログが開くユーザもおり、正直なところ何を基準にそうなっているのかわかりませんでした。

参考URLのようにパラメータを指定することで強制的に認証ダイアログを表示するなり、シークレットをリセットするなりするのが対応としては良さそうです。

参考

https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token


gradle daemonを無効にする

javaのバックグラウンドを長く持つ自分にとって最近のお気に入りの言語はgroovyです。

もともと自分は言語の習得にコストを掛けたくなく、新しい言語が少なからず生み出される現状に少なからず嫌悪感を持っているところがなくはないです。
歴史的な経緯で高級言語が生み出されるようなことは好ましいのですが、同時代に構文だけが異なる似たような言語や、今ままでの低級言語のAPIから何故かひどく乖離した構文を提供する言語は疎ましくも思います。

もともとjavaは広く普及していたので自然と使わざるを得なかったのですが、広く使われているだけあってかなり洗礼されています。
javaはやはりLLと比較した時に型付き言語であるためコンパイル時点でかなり品質を担保できます。
またそれでいてCやC++などのレガシーな言語ほどコストを掛けずに開発できることから自分の中では攻撃力/防御力ともに優れた言語、と言った印象でした。
もっというと優れたIDEがあるのがよいですね。

なんですが欲を言うとjavaはその防御力の高さや歴史的には比較的古くから生み出された故にそのAPIに一部不満がある点もなくはないのです。
java8などの新しいAPIでは新しい流れをかなり汲み取っているようで不満があるかというとそういうわけでもないですが、いかんせん他の流れが早すぎるのだと思います。
新しい言語と比べると記述がかったるいことがあります。

このgroovyという言語はjavaというバックエンドを全く損なうことなく、実装速度をぐっとお仕上げてくれるようなものです。
というわけですごく気に入っているのです。

が、良いことばかりかなというと深い部分に行ったりするとちょっと不明な挙動を示したりすることもあるんですが、今回取り上げるのは gradle daemon と呼ばれる仕組みです。

groovyは様々なビルドシステムを利用することができますが、その中でも有名なものの一つにgradleと呼ばれるものがあります。
gradleはgroovyで開発されており、javaやgroovyのビルドツールとして利用できます。

ビルとツールによりgroovyソースはコンパイルされjavaのclassファイルへと変換されたりと様々な事が行われるのですが、その際に多くのオーバーヘッドを含みます。
そのためgradleではそのビルドコストを少なくするためにgradle daemonという仕組みを提供しています。
このdaemonは常駐することでビルドにかかるコストを削減してくれます。

今回この gradle daemon によりちょっとした不都合が起こりました。
現在個人として開発中のシステムで、ものすごくメモリを食うプロセス(javaではない)を起動しなければなく、しかしメモリを食いすぎるゆえに oom-killer によってプロセスが殺されることが続きました。

はじめはそのプロセスのメモリの消費量を抑えることを考えていたので、それが非常に困難であることがわかり、発想を変えて他のプロセスの精査を行うことにしました。

で、TOPコマンドで見てみるといるじゃないですか。でかいのが。

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
15032 z   20   0 3495m 433m  14m S 130.3 23.1   1:09.40 java

上記のプロセスの実態は gradle daemon です。

メモリを大量に消費していますね。物理メモリのおよそ25%をgradle daemonが抱えていることになります。
上記はコンパイル時の状態なんですが、daemonはその目的達成のために各種リソースを抱えたまま常駐します

これはとんでもないということで即見直しの対象として検討しました。
使用されなければswapに吐き出されるんですが、swapに吐き出されるとしてもswapを消費してしまって結構邪魔だし、そこまで頻繁にビルドしない。
そして結局 gradle daemon も oom-killer に殺されているという始末。
というわけでdaemonを起動しないようにしました。

実装としては gradle.properties に下記のように記述を加えました。

org.gradle.daemon=false

これにより無事にメモリを確保することができ、目的のプロセスを起動することができました。

gradleは便利さ故に色々できるのですが、マニュアルがかなり膨大です。
しかも失礼ですが和訳は結構わかりにくいです。おそらく、原文を読んだほうが良いでしょう。

便利なものですが気をつけて使わないとトラブルの温床となりますね。


javaのクラスローダの仕組みについて

javaのクラスローダに関して下記のようなコードが存在するときの内部動作がよくわからなかったので調べ所を残すことにしてみました。

Class clazz = App.class;
ClassLoader cl = clazz.getClassLoader();

クラスとクラスローダについて

さてこの getClassLoader というメソッドは何なのか、ということですが文字通りクラスローダを取得することができます。
このメソッドは Class クラスのメソッドですが、例えばアプリケーションを実装していて同プロジェクトに B というクラスと C というクラスが作成したときに下記のようなコードが存在したときに、この違いはあるんでしょうか?

ClassLoader clB = B.class.getClassLoader();
ClassLoader clC = C.class.getClassLoader();

先に答えを言ってしまうと、この2つで返却されるクラスローダは同じものになります。

理由ですがこの getClassLoader() を呼び出すとクラスは自分の参照するクラスローダを返却します。
すべてのクラスは必ず一つのクラスローダへの参照を持っており、それは自クラスをロードしたときに使用されたクラスローダを参照するようになっています。

そのため B, C は同プロジェクトのアプリケーションとして実装されているので、この2つは同じクラスローダによってロードされるため上記のような場合は大抵は同じクラスローダが返却されます。

クラスローダによるクラスの解決について

またクラスローダは自分の解決するクラスの領域というものが決まっており、階層構造を持ちます。
これはDNSで各ネームサーバが自分のゾーンだけを管理して、解決できない場合は親ネームサーバへと処理を移譲する仕組みと同じようなものです。

javaのデフォルトパッケージを解決するクラスローダのことを「ブートストラップクラスローダ」と呼び、これは一番親のクラスローダとして位置します。
さらにその下にアプリケーションのクラスを解決するための「システムクラスローダ」というクラスローダが存在します。
そんなに複雑でないシステムの場合はこの「システムクラスローダ」がアプリケーションクラスのクラスローダとして機能すると考えていて問題ないと思います。

クラスローダはクラスロードの依頼を受けた際にはまず親のクラスローダに対して解決を移譲します。
もしクラスが解決できないようであれば自分自身の管理する空間を探索します。

例えば下記のようなサンプルコードを用意して実行してみましょう。
ちなみにこのコードはgroovyにて記述してあります。

class Main {
    static void main(String[] args) {
        ClassLoader cl = Main.class.getClassLoader()
        println cl
    }
}

この実行結果は下記のように出力されました

sun.misc.Launcher$AppClassLoader@18b4aac2

「システムクラスローダ」というのは一般的な機能に対する名称のようで、クラス実態としては sun.misc.Launcher.AppClassLoader というクラスが実態のようです。
通常はこのシステムクラスローダがすべてのアプリケーションクラスのクラスローダとして機能します。

異なるクラスローダが用いられるアプリケーションはあるのか

主にJ2EEサーバなどにおいてはwarをおいただけで異なるアプリケーションをホストできる仕組みを提供しているため、各種ウェブアプリケーション単位でシステムクラスローダが異なるような形になると思われます。
設置した複数のwarファイルの中で同じ名称のクラスが存在する際に、うまく解決できないようではアプリケーションとして致命的ですので、このような場合にうまく機能してくる仕組みだと思われます。

通常はひとつのプロジェクトが固有の一つのクラスローダを持つ、、的な理解で問題ないと思います。

参考ページ

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/ClassLoader.html
http://www.nminoru.jp/~nminoru/java/class_unloading.html


javaアプリケーションのcron起動時の文字化けに対応する

概要

javaアプリケーションの起動を行った際に、ある部分で文字化けが起こったので対応について調査する。

具体的には不具合が起こった箇所はjavaアプリケーションから外部プロセスを起動する部分や、メールを送信する機能の本文にVelocityのテンプレート機能を利用しており、これらを利用する箇所でそれぞれ日本語箇所が文字化けしていた。

原因

まず注目したいのは文字化けが起こっているケースがcronによるバッチからの起動に限定されることである。
バッチとは別にwebプロセスも動作しており、これはfat jarを作成してjavaコマンド経由でbashインタプリタ経由で起動している。が、こちらのwebプロセスは特に問題はない。

また先程メールが文字化けすると述べたが、実は文字化けするのは本文だけで、タイトルに関しては文字化けしていなかった。両者の違いは本文は外部テンプレートファイルから読み込んでいるのに対して、タイトルはソースコードに日本語をハードコーディングしている。

このことから大きく予想できるのはcron経由で外部ファイルを読み込むときに何らかの不具合が生じていることである。
cronはenvなどが特殊なんだろうかと当たりをつけて軽くウェブを検索するとこちらの記事を発見

どうやらcron経由でのコマンドはユーザの環境変数は一切適用されない模様。

解決

わかってしまえばなんてことはない。今回は参考サイトに則ってcrontabの記述の上にenv設定を行うことにします。
今回の場合はjavaがおそらく外部ファイルの文字コードを認識できていないのでcrontabの上部に下記のようにファイルエンコーディングを追記しました。

_JAVA_OPTIONS=-Dfile.encoding=UTF-8

無事にアプリケーションが動きましたとさ