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

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

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:

OpenWrtの設定を強制的に初期状態に戻す (failsafe modeに入る)

OpenWrtでうっかりLANのInterfaceの設定をしくじってしまったりすると,Web UIに入れなくなってしまい (もちろんsshもできない) その後の設定が一切不能になって詰む事があります.そういう時に工場出荷時 (という表現が正しいのかどうかはわかりませんが) の状態に初期化する際の手順について記します.

ずばりこのStackOverflowなわけですが;

stackoverflow.com

しかし下記のバグトラッカーに記されているように,Chaos Calmer 15.05だと firstboot コマンドが上手く動かない様子.

#20168 (CC - firstboot is broken) – OpenWrt

というわけでChaos Calmer 15.05の場合は以下のような手順を踏むと良い.

  1. ルータの電源を切る
  2. WANポートにつながっているケーブルを外す
  3. マシンのNICIPアドレス192.168.1.2/24 に設定し,デフォルトゲートウェイ192.168.1.1 に設定する
  4. マシンとルータを有線で繋ぐ
  5. ルータに電源を入れる
  6. 赤いLEDが点滅しはじめたらルータのリセットボタンを押す (長押し?)
  7. 赤いLEDの点滅が高速になったらfailsafe modeに入っているということなので telnet 192.168.1.1 でルータに入って以下のコマンドを流し込む
mount_root ## ファイルを読み書き出来るようにマウントしなおす
mtd -r erase rootfs_data  ## firstbootとreboot -fの代替.しばらく待つと自動で再起動される

最初,WZR-HP-AG300Hのリセットボタンの位置が分からなくて困ったので取扱説明書は重要であるということがわかりました (ボタンは底面にあった).

なんらかの設定ミスでにっちもさっちも行かなくなっている場合は,初期化までしなくてもfailsafe modeのtelnetの中で設定ファイルを直せば済むという感じもあります.


failsafe modeの公式ドキュメント: OpenWrt Failsafe [OpenWrt Wiki]

WZR-HP-AG300HにOpenWrtを焼く

BUFFALOの無線LANルータであるところのWZR-HP-AG300HにOpenWrtを焼いて利用する際のメモです.なおマシンのOSはmacOSです.Windows等だとちょっと違うかもしれないが概ね問題ないはず.

buffalo.jp

AG300HとマシンをLANケーブルで繋ぐ

  • 適切なケーブルで繋ぐ
  • マシンのNICIPアドレス192.168.11.2/24 に固定する
  • 192.168.11.1 にアクセスするとBUFFALOの設定Web UIにアクセスできる

参照: インターネット接続のためブラウザーを開き、ユーザー名に「root」と入力すると認証エラーになります(WHR-G300N、WZR-HP-G300NH、WZR-AGL300NH、WZR2-G300N) - アンサー詳細 | BUFFALO バッファロー

AG300HのBUFFALO純正ファームを更新する

必要なさそうだが,念のためにやっておく.
Web UIのオンラインバージョンアップ機能を利用して1.75 (2017/05/07時点の最新バージョン) にファームのバージョンを上げる.別途ファームのバイナリをあらかじめダウンロードしておき,そのファイルを利用するのでも問題ない.

OpenWrtを焼く

https://downloads.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/openwrt-15.05.1-ar71xx-generic-wzr-hp-ag300h-squashfs-factory.bin

2017/05/07時点での最新バージョンであるところの,Chaos Calmer (15.05.1) を利用する.

純正ファームのAG300Hの為のOpenWrtバイナリがあるのでそれをダウンロードしてきて,Web UIのファーム更新ページ経由でそのバイナリを指定してOpenWrtを焼く.なお factory.bin というファイル名は「純正ファームからOpenWrtを焼く時に使用する」という意味らしい.

なおこの時点ではWi-Fiは無効になっているので,Wi-Fiを有効にする場合は引き続き有線で設定する必要がある.

OpenWrtのWeb UIであるところのluciに繋ぐ

BUFFALOのファームのときは 192.168.11.1 でWeb UIに繋いでいたが,OpenWrtでは192.168.1.1 がWeb UIのIP Addrとなるのでそちらに繋ぐ.適宜マシンのNICIPアドレスを固定する必要がある (場合がある).
Web UIは初期状態だとユーザ名root,パスワード空でログインができる.
ログインしたら適当にOpenWrtのパスワードなり,SSHの設定なり,Wi-Fiの有効化なりをやる必要がある.

メモ

FLETSのONUの出力をAG300HのWANに繋いでいると,192.168.1.1/24 を食い合うっぽいので,適宜LANの静的IPアドレスの設定で 192.168.11.1/24 なりに変更しておく必要がありそう.
というか面倒なのでAG300Hの設定を行っている時は,色々めんどいのでWANに繋がない方が楽な感じがした.

雑感

インターネットを眺めているとWZR-HP-AG300Hは純正ファームだと不安定だけど,OpenWrtにすると安定運用できる様子が伺える.
AG300HでOpenWrtを利用する際の情報もやけに充実していて便利 (Buffalo WZR-HP-AG300H [OpenWrt Wiki]).OpenWrtの為に登場した無線LANルータであるようにすら思える.
今回AG300Hをメルカリで買ったんだけど,中古だとだいたい2000円くらいで買えてお手軽.安い時だと1000円代で購入できることもあるのでどんどん買っていきたい.

MyBatis + GroovyでMapperを作っている時に良い感じでWHERE IN使いたいんですけど〜って時

MyBatisのMapperをGroovyのannotationを使って書くと何かと便利 (主に「XMLを書かなくても良い」という点で便利) なわけですが,そんな中で「WHERE IN」を利用したSELECTを @Select annotationベースでどうやって書くのかという件です.要は SELECT * FROM users WHERE id IN (?, ... , ?) のようにプレースホルダを利用した感じのクエリを書きたい.SQLインジェクションを防ぎたいので,その辺はORMの機構に任せてしまいたいのです.

結論から書くと,MyBatis XMLマクロの foreachを使うのがシンプルな様子.

@Select("""
<script>
  SELECT *
  FROM users
  WHERE user_id IN
  <foreach item='userId' collection='userIds' open='(' separator=',' close=')'>
    #{userId}
  </foreach>
</script>
""")
List<User> findUsersByIds(@Param('userIds') List<Long> userIds)

結局のところXMLを書く必要があるのだ!!!! (XMLを使うことでシンプルに済ませることが出来るのであればXMLを使ったら良いという事がわかりました).

Official doc:
http://www.mybatis.org/mybatis-3/dynamic-sql.html