Posts for: #Mongodb

モジュールは複数データソースに依存させない

デバッグしていて遅くなり、帰ってから慌ててスーパーに買いものへ行ったりしていて時間がなくなったので今日はバドミントン練習をお休み。

データベースとメッセージキューの整合性を考える

昨日の続き。トランザクションを導入したことで mongodb と mq でのデータフローが意図せず変わってしまっていることが調査してわかった。

従来は次のように動いていた。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがエントリー情報を mongodb に保存する
  3. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する
  4. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  5. consumer がメッセージを受信する
  6. consumer が mongodb からジョブ情報を参照する
  7. api ハンドラーがレスポンスを返す
  8. consumer がメッセージを処理する

トランザクションを導入したことで mongodb にデータが保存されるタイミングが変わってしまった。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがトランザクションを開始する
  3. api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
  4. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
  5. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  6. consumer がメッセージを受信する
  7. consumer が mongodb からジョブ情報を参照するが、トランザクションがコミットされていない可能性がある
  8. api ハンドラーがトランザクションをコミットする
  9. api ハンドラーがレスポンスを返す
  10. consumer でエラーが発生する

処理の流れを見直して次のように改修した。内部実装の都合があってやや煩雑な変更となった。

  1. api サーバーがエントリー情報を受け取る
  2. api ハンドラーがトランザクションを開始する
  3. api ハンドラーがエントリー情報を mongodb に保存する (この時点では未コミット)
  4. api ハンドラーがメッセージを管理するジョブ情報を mongodb に保存する (この時点では未コミット)
  5. api ハンドラーがトランザクションをコミットする
  6. api ハンドラー内の producer が rabbitmq にメッセージを送信する
  7. consumer がメッセージを受信する
  8. consumer が mongodb からジョブ情報を参照する
  9. api ハンドラーがレスポンスを返す
  10. consumer がメッセージを処理する

これは単純にトランザクションのコミットタイミングと mq へのメッセージ送受信のタイミングを見直せばよいという話しではない。本質的に mongodb で管理しているジョブ情報と rabbitmq へ送信しているメッセージの整合性を保証することはできないということを表している。producer はメッセージ送信に失敗する可能性があるから、そのときにジョブ情報を書き換える必要はあるが、その前にトランザクションをコミットしてしまっているため、api ハンドラー内でデータのコミットタイミングが複数になってしまう。トランザクションを導入したメリットが失われてしまい、Unit of work のパターンも実現できない。consumer の処理に必要な情報を mongodb にあるデータとメッセージの2つに分割しているところが整合性の問題を引き起こしている。アーキテクチャ上の設計ミスと言える。consumer の処理に必要な情報はすべてメッセージに含めてしまい、メッセージを処理した後に mongodb に結果を書き込むといった設計にすべきだった。

初期実装のときからジョブ情報を mongodb で管理する必要はあるのか?という懸念を私はもっていた。要件や機能が曖昧な状況でもあり、メンバーもなんとなく db に管理情報を残しておいた方が将来的な変更に対応できて安心といった理由だったと思う。当時は整合性の問題が起きることに、私が気付いていなかったためにこの設計を見直すように強く指摘できなかった。トランザクションを導入したことで consumer が必要な情報を db に保持すると、db とメッセージ処理のタイミングにおける整合性の問題が生じるという学びになった。

いまとなってはこのジョブ情報を使う他の機能もあるため、この設計を見直すことはできない。今後の開発プロジェクトで db とメッセージを扱うときはこの経験を活かすためにふりかえりとして書いておく。

mongodb のトランザクション調査

今日のバドミントン練習はエアシャトルでリフティングを10分、連続最大回数は120回ほど、ショートサーブ30回した。フットワーク練習はお休み。筋肉痛もあったから、みなとのもり公園をジョギングで3周 1.4km、縄跳び15分で1712回だった。縄跳びも2ヶ月ぶり。8月に過去最高が1838回に対して、2ヶ月ぶりにやって2分弱の休憩、十数回のミスがあっても1700回を超えるんやと思って少し驚いた。8月に比べたらいまは涼しいから記録が伸びるのもある。ジョギングで足が筋肉痛になると走るよりも縄跳びの方が負担が小さい。昨日と同じように運動してストレッチして十分にカラダを動かした。

