Posts for: #Go

エラーのラップと型階層

0時に寝て何度か起きて7時に起きた。署名・押印を完了した相続の書類を朝一で郵便局へ言って送付した。これで後は弁護士さんや司法書士さんが残りの手続きをやっておいてくれるのかな?少し気が楽になった。

ラップしたエラーのチェック

java でいうところの Chained Exceptions を go では Wrap という概念で表現する。Unwrap する interface を提供することで事実上の Chained Exceptions と同じようにオリジナルのエラーを辿ることができる。go はオブジェクト指向言語ではない と先日の勉強会でも念押し確認してエラーの型階層という概念はないが、Wrap をみていると型階層の方が自然じゃない?と考えられなくもない。言語の設計を考えるよいお題だと思えた。

エラーチェックについては次の記事がわかりやすかった。

カスタムのエラーを定義する。

type MyError struct {
	message string
	err     error
}

func (e *MyError) Error() string {
	return e.message + ": " + e.err.Error()
}

func (e *MyError) Unwrap() error {
	return e.err
}

このチェックのために errors パッケージに次の2つの関数がある。

  • errors.Is: エラーのインスタンスが同じかどうか
  • errors.As: エラーの型に代入可能かどうか (事実上はインスタンスの型が同じかどうか?)

たしかにこれでもすぐに実装できた。型階層なんかなくてもこれで十分でしょというのが go の意見かな。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。今日の議題はこれら。

週末は勉強会と実家に帰るので余裕がなかったのであまりネタがなかった。能の話しをしていて、子どものときに関心がなかったものを大人になって受け取り方が変わるものがあるのはなぜか?という話題で雑談した。はらさんも落語がわかるようになったという話しをされていた。この話しはストレッチのトレーナーさんとも盛り上がった。

子どもだともっている情報が少な過ぎて情報を理解できないのではないか?

情報や知識を蓄積することで対象への解像度があがるにつれて、みる視点が変わったり、興味が変わったりすることはあるかもしれないなと聞いていて思えた。課題管理の話しをしても、多くの開発者は無関心だったり共感を得られなかったりするが、そこにも通じるものがあるのかもしれないとなにかしらの取っ掛かりになるかもしれないと思えた。

go のジェネリクスと型の集合

0時に寝て何度か起きて7時に起きた。22時頃に素麺を食べたら夜に吐き気がして食べるんじゃなかった。

ストレッチ

今週はいつも通りの普通の1週間を過ごした感じ。休養していたわけでもなく負荷が高かったわけでもない。私の感覚的には右太もも後ろの筋と右腰の張りが強かった。トレーナーさん的には腰は大丈夫そうに言っていて、股関節全体の硬さは常態化しているものだけど、すねの筋もやや張りがあったように話していた。私の感覚とトレーナーさんの感覚がちょっとズレていた。今日の開脚幅は開始前154cmで、ストレッチ後158cmだった。一時期の3-4月の疲弊した状態ではないので一番悪い時期は脱したようにもみえる。

go の学び直し

Gopher塾 #5 - ジェネリクスが書けるようになろう に参加した。

すでにうちのプロダクトはジェネリクスを使った開発を行ってはいるけれど、私自身まだ曖昧なところがあったり、どういった設計がよいのかの手探り状態である。go はオブジェクト指向言語ではない (それ自体は構わない) ところが java のジェネリクスとは異なっていて、なにがその根本的な違いになっているのかを、私の中ではまだ理解できていなかった。その答えがこの勉強会に出てわかった気がする。もう少し独学して理論的に理解する必要はあるが、私に足りなかった知識が参加前よりも明確になったのでこの後の学習は容易に思える。

イベントではワークシートに自分で理解したメモや課題の回答を書き出すよう tenntenn さんが促していた。私はメモを自社の課題管理システムに書いていたのでワークシートには書いていないが、自分の理解を書き出すことの重要性は同意できる。tenntenn さんは次のように説明していた。

概念や理解したことを自分の言葉で表現できるか?を確認するために書くことは大事。

もし書いた内容が間違っていても、間違ったことを認識できることにも意味がある。

つまり、学んだことを書くことは正しくても間違っていても得られるものがある。

