admin のすべての投稿

コワーキングスペースの活用法がよくわからない

自分は多くの時間は自宅で作業しています。ですがこないだ気分転換も兼ねて前々から気になっていたコワーキングスペースとやらに行ってきました。
近場にあるのをwebで見つけて、ホームページ確認しつつ良さげかなと。
特に何かを求めていったわけではないのですが作業できればいいかなと思っていってきました。

結論行ってしまうと、正直言って多分一人では二度と使わないんじゃないかと思います。ああいうところに出入りされている方々はどういったメリットがあってご利用されているのか興味があります。

下記、とりあえず思ったこと

良さげな点

  • みんな集中して作業しているので作業する雰囲気がある。家にある誘惑がない。
  • カフェとかと比べると静か。当然パソコンのキータイプは認識されるノイズなので図書館などとは違った許容されるノイズが有る。
  • エアコンが効いている。自宅の作業部屋にエアコンが無いので、特に夏は助かりそう
  • これは一番のメリットだと思うのですが、すでに既知の知り合いと一緒に開発作業したりするのは結構楽しそう。
  • なんか出会いがありそうな雰囲気はある

こんなところかな

悪そうな点 / 理解出来ない点

  • なんか参考書がある。けど持ち出せないし、じっくり読むような場でもないし、なんの意味があるかよくわからない。雰囲気作りか?
  • セパレートつきの座席(いわゆる集中用の作業机)の数が少ない。4つくらしかなくて常に埋まっていた。
  • オープン式の座席は人の気配が気になり全然集中できない。
  • (たぶん)良い席を手に入れるためにはそれなりに頑張らないといけない
  • 結構画面が丸見えなのでソーシャルハックされそう。個人的にはあまり画面は見られたくない。みんな気にならないのかしら。
  • 作業する雰囲気はありますが、なんだかんだで気が散る要因が多いので集中できない。ゴリゴリにコーディングしたり頭使って設計するときは微妙。あれ・・・じゃあいつ使うんだろう
  • そもそも遠い。わざわざ何十分も時間をかけて行くような場所じゃないかな。実は今回はチャリンコで1時間くらいかけていった(もっと近くかと思ったら思いの外遠かった)。車とか使うにしても基本駅の近くにあるので駐車場代が高くつく。
  • 飯や買い物に外出する際に座席を片したり、受付したりと気を使う。自宅であればそんなことは必要ないので。
  • 基本オープン席は外部ディスプレイがない。あと必要な周辺機器も持ち歩くのが手間。

ああいうところで出会った人と一緒に仕事したりっていうちょっと憧れるようなシチュエーションってあるんだろうか。

もうね、いたたまれなくなって3時間くらいのプランで入室したところを30分くらいで早々に退出しました。

とりあえず悪いところが目立ったけれども、1店舗行っただけですしそこが自分にはあまり合わなかっただけな話ですが、活用する意義はシチュエーションやその人の性格によって色々あるんじゃないかなという気はします。
とりあえずお邪魔した店舗に関しては今後は一人じゃ絶対利用しないだろうな。

なんならちょっと静かめのカフェとか、まるまる借り上げちゃうタイプのレンタルオフィスの方が作業スペースとしては色んな面で優秀かなと思うので、やはりコワーキングスペースにしかない価値というのを皆さん発見してらっしゃるんだと思う。


アナリシスパターンを理解する

最近立ち返ってDDD本を読み進めています。
今回は11章の気づきに関して取りまとめます。

概要

第11章ではアナリシスパターンについて解説している。
本記事ではそれに対する個人的な解釈を述べる。
実際にはアナリシスパターンは非常に抽象的な話で、概念的に理解するのはそこまで難しくはない。

アナリシスパターンとは

アナリシスパターンとは既存の業界についてすでに洗礼されたモデル(それはシステムとしてという文脈とは全く関係なくて構わない)が存在する際に、そのドメインを学習することでドメイン設計に対するアプローチとすることである。

本章では具体的に会計システムの開発を行う際に、会計の専門書を解読することで設計のアプローチとする例が取り上げられている。

また外部の知識と合わせて現場にドメインエキスパートがいるようであれば、ユビキタス言語を用いて設計をすり合わせることでより洗礼されたモデルを設計することができる。

これは非常に強力な手法であり、まず率先して取り入れるべき方法だと思う。
特にその業界に対する知見が全く無い場合丸腰で設計するのは自殺行為に等しい。
システムという側面に囚われすぎず、一般的な専門書などから得られる洞察は膨大なものになるだろう。

