メニュー

ラベル python の投稿を表示しています。 すべての投稿を表示
ラベル python の投稿を表示しています。 すべての投稿を表示

2014年3月17日

Apple Push Notifications (APNs) のPythonライブラリを作りました

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を提供すればいいのにと、つくづく思いました。

2012年11月28日

AppEngine開発環境でのConsistency


AppEngineのDatastore(今では標準となったHigh Replication Datastore)では、クエリのConsistency(一貫性)はEventualです。

このため、開発環境でもHRDの設定にしている場合、Eventual Consistencyな動作になります。

実は開発環境でのDatastoreのConsistencyの動作は設定によって変えられるのですが、デフォルトはTimeBasedHRConsistencyPolicyというものになっています。

# このクラスは google.appengine.datastore.datastore_stub_util の中にあります。

これは名前の通り、時間の経過によってクエリに検出される確率があがります。

Python SDKのコードを読むと、その確率は100msで98%、300msで99%、2000msで99.5%、240秒で100%となっています。

さて、AppEngineのSDKは単体テストを開発環境だけで実行することが容易になっていますが、前述したConsistencyについて、テストの場合には注意が必要なので、解説します。

Python版では、単体テストにはtestbedというモジュールを使います。これは、テスト用の各種サービス(API)のスタブです。

testbedについては公式のドキュメントも参照してください。
https://developers.google.com/appengine/docs/python/tools/localunittesting?hl=en

単体テストの場合、ConsistencyのデフォルトはMasterSlaveConsistencyPolicyです。つまりConsistencyはStrongで、保存したデータは即座にクエリに検出されます。

単体テストではStrong Consistencyは便利なのですが、MSなのでCross Group Transactionが使えません。よって、HRDアプリのテストではHRD用のConsistencyを使う必要があります。
self.testbed = testbed.Testbed()
self.testbed.activate()
self.testbed.init_datastore_v3_stub(consistency_policy= datastore_stub_util.TimeBasedHRConsistencyPolicy())

こんな感じです。

ここで困るのは、Eventualだとテストが書きづらくなることです。

保存したデータがクエリに検出されるかどうか、というテストが、Eventualなので成功したり失敗したりします。

実は、保存したデータをGetするとインデックスが作られるので、 Put > Get > Query とすると、テストが上手く行きます。

しかし、もっとスマートにテストを書くために、TimeBasedHRConsistencyPolicy ではなく、 PseudoRandomHRConsistencyPolicy を使ってみましょう。

これはクエリに検出されるかどうかを、(時間経過によらず)ランダムに決定するポリシーです。
コンストラクタの引数で確率を0.0から1.0までで指定できます。ここで確率を1.0、つまり100%にすると、Strong相当の動作になります。
self.testbed.init_datastore_v3_stub(consistency_policy= datastore_stub_util.PseudoRandomHRConsistencyPolicy(1.0))

これで、シンプルにテストを書けるようになります。


なお、Javaにも同様の機能があるようです(LocalDatastoreServiceTestConfigクラス)。
https://developers.google.com/appengine/docs/java/tools/localunittesting

2011年4月15日

Queryを非同期に使う

Queryは重い処理なので、複数回呼び出すときは非同期にするとよい。
Python版では、Query.run()を使うと非同期呼び出しができる。

例えば Diary というモデルがあったとして:

q = Diary().filter(...)
iterator = q.run()

このようにしてrunメソッドを呼び出すと、
裏では非同期にAPIが呼び出される。

runの戻り値はイテレータであり、
これを使おうとしたときに結果の待ち合わせが行われる。

よって、イテレータを使わないでおいて、
他のQuery.run()を呼び出せば、複数のAPIが並列にできる:

diary_query = Diary().filter(...)
diary_iterator = diary_query.run()

comment_query = Comment().filter(...)
comment_iterator = comment_query.run()

#これ以降でdiary_iterator,comment_iteratorを使う

Query.run()はイテレータを返すので、
Query.fetch()のようにlimitを指定できない。

そのままイテレーションすると、結果と時間の許す限り、
ずっとイテレーションしてしまう。

limitを指定したければイテレーション中に件数をカウント
すればいいが、面倒なので、自前のイテレータでラップする。


class QueryIterator(object):

  def __init__(self, query, limit=None):
    self.limit = limit
    self.count = 0
    if limit:
      config = datastore_query.QueryOptions(limit=limit, prefetch_size=limit)
    else:
      config = None
    self.iterator = query.run(config=config)

  def __iter__(self):
    return self

  def next(self):
    if self.limit and self.limit <= self.count:
      raise StopIteration()
    self.count += 1
    return self.iterator.next()
 
  def get_result(self):
    return [e for e in self]


これを使うと:

q = Diary().filter(...)
iterator = QueryIterator(q, limit=100)

for entity in iterator:
  ...

# 第一引数がq.run()じゃないことに注意

このイテレータは指定された件数に達するとイテレーションを終えるので、
際限なく処理が続いてしまうことはなくなる。

また、結果をリストで欲しい時は:

q = Diary().filter(...)
iterator = QueryIterator(q, limit=100)
entities = iterator.get_result()

のように使える。

2011年3月7日

自分の好きな言語の、嫌いな5つのことがらは? Python編

What are five things you hate about your favorite language?
http://stackoverflow.com/questions/282329/what-are-five-things-you-hate-about-your-favorite-language

なるものがありました。

自分の好きな言語の、嫌いな5つのことがらは? だそうです。

最近よく使うPythonで答えてみました。


  • コロン

せっかく改行までが1行という決まりなのに、def、class、ifのあとにコロンが必要。
最近は手が覚えましたが、最初はコロンを忘れまくりました。
無くていい。


  • strとunicode