この考え方は私が提唱する課題管理にも通じている。なぜ書くかの理由の1つは自分の理解を整理するためでもある。そして、その文章を外部から監視できることで上長が助言できる。私にとっては習慣として身に付いた事柄ではあるけれど、改めてこういうことをチームのメンバーに啓蒙したり、開発ガイドに書いておくとよいように思えた。go の勉強会に参加して課題管理で学ぶことがあるとは思わなかった。よいこと尽くめだ。

閑話休題。本題のジェネリクスについて3割ぐらい知らないことがあった。私が java のジェネリクスと比べて go のジェネリクスを完全に理解できていなかったところの要因は 型の集合 (Type sets) の概念を理解できていなかったところに起因する。go ではジェネリクスを導入するにあたってインターフェースに型の集合という概念を導入した。それまでのインターフェースはメソッドの集合を管理するだけだったが、型の集合も管理できるようになった。また go には Underlying types という概念が当初から存在したが、それを意識してプログラミングすることはなかった。これを日本語にすると「基底型」となるが、オブジェクト指向言語で言うところの基底型とはまったく異なる。なにせ継承できないのだから。インターフェースに型の集合として次の記述ができるようになった。Underlying types はこれまで概念としてしか存在していなかったのがチルダを用いた文法で表現できるようになったため、その理解も強いられるようになった。

Go の “Type Sets” proposal を読む によると、現時点での型の集合とは次の2つを指す。

  • ~T approximation element (近似要素)
  • T | U union element (合併要素)

union 型のようなものは他言語でもある概念なのでイメージしやすいが、Underlying types をチルダを使って指定するのは go 独自?の概念なので新たに学び直す必要がある。

初めてのコントリビュート経験

4時過ぎに寝て7時に起きた。3時前まで相続手続きの書類作業して帰ってきてから本を読んでたら寝るのが遅くなった。

メンバーの oss へのコントリビュート

チームのメンバーがプロダクトで使っているライブラリにコントリビュートした。土日にライブラリのメンテナーからコメントがあって、その指摘にもすぐに対応して日曜日にマージされていた。ライブラリ側のレビューもすぐに終わってすごくうまくいったと言える。

もともとうちらが欲しい機能を作りかけた pr があったものの、作業途中でマージされずにクローズされていた。その残骸を参考にうちらの要件を満たせるかどうかの調査から始めた。調査を進めているうちにうちらの要件には足りない機能があってそれを拡張するようにも指示していた。すぐに出来たというのでライブラリにコントリビュートするように私が指示していた。

その後メンバーに聞いてみると、oss のライブラリにコントリビュートしたのは今回が初めてだという。開発者なら初めて oss にコントリビュートしたときのことを覚えているだろうか?私は、、、何だったか覚えてないが、おそらく linux distributor で働いているときに簡単なパッチを送った気がする。昔はメールや課題管理システムにパッチを投稿していたのが、いまは pr で気軽にパッチを送れる。若いメンバーがうまくコントリビュート経験を積めたことの背景に github がもたらしたパッチを送る簡単さがあるようにも思えた。oss にコントリビュートする機会は普通に oss を使って開発していればいくらでもある。マネージャーがその機会を見逃さず、ちゃんとメンバーにアサインしてあげることの大事さもあると思う。メンバーにはこの成功体験を活かして今後とも活躍してほしい。

サイトデザイン打ち合わせ

先日の ワイヤーフレームのレビュー の続き。顧問のはらさんとデザイナーさんと私の3人でデザイン案をみながらレビュー会をした。素人の私からみたら十分によいものが出来つつあるように思えるのでデザイナーさんのモチベーションを削がないようにサポートできればと思う。はらさんはデザインの専門家なので細かいところの確認や気付きをあげていてさすがという印象。きっとデザイナーさんのモチベーションも上がるのではないかと推測する。私はこれまでデザインの打ち合わせに出たことがなかったので参考になることが多々ある。サイトのトップは普通は会社の紹介があるものらしいけど、私がそれはいらないとデザイナーさんに伝えたのでトップで会社紹介を一切しない稀な構成のサイトのデザインが出来上がった。その後、社名ぐらいあった方がよいのではないかという話しもあってこれから変えるかもしれない。私の会社情報を書かない理由は次になる。

  • 一見さんの訪問客がみるようなサイト (会社) ではない
  • 知人やうちの会社の関係者がみて近況を手早く分かるようにしたい
  • デザインはシンプルな構成にしたい

goleak と context によるキャンセル制御

0時に寝て何度か起きて7時に起きた。いつもなら日曜日は徹夜して翌日の早朝に出掛けるのが月曜日は行かなくて済むのでちょっと楽になった。

