もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

PythonでPostgreSQLの通知を受け取る[NOTIFY/LISTEN]

PostgreSQLには、接続しているクライアントに通知をおくることが出来るらしい。

今まであるテーブルの全レコードのフラグを監視して、見つけたら取り出し→処理→フラグ更新ってことばかりをやっていたけど

これを「通知」に置き換えることができそう

これは便利

というわけで普段あまり使わないPythonの練習がてら、通知処理を使ってみた

やってみた環境

  • サーバ側
    1. CentOS6.8(仮想OS。IP:192.168.192.130)
    2. PostgreSQL9.5.4
      • DB名:testdb


  • クライアント側
    1. Windows10(仮想のホスト。IP:192.168.192.1)
    2. Python2.7.12

サーバの設定

/var/lib/pgsql/9.5/data/postgresql.confで受け付けるIPアドレスの変更

#listen_addresses = 'localhost'          # what IP address(es) to listen on;
↑確かこうなっているのを
↓変更
listen_addresses = '*'          # what IP address(es) to listen on;

/var/lib/pgsql/9.5/data/pg_hba.confに、ホストからのアクセスを許可

host    all             all             127.0.0.1/32            trust
host    all             all             192.168.192.1/24        trust
↑追記(パスワードなんて認証なんて無い。イイネ?)

通知を受け取る

PostgreSQLから通知を受け取るアプリをPythonで作る。

以下のサイトを参考に作成した More advanced topics — Psycopg 2.6.2 documentation

※例外処理は適当

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import psycopg2
import time
import select
import traceback


def create_connection():
    try:
        dsn = "host=192.168.192.130 port=5432 dbname=testdb user=postgres password=postgres"
        connection = psycopg2.connect(dsn)
        connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
        return connection
    except:
        traceback.print_exc()
        return None


def start_listen(connection):
    try:
        cursor = connection.cursor()
        cursor.execute('LISTEN test;')
        cursor.close()
        return True
    except:
        print "unable to notify listen"
        return False


if __name__ == '__main__':

    connection = None

    while True:
        try:
            if (connection is None) or (connection is not None and connection.closed):
                print "create connection. wait 5 sec"
                time.sleep(5)
                connection = create_connection()
                start_listen(connection)
            elif connection is not None and not connection.closed:
                print "wait notify 5 sec."
                if select.select([connection], [], [], 5) == ([], [], []):
                    print 'listen timeout'
                else:
                    connection.poll()
                    while connection.notifies:
                        notify = connection.notifies.pop(0)
                    print "Got NOTIFY:", notify.pid, notify.channel, notify.payload
        except:
            traceback.print_exc()
            if connection is not None and not connection.closed:
                connection.close()
            connection = None

ポイントは、トランザクションの分離レベルを「ISOLATION_LEVEL_AUTOCOMMIT」に設定しているところ

これをしてないと通知が受け取れなかった

psycopgのドキュメントに書いてあった内容

Because of the way sessions interact with notifications (see NOTIFY documentation), you should keep the connection in autocommit mode if you wish to receive or send notifications in a timely manner.

イムリーに通知の送受信をするためには「オートコミットモード」にしろ。(詳しくはPostgreSQLのNOTIFY読んでね)

これだけだと「オートコミット」じゃないといけないのかよくわからん

PostgreSQLのドキュメントも見ると、以下ことが書いてあった。

NOTIFYとSQLトランザクションの間には、いくつかの重要な相互作用があります。 まず、NOTIFYがトランザクション内部で実行された場合、通知イベントはトランザクションがコミットされない限り配送されません。 トランザクションがアボートされた場合、NOTIFYだけでなく、そのトランザクション内で行われたコマンドが全て無効化されるので、これは妥当といえます。 しかし、通知イベントが即座に配送されることを期待していた場合、当惑するかもしれません。 次に、監視中のセッションがトランザクション処理中に通知信号を受け取った場合、そのトランザクションが(コミットもしくはアボートされて)完了するまで、通知イベントは接続しているクライアントに配送されません。 この理由も同じです。トランザクションに通知が配送された後にそのトランザクションがアボートされた場合、何とかして通知を取り消したくなりますが、サーバはいったんクライアントに送信した通知を「取り戻す」ことはできません。 したがって、通知イベントはトランザクショントランザクションの合間にのみ配送されます。 結論として、NOTIFYを使用してシグナルの実時間処理をするアプリケーションではトランザクションを短くしておかなければなりません。