トランザクションとコールバック

お仕事は昨日の続きでリクエスト途中のサービス終了したときに web api ハンドラーの処理が途中でキャンセルされ、そのために内部のデータが不整合になることがわかった。データを不整合にしないため mongodb のトランザクション の仕組みを調べてデバッグしたりしていた。echo のミドルウェアで Unit of work を実装できないかをプロトタイプ実装していた。

基本は次のように callback 関数を渡せばトランザクションに失敗したときにリトライもしてくれて便利なのだけど、いろいろ api サーバーの既存アーキテクチャの都合もあってこれは使えないことがわかった。

session, err := client.StartSession()
if err != nil {
	return err
}
defer session.EndSession(ctx)
result, err := session.WithTransaction(ctx, callback)

ミドルウェアのコンテナの振る舞い検証

今日もバドミントン練習はお休み。

mongodb の healthcheck

bitnami/mongodb というサードパーティのコンテナ を使って mongodb サービスを設定している。docker compose でコンテナサービスの依存関係を記述できるが、特別な設定をしないとコンテナサービスの起動をトリガーに依存関係を制御する。実際はコンテナが起動して内部のサーバー/デーモンが正常に起動するまで少し時間がかかる。たとえば mongodb のコンテナであれば mongod デーモンに初期設定をして再起動したりといった処理を内部的に行っている。そんなときに healthcheck を使うことで実際に mongod デーモンに接続できるかどうかでコンテナのサービス間の依存関係を制御できる。

これまで mongodb には healthcheck の設定をしていなかったので調査して次の設定を追加した。

healthcheck:
  test: mongosh "mongodb://localhost:37017/test?directConnection=false&replicaSet=${MONGO_REPLICA_SET}" --eval 'db.runCommand("ping").ok' --quiet
  interval: 60s
  timeout: 5s
  retries: 3
  start_period: 30s
  start_interval: 3s

mongosh で db に接続して ping を実行するだけなら認証は必要ない。mongosh でなにもパラメーターを指定せずに接続すると direct 接続になってしまう。replica set の設定が完了していることを検証するために replica set 接続にしている。また interval は起動中もずっと死活監視に test コマンドを実行している。それとは別に start_interval を指定することでサービス開始時と通常の運用時の test コマンドによる制御をわけて管理できる。

rabbitmq のアップグレード

19時過ぎに業務終了報告をして、帰ろうと思ったときにふと rabbitmq のバージョンを最近あまり確認していないことに気付いた。いま 3.12.14 を使っているが、Release Information をみるとコミュニティサポートは切れていて、現行バージョンは 4.0 になっていることに気付いた。試しに結合テストの rabbitmq のバージョンを 4.0.3 に上げてみたところ、問題なく動作している。テスト環境の移行は他のメンバーが使っていない夜にやった方がいいかと帰ることをやめて普通に移行作業をやり始めてしまった。メッセージキューは永続化したデータを基本的には保持しないため、メジャーバージョンアップで互換性がなかったとしても volume 配下のデータを削除して exchange/queue を移行すればよい。

rabbitmq の http api client として rabbit-hole というツールを使っている。それも v2 から v3 へアップグレードしていて Changes Between 2.16.0 and 3.1.0 (Oct 31, 2024) に書いてあるが、機能的な変更も非互換の変更もいまのところはないが、4.0 にあわせて将来的に非互換な変更をやりやすいよう、メジャーバージョンを上げると書いてある。go.mod の依存関係も更新したりした。

19時過ぎに帰ろうと思ってから、なんやらかんやらしているうちに最終的には21時半まで作業していた。

飛行機での出張帰り

夜に神戸に戻ってきてからオフィスに来て、機内でやっていた作業を区切りのよいところまで進めてコミットしたりしていた。その後スーパーへ買いものへ行ったりしていた。今日もバドミントン練習はおやすみ。

