その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

YAPC::Fukuoka Hakata 2017にてWeb Application Good Error Messageというタイトルで話してきました

表題のとおりです.話しました.
これは僕が普段の開発中にエラーメッセージと触れあう時に気にしていたり,考えていることを上手いこと言語化したいという試みから始まったものです.

speakerdeck.com

発表中にdan kogaiさんから「『間違えたことを言っているエラーメッセージ』も悪いエラーメッセージじゃないのか」というフィードバックを頂いて,確かに!! と思いました.すっかり抜けていました.おっしゃる通りです.

発表後頂いた質問としては「(セキュリティ的な観点から) エンドユーザ (非開発者) に詳細なエラーメッセージを表示しては駄目な場合とかがあると思うんだけど,そこらへんどうしてるのか」というものがあったんですが,回答としましては:

  • 技術的に詳細なエラーメッセージはエンドユーザに提供しない
    • エラーが出ている場合,エンドユーザに技術的詳細を提供しても基本的に対処不可能 (多くの場合サーバのエラーなので)
    • 非技術的な方法で対処できる場合はその手順をエラーメッセージとして示す (e.g. 「ネットワーク状態の良いところで試してください」)
    • 仮に「エンドユーザの操作・手順がおかしいからエラーが出ている」という場合があったとしても,それはアプリケーション側のバグ (ユースケースを想定してない,あるいは限定しきれていない) ということなので,「ユーザに正しい使い方を提示する」といったエラーメッセージは (基本的に) 提示しない
    • Tracking IDみたいなものを併記すると問い合わせとかに使えて良さそう
  • 何らかの統一的なエラーに統一する,みたいな手法もある
    • 「アプリケーションがおかしいので後でやり直してみてください」みたいなのに全部のエラーを隠蔽するとか……
    • これに関しては賛否あると思いますが
  • 技術的詳細が書かれた「解決」の為のエラーメッセージは開発者向けには出す

という感じでした.このへんは発表時間の都合上,省略してしまったので情報が薄くなってしまいました.反省しています.

あと「moznionを呼ぶボタン」はどういう実装になってるのかという質問もあったんですが,あれはボタンを押すとikachanが発火して僕にチャットでメンションが飛ぶという素朴な仕組みになっています.


今回のスライドに書かれていることは個人の思想が強いので,他にも様々な意見等あることと思います.エラーメッセージに関してはもっと色々な考えや,トピックや,思想があると思うんですがあまり表立ったものが無い気がするので,色々議論したい感じがしています.しましょう!

Exit statusのセマンティクス

*nixのexit statusのセマンティクスについてかつて質問して,答えていてもらっていたことを思い出したので記します.

moznion   [5:46 PM] signal受け取ってexitする時,そのsignalの値をそのままexit codeに使う,みたいなお作法みたいなのってあるんでしたっけ
takesako  [5:50 PM] ないと思いますー
moznion   [5:50 PM] 特に無いんですねえ,ありがとうございます
songmu    [5:51 PM] なんか、ラッパースクリプトとか書くときは、ものによるけど維持するように気をつけることとかある。
          [5:51 PM] horensoとかは維持するようにしてたはず。
moznion   [5:52 PM] なんかそこら辺はお行儀みたいな感じですかねえ
songmu    [5:53 PM] 上位でどのシグナルで殺されたかとか判断したいかどうか、とかかなぁ。
xaicron   [5:53 PM] eixt code はアプリ自体で定義しているものを出すってことに決めればいいと思ってる派
          [5:53 PM] コマンドラインだったら成功か失敗かぐらいしかほとんどユースケース無い気がする。あとはログを出そう
moznion   [5:55 PM] まあですよねえ
          [5:55 PM] 利用者がexit codeで挙動変えるような使い方をしてるかも知れないから,ラッパーであればcodeを維持するみたいな理念だと察しました
songmu    [5:55 PM] そですね
moznion   [5:57 PM] ログを出しておいたばかりにそれを利用者側に正規表現で引っ掛けられて挙動を分岐させられるのは起こりえそうですが知ったことではない
hirose31  [6:09 PM] exit code、意味あるで
tokuhirom [6:12 PM] どこがダジャレになってるのか気になっている
hirose31  [6:12 PM] w
          [6:12 PM] http://www.unix.com/man-page/all/3/sysexits/ とか /usr/include/sysexits.h とか。
          [6:13 PM] sendmailとかは、aliasesで呼ばれてるフィルタプログラムが exit 75 (EX_TEMPFAIL) すると再実行したりするで。
