MaridDB Galera Cluster を MySQL Connector/J + Scala でロードバランスしながらアクセス

 MariaDB Galera Cluster ですが、マルチマスターのクラスター構成になるので、接続するサーバ自体をクライアント側の判断により なんらかの手段で選択する必要があります。(逆に言えば、同じマシンスペックならどのマスターにつないでも同じということですが)
 色々なロードバランサーやプロキシを使ったり、クライアントでがりがりと機能を実装したり色々手段は考えられますが、 今回は、お手軽に MySQL Connector/J を使ってみました。
 URLの記述を変えるだけで、別途いろいろやる手間がなくてお手軽に構築できるからです。
 そうなると、自ずと JVM で動く環境ということで、お勉強もかねて Scala で実際のベンチマークは書いてます。
 世の中、当然 Java や Scala から MySQL にアクセスするためのものが色々ありますが、特殊なテストしたりするときに便利なので、 自前で直接 JDBC を叩くクラスも自前で書いてみました。

 以下のリンクは、Scala クラスの全裸ソースは以下のアーカイブ(zip)です。
 JUnit4 + ScalaTest の単体テストコード付きですので、使い方はそっちを読んでください。
 当然、別途 MySQL Connector/J の jar ファイルは必要になりますので、MySQL のサイトからダウンロードしておいてコンパイラのライブラリで指定しましょう。

ソースへのリンク : myoriginal.zip


接続

基本的には、URL の "jdbc:mysql:" の部分を、"jdbc:mysql:loadbalance:" に変えて、サーバを羅列すれば マルチマスタロードバランスの接続が一応実現できます。(デフォルトなので、ランダム切り替えになるはず)
URL生成部分の関数は具体的にはこんな感じ。(明示的にランダム切り替えを指定)
  /**
   *  URL 生成
   *  @param hosts     DBホストのリスト
   *  @param dataBase  データベース名
   *  @param port      ポート番号
   *  @param isLBEnabled Connector/J ロードバランサ有効
   *  @return          接続 URL
   */
  def makeUrl(hosts: Seq[String], dataBase: String, port: Int, isLBEnabled: Boolean = true) = {
    val h = hosts.foldLeft("")((r, x) => if (r == "") r + x else r + "," + x)
    (if (isLBEnabled) "jdbc:mysql:loadbalance://" else "jdbc:mysql://") + h + ":" + port + "/" + dataBase + "?zeroDateTimeBehavior=convertToNull&connectTimeout=" + connectTimeout + "&socketTimeout=" + socketTimeout
  }

※今回は、別所で "loadBalanceStrategy" をパスワードなどと一緒にパラメータで指定しています。こ れは、"bestResponseTime" も指定できますが、クラスタの一部サーバが落ちてるときは、接続に多少余分に時間かかかる現象が出たので、 "random" にしました。こちらのケアレスミスの可能性もありますが最適を選ぶロジックに若干癖があるのかもしれません。
本来は NDB 向けなどで、マルチマスター構成向けではないでしょうしね。


SQL 実行部分

 デッドロックを検出したら、再帰コールを利用してリトライを試みるようにしています。
 実処理部分を高階関数で実装できたり、try~catch~finally 式で、値が finally の部分が返るわけではないのはとても便利ですね。
 disposeStmt は準備したステートメントを常に開放する内部のメソッドです。関数型プログラミングとしては副作用ばりばりにありですが。

  /** デッドロック検出時の最大リトライ回数(極端に大きいのは禁止) */
  val DEADLOCK_RETRY = 50
  /** デッドロック検出時のリトライ待ち時間(msec) */
  val DEADLOCK_RETRY_SLEEP = 200

....... 省略 ........

  /**
   *  ステートメント後始末付き汎用 SQL 実行 (とは言っても実行自体は外部関数)
   */
  def _executeCommon[A](exception: Boolean)(sql: String)(f: (String) => A): Option[A] = {
    try {
      _executeWithDeadlockRetry(sql)(f)(DEADLOCK_RETRY)
    } catch {
      case e: Exception => if (exception) throw e else None
    } finally {
      disposeStmt
    }
  }

  /**
   * リトライ機能付き SQL 実行
   */
  protected def _executeWithDeadlockRetry[A](sql: String)(f: (String) => A)(retryCnt: Int): Option[A] = {
    try {
      Some(f(sql))
    } catch {
      case e: java.sql.SQLException if e.getErrorCode() == 1213 && retryCnt > 0 => mySleep(DEADLOCK_RETRY_SLEEP); _executeWithDeadlockRetry(sql)(f)(retryCnt - 1)
      case e: Exception => throw e
    } finally {
    }
  }


結果セットの処理

 SELECT などで取得した結果は、Java の ResultSet のままでは Scala 的には相当に微妙なので Scala コレクションに変換します。
 データ構造を柔軟かつお手軽に扱えるように、データは Seq[Map[K,V]] で扱ってます。要するにハッシュ(カラム)のリスト(行)です。
 Scala は、Perl や PHP などのスクリプト言語のハッシュ配列と同様に簡潔な記述で Map が扱えるのは至極便利ですよね。
 変換処理は、再帰などを使って不変コレクションでも実現できますが、あんまりメリットないだろうということで可変コレクションで データを変換して纏めたのちに不変コレクションに変換してます。とりあえず極端なメモリ、パフォーマンス重視でもないので。
 以下は取得結果全一括変換の場合なので全変換関数呼び出してますが、莫大な結果を1行ずつ処理したいときも、そういう関数を 作って渡す関数を変えるだけで実現できるのは関数型言語の便利なところ。
 他の言語でもコールバック処理はできますが、結構記述が面倒ですしね.....。

  /**
   * 1行結果変換
   * @param a   結果セット
   * @return    1行
   */
  def oneRowToMap(a: java.sql.ResultSet): Row = {
    val meta = a.getMetaData
    val coln = meta.getColumnCount() // 列項目数
    val m = collection.mutable.HashMap[String, Any]()
    for (i <- 1 to coln) m += meta.getColumnName(i) -> a.getObject(i)
    m.toMap
  }

  /**
   * ResultSet(Java) を RowSet(scala コレクション) に変換する
   * @param a   結果セット
   * @return    取得行セット
   */
  def resultSetAllToRowSet(a: java.sql.ResultSet): RowSet = {
    val r = MutableList[Row]()
    while (a.next) r += oneRowToMap(a)
    r.toList // パフォーマンス的には無駄だけど、安全のために List に変換
  }

  /**
   * 汎用 SELECT
   */
  def _select[A](sql: String, converter: java.sql.ResultSet => A, params: Seq[Any] = Seq()): Option[A] = {
    def qfunc(sql: String) = {
      val r = if (params.isEmpty) _query(sql) else _query(sql, params)
      val m = converter(r)
      r.close
      m
    }
    _executeCommon(errorException)(sql)(qfunc)
  }

  /**
   * SELECT 結果全取得
   * @param sql     SQL文字列。プレースホルダ使用可能
   * @param params  プレースホルダに渡すパラメータ。nullでパラメータなし
   * @return        検索結果(全行)
   */
  def selectAll(sql: String, params: Seq[Any] = Seq()): Option[RowSet] = _select(sql, resultSetAllToRowSet, params)