ローカルは standalone mongodb

mongodb の レプリケーション 機能を使っていて、replica set の設定が容易なことから bitnami/mongodb というコンテナを使っている。これまで結合テストもレプリケーション機能を設定して実行するようにしていた。結合テストの量が増えてきたのもあって実行時間が徐々に遅くなってきた。いまテストのリファクタリングをしているため、ローカルで検証のために何度も実行する必要がある。コードの品質を上げるためにテストを書きやすくする必要がある。実行時間は短いほどよい。そこでローカルでの実行はレプリケーションを設定しないようにしてみた。さらに go のデータ競合を検知する -race オプションも外してテストを実行してみたところ、この2つの改善で macbook の環境で改善前より45%ほど速くなった。十分な高速化を図れた。

飛行機ルートのふりかえり

8月に大雨で新幹線が止まってしまい 神戸に帰れない状況 に直面した。私はほとんど飛行機に乗ったことがなかったため、新幹線が動かないときの迂回手段として飛行機のルートに慣れておく。普段リュックサックに小さいカッターとマイナスドライバーを携帯している。これらは飛行機の手荷物検査でとめられてしまう。荷物を預けるか、検査場で処分してもらうしかない。前回は ANA で帰ってきたが、今回は スカイマーク の飛行機に乗った。値段が安いから LCC だと思っていたら スカイマークは安くて快適なMCC らしい。ANA に比べたらずっと小さい飛行機だった。

タイムスケジュールは次のようになった。

18:15 (都営浅草線) 五反田 発
18:56 (京急本線) 羽田空港 着
19:45 搭乗開始
19:55 飛行機の座席に着く
20:08 羽田空港の滑走路を発進
20:20 離陸
(機内は暗かった、ラップトップで作業)
21:16 神戸空港の滑走路に着陸
21:22 降機
21:25 空港内の荷物受け取り場に着く
21:30 荷物を受け取り
21:35 (ポートライナー) 神戸空港 発
21:51 (ポートライナー) 貿易センター 着

新幹線だと次のようなタイムスケジュールになる。トータルの時間でみれば新幹線の方が20-30分早いかなといったところ。一方で新幹線だと2時間半、同じ姿勢で座っている必要がある。飛行機だと座っている時間は1時間強。飛行機は搭乗手続きのチェックポイントがいくつかあってそれぞれに細かい待ち時間ができてしまうのが気になる。そのときの体調によってもどちらがよいかは変わってくるかもしれない。来月は行きは飛行機、帰りは新幹線にしてみる。

17:57 (JR) 五反田 発
18:03 (JR) 品川 着
18:19 (新幹線) 品川 発
20:53 (新幹線) 新神戸 着
21:10 (市営地下鉄) 新神戸
21:12 (市営地下鉄) 三宮

mongodb のテストデータ管理

今日のバドミントン練習はエアシャトルでリフティングを45分した。連続最大回数は146回できた。調子は悪くなく安定的に30回前後は続くものの、50回を超えたぐらいで失敗してしまう。打ち上げる高さの違いかな?と気付いて少し高めに打ち上げるようにしたら100回を超えた。エアシャトルはラケット面に対して適切な角度でコルクを打たないとあらぬ方向に飛んでいってしまう。高く打ち上げるほど、重力で落ちてくるときにコルクが下を向きやすくなるため、うまくシャトルを打ち上げやすくなる。ラケットコントロールをうまくできれば、エアシャトルをより低い高さでリフティングできるようになるかもしれない。まだ私はそのレベルには満たない。

json からの mongodb にテストデータを追加する

結合テストの改善をしていてテストデータを json で管理したい。これまで go の構造体でテストデータを定義して mongodb の client で insert するといったことをしていた。それも役に立つのだけど、共有のテストデータがどこにあるのか、ソースコードに書いてしまうと時間とともに散らばっていって把握できなくなっていく。テストデータを管理するためのディレクトリを設け、そこに json で記述してどのテスト関数で使うかといったメタ情報も定義できるようにした。次のコードは mongodb に json からデータをインポートするための原理を説明するための疑似コードのようなもの。