cho45     [6:15 PM] 絶妙に中途半端な数字だ
hirose31  [6:16 PM] 「いそのー EX_UNAVAILABLE しようぜー」
kazuho    [6:31 PM] execすればexitコード一致問題なくなるで
yappo     [6:52 PM] ひどいw
moznion   [7:31 PM] exit code割と無自覚に使ってたもんで,なんか紳士協定とかがあるのか気になったという次第でした
mattn     [7:32 PM] http://linuxjm.osdn.jp/html/LDP_man-pages/man3/exit.3.html
          [7:33 PM] https://ja.wikipedia.org/wiki/%E7%B5%82%E4%BA%86%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9
          [7:33 PM] こっちか
          [7:33 PM] 普段 0/1 だけど usage 出す時に変えたりしますね
moznion   [7:34 PM] fsckって論理和でステータス変わるのか……
          [7:34 PM] ですね,僕もそういう感じで使ってました

JSON::XSでデシリアライズする時にtrueとfalseの扱いを変更する

Perl5の話題です.
JSON::XSを用いて,JSON StringをPerlのHashRefにデシリアライズする時にtrueとfalseの扱いを変えたいという話です.

デフォルト状態でJSON::XSを用いてデシリアライズすると, trueTypes::Serialiser::true すなわち JSON::PP::Boolean の真値として, falseTypes::Serialiser::false すなわち JSON::PP::Boolean の偽値として扱われます.
普通の処理であればこれで問題ないかもしれませんが,例えば「JSON StringをPerlのHashRefにしてそれを更に何らかのシリアライザに通す」といったような処理をしようとすると,JSON::PP::Boolean のオブジェクトだと取り回しが悪い場合があります.そういった時に,true/falseを任意の値にマッピングするにはどうすれば良いか.

https://metacpan.org/pod/JSON::XS#true,-false
Types::Serialiser - simple data types for common serialisation formats - metacpan.org

やり方としては上記のドキュメントのまわりにあるように, $Types::Serialiser::true 及び $Types::Serialiser::false を任意の値 (reference) に書き換えてやると良い.これらの変数はourで宣言されており,外から操作することができる.

use Types::Serialiser;
local $Types::Serialiser::true;
local $Types::Serialiser::false;
BEGIN {
    $Types::Serialiser::true = \1;
    $Types::Serialiser::false = \0;
}

use JSON::XS qw/decode_json/;

my $json = decode_json('{"true": true, "false": false}');
use Data::Dumper; warn Dumper($json);

例えばこのようにすると true\1 に,false\0マッピングされるようになります.

あるいは以下のように所望のオブジェクトにマッピングすることもできます;

package MyBool {
    sub new {
        my ($class, $val) = @_;

        return bless {
            val => $val,
        }, $class;
    }
}

use Types::Serialiser;
local $Types::Serialiser::true;
local $Types::Serialiser::false;
BEGIN {
    $Types::Serialiser::true = MyBool->new(1);
    $Types::Serialiser::false = MyBool->new(0);
}

use JSON::XS qw/decode_json/;

my $json = decode_json('{"true": true, "false": false}');
use Data::Dumper; warn Dumper($json);

注意としては, $Types::Serialiser::true 及び $Types::Serialiser::false はreferenceを要求しているという点です.仮にここにreferenceではないscalar値なんかを突っ込むとなにが起きるかというと

use Types::Serialiser;
local $Types::Serialiser::true;
local $Types::Serialiser::false;
BEGIN {
    $Types::Serialiser::true = 1;
    $Types::Serialiser::false = 0;
}

use JSON::XS qw/decode_json/;

my $json = decode_json('{"true": true, "false": false}');
use Data::Dumper; warn Dumper($json);
$ perl json.pl
Segmentation fault: 11

SEGVします. JSON::XSはxsの処理の中身で Types::Serialiser::trueTypes::Serialiser::false の中身を SV * つまりreference相当に割り当てているためです.ここで Types::Serialiser::true|false にscalar valueすなわち SV を割り当てると当然SEGVしてしまいます.

このへん
https://metacpan.org/source/MLEHMANN/JSON-XS-3.03/XS.xs#L95
https://metacpan.org/source/MLEHMANN/JSON-XS-3.03/XS.xs#L1974
https://metacpan.org/source/MLEHMANN/JSON-XS-3.03/XS.xs#L136

<追記>
誤りの指摘を頂いたので修正しました.SvROKで事前チェックすれば良い気もするんですがどうなんでしょうね.