| | Comments (129) | TrackBack (0)

MariaDB Galera Cluster (RC) のテスト

数か月ほど前から、すでに稼働中の MySQL サーバのスケールアウト & リプレース対象で MariaDB Galera Cluster RC を合間をみて評価中です。
職場ごとおさらばなので、プータローになる前にいろいろまとめてます。
たらたらと Scala もつかったりしてるので、JDBC ドライバレベルでのロードバランシング機能や、勉強も含めて 評価してみようということで、Scala で JDBC を直接たたくクラスを書いたり色々やってます。
その辺のソースとかも近日あげる予定です。

はじめは、これもわりかし最近公開された MariaDB Client でやろうかと思ったんですが、現在のバージョン(2013/2月時点) だと、ロードバランシングの機能がないようなので、途中から MySQL の Connector/J で接続する方向に転換しました。
Galera Cluster は、同期レプリケーションなので、原理的にいずれかのメンバーサーバのディスク I/O が書き込み性能限界に達した時点で、 台数を増やしても更新処理はスケールアウトしなくなるんでしょうけど、それで十分であろうという目算の元ですね。 ストレージのみに起因する単純な書き込み限界に達するのはかなり先っぽいので。(Handler socket なんかの実績を見てると)
海外の記事だとサーバ 6 台ぐらいまでは NDB よりも性能いいけど、その先は書き込みがネックになるのか逆転される みたいなベンチマークも出てますし。
NDB Cluster は運用が少し面倒なことや、色々癖もあったり、Oracleがこの先どういうつもりなのか謎.....など微妙な点も多いですし。
No SQL もよいですが、既存の延長で十分な性能出せるならそれはそれでよいですしね。(というか Galera を New SQL に分類してるサイトも 海外だと合ったりしますね)

ついでに、スレーブへのレプリケーションも同時に行ったりしてます。
(スレーブをバックアップ取りで使うなどの応用も考えられるので)

クラウド環境でセットアップの記事がいくつか出てますが、わたしは、手元の小型サーバマシンに VMWare ESXi を入れて、そこの仮想マシンに普通に CentOS 6.3 を入れてやってます。 あと、とりあえず SELinux は切ってます。

名前IPアドレス/ネットマスク用途備考
mdb01 172.16.0.128/12 DB Master 1 原初起動用
mdb02 172.16.0.129/12 DB Master 2
mdb03 172.16.0.130/12 DB Master 3
mdbslave 172.16.0.140/12 DB Slave 通常のスレーブ。RO設定