go の構造体で定義したテストデータと json で管理するのとどちらがよいかというのは議論の余地はあるし、一概に言えないとは思う。

type testData struct {
	Documents    []bson.Raw `bson:"documents"`
}

func InsertData(
	t *testing.T, client *mongo.Client, b []byte,
) (func()) {
	var data testData
    err := bson.UnmarshalExtJSON(b, false, &data)
	require.NoError(t, err, "failed to get json files: %v", err)

	ctx := context.Background()
	col := client.Database(dbName).Collection("mycollection")
	r, err := col.InsertMany(ctx, docsToInterfaces(data.Documents...))
	require.NoError(t, err, "failed to insert: %v", err)

	return insertResult, func() {
		t.Helper()
	    col := client.Database(dbName).Collection("mycollection")
		for _, id := range r.InsertedIDs {
			filter := bson.D{{Key: "_id", Value: id}}
			if _, err := col.DeleteOne(ctx, filter); err != nil {
				t.Errorf("failed to delete %s: %v", id, err)
				return
			}
		}
	}
}

呼び出し側のイメージ。defer で teardown を呼ぶことでテスト完了時に追加したテストデータを削除してくれる。

teardown := mongotest.InsertData(t, mongoClient, b)
defer teardown()

...

bson.Raw として読み込める json データは bson パッケージのユーティリティを使って dump できる。

func dumpAsJSON(value any) {
	b, err := bson.MarshalExtJSON(value, false, false)
	if err != nil {
		slog.Error("failed to marshal as extended JSON", "err", err)
		return
	}
	fmt.Println(string(b))
}

go の iterator 実装へのリファクタリング

mongo-go-driver を使った go の iterator 実装

先日 go 1.23 がリリースされた。このバージョンの目玉機能の1つに range-over function iterators がある。簡潔に言えば、ユーザー定義のイテレーターを言語機能の仕組みを使って簡単に実装できる。ある制約を満たして関数を実装すれば、for 文の range 構文を使ってイテレーターとして扱える。ユーザー定義のイテレーターを実装するのが、ほとんど関数を実装するだけで出来てしまうので実装の難易度が減ったと言える。どんなコードかをみるには次のチュートリアルがわかりやすいと思う。

昨日で私が抱えていたプロジェクトのボトルネックを解消できた。ここからはボーナスステージで相対的に自由に開発できる。手始めにプロダクトの依存バージョンを go 1.23 へアップグレードして mongodb からの fetch 処理をイテレーターに置き換えるリファクタリングをした。アプリケーションからみたら mongodb は Store という generic なインターフェースを定義していて、そこに Iter() メソッドを追加した。mongodb のコレクションは必ず Iter() メソッドを実装しなければいけないという制約を課す。

type Store[T any] interface {
    ...
    Iter(ctx context.Context, query query.Query, opt *sort.Option) iter.Seq2[*T, error]
}

mongo-go-driver はもともと cursor でイテレーター機能を提供しているため、これを range-over function iterators を使ってアプリケーションから使えるように間をつなげてあげればよい。具体的には generics を使って次のような汎用の iter() メソッドを mongodb client に実装する。

func (c *client[R, E]) iter(
	ctx context.Context, name string, filters queryFilters, sortOpt *sort.Option,
) iter.Seq2[*E, error] {
	if c.toEntity == nil {
		panic(fmt.Errorf("require toEntity() function"))
	}
	return func(yield func(*E, error) bool) {
		collection := c.raw.Database(c.db).Collection(name)
		opts := []*options.FindOptions{}
		if sortOpt != nil {
			opt := options.Find().SetSort(makeSortSet(*sortOpt))
			opts = append(opts, opt)
		}
		cursor, err := collection.Find(ctx, filters, opts...)
		if err != nil {
			if !yield(nil, err) {
				return
			}
		}
		defer cursor.Close(ctx)
		for cursor.Next(ctx) {
			var result R
			if err := cursor.Decode(&result); err != nil {
				if !yield(nil, err) {
					return
				}
			}
			if !yield(c.toEntity(&result), nil) {
				return
			}
		}
		if err := cursor.Err(); err != nil {
			if !yield(nil, err) {
				return
			}
		}
	}
}