「通知イベントはトランザクショントランザクションの合間にのみ配送されます。」

これに対応するために「ISOLATION_LEVEL_AUTOCOMMIT」にしないと動作しないのだろうか?

通知を送信する

通知を送るのは、「psql」で可能

# psql -U postgres testdb
psql (9.5.4)
"help" でヘルプを表示します.

testdb=# notify test, 'this is payload∠( ゚д゚)/';

通知の受信結果

こんな感じ

wait notify 5 sec.
listen timeout
wait notify 5 sec.
listen timeout
wait notify 5 sec.
Got NOTIFY: 4953 test this is payload∠( ゚д゚)/
wait notify 5 sec.
Got NOTIFY: 4953 test this is payload∠( ゚д゚)/

参考サイト PostgreSQL: Documentation: 9.5: NOTIFY More advanced topics — Psycopg 2.6.2 documentation

【PHP】DateTimeクラスの月の計算がおかしいので対策した

PHPのDateTimeクラスで月の足し算をしていると、ちょっとおかしい挙動をする

具体的な挙動だと「3月31日」に1ヶ月足すと「5月1日」になる

予想では以下のような挙動をしている

  1. 「3月31日」に1ヶ月足し、「4月31日」になる。
  2. 「4月31日」は存在しないので、「4月30日 + 1日」になる。
  3. 「4月30日 + 1日」より、結果が「5月1日」になる

※この記事のPHPは諸事情により5.3系を使用

この挙動に対する対策を見つけたが、date関数を使用していた

loumo.jp

date関数は「2036年問題」があるので、そこまでアプリを使わないような気がするが、なるべく避けたい

なのでDateTimeクラスを継承して、対策したクラスを作成した

初版(まだ「バグ」が存在しているクラス)

<?php
/**
 * 標準クラス「DateTime」の月またぎ(1/31の1ヶ月後)の計算がおかしいので、
 * 月計算を独自拡張した
 */
class DateTimeEx extends DateTime
{

    /**
     * 月を加算
     *
     * @param int $monthNum
     * @throws InvalidArgumentException
     */
    public function addMonth($monthNum)
    {
        if (empty($monthNum) || $monthNum <= 0) {
            throw new InvalidArgumentException();
        }

        $tmp = clone $this;
        $tmp->modify("+{$monthNum} month");
        $interval = $tmp->diff($this, /*absolute=*/true);

        // 月の「勝手な繰り上げ」が起きた時、dプロパティに
        // 繰り上げられた日付が入ってる。(1日~2日)
        if ($interval->d) {
            $this->modify("last day of +{$monthNum} month");
        }
        else {
            $this->modify("+{$monthNum} month");
        }
    }


    /**
     * 月を減算
     *
     * @param int $monthNum
     * @throws InvalidArgumentException
     */
    public function subMonth($monthNum)
    {
        if (empty($monthNum) || $monthNum <= 0) {
            throw new InvalidArgumentException();
        }

        $tmp = clone $this;
        $tmp->modify("-{$monthNum} month");
        $interval = $this->diff($tmp, /*absolute=*/true);

        // 月の「勝手な繰り上げ」が起きた時、dプロパティに
        // 繰り上げられた日付が入ってる。(大体30日)
        if ($interval->d) {
            $this->modify("last day of -{$monthNum} month");
        }
        else {
            $this->modify("-{$monthNum} month");
        }
    }

}

動作確認

実際にアプリケーションで使ってみたらなんかオカシイ・・・

テストコード(自動ではない)を書いてみた

<?php
require_once 'DateTimeEx.php';

