Locustでシャーディング的なことをしたい

Locustは大変お手軽に負荷試験スクリプトを作れる、かつ、テストスクリプトの柔軟性があってよい。

今回は以下のような要件があった。

  • 複数のWorkerで処理をスケールアウトさせたい
  • あるIDは、同じWorkerで処理をさせたい(同じIDを別のWorkerで処理させたくない)

Out of boxなサポートはないものの、Locustの Custom Messageのサンプル がほぼこれに該当する。

  • @events.test_start.add_listener のリスナーでWorkerにIDを分配する
    • このタイミングまでには、Master, Workerともに立ち上がっているので、Master側はいくつWorkerを持っているか把握している
    • environment.runner.worker_count および environment.runner.clients を参照すればOK
  • 各Worker向けに、処理すべきIDをMessage経由で頒布すればOK
    • これを応用して、ID自体ではなくて、処理すべきIDのレンジを渡す、とかもできそう
@events.test_start.add_listener
def on_test_start(environment, **_kwargs):
    # When the test is started, evenly divides list between
    # worker nodes to ensure unique data across threads
    if not isinstance(environment.runner, WorkerRunner):
        users = []
        for i in range(environment.runner.target_user_count):
            users.append({"name": f"User{i}"})

        worker_count = environment.runner.worker_count
        chunk_size = int(len(users) / worker_count)

        for i, worker in enumerate(environment.runner.clients):
            start_index = i * chunk_size

            if i + 1 < worker_count:
                end_index = start_index + chunk_size
            else:
                end_index = len(users)

            data = users[start_index:end_index]
            environment.runner.send_message("test_users", data, worker)

IntelliJ IDEA x WSL2がクソ重い件

困ったこと

  • WSL2上のMavenプロジェクトをWindows側で開いた
  • "Scanning files to index...." でほぼハングする

原因

Windows Defenderが悪さしていたっぽい。以下のJetBrainsのForumも結構荒れていた模様。

https://youtrack.jetbrains.com/issue/IDEA-286059

対応

Forumのコメントにあったプロセスの除外設定だけでは症状は改善せず、こちらのGistの内容でガバっと指定する必要があった。

使ってるDistroに応じて指定するパスは変わってくるので要注意。

パスで \wsl$\Ubuntu-22.04 を指定

MySQL + JDBC Timezoneまとめ

choge.hatenadiary.com

前回からの続き。JDBCでのデータ書き込み、読み出しの際のタイムゾーン周りを確認する。ややこしくて頭が爆発しそう。

前提条件

前提知識

  • JDBCMySQLDATETIME ないし TIMESTAMP 型に値を渡す場合、Java側は java.sql.Timestamp の値を用いる。
  • JDBCの接続文字列で指定可能なオプションの一つに useLegacyDatetimeCode があり、この値によって挙動が変わる。

実験

以下の2点のパラメータをいじりながら、データの書き込み、読み込みを観測する。 書き込む値は前回と同じ、 '2021-02-02T12:00:00+00:00' とする。 java.sql.Timestamp に変換する際は、 java.text.SimpleDateFormatUTCで用いる。

  • JVMのデフォルトのタイムゾーンTimeZone.setDefault() で変更する。
  • JDBCのパラメータ useLegacyDatetimeCode を変更してみる。

結果

  • MySQL側に保存する時刻のタイムゾーンを統一するときは useLegacyDatetimeCode=false を指定する。統一せず、個別のレコードごとにタイムゾーンを管理する場合は、 useLegacyDatetimeCode=true でよい。
    • 後者のユースケースはあまり思いつかないので、MySQL 5.7系を利用している場合は基本 useLegacyDatetimeCode=false で良い気がする。
    • というか、そもそもビジネスロジック〜データ層のコードの時点でUTCに統一しておきたいよね。
  • useLegacyDatetimeCode=false を指定すると、 java.sql.Timestamp の値をUTCとして捉えて、MySQLのSystem time zoneでの時刻に変換をかける。
    • よって、UTCで昼12時だった場合、タイ時間 Asia/Bangkok (UTC+7) での19時としてDB内に保存される。このとき、Java側のタイムゾーンは考慮されない。

    • 内部的にタイ時間19時として持ってるのか、UTCの12時で持ってるのかは未確認。

  • useLegacyDatetimeCode=true (あるいは未指定=デフォルト値でtrue) の場合、 Java側のタイムゾーンjava.sql.Timestamp を変換した時刻を保存する。
    • UTCで昼12時 & JavaタイムゾーンがAsia/BangkokMySQL側と一致している場合、挙動は useLegacyDatetimeCode=false の場合と同じ。UTC12時相当が格納される。
    • UTCで昼12時 & Javaタイムゾーンが例えばAustralia/Sydney(現時点でUTC+11, Asia/Bangkokから+4時間)の場合、差分の4時間が加算された値が保存される。
id system_tz session_tz raw_datetime legacy_code datetime timestamp
1 Asia/Bangkok Asia/Bangkok 2021-02-02T12:00:00+00:00 NULL 2021-02-02 12:00:00 2021-02-12 12:00:00
2 Asia/Bangkok UTC 2021-02-02T12:00:00+00:00 NULL 2021-02-02 12:00:00 2021-02-12 19:00:00
3 Asia/Bangkok Australia/Sydney 2021-02-02T12:00:00+00:00 NULL 2021-02-02 12:00:00 2021-02-12 08:00:00
4 Asia/Bangkok Asia/Bangkok 2021-02-02T12:00:00+00:00 TRUE 2021-02-02 19:00:00 2021-02-02 19:00:00
5 Asia/Bangkok UTC 2021-02-02T12:00:00+00:00 TRUE 2021-02-02 12:00:00 2021-02-02 12:00:00
6 Asia/Bangkok Australia/Sydney 2021-02-02T12:00:00+00:00 TRUE 2021-02-02 23:00:00 2021-02-02 23:00:00
7 Asia/Bangkok Asia/Bangkok 2021-02-02T12:00:00+00:00 FALSE 2021-02-02 19:00:00 2021-02-02 19:00:00
8 Asia/Bangkok UTC 2021-02-02T12:00:00+00:00 FALSE 2021-02-02 19:00:00 2021-02-02 19:00:00
9 Asia/Bangkok Australia/Sydney 2021-02-02T12:00:00+00:00 FALSE 2021-02-02 19:00:00 2021-02-02 19:00:00
mysql> SELECT @@system_time_zone, @@session.time_zone;
+--------------------+---------------------+
| @@system_time_zone | @@session.time_zone |
+--------------------+---------------------+
| +07                | SYSTEM              |
+--------------------+---------------------+
1 row in set (0.00 sec)

mysql> SELECT id, system_tz, session_tz, raw_datetime, (jdbc_param NOT LIKE '%useLegacyDatetimeCode=false') as legacy_jdbc, datetime, timestamp FROM tz_sample;
+----+--------------+------------------+---------------------------+-------------+---------------------+---------------------+
| id | system_tz    | session_tz       | raw_datetime              | legacy_jdbc | datetime            | timestamp           |
+----+--------------+------------------+---------------------------+-------------+---------------------+---------------------+
|  1 | Asia/Bangkok | Asia/Bangkok     | 2021-02-02T12:00:00+00:00 |        NULL | 2021-02-02 12:00:00 | 2021-02-12 12:00:00 |
|  2 | Asia/Bangkok | UTC              | 2021-02-02T12:00:00+00:00 |        NULL | 2021-02-02 12:00:00 | 2021-02-12 19:00:00 |
|  3 | Asia/Bangkok | Australia/Sydney | 2021-02-02T12:00:00+00:00 |        NULL | 2021-02-02 12:00:00 | 2021-02-12 08:00:00 |
|  4 | Asia/Bangkok | Asia/Bangkok     | 2021-02-02T12:00:00+00:00 |           1 | 2021-02-02 19:00:00 | 2021-02-02 19:00:00 |
|  5 | Asia/Bangkok | UTC              | 2021-02-02T12:00:00+00:00 |           1 | 2021-02-02 12:00:00 | 2021-02-02 12:00:00 |
|  6 | Asia/Bangkok | Australia/Sydney | 2021-02-02T12:00:00+00:00 |           1 | 2021-02-02 23:00:00 | 2021-02-02 23:00:00 |
|  7 | Asia/Bangkok | Asia/Bangkok     | 2021-02-02T12:00:00+00:00 |           0 | 2021-02-02 19:00:00 | 2021-02-02 19:00:00 |
|  8 | Asia/Bangkok | UTC              | 2021-02-02T12:00:00+00:00 |           0 | 2021-02-02 19:00:00 | 2021-02-02 19:00:00 |
|  9 | Asia/Bangkok | Australia/Sydney | 2021-02-02T12:00:00+00:00 |           0 | 2021-02-02 19:00:00 | 2021-02-02 19:00:00 |
+----+--------------+------------------+---------------------------+-------------+---------------------+---------------------+
9 rows in set (0.00 sec)

その他まとめ

JDBCの接続作成

            String defaultTimeZone = "Australia/Sydney";
            TimeZone.setDefault(TimeZone.getTimeZone(defaultTimeZone));
            Class.forName("com.mysql.jdbc.Driver").newInstance();
            Connection conn = DriverManager.getConnection(CONNECTION_STRING_BASE + "&useLegacyDatetimeCode=false");

データ書き込み

日付はRFC3339形式のUTCでもらう想定。 SimpleDateFormat で文字列をパースする前に、タイムゾーンを設定しておく。

    private static void insertItem(String datetimeInRFC3339, TimeZone tz, Connection conn) throws ParseException, SQLException {
        SimpleDateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+00:00");
        rfc3339.setTimeZone(TimeZone.getTimeZone("UTC"));
        long epoch = rfc3339.parse(datetimeInRFC3339).getTime();  // Get the epoch seconds in UTC
        Timestamp ts = new Timestamp(epoch);

        PreparedStatement pstmt = conn.prepareStatement("INSERT INTO tz_sample" +
                "(system_tz, session_tz, raw_datetime, jdbc_param, datetime, timestamp)" +
                "VALUES (?, ?, ?, ?, ?, ?)");
        pstmt.setString(1, "Asia/Bangkok");
        pstmt.setString(2, tz.getID());
        pstmt.setString(3, datetimeInRFC3339);
        pstmt.setString(4, conn.getMetaData().getURL());
        pstmt.setTimestamp(5, ts);
        pstmt.setTimestamp(6, ts);

        pstmt.execute();
    }

データ読み込み

Epoch秒も一緒に表示しておく。

    private static String getOneRow(ResultSet rs) throws SQLException {
        String systemTZ = rs.getString("system_tz");
        String sessionTZ = rs.getString("session_tz");
        String rawDatetime = rs.getString("raw_datetime");
        String jdbcParam = rs.getString("jdbc_param");
        Date datetime = rs.getDate("datetime");
        Timestamp timestamp = rs.getTimestamp("timestamp");

        int rowNum = rs.getRow();

        Object[] params = {rowNum, systemTZ, sessionTZ, rawDatetime, jdbcParam,
                dateToStr(datetime), timestamp.toString(), timestamp.getTime() / 1000};
        return FMT.format(params);
    }

    private static String dateToStr(Date date) {
        return dateToStr(date, TimeZone.getDefault());
    }

    private static String dateToStr(Date date, String tzString) {
        TimeZone tz = TimeZone.getTimeZone(ZoneId.of(tzString));
        return dateToStr(date, tz);
    }

    private static String dateToStr(Date date, TimeZone tz) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");
        sdf.setTimeZone(tz);
        return sdf.format(date);
    }

MySQL Timezoneまとめ

やりたいこと

前提知識

