モヒカンメモ

髪色が定期的に変わることに定評のある(比較的)若者Webエンジニアの備忘録

`set -e` していても `command1 || command2` みたいな書き方ができる #些細な検証メモ

f:id:pinkumohikan:20170611154955p:plain

ことのはじまり

シェルスクリプトを書くときに set -e しますよね

これをしておくとシェルスクリプトの途中でエラーが発生した場合に処理を中断してくれるので、ある処理が失敗したあと、意図せず後続の処理が動いて悲劇が起こるのを防いてくれます

ところが先日、 「set -eしちゃうと、command1 || command2 みたいな書き方が出来なくてアレ」 みたいな記事を目にしたので、そんなことあらへんやろと検証してみました (100%の自信はなかったので、念のためにw)

結論

set -e していても command1 || command2 みたいな書き方ができる

検証

下記のようなそれっぽいスクリプトを用意して

set -ex

echo 'hoge'

# ここで中断されずに `echo 'yay'` が実行されて欲しい
mkdir /home || echo 'yay'

# ここで非ゼロexit codeとなるので、中断されるはず
mkdir /tmp

# ここまで行くと行き過ぎ
echo 'Error! Should not be call this line!' && exit 1

動かすと、、、

$ bash hoge.sh
+ echo hoge
hoge
+ mkdir /home
mkdir: cannot create directory ‘/home’: File exists
+ echo yay
yay
+ mkdir /tmp
mkdir: cannot create directory ‘/tmp’: File exists

最初のmkdirに失敗しても、or実行しているechoに成功しているので 中断されていません もちろん、2度目のmkdirでは普通に非ゼロのexitとなったので中断されました

上記の検証で、 set -eしている状態で前者のor演算が失敗しても処理は中断されない ことがハッキリしたので、set -e していても臆せず command1 || command2 みたいな書き方ができますね

アイキャッチ画像は いらすとや からお借りしました

PHPでマジックメソッド __toString 内で例外を投げるとfatalになる #PHPの不思議な世界

f:id:pinkumohikan:20170611154720p:plain

はじめに

PHPは素敵な言語です。

※ 「素敵」という表現は主観的なものであり、何を素敵と思うかは個々の自由である。そのため異論は一切認めない。

この #PHPの不思議な世界 という謎のタグの付いている記事は、職業プログラマーとしてPHPコードを書いていて「ふぇー」と思った仕様や、「ふーん」と思った仕様についてダラダラとコードと感想を書いていくコーナーです。 PHPをdisる意図は決してなく、日頃ご飯を食べさせて頂いているPHP様には足を向けて寝ることはできません。

お題

マジックメソッド __toString 内で例外を投げるとfatalになる

前説

PHPには、マジックメソッドと呼ばれる仕組みが存在します。

特定の条件が満たされた時に、明示的にcallしなくても勝手に規定のメソッドがcallされます。 メソッド名の先頭がアンダースコア二つ __ で始まっているのが特徴です。

PHP: マジックメソッド - Manual

解説

マジックメソッド toString は、文字列化メソッドです。 文字列が期待されている関数やメソッドに対してオブジェクトを渡した際に、 toString が呼ばれます。

スキームとドメイン、パスしかないシンプルなURLクラスを考えます。

class Url
{
    private $scheme;
    private $domain;
    private $path;

    public function __construct($scheme, $domain, $path)
    {
        $this->scheme = $scheme;
        $this->domain = $domain;
        $this->path = $path;
    }

    public function __toString()
    {
        return sprintf(
            '%s://%s/%s',
            $this->scheme,
            $this->domain,
            $this->path
        );
    }
}

上記クラスをnewしてインスタンスを生成し、引数として文字列を期待する関数に渡してあげると、 __toString メソッドで定義した方法で文字列化して出力されます。

$ php -a
Interactive shell

php > print PHP_VERSION;
5.6.29
php >
php > require_once 'Url.php';
php > $url = new Url('https', 'pinkumohikan.com', 'feed');
php > print $url;
https://pinkumohikan.com/feed

本題

一言で言うと表題の通りで、「マジックメソッド __toString 内で例外を投げると、fatal errorになります」。

※ __toString はインスタンスが保持しているデータを文字列化する方法を定義するためのマジックメソッドなので、例外を投げたくなることは普通はありません。というかそういう設計をしてはいけません。振りではありません。

試しに、先ほどのUrlクラスの __toString メソッドを、仕事をせずに例外を投げるだけのゴミみたいな実装に書き換えたFreedomUrlクラスを用意します。