$test = array(
    array('date' => '2016/01/30 00:00:00', 'period' => 1),
    array('date' => '2016/01/30 01:00:00', 'period' => 1),
    array('date' => '2016/01/30 02:00:00', 'period' => 1),
    array('date' => '2016/01/30 07:00:00', 'period' => 1),
    array('date' => '2016/01/30 08:00:00', 'period' => 1),
    array('date' => '2016/01/30 08:58:00', 'period' => 1),
    array('date' => '2016/01/30 08:59:00', 'period' => 1),
    array('date' => '2016/01/30 08:59:59', 'period' => 1),
    array('date' => '2016/01/30 09:00:00', 'period' => 1),
    array('date' => '2016/01/30 09:01:00', 'period' => 1),
    array('date' => '2016/01/30 09:02:00', 'period' => 1),
    array('date' => '2016/01/30 10:00:00', 'period' => 1),
    array('date' => '2016/01/30 11:00:00', 'period' => 1),
    array('date' => '2016/01/30 12:00:00', 'period' => 1),
    array('date' => '2016/01/30 13:00:00', 'period' => 1),
    array('date' => '2016/01/30 23:00:00', 'period' => 1),
);

echo '<pre>';
foreach ($test as $value) {

    $date = new DateTimeEx($value['date']);
    $date->addMonth($value['period']);

    echo sprintf('%25s|%25s', $value['date'], $date->format('Y-m-d H:i:s')) . "\n";
    echo "----------------------------------------------------\n";
}
echo '</pre>';

また、減算は以下のコード

<?php
require_once 'DateTimeEx.php';

$test = array(
    array('date' => '2016/01/31 00:00:00', 'period' => 2),
    array('date' => '2016/01/31 01:00:00', 'period' => 2),
    array('date' => '2016/01/31 02:00:00', 'period' => 2),
    array('date' => '2016/01/31 07:00:00', 'period' => 2),
    array('date' => '2016/01/31 08:00:00', 'period' => 2),
    array('date' => '2016/01/31 08:58:00', 'period' => 2),
    array('date' => '2016/01/31 08:59:00', 'period' => 2),
    array('date' => '2016/01/31 08:59:59', 'period' => 2),
    array('date' => '2016/01/31 09:00:00', 'period' => 2),
    array('date' => '2016/01/31 09:01:00', 'period' => 2),
    array('date' => '2016/01/31 09:02:00', 'period' => 2),
    array('date' => '2016/01/31 10:00:00', 'period' => 2),
    array('date' => '2016/01/31 11:00:00', 'period' => 2),
    array('date' => '2016/01/31 12:00:00', 'period' => 2),
    array('date' => '2016/01/31 13:00:00', 'period' => 2),
    array('date' => '2016/01/31 23:00:00', 'period' => 2),
);

echo '<pre>';
foreach ($test as $value) {

    $date = new DateTimeEx($value['date']);
    $date->subMonth($value['period']);

    echo sprintf('%25s|%25s', $value['date'], $date->format('Y-m-d H:i:s')) . "\n";
    echo "----------------------------------------------------\n";
}
echo '</pre>';

加算結果

      2016/01/30 00:00:00|      2016-03-01 00:00:00
----------------------------------------------------
      2016/01/30 01:00:00|      2016-03-01 01:00:00
----------------------------------------------------
      2016/01/30 02:00:00|      2016-03-01 02:00:00
----------------------------------------------------
      2016/01/30 07:00:00|      2016-03-01 07:00:00
----------------------------------------------------
      2016/01/30 08:00:00|      2016-03-01 08:00:00
----------------------------------------------------
      2016/01/30 08:58:00|      2016-03-01 08:58:00
----------------------------------------------------
      2016/01/30 08:59:00|      2016-03-01 08:59:00
----------------------------------------------------
      2016/01/30 08:59:59|      2016-03-01 08:59:59
----------------------------------------------------
      2016/01/30 09:00:00|      2016-02-29 09:00:00
----------------------------------------------------
      2016/01/30 09:01:00|      2016-02-29 09:01:00