karupanerura [17:59]
リファレンスでもスカラ値でも `SV *` だから、ここでSvRVしているときにリファレンスじゃないとNULLが返ってくるのでSEGVしているというのが濃厚なきがする https://metacpan.org/source/MLEHMANN/JSON-XS-3.03/XS.xs#L139
[18:01]
リファレンスは `SvRV` というSVで、数値も `SvIV` というSVで差異はなくて、XSでSVを扱うときは基本的に `SV *` として扱うので、<追記ここまで>

きをつけましょう.

List::Haystack - A immutable list utility to find and count element


Perl5の話題です.List::Haystackというものを書きました.CPANにもアップロードしています.
経緯としては以下.


というものが欲しかったのです.List内の要素検索について,Immutableに処理をしたい + lazilyに処理したい,みたいなモチベーションです.

個別の使い方としてはpodを読んでもらうとして,例えば以下のように書くだけでシェークスピアのワードカウントなんてのを手っ取り早く書けるわけです.皆さん大学とかでやりましたでしょう.僕はやりませんでした.

use LWP::UserAgent ();
use List::Haystack;
use Data::Dumper;

my $response = LWP::UserAgent->new->get('https://ocw.mit.edu/ans7870/6/6.006/s08/lecturenotes/files/t8.shakespeare.txt');
my $txt = $response->decoded_content;
my $words = [grep { $_ } split /[^a-zA-Z0-9]/, $txt]; # XXX: Sloppy word separation!!!
my $haystack = List::Haystack->new($words);
print Dumper($haystack->haystack);

まあこういうのってperlだとHashとかで簡単に書けるんで別にモジュールにしなくても……と言う感じですが,アドホックに書くと同じような処理が随所に散らばってしまいがちで精神衛生上良くなかったので一丁モジュールにしたためた次第です.あとlazilyなconstructionが欲しかったので.

ご利用くださいませ.

Perl5のサブルーチン呼び出し時に二項演算子を期待していたら単項演算子の引数として認識されてしまう

sub func {
    return 100;
}

みたいなサブルーチンがあった時に,

say(func - 99);

みたいな感じで呼び出すと,パッと見 1 が出力されそうに見えるが,実際は 100 が出力される.
これは - 99func の引数として解釈されて食われるためである. B::Deparse するとわかりやすい.

$ perl -MO=Deparse func.pl
sub func {
    use warnings;
    use strict;
    use feature 'say';
    return 100;
}
use warnings;
use strict;
use feature 'say';
say func(-99);
func.pl syntax OK

ここから,二項演算子としての振る舞いを期待していた -99 と結合して単項演算子として扱われ, -99 が引数として扱われているのが読み取れる.

もちろん,サブルーチン呼び出し時にかっこを付けて

say(func() - 99);

という感じで呼び出してやれば当初の期待値である 1 が表示されるようになる.もしくはプロトタイプを利用する方法もある.

sub func () {
    return 100;
}

このように引数を取らないことをプロトタイプで明示すると,サブルーチン呼び出し時にかっこを付けなくても後続の値が引数として解釈されることはなくなる.その場合の B::Deparse の結果は以下のようになる.明解.

$ perl -MO=Deparse func.pl
sub func () {
    use warnings;
    use strict;
    use feature 'say';
    return 100;
}
use warnings;
use strict;
use feature 'say';
say func - 99;
func.pl syntax OK

これらは基本的な挙動ではあるのだけれど,日々を生きているとたまにハマりがち.例えば Time:PieceTime::Seconds をあわせて使ったときなんかに

use Time::Piece;
use Time::Seconds qw/ONE_HOUR/;
my $t = localtime - ONE_HOUR;
say $t;

などと書こうものなら,結果が Thu Jan 1 08:00:00 1970 というふうになり破滅する.localtime(-ONE_HOUR) となるからだ.
普段,perlの組み込み関数の時は (基本的に) かっこを付けずに記述するスタイルで書いていたので,組み込み関数である localtime をその感覚で使っていたところ綺麗にハマってしまった. *1


いままでは割と野生の勘みたいな感じでこのへんの諸問題を回避していて,「そういう掟なのじゃ」という感じで生活していたんですが真面目に B::Deparse とかで調べてみると色々学びがあるものですね.

*1:Time::Pieceはlocaltimeをoverrideするから最早組み込み関数ではないんでは? という意見はあると思いますが

YAPC::Fukuoka 2017 HAKATAのトークプロポーザルを出した

yapcjapan.org

出しました.最近YAPCでしか喋ってない感じがしますね.果たしてそうなのです.