class FreedomUrl
{
    private $scheme;
    private $domain;
    private $path;

    public function __construct($scheme, $domain, $path)
    {
        $this->scheme = $scheme;
        $this->domain = $domain;
        $this->path = $path;
    }

    public function __toString()
    {
        throw new \RuntimeException('I am freedom!!!!');
    }
}

実行してみます。

$ php -a
Interactive shell

php > print PHP_VERSION;
5.6.29
php >
php > require_once 'FreedomUrl.php';
php > $url = new FreedomUrl('https', 'pinkumohikan.com', 'feed');
php > print $url;
PHP Fatal error:  Method FreedomUrl::__toString() must not throw an exception in php shell code on line 0
PHP Stack trace:
PHP   1. {main}() php shell code:0

Method FreedomUrl::__toString() must not throw an exception と、非常にお怒りです。

上位でtry ~ catch構文で拾おうとしてもダメです。 see __toString()でExceptionが発生すると死ぬ - Qiita

このことはPHPリファレンスの __toString メソッドの説明 にも、「絶対例外投げんなよ!絶対だぞ!」と書かれています。

警告 __toString() メソッド内から例外を投げることはできません。そうした場合、致命的なエラーが発生します。

こういうエラーを見たら、おそらく __toString メソッドが「オブジェクトが持つデータの文字列化」以上の責務を負っていると思われるので、是非設計を見直してあげて下さい。

MySQLのtimestamp型カラムにUTC1970年1月1日 00:00:00よりも前の日時を入れようとしてハマった

f:id:pinkumohikan:20170611145446p:plain

timestamp=0よりも前の日時をinsertしようとして怒られたときの備忘録

できごと

いつものように怪しいWebアプリ作りに勤しんでいたら、とある怪しいWebアプリがエラーを吐いていた

Next Illuminate\Database\QueryException: SQLSTATE[22007]: Invalid datetime format: 1292 Incorrect datetime value: '1970-01-01 07:30:17' for column 'xxx_at' at row 1 (SQL: xxx) in /web/xxx/vendor/laravel/framework/src/Illuminate/Database/Connection.php:770

データベースへレコードをinsertしようとしたときにエラーが発生し、LaravelのDBライブラリから例外が投げられてバッチがお亡くなりになっている模様

こうしてブログネタが見つかった、めでたしめでたし (w)

デバッグする

最初は Invalid datetime format と言われているので、バグか何かで日付として不正な値をinsertしようとしてエラーになったのかな?と思ってデータを確認した

1970-01-01 07:30:17

が、日付をよく見ても特に気になるところは無し 問題のない Y-m-d H:i:s 形式 (通称: 山田形式 )。ちゃんとパースできるはず

次に気になるのが 1970-01-01 と言う部分

ピンと来ました? Unixtimeが始まった日です

日時まで見て見ると

1970-01-01 07:30:17

なるほど07:30、 すき家で朝食メニューが頼める時間

原因は完全にコレで、

php > print date_default_timezone_get();
Asia/Tokyo
php > print date('Y-m-d H:i:s', 0);
1970-01-01 09:00:00

UNIX Timestamp 0 = JST 1970-01-01 09:00:00

つまり、 JST 1970-01-01 09:00:00よりも前はUNIX Timestampでは表現できない

とても当たり前だが、普段はJSTはあまり考えないので失念していた

対応を考える

テーブルには 1970-01-01 07:30:17 と言う日付をinsertしたいのに、カラム型の制約で Invalid datetime format と言われてしまう

パッと思いつく対応としては、

  1. 1/1 9時よりも前の日時を、一律9時きっかりに丸める
  2. 1/1 9時よりも前の日時を扱えるカラム型に変更する

といったもので、それぞれ

1: ビジネス的にそれが許されるなら、と言う事前条件がある 2: (ディスク容量的な意味で)データ量が増える

という難点がある

数千万件、数億件オーダのデータを扱うなら4バイト -> 8バイトへの変更は気になるだろうが、今回の場合は高々数百万件程度なので気にせずdatetime型へ変えてしまう

補足

今回リファレンスをちゃんと読むまで知らなかったのだが、timestamp型、datetime型カラムのデータ保持に必要なディスク容量が MySQL 5.6.4を境に 変わっていたΣ

詳しくはsee MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.7 データ型のストレージ要件

対応する

問題のカラムをtimestamp型からdatetime型へ変更する

alter table some_table modify xxx_at datetime not null;
  • Laravel Migration Fileだと
Schema::table('some_table', function (Blueprint $table) {
    $table->datetime('xxx_at')->change();
});

今回のアプリを動かしているデータベースサーバはMySQL5.6以上なので、オンラインで上記DDLを実行しても他からの読み書きはブロックされない

see MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.11.1 オンライン DDL の概要

5.6未満のMySQLを運用中で人生が辛いですか? We’re hiring!

まとめ

  • JST 1970-01-01 09:00:00よりも前の日時はtimestamp型では扱えない
    • 1970年1月1日前後の日付でエラーが出たら、timestamp型問題を疑ってみる
  • どうしても扱いたかったらtimestamp型ではなくdatetime型を使う
  • カラムの型変更はMySQL5.6以上ならノンブロッキングに実行可能

余談

本文中では触れていませんがデバッグの過程で、テーブルの型定義とアプリから入れようとしている日付のフォーマットが違っていて怒られているのかと思って調べていたら、自称エンジニアな方が

そういう時は set SQL_MODE = ''; すればいいんだよ!

って言っているのをいくつか見かけて、とても強く生きたい気持ちになった

一方で、SQLモードについて詳しく解説してくださっている記事も見つけて、MySQLについて学びがあった

MySQLの自動変換を丁重にお断りするためのたった1種類の呪文 - sakaikの日々雑感~(T)編

感謝++

WordPressの予約投稿が仕事しなかったのでcronの設定をした

WordPressの予約投稿がきちんと動かずに、「投稿失敗」してしまったのでその原因調査と対応の備忘録を残しておく

できごと

WordPressでブログを構築して翌日公開の予約投稿を設定したが、時間になっても公開されなかった (記事が公開されるとJetPackのパブリサイズ機能によってSNSに記事公開の通知がされるはずだが、所定時間を過ぎても通知されていなかった)。

なぜだろうと思い、ブログにアクセスしたところ予約投稿が公開された (SNSへ記事公開の通知がされた)

原因調査

「wordpress 予約投稿 失敗」みたいなキーワードでググって色々調べた結果、デフォルトのWordPressの設定だと、

ブログへのアクセスをトリガーに

  1. 予約投稿チェック
  2. 公開時刻を過ぎている記事があればそれを公開

という動きをする模様 (コードレベルではまだ追っていない)

今回の場合は、投稿が公開される時間帯にアクセスが全然無かったため、所定時間を過ぎても公開されなかった、と言うのが原因のよう

確かにそれだったら

なぜだろうと思い、ブログにアクセスしたところ予約投稿が公開された」

という事象にも説明がつく

レンタルサーバとかで動かすことを考えたらcronが使えない環境でも動く必要があって、それにはアクセスをトリガーにするしかないよな、と設計的にも理解できる

ただ、これはこれでアクセスのたびに予約投稿があるかをDBに問い合わせているはずで、それで発生するオーバーヘッドはブログへのアクセス数が大きくなっていくにつれて無視出来ない大きさになるのでは?という疑念が生まれる

対応

結論としては アクセスをトリガーにしない設定を追加 + cronでwp-cron.phpを定期実行させる という対応をした

それによって、アクセスをトリガーに動かなくなるのでアクセスのたびに余計なオーバヘッドが乗ることもないし、アクセスがないから予約投稿が公開されない、、、と言う悲劇も起こらない (はず)

1. wp-config.phpへ設定

wp-config.phpの最下部あたりに、下記を追加する

define('DISABLE_WP_CRON', 'true');

これで、アクセスをトリガーに動かなくなる (はず)

2. crontabへ設定

cron = 指定した時間 or 一定周期で自動的にプログラムを動かす仕組み crontab = cronの設定

crontab -e でcrontab設定ファイルを開き、下記を追加

* * * * * curl -sS https://pinkumohikan.com/wp-cron.php

上記の意味としては、毎分curlと言うコマンドを使って https://pinkumohikan.com/wp-cron.php に対してアクセスするよ、って感じ ローカルなんだから直接 php /path/to/wp/wp-cron.php を叩けばいいじゃんっていうのも分かるけど、実行ユーザとディレクトリ構成に強く依存しちゃうのが好みじゃないので今回はこれで。

参考資料

  1. 意外と知らない、WordPressの正しいcronの設定方法|ある蜜柑の上にアルミ缶。
  2. WordPressの予約投稿が失敗した時に試す4つの解決策 | OXY NOTES