----------------------------------------------------
      2016/01/30 09:02:00|      2016-02-29 09:02:00
----------------------------------------------------
      2016/01/30 10:00:00|      2016-02-29 10:00:00
----------------------------------------------------
      2016/01/30 11:00:00|      2016-02-29 11:00:00
----------------------------------------------------
      2016/01/30 12:00:00|      2016-02-29 12:00:00
----------------------------------------------------
      2016/01/30 13:00:00|      2016-02-29 13:00:00
----------------------------------------------------
      2016/01/30 23:00:00|      2016-02-29 23:00:00
----------------------------------------------------

減算結果

      2016/01/31 00:00:00|      2015-12-01 00:00:00
----------------------------------------------------
      2016/01/31 01:00:00|      2015-12-01 01:00:00
----------------------------------------------------
      2016/01/31 02:00:00|      2015-12-01 02:00:00
----------------------------------------------------
      2016/01/31 07:00:00|      2015-12-01 07:00:00
----------------------------------------------------
      2016/01/31 08:00:00|      2015-12-01 08:00:00
----------------------------------------------------
      2016/01/31 08:58:00|      2015-12-01 08:58:00
----------------------------------------------------
      2016/01/31 08:59:00|      2015-12-01 08:59:00
----------------------------------------------------
      2016/01/31 08:59:59|      2015-12-01 08:59:59
----------------------------------------------------
      2016/01/31 09:00:00|      2015-11-30 09:00:00
----------------------------------------------------
      2016/01/31 09:01:00|      2015-11-30 09:01:00
----------------------------------------------------
      2016/01/31 09:02:00|      2015-11-30 09:02:00
----------------------------------------------------
      2016/01/31 10:00:00|      2015-11-30 10:00:00
----------------------------------------------------
      2016/01/31 11:00:00|      2015-11-30 11:00:00
----------------------------------------------------
      2016/01/31 12:00:00|      2015-11-30 12:00:00
----------------------------------------------------
      2016/01/31 13:00:00|      2015-11-30 13:00:00
----------------------------------------------------
      2016/01/31 23:00:00|      2015-11-30 23:00:00
----------------------------------------------------

9時で挙動が変わる

デバッガでDateTimeのdiffメソッドの戻り値(DateIntervalクラス)を確認してみた



0-8時のdiffメソッドの戻り値(オブジェクト)

y : 0
m : 1
d : 0
h : 0
i : 0
s : 0
invert : 0
days : 31

0-8時以降のdiffメソッドの戻り値(オブジェクト)

y : 0
m : 1
d : 2
h : 0
i : 0
s : 0
invert : 0
days : 31



「勝手な月の繰り上げ」の判定に使用しているdプロパティの格納値が時間によって違う

タイムゾーンの影響っぽいけど、php.iniでは「Asia/Tokyo」設定してるし、デバッガでオブジェクトの中にも「Asia/Tokyo」が設定されてる