ドメインエキスパートについて

この例ではドメインエキスパートが出てき、開発者とともに簡素なクラス図のようなものを用いている。
だが実際に現場にいるドメインエキスパートがここまで開発側の用いている手法に対して理解があることは少ないのではないかと思う。
それは協力的な姿勢であったり、そもそも開発者が用いているクラス図がどのようなもので、どういったヒントを求めているかというコンテキスト的な解釈ができるかどうかという事も含めてである。

そういう意味で、本章に出てきているドメインエキスパートを含むチームはすでにチームビルディングに巧みに成功している例といえ、自分の経験上現実にこのような状況になることは難しい。

チーム全体がDDDを理解しているためには組織全体が積極的に働きかけ、エンジニアだけでなく広い範囲でこうした開発スキームの理解をし、さらに時間をかけて学習している必要がある。

否定的な意見を言ったが、現実的にはクラス図などを用いなくとも、ドメインエキスパートから話を聞くということだけでも大いに有益であり、これも積極的に取り入れることで恩恵に預かれる。

アナリシスパターンの肝

個人的に「業界知識」「ドメイン設計」「実装」この3つのバランスを上手く取ることがアナリシスパターンの肝だと感じる。
まず「業界知識」「ドメイン設計」の部分であるがこの2つは実際に最終的に落とし込む場合に異なる箇所が出てくる。当然全く同じ状態になるかとそうでもない。
アプリケーションを実現するということが第一目的であるため、独自の知識や組み合わせなどが出てくることがあるからである。
適切な境界やモデルが出来上がるまでに何度も設計を吟味してドメインを練り直す必要がある。この際に業界知識に引っ張られすぎてはいけない。

また「ドメイン設計」「実装」に関しては実装が持つルールやフレームワークを使用している場合、また言語の特性によってはドメイン設計をそのまま実装に落とし込むことが困難になる場合がある。
そのためドメイン設計を多少捻じ曲げて実装を行うこともある。
この際なぜそうなっているのかという証拠などを十分に残して置くことなどが大切になるのではないかと思う。


しなやかな設計を理解する

最近立ち返ってDDD本を読んでいるのでそのまとめ。
今回は10章の気づきをば。

概要

10章は設計と実装とをより深く結びつけていくための様々な概念が登場するため、それまでの章と比べて毛並みが違うイメージがある。
具体的にはより実践的なアプローチが紹介されているように思う。

本章で紹介されている見出しを以下にピックアップする。

  • 意図の明白なインタフェース
  • 副作用のない関数
  • 表明
  • 概念の輪郭
  • 独立したクラス
  • 閉じた捜査
  • 宣言的な設計
  • 設計の宣言的スタイル
  • 攻める角度(サブドメインを切り取る)

となっている。
このように列挙してみると色々あり混乱しそうになるが、個人的には結局は「宣言的な設計」ということが目指すべきゴールなのではないかなと言うことだと思う。

それぞれ噛み砕く

意図の明白なインタフェース

これに関しては、DDDという文脈でなくとも実装をかじっている際に誰もが自然と意識していることなのではないかと思う。
要するにわかりやすいインタフェース(メソッド名)を設計しろと言うこと。
先人たちが作ったライブラリまたはアプリケーションを実装する際に別々のレイヤを実装している共同作業者などの成果物が、外部インタフェースを確認するだけで何をやっているか自然にイメージできるような設計にしろ。という解釈。

言わずもがな、人間一人ができる作業領域というのは限られる、他人の成果物を利用するのが当たり前。
OSもその最たるものの一種である。
アプリケーション開発を行う際にはそれらAPIがインタフェースから明確に理解できる必要がある。
内部実装までトレースすることで挙動を確認することは現実的ではない。
要するに副作用など含んだこちらの意図していない挙動を示す実装など使い物にならない、ということ。

これはDDD本内部では言及はないが、いわゆるAPIドキュメントを公開するということもこのアプローチと相違しない考え方なのではないかと思う。
例えばJavaの実装ではJavaDoc、RubyではRubyDocなどのドキュメントの仕組みが充実しているので、それらを合わせて初めから開発をすすめるというのも取れるアプローチなのではないか。

副作用のない関数

こちらDDD文脈でなくとも実装をしているうちに何となくイメージができるものだと思う。
副作用がある関数というのはその気持ち悪さが言語化できなかったとしても、開発者は妙な気持ち悪さというものを感じているのではないか。

ここで一つ悪い実装例を提示する。
自分が保持している液体の容量を持つコップというクラスがあってそれはあとから液体を足すことができる。
アプリケーションとして別のコップから液体を移動することができる。

簡単化のためにメンバ変数をpublicにしている。
ここでものすごくイケていないのは、cup1にcup2を加えた際にcup2の容量まで変更されているということである。
この気持ち悪さを言語化しているのが「副作用」であり、クライアント側から想像していないような変化が内部で生じているという状態。

副作用は単体で悪さをしているがなぜそうなるかということはおそらくもっと広い視点で設計自体をないがしろにしている可能性が高い。
その設計モデルがドメインと全く適合していないのである。(最もこの例ではそれ以前の問題ではあるが)
そのためにこの章で合わせて説明される他のアプローチと合わせて対応する必要がある。

表明

副作用はできるだけなくしたほうが良いというのは先程の例を見れば一目瞭然だと感じていただけるはずだ。
ただしやむを得ずなくならないというようなパターンもある。
副作用を含んだメソッド呼び出しのことを「コマンド」と呼ぶが、それでもこのコマンドをどうにか担保するというのが「表明」と呼ばれる対応である。

表明はこれまでの概念と比べて実装的な側面が強い。
ぶっちゃけていうとユニットテストと言っても差し支えはないだろう。

副作用が起こる前後の「事前条件」と「事後条件」というものを定義して、コマンドを実施した際の変化を担保しろということ。
またこの表明が存在することによって対象ドメインの内部実装まで確認せずともそのふるまいが理解できる。
テストケースを見ればどういう挙動かどうか推測できるという感覚である。

また言語によってはこの表明(assertionと呼ばれる)という仕組みを実装していることがあり、その場合は実装中に含まれることになる。

この「表明」については個人的なところあまり頻繁に活用するものではないという印象がある。どうしてもやむを得ないときだけに抑えるべきだろう。
結局表明を確認しなければいけないほど内部の振る舞いがこじれているということは、設計に立ち返って新たな適合するドメインを模索する必要があるのではないかと思う。

概念の輪郭

インタフェース、クラス、集約、操作などを考慮する際に自分の直感と立ち向かって常にリファクタリングを行うことを惜しむな。
実装途中に特定の処理があるメソッド中にさながらトランザクションスクリプトのようになってきたらそれはモデルが適切ではなくなっている。新しいエンティティやバリューオブジェクトなどを切り出すということが必要になってくるだろう。

この概念の輪郭については実装当初から完全に洗い出して着手するということは、あまり現実的ではない。
特にそれに対する知識が不足しているような状況であればなおさらのことである。

実装しているうちに当初考えていたモデルが適合しなくなって来た際にはその直感を大事にして、新しい輪郭を検討する必要がある。

リファクタリングが小さな範囲に収まっている場合は大きな輪郭としては適切に設計できているということである。
それが大きな範囲になってしまった場合はそもそもの根底部分の設計が間違っていた可能性がある。
この際にも非常に大きなコストを払う必要があるが、輪郭を再度設計する必要がある。

現場にいると様々なプロジェクトで既存の実装を捨てて一から作り直すような、大規模なリファクタリングを選択するシーンがそんなに少なくないと思う。
そこに技術者が新しい技術を導入したいという意思が介在していることも大きいが、その一端には古いモデルを捨てて新しい輪郭に沿った実装を行いたいという意味合いも含まれると思う。
業務に詳しくなっていくうちに、それまでの設計がそぐわないことに気づくことはよくある。

独立したクラス

クラス同士が過度に依存していることはあまり好ましくない。
クラスの同士はなるべく低結合とするべき。

こちらもそもそも不必要な依存が大量に発生している場合は設計に立ち返って見るべきである。

閉じた操作

閉じた操作とはインタフェースに同クラスを与えて、返り値にも同クラスが返却されるようなインタフェースのことを指す。
この状態が出来上がると直感的には、そのクラスのみが変更されて更に変更された新しいクラスが返却されてくるのだなというイメージができる。
またそれによって副作用なども含みようがないので非常に明瞭である。

このアプローチは積極的に採用すべき。
逆説的だがこれまで上げたアプローチが達成できていれば、自然と閉じた操作ができるか、またはほとんどコストを掛けずに適用することができる状態になっているはず。

宣言的な設計

閑話休題。これまでの話と少しそれるのだが宣言的な設計というものがある。
「宣言的」の対義は「手続き的」であり、コンピュータは当然後者に親和性が高い。
人間が介在する際には、宣言的である方が自然であり、解釈しやすく、同然間違いも生まれにくい。という考え方である。

ルールを矯正することで宣言的に動作する代表的で馴染みのあるものはフレームワークである。
こういったモノは理論的には上手く機能するように思うが実際にはうまくいかないことも多々ある。
まず一つのリスクとしてはフレームワークが巨大である場合学習コストが高くつく、ということである。
しかもそれはプロジェクトなどに適用する場合全員がもれなく従う必要がある。
またそのフレームワークがドメインで実現したいことをサポートしなかった場合、何らかの形で対応する必要がある。
大抵その対応は醜いものになるだろう。

DDD本では適用するのは一部の効率的に機能する部分をサポートするようなフレームワークに留めるのが良いのではないか。ということを指摘している。

設計の宣言的スタイル

本章の目指すところ。
宣言的な設計を目下のドメインの実装に取り込むこともでき、これが達成できれば非常に人間にとって親和性の高い気持ちのよい実装が実現できる。
一行一行目を通してようやく実装を把握するような処理ではなくて、メソッド名をひと目見て把握できるような宣言とするべきである。
その末にはまるで述語を記述するような形でのメソッドが適用できるようになる。

解説を投げてしまうが本章で説明されている「仕様」の組み合わせ。の例は素晴らしい。
閉じた操作に則っていることで仕様からまた新しい仕様を動的に生成できる。
仕様を持ち回すことができ、コードのクライアントはまるで述語を記述するように実装できる。
これは言語を書いているようで、非常に直感的で人間と親和性が高い。

攻める角度(サブドメインを切り取る)

設計を行う際に、どういったアプローチを取るかというのは無限にある。
その中で切り出すドメインによってはとても適切であり、効果的に表現力を持てることがあることがある。

その最たるものとして「数学」がある。
そのドメインの中に内在する数学を切り取ることで他のドメインからは一切の数学を排除できる。
また数学は誰もが学習していることであり、学習コストが低い。

その他の特別なドメインを切り出す場合、それが一般的でない場合全員が学習する必要があり、またモチベーションなどの側面からも効果的ではない。


ndenv の初期化スクリプトが何しているか追いかけてみる

node のバージョン管理ツール ndenv をインストールすると初期化スクリプト内部に下記のコマンドを追加します。

せっかくなので下記のコマンドがどのように解釈されるのかというところを追ってみることにしました。
検証は osx 上で実施しています。各環境で差異があると思われますので注意してください。

evalされるスクリプトを確認する

まずはターミナル上でコマンドを実施してその内容を確認してみます

typesetとは

まず man コマンドを用いて調べるが、ドキュメントが存在しない。
なのでググりながら色々試して把握してみる。

どうやら typeset は変数の宣言を行うコマンドであるが、特徴的なのが変数に実際に格納されたデータとは別に型の情報を持つことができる。
これはイメージとしては高級なプログラミング言語で変数に型があるような感覚と少し似ている。
確認してみよう。

例えばシェル上で次のコマンドを実施する

このあとに typeset コマンドを実施することですべての変数をリスト形式で表示する

おそらく上記のような出力がされるのだが、出力された他の項目と比較してみると若干様子が異なる。
そう。型情報がない。

そこでこのhoge変数に型情報を宣言してみる。-u というオプションを指定することで「小文字を大文字に変換する」という型を宣言することになる。

このあとに先と同様に変数リストを表示してみる

再度typesetコマンドを実施すると上記のように uppercase という型が宣言されていることがわかる
ここで echo コマンドを実施してみると

と出力される文字が大文字に変換されて出力されることがわかる。
これは echo コマンドが typeset で宣言されている型の情報まで解釈してくれ、実態として持っている fuga という値を uppercase という型に準じて大文字に変換して表示してくれている。

というわけで本筋に戻ると、下記のようなコマンドでした。

これは単純に command という変数を宣言しているに過ぎないです。
なので、なくても困らないのではないかなと思います。

次に行きましょう。

こちらは単純に第一引数を command 変数に格納しているだけです。
続いて command 変数に格納した第一引数が不要になったので破棄しています。

最後の箇所ですが、コマンドに応じて分岐した処理を実施します。

例えば rehash を実施すると、下記のコマンドの結果を評価するものと同等の内容になります

hashコマンドはシェルが記憶しているコマンドをキャッシュしています。
-r オプションはキャッシュされた内容を削除します。そうすることによってキャッシュされたコマンドのパスをクリアすることができます。

また環境によると思いますが動作環境では hash -r したあとにコマンドを実施してみると、コマンドは普通に実行できます。
これはおそらく hash -r で削除したんだけれども、コマンドを実施する時点ではまた環境変数 PATH をもとにコマンドを探索するような処理が走っているのかなと考えられます。

何れにせよ。ndenv rehash のコマンドはキャッシュされた node コマンドのパスのクリアができることがわかりました。
command はそのまま ndenv へと引数を渡しつつコマンドを実施することがわかりました。


javaのPathについて調べる

javaにはパスを取り扱うためのクラスとして java.nio.file.Path クラスとしてAPIが用意されています。
この Path クラスは java.io.File クラスと相互変換が可能なように設計されているので、JVM上でファイルやディレクトリを操作する際には主にこれらのクラスを利用することになります。

なお本記事のサンプルコードは基本的にgroovyを用いて確認していますので注意してください。

まずは概要をおさらい

公式ドキュメントの内容を噛み砕きますので、基本的にそちらを参照していただければと思います。

PathクラスのAPIでは一つ一つの構成要素のことをコンポーネントという呼び方をするようです。
例えば /tmp/hoge/fuga.txt というパスが存在したときに、4つのコンポーネントが存在します。

特にルートを表す冒頭の /(スラッシュ)に関してはルートコンポーネントと呼ばれます。
ルートコンポーネントは存在しないこともあります。その場合はシステムのワーキングパスからの相対パスを表現しています。
また tmp, hoge というディレクトリを表すパスコンポーネント及び fuga.txt というファイルを表すパスコンポーネントから成り立っています。
このパスは resolve などのメソッドを利用することで結合することが可能です。

実際に利用する

また java.nio.file.Path はあくまでパスを取り扱うリソースのためのインタフェースの定義となります。
実際に実装で使用するための手っ取り早い方法は java.nio.Paths クラスを用いることです。
PathsクラスにはPath実装のインスタンスを取得するためのstaticメソッドが用意されています(というかそれしか用意されていません)

具体的には先程の例で言うと

Path path = Paths.get("/tmp/hoge/fuga.txt")

などとすることでpathインスタンスを取得することが出来ます。
またのちほど説明しますが、このPathsクラスはあくまでデフォルトのFileSystemへの実装となるので込み入った用途には向きません。

またURIインスタンスを引数として受けとり、Pathへと変換するメソッドも用意されています。
こちらは下記のように使用できます。

URI uri = new URI("file:///tmp/hoge/fuga.txt")
println Paths.get(uri).toAbsolutePath() // -> /tmp/hoge/fuga.txt

もう一つ試しにfile以外のスキームを持つURIを与えてみましょう。

URI uri = new URI("http://google.com/")
println Paths.get(uri).toAbsolutePath()

こちらを実行すると下記のような例外が発生します

Caught: java.nio.file.FileSystemNotFoundException: Provider "http" not installed
java.nio.file.FileSystemNotFoundException: Provider "http" not installed
  at java_nio_file_Paths$get.call(Unknown Source)
  at Hoge.run(Hoge.groovy:8)

先程述べたようにデフォルトのFileSystem実装を利用するため、httpスキームなど知りません。といったような例外が吐き出されます。

さっきから出てくるFileSystemってなんなのさ

java.nio.file.FileSystem についてだんだん気になってきたと思いますのでこちらも調べましょう。
FileSystemはファイルシステムへのインタフェースを提供します。
デフォルトのファイルシステムではJVMからアクセス可能なファイルシステムへのアクセスを提供します。
ですので先程file以外のURIスキームを引数に与えたときに例がが発生したのはこのためですね。

さらにこのFileSystemインスタンスを取得するためには FileSystems というFileSystemのファクトリクラスを用います。

このクラスに定義されたメソッドを用いることで当然デフォルトのファイルシステムを取得することも出来ますし、独自のカスタムファイルシステムを定義することも可能です。

まとめ

あっさりした内容になりましたがPathAPIの周辺情報が確認できました。

参考

Path
java.nio.file.FileSystemFileSystem
FileSystems


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などでカバーするっていうのは、なんかちょっと無駄なような気がしますし、できたとしてもそれに追従するコストが結構かかります。
ようするに無駄なコストをかけざるをえないんじゃないかと思います。

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


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 ステートメントで設定されたロック