というわけでプロポーザル内容は以下のとおりです.

Web application good error messages and bad error messages

Webアプリケーションを作り,運用していると例外的な状況が発生するものです.
例外的な状況 (エラー) は起きない (起こさない) に越したことはありませんが,しかしながらそれらをゼロにするのには相当のコストを要しますし,そもそも「本当にエラーが起きる余地が無いのかどうか」を証明するのも困難です (こうした問題領域を解決する為の手法やツールもありますが,そうしたものについては本トークでは触れません).
従って現実問題として,そうした例外的な状況に対応していく必要があります.そのような時にプリミティヴな武器として有効なもののひとつに「エラーメッセージをログに書き込んでおく」というものが挙げられると思います.また,エラーメッセージはサーバ内にログとして留めておくだけではなく,クライアントに対して表現する必要がある場合もあります.つまり,サーバ・クライアントを問わず,エラーメッセージは問題の修正・解決にとって重要な役割を果たしていると言うことが出来るでしょう.これはユーザ体験にも直結する要因のひとつとも言えます.

本セッションでは

  • サーバ内部でのエラーメッセージ
  • 外部に提供するエラーメッセージ

という2つの文脈について

  • 問題の「修正」に役立つエラーメッセージとは
  • 問題の「解決」に役立つエラーメッセージとは
  • エラーメッセージの粒度
  • エラーメッセージのレベル
  • エラーメッセージの検索性
  • perlにおけるロギング

などといった話題について触れたいと考えています.




エラーメッセージやロギング等はアプリケーションの構築および運用において不可欠で重要な存在だと思っているのですが,あまりそこら辺にフォーカスを当てた発表や資料が無いな〜と思ったので *1 こうしたテーマを選択した次第です.

採択されたら喋れます.よろしくお願いします.

*1:しっかりあることにはある.例えば右は非常によい資料です: 良いデバッグログはプロジェクトの資産である // Speaker Deck

OpenWrtの環境に対してAnsibleでプロビジョニングをかける

メモ.
Ansibleを利用するという文脈におけるbareなOpenWrtの環境の特徴としては以下のようなものがある;

  • sftpが有効ではない
  • Pythonランタイムが入っていない

これらを解決すれば,あとはいつものように普通にAnsibleを使える (はず).
というわけでそれぞれに対する対応は以下の通り.

sftp-server が有効ではない

そうした場合にansibleを走らせると以下のようなエラーが出る.

failed to open a SFTP connection (Channel closed.)

対応としては ansible.cfg に以下のように設定を書き込むと良い.

[ssh_connection]
scp_if_ssh=True

公式ドキュメントによると,scp_if_ssh をTrueに設定すると,sftpの代わりにscpを利用してファイルの転送を行なうようになるとのこと *1
なお,環境変数で指定する場合には ANSIBLE_SCP_IF_SSH=true という風に指定すると良い様子.(参照: https://github.com/ansible/ansible/blob/feafae70b579a3890951fd5c7c7b6645c340ff00/lib/ansible/constants.py#L221)

参照: linux - Is the SSH SFTP subsystem required on the managed nodes for Ansible to work? - Server Fault

Pythonランタイムが入っていない

Pythonを入れれば良い……のだが,恐らくOpenWrtが動作するような環境では少しでもストレージの使用量を節約したいというのが人情でしょう.ここではPythonランタイムをインストールせずに済ませる方法を記す.

プロビジョニング対象の環境内にPythonが無い場合,playbook内に gather_facts: no と記述した上で,rawでタスクをモリモリ書いていけば良い.以下例.

- hosts: openwrt
  remote_user: root
  gather_facts: no
  tasks:
  - name: update opkg
    raw: opkg update
  - name: install openssl-util
    raw: opkg install openssl-util

なおplaybookではなくコマンドラインで何らか実行する際にrawを指定するには -m raw というオプションを付けてやると良い.
こうするとPython無しでも動く……のだがAnsibleの旨味がごっそり抜けてしまう (せっかくのOpenWrtサポートを利用できない) ので,もう諦めてPythonを入れてしまったほうが楽かもしれない.

参照: Python が入っていない NW 機器も Ansible で (一応) 制御できる - Qiita

考察

Ansibleを使わないで,普通に環境構築済みのイメージを用意してROM焼いてしまえば良いのでは無いか? (とは言え設定ファイルみたいなやつはAnsibleで撒きたい,みたいなのはあります)

*1:>Occasionally users may be managing a remote system that doesn’t have SFTP enabled. If set to True, we can cause scp to be used to transfer remote files instead: