刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

FlatpakのPPSSPPがv1.11.3になったら、コントローラが効かなくなったので

昨日、KDE の Wayland セッションの動作確認をする際、久しぶりに PPSSPP を起動したところ、自動的にアップデートされました。

$ flatpak list
Name                Application ID                Version  Branch Installation
Freedesktop Platfo… org.freedesktop.Platform      20.08.14 20.08  system
Mesa                …edesktop.Platform.GL.default 21.1.4   20.08  system
nvidia-460-91-03    …Platform.GL.nvidia-460-91-03          1.4    system
openh264            …reedesktop.Platform.openh264 2.1.0    2.0    system
GNOME Application … org.gnome.Platform                     40     system
Breeze Gtk theme    org.gtk.Gtk3theme.Breeze               3.22   system
PPSSPP              org.ppsspp.PPSSPP             1.11.3   stable system
$ flatpak run org.ppsspp.PPSSPP --version
v1.11.3

早速、PS3 用のコントローラ (HORI PAD 3 MINI) を接続して、動作確認してみました。

しかし、結果は NG 。コントローラが期待通りに動作しません。
過去に v1.10.3 で調査した際は問題なかったのに、まるで初期状態のようです。

念の為、assets ディレクトリの所在を確認したところ、 パスが変更になってました。

・v1.10.3 の assets ディレクト

/var/lib/flatpak/app/org.ppsspp.PPSSPP/current/active/files/ppsspp/assets/

・v1.11.3 の assets ディレクト

/var/lib/flatpak/app/org.ppsspp.PPSSPP/current/active/files/share/ppsspp/assets/

パスの変更に伴い、assets/gamecontrollerdb.txtの内容も初期化されてました。
コントローラが効かないのも納得です。

....

対処として、以前の記事と同様に、新しいパスに存在する gamecontrollerdb.txt に HORI PAD 3 MINI の定義を追記したところ、無事にコントローラが動作しました。

少し焦りましたが、元通りになって良かったです。

GPUがGeForceなのでKDE Plasma 5.22のWaylandセッションはそこそこに

Ubuntu 21.04 では Wayland セッションが既定になりました。

しかし、私の PC 環境では GPUGeForce 1050Ti なので Xorg のままです。
常用環境も KDE neon (Ubuntu 20.04 LTS ベース) が快適なので、まったく困りません。

とは言うものの、今のままなら次の LTS である Ubuntu 22.04 で Wayland が採用される可能性が高いです。

KDE neonUbuntu の 最新 LTS をベースにしているので、Wayland に関する準備を何もしてないと、いざという時に困りそうです。

....

現在、私の PC 環境は次のような構成です。

CPU AMD Ryzen 5 2600X
M/B GIGABYTE B450 AORUS PRO WIFI (BIOS: F32)
GPU NVIDIA GeForce GTX 1050 Ti
OS KDE neon User Edition 5.22
KDE Plasma Version: 5.22.4
KDE Frameworks Version: 5.84.0
Qt Version: 5.15.3
Kernel 5.11.0-25-generic
GPU Driver NVIDIA Driver Version: 460.91.03

当然、既定のセッションは X11 (Xorg) です。

NVIDIA 側の Wayland に対する諸々も聞き及んでます。
それでも Wayland を試したい場合は、KDE Community Wiki の記事が参考になります。

このサイトの記述に従えば現状でも Wayland を体験できます。
ただし、多少の不具合には目を瞑りましょう。

1. 事前確認

NVIDIA GPU のドライバはプロプライエタリなものです。
そして、KDE Plasma と Qt のバージョンは条件を満たしています。

後は Nvidia egl library が必要との事なので確認。

$ apt search libnvidia-egl-wayland1
ソート中... 完了
全文検索... 完了  
libnvidia-egl-wayland1/focal,now 1:1.1.3-1ubuntu1 amd64 [インストール済み]
  Wayland EGL External Platform library -- shared library