amqp091-go の context 制御

goroutine リークを検出するツールに uber-go/goleak がある。ずっと前から余裕のあるときに結合テストの導入しようという issue を作っていたものの、適当なタイミングがなかった。先週末に少し手が空いたので着手した。goleak は個別のテストメソッドにも TestMain にも両方に対応している。結合テストの TestMain に入れた方が保守コストが下がるのでそういった用途がよいのではないかと思う。

go の TestMain がこういうものかもしれないが、defer 文を使う終了処理があるとそのコードを直接 TestMain には実装できない。関数で wrap して m.Run() を実行した結果を返すようにしないといけない。そこに goleak を入れる場合、goleak.Cleanup を何もしない関数に置き換えて m.Run() の結果を返せばよいのではないかと思う。そして VerifyTestMain() は m.Run() を実行してからすぐに goroutine が動いていないかをチェックする。ここで結合テストを動かすための、環境構築のために http サーバーを goroutine で起動するとか、テストのための goroutine が動いているとそれも検出してしまうのでそれらの goroutine は無視できるよう、2つのオプションが用意されている。

  • IgnoreTopFunction: 明示的に無視してよい goroutine のトップ関数を指定する
  • IgnoreCurrent: オプションを登録した時点で稼働している goroutine を無視する

これらを踏まえて TestMain で goleak を使うと次のようなコードになった。しかし、おそらくこの使い方はあまりよくない。いくつか goroutine を無視する設定を追加したために、そこに意図しない goroutine リークが隠蔽されてしまう懸念がある。

func main(m *testing.M) int {
    defer myTearDown()

	var code int
	goleak.VerifyTestMain(
		m,
		goleak.Cleanup(func(exitCode int) {
			fmt.Println("skip goleak cleanup", exitCode)
			code = exitCode
		}),
		goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"),
		goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"),
		goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"),
		goleak.IgnoreCurrent(),
	)
	return code
}

func TestMain(m *testing.M) {
	os.Exit(main(m))
}

さらにこの調査をしているときに amqp091-go の api も context 受け取った方がシンプルでいいんじゃない?と思って提案の pr を送ってみた。context 使わなくても自前でキャンセルする api は提供されているため、開発者の考え方によってこの提案を拒否するのも妥当な判断だと思える。次のメジャーバージョンとか、互換性を維持しなくてよいタイミングから取り入れようという考え方もあるかもしれない。

unix crypt(3) をよくわかってなかった

0時に寝て2回ほど起きて7時に起きた。わりと気分がよい方。

unix の crypt(3) というライブラリ実装

google の Admin console の api の REST Resource: usershashFunction として crypt を選択してハッシュ化したパスワードを連携できる。

crypt - C crypt ライブラリに準拠しています。DES、MD5(ハッシュ プレフィックス $1$)、SHA-256(ハッシュ プレフィックス $5$)、SHA-512(ハッシュ プレフィックス $6$)ハッシュ アルゴリズムをサポートします。

この crypt というのは単純に sha256 や sha512 でハッシュ化すればよいわけではなく、歴史的経緯でそれぞれの os ごとにある crypt ライブラリの実装に依存しているらしい。

$ man 3 crypt

おそらく google のドキュメントがいう C crypt ライブラリというのは glibc のことを指していると考えてよいと思うが、go の準標準パッケージである golang.org/x/crypto を探してもその実装は存在しない。これも推測だが、仕様が曖昧なものを go の開発者は実装しようとしないのだと思う。とはいえ、c の crypt ライブラリをラップして go から使うのも面倒と言えば面倒なので誰かが crypt ライブラリを真似て野良実装して、それが一部で使われていたりするようにみえる。しかし、なぜかそのオリジナルを作った開発者はそのコードのリポジトリを削除していて、ソースコードのコピーがまわりまわって、いま github.com/GehirnInc/crypt で保守されているらしい。このライブラリを使ってエンコードすると c の crypt ライブラリの出力と一致することは確認できた。この実装をみれば、単純にエンコードすればよいといったものではないことが伺えるので pure go のライブラリとして共有されているのは有り難い。

このライブラリを使ってハッシュ化した文字列と c 言語のコードも chatgpt に書いてもらっていくつか一致することは検証できた。デバッグしていて、もう1つ salt を生成も特定の文字しか使えないのでうっかり乱数を使って文字列生成していると間違ってしまう。

var saltChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./")

func GenerateSalt(method Method) []byte {
	var b = make([]byte, 16)
	charsLength := len(saltChars)
	for i := range b {
		b[i] = saltChars[rand.Intn(charsLength)]
	}
	var salt []byte
	switch method {
	case SHA256:
		salt = append([]byte("$5$"), b...)
	case SHA512:
		salt = append([]byte("$6$"), b...)
	default:
		panic(fmt.Sprintf("unsupported salt method: %s", method))
	}
	return salt
}

ここで生成した salt を使って github.com/GehirnInc/crypt を使うとこんな感じで crypt を使って google のユーザーアカウント連携ができる。

func Crypt(password, salt []byte) (string, error) {                              
    if len(salt) < 3 {                                                           
        return "", fmt.Errorf("invalid salt: %s", string(salt))                  
    }                                                                            
                                                                                 
    var crypter crypt.Crypter                                                    
    switch string(salt[0:3]) {                                                   
    case "$5$":                                                                  
        crypter = crypt.SHA256.New()                                             
    case "$6$":                                                                  
        crypter = crypt.SHA512.New()                                             
    default:                                                                     
        return "", fmt.Errorf("unsupported salt prefix: %s", string(salt[0:3]))  
    }                                                                            
    hashed, _ := crypter.Generate(password, salt)                                
    err := crypter.Verify(hashed, password)                                      
    return hashed, err                                                           
}

ハッシュ化した文字列が正しいかどうかは実際に google にログインしてみないと判別できないのでわりとデバッグや検証に時間がかかった。

リファレンス

サーバーサイド開発とセマフォ

0時に寝て7時に起きた。

web api サーバーへの負荷テスト

web api サーバーへ数百から数千件の同時リクエストを送ってエラーが発生しないことを確認する。チームのメンバーがテストを実施していたら producer がメッセージを送信するときに rabbitmq との接続エラーがいくつか発生した。いくつか対応方法を考えられるが、既存のコードを大きく変更せず解決するものとしてセマフォを導入してみた。自分で作っても難しいものではないが、golang.org/x/sync/semaphore で準標準パッケージとして提供されている。次のように簡単に使える。

sem := semaphore.NewWeighted(maxConcurrentSessions)
...
ctx := context.Background()
if err := sem.Acquire(ctx, 1); err != nil {
    return err
}
defer sem.Release(1)

これで rabbitmq との同時接続数を制御する。rabbitmq 側もどのぐらいの接続を受け付けるかは Networking and RabbitMQ を参照して設定で制御できる。デフォルトは 128 となっているので 1024 ぐらいまで増やしてみた。

サーバーサイド開発のおもしろさの1つとしてボトルネックは移動するという概念がある。必ずどこかにニーポイント (ボトルネック) は現れるので意図したパフォーマンスや負荷を耐えるようにリソース制限をしてサーバーが堅牢になるよう調整する。この手の作業はサーバーサイドエンジニアをやってきた私の得意とするところ。

一日中リファクタリング

0時に寝て7時に起きた。昨日は夜にホテルで作業しようと思いながらテレビをみているうちに寝落ちしてた。朝から夜までずっとリファクタリングのためにコードを書いたり、コンテナ環境の設定を変更したりしていた。

mongodb のコネクションプール

MongoDB Drivers の Connection Example に次のようなことが書いてある。

Reuse Your Client

We recommend that you reuse your client across sessions and operations. You can use the same Client instance to perform multiple tasks, instead of creating a new one each time. The Client type is safe for concurrent use by multiple goroutines. To learn more about how connection pools work in the driver, see the FAQ page.

mongodb drivers の client は goroutine safe なので再利用することを推奨している。内部的にはコネクションプールをもっていて mongodb とのコネクションを再利用できる。具体的にはライブラリ内部に次のようなコードがみつかる。context にセッション情報があればそれを使い、なければクライアントの sessionPool (コネクションプール) を使ってセッションを取得して mongodb にアクセスする関数の終わりで終了処理を行う。

sess := sessionFromContext(ctx)
if sess == nil && coll.client.sessionPool != nil {
	sess = session.NewImplicitClientSession(coll.client.sessionPool, coll.client.id)
	defer sess.EndSession()
}

既存のコードはコネクションプールのことを考慮していないコードになっていたので大きくリファクタリングして効率化した。

