iOSデバイスにプッシュ通知をする仕組みとして Apple Push Notifications (通称APNs) というものが用意されています。これを利用するためのライブラリを作成しました。
https://github.com/najeira/pyapns
使い方は上記のリポジトリのREADMEやソースを読んでください。
作る前にGithubやPyPIでPythonのライブラリを探してみたのですが、エラーハンドリングをちゃんとしているライブラリが見当たらなかったため作りました。
なぜライブラリでエラーをちゃんと処理しているものが見当たらなかったかというと、APNsの仕様が関係していると思います。
APNsによるプッシュ通信は、バイナリプロトコルのメッセージを、TCPで順次送っていくようになっています。
そして、エラーが返ってこなかったら成功、エラーが返ってきたらエラーという仕様になっています。つまりエラーが返ってきていない時に「成功したからエラーが返ってこない」のか「まだエラーが返ってきていない」のか区別が出来ません。それともちろん非同期です。
例えばHTTPのような1リクエスト対1レスポンスの仕組みだったら楽なのですが……。
仮にAPNsのための同期APIを実装しようとすると、ソケットに対してWriteしたあとに1秒ほど待ってからReadしてみて、エラーが返ってきていなければ成功とするような実装になってしまいます。これだと1回のAPIの呼び出しに1秒以上かかってしまいます。とても大量のプッシュ通知をすることはできません。
あるいはエラー応答を無視すれば、待ち時間なく大量にプッシュ通知をできるので、これで解決……と思いきや、そうはうまく行きません。というのは、APNsではエラーが発生すると、それ以降のリクエストもすべて失敗します。よってエラーを無視すると、どこまで成功して、どこから失敗したか分からなくなります。
他にも1メッセージごとに接続と切断を繰り返す実装にすると、エラーが後続に波及しないので、うまくいくかに思えますが、接続はSSLなのでオーバーヘッドが大きくて難しい。
そういうわけで、ソケットに対してWriteでメッセージを送りつつ、向こうからの応答があればReadしてエラーを検知し、どのメッセージがエラーだったのかを遡って判定して、エラー箇所の次のメッセージからリトライする必要があります。面倒ですね。
AppleはKeep−AliveなHTTPでのAPIを提供すればいいのにと、つくづく思いました。
najeira
2014年3月17日
2014年3月6日
GrowthForecastインストール手順のメモ
# GrowthForecast
see: http://kazeburo.github.io/GrowthForecast/
## yum
```
sudo yum update
sudo yum install pkgconfig glib2-devel gettext libxml2-devel pango-devel cairo-devel
sudo yum groupinstall "Development Tools"
```
## useradd
add a user
```
sudo useradd growthforecast
```
change user
```
sudo su - growthforecast
```
## perlbrew
install perlbrew
```
curl -kL http://install.perlbrew.pl | bash
echo '[[ -s "$HOME/perl5/perlbrew/etc/bashrc" ]] && source "$HOME/perl5/perlbrew/etc/bashrc"' >> .bash_profile
source $HOME/perl5/perlbrew/etc/bashrc
```
install perl
```
perlbrew available
perlbrew install perl-5.18.2
perlbrew switch perl-5.18.2
```
## cpanm
install cpanm
```
perlbrew install-cpanm
```
## growthforecast
install GrowthForecast
```
cpanm -n GrowthForecast
```
make a directory for data
```
mkdir /media/ephemeral0/growthforecast
```
## supervisor
install supervisord
```
sudo yum --enablerepo=epel install supervisor
```
/etc/supervisord.conf
```
[supervisord]
http_port=/var/tmp/supervisor.sock ; (default is to run a UNIX domain socket server)
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (logging level;default info; others: debug,warn)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
[supervisorctl]
serverurl=unix:///var/tmp/supervisor.sock ; use a unix:// URL for a unix socket
[program:growthforecast]
command=/home/growthforecast/perl5/perlbrew/perls/perl-5.18.2/bin/perl
/home/growthforecast/perl5/perlbrew/perls/perl-5.18.2/bin/growthforecast.pl
--data-dir /media/ephemeral0/growthforecast
--front-proxy=127.0.0.1
process_name=%(program_name)s
directory=/home/growthforecast
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
user=growthforecast
autostart=true
autorestart=true
```
make a directory for logging
```
sudo mkdir /var/log/supervisor
```
add superversord to upstart
/etc/init/supervisord.conf
```
description "supervisord"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
exec /usr/bin/supervisord -n
```
start supervisord via upstart
```
initctl start supervisord
```
see: http://kazeburo.github.io/GrowthForecast/
## yum
```
sudo yum update
sudo yum install pkgconfig glib2-devel gettext libxml2-devel pango-devel cairo-devel
sudo yum groupinstall "Development Tools"
```
## useradd
add a user
```
sudo useradd growthforecast
```
change user
```
sudo su - growthforecast
```
## perlbrew
install perlbrew
```
curl -kL http://install.perlbrew.pl | bash
echo '[[ -s "$HOME/perl5/perlbrew/etc/bashrc" ]] && source "$HOME/perl5/perlbrew/etc/bashrc"' >> .bash_profile
source $HOME/perl5/perlbrew/etc/bashrc
```
install perl
```
perlbrew available
perlbrew install perl-5.18.2
perlbrew switch perl-5.18.2
```
## cpanm
install cpanm
```
perlbrew install-cpanm
```
## growthforecast
install GrowthForecast
```
cpanm -n GrowthForecast
```
make a directory for data
```
mkdir /media/ephemeral0/growthforecast
```
## supervisor
install supervisord
```
sudo yum --enablerepo=epel install supervisor
```
/etc/supervisord.conf
```
[supervisord]
http_port=/var/tmp/supervisor.sock ; (default is to run a UNIX domain socket server)
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (logging level;default info; others: debug,warn)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
[supervisorctl]
serverurl=unix:///var/tmp/supervisor.sock ; use a unix:// URL for a unix socket
[program:growthforecast]
command=/home/growthforecast/perl5/perlbrew/perls/perl-5.18.2/bin/perl
/home/growthforecast/perl5/perlbrew/perls/perl-5.18.2/bin/growthforecast.pl
--data-dir /media/ephemeral0/growthforecast
--front-proxy=127.0.0.1
process_name=%(program_name)s
directory=/home/growthforecast
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
user=growthforecast
autostart=true
autorestart=true
```
make a directory for logging
```
sudo mkdir /var/log/supervisor
```
add superversord to upstart
/etc/init/supervisord.conf
```
description "supervisord"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
exec /usr/bin/supervisord -n
```
start supervisord via upstart
```
initctl start supervisord
```
2013年12月25日
Go言語の関数とメソッドのちょいネタ
Go Advent Calendar 2013-12-25 の記事です。
Go言語の関数とメソッドについてです。
前半はおさらい的な内容で、後半でちょっとしたネタを紹介します。
メソッドを変数に代入したあとに元の値を書き換えると……
これは`hello`メソッドのレシーバーがポインタではなく値だからです。
変数`f`のレシーバーは、メソッドを変数に代入した時にコピーされており、`Name`の中身はGophersになっています。その後で行われた変数`t`への操作の影響は受けません。
レシーバーの型をポインタにしてみると
レシーバーの型が値なのかポインタなのかは、メソッド内でレシーバーの中身を書き換える場合にも関係しますので、意識しておきましょう。
以下の例だと変数`c`はnilです。その変数`c`からメソッド`hello`を呼び出します。
なぜならば、nilポインタとなっている変数`c`の中身が使われないので、不正なアドレスへのアクセスが起きないためです。
変数`c`の中身はnilですが、変数`c`自体は実態を持っていますのでメソッドが呼び出せるわけです。
ちなみに変数`c`の中身を使おうとするとpanicします。
`Sample.hello`の型は`func(Sample)`となっていて、第一引数に`Sample`を受け取るので、上記の例のように呼び出すこともできます。
レシーバーがポインタの場合は以下のようになります。
もちろん、変数に代入することも出来ます。
処理の対象となるオブジェクトが定まっていないけれど、メソッドを変数に入れておいて使いたい、という時に使えそうです。
interfaceでも同様のコードを書くことが出来ます。
もちろんinterfaceの定義を満たしているかどうかによるサブタイピングがあるので、メソッドを定義する意味はありますし、使わない手はありません。
以上、ちょっとしたネタでしたが、知っておくといつか役に立つと思います。
- http://golang.org/ref/spec#Method_expressions
- http://golang.org/ref/spec#Method_values
Go言語の関数とメソッドについてです。
前半はおさらい的な内容で、後半でちょっとしたネタを紹介します。
関数
まず普通の関数です。func hello() { fmt.Println("Hello, Gophers") }
関数を変数に代入
関数を変数に代入することも出来ます。f := hello f(); // "Hello, Gophers"
匿名関数
関数リテラルで匿名関数を定義することが出来ます。f := func() { fmt.Println("Hello, Gophers") } f(); // Hello, Gophers
メソッド
レシーバーと関連付けた関数をメソッドと言います。type Test struct { Name string } func (t Test) hello() { fmt.Printf("Hello, %s\n", t.Name) } func main() { t := Test{Name: "Gophers"} t.hello() // "Hello, Gophers" }
メソッドを変数に代入
メソッドも関数と同じように変数に代入することができます。t := Test{Name: "Gophers"} f := t.hello f(); // "Hello, Gophers"このとき、レシーバーの値はメソッドと関連付けて保持されます。
レシーバーの型
このあたりから本番です。メソッドを変数に代入したあとに元の値を書き換えると……
t := Test{Name: "Gophers"} f := t.hello f(); // "Hello, Gophers" t.Name = "Go Rangers" f(); // "Hello, Gophers" になる。"Hello, Go Rangers" ではない!おや、"Hello, Go Rangers" にならずに "Hello, Gophers" となりました。
これは`hello`メソッドのレシーバーがポインタではなく値だからです。
変数`f`のレシーバーは、メソッドを変数に代入した時にコピーされており、`Name`の中身はGophersになっています。その後で行われた変数`t`への操作の影響は受けません。
レシーバーの型をポインタにしてみると
func (t *Test) helloPtr() { fmt.Printf("Hello, %s\n", t.Name) }
f := t.helloPtr f(); // "Hello, Gophers" t.Name = "Go Rangers" f(); // "Hello, Go Rangers"メソッドを変数に代入したあとの、Nameへの変更が反映されています。変数`f`にバインドされているのはポインタになっているからです。
レシーバーの型が値なのかポインタなのかは、メソッド内でレシーバーの中身を書き換える場合にも関係しますので、意識しておきましょう。
nilに対してメソッドを呼び出す
さて、レシーバーがポインタの場合にnilに対してメソッドを呼び出してみましょう。以下の例だと変数`c`はnilです。その変数`c`からメソッド`hello`を呼び出します。
type Cat struct { Name string } func (c *Cat) hello() { fmt.Println("myaa") } func main() { var c *Cat // c は nil c.hello() // myaa }ぬるぽ……にならず、この例だとpanicも起きずに動作します。
なぜならば、nilポインタとなっている変数`c`の中身が使われないので、不正なアドレスへのアクセスが起きないためです。
変数`c`の中身はnilですが、変数`c`自体は実態を持っていますのでメソッドが呼び出せるわけです。
ちなみに変数`c`の中身を使おうとするとpanicします。
func (c *Cat) hello2() { fmt.Printf("myaa, %s\n", c.Name) // panic: runtime error: invalid memory address or nil pointer dereference }これはnilのアドレスを解決しようとしているので、当然の動作です。
メソッドに違う方法でアクセスしてみる
さて、また違った例を見てみましょう。type Sample struct { Name string } func (s Sample) hello() { fmt.Printf("Hi %s\n", s.Name) } func main() { s := Sample{"Gophers"} Sample.hello(s) // Hi Gophers }変数`s`を通してではなく、`Sample.hello`という関数名を通してメソッド呼び出しています。
`Sample.hello`の型は`func(Sample)`となっていて、第一引数に`Sample`を受け取るので、上記の例のように呼び出すこともできます。
レシーバーがポインタの場合は以下のようになります。
func (s Sample) helloPtr() { fmt.Printf("Hi %s\n", s.Name) }
sp := &Sample{"Gophers"} (*Sample).helloPtr(sp) // Hi Gophersこちらもレシーバー型とメソッド名から、メソッドを取得できています。`(*Sample).helloPtr`の型は`func(*Sample)`です。
もちろん、変数に代入することも出来ます。
fv := Sample.hello fp := (*Sample).helloPtr
処理の対象となるオブジェクトが定まっていないけれど、メソッドを変数に入れておいて使いたい、という時に使えそうです。
interfaceでも同様のコードを書くことが出来ます。
type Duck struct {} type Sounder interface { Sound() } func (d *Duck) Sound() { fmt.Println("quack") } func main() { var f func(Sounder) = Sounder.Sound d := &Duck{} f(d) // quack }
感想など
メソッドの型は、第一引数に構造体を受け取る関数と同じになっています。変数を通してメソッドを呼び出せますが、ある種のシンタックスシュガーと言えるかもしれません。もちろんinterfaceの定義を満たしているかどうかによるサブタイピングがあるので、メソッドを定義する意味はありますし、使わない手はありません。
以上、ちょっとしたネタでしたが、知っておくといつか役に立つと思います。
参考
- http://golang.org/ref/spec#Function_literals- http://golang.org/ref/spec#Method_expressions
- http://golang.org/ref/spec#Method_values
2013年12月24日
MySQLでUnicode絵文字を使う
文章中に一部の絵文字があると、エラーになる問題に遭遇。
Unicode絵文字は4バイトなので、MySQLで使う場合は utf8mb4 という文字コードを使う必要があります。
というわけで、カラムの文字コードは utf8mb4 でテーブルの照合順序も utf8mb4_general_ci にしていたのですが……これだけではだめでした。
以下の項目を utf8mb4 に設定して通るようになりました。
クライアントとサーバの通信部分でも utf8mb4 になるように、しておく必要があるわけですね。
さて、環境がRDSだったので新規パラメーターグループを作成して上記を設定し、RDSのパラメーターグループを変更して再起動して完了。
Unicode絵文字は4バイトなので、MySQLで使う場合は utf8mb4 という文字コードを使う必要があります。
というわけで、カラムの文字コードは utf8mb4 でテーブルの照合順序も utf8mb4_general_ci にしていたのですが……これだけではだめでした。
以下の項目を utf8mb4 に設定して通るようになりました。
- character_set_client
- character_set_connection
- character_set_database
- character_set_results
- character_set_server
クライアントとサーバの通信部分でも utf8mb4 になるように、しておく必要があるわけですね。
さて、環境がRDSだったので新規パラメーターグループを作成して上記を設定し、RDSのパラメーターグループを変更して再起動して完了。
2013年10月16日
Google App Engine 1.8.6 で Go言語の単体テストがサポート
Google App Engine 1.8.6 がリリースされました。
そしてついに、Go版で単体テスト用のパッケージ appengine/aetest が追加されました!
AppEngineのローカルでの開発サーバはPythonで実装されており、Pythonの開発サーバがGoアプリを呼び出すように動作しています。この仕組み上、Goの単体テストからAppEngineのAPIを呼び出すことが困難でした。
新たに追加された appengine/aetest を使うと、以下のように単体テストを書くことができます。
package foo_test
import (
"testing"
"appengine/memcache"
"appengine/aetest"
)
func TestFoo(t *testing.T) {
c, err := aetest.NewContext(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
it := &memcache.Item{
Key: "some-key",
Value: []byte("some-value"),
}
err = memcache.Set(c, it)
if err != nil {
t.Fatalf("Set err: %v", err)
}
it, err = memcache.Get(c, "some-key")
if err != nil {
t.Fatalf("Get err: %v; want no error", err)
}
if g, w := string(it.Value), "some-value" ; g != w {
t.Errorf("retrieved Item.Value = %q, want %q", g, w)
}
}
この例では、memcache APIを使ったテストをしています。aetest.NewContext が appengine.Context インタフェースを返すので、これを使って各種APIを呼び出すことが出来ます。
単体テストの実行には go test ではなくSDKに同梱されている goapp コマンドを使います。
export APPENGINE_API_SERVER=/path/to/go_appengine/api_server.py
goapp test
環境変数 APPENGINE_API_SERVER は、SDKをインストールした場所に応じて値を設定します。aetest は裏でPythonのAPI Serverを起動し、そこと通信することでAPIを動かしていますので、そのためにSDKの場所を設定する必要があります。
※aetest.NewContextするたびに、データストア等は新しい領域が使われるようで、テストをまたがってデータが永続化されてしまうことは起きないようです。
※NewContextはテストケースごとに書きますが、中ではPythonのプロセスを起動しているので、やや重い気がします。ここは今後の改善に期待したいところ。
※aetest.NewContextするたびに、データストア等は新しい領域が使われるようで、テストをまたがってデータが永続化されてしまうことは起きないようです。
※NewContextはテストケースごとに書きますが、中ではPythonのプロセスを起動しているので、やや重い気がします。ここは今後の改善に期待したいところ。
Enjoy Testing!
登録:
投稿 (Atom)