親に向かってなんだそのContextは。

親Contextの都合でcancelしてほしくないが、loggerの引き回しはContextでやりたい、みたいなときにDeadline()/Done()/Err()を無視すればいいのかなぁ…と思って実装してみた。

cancelされたくないというのは、SIGINTですべてのgoroutineを停止したいんだけど、goroutineのトランザクションは完了しててほしい、みたいな。

package ctxutil

import (
    "context"
    "time"
)

type withoutCancelCtx struct {
    parent context.Context
}

func (*withoutCancelCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*withoutCancelCtx) Done() <-chan struct{} {
    return nil
}

func (*withoutCancelCtx) Err() error {
    return nil
}

func (ctx *withoutCancelCtx) Value(key any) any {
    return ctx.parent.Value(key)
}

func WithoutCancel(parent context.Context) context.Context {
    return &withoutCancelCtx{parent}
}
   ctx = ctxutil.WithoutCancel(ctx)

ctxutil.WithoutCancel()を読んでいる関数の上位の関数では、親ContextのDone()をチェックして、子が終わったら終了する想定。

キャンセルされたくないトランザクションはいろいろあるきがしているけど、その対応の実装例をあまり見ないような気がしている。他の人、どうしているんだろう?

cronのスケジュールを表示する雑Webサービス作った

https://cronplan.in/

Show cron schedule.

USAGE:

  curl cronplan.in -d '5 0 * * ? *'

  curl cronplan.in -G --data-urlencode 'e=5 0 * * ? *'

see http://cronplan.in?e=5+0+*+*+?+*
$ curl cronplan.in -d '5/3 0/3 ? * FRI *'
Fri, 21 Oct 2022 00:05:00
Fri, 21 Oct 2022 00:08:00
Fri, 21 Oct 2022 00:11:00
Fri, 21 Oct 2022 00:14:00
Fri, 21 Oct 2022 00:17:00
Fri, 21 Oct 2022 00:20:00
Fri, 21 Oct 2022 00:23:00
Fri, 21 Oct 2022 00:26:00
Fri, 21 Oct 2022 00:29:00
Fri, 21 Oct 2022 00:32:00

EventBridgeのcron式のパーサーを作った話とDay of weekの増分の仕様

追記3

もろもろ作り直した

github.com

追記2

いろいろ間違っていた…

追記

1-10/3 とか MON-FRI/3 とかを忘れていたので作り直し…


cron式をパースするGolangのライブラリを書いた。

github.com

モチベーションとしては、Amazon EventBridgeのcron式を書くときに毎回JSTからUTCに変換するのがめんどくさくて「いつの日かterraformにユーザー定義関数が追加されるときに備えて(くるのか?)タイムゾーンをシフトするライブラリを書こう」という感じで始めた。

パーサー自体は alecthomas/participle でサクサク書けたが

github.com

時間をシフトする処理は難しくて諦めた。

たとえば 0 5-10 ? * FRI * のようなJSTのcron式をUTCに直すと 0 20-23 ? * THU *0 0-1 ? * FRI * の2つに分かれる。cron式の分解と合成のうまいやり方が思いつかなかった。自分が無知なだけで良い方法があるのかも


EventBridgeの仕様を参考にしたが、いくつか知らない仕様があって勉強になった。

たとえば

  • 0 0 ? * 3#2 *: 毎月の第二水曜日の00:00に実行
  • 0 0 8W * ? *: 毎月8日に一番近い平日に実行
    • 2022/10だと10/8は土曜日なので10/7に実行される
  • 0 0 L * ? *: 毎月の最終日の00:00に実行
  • 0 0 ? * L *: 毎週の最終日、つまり土曜日の00:00に実行

など

オラクルのドキュメントでも同様の記号が書かれていたので、AWSだけの仕様ではない…ような気がする(オラクルのほうのCの意味はわからなかった)

Day of weekの増分の仕様

時間のシフトを計算する処理は諦めたが、ASTだけあってもあまり有用ではなさそうなので、ついでとある時刻がcronのスケジュールにマッチするかどうか判別する機能も付けてみた。しかし曜日(Day of week)フィールドの処理で結構ハマってしまった…

増分の表現は、分子≒オフセット・分母≒増分と、分数のようなかたちで書く。(改めて書くほどのことでもないが)

  • 分・時フィールドの場合 */30/3 と同じで、実行される時間は 0 3 6 9 …となる。1/3の場合は1 4 7 10 …
  • 日(Day of month)フィールドだと、*/31/3 と同じ。実行される日付は 1 4 7 10 13…
  • さらに年フィールドの場合は、*/31970/3 と同じになる模様(AWSコンソール調べ)

曜日(Day of week)フィールドの場合、*/31/3 と同じ結果になる。

曜日の数値 1-7 は MON-SUN にマップされているので MON(1) THU(4) SUN(7)… となると思いきや、実はSUN始まりで SUM(0) WED(3) SAT(6)… となる。

曜日(Day of week)を考えないと、時刻をnとしたときに n % (分母) == (分子) という計算でその時刻がcronを実行する時刻なのか判別できるが、曜日(Day of week)は他と分子の扱いが違うので特別な処理を挟む必要があって、だいぶ実装に時間が取られてしまった。

これLinuxでも同様の動作なのかな

あと、EventBridgeだとMON-SUNが1-7に対して、プログラム上だとSUN-SATが0-6になっているので細々した変換が必要で、そちらもすこしめんどくさかった。

RDSのインスタンスクラスごとのメモリ量を取得するterraform providerを作った

github.com

これはなに?

RDSのインスタンスクラスごとのメモリ量を習得するterraform provider。

インスタンスクラスごとにデータソースを定義するか、

data "rds_db_instance_memory" "rds" {
  instance_class = "db.t3.micro"
}

#=> {
#     "instance_class" = "db.t3.micro"
#     "memory" = 1
#   }

インスタンスクラスごとのメモリ量のマップを定義するデータソースを使って、メモリ量を取得できる。

date "rds_db_instance_memory_map" "rds" {
}

#=> {
#     "memory_by_instance_class" = tomap({
#       "db.m1.large" = 7.5
#       "db.m1.medium" = 3.75
#       "db.m1.small" = 1.7
#       ...
#     })
#   }

あと、クラスタインスタンスも合わせたすべてのDBインスタンスインスタンスクラスを取得するデータソースも追加した。

data "rds_db_instances" "rds" {
}

#=> {
#     "instances" = tolist([
#       {
#         "instance_class" = "db.t3.micro"
#         "name" = "database-1"
#       },
#       {
#         "instance_class" = "db.t3.small"
#         "name" = "database-2"
#       },
#       ...
#     ])
#   }

用途

terraformでDatadogのアラートを設定するときに、メモリ使用量の閾値をパーセンテージで設定したいときとかに使う想定。

RDS情報の取得先

instances.vantage.sh

Vantageというクラウドコストの最適化サービスをやっている会社のOSSのよう。

https://instances.vantage.sh/rds/instances.json からJSONでRDSの情報を取得できるので

curl -sSfL https://instances.vantage.sh/rds/instances.json \
| jq 'map({key: .instanceType, value: .memory | tonumber}) | sort_by(.key) | from_entries'

とやってインスタンスクラスごとのメモリ量のJSONを生成している。

クエリログを使ったPostgreSQLの負荷テスト

クエリログ

log_statement=allを設定するとpostgresql.logにクエリログされる。

$ cat postgresql.log
2022-05-30 04:59:41 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG:  statement: select now();
2022-05-30 04:59:46 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG:  statement: begin;
2022-05-30 04:59:48 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG:  statement: insert into hello values (1);
2022-05-30 04:59:50 UTC:10.0.3.147(57382):postgres@postgres:[12768]:LOG:  statement: commit;
...

これを自作のツールposlogでndjsonに変換。 log_min_duration_statementで出力されたログもパースできた…はず。

github.com

$ poslog postgresql.log > data.jsonl
$ cat data.jsonl
{"Timestamp":"2022-05-30 04:59:41 UTC","Host":"10.0.3.147","Port":"57382","User":"postgres","Database":"postgres","Pid":"[12768]","MessageType":"LOG","Duration":"","Statement":" select now();"}
{"Timestamp":"2022-05-30 04:59:46 UTC","Host":"10.0.3.147","Port":"57382","User":"postgres","Database":"postgres","Pid":"[12768]","MessageType":"LOG","Duration":"","Statement":" begin;"}
{"Timestamp":"2022-05-30 04:59:48 UTC","Host":"10.0.3.147","Port":"57382","User":"postgres","Database":"postgres","Pid":"[12768]","MessageType":"LOG","Duration":"","Statement":" insert into hello values (1);"}
{"Timestamp":"2022-05-30 04:59:50 UTC","Host":"10.0.3.147","Port":"57382","User":"postgres","Database":"postgres","Pid":"[12768]","MessageType":"LOG","Duration":"","Statement":" commit;"}
...

-fingerprintオプションを付けるとpt-fingerprintのようなinsert into hello values(?+);という感じのFingerprintフィールドも付与される。 「SELECTだけ」みたいなテストデータのフィルタリングに使えると思う。 あとは、似たようなクエリの集計とか。

また、-fill-paramsを付けるとStatementフィールドのプレースホルダ$1 $2…が実際の値で置換される。*1

負荷テスト

自作のツール qrn を使って、上記ndjsonをテストデータとした負荷テストを実行できる。

https://github.com/winebarrel/qrn

こんな感じ。

qrn -dsn "postgres://pgx_md5:secret@localhost:5432/pgx_test" \
  -data data.jsonl -rate 5 -time 300 -nagents 10

上の場合、5エージェント(ユーザー)で5分間、エージェントごとに5 qpsでDBに負荷を与える。

トランザクション

テストデータを複数与えるとエージェントごとに異なるテストデータを実行できるが、簡易化のために -commit-rate というオプションを用意していて、エージェントがNクエリ実行するごとにcommit; beginを実行して新しいトランザクションを開始することができるようにしている。

*1:適当なテキストの置換なのでプレースホルダー以外の$1を含むクエリだとうまく置換できないかも

ridgepoleのパーティショニング対応を削除することを考えています

ridgepoleの1.0.2でパーティショニングの対応が入ったのですが、これを削除することを考えています。 理由としては、パーティショニングの機能は多岐にわたりそのすべてをサポートするのは難しいのですが 現状では本体に密結合になっているため、オプショナルな動作にすることができず、 サポートされていない機能を使おうと思うとridgepoleそのものが使用不可になる状態のためです。

オプショナルな動作にするための改修を進めたいのですが、ちょっと時間がとれず いったんすべてをrevertすることを考えてます。

6月中にはなにか対応入れます

PR: https://github.com/ridgepole/ridgepole/pull/392

追記