Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2013-05-07

RabbitMQ 3.x から Queue のミラーリング方法が変更になった

今まで, Queue 作成時に指定していたミラーリングのパラメータは廃止になり, rabbitmqctl コマンドで指定するように変更なった.

Perl の AnyEvent::RabbitMQ や Python の kombu(celery) で x-ha-policy を指定する方法は, RabbitMQ 2.x までしか通用しない.*1

経緯や詳細は, 下記二つを参照の事.

指定方法

Web UI の Manage Console からも指定できるが, 今回は rabbitmqctl の例を挙げる.*2

Usage は下記の通り.

$ rabbitmqctl set_policy <ポリシーの名前> <パターン> <モード> [<優先度>]
ポリシー名ポリシーを一意にし, 更新や削除の操作対象とする.
パターンキュー名がパターンにマッチした場合, ポリシーの対象とする. 正規表現が使える.
モードRabbitMQ - Highly Available Queues 参照の事.
優先度数値が高い程, 優先. 未指定で 0 が入る.

指定例

全てのキューをミラーリングする(ものぐさ設定w;).

$ rabbitmqctl set_policy all '^.*' '{"ha-mode": "all"}'

キュー名の接頭辞に ha が付いた場合にミラーリングする.

$ rabbitmqctl set_policy ha-all "^ha\." '{"ha-mode":"all"}'

キュー名の接頭辞に amq が付いた場合(キュー名を指定せず, RabbitMQ に自動採番させた場合) "以外" にミラーリングする.

$ rabbitmqctl set_policy ha-ignore-amq '^(?!amq\.).*' '{"ha-mode": "all"}'

運用中にポリシーの変更が可能らしいが, 検証はこれから行う予定.

運用中にポリシーの変更は可能. また, クラスタリング時の Manage Console のポリシー表記が変だったので, 下記のコマンドで動作確認した方が良い.

$ rabbitmqctl list_queues name policy pid owner_pid slave_pids messages_ready messages_unacknowledged messages

尚, ポリシーは, パターンにマッチする Exchange にも適用されている. これが, どのような影響を及ぼすのかは調査中.

*1:3.x で指定してもエラーにならないので, 問題に気がつき難い

*2:私は, Manage Console で操作するのが良いと思う

2013-01-06

Python の amqplib とか py-amqp で Message を Consume する際, ヘッダに x-death が付与されていると落ちる件

データに謎の 'A' という型が定義されているのが問題. AMQP の Elementary domains を見たのだけれど 'A' が何か見当たらない…。助けて偉い人orz

とりあえず, amqplib 1.0.2 は, 以下の黒魔術で回避可能. kombu 2.5.0 から amqplib に代わり py-amqp がデフォルトで使用されるのだけれど, そちらはおいおい.

from struct import unpack
from decimal import Decimal
from amqplib.client_0_8.serialization import AMQPReader

def _patched_read_table(self):
    """
    Read an AMQP table, and return as a Python dictionary.
    """
    self.bitcount = self.bits = 0
    tlen = unpack('>I', self.input.read(4))[0]
    table_data = AMQPReader(self.input.read(tlen))
    result = {}
    while table_data.input.tell() < tlen:
        name = table_data.read_shortstr()
        ftype = ord(table_data.input.read(1))

        if ftype == 65: # 'A' これが新しく加わった!!
            len = unpack('>I', table_data.input.read(4))[0]
            ftype = ord(table_data.input.read(1))

        if ftype == 83: # 'S'
            val = table_data.read_longstr()
        elif ftype == 73: # 'I'
            val = unpack('>i', table_data.input.read(4))[0]
        elif ftype == 68: # 'D'
            d = table_data.read_octet()
            n = unpack('>i', table_data.input.read(4))[0]
            val = Decimal(n) / Decimal(10 ** d)
        elif ftype == 84: # 'T'
            val = table_data.read_timestamp()
        elif ftype == 70: # 'F'
            val = table_data.read_table() # recurse
        else:
            raise ValueError('Unknown table item type: %s' % repr(ftype))
        result[name] = val
    return result

AMQPReader.read_table = _patched_read_table

2012-12-09

Python Web Framework Advent Calendar 2012 (9日目) Django Model で Named Scope

前置き

この記事は、2012 Pythonアドベントカレンダー(Webフレームワーク) - connpass の 9 日目の記事となります。

今回は、Rails の Named Scope の真似を Django Model で実現する方法と、それを利用した論理削除の紹介を行います。

Django Model で Named Scope を実現する

そもそも何をしたいのか?

まず、ECサイトやソシャゲー等のユーザ情報から、最近登録したユーザの中から直近のアクセス順に上位5人を取得する例を挙げます。

import datetime
from django.utils.timezone import get_default_timezone

# 一週間以内の登録を "最近登録した" とみなす
dt = datetime.datetime.now() - datetime.timedelta(weeks=1)

# utc でも良いけれど、何となくローカライズ
dt = get_default_timezone().localize(dt)

User.objects.filter(created_at__gt=dt).order_by('-logged_in_at')[:5]

Named Scope を使用すると、次のようになります。

User.objects.by_newbie().order_by_active()[:5]
Named Scope を作る