docker hub の pull 制限

午前中はリファクタリング、午後は docker compose 環境の変更と再構築、午後はバグ修正と一日中 docker image を取得する作業をしていた。gitlab ci/cd が動くとテストと docker image 生成の処理が動くのでその過程で関連する docker image を pull する。夕方になって gitlab ci/cd で初めて次のエラーが発生することに気付いた。前にお手伝いしていた職場でもそういう現象が起こると聞いて、docker login するコードを github actions のスクリプトに追加していたので、rate limit がかかることは知っていた。

You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limits.

Understanding Your Docker Hub Rate Limit によると、6時間あたり匿名アクセスは100、 free ユーザーは200を上限としているらしい。匿名アクセスは ip アドレスでカウントしているのだろうから場合によっては会社内からのアクセスをすべてカウントされたりするかもしれない。課金するとこの上限が24時間あたり5000になる。docker hub のプライベートリポジトリを利用する意図で team プランの課金を検討していたが、docker hub のアクセス制限を緩和するために課金する必要があるかもしれない。

docker/compose のモジュールの使い方がわかってきた

0時に寝て7時に起きた。前日はバテてだらだらしていたので寝過ぎた。

案ずるよりもツールできた

先週末に docker/compose 関連のライブラリ調査 を終えて実際のツール作りをしていた。本当は日曜日に作ってしまおうと思いつつ休んでしまった。なぜか今日はメンバーが全員お休みでチームで私しか働いていなかった。年度末で有休消化しているのかな?問い合わせ対応やメンバーのサポートが不要だったので1日中、自分の開発に集中できた。そして開発に集中できた結果、一通りツールの機能の開発を終えられた。火曜日までには仕上げたいと思っていたのでぎりぎり間に合った。

最終的に testcontainers-go の compose モジュールを使うのは断念して compose cli のみ go 標準の os/exec パッケージを使ってプロセスを fork するようにした。また docker image をコンテナレジストリから取得するときに認証が必要な場合、最初の docker login すると credentials store にパスワード (またはトークン) 情報が記録される。設定情報は $HOME/.docker/config.json からも確認できる。この仕組みを使ってコンテナレジストリへのログインを自動化できる。私の環境では macbook に docker desktop をインストールしているが、普通に使っていると次のように credentials が保存されてその内容を確認できる。

$ docker-credential-desktop list | jq .
{
  "https://index.docker.io/v1/": "t2y1979",
  "https://index.docker.io/v1/access-token": "t2y1979",
  "https://index.docker.io/v1/refresh-token": "t2y1979"
}
$ echo "https://index.docker.io/v1/access-token" | docker-credential-desktop get
{"ServerURL":"https://index.docker.io/v1/access-token","Username":"t2y1979","Secret":"***"}

これと同じことを docker のライブラリで行うには次のようにする。取得したい docker image の uri を参照すればコンテナレジストリがわかる。そこからこの cli でやっているようなことを順番にやっていけばよい。これらのユーティリティは3つのリポジトリで管理されていて、この雰囲気をみただけでもこのモジュール分割が本当に適切なんやろか?とか思ったりもする。

func getRegistryAuthFromImage(
	ctx context.Context, imageURI string,
) (string, error) {
	ref, err := reference.ParseNormalizedNamed(imageURI)
	if err != nil {
		return "", fmt.Errorf("failed to parse image uri: %w", err)
	}
	repo, err := registry.ParseRepositoryInfo(ref)
	if err != nil {
		return "", fmt.Errorf("failed to parse repository: %w", err)
	}
	dcli, err := command.NewDockerCli()
	if err != nil {
		return "", fmt.Errorf("failed to create docker cli: %w", err)
	}
	auth := command.ResolveAuthConfig(ctx, dcli, repo.Index)
	encoded, err := command.EncodeAuthToBase64(auth)
	if err != nil {
		return "", fmt.Errorf("failed to encode auth: %w", err)
	}
	return encoded, nil
}

プライベートリポジトリの go アプリケーションの依存解決

0時に寝て6時半に起きた。面倒なお仕事を朝から集中して片づけた。

go.mod のプライベートリポジトリの依存解決

非公開のプライベートリポジトリで開発している go アプリケーションを他のリポジトリから依存ライブラリとして使う方法を調べた。go modules は基本的に公開されたパブリックなリポジトリを前提としている。go.mod のワークフローで他の依存ライブラリと同様にバージョン管理ができるようにするには、プライベートリポジトリであることを go.mod に認識させ、トークンなどを使って認証する必要がある。

go 1.13 から GOPROXYGOPRIVATE という環境変数が追加された。デフォルトでは GOPROXY は https://proxy.golang.org に設定されており、このプロキシサーバー経由で依存ライブラリを取得する。これは公開リポジトリを、ある日、作者が急に削除したり非公開にしたときにビルドできないといった問題を防ぐために依存ライブラリのリポジトリをキャッシュしてくれる役割を担っている。

一方でインターネット上のプロキシサーバーからはプライベートリポジトリへアクセスできないので GOPRIVATE を設定してアクセスできないサイトを go.mod へ教えてあげる必要がある。

$ go env -w GOPRIVATE="private.repo.jp"
$ go env | grep GOPRIVATE
GOPRIVATE="private.repo.jp"

通常 go.mod は https で依存ライブラリをリポジトリから取得 (クローン) しようとする。このときに git の設定を変更することで特定のサイトへのアクセスを ssh 経由に変更できる。次の例では https://private.repo.jp へのアクセスをすべて ssh://git@private.repo.jp でクローンできる。

$ git config --global url."ssh://git@private.repo.jp".insteadOf "https://private.repo.jp"`
$ tail -n 2 ~/.gitconfig
[url "ssh://git@gitlab.osstech.co.jp"]
        insteadOf = https://gitlab.osstech.co.jp

これで git リポジトリのクローンは ssh で行われるようになるが、go.mod のワークフローでは https でもアクセスする処理があるようにみえる。https でもアクセスできるように PAT などのトークンを取得して次のように ~/.netrc に https でプライベートリポジトリへアクセスするための認証設定を追加する。

machine private.repo.jp
login t2y 
password ${PERSONAL_ACCESS_TOKEN}

コワーキングのオンラインイベント

月例のカフーツさんのオンラインイベントに参加した。先月の所感はここ 。いとうさんの近況を把握していないが、どうやらバリ島 (インドネシア) のコワーキングスペースをいくつか訪問してきたらしい。その旅程を写真をみながらふりかえるようなイベントだった。バリ島の自然や建物の雰囲気が伺えてとてもおもしろかった。

いとうさんからバリ島はデジタルノマドの先進的な取り組みをしているように聞いていた。例えば バリ島でリモートも可能!?インドネシアのノマドビザは最長5年を検討中 にあるように最長5年のビザを用意しているという話しが日本で盛り上がっているが、現地ではまだ正式にそのようなビザがあるようには存在が確認されていないらしい。それに近い長期のビザを取得するには、一定の金融資産があることを証明しないといけないらしく、日本円だと数千万円程度ないとビザを取得できないのではないか?といった話題もあったと思う。基本的にインドネシア政府は海外から金持ちを呼び込みたいらしく、金持ち向けに待遇のよいビザを整備するのではないかとのこと。ビザの話しはともかく、ここ2-3年でバリ島のコワーキングスペースもいくつか廃業しているらしい。それは利用者の大半が外国人であったため、コロナ禍により、インドネシア政府からの要請もあって外国人に帰国を促したという。外国人利用者の半分ぐらいが自分たちの国に帰ってしまい、施設の運用コストを維持できなくなった。バリ島は今後の成長が見込まれていることから数年前から外資が入ってきて土地や家賃が急騰しているために利用者が激減すると運用コストを維持できなくなったとのこと。

肝心のバリ島のコワーキングスペースの雰囲気を聞いていると、利用者は外国人が大半で、感覚的にはヨーロッパから来ている人が多そうだといとうさんが話された。バリ島のコワーキングスペースでいとうさんが見学していたときには、現地の人たちと外国人のコミュニティが活発に活動しているようにはあまりみえなかったらしい。利用者もそう多くはなかったし、その人たちも静かにただ作業しているだけのように映ったという。いとうさんはコワーキングスペースとはコミュニティやコラボレーションが重要という話しをよくしているけれど、バリ島のコワーキングスペースに関しては裕福な外国のデジタルノマドを呼び込むのに成功しただけのような、もしかしたらコロナ禍でそのコミュニティが破壊されてしまったのかもしれないが、普段このイベントで話しているような高い期待値に応えられるような状況ではないようにみえたという。

とはいえ、日本の田舎よりは遥かにデジタルノマドが集まる場所として世界的に認知されているところなので写真をみながら私もいつか行ってみたいと思えた。

リファクタリングにはまった

2時に寝て変な夢をみて何度か起きて7時に起きた。いつも通りな感じ。

go のシリアライズ/デシリアライズとポインタ

ldap の distinguished name (以下dn) をパース するときはエスケープを扱う必要があるのでわりとややこしいのでライブラリを使った方がよいことを前回学んだ。

複数の web api で dn を扱っていると、それぞれで dn をパースして正規化した形で扱わないと大文字小文字の表記揺れなどに対応できなくて困るということに気付いた。いや、前回もうっすら気付いていたのだけど、既存の api はすべて対応しているからいいかなと楽観的に考えていた。すると、たまたま新規に dn を扱う web api を作ったときに正規化を忘れていることに気付いた。今後の保守や拡張を考慮すると、dn を string 型として扱うのは潜在的に正規化漏れの懸念があることからよくないと理解できた。そのため、既存のリクエストで受け取る dn を特別な型として必ず正規化して扱えるようにリファクタリングすることにした。あちこち直す必要はあったが、幸いにも単体テストも結合テストもそこそこあるのでバグっていればテストが落ちることで不具合には気付けるようになっていた。

encoding/json に Marshaler/Unmarshaler のインターフェースが定義されているのでそれぞれのメソッドを実装する必要がある。DNParameter の値を json にシリアライズするときは値レシーバで MarshalJSON メソッドを実装し、デシリアライズするときはポインタレシーバで UnmarshalJSON メソッドを実装しないと json ライブラリで意図した振る舞いにならないようにみえる。ここで UnmarshalJSON するときに byte 列から一旦 json の文字列に変換 (引用符を外す) してから ldap.ParseDN() しないといけない処理を直接 string 型に変換する誤ったコードを書いてしまって、この誤りに気付くのに1-2時間はまってしまった。

  • 誤ったコード
func (p *DNParameter) UnmarshalJSON(data []byte) error {
	dn, err := ldap.ParseDN(string(data))
	if err == nil {
		(*p).Value = dn
	}
	return err
  • 正しいコード
func (p *DNParameter) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return fmt.Errorf("dn should be a string")
	}
	dn, err := ldap.ParseDN(s)
	if err == nil {
		(*p).Value = dn
	}
	return err

このときに引用符を ldap のパーサーがエスケープした形で扱えてしまい、テストは失敗するけれど、見た目がほとんど同じ文字列で動いてしまうのにはまった。引用符がエスケープされてテストが落ちることには気付いたものの、どこの処理が問題なのかが分からなくてはまっていた。おそらくスクラッチからこの仕様でコードを書いていたらすぐに気付いたと思えるが、リファクタリングであちこち書き換えていたからどこの処理が誤っているのかの切り分けに時間がかかった。

syncrepl ことはじめ

1時に寝て5時に起きて7時に起きた。

openldap の syncrepl

openldap サーバー同士のレプリケーションの仕組みとして syncrepl (LDAP Sync Replication) と呼ばれる仕組みがある。experimental ではあるものの、仕様は rfc で提案されていてオープンになっている。実際には openldap サーバー (slapd) で実装されている参照実装が仕様みたいなものかもしれない。

既存の java のアプリケーションで syncrepl の機能を使うツールがあって、ソースコードを読むとかなりレガシーで、もっと言うと設計はひどく、コードも推定バグのような実装をみかけるのであまり使いたくない。go-ldap で syncrepl できれば作り直してしまえばよいのではないかと考えている。ドキュメントには provider (master) と consumer (slave) という用語で説明されているので pubsub に近いような仕組みで実装されているのではないかと推測する。slapd は provider にも consumer にもなる可能性があるのでどちらの実装ももっている。私がやりたいことは更新エントリを取得したいだけなので go-ldap で consumer として振る舞うモジュールのみを作ればよい。

基本的には openldap サーバーに接続して bind して、syncrepl に準拠したやり取りをすればよいのでそんな難しそうにはみえない。いくつかの定型的な通信の実装をすれば consumer ができるのではないかと思う。go-ldap のソースを grep してみる分には特別な機能はなさそうにみえる。ないならないで私が作ってもいいし、このライブラリでやらない方がよい背景があるのであれば、それに従う。ひとまず背景がわかっていないので issue を立てて聞いてみた。