"mdb01"の /etc/my.cnf.d/server.cnf は下記のような感じです。
  • 初めの起動はコマンドラインでやる前提で、参照アドレスは隣にしてます。
  • 通常のマスタスレーブも組んでみたので、サーバIDを設定。(スレーブ側は設定は通常の、ROWレプリケーションと同じように設定します)
  • ストアドプロシージャの変更もレプリケーションで伝搬するように、log-bin-trust-function-creators を設定 サーバ間自動同期は rsync 使用 (mysqldump方式も可能なはず)

    /etc/my.cnf.d/server.cnf ファイル(mdb01)
    [mysqld]
    server-id=100
    datadir=/var/lib/mysql
    socket=/var/lib/mysql/mysql.sock
    user=mysql
    # Disabling symbolic-links is recommended to prevent assorted security risks
    symbolic-links=0
    character-set-server=utf8
    default-storage-engine=InnoDB
    max_allowed_packet=1MB
    binlog_format=ROW
    log-bin=mysql-bin
    expire_logs_days=5
    log_slave_updates
    log-bin-trust-function-creators=1
    
    # galera replication
    wsrep_cluster_name=DBCLUSTER
    wsrep_cluster_address=gcomm://172.16.0.129
    #wsrep_cluster_address=gcomm://
    wsrep_node_address=172.16.0.128
    wsrep_provider='/usr/lib/galera/libgalera_smm.so'
    wsrep_sst_method=rsync
    wsrep_slave_threads=4
    innodb_autoinc_lock_mode=2
    innodb_locks_unsafe_for_binlog=1
    
    [mysqld_safe]
    log-error=/var/log/mysqld.log
    ####pid-file=/var/run/mysqld/mysqld.pid
    
    手動起動スクリプト
    [root@mdb01 bin]# cat /usr/local/sbin/mdbclusterstart
    #!/bin/sh
    # MariaDB galera cluster : Cluster start (for 1st server only)
    service mysql start --wsrep_cluster_address=gcomm://
    
    動作ですが、データの更新はもとより、ユーザ追加などもちゃんとマスター間で同期します。
    通常のスレーブレプリケーションも問題ないです。
    ちょっと変わったところでは、ストアドプロシージャの登録などもやってみましたが、追加オプション(log-bin-trust-function-creators)のおかげ? .....かどうかはわかりませんが、現状では問題なく同期してくれています。

    気になったのは、RCでの話なので治ってるかもしれませんが、ときたま wsrep での通信に失敗するのか、デーモン起動までは成功するのですが mysql コマンドでつながらないのであれ?と思って ps コマンドなどで確認すると、さっきあったはずのデーモンが消えてることがあります。
    これは、wsrep でのエラーは検出に時間がかかり、その検出前にデーモンとしては正常起動するという動作のなせる現象っぽいですね。 ともかく、起動後は mysql コマンドで接続できるか回確認したほうがよいかも?です。
    とりあえず、一度確立できれば途中で接続できなくなるとかの現象は発生していません。

    注意点として、ポート 4444 を、ほかの MariaDB サーバ向けに開けておく必要があります。(iptablesなどで)
    そうしないと、マスター間で同期ずれができたとき(サーバダウン、そのまま他DBを更新、その後再アップをした時など) に、次のような感じでエラーが出ます。要するに内蔵してる専用の rsync の失敗で、再起動できないということらしいですね。
    この現象は、同期ずれが実際発生するまで、正常に動き続けるので発見ができないので注意が必要です。
    開け忘れてると、いざというときにメンバー復帰や追加ができずにあたふた....ということになりそうです。
    当然、ポート解放時は、FWの内部以外のケースでは、IPを限定するなどして設定しないと全世界公開となって攻撃の穴になりかねないのでその点は要注意。
    130222 12:31:27 [Note] WSREP: Shifting OPEN -> PRIMARY (TO: 1027)
    130222 12:31:27 [Note] WSREP: State transfer required:
            Group state: a25c94a8-7abf-11e2-0800-a30e5747ead0:1027
            Local state: a25c94a8-7abf-11e2-0800-a30e5747ead0:1024
    130222 12:31:27 [Note] WSREP: New cluster view: global state: a25c94a8-7abf-11e2-0800-a30e5747ead0:1027, view# 6: Primary, number of nodes: 2, my index: 0, protocol version 2
    130222 12:31:27 [Warning] WSREP: Gap in state sequence. Need state transfer.
    130222 12:31:29 [Note] WSREP: Running: 'wsrep_sst_rsync --role 'joiner' --address '172.16.0.129' --auth '' --datadir '/var/lib/mysql/' --defaults-file '/etc/my.cnf' --parent '2525''
    cat: /var/lib/mysql//rsync_sst.pid: そのようなファイルやディレクトリはありません
    130222 12:31:29 [Note] WSREP: Prepared SST request: rsync|172.16.0.129:4444/rsync_sst
    130222 12:31:29 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
    130222 12:31:29 [Note] WSREP: Assign initial position for certification: 1027, protocol version: 2
    130222 12:31:29 [Note] WSREP: Prepared IST receiver, listening at: tcp://172.16.0.129:4568
    130222 12:31:29 [Note] WSREP: Node 0 (mdb02) requested state transfer from '*any*'. Selected 1 (mdb01)(SYNCED) as donor.
    130222 12:31:29 [Note] WSREP: Shifting PRIMARY -> JOINER (TO: 1028)
    130222 12:31:29 [Note] WSREP: Requesting state transfer: success, donor: 1
    130222 12:31:29 [Warning] WSREP: 1 (mdb01): State transfer to 0 (mdb02) failed: -113 (No route to host)
    130222 12:31:29 [ERROR] WSREP: gcs/src/gcs_group.c:gcs_group_handle_join_msg():712: Will never receive state. Need to abort.
    130222 12:31:29 [Note] WSREP: gcomm: terminating thread
    130222 12:31:29 [Note] WSREP: gcomm: joining thread
    130222 12:31:29 [Note] WSREP: gcomm: closing backend
    130222 12:31:30 [Note] WSREP: view(view_id(NON_PRIM,56f04574-7ca0-11e2-0800-c955f7f6a680,6) memb {
            56f04574-7ca0-11e2-0800-c955f7f6a680,
    } joined {
    } left {
    } partitioned {
            c84c0c5d-7c91-11e2-0800-fc011262b763,
    })
    130222 12:31:30 [Note] WSREP: view((empty))
    130222 12:31:30 [Note] WSREP: gcomm: closed
    130222 12:31:30 [Note] WSREP: /usr/sbin/mysqld: Terminated.
    130222 12:31:30 mysqld_safe mysqld from pid file /var/lib/mysql/mdb02.pid ended
    Parent mysqld process (PID:2525) terminated unexpectedly.
    WSREP_SST: [INFO] Joiner cleanup. (20130222 12:31:30.717)
     done.
    
    ずれ発生時の自動同期ですが、自作のベンチだとデータサイズが大したことないので、動かしながら個々のノードを止めて外したり、その後戻したりしても、 一貫性の問題やエラーも今のところまったく出ませんので(正確には、MySQL Connector/Jのなかで自動切り替えされる)、まったく気にならないです。
    ただし、これは実際確認していることではまったくないんですけど、動作ホットな状態で rsync で同期中は、更新が事実上禁止(終わるまで待機?)になってるのでは? と思ったりします。(そうじゃないとサーバ間で更新の完全同期ができないので)
    そうなると、ファイルが非常に大きいと、コピーしてる間はほかのノードの更新処理も事実上結構待たされる可能性があるんじゃないか? とかも思ったりします。

    ちなみに、デッドロックはやはり起きやすいですね。
    (ベンチは、毎回同じレコードの更新も行うようにしたので、容易に現象が起きる)
    これは、例外処理で、デッドロックの例外 java.sql.SQLException (エラーコード:1213)を捕まえて、リトライを行う処理をしますたが、 現状ではテストレベル問題なく動作しているようです。

    | | Comments (1298) | TrackBack (0)

    Scala で書いた麻雀役判定のプログラムを Haskell で書き直してみた

    Scala で書いた麻雀の役判定の基本処理を、これもお勉強で今度は Haskell で書いてみた。
    これは本当に初めて書いた Haskell プログラム。
    アルゴリズムも基本的な流れは同じだが、Scala版よりも数か月時あとに書いたのもあって、事前に牌種を分ける 処理を省いたりなど多少改良されている。
    表示方法などはちょっと手を抜いてるが.....

    純粋なテストなので、テスト用のデータも一緒に書いてあります。
    しかし、Haskell だとホント、yacc 気分をより感じますね。
    文法も手伝って、定義ファイルっぽくてすっきりというか。

    mj.hs ファイル
    module MJTest
    (
    )
    where
    
    import Data.List
    import Data.Maybe
    
    -------- 汎用的なもの
    -- 組み合わせ
    comb :: [a] -> Int -> [[a]]
    comb xs n
      | n <= 0 = []
      | n == 1 = [ [x] | x <- xs ]
      | otherwise = [ xs!!x : y | x <- [0..lenm], y <- comb (drop (x+1) xs) (n-1)]
        where
            lenm = length xs - 1
    
    -- 全網羅組み合わせ生成
    allComb :: [a] -> [[a]]
    allComb s = foldl' (\r n -> r ++ comb s n) [] [1..length s]
    
    -- リスト中の指定した要素数を数える
    count :: Eq a => [a] -> a -> Int
    count xs p = foldl' (\ a x -> if x==p then a+1 else a) 0 xs
    
    -- ユニークな要素を抽出 (テストでnubを使わずに)
    distinict :: Eq a => [a] -> [a]
    distinict xs = foldr (\x a -> if elem x a then a else x : a) [] xs
    
    -------- ほぼ専用なもの
    
    -- 牌の種類
    data PaiType = Pinzu | Souzu | Wanzu | Tsu deriving(Show,Eq,Ord)
    
    -- 牌1つ
    data Pai = Pai { typ :: PaiType, idx :: Int } deriving(Eq,Ord)
    instance Show Pai where         -- Show カスタマイズ
        show (Pai Tsu i)
            | i==1 = "TON"
            | i==2 = "NAN"
            | i==3 = "SHA"
            | i==4 = "PEI"
            | i==5 = "HAKU"
            | i==6 = "HATSU"
            | i==7 = "CHUN"
            | otherwise = "?"
        show (Pai t i) = show t ++ ":" ++ show i
    
    -- 牌数。ある牌が何個あるかを保持する
    data PaiN = PaiN { pai :: Pai, pn :: Int } deriving(Show,Eq,Ord)
    
    -- 分類後の牌1セット
    data PaiAnalyzed = PaiAnalyzed { toitsu :: [Pai], koutsu :: [Pai], jyuntsu :: [Pai] } deriving(Show,Eq,Ord)
    
    -- 牌の数としてカウントする
    countPai :: [Pai] -> Pai -> PaiN
    countPai l p = PaiN p (count l p)
    
    -- 各牌が何枚あるか数えて牌数リストにする
    pail2PaiN :: [Pai] -> [PaiN]
    pail2PaiN s = map (countPai s) (distinict s)
    
    -- 牌数値から順値を取り出す
    getpaiidx a = idx (pai a)
    
    -- 牌数値からタイプを取り出す
    getpaityp a = typ (pai a)
    
    -- 牌数値から牌数を取り出す
    getpaiN a   = pn a
    
    -- 対子、刻子候補を選抜する
    koutsuList a = [ pai x | x <- a, getpaiN x >= 3 ]
    toitsuList a = [ pai x | x <- a, getpaiN x >= 2 ]
    
    -- 順子チェック (牌種、順序のチェック)
    juntsuCheck a b c = t /= Tsu && typEq3 && jcheck
        where
            t = getpaityp a
            typEq3 = t == getpaityp b && t == getpaityp c
            jcheck = getpaiidx a == (getpaiidx b) - 1 && getpaiidx a == (getpaiidx c) - 2
    
    -- 先頭の順子をひとつ検出する。順序関係を見るのでソート済み必須
    checkHeadJuntsu (x0:x1:x2:_)
        | juntsuCheck x0 x1 x2 = True
        | otherwise = False
    checkHeadJuntsu _ = False   -- 3個取り出せない場合は問答無用に失敗
    -- 先頭の順子分を1つ取り出す。チェック済みなこと前提
    dropHeadJuntsu p = [ PaiN (pai x) (-1 + getpaiN x) | x <- take 3 p, (getpaiN x) > 1 ] ++ drop 3 p
    
    -- すべてを順子に分解する。牌数リストはソート済み必須。余りがでた場合(全分解不可)は Nothing
    divideJuntsu :: [PaiN] -> Maybe [Pai]
    divideJuntsu p = f p []
        where
            f [] r = Just r                         -- 残り牌がないので成功終了
            f pl r 
                | checkHeadJuntsu pl    = f (dropHeadJuntsu pl) ( (pai (head pl)) : r)
                | otherwise             = Nothing   -- 牌があるのに順子が取り出せないので失敗
    
    -- 牌数リストから指定リストの刻子/対子を取り出す(1つ目の引数で減算数を指定)
    dropKoutsu :: Int -> [PaiN] -> [Pai] -> [PaiN]
    dropKoutsu m xs kls = filter ff (map mf xs)
        where
            ff x = getpaiN x > 0                    -- 残なし牌消去フィルタ関数
            mf x = if pchk then (PaiN p nn) else x  -- 刻子削除(減数)処理関数
                where
                    p = pai x
                    nn = getpaiN x - m
                    pchk = p `elem` kls             -- 刻子リストに入っているかチェック
    
    -- 刻子候補リストを与えて評価(結果生成のために対子も渡す)
    paiEvaluteGiveKoutsu :: [PaiN] -> Pai -> [[Pai]] -> [PaiAnalyzed]
    paiEvaluteGiveKoutsu pl ts kl = [ PaiAnalyzed [ts] k (fromJust mdj) | k <- kl, let mdj = divideJuntsu (dk k), isJust mdj]
        where
            dk = dropKoutsu 3 pl
    
    -- 対子を与えて残りを全パターン評価
    paiEvaluteGiveToitsu :: [PaiN] -> Pai -> [PaiAnalyzed]
    paiEvaluteGiveToitsu pl t = paiEvaluteGiveKoutsu npl t al
        where
            tl = [t]
            npl = dropKoutsu 2 pl [t]               -- 対子を除いた牌数リスト
            al = [] : allComb (koutsuList npl)      -- 刻子全パターン+刻子なしの刻子候補リストを生成
    
    -- 評価 (全対子候補ごとに評価を行う)
    paiEvalute :: [Pai] -> [PaiAnalyzed]
    paiEvalute src = foldl' (\r x-> (evf x) ++ r) chktoi tlist
        where
            pain = sort (pail2PaiN src)             -- 牌数リスト化してソートしたもの
            tlist = toitsuList pain                 -- 全対子候補
            chktoi = if (length tlist) == 7 then [ PaiAnalyzed tlist [] [] ] else []    -- 七対子判定 & 生成
            evf = paiEvaluteGiveToitsu pain
    
    
    -- テスト用データ
    plist0 = [ Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 3 ]       -- 失敗
    plist1 = [ Pai Tsu 1, Pai Tsu 2, Pai Tsu 3, Pai Tsu 5, Pai Tsu 5 ]                              -- 失敗
    plist2 = [ Pai Tsu 1, Pai Tsu 1, Pai Tsu 1, Pai Tsu 2, Pai Tsu 2, Pai Tsu 2, Pai Tsu 3, Pai Tsu 3, Pai Tsu 3, Pai Tsu 4, Pai Tsu 4, Pai Tsu 4, Pai Tsu 5, Pai Tsu 5 ]
    plist3 = [ Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 4, Pai Pinzu 4 ]
    plist4 = [ Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 1, Pai Pinzu 3, Pai Pinzu 4, Pai Pinzu 4]
    plist5 = [ Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Wanzu 5, Pai Wanzu 5, Pai Wanzu 5, Pai Tsu 1, Pai Tsu 1]
    plist6 = [ Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 4, Pai Pinzu 4, Pai Pinzu 5, Pai Pinzu 5, Pai Pinzu 6, Pai Pinzu 6, Pai Pinzu 7, Pai Pinzu 7 ]
    plist7 = [ Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 4, Pai Pinzu 4, Pai Pinzu 4, Pai Souzu 5, Pai Souzu 5 ]
    plist8 = [ Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 1, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 2, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 3, Pai Pinzu 4, Pai Pinzu 4, Pai Pinzu 4, Pai Pinzu 5, Pai Pinzu 5 ]
    
    
    -- 表示付きテスト関数
    
    paiListDisp msg list = if list == [] then 
            return () 
        else do
            putStr $ " " ++ msg ++ "=" ++ show list
    
    paiAnalyzedDisp a i = do
        putStr $ show i
        paiListDisp "Toitsu" t
        paiListDisp "Koutsu" k
        paiListDisp "Juntsu" j
        putStrLn ""
        where
            t = toitsu a
            k = koutsu a
            j = jyuntsu a
    
    anaDisp list = f (paiEvalute list) 1
        where
            f (x:xs) i = do
                paiAnalyzedDisp x i
                f xs $ i+1
            f _ _ = return ()
    


    GHCi での実行例
    C:\haskell>ghci
    GHCi, version 7.4.2: http://www.haskell.org/ghc/  :? for help
    Loading package ghc-prim ... linking ... done.
    Loading package integer-gmp ... linking ... done.
    Loading package base ... linking ... done.
    Prelude> :load mj
    [1 of 1] Compiling MJTest           ( mj.hs, interpreted )
    Ok, modules loaded: MJTest.
    *MJTest> anaDisp plist0
    *MJTest> anaDisp plist1
    *MJTest> anaDisp plist2
    1 Toitsu=[HAKU] Koutsu=[TON,NAN,SHA,PEI]
    *MJTest> anaDisp plist3
    1 Toitsu=[Pinzu:4] Juntsu=[Pinzu:1,Pinzu:1]
    2 Toitsu=[Pinzu:1] Juntsu=[Pinzu:2,Pinzu:2]
    *MJTest> anaDisp plist4
    1 Toitsu=[Pinzu:4] Juntsu=[Pinzu:1,Pinzu:1,Pinzu:1]
    2 Toitsu=[Pinzu:4] Koutsu=[Pinzu:1,Pinzu:2,Pinzu:3]
    3 Toitsu=[Pinzu:1] Juntsu=[Pinzu:2,Pinzu:2,Pinzu:1]
    *MJTest> anaDisp plist5
    1 Toitsu=[TON] Koutsu=[Wanzu:5] Juntsu=[Pinzu:1,Pinzu:1]
    *MJTest> anaDisp plist6
    1 Toitsu=[Pinzu:7] Juntsu=[Pinzu:4,Pinzu:4,Pinzu:1,Pinzu:1]
    2 Toitsu=[Pinzu:4] Juntsu=[Pinzu:5,Pinzu:5,Pinzu:1,Pinzu:1]
    3 Toitsu=[Pinzu:1] Juntsu=[Pinzu:5,Pinzu:5,Pinzu:2,Pinzu:2]
    4 Toitsu=[Pinzu:1,Pinzu:2,Pinzu:3,Pinzu:4,Pinzu:5,Pinzu:6,Pinzu:7]
    *MJTest> anaDisp plist7
    1 Toitsu=[Souzu:5] Koutsu=[Pinzu:1] Juntsu=[Pinzu:2,Pinzu:2,Pinzu:2]
    2 Toitsu=[Souzu:5] Koutsu=[Pinzu:4] Juntsu=[Pinzu:1,Pinzu:1,Pinzu:1]
    3 Toitsu=[Souzu:5] Koutsu=[Pinzu:1,Pinzu:2,Pinzu:3,Pinzu:4]
    *MJTest> anaDisp plist8
    1 Toitsu=[Pinzu:5] Koutsu=[Pinzu:1] Juntsu=[Pinzu:2,Pinzu:2,Pinzu:2]
    2 Toitsu=[Pinzu:5] Koutsu=[Pinzu:4] Juntsu=[Pinzu:1,Pinzu:1,Pinzu:1]
    3 Toitsu=[Pinzu:5] Koutsu=[Pinzu:1,Pinzu:2,Pinzu:3,Pinzu:4]
    4 Toitsu=[Pinzu:2] Koutsu=[Pinzu:1] Juntsu=[Pinzu:3,Pinzu:3,Pinzu:2]
    *MJTest>
    

    | | Comments (0) | TrackBack (0)

    Scala で麻雀の役判定の基本処理の部分を書いてみた

    半年以上前の話だが、Scala とか勉強するのに数学の課題とかを解く気が個人的に起きないので、麻雀の役判定の基礎部分を Scala で書いてみた。
    出先仕事も今月で終わりであとは今の所ないので暇だし、後で自分で見たりするかも?ということであげておく。
    関数型プログラミングも含めてこれがほとんど初めて書いた Scala のプログラムなので、それらしいとかまったく不明。
    あんまり意味もないことも多少やっていたり。
    しかし、関数型言語のプログラムは、昔 ImPP というパイプライン処理プロセッサのプログラムや、awk、yacc の定義ファイルを書いてる気分になってくるな。

    大雑把には以下のような手順。

    • 計算しやすいように、牌毎の数リストに再構成
    • 対子の候補を全部抽出
    • 各対子を仮定したときに、刻子の候補を全抽出
    • 刻子のすべての組み合わせを仮定したときに、残りが順子に分解できるか処理
    • 余りがなければ成功、余ったら失敗
    • 成功例のみを集積
    ちなみに、槓子はプレイ中に意識的に分離しているはずなので、事前に抽出済みな前提。

    yakuhantei.scala ファイル
    // 牌の種別を表す列挙型のようなもの
    class paiGroup(val code: Int) {
      // 比較関数実装
      def <(arg: paiGroup): Boolean = code < arg.code
      // 順子になれる牌か判定
      def isJyun: Boolean = this < paiGroup.tu
    }
    
    object paiGroup {
      case object sz extends paiGroup(1)
      case object pz extends paiGroup(2)
      case object wz extends paiGroup(3)
      case object tu extends paiGroup(4)
    
      // 整数から生成。異常時はマッチング例外になる
      def fromInt(n: Int) = n match {
        case 1 => this.sz
        case 2 => this.pz
        case 3 => this.wz
        case 4 => this.tu
      }
    }
    
    // 牌1枚を表すケースクラス (idx = 1~9)
    case class pai(grp: paiGroup, idx: Int) {
      // 基本コンストラクタ
      if (idx <= 0 || idx > 9) throw new Exception("pai.idx is error value ! (" + idx.toString + ")")
    
      // 拡張コンストラクタ
      def this(n: Int) = this(paiGroup.fromInt(n / 10), n % 10)
      def this(n: String) = this(n.toInt)
    
      // 比較関数実装
      def <(arg: pai): Boolean = grp < arg.grp || (grp == arg.grp && idx < arg.idx)
      // 順子になれる牌か判定
      def isJyun: Boolean = grp.isJyun
    
      override def toString = grp.toString.substring(0, 1) + idx.toString
    }
    
    // 役評価処理
    object yakuhyouka {
      // 牌のリストの型
      type PL = List[pai] // 牌リスト。手牌や、対子、刻子、順子一つなど
      type TSU = List[pai] // 分類後の牌セット。対子、刻子、順子一つなど
      type GRPTSU = List[TSU] // 1つの分析結果セット。牌リストの集合
    
      // ソートしたものを取得
      def sortList(src: PL) = src.sortWith { _ < _ }
    
      // 対子になる牌種すべてを取得 (同じ牌は折りたたまれる)
      def toiList(src: PL) = src.filter(n => src.count(n == _) >= 2).distinct
    
      // 刻子になる牌種すべてを取得
      def kouList(src: PL) = src.filter(n => src.count(n == _) >= 3).distinct
    
      // 対子刻子を抜いた牌リストを生成
      // 意味はまったくないが、ためしに関数リテラル+クロージャーで書いてみた
      val partitionRemove = (src: PL) => (p: pai) => {
        val (lmy, lother) = src.partition(p == _)
        (n: Int) => if (lmy.size <= n) lother else sortList(lother ++ lmy.drop(n)): PL
      }
    
      def removeToi(src: PL)(p: pai) = partitionRemove(src)(p)(2)
      def removeKou(src: PL)(p: pai) = partitionRemove(src)(p)(3)
    
      // 刻子/槓子のリストを全削除
      def pListRemove(F: (PL) => (pai) => PL, src: PL, klist: PL): PL = if (klist.isEmpty) src else pListRemove(F, F(src)(klist.head), klist.drop(1))
    
      // 牌リストを種類ごとに分解する
      def grpGrouping(src: PL) = src.groupBy(_.grp)
    
      // 牌リストをインデックスごとに分解する
      def idxGrouping(src: PL) = src.groupBy(_.idx)
    
      // セット(2~3枚)を1つ作成
      def makeToi(p: pai): TSU = List.fill(2)(p)
      def makeKou(p: pai): TSU = List.fill(3)(p)
      def makeJun(p: pai): TSU = List(p, pai(p.grp, p.idx + 1), pai(p.grp, p.idx + 2))
    
      // 牌リストをセットリストに変換
      def makeKouList(pl: PL): GRPTSU = pl.map { makeKou }
    
      // 七対子を生成
      def makeChitoi(pl: PL): GRPTSU = pl.map { makeToi }
    
      // 数値のみで順子に分解。全分解出来なければ None。牌の種類は選り抜いてあること
      def junDivide(s: List[(Int, Int)])(j: List[Int]): Option[List[Int]] = {
        def f(p0: (Int, Int), p1: (Int, Int), p2: (Int, Int), oth: Seq[(Int, Int)]) = {
          // 分離したものを除いたリストを再生成 (残数0は含まれないし除去される)
          val c0 = p0._1
          val cl = List(p0, p1, p2).collect { case (x, n) if (n > 1 && (x == c0 || x == c0 + 1 || x == c0 + 2)) => (x, n - 1) } ++ oth
          // さらに分解
          junDivide(cl)(j :+ c0)
        }
        s match {
          case List() => Some(j)
          case List(p0, p1, p2, other @ _*) if p1._1 == p0._1 + 1 && p2._1 == p0._1 + 2 => f(p0, p1, p2, other)
          case _ => None
        }
      }
    
      // 牌リストを順子として分類する(字牌は事前に抜いてあること)
      def junList(src: PL): Option[GRPTSU] = {
        // 牌種ごとにグループ分け
        val typl = grpGrouping(src)
        // 牌種ごとにインデックスごとの牌数リストに分解後、順序判定のためにソート
        val igmap = typl.mapValues(n => idxGrouping(n).map { m => (m._1, m._2.length) }.toList.sortWith { _._1 < _._1 })
        // 牌種ごとに順子への分解
        val resultmap = igmap.mapValues(junDivide(_)(List[Int]()))
        // 与えられた全てを順子に分解できたか判定し、分解出来ていた場合のみ結果を生成
        if (resultmap.values.exists(_.isEmpty)) None else Option(resultmap.transform { (grp, v) => v.get.map { idx => makeJun(pai(grp, idx)) } }.values.toList.flatten)
      }
    
      // 全組み合わせ生成関数
      def mkFullList(l: PL)(res: List[PL])(idx: Int): List[PL] = if (idx == 0) res else mkFullList(l)(res ++ l.combinations(idx))(idx - 1)
    
      // 刻子の全組み合わせケースで評価実行(槓子は分離されてるはずなので、事前抜出済みな前提)
      def evalSetKou(src: PL)(t: pai): List[GRPTSU] = {
        // 字牌(tsrc)とそれ以外(jsrc)に分離
        val (jsrc, tsrc) = src.partition(_.isJyun)
        // 字牌は刻子以外にはならないので、刻子以外が残ったらすでに評価できないこと確定
        val tkl = kouList(tsrc)
        if (tkl.length * 3 != tsrc.length) List()
        else {
          // 対子と字牌の刻子は決定物なのであらかじめ生成しておく
          val tlresult = List(makeToi(t)) ++ makeKouList(tkl)
          // 字牌のみの場合は確定
          if (jsrc.length == 0) List(tlresult)
          else {
            // 刻子のリストを取得
            val jkl = kouList(jsrc)
            // 全チェックパターン生成 (先頭は刻子として評価しないパターン)し順子割り当て
            mkFullList(jkl)(List(List()))(jkl.length).map {
              n =>
                {
                  // 刻子を省いた残りを順子に分解
                  val yl = junList(pListRemove(removeKou, jsrc, n))
                  // 分解出来たらここでの評価は成功とする
                  if (yl.isEmpty) List() else tlresult ++ makeKouList(n) ++ yl.get
                }
            }.filter { !_.isEmpty }.toList
          }
        }
      }
    
      // 役基本評価実行
      def evaluate(src: PL): List[GRPTSU] = {
        // 対子候補のリストを生成
        val toilist = toiList(src)
        // 七対子は特殊なのでここで処理
        val t7: List[GRPTSU] = if (toilist.length == 7) List(makeChitoi(toilist)) else List()
        // 対子を仮定した評価を実行 (+七対子の処理)
        (t7 ++ toilist.map { t => evalSetKou(removeToi(src)(t))(t) }.flatten).filter { !_.isEmpty }
      }
    
    }
    
    // テスト用のメインオブジェクト
    object testmain {
    
      def input: Option[String] = {
        val in = readLine
        if (in.length == 0) None else Some(in)
      }
    
      def main(args: Array[String]) = {
        println("START!");
        println("paiid = type * 10 + index");
        var s: Option[String] = Some("");
        while (s.isDefined) {
          print("Input pailist :");
          s = input
          if (s.isDefined) {
            val sp = s.get.trim.split(' ').toList.map { new pai(_) }
            val start = System.currentTimeMillis
            val result = yakuhyouka.evaluate(sp).toList
            println ((System.currentTimeMillis - start) + " msec")
            // 結果表示
            result.foreach {
              one =>
                {
                  one.foreach {
                    k =>
                      {
                        print("[")
                        k.foreach {
                          print
                        }
                        print("]")
                      }
                  }
                  print("\n")
                }
            }
          }
        }
        println("END!");
      }
    }
    
    コンパイル (sjisでファイルを書いたのでエンコードを指定してる)
    C:\scala\yakuhantei>scalac yakuhantei.scala -encoding sjis
    
    実行例 (ちなみに、41~ は字牌)
    C:\scala\yakuhantei>scala testmain
    START!
    paiid = type * 10 + index
    Input pailist :11 11 12 12 13 13 14 14 15 15 16 16 17 17
    16 msec
    [s1s1][s2s2][s3s3][s4s4][s5s5][s6s6][s7s7]
    [s1s1][s2s3s4][s2s3s4][s5s6s7][s5s6s7]
    [s4s4][s1s2s3][s1s2s3][s5s6s7][s5s6s7]
    [s7s7][s1s2s3][s1s2s3][s4s5s6][s4s5s6]
    Input pailist :11 11 11 12 12 12 13 13 13 14 14 14 15 15
    63 msec
    [s2s2][s1s1s1][s2s3s4][s3s4s5][s3s4s5]
    [s5s5][s1s1s1][s2s2s2][s3s3s3][s4s4s4]
    [s5s5][s1s1s1][s2s3s4][s2s3s4][s2s3s4]
    [s5s5][s4s4s4][s1s2s3][s1s2s3][s1s2s3]
    Input pailist :11 12 13 12 13 14 41 41 41 42 42
    15 msec
    [t2t2][t1t1t1][s1s2s3][s2s3s4]
    Input pailist :41 42 43 44 44
    0 msec
    Input pailist :
    END!
    

    | | Comments (14) | TrackBack (0)

    Apache 用 mod_auth_fb (Firebird用認証モジュール)新版公開

    Apache + Firebird RDBMS の組み合わせで簡単に BASIC 認証を行うことが出来る Apache 用モジュール (DSO) の新版の公開を開始しました。PHPなどを使用しなくてもデータベースを利用してベーシック認証を行うことが出来ますので、Firebird データベースにより簡単に認証を行いたい場合には非常に便利です。

    Windows版(バイナリ)、Linux版 (ソース)の両環境向けで、さらに Apache も 1.3 と 2.0 に対応しています。
    色々テストを行って処理内容を変更した結果、最初の公開バージョンと比べて安定度は大幅に向上しています。

    Firebird RDBMS データベース認証用 Apache DSOで公開しています。

    また、暗号化パスワードの使用をサポートするための UDF もこちらで公開中です。
    ストアドプロシ-ジャにより認証で使用するとなかなか便利です。
    ちなみに、テーブルを用いた認証でのデフォルトのパスワード照合処理は、Apache の標準機能に準じています。このため、設定するパスワードは htpasswd で生成したものである必要があります。

    | | Comments (1) | TrackBack (0)

    mod_auth_fb のテスト中に Windows版のApache 1.3 はマルチスレッド実装なことに気が付いた

    Firebird での BASIC 認証を実現するモジュール mod_auth_fb の負荷テストをしてて気が付いたんですけど、実は Windows 版の Apache 1.3 はマルチスレッドで実装されてるんですね。
    まぁ少し考えれば当たり前といえば当たり前だけど、あまり気にしてなかったので今まで気が付きませんでしたよ。
    なにかトラブルにあたらないと、人のソースを細かくは見ないですなぁ。

    あまり美しくはないんですが、面倒なんで Windows で動作するときはクリティカルセクションで排他制御することにしてしまいましたよ。
    ちなみに、Apache 2.0 用はマルチスレッドなのが分かっているので、最初から Apache のスレッド間排他関数でやってたんですけどね。うちでは MPM=worker の指定で動かしていますが、負荷テストで100万ヒット(並列度64)しても問題なく動いているようです。

    とりあえず、mod_auth_fb.so (mod_auth_fb.dll)の方は、今ドキュメントを書いてます。
    近々 Windows版、Linux版(ソース)をまとめて公開予定です。
    Linux 版は、プログラムがまったく出来ない人にはちょっと敷居高いかもしれませんが...。

    | | Comments (3) | TrackBack (0)

    mod_auth_fb改造中にWin版とUNIX系でのApacheのパスワード照合が違うことに気が付いたり

    たらたらとですが、mod_auth_fb.so(Apache+Firebird RDBMS 用 BASIC認証DSO)を Linux に移植中です。
    それで気が付いたんですが、Apache のパスワード照合関数は、実は Windows 向けのものと UNIX 系向けのものでは動作が違うんようですね。動きがおかしいのでApacheのソースを除いたらわかりました。

    以前から mod_auth_dbm では MD5/SHA1が動いてるので関数の中がバージョンによって動きが変わってるのかな?と思っていたんですが、実は動作環境によって違いがあることがわかりました。
    FreeBSD Style MD5ヘッダや Netscape / SHA1 ヘッダが格納データ(ハッシュなど)にない場合、Windowsでは平文を、その他の環境では crypt() を施したものが格納されているものとして処理していたんですね。
    どうりで、Windows版ではパスワードは平文で動作するはずだと思いました。

    というわけで、実は現状の Windows 版でも MD5/SHA1 は使えるようです。
    ただ、Apacheの内部関数と一致する内容・ヘッダでないと動作しませんけどね。

    あと、Linux の場合はどうしようか思案中です。
    拙作のMD5/SHA1計算UDFのMD5/SHA1で格納できる方がFirebirdがある環境においては扱いやすいような気もしますしね.....。一般的なスタイルは今のままでも使えるわけだし。
    ディレクティブを追加して平文比較オプションでも追加しようかな?

    | | Comments (0) | TrackBack (0)

    Firebird BASIC 認証サポート用 UDF (MD5/SHA1暗号化)

    長期放置に加えてドキュメント書きに手間取ってしまったのですが、Apache + Firebird でユーザー認証を行う "mod_auth_fb.so" の使用をサポートするUDFの公開を開始しました。Firebird 1.5.2 で動作確認してあります。
    MD5 または SHA1 での暗号化をサポートします。
    Windows版のバイナリと、全ソースが公開してありますので、他の環境への移植も容易です。

    Firebird RDBMS データベース認証サポート用 UDFで公開しています。
    アーカイブにランタイムを入れ忘れたので修正しました。(2005/06/21)

    | | Comments (0) | TrackBack (0)

    Apache 用 mod_auth_fb (Firebird用認証モジュール)公開開始

    ドキュメント書きに手間取ってしまったのですが、Apache + Firebird でユーザー認証を行う "mod_auth_fb.so" の公開を開始しました。
    Apache 1.3.x と 2.0.x に対応しています。
    3/3日現在、Linux 環境をロスト中なので、とりあえずは Windows 用のバイナリのみを公開しています。

    Firebird RDBMS データベース認証用 Apache DSOで公開しています。

    Linux 向けは環境が整備できてコンパイルなどが正しくできることを確認した後に公開する予定です。

    ※2005/07/11追記
    実は、良くあるBSDスタイルでヘッダ+暗号(ハッシュ値)で格納すれば、MD5やSHA1が現状でも使えますね。
    パスワードの認証にApacheの関数を使ってるので、そっちがサポートしてました。

    ※2005/07/28追記
    新たにLinux版も含めた新バージョンも公開しました。
    URLはこちらです。

    | | Comments (0) | TrackBack (1)

    Apache 用 mod_auth_fb (Firebird用認証モジュール)作成中

    先週末から、暇を見つけて"mod_auth_fb.so"なるものを作成中です。
    Apacheに詳しい人ならピンと来るでしょうが、Webサーバの Apache でデータベースを用いた認証を行うためのモジュール(DSO)という訳ですね。それの Firebird / InterBase 専用のものを作っているわけですが。
    MySQLなどには存在してるんですが、探した限りでは Firebird 用のものがないので自分で作ってしまおうという感じですな。mod_perl と DBI を組合わせても一応実現可能な訳ですが、それだとお手軽さに欠けますし、mod_perlを入れたくないような場合にも便利なので、やはり専用のも一応欲しいですしね。

    というわけで、Apacheモジュール作成と Firebird API に関しては初体験ですよ。
    サンプル見ながらこねくり回してます。

    今のところ Windows2000 + VC + Apache 1.3.x の組合わせ版は動作していて、これをベースに Apache 2.0.x 向けのものを製作中です。そこまでできたら、とりあえず Windows 用のバイナリ版だけでも公開してしまおうかと思っています。

    | | Comments (1) | TrackBack (0)

    «*istD / *istDS 関係を独立させました。