では、実際の実現方法を紹介します。

import datetime
from django.utils.timezone import get_default_timezone

from django.db import models
from django.db.models.query import QuerySet

# Manager と QuerySet で同様のメソッドを使用するので Mix-in Class として切り出す
class UserScopesMixin(object):
    _newbie_term = datetime.timedelta(weeks=1)

    def by_newbie(self):
        dt = datetime.datetime.now() - self._newbie_term
        dt = get_default_timezone().localize(dt)
        return self.filter(created_at__gt=dt)

    def order_by_active(self):
        return self.order_by('-logged_in_at')


# QuerySet に Scope を Mix-in する
# 継承順は賛否分かれる所ですが、この記事では、社内の目があるので Mix-in Class を後ろに羅列します(w;
# 蛇足ですが、私は、私用で Python を書く場合に限り、Mix-in Class を前に羅列する派です
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# Manager に Scope を Mix-in し, 上記で定義した QuerySet を返すようにする
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model)


# 上記で定義した Manager を objects に設定する
class User(models.Model):
    objects = UserManager()

    created_at = models.DateTimeField(auto_now_add=True, index=True)
    logged_in_at = models.DateTimeField(auto_now=True)

    @classmethod
    def get_active_newbies(cls, limit=5):
        return User.objects.by_newbie().order_by_active()[:limit]

結局、get_active_newbies() を定義するのであれば、Named Scope なんて不用ではないか?と思われるかもしれません。

しかし、get_active_newbies() の様なメソッドを多数定義する場合、スッキリ書けるのでオススメです。

また、Named Scope が癖になっていると、そもそも QuerySet をカスタム済みであるため、他のカスタム QuerySet を組み込む際に労力が減るという副作用もあります。*1

論理削除の実例

物理的にレコードを削除せずに、削除フラグを立てて削除した事にするアレ。

class LogicalDeleteScopesMixin(object):
    def by_alive(self):
        return self.filter(deleted_uuid='')

    def delete(self):
        self.update(deleted_uuid=uuid.uuid4(),
                    deleted_at=datetime.datetime.now(pytz.utc))


class LogicalDeleteQuerySet(QuerySet, LogicalDeleteScopesMixin):
    pass


class LogicalDeleteManager(models.Manager, LogicalDeleteScopesMixin):
    def get_query_set(self):
        return LogicalDeleteQuerySet(self.model).by_alive()


# Mix-in Class であるため、object を継承したいが、
# Django Model の制約により models.Model を継承する必要がある。
class LogicalDeleteModelMixin(models.Model):
    class Meta:
        abstract = True


    class RedeletedError(Exception):
        pass


    objects = LogicalDeleteManager()

    # delete_at を有効/無効の確認に利用すると、
    # 一意キー制約を設けた際に、一秒以内の delete が使えないため、
    # 有効/無効を判断するための UUID フィールドを設ける。
    # 初期値に NULL を指定すると、NULL はレコード毎に異なる値と認識されるため、
    # UUID フィールドを一意キー制約に含められない。
    # そこで、初期値には空文字列を明示的に指定しておく。
    deleted_uuid = models.CharField(max_length=255, db_index=True, default='')

    # 念のため、記録として削除日時を残しておく。
    deleted_at = models.DateTimeField(blank=True, null=True)

    def delete(self):
        if self.deleted_uuid:
            raise self.RedeletedError, self.pk

        self.deleted_uuid = uuid.uuid4()
        self.deleted_at = datetime.datetime.now(pytz.utc)
        self.save()

早速、先ほどの User Model で使用してみましょう。

# LogicalDeleteScopesMixin を継承する
class UserScopesMixin(LogicalDeleteScopesMixin):
    pass # 内容に変更がないため省略


# LogicalDeleteScopesMixin を継承して UserScopesMixin を定義したので
# UserQuerySet に変更はない。
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# by_alive() を使用する必要がある
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model).by_alive()


class User(models.Model, LogicalDeleteModelMixin):
    pass # 内容に変更がないため省略

その他

私は、Python 歴 = Django 歴 = 半年未満であり、Django 以外の他の Python Web Framework の知識は皆無という状態ですが、今回紹介させて頂いた Named Scope や、Class Based View の存在から、Django は OO 設計し易いフレームワークだと認識しており、これからも末永くお付き合いできれば嬉しいなぁ〜と考えております。

参考 URL

*1:拙作に MemoizePerRequestQuerySet と MemcacheQuerySet というものがあるのですが、そちらは、Python 系勉強会で紹介予定です

2012-07-28

ZeroMQ Erlang Binding(NIF) の inproc と Erlang の素のメッセージ送信の速度を比較してみた

コードと結果は下記の通り

https://gist.github.com/3193117

Erlang で作ったサーバに LL で作ったワーカーをぶら下げようと考えており、どうせならナウでヤングな ZeroMQ を間に入れてみようと思い立ちました。

ズボラな私は、ZeroMQ にワーカーのロードバランスをして欲しかったので、Erlang の中で Queue デバイスを使用し、そこに複数の Erlang プロセスから inproc でメッセージを送信しまくる予定でした。

しかし、ここまで素のメッセージ送信と速度に差があるなら、Erlang の中で自前でロードバランスした方が良さそうかなぁと…思い直してます。

ちなみに、inproc を使ってみて初めて気がついたのですが、inproc は他のトランスポートと下記の点で異なります。

  • bind と connect に使うコンテキストは同じ物でなければいけない
  • connect の前に必ず bind を行う必要がある

2012-07-08

100マス計算のシートを生成する

パズル教室にて娘の数学的センスをベタ褒めされたものの、計算速度が遅いので100マス計算を家族でやるようにと指示を受けた。

早速、Python で100マス計算シートを HTML 形式で出力するコマンドを作成したのだが、身近に Haskeller が居るのだから Haskell で書いて添削してもらおうと思い立った。ケーキ一切れで請け負ってくれるだろうか?w;

とりあえず、添削前のコードを晒しておく。添削後には、もっと美しく高機能(例えば HTML で出力する機能が加わったり…)になる予定。

import System.Random
import System.IO
import Data.List

gen_cells :: Integer -> Integer -> IO [String]
gen_cells min max = do
    gen <- newStdGen
    return $ take 10 $ map (\n -> show n) $ randomRs (min, max) gen

i2path :: Integer -> String
i2path i = "./" ++ show i ++ ".txt"

print_table :: String -> String -> [String] -> [String] -> IO ()
print_table path method x_cells y_cells = do
    let head = concat $ intersperse " " x_cells
    let head_line = method ++ " " ++ head
    let lines = head_line : y_cells
    withFile path WriteMode $ \handle -> do
        sequence_ $ map (hPutStrLn handle) lines

mk_html' :: Integer -> String -> (Integer, Integer) -> (Integer, Integer) -> IO ()
mk_html' n method (x_min, x_max) (y_min, y_max) = do
    x_cells <- gen_cells x_min x_max
    y_cells <- gen_cells y_min y_max
    print_table (i2path n) method x_cells y_cells

mk_html :: Integer -> IO ()
mk_html n
    | n `rem` 4 == 1 = mk_html' n "+" (1, 99) (1, 99)
    | n `rem` 4 == 2 = mk_html' n "−" (50, 99) (0, 49)
    | n `rem` 4 == 3 = mk_html' n "×" (1, 99) (0, 9)
    | otherwise      = mk_html' n "÷" (1, 99) (1, 9)

main = sequence_ $ map mk_html [1..20]

早速、リファクタして頂いたのでコードを公開

{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
import System.Random
import System.FilePath
import System.IO
import Data.List
import Control.Applicative
import Text.Hamlet
import Text.Blaze.Html.Renderer.String
import Text.Blaze

data Method = Plus | Minus | Mult | Div
type Range = (Integer, Integer)
type Cells = [Integer]
type Answers a = [[a]]

get_calc_fun :: Method -> Integer -> Integer -> Integer
get_calc_fun Plus  = (+)
get_calc_fun Minus = (-)
get_calc_fun Mult  = (*)
get_calc_fun Div   = div

method2str :: Method -> String
method2str Plus  = "+"
method2str Minus = "−"
method2str Mult  = "×"
method2str Div   = "÷"

gen_cells :: Range -> IO Cells
gen_cells range = (take 10 . randomRs range) <$> newStdGen

calc_answer :: Method -> Cells -> Cells -> Answers Integer
calc_answer method x_cells y_cells = let f = get_calc_fun method in
    [[f x y | x <- x_cells] | y <- y_cells]

null_answer :: Answers Integer -> Answers String
null_answer answers = map (map (const "")) answers

i2path :: String -> Integer -> FilePath
i2path p i = "." </> (p ++ show i) <.> "html"

-- Integer や String は ToMarkup のインスタンス
print_table :: ToMarkup a => FilePath -> Method -> Cells -> Cells -> Answers a -> IO ()
print_table path method x_cells y_cells answers = do
    let rows = zip y_cells answers
    writeFile path $ renderHtml [shamlet|
!!!
<head>
  <title>100cells
<body>
  <table>
    <tr>
      <th>#{method2str method}
      $forall x <- x_cells
        <th>#{x}
    $forall (y, zs) <- rows
      <tr>
        <th>#{y}
        $forall z <- zs
          <td>#{z}
    |]

mk_html' :: Integer -> Method -> Range -> Range-> IO ()
mk_html' n method x_range y_range = do
    x_cells <- gen_cells x_range
    y_cells <- gen_cells y_range
    let answers = calc_answer method x_cells y_cells
    print_table (i2path "a" n) method x_cells y_cells answers
    print_table (i2path "p" n) method x_cells y_cells $ null_answer answers

mk_html :: Integer -> IO ()
mk_html n
    | n `rem` 4 == 1 = mk_html' n Plus  ( 1, 99) (1, 99)
    | n `rem` 4 == 2 = mk_html' n Minus (50, 99) (0, 49)
    | n `rem` 4 == 3 = mk_html' n Mult  ( 1, 99) (0,  9)
    | otherwise      = mk_html' n Div   ( 1, 99) (1,  9)

main = mapM_ mk_html [1..20]