存在しなければ、sudo apt install libnvidia-egl-wayland1を実行します。

2. カーネル パラメータ の確認とログイン

記事に従い、現状のパラメータ値を確認します。

$ sudo cat /sys/module/nvidia_drm/parameters/modeset
Y

この結果が N の場合は、パラメータ値を変更する必要があります。

いくつか方法があるようですが、私は/etc/default/grubを編集しました。

  1. エディタで/etc/default/grubを開く。
  2. GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" の行を探す。
  3. GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nvidia-drm.modeset=1" にする。
  4. 保存して終了する。
  5. ターミナルからsudo update-grubを実行する。
  6. 再起動。
  7. ログイン時、デスクトップセッションを Plasma: (Wayland) に変更してからログインする。
  8. Wayland セッションを評価する。

設定は以上です。

ログイン後に nvidia のモジュールを確認すると、こんな感じです。

$ nvidia-smi
Thu Jul 29 08:01:00 2021       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.91.03    Driver Version: 460.91.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  GeForce GTX 105...  Off  | 00000000:06:00.0  On |                  N/A |
| 20%   40C    P5    N/A /  75W |    281MiB /  4036MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      3279      G   /usr/lib/xorg/Xorg                 20MiB |
|    0   N/A  N/A      3366      G   kwin_wayland                      140MiB |
|    0   N/A  N/A      3468      G   /usr/bin/plasmashell               99MiB |
+-----------------------------------------------------------------------------+
$ lsmod | grep nvidia
nvidia_uvm           1011712  0
nvidia_drm             57344  8
nvidia_modeset       1228800  5 nvidia_drm
nvidia              34168832  601 nvidia_uvm,nvidia_modeset
drm_kms_helper        237568  1 nvidia_drm
drm                   548864  11 drm_kms_helper,nvidia_drm
$

3. 所感

ざっと試した感じでは、以前に比べて格段に進歩してます。
FirefoxChrome 等は X11 セッション と遜色なく利用できてます。

GeForce なのに Wayland が普通に使えるなんて、なんか気持ち悪いなー

....

しかし、私が利用してる環境では問題があるため、残念ながら常用はできません。

特に問題なのが KDE の「アプリケーションメニュー」でコンテキストメニューやチップヘルプのウィンドウが正しく表現されない事です。

特にメニューの階層をマウスで辿ろうとすると、最初に描画されたウィンドウの内容のまま更新されないという、珍妙な不具合があります。

Application Dashboard が推しだから代替は後回しにされているのか、それとも GPU ドライバの問題なのか。
X11 セッションでは正常なので、どちらの問題なのでしょう?

そして、Flatpak な PPSSPP や i386 な PCSX2 も色々と問題が生じます。
特に CPU 使用率は X11 セッションではありえないくらい酷い状況です。

もし改善しないのであれば、別途 PC 環境を用意して X11 セッション専用にするのも仕方ないのかな。

....

Ubuntu 22.04 のリリースまで約 1 年を切りましたが、それまでは OS アップデートの都度、ちまちま確認していこうと思います。

手持ちPCのWindows 11対応を確認して一安心

Windows 11 が発表され、割と厳しめな要求スペックに今までとの違いを感じてます。

先日 Windows 11 の互換性チェックプログラムがリリースされたので、私も手持ちの PC について確認してみました。 

CPU AMD Ryzen 5 2600X
M/B GIGABYTE B450 AORUS PRO WIFI (BIOS: F32)
GPU NVIDIA GeForce GTX 1050 Ti
UEFI CSM UEFI CSM Support は Enabled
BIOS で「BIOS」メニューの「CSM Support」を Disabled に設定していたが、既定値の Enabled に戻した。
DISK GPT 形式で EFI パーティションが必要。
Windows 10 インストール時は手違いで MBR 形式でインストール実施。
その後 UEFI Boot にするため、MBR 形式から GPT 形式に変換済み。
TPM TPM 2.0
BIOS で「Peripherals」メニューの「AMD CPU fTPM」を Enabled に設定。
「Trusted Computing 2.0」の各設定は既定値のまま。
SVM AMD SVM Mode は Enabled
BIOS で「M.I.T」メニューの「Advanced Frequency Settings」から「Advanced CPU Core Settings」に進み「SVM Mode」を Enabled に設定。

この構成で「PC 正常性チェック」をインストールし、実行したところ、無事に「この PC で Windows 11 を実行できます」と表示されました。

いつもなら気にしないのですが、これで一安心です。

今のところ、既存 PC の対応状況は 2018 年付近で明暗を分けてるみたいですね。
本家本元 Microsoft からの情報も何やら錯綜してるので、しばらくは傍観に徹します。

....

MBR 形式から GPT 形式に変換したディスクについて少し補足します。

Windows 10 をインストールする際、UEFI Boot で USB メモリからインストーラを起動するのですが、どうやら起動選択を誤ったようです。

最終的にmbr2gptコマンドでディスクを GPT 形式に変換したのですが、そのパーティション構成はこんな感じです。

f:id:infinity_volts:20210629132017j:plain

ディスク 0 が Windows 10 の起動ディスクです。
変換により EFI パーティションが 通常とは異なるパーティション位置にありますが、特に問題なく利用できてます。

Windows 11 では UEFI ブートが必須になりましたが、このような構成でも大丈夫みたいです。

MacBook Pro Mid 2014で再起動時にBluetooth機器を見失う

私は MacBook Pro Mid 2014 を常時クラムシェルで利用してます。

諸事情により macOS は Mojave で留めてますが、ずっと快適に利用してました。

ところが、3 つくらい前のセキュリティアップデートからでしょうか。
再起動して、直後のログイン画面で Bluetooth 接続のキーボードと Magic Trackpad を見失うようになりました。

クラムシェルを解除すれば先に進めますが、あまりに不便です。

先日、不本意ながら NVRAM クリアを試したところ、この症状は発生しなくなりました。

とりあえず解決しましたが、こういう問題が発生するのは嬉しくないです。

....

最初は機器故障や電波干渉を疑いました。

クラムシェルを解除して、電源投入時のように MacBook Pro のキーボードからログインすれば、通常通り Bluetooth 機器に接続できます。
接続してしまえば、クラムシェルに戻しても接続が切れる事はありません。

また、スリープ状態へ移行し、スリープから復帰しても問題ありません。

再起動時の初回だけ見失うのであれば、故障ではなく別に原因があるはず。
故障でなければ、そのうちアップデートで治るだろう、と放置してました。

今回は NVRAM クリアで症状はなくなりましたが、再発する可能性もあります。
原因が明確でない対処療法は、とにかく気持ち悪いです。

NVRAM クリアする前にnvramコマンドで設定値確認するのを忘れてたのは痛い。

....

MacBook Pro Mid 2014 は macOS Monterey ではサポート対象外になりました。
初期 macOS Big Sur での文鎮化問題を鑑みれば然もありなん。

そろそろ Apple Silicon Mac の購入を検討する段階ですかね。

RustでSQLServerへの接続プールにdeadpoolを試してみた

以前、PostgreSQL で非同期な接続プールに deadpool を利用したことがあります。

PostgreSQL への接続では deadpool を直接扱うのではなく、deadpool-postgres (w/ tokio-postgres) が用意されているので、そちらを利用します。

deadpool 自体は汎用的な接続プールなので、tiberius と組み合わせて、構造体やトレイトを実装すれば、SQLServer への接続プールとして利用できるのですが、今のところ crates.io で探しても該当するものはありません。

私の場合、deadpool の Managed pool が利用できれば十分なので、とりあえず tokio ランタイムで動作するようにしてみました。

1. 環境とデータベース

Rustでasync/awaitに対応したTiberiusからSQLServerに接続する

※ここで構築した環境を引き続き利用します。

  • OS: KDE neon 5.21.5 (Ubuntu 20.04 ベース) / macOS Mojave v10.14.6
  • rustc 1.52.1
  • SQL Server 2019 (RTM-CU10) (KB5001090) - 15.0.4123.1 (X64) on Linux
  • データベース: my_test_db

2. プロジェクト

(1) 準備

適当なディレクトリにプロジェクトを作成します。(今回は ~/work/pool_tokio)

$ cd ~/work
$ cargo new pool_tokio
$ cd pool_tokio
(2) Cargo.toml の編集

[dependencies] セクションに下記の内容を追加します。

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.6", features = ["compat"] }
async-trait = "0.1"
deadpool = "0.8"
tiberius = { version = "0.5", features = ["chrono"] }
chrono = "0.4"
futures = "0.3"
(3) ソースコード: src/mssql.rs

プロジェクトの src ディレクトリに下記のソースコードmssql.rs として保存します。 

use async_trait::async_trait;
use tokio_util::compat::TokioAsyncWriteCompatExt;

pub struct Manager {
    config: tiberius::Config,
}

impl Manager {
    pub fn new(config: tiberius::Config) -> Self {
        Self { config }
    }
}

type Client = tiberius::Client<tokio_util::compat::Compat<tokio::net::TcpStream>>;

pub struct ClientWrapper {
    client: Client,
}

impl ClientWrapper {
    fn new(client: Client) -> Self {
        Self { client }
    }
}

impl std::ops::Deref for ClientWrapper {
    type Target = Client;
    fn deref(&self) -> &Client {
        &self.client
    }
}

impl std::ops::DerefMut for ClientWrapper {
    fn deref_mut(&mut self) -> &mut Client {
        &mut self.client
    }
}

#[async_trait]
impl deadpool::managed::Manager for Manager {
    type Type = ClientWrapper;
    type Error = tiberius::error::Error;

    async fn create(&self) -> Result<Self::Type, Self::Error> {
        let tcp = tokio::net::TcpStream::connect(self.config.get_addr()).await?;
        tcp.set_nodelay(true)?;
        let client = Client::connect(self.config.clone(), tcp.compat_write()).await?;
        let client_wrapper = ClientWrapper::new(client);
        Ok(client_wrapper)
    }

    async fn recycle(&self, _: &mut Self::Type) -> deadpool::managed::RecycleResult<Self::Error> {
        Ok(())
    }
}

数日前に deadpool version = "0.8.0" で deadpool::managed::Manager トレイトの仕様が変更されたので、それに合わせてあります。

(4) ソースコード: src/main.rs

下記のソースコードをコピーして src/main.rs を書き換えます。
※ソース中の conn_str の内容は、試す環境に合わせて変更してください。

mod mssql;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("#### Start ####");
    let started = std::time::Instant::now();

    let conn_str = "Server=tcp:localhost,1433;TrustServerCertificate=true;Database=my_test_db;UID=sa;PWD=abcd1234$";
    let config = tiberius::Config::from_ado_string(conn_str)?;
    let manager = mssql::Manager::new(config);
    let pool = deadpool::managed::Pool::<mssql::Manager>::new(manager, 10);

    let mut threads = Vec::new();
    for idx in 0..20 {
        let pool = pool.clone();
        let handle = tokio::spawn(async move {
            println!("Thread #{}", idx);
            let mut conn = pool.get().await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
            let _ = conn
                .simple_query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 誕生日 DESC")
                .await
                .unwrap()
                .into_first_result()
                .await
                .unwrap()
                .iter()
                .map(|row| {
                    println!(
                        "#{} | {} | {} | {} |",
                        idx,
                        row.get::<i32, _>("番号").unwrap(),
                        row.get::<&str, _>("氏名").unwrap(),
                        row.get::<chrono::NaiveDate, _>("誕生日")
                            .unwrap()
                            .format("%Y/%m/%d"),
                    )
                })
                .collect::<Vec<_>>();
        });
        threads.push(handle);
    }
    futures::future::join_all(threads).await;

    println!("#### Finish ####");
    println!("経過時間: {:?}", started.elapsed());
    Ok(())
}

準備は以上です。

3. 実行結果

接続プールの最大値を 10 に設定した場合の実行結果です。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/pool_tokio`
#### Start ####
Thread #0
Thread #1
Thread #4

・・・(ざっくり省略)・・・

#18 | 105 | 江口 美奈 | 1979/06/23 |
#13 | 307 | 中井 雄樹 | 1984/02/29 |
#11 | 307 | 中井 雄樹 | 1984/02/29 |
#13 | 105 | 江口 美奈 | 1979/06/23 |
#11 | 105 | 江口 美奈 | 1979/06/23 |
#### Finish ####
経過時間: 554.619646ms
$ 

4. おまけ

deadpool = "0.7" の場合、src/mssql.rs の内容はこちらになります。

use async_trait::async_trait;
use tokio_util::compat::TokioAsyncWriteCompatExt;

pub struct Manager {
    config: tiberius::Config,
}

impl Manager {
    pub fn new(config: tiberius::Config) -> Self {
        Self { config }
    }
}

type Connection = tiberius::Client<tokio_util::compat::Compat<tokio::net::TcpStream>>;
type Error = tiberius::error::Error;

#[async_trait]
impl deadpool::managed::Manager<Connection, Error> for Manager {
    async fn create(&self) -> Result<Connection, Error> {
        let tcp = tokio::net::TcpStream::connect(self.config.get_addr()).await?;
        tcp.set_nodelay(true)?;
        let client = tiberius::Client::connect(self.config.clone(), tcp.compat_write()).await?;
        Ok(client)
    }
    async fn recycle(&self, _conn: &mut Connection) -> deadpool::managed::RecycleResult<Error> {
        Ok(())
    }
}

その際、src/main.rs のlet pool = 〜の行も修正します。

    let pool = deadpool::managed::Pool::new(manager, 10);

deadpool = "0.7" では接続プールから接続そのものを取得する仕様だったのですが、deadpool = "0.8" からオブジェクトを取得するようになりました。

今日はこの事実に気づかず、先週まで動作してたプロジェクトがコンパイルエラーになって焦りました。

私の用途だと、前の仕様のままでも十分なんですけどね。

....

ここでは tokio での実装サンプルを掲載しましたが、async-std でも同様でした。

ライブラリ化するなら、bb8-tiberius と deadpool-postgres を参考に Managed pool と Unmanaged pool を実装して、Config に対応する感じでしょうか。

mobc も同様のノリで実装してみましたが、Web 系フレームワークとの組み合わせでは deadpool の方が性能良かったので、deadpool を採用しました。
※あくまで私が試した環境での話です。

tokio では rweb、async-std では tide との組み合わせで、ほぼ期待通りの結果が得られています。

RustでSQLServerへの接続プールにbb8 / bb8-tiberiusを試してみた

バックエンドで Tiberius を利用する非同期な接続プールに bb8 があります。

bb8 は SQLServer 用のアダプタ bb8-tiberius と組み合わせて使用します。
※async-std ではなく tokio 用です。

....

接続プールの使用方法はシンプルなので、前述したサイトの説明に従ってコードを記述すれば問題ないと思います。

接続プールを構築する際、接続数の最大値を .max_size() で指定できるのですが、この値の増減による実行結果を確認してみました。

1. 環境とデータベース

Rustでasync/awaitに対応したTiberiusからSQLServerに接続する

※ここで構築した環境を引き続き利用します。

  • OS: KDE neon 5.21.4 (Ubuntu 20.04 ベース) / macOS Mojave v10.14.6
  • rustc 1.51.0
  • SQL Server 2019 (RTM-CU10) (KB5001090) - 15.0.4123.1 (X64) on Linux
  • データベース: my_test_db

2. プロジェクト

(1) 準備

適当なディレクトリにプロジェクトを作成します。(今回は ~/work/conn_pool)

$ cd ~/work
$ cargo new conn_pool
$ cd conn_pool
(2) ファイル

・Cargo.toml の編集

[dependencies] セクションに下記の内容を追加します。

[dependencies]
tiberius = { version = "0.5", features = ["chrono"] }
bb8 = "0.7"
bb8-tiberius = "0.5"
tokio = { version = "1.5", features = ["time"] }
futures = "0.3"
chrono = "0.4"

ソースコード: src/main.rs

下記のソースコードをコピーして src/main.rs を書き換えます。
※ソース中の conn_str の内容は、試す環境に合わせて変更してください。

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("#### Start ####");
    let started = std::time::Instant::now();

    let conn_str = "Server=tcp:localhost,1433;TrustServerCertificate=yes;Database=my_test_db;UID=sa;PWD=abcd1234$";
    let manager = bb8_tiberius::ConnectionManager::build(conn_str)?;
    let pool = bb8::Pool::builder().max_size(10).build(manager).await?;

    let mut threads = Vec::new();
    for idx in 0..20 {
        let pool_tmp = pool.clone();
        let handle = tokio::spawn(async move {
            println!("Thread #{}", idx);
            let mut client = pool_tmp.get().await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
            let result = client
                .simple_query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 誕生日 DESC")
                .await
                .unwrap()
                .into_first_result()
                .await
                .unwrap();
            for row in result {
                println!(
                    "#{} | {} | {} | {} |",
                    idx,
                    row.get::<i32, _>("番号").unwrap(),
                    row.get::<&str, _>("氏名").unwrap().to_string(),
                    row.get::<chrono::NaiveDate, _>("誕生日")
                        .unwrap()
                        .format("%Y/%m/%d"),
                );
            }
        });
        threads.push(handle);
    }
    futures::future::join_all(threads).await;

    println!("#### Finish ####");
    println!("経過時間: {:?}", started.elapsed());
    Ok(())
}

このコードでは、接続プールの最大値を 10 とし、非同期タスクを 20 個実行します。
また、SQL クエリの実行時間が短いため、敢えて非同期タスクの中で 200 ミリ秒待機しています。
最後に、すべての非同期タスクが完了した後、処理の経過時間を表示します。

準備は以上です。

3. 実行結果

.max_size(10) として実行した結果です。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/conn_pool`
#### Start ####
Thread #0
Thread #2
Thread #1
Thread #3
Thread #4