MySQLサーバ側のタイムゾーンの概念

  • System time zone: サーバ起動時に指定するタイムゾーン。基本的に不変。使い所よくわかんない。
  • Server current time zone: サーバが現時点で使ってるタイムゾーン。デフォルトではSystem time zoneと同じ。
    • 以下のいずれかの方法で変更可能
      • MySQLサーバ起動時に --default-time-zone='Asia/Bangkok' のように指定する
      • 管理者権限( SYSTEM_VARIABLES_ADMIN 権限)で SET GLOBAL time_zone = 'Aisa/Bangkok' のように指定する
  • Per-session time zone: SessionごとのタイムゾーンSET time_zone = 'Asia/Bangkok' のように変更可能

参考

dev.mysql.com

データ型 DATETIMETIMESTAMP

  • DATETIME: 日時を持つ。基本的にタイムゾーンの概念はない。
  • TIMESTAMP: 内部的にEpoch秒を持つ。以下の特徴を持つ。

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.) By default, the current time zone for each connection is the server's time. The time zone can be set on a per-connection basis. As long as the time zone setting remains constant, you get back the same value you store. If you store a TIMESTAMP value, and then change the time zone and retrieve the value, the retrieved value is different from the value you stored. This occurs because the same time zone was not used for conversion in both directions. The current time zone is available as the value of the time_zone system variable. For more information, see Section 5.1.15, “MySQL Server Time Zone Support”.

参考

dev.mysql.com

実験

前提条件

対応内容

以下のようなテーブルを作り、セッションおよびJDBCのパラメータを変えながらタイムスタンプの値を見ていく。

mysql> CREATE TABLE tz_sample
    -> (
    ->   id INT AUTO_INCREMENT PRIMARY KEY,
    ->   system_tz VARCHAR(256),
    ->   session_tz VARCHAR(256),
    ->   raw_datetime VARCHAR(256),
    ->   jdbc_param TEXT,
    ->   datetime DATETIME,
    ->   timestamp TIMESTAMP
    -> );
Query OK, 0 rows affected (0.04 sec)

INSERT文は基本的に以下で固定。

INSERT INTO tz_sample
(system_tz, session_tz, raw_datetime, jdbc_param, datetime, timestamp)
VALUES
('Asia/Bangkok', 'そのときのセッションのタイムゾーン', '2021-02-02T12:00:00+00:00', '2021/02/02 12:00:00', '2021/02/02 12:00:00');

確認項目

データ確認

  • TIMESTAMP 型に日時を保存する際にタイムゾーンを明示しない場合、現在のセッションのタイムゾーンの日時として認識される。
    • '12:00' だと、タイ時間の昼12時 = UTCの朝5時となる。
  • TIMESTAMP 型は常にUTCで時刻を保存している。このため、データを読み出すときは、セッションのタイムゾーンによって表示される時刻が変わる。
  • DATETIME型はタイムゾーン情報を持たない。よって、セッションのタイムゾーンを変更しても、表示される時刻は同じ。

Timezone = Asia/Bangkokで読み出す

mysql> SELECT @@system_time_zone, @@session.time_zone;
+--------------------+---------------------+
| @@system_time_zone | @@session.time_zone |
+--------------------+---------------------+
| +07                | SYSTEM              |
+--------------------+---------------------+
1 row in set (0.00 sec)

mysql> SELECT id, system_tz, session_tz, raw_datetime, datetime, timestamp FROM tz_sample WHERE jdbc_param IS NULL;
+----+--------------+------------------+---------------------------+---------------------+---------------------+
| id | system_tz    | session_tz       | raw_datetime              | datetime            | timestamp           |
+----+--------------+------------------+---------------------------+---------------------+---------------------+
|  1 | Asia/Bangkok | Asia/Bangkok     | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 12:00:00 |
|  2 | Asia/Bangkok | UTC              | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 19:00:00 |
|  3 | Asia/Bangkok | Australia/Sydney | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 08:00:00 |
+----+--------------+------------------+---------------------------+---------------------+---------------------+

Timezone = UTCで読み出す