解せない(´ε` )

PHPの不具合なんだろうか・・・?(5.3以降は変わっているかも)

仕方ないので改良版を作成

完成版

<?php
/**
 * 標準クラス「DateTime」の月またぎ(1/31の1ヶ月後)の計算がおかしいので、
 * 月計算を独自拡張した
 */
class DateTimeEx extends DateTime
{

    /**
     * 月を加算
     *
     * @param int $monthNum
     * @throws InvalidArgumentException
     */
    public function addMonth($monthNum)
    {
        if (empty($monthNum) || $monthNum <= 0) {
            throw new InvalidArgumentException();
        }

        $tmp = clone $this;

        // 0時~8時とそれ以外の間で、diffメソッドの結果が異なる。(PHPのバグかも)
        // そのため、0時~8時の間で「勝手な月の繰り上げ」がDateIntervalでは判別できないため、
        // 時刻を別変数に保存しておき、「9時」に設定し、diffメソッドを使用する
        $time = array();
        $terget = $this->format('H:i:s');
        preg_match('/^(?P<hour>\d+):(?P<minute>\d+):(?P<second>\d+)$/', $terget, $time);
        $tmp->setTime(9, 0, 0);
        $this->setTime(9, 0, 0);

        $tmp->modify("+{$monthNum} month");
        $interval = $tmp->diff($this, /*absolute=*/true);

        // 月の「勝手な繰り上げ」が起きた時、dプロパティに
        // 繰り上げられた日付が入ってる。(1日~2日)
        if ($interval->d) {
            $this->modify("last day of +{$monthNum} month");
        }
        else {
            $this->modify("+{$monthNum} month");
        }

        // 計算用に設定しておいた時刻を元に戻す。
        $this->setTime((int)$time['hour'], (int)$time['minute'], (int)$time['second']);
    }


    /**
     * 月を減算
     *
     * @param int $monthNum
     * @throws InvalidArgumentException
     */
    public function subMonth($monthNum)
    {
        if (empty($monthNum) || $monthNum <= 0) {
            throw new InvalidArgumentException();
        }

        $tmp = clone $this;

        // 0時~8時とそれ以外の間で、diffメソッドの結果が異なる。(PHPのバグかも)
        // そのため、0時~8時の間で「勝手な月の繰り上げ」がDateIntervalでは判別できないため、
        // 時刻を別変数に保存しておき、「9時」に設定し、diffメソッドを使用する
        $time = array();
        $terget = $this->format('H:i:s');
        preg_match('/^(?P<hour>\d+):(?P<minute>\d+):(?P<second>\d+)$/', $terget, $time);
        $tmp->setTime(9, 0, 0);
        $this->setTime(9, 0, 0);

        $tmp->modify("-{$monthNum} month");
        $interval = $this->diff($tmp, /*absolute=*/true);

        // 月の「勝手な繰り上げ」が起きた時、dプロパティに
        // 繰り上げられた日付が入ってる。(大体30日)
        if ($interval->d) {
            $this->modify("last day of -{$monthNum} month");
        }
        else {
            $this->modify("-{$monthNum} month");
        }

        // 計算用に設定しておいた時刻を元に戻す
        $this->setTime((int)$time['hour'], (int)$time['minute'], (int)$time['second']);
    }

}

加算結果

      2016/01/30 00:00:00|      2016-02-29 00:00:00
----------------------------------------------------
      2016/01/30 01:00:00|      2016-02-29 01:00:00
----------------------------------------------------
      2016/01/30 02:00:00|      2016-02-29 02:00:00
----------------------------------------------------
      2016/01/30 07:00:00|      2016-02-29 07:00:00
----------------------------------------------------
      2016/01/30 08:00:00|      2016-02-29 08:00:00
----------------------------------------------------
      2016/01/30 08:58:00|      2016-02-29 08:58:00
----------------------------------------------------
      2016/01/30 08:59:00|      2016-02-29 08:59:00
----------------------------------------------------
      2016/01/30 08:59:59|      2016-02-29 08:59:59
----------------------------------------------------
      2016/01/30 09:00:00|      2016-02-29 09:00:00
----------------------------------------------------
      2016/01/30 09:01:00|      2016-02-29 09:01:00
----------------------------------------------------
      2016/01/30 09:02:00|      2016-02-29 09:02:00
----------------------------------------------------
      2016/01/30 10:00:00|      2016-02-29 10:00:00
----------------------------------------------------
      2016/01/30 11:00:00|      2016-02-29 11:00:00
----------------------------------------------------
      2016/01/30 12:00:00|      2016-02-29 12:00:00
----------------------------------------------------
      2016/01/30 13:00:00|      2016-02-29 13:00:00
----------------------------------------------------
      2016/01/30 23:00:00|      2016-02-29 23:00:00
----------------------------------------------------

減算結果

      2016/01/30 00:00:00|      2015-11-30 00:00:00
----------------------------------------------------
      2016/01/30 01:00:00|      2015-11-30 01:00:00
----------------------------------------------------
      2016/01/30 02:00:00|      2015-11-30 02:00:00
----------------------------------------------------
      2016/01/30 07:00:00|      2015-11-30 07:00:00
----------------------------------------------------
      2016/01/30 08:00:00|      2015-11-30 08:00:00
----------------------------------------------------
      2016/01/30 08:58:00|      2015-11-30 08:58:00
----------------------------------------------------
      2016/01/30 08:59:00|      2015-11-30 08:59:00
----------------------------------------------------
      2016/01/30 08:59:59|      2015-11-30 08:59:59
----------------------------------------------------
      2016/01/30 09:00:00|      2015-11-30 09:00:00
----------------------------------------------------
      2016/01/30 09:01:00|      2015-11-30 09:01:00
----------------------------------------------------
      2016/01/30 09:02:00|      2015-11-30 09:02:00
----------------------------------------------------
      2016/01/30 10:00:00|      2015-11-30 10:00:00
----------------------------------------------------
      2016/01/30 11:00:00|      2015-11-30 11:00:00
----------------------------------------------------
      2016/01/30 12:00:00|      2015-11-30 12:00:00
----------------------------------------------------
      2016/01/30 13:00:00|      2015-11-30 13:00:00
----------------------------------------------------
      2016/01/30 23:00:00|      2015-11-30 23:00:00
----------------------------------------------------

最近のPHPで解決してるといいなー

参考サイト

koyhogetech.hatenablog.com

Apacheでcss・js等除くAjaxのみキャッシュを無効にする

とあるWEBアプリでAjax周りで不具合が出た

調べてみるとよくあるIEだけの現象

ChromeFirefoxでは問題ないが、IEだけAjaxのキャッシュが使われ、データが古いまま

GETクエリに時刻文字列のようなユニークキーを追加したり、Angularの設定で「If-Modified-Since」を明示的に設定してもIEさんのキャッシュは頑張ってくれていた・・・

(╬⓪益⓪)なんでじゃー

Javascriptで頑張るのは諦めて、以下のサイトを参考に、Apacheの設定を追加してキャッシュを無効化した

ameblo.jp

↓その時の設定

FileEtag None
RequestHeader unset If-Modified-Since
Header set Cache-Control no-store

この設定の場合、すべてに対してキャッシュ無効にする

css・js・png等はキャッシュしてほしい・・・

css・js・png等以外はキャッシュしない」ようにApacheの設定を色々試したみたがうまく動作しなかった


失敗その1

<FilesMatch "^\.(js|pdf|ico|gif|jpe?g|png|css|html|xml)$"> 
  FileEtag None
  RequestHeader unset If-Modified-Since
  Header set Cache-Control no-store
</FilesMatch>


失敗その2

<FilesMatch "\.(?!js|pdf|ico|gif|jpe?g|png|css|html|xml)$"> 
  FileEtag None
  RequestHeader unset If-Modified-Since
  Header set Cache-Control no-store
</FilesMatch>


失敗その1・失敗その2で「css」だけ除外して、「css」が毎回問い合わせされるかfiddlerで確認したところ、期待通りの動作にならなかった・・・

アプローチを変えて、とりあえず問答無用でキャッシュを無効化し、css・js・png等のファイルのアクセスの場合、再度キャッシュを設定するようにした

その時の設定はコチラ↓

FileEtag None
<IfModule mod_headers.c>
  RequestHeader unset If-Modified-Since
  Header set Cache-Control no-store

  <FilesMatch "\.(js|pdf|ico|gif|jpe?g|png|css|html|xml)$"> 
    Header set Cache-Control "max-age=86400"
  </FilesMatch>
</IfModule>


「<IfModule mod_headers.c>」は「モジュールが無い!」で怒られたくないので保険のため追記

FilesMatchで、静的ファイルのアクセスの場合、キャッシュの再設定を行っている

動作確認でcssを削除したところ、cssだけ毎回問い合わせされているのは確認できた

不具合も出てないみたいなので、これでいいのだー(多分)

ただ、正規表現の否定でうまく設定できないのが解せない・・・

書き方が悪いのかしら?