・・・(ざっくり省略)・・・

#11 | 307 | 中井 雄樹 | 1984/02/29 |
#19 | 105 | 江口 美奈 | 1979/06/23 |
#16 | 307 | 中井 雄樹 | 1984/02/29 |
#11 | 105 | 江口 美奈 | 1979/06/23 |
#16 | 105 | 江口 美奈 | 1979/06/23 |
#### Finish ####
経過時間: 539.765411ms

4. 実行結果のまとめ

今回の処理では、経過時間は接続プールの最大接続数によって変化します。

最大接続数を 1 から 10 まで変化させた場合の経過時間をまとめました。
参考までに最大接続数を非同期タスク数と同じ 20 に設定した場合の結果も載せています。

最大接続数 1回目 2回目 3回目 4回目 5回目
.max_size(1) 4.243623s 4.17383s 4.180024s 4.175134s 4.174535s
.max_size(2) 2.416127s 2.159097s 2.159124s 2.155713s 2.17077s
.max_size(3) 1.773368s 1.544799s 1.553429s 1.552768s 1.58303s
.max_size(4) 1.178223s 1.141275s 1.13779s 1.14564s 1.154331s
.max_size(5) 938.821ms 943.597ms 945.557ms 933.896ms 948.629ms
.max_size(6) 942.27ms 935.486ms 941.867ms 946.894ms 936.995ms
.max_size(7) 822.035ms 737.501ms 744.878ms 743.485ms 752.166ms
.max_size(8) 750.744ms 741.996ms 744.562ms 731.633ms 747.467ms
.max_size(9) 754.524ms 741.767ms 743.779ms 752.383ms 739.935ms
.max_size(10) 538.341ms 539.604ms 542.781ms 541.15ms 537.545ms
.max_size(20) 349.346ms 355.685ms 352.035ms 354.031ms 349.371ms