Store インターフェースを満たすコレクションの実装は次のように型チェックのためのメソッド実装をもてばよい。

func (c *MyCollection) Iter(
	ctx context.Context, query query.Query, opts *sort.Option,
) iter.Seq2[*entry.MyEntity, error] {
	return c.iter(ctx, c.name, makeFilters(query), opts)
}

朝から iterator 実装の試行錯誤や使い方の勘どころを確かめながら、これも1日でほとんどリファクタリングして移行できた。python のジェネレーターもそうだけど、関数をイテレーターにできると直感的にわかりやすく簡潔に実装できる。すごくよい機能だと思う。1.23 以降の go のコードはイテレーターを使うよう、大きく変わっていくと思われる。

mongodb も一応は外部結合できる

今日は出張中にたまっていた日記をまとめて書いていた。昨日筋トレしたから運動はおやすみ。

mongodb における外部結合

出張中のふりかえりやプロジェクトマネジメントの合間に mongodb の $lookup (aggregation) 機能を調査していた。一言で言えば、rdbms で言うところの外部結合 (join) を mongodb で実現する機能と言える。しかし、aggregation の機能として実装されていることから rdbms の外部結合とはパフォーマンスの側面で大きく違うのでは?と推測する。mongodb のような kvs では基本的に外部結合のようなことは行わず、結合済みのコレクションを定義するやり方がプラクティスとされる。一方で整合性を保証するには外部結合は有効なデザインパターンの1つなので使いたい場面があってもおかしくない。実際に mongodb に $lookup が実装されているのだから、そのニーズは大きいのだと思われる。

ちょうどいまやっている開発でその要件があったので mongo-go-driver の次のチュートリアルをみながら実装してみた。

例えば、次のような2つのコレクションがある。

type Group struct {
	ID          string      `bson:"_id"`
	Name        string      `bson:"name"`
	UpdatedAt   time.Time   `bson:"updatedAt"`
}

type MyData struct {
	ID           string    `bson:"_id"`
	GroupID      string    `bson:"groupID"`
	RegisteredAt time.Time `bson:"registeredAt"`
}

MyData の GroupID フィールドが Group コレクションの ID を保持して外部参照している。MyData コレクションに対する aggregate に渡すパラメーターとして pipeline を渡す。

myPipeline = mongo.Pipeline{
	{{
		Key: "$lookup", Value: bson.D{
			{Key: "from", Value: "group"},
			{Key: "localField", Value: "groupID"},
			{Key: "foreignField", Value: "_id"},
			{Key: "as", Value: "foreignGroup"},
		},
	}},
	{{
		Key: "$unwind", Value: bson.D{
			{Key: "path", Value: "$foreignGroup"},
			{Key: "preserveNullAndEmptyArrays", Value: false},
		},
	}},
}

$lookup の操作の後に $unwind で配列を flatten する処理がセットになる。result set にマッピングするためには flatten が必要になるのだと推測する。aggregate() を呼び出すと cursor が返ってきて、その cursor に外部結合される値の配列を渡すことで result set を取得できる。デバッグはややこしいし、コード上もちょっとわかりにくいけど、これは mongodb のお作法だと割り切ってユーティリティ化してしまえばよいものだと思う。

type joinedMyData struct {
	ID           string    `bson:"_id"`
	Group        Group     `bson:"foreignGroup"`
	RegisteredAt time.Time `bson:"registeredAt"`
}
...

cursor, err := collection.Aggregate(ctx, myPipeline)
if err != nil {
	return nil, err
}
var joined []joinedMember
if err = cursor.All(ctx, &joined); err != nil {
	return nil, err
}

また時間があるときにある程度のデータ量でどのぐらいの負荷に対してパフォーマンスが出るのかを測ってみたい。

ストレッチ

昨日1週間ぶりに筋トレをしたせいか、今朝から筋肉痛になっていてカラダが痛い。その影響もあって今日の開脚幅は開始前146cmで、ストレッチ後152cmとカラダが硬かった。足も腰も肩周りも全身にわたって軽い筋肉痛だった。それだけよい感じの負荷をかけて筋トレができていたと言えるのかもしれない。あちこち筋肉痛はあったものの、ストレッチを受けていての辛さのようなものは感じなかった。ちょうどよい感じにほぐされているような印象を受けた。もしかしたらトレーナーさんが気を遣っていつもよりは弱めにストレッチしてくれたのかもしれない。トレーナーさんが立候補していた店長選挙は落選してしまったらしい。もし店長になったらよそのお店へ転勤になって担当者が変わってしまうのかなとも思っていた。トレーナーさんは私が通っているお店の副店長を務める方向で調整しているらしい。待遇をあげていくにはキャリアアップをしていかないといけないといった背景もあるようにみえる。

mongodb のデータ移行のあれこれ

昨日の夜に縄跳びに出掛けようと思ったところ、雨がパラパラしてきて諦めた。今週は天気が悪いからあまり外で運動できないかもしれない。

今日の運動は腕立て,スクワットをした。統計を 運動の記録 にまとめる。

mongosh でデータ移行

あるコレクションのデータ構造を変更したので既存データを移行しないといけない。mongosh を使うと javascript っぽい文法で repl から mongodb のデータを操作できる。私は sql を書く方が好みだけど、慣れの問題で mongosh はプログラミングに近い形でオブジェクトを操作してデータを更新できる。例えば、history というコレクションを3件だけ取得して1件ずつ dump するコードは次のようになる。

> db.history.find().limit(3).forEach(function(i) {console.log(i)})

なにかしら関数内で処理した結果を用いて更新しないといけないようなときに forEach を使うと簡単にデータ移行できる。しかし、これは1件ずつ更新を実行するので時間はかかる。余談だけど、mongosh をリモートから接続すると forEach で取得するデータをローカルに fetch してくるので document のサイズに比例してデータの取得分の時間だけ遅くなる。forEach を使うときは ssh で mongodb のサーバーにログインして、そこで mongosh を起動した方が効率よくデータ移行できる。

compose 環境の mongodb に direct 接続するときはこんな感じ。

$ docker compose exec -it mongo mongosh "mongodb://${USER}:${PASSWORD}@localhost:27017/?authMechanism=DEFAULT&directConnection=true"

シンプルな条件で更新できるようなときは updateMany を使ってバッチ更新すると1件ずつ更新するよりめちゃくちゃ速い。私の環境では270万件ほど更新するのが数分で完了した。仮想マシン上のコンテナ環境なので実機だったらもっと速いはず。

> db.history.updateMany({type:null}, {$set:{type:"myType"}}, {})

主キーの特性

今日の運動は腹筋ローラー,腕立て,スクワット,縄跳び(両足跳)をした。統計を 運動の記録 にまとめる。

コレクションデータの再定義

ある mongodb のコレクションのデータ定義で _id に ldap の dn の値を使っていた。dn は一意な値なので主キーとして使ってもよさそうに思えたが、ここで運用上 dn の値が変更されるケースがいくつかあることがわかってきた。例えば、dn に姓名が含まれる場合、結婚して姓名が変わると dn の値が変わることはありえる。他にも dn に含まれる ou の値が現実の組織名を表している場合、組織変更によって ou の値が変わったときに dn の値も連動して変わってしまう。一意な値というだけでデータベースの主キーにするのはよくないということがわかってきた。主キーは一意な値、且つ immutable が望ましい。

たとえば mongodb では _id を主キーとして使う。mongodb は主キーの値を変更することはできなくて実装上は delete & insert になる。

delete & insert の運用上の問題は更新時にトランザクションを使わないといけないため、パフォーマンスが悪い。さらに id 連携という業務に特化して言うと、たとえば、姓名の変更は名前が変わったというだけでその人が退職したわけではない。これをシステム上 delete & insert で扱うと、古いユーザーデータを削除して、新規にユーザーデータを作成するといった振る舞いになってしまう。そうすると、古いユーザーがもっていた権限やデータなどを移行しないといけないわけだが、それらをすべて自動化できるか?という難しい課題も積み重なってしまう。本質的に rename を delete & insert で扱うことそのものが誤っているのだ。

メンバーと相談して _id に uuid を発行して dn はフィールドに unique 制約を課して保持するよう設計を変更することに決めた。コレクションのデータ定義の主キー変更なのであちこち修正してテストコードも修正しないといけない。一意な値、且つ immutable な値のみを主キーとして使うのが今回の学びとなった。私自身、初期の設計に関わったときにこのことに気付かなかったから、これは私のレビューの失敗・見逃しでもある。

事務手続きに追われまくり

深夜に山本由伸投手のフォームのなにがすごいかを調べたりして遅くなった。

今日は別に体調は悪くなかったのだけど、単純にお仕事が忙しくて運動している余裕がなくてお休みした。たまにはそういうこともあるか。

mongodb の OR 検索

mongodb で OR 検索するには $or operator を使う。mongo-go-driver では次のように実装すればよい。

filters := make(bson.D, 0, len(query))
...
conds := make(bson.A, 0, len(v.Values))
for _, vv := range v.Values {
    conds = append(conds, makeFilters(vv))
}
filters = append(filters, bson.E{Key: "$or", Value: conds})

本業以外の雑多なお仕事

本業の開発も課題が山盛りで忙しいのだけど、それ以外にも雑多な事務手続きが増えてきてなかなか大変。人を相手にするとやり取りが発生して時間がかかる。お昼ご飯を食べる余裕がない。ダイエットにはよいかもしれない。

  • 社宅契約の手続き
  • 新たに顧問弁護士さんとの契約手続き
  • 信託銀行の投資信託運用のための手続き

結合テストのデバッグ

1時に寝て5時半に起きて7時半に起きた。なんか週の前半からバテている。

gitlab ci/cd の dind で mongodb のレプリカセット接続ができない

先日対応した mongodb のレプリカセット対応 で残った最後の課題。ローカルで実行すれば結合テストは動くが、gitlab ci/cd 環境では動作しないという問題が残っていた。gitlab-runner をローカルで実行できる ようにして、設定やパラメーターを変えたり、デバッグコードを埋め込んだり、コンテナに attach して振る舞いを確認したり、いろいろデバッグして原因はレプリカセット接続におけるホスト名の解決がコンテナ間でできていなかったことがわかった。

mongodb の結合テストは dockertest を使って実装している。これを gitlab ci/cd で動かすには dind を有効にする必要がある。dind 環境では2つのコンテナを使って結合テストが実行されるわけだが、テストが実行されるコンテナと mongodb コンテナが起動するコンテナの2つが生成される。このときにテストが実行されるコンテナから実際に mongodb が起動するコンテナのホスト名の解決と、mongodb が起動するコンテナ上での自分のホスト名の解決の2つが成立していないとレプリカセット接続ができない。要は1台のローカルホスト上で結合テストを実行するのと、2つのコンテナ上で実行されるのでは設定を変更する必要があるということに気付いた。

具体的には dockertest の次のパラメーターを、実行環境から解決するホスト名を考慮して設定すればよいと気付いた。

pool.RunWithOptions(&dockertest.RunOptions{
    ... 
    Hostname:   executor,
    Env: []string{
        ...
        fmt.Sprintf("MONGODB_ADVERTISED_HOSTNAME=%s", executor),
        ...
    }
})

たったこれだけの修正だし、現状の動作の振る舞いが分かればすぐに直せるものではあるけれど、このデバッグにはまた2-3時間を費やした。mongodb のレプリカセット接続はなかなか大変。