mysql> SET time_zone = 'UTC';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT id, system_tz, session_tz, raw_datetime, datetime, timestamp FROM tz_sample WHERE jdbc_param IS NULL;
+----+--------------+------------------+---------------------------+---------------------+---------------------+
| id | system_tz    | session_tz       | raw_datetime              | datetime            | timestamp           |
+----+--------------+------------------+---------------------------+---------------------+---------------------+
|  1 | Asia/Bangkok | Asia/Bangkok     | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 05:00:00 |
|  2 | Asia/Bangkok | UTC              | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 12:00:00 |
|  3 | Asia/Bangkok | Australia/Sydney | 2021-02-02T12:00:00+00:00 | 2021-02-02 12:00:00 | 2021-02-12 01:00:00 |
+----+--------------+------------------+---------------------------+---------------------+---------------------+
3 rows in set (0.00 sec)

Admin権限なしでRustをWindowsにインストール / Installing Rust on Windows without Admin

Why

  • Rust製の諸々のツールを使いたい
  • 通常、RustをWindowsにインストールするときは管理者権限必要
  • 会社用のPCとかだと、諸々あってAdmin権限がない

How

  1. scoop をインストール
  2. scoop install msys2 でMSYS2をインストール。これでMinGW64が勝手に入る。
  3. scoop install gcc は、メンテナンスされていないようでインストールに失敗するので要注意
  4. scoop install rustup でRustupをインストール。多分これは公式サイトからダウンロードしたものでもOKなはず(試してない)
  5. rustup default stable-x86_64-pc-windows-gnu で、MinGW64を使うよう設定
  6. 一部のツールは gcc が必要なので、MSYS2側でインストールしてPATHを通しておく。
  7. 完了!

参考

stackoverflow.com

www.reddit.com


Why

  • I wanted to use fancy tools made by Rust (like bat or fd)
  • Usually, the Admin privilege is required to install Rust on Windows
  • I don't have the Admin privilege of some laptop

How

  • Install scoop
  • Install msys2 via scoop install msys2. This will install MinGW64 as a part of msys2.
    • Some old articles mention scoop install gcc, but it seems this package is no longer maintained and fails to install
  • Install rustup via scoop install rustup. Maybe we can download the official binary.
  • Configure rustup to use MinGW64 with rustup default stable-x86_64-pc-windows-gnu.
  • DONE!

Reference

stackoverflow.com

www.reddit.com

ES6の書式でJestでテストする (Bebelなしで)

やりたいこと

  • Jest を使ったテストを導入したい
  • ただ、 require じゃなくて import とかのES6の構文を使いたい
  • Babelの設定とかで消耗したくない

注意事項

JestのES Modulesサポートはまだ実験段階で、サポートされてない機能が結構あるらしい。

github.com

やり方

jest の導入

普通に npm でインストールする。 --save-dev で開発時のみ必要なDependencyであることを明示する。

npm install jest --save-dev

package.json の編集

  • "scripts": { "test": "..." } 部分の編集
  • "jest" { "transform": {} } の追加

がポイント

{
  "name": "paiza",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^26.6.3"
  },
  "jest": {
    "transform": {}
  }
}

transformの無効化

ドキュメントによれば、

  • コードの変換を無効化する (-> transform: {} を指定する)
  • 変換先をCommonJSじゃなくてESMにする

のいずれかが必要。今回できればBabelを使いたくないので、一つ目を採用。

nodeの --experimental-vm-modules フラグの追加

そもそも node 自体に --experimental-vm-modules フラグを渡さないとES Modulesの import を認識してくれない。 よって、テストを実行する際にこのパラメータを加えておく。

参照

基本的に以下のドキュメントに従えばOK。

jestjs.io

Vivaldi: YouTube MusicをWeb Panelに追加する

事象

Web PanelにYouTube Musicを追加する。再生した後にWeb Panelを閉じると、再生が止まってしまう。

原因

  • デフォルトでモバイル版のサイトがWeb Panelに登録される
  • モバイル版は、フォーカスを失うと再生が止まる仕様っぽい

対処

  • パネルのアイコンを右クリックして、「デスクトップ版を表示」を選べばOK 左側のWeb Panelのアイコン右クリック→「デスクトップ版を表示」を選択

参考

forum.vivaldi.net