非同期な接続プールとして特に問題なく、期待通りの結果になりました。

当然の事ですが、この結果は 1 接続あたりの処理数によって変化しています。
最大接続数が 20 の場合、1 接続 1 処理になるので、この結果が最速です。

また、最大接続数が 10 〜 19 の場合、1 接続あたり 1 または 2 処理になるので経過時間に大きな変化はありません。そのため、最大接続数 10 以外は省略しました。

4. 補足

データベースへの接続をここまで簡単に非同期タスクに渡せるのは本当に助かります。

最初は async-std に対応している SQLServer 用の接続プールを探してたのですが、現時点では見つけられませんでした。

bb8-tiberius は features の設定で async-std にも対応してるみたいなので、async-std でコードを書きたい場合は bb8 の対応や代替クレートを待つ感じでしょうか。

個人的には tokio でも特に問題ないので、このまま利用したいと思います。

 

Rustでasync/awaitに対応したTiberiusからSQLServerに接続する

一年くらい前に Rust から ODBC ドライバで接続する方法を試してました。

その当時、直接 TDS プロトコルを扱う tiberius クレートも試してみたのですが、依存するクレートのバージョンを固定する必要があり、コードが書き辛い状態でした。

しかし、改めて確認したところ、いつの間にやら async/await に対応した tiberius クレートの開発が進んでます。

tokio だけでなく async-std にも対応しているとの事なので、今回は async-std を利用して CRUD なコードを試してみました。

1. 環境

2. プロジェクト

(1) 準備

適当なディレクトリにプロジェクトを作成します。(今回は ~/work/crud_mssql)

$ cd ~/work
$ cargo new crud_mssql
$ cd crud_mssql
(2) データベース

テスト用のデータベースを用意します。(今回は my_test_db)
※既存のデータベースを利用する場合は不要です。

$ sqlcmd -S localhost -U sa -P abcd1234$ -Q "CREATE DATABASE my_test_db;"
(3) ファイル

・Cargo.toml の編集

[dependencies] セクションに下記の内容を追加します。

[dependencies]
tiberius = { version = "0.5", features = ["chrono"] }
async-std = { version = "1.9", features = ["attributes"] }
chrono = "0.4"

ソースコード: src/main.rs

下記のソースコードをコピーして src/main.rs を書き換えます。
※ソース中の conn_str の内容は、試す環境に合わせて変更してください。

use async_std::net::TcpStream;
use tiberius::{Client, ColumnData, Config, FromSqlOwned, QueryResult};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("#### Start ####");

    let conn_str = "Server=tcp:localhost,1433;TrustServerCertificate=yes;Database=my_test_db;UID=sa;PWD=abcd1234$";
    let config = Config::from_ado_string(conn_str).unwrap();
    let tcp = TcpStream::connect(config.get_addr()).await?;
    tcp.set_nodelay(true)?;
    let mut client = Client::connect(config, tcp).await?;

    println!("-- DROP & CREATE TABLE");
    client.execute("DROP TABLE IF EXISTS 会員名簿", &[]).await?;
    client
        .execute(
            "CREATE TABLE 会員名簿 (番号 int, 氏名 nvarchar(40), 誕生日 date)",
            &[],
        )
        .await?;

    println!("-- INSERT");
    let members = vec![
        (110, "岸本 龍也", "1989-11-06"),
        (210, "荒井 伸次郎", "1974-01-30"),
        (105, "江口 美奈", "1979-06-23"),
        (304, "長田 隆次", "1991-05-25"),
        (307, "中居 雄樹", "1984-02-29"),
    ];
    for (id, name, birthday) in members {
        let inserted = client
            .execute(
                "INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (@P1, @P2, @P3)",
                &[&id, &name, &birthday],
            )
            .await?;
        assert_eq!(&[1], inserted.rows_affected());
    }
    let result = client.simple_query("SELECT * FROM 会員名簿").await?;
    display(result).await?;

    println!("-- UPDATE");
    let update_id = 307;
    let new_name = "中井 雄樹";
    let updated = client
        .execute(
            "UPDATE 会員名簿 SET 氏名=@P1 WHERE 番号=@P2",
            &[&new_name, &update_id],
        )
        .await?;
    assert_eq!(&[1], updated.rows_affected());
    let result = client
        .simple_query("SELECT 番号,氏名 FROM 会員名簿 ORDER BY 番号")
        .await?;
    display(result).await?;

    println!("-- DELETE");
    let delete_id = 210;
    let deleted = client
        .execute("DELETE FROM 会員名簿 WHERE 番号=@P1", &[&delete_id])
        .await?;
    assert_eq!(&[1], deleted.rows_affected());
    let result = client
        .simple_query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 番号")
        .await?;
    display(result).await?;

    println!("#### Finish ####");
    Ok(())
}

async fn display(result_set: QueryResult<'_>) -> Result<(), Box<dyn std::error::Error>> {
    let cols = result_set.columns().unwrap();
    for col in cols {
        print!(" | {}", col.name());
    }
    println!(" |");

    let rows = result_set.into_first_result().await?;
    let rows_affcted = rows.len();
    let mut row_count = 0;
    for row in rows {
        for col in row {
            match col {
                ColumnData::I32(Some(v)) => print!(" | {}", v),
                ColumnData::String(Some(v)) => print!(" | {}", v),
                ColumnData::Date(_) => {
                    print!(" | {}", chrono::NaiveDate::from_sql_owned(col)?.unwrap())
                }
                _ => print!(" | {:?}", col),
            };
        }
        println!(" |");
        row_count += 1;
    }
    println!("結果 {} 行 ({})", row_count, rows_affcted);
    Ok(())
}

準備は以上です。

3. 実行結果

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 1.81s
     Running `target/debug/crud_mssql`
#### Start ####
-- DROP & CREATE TABLE
-- INSERT
 | 番号 | 氏名 | 誕生日 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 210 | 荒井 伸次郎 | 1974-01-30 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中居 雄樹 | 1984-02-29 |
結果 5 行 (5)
-- UPDATE
 | 番号 | 氏名 |
 | 105 | 江口 美奈 |
 | 110 | 岸本 龍也 |
 | 210 | 荒井 伸次郎 |
 | 304 | 長田 隆次 |
 | 307 | 中井 雄樹 |
結果 5 行 (5)
-- DELETE
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中井 雄樹 | 1984-02-29 |
結果 4 行 (4)
#### Finish ####

4. 補足

前述のコードでは async-std で属性によるランタイムの起動を指定してます。
初めての async-std だったのですが、async_std::task::spawn() する良い例が思い浮かばなかったので、CRUDの処理をそのまま書いてます。

この件とは別にコードを書いて試したところ、ODBC 接続を試した際に微妙だった日本語や絵文字や結合文字等は、特に問題ありませんでした。
日付型や数値型についても、それぞれ chrono, rust_decimal を features に指定すれば利用できてます。

現段階では、接続プールが実装されていないため、非同期な接続プールを利用するには、少々工夫が必要です。
※と書きましたが、tokio にして bb8 / bb8-tiberius で進める方が無難です。

何れにしても、以前より格段に書きやすく喜ばしい限りです。