文字列にstrとunicodeの2種類あるのは混乱の元。
(3では文字列はunicodeに統一されます)


  • タプルのリテラルが()なこと

関数呼び出しと同じ記号なので、関数呼び出しと一緒に使うと見づらいです。
とはいえ、他に括弧の記号がないのでしょうがないですね。


  • インデントの標準がスペース。個人的にはタブ派

これは個人的な好みです。タブがいいです……。


  • self、except、raise

他の言語に慣れてるとthis、catch、throwが良かったかなーと。


というわけで5つです。

良く言われる、メソッドの第一引数にselfがあることは、僕は嫌いではありません。

class Foo(object):
  def bar(self):
    pass

obj = Foo()

のとき、以下の2つの呼び出しが一緒になってくれるから。

obj.bar()
Foo.bar(obj)


2010年8月11日

Google App EngineでListPropertyを使おう

ListPropertyは複数の値が格納できる、便利なプロパティです。

複合インデックスでのインデックス爆発という問題はありますが、ListPropertyはGoogle App Engineには欠かせません。

複数選択が可能な(formでチェックボックスになるもの)は、ListPropertyを使うと実装しやすいです。

例えば、好きな動物というデータがあったとして:

class User(db.Model):
  fav = db.StringListProperty()

user = User()
user.fav = ['cat', 'dog', 'penguin']

としておけば、

users = User.all().filter('fav =', 'penguin')

のように、ペンギンが好きなユーザをクエリできます。

ここで問題なのは「どの動物も好きではない」ユーザをクエリで検索できないことです。
GQLではListPropertyが空であるという条件を表現できません。

そこで、ListPropertyのサイズも保存しておきます。

class User(db.Model):
  fav = db.StringListProperty()
  fav_len = db.IntegerProperty()

user = User()
user.fav = ['cat', 'dog', 'penguin']
user.fav_len = len(user.fav)

こうしておくと、fav_lenに要素数が入ります。
「どの動物も好きではない」ユーザは要素数がゼロですので、

users = User.all().filter('fav_len =', 0)

としてクエリで取得することが出来るようになります。

なお、他のプロパティと連動するプロパティにはComputedPropertyが便利です。
上記のUserを書き換えると:

class User(db.Model):
  fav = db.StringListProperty()
  @db.ComputedProperty
  def fav_len(self):
    return len(self.fav)

となります。これでエンティティが保存されるときに、自動でfav_lenがセットされます。

2010年8月8日

Google App Engineで、ときどきImportError

Google App Engineで、ときどきImportErrorが出ます。

もちろん開発サーバでは出ませんし、本番でも普通は出ません。ところが、ときどきImportErrorを出しまくるインスタンスがいます。

App Engineでは、一度importされたモジュールはキャッシュされ、2度目以降のimportはキャッシュされたものが使われます。

ところが、初回起動時のimportの途中でDeadlineExceededExceptionが発生した場合に、中途半端なimportが残ってしまい、ImportError多発となるようです。

Issue:
http://code.google.com/p/googleappengine/issues/detail?id=1409

この状態になってしまったインスタンスは、ずっとエラーを投げるので、どうしようもありません。そのインスタンスを終わらせる(スピンダウン)するしかありません。

簡単な方法としては、デプロイがあります。
新たにデプロイをすると、新しいバージョンで起動していくので、ImportErrorから復帰することが出来ます。

もうひとつは @higayasuo さんから聞きましたが、メモリ不足にすると、インスタンスは終了するようです。これはメモリリークのためのリセットの仕組みですね。

試しにメモリを確保しまくってみたところ、以下のログが記録されました。
Exceeded soft memory limit with 278.605 MB after servicing 2 requests total

これによれば、Pythonでは300MBくらいのヒープがあるようです。

"servicing 2 requests" はインスタンスが起動してからのリクエスト処理数ですので、あまり意味はありません。
# メモリリークの調査であれば意味はありますが

また、以下のようにもログが記録されます。

After handling this request, the process that handled this request was found to be using too much memory and
was terminated. This is likely to cause a new process to be used for the next request to your application.
If you see this message frequently, you may have a memory leak in your application.

訳すと

メモリ不足のため、このリクエストの終了後にプロセスは終了します。
これにより、次のリクエストの処理には新しいプロセスが利用されます。
このメッセージが頻繁に出る場合、アプリケーションにメモリリークの可能性があります。

というわけです。

ImportErrorの発生したインスタンスを強制的にスピンダウンするため、
わざとメモリ不足を発生させる……のもありかもしれません。

本当は、import中のDEEで、中途半端なimportをクリア出来ればいいのですが、方法が分かる方がいらっしゃいましたら、教えて頂けるとありがたいです。

2010年8月4日

Google App Engine 1.3.6 プレリリース

Google App Engine 1.3.6 のプレリリースがありました。

Python SDKの中身を見てみました:
  • db.is_in_transactionが追加
    # 正式APIなのはありがたい
  • fancy_urllibが追加
  • countの1000上限が撤廃
  • Datastore関連でunapplied_log_timestampというPBあり
    # なんだろう?
  • AllocateIdsでMAXが指定できる。allocate_id_rangeも復活
    # むかし、SDKにこっそりあったけど、途中でなくなったよね?
  • inbound_servicesにwarmupが追加
    # スピンアップ高速化のための予備インスタンスとか?
  • 500のエラーハンドラが設定可能に
    # これは素晴らしい! 標準のエラー画面がひどすぎるので……。
  • memcacheのPBにCAS向けの値が追加。APIは1.3.7か?
  • Blobstoreの画像のリサイズしたものへのURLを生成できる
    # Appでリサイズしなくていい!