RustのLT会 Shinjuku.rs #13 の参加メモ

FORCIAさん主催のオンライン勉強会 RustのLT会 Shinjuku.rs #13 に参加してきました。オンライン開催です。

forcia.connpass.com

LT#1: RustでPostgresの拡張を書く ( @matsu7875 さん)

発表資料は https://speakerdeck.com/matsu7874/write-postgres-extensions-with-rust です。

タイトルの通り、Rustでユーザ定義関数を実装する、という内容の発表でした。

私を含め、そもそも拡張できるんだ!と驚かれた方も多かったと思います (PostgreSQLのドキュメント) 。 このユーザ定義関数をC言語 (またはCと互換する言語) で記述し、動的ロード可能なオブジェクト (共有ライブラリ) として提供することで、PostgreSQLから実行できるそうです。

発表では、Rustで実装するときに使えるフレームワークとしてzombodb/pgxを紹介されていました。詳細はスライド参照なのですが、名前や型変換のルールなどは熟知する必要があるのですが、C言語実装と比較して機能差があるわけではなさそうなので、保守しやすい言語・環境で実装できるメリットは大きい、と思いました。

こちらのブログエントリ RustでPostgreSQLのユーザー定義関数を書く│FORCIA CUBE│フォルシア株式会社 では速度についても簡単に比較されており、C言語実装と差はない (Rustのほうがやや速い) ようでした。

www.forcia.com

LT#2: RustでKeyValueStoreをつくる ( @ymgyt さん)

発表資料は https://docs.google.com/presentation/d/12TdMPOAOoVOZugjdTA7O_Y630YHK-wtMmXmw0sn7_Ys/edit です。

KvsdというKVSを開発されています。今回の発表ではTokioを使った client / server システムの実装やテストがメインテーマでした。

github.com

通信部分は、Tokioのサンプルプロジェクトである mini-redis を参考に、TCP上で自前のプロトコルを実装されています。開発時に役立つtipsをいくつも紹介されてました。

  • tokio::sync::Semaphoreを使ったコネクション数制限や、シグナルハンドリングの実装
  • tokio::io::duplexでコネクションの両端を作ることで、TCP listnerを作らずにテストできる
  • TokioでTLSを使うためのラッパとしてtokio_rustlsが便利

ymgytさんのブログエントリで更に詳しい解説がありますのでぜひご覧ください。

blog.ymgyt.io

Python: tenacityでリトライを実装する

信頼できない環境で稼働するアプリケーションには、リトライ処理が不可欠です。

難しくはないので自前で実装してしまうのですが、特定の例外のみリトライしたい・リトライ間隔を指数関数的に増やしたい・リトライ時はログ出力したいなどの細かなリクエストをそれぞれ記述してしまうと、本質的な処理が追いづらくなるので、可能であればライブラリを使って解決したくなります。

tenacity

リトライを簡単に実装するためのPythonライブラリにもいくつかあるのですが、今回は最近でもアップデートされている tenacity を紹介します。類似ライブラリとしてretryretryingが挙げられますが、いずれも数年以上リリースされていません。

pip install tenacityでインストールできます。以降のコードはこちらのバージョンで実行してます。

$ python --version        
Python 3.8.6

$ pip list | grep tenacity
tenacity         6.2.0

retryを関数 (ここでは一定確率で失敗するfail_sometimesを定義) にアノテーションするだけでリトライ処理が実装できます。引数無しですと、成功するまで間隔を開けずに繰り返されます。

import random
import time
from tenacity import retry

@retry
def fail_sometimes(n: int):
    v = random.choice(list(range(n)))
    time.sleep(1)
    print("v:", v)
    if v > 0:
        raise IOError("Error")

fail_sometimes(10)

停止条件

retry関数はRetryingのラッパーなので、Retryingのコンストラクタを見ると設定可能なパラメータがわかります。

例えばstopには、リトライする回数 (stop_after_attempt) や制限時間 (stop_after_delay) を設けることができます。

from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(3))  # 合計3回まで
def fail_sometimes(n: int):
    # ...

fail_sometimes(10)
# 実行すると3回まで試して、それでもダメならIOError
# $ python tenacity_example/main.py
# v: 6
# v: 4
# v: 2
# Traceback (most recent call last):
# ...
# IOError: Error

stop_after_delayはリトライ時に指定した秒数を経過していなければ処理開始します。処理の途中で打ち切られることはありません。

from tenacity import retry, stop_after_delay

@retry(stop=stop_after_delay(3))  # 開始から3秒以内ならリトライ
def fail_sometimes(n: int):
    v = random.choice(list(range(n)))
    time.sleep(2)
    print("v:", v)
    if v > 0:
        raise IOError("Error")

fail_sometimes(10)
# $ python tenacity_example/main.py
# v: 9
# v: 5
# Traceback (most recent call last):
# ...
# IOError: Error

リトライ間隔

リトライ間隔はwaitパラメータで設定します。

一定時間の場合はwait_fixed、ランダム時間の場合はwait_random、指数関数で増やす場合はwait_exponentialをそれぞれ使います。

import random
import time
from datetime import datetime
from tenacity import retry, wait_fixed

@retry(wait=wait_fixed(2))  # 2秒間隔でリトライ
def fail_sometimes(n: int):
    v = random.choice(list(range(n)))
    print(datetime.now(), "v:", v)
    if v > 0:
        raise IOError("Error")

fail_sometimes(5)
# $ python tenacity_example/main.py
# 2020-11-21 15:20:34.616393 v: 1
# 2020-11-21 15:20:36.618325 v: 4
# 2020-11-21 15:20:38.623362 v: 0

リトライ条件

特定の例外のみリトライして、それ以外はそのままraiseしてほしいケースがあります。retryパラメータにretry_if_exception_typeを渡すことで実現できます。

以下の場合、IOErrorのみリトライして、それ以外の例外はraiseされて終了します。

from tenacity import retry, retry_if_exception_type

@retry(retry=retry_if_exception_type(IOError))
def fail_sometimes(n: int):
    v = random.choice(list(range(n)))
    print(datetime.now(), "v:", v)
    if v == 1:
        raise ValueError("Value Error")
    elif v > 1:
        raise IOError("IO Error")

fail_sometimes(5)
# $ python tenacity_example/main.py
# 2020-11-21 15:34:06.285533 v: 2
# 2020-11-21 15:34:06.285752 v: 1
# Traceback (most recent call last):
# ...
# ValueError: Value Error

ログ出力

リトライ時にログ出力もbeforeやafterのパラメータで設定できます。以下ではafter_logを使ってリトライ時にログ出力するようにしています。

import logging
from tenacity import retry, after_log

logger = logging.getLogger(__name__)

@retry(after=after_log(logger, logging.WARNING))
def fail_sometimes(n: int):
    v = random.choice(list(range(n)))
    print(datetime.now(), "v:", v)
    if v > 0:
        raise IOError("IO Error")

fail_sometimes(5)
# $ python tenacity_example/main.py
# 2020-11-21 15:53:27.852080 v: 4
# Finished call to '__main__.fail_sometimes' after 0.000(s), this was the 1st time calling it.
# 2020-11-21 15:53:27.852484 v: 0