emahiro/b.log

Drastically Repeat Yourself !!!!

MySQL で時刻を時刻型で保存するか Unix Timestamp で保存するかを考えた話

Overview

MySQL でよくある CreatedAt と UpdatedAt を時刻型で保存するのか、Unix Timestamp で保存するかを考えてみたのでその備忘録です。

なお考えるにあたって以下のブログを参考にさせてもらいました。

www.m3tech.blog

前提

この議論を考えるにあたっての前提は以下です。

Unix Timestamp

Pros

  • MySQL 上のデータサイズが時刻型より小さい。
  • 数字型(INT型 or BIGINT型)の場合コード上で取り扱いやすい (*1)
  • Go を使う場合 Time 型を使うよりメモリに優しい。

*1 について

数字で時刻を取り扱うことが可能なので時間の前後を数字の大小として比較することが可能です。

// User.CreatedAt が Datetime のとき

now := time.Now()

// ユーザーが作られた時間が現在より前かどうかを検査したいとき
if user.CreatedAt.Before(now) {
    // TBD
}

と書くところを以下のように書ける。

// User.CreatedAt が int64 のとき

now := time.Now().Unix()
// ユーザーが作られた時間が現在より前かどうかを検査したいとき
if user.CreateAt < now {
   // TBD
} 

Before/After でも分かりやすいが数値で比較する方がより直感的で分かりやすいと思います。

Cons

  • TIMESTAMP型 を使うと 2038年問題 がある。2020年代でこの型を使うのはあまり現実的ではない。なお、この 2038 年問題は INT (もしくは BIGINT 型) で Timestamp を保存することで回避可能ではあある。

TIMESTAMP には、'1970-01-01 00:00:01' UTC から '2038-01-19 03:14:07' UTC の範囲があります。

ref: https://dev.mysql.com/doc/refman/8.0/ja/datetime.html

  • unix time は直感的にそれが何時何分なのかが分かりづらい。
    • from_unixtime() 関数 を使えば、Datetime 同様の直感性は手に入れらますが、毎回これを差し込むことになります。

時刻型(DATETIME)

Pros

  • MySQL 上で時刻が分かりやすい。
  • 時刻で絞ったクエリを書きたい場合 createdAt < "2022-04-08" のような直感的なクエリを書くことが可能。

Cons

  • Unix timestamp よりデータ型が大きいです。(と言っても意識するレベルではないと思いますが。)
  • Timezone の影響を受ける。
    • これも原則時刻型は UTC にして保存しておく、等の設計の工夫が求められるポイントになる。

結局どちらを選んだか?

今回は業務で実装するシステムもあり Datetime を選びました。理由の大きいところとしてはやはり運用のしやすさ、時刻型の Pros の 2 つ目の理由が大きいです。
データサイズやコード上での取り回しやすさを考えるとエンジニアとしては Unix Timestamp を採用したくなります。
Pros/Cons には入れませんでしたが、Unix Timestamp でフォーマットの影響を受けないという単純さも魅力です。パースはGUI側で取り回す(= フロント側の責務に持っていく)方が今時っぽい関心の分離方法だと思います。

しかし、事業をする上でやはりすぐにクエリを引きたい、という要求は後々ニーズとして大きくなるものです。分析でも使いますし、障害対応のケースなどでも影響範囲を出すためにクエリを引くことはあります。

初期のデータモデルが後々まで尾を引くことになるので、最初から多少のデメリットはあってもシンプルなクエリを書けることを優先しました。(データサイズもクラウドがスタンダードの現代にあっては多少は許容できるデメリットだと判断しました。)

まとめ

チームで軽く雑談で聞いてみましたが、Unix Timestamp 側の観点でいた自分にとっては運用観点を知るいい機会になりました。
運用は本当に大事で運用のことを少しでも考えて設計できるかはエンジニアとしての実力が出るところなので1つの観点を知るきっかけになってもらえれば幸いです。

Go1.18 の Generics を使って Slice の重複削除の処理を書く

Overview

Go でスライスの重複処理を実装するのに Generics が使えるので実際に実装してみました。

なお、Go1.18 以前の世界ではスライスの重複を削除するには map と空の struct を使って以下のように実装する必要がありました。

ema-hiro.hatenablog.com

なお、for を1回で実装するなら以下のようになります。

arr := [string]{"a", "b", "c", "a"}
m := make(map[string]struct{}) // 空のstructを使う
uniq := [] string{}
for _, ele := range arr {
    if _, ok := m[ele]; ok {
        continue
    }
    m[ele] = struct{}{} // m["a"] = struct{}{} が二度目は同じものとみなされて重複が消える。
    uniq := append(uniq, ele)
}

golang.org/x/exp/slices を使って重複削除の実装を書く

golang.org/x/exp/slices にある実装を使って重複削除を実装できます。
使用するのは以下の3つの関数です。

  • slices.Clip ... 余分に確保してるメモリ領域を解放する。後述の Compact をかけた前後で Slice が占有してるアロケーション領域が変わらない(要素は4になるけどcapacity は 8 のまま、ということ)のでこのアロケーション領域を圧縮するのに使います。
  • slices.Sort ...予約型のスライスを昇順で Sort する。
  • slices.Compact ... 昇順でソートされているスライスの重複要素を削除する。
    ※ Compact は string 型においては Sort されている状態じゃないと重複要素を削除できません。数値型については Sort されている状態ではなくても重複削除されます。
    ref: https://go.dev/play/p/tkfgpLwGgFX

上記の条件を踏まえた上で golang.org/x/exp/slices を使って重複削除の実装は以下になります。

func Uniq[T constraints.Ordered](s []T) []T {
    slices.Sort(s)
    return slices.Clip(slices.Compact(s))
}

※ Type Parameter には Sort 可能な constrains.Ordered を使用することで slices.Sort での並び替えを使えるようにします。

ベンチマークしてみる

書き換えたところで Generics を使っていない実装よりもパフォーマンスが落ちるのであれば抽象化したところでコードの削除とパフォーマンスがトレードオフになってしまいます。そのため Generics ( golang.org/x/exp/slices 内の実装) を使った重複削除処理のベンチを取ってみます。

※ 今まで使っていた重複削除処理を比較対象とします。

検証する実装は以下

func uniq[T constraints.Ordered](s []T) []T {
    slices.Sort(s)
    return slices.Clip(slices.Compact(s))
}

func legacyUniq(s []string) []string {
    m := make(map[string]struct{}, len(s))
    uniq := s[:0]
    for _, ss := range s {
        if _, ok := m[ss]; ok {
            continue
        }
        m[ss] = struct{}{}
        uniq = append(uniq, ss)
    }
    return uniq
}

ベンチマークを取る実装は以下

func BenchmarkUniq(b *testing.B) {
    s := []string{"1", "2", "3", "10", "9", "10", "9", "8", "100", "200", "100"}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        uniq(s)
    }
}

func BenchmarkLegacyUniq(b *testing.B) {
    s := []string{"1", "2", "3", "10", "9", "10", "9", "8", "100", "200", "100"}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        legacyUniq(s)
    }
}

結果は以下です。

go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/emahiro/il/go-generics-il
cpu: VirtualApple @ 2.50GHz
BenchmarkUniq-10                13086816                91.91 ns/op            0 B/op          0 allocs/op
BenchmarkLegacyUniq-10           2504686               474.2 ns/op           288 B/op          1 allocs/op
PASS
ok      github.com/emahiro/il/go-generics-il    3.260s

Generics を使った処理の方がパフォーマンスが良い、という結果になりました。

まとめ

既に公開されている slices パッケージの機能を使って、今まで Go では冗長に書かざる得なかった重複削除処理をスマートに書き直すことができました。
パフォーマンス観点でも新しい機能を使ってみた方が良い結果が得られたのも良い発見でした。

Generics をさっと触ってみて思いましたが、Go1.18 で導入された Generics を使う際には、ゴリゴリこれを使って今まで Go になかったような機能を実装するよりもまず Go ならではの冗長な実装を Generics を使って抽象化していく、あたりから始めると良さそうだなと思いました。まだ出来立てホヤホヤの機能だし必要になったら使う、くらいのモチベーションの方がいいのかもしれません。
それはそれとして慣れるために何がなんでも Generics 使って書く、みたいなのもまぁありだとは思います。

Go1.18 の Generics を使ってみた話

Overview

Go 1.18 がリリースされて目玉の機能である Generics について一通り触ってみました。

github.com

実装に入門する

公式の Go の Genericsチュートリアルを参考に実装してみました。

go.dev

またより詳しい入門には以下の公式のブログがあります。

go.dev

サンプル実装

map の key を取り出す

ありがちな実装ですが map から key のみを取り出したり、value だけを取り出したり、といった処理を書くことができます。
今までは取り出した値の型ごと(int, string) に Keys だったり Values の実装が必要でしたが、これからは以下のようにかけます。

func Keys[T comparable](m map[T]interface{}) []T {
    keys := make([]T, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

簡単に再実装をしてみましたが、これは https://pkg.go.dev/golang.org/x/exp@v0.0.0-20220325121720-054d8573a5d8/maps#Keys に既に実装されてました。

スライスのシャッフル処理

func shuffle[T comparable](s []T) {
    rand.Seed(10) // seed は任意の自然数
    rand.Shuffle(len(s), func(i, j int) {
        s[i], s[j] = s[j], s[i]
    })
}

ref: https://go.dev/play/p/OToYELH1S-1

その他使えそうなユースケケースについて

以下でまとめたことがありましたが、API クライアントの共通化処理において Setter っぽい実装で Getter を書くというユースケースにおいては Generics を使うことができそうだなと思いました。

ema-hiro.hatenablog.com

golang.org/x/exp 配下のパッケージついて

今まで型ごとに冗長な実装をせざる得なかった、slice や map の操作については以下の2つの packages にまとまっています。

  • golang.org/x/exp/maps
  • golang.org/x/exp/slices

ありがちな sort なども slices パッケージに実装されています。

https://cs.opensource.google/go/x/exp/+/054d8573:slices/zsortordered.go;drc=054d8573a5d8a1df3b1d31df254b260a09c7ebe0;l=12

まとめ

以下のブログにある通り、これを使ってもっとゴリゴリコードを抽象化したり関数型っぽい実装が増えることもあるのかもしれませんが当面はユースケースを絞って適用していく、くらいのところでまずは留めておこうかなと思っています。特にプロダクションで書く場合には。

future-architect.github.io

Go の unstable version で gopls を使う

以下の設定をする。

# Create an empty go.mod file, only for tracking requirements.
cd $(mktemp -d)
go mod init gopls-unstable

# Use 'go get' to add requirements and to ensure they work together.
go get -d golang.org/x/tools/gopls@master golang.org/x/tools@master

go install golang.org/x/tools/gopls

ref: https://github.com/golang/tools/blob/master/gopls/doc/advanced.md#unstable-versions

gopls-unstable dir の部分は .local/go などの任意のディレクトリでも可能。

go install ~ すると GOPATH 配下にコマンドがインストールされて path が通るので、上記手順で自動的に最新版(手元にある Unstable version )で gopls が動くようになります。

strings package を使って簡単なメールのフォーマットチェックをする

Overview

タイトルの通りです。Go の strings パッケージを使って簡単なメールアドレスのフォーマットチェックをします。
Go の正規表現は重たい(Go に限った話ではないですが)処理なので何かの文字列の検査をするときは strings package を使う方がパフォーマンスにも優しいことが多いです(ちょっとめんどくさいですが)

strings.Split を使う

@ ドメインでぶった斬るパターンですね。ema@emahiro.com みたいな文字列を考えると Split した結果の slice の長さは 2 なので ema@@emahiro.com みたいなケースをカバーして以下のようなサンプルをかけます。

func StringSplit(email string) error {
    splt := strings.Split(email, "@")
    err := errors.New("invalid format")
    if len(splt) != 2 {
        return err
    }
    if len(splt[:len(splt)-1]) == 0 &&
        len(splt[len(splt)-1:]) == 1 {
        return err
    }
    if len(splt[:len(splt)-1]) == 1 &&
        len(splt[len(splt)-1]) == 0 {
        return err
    }
    return nil
}

strings.Index を使う

特定の文字までの位置を返してくれる Index も使えます。ここでは @ までの Index の数を考えると

  1. ema@ の時 -> Index の値が len("ema@") - 1 に一致する。
  2. @emahiro.com のとき -> Index の値が 0
  3. ema のとき -> Index は -1
func StringIndex(email string) error {
    switch strings.Index(email, "@") {
    case -1, 0, len(email) - 1:
        return errors.New("invalid format")
    }
    return nil
}

余談: パフォーマンスを見てみる

strings.Split を使っても strings.Index を使っても検査は可能ですが試しにパフォーマンスを見てみます。

package main

import (
    "strings"
    "testing"
)

func BenchmarkStringIndex(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = strings.Index("ema@emahiro.com", "@")
    }
}

func BenchmarkStringSplit(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = strings.Split("ema@emahiro.com", "@")
    }
}

出力結果は以下

go test -v -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: emahiro.dev/go-bench-sandbox
cpu: VirtualApple @ 2.50GHz
BenchmarkStringIndex
BenchmarkStringIndex-10         153256222                7.003 ns/op           0 B/op          0 allocs/op
BenchmarkStringSplit
BenchmarkStringSplit-10         17826536                65.38 ns/op           32 B/op          1 allocs/op
PASS
ok 

Index の方がパフォーマンスが良いですね。

まとめ

文字列チェックするときは Index 使う方が良いっぽいです。

SES SendRawEmail API を使って画像付きメールを送信する

Overview

SES SendRawEmail API を利用して画像付きのメールを送信するための備忘録です。

実装について

基本的な実装方法については Amazon SES API を使用して raw E メールを送信する を参考にすればできます。

画像付きのメールを送信する上でポイントになるのは 添付ファイル のチャプターを参考にします。

E メールにファイルを添付するには、base64 エンコードを使用して添付ファイルをエンコードする必要があります。
添付ファイルは通常、次のヘッダーを含む専用の MIME メッセージ部分に配置されています。

- Content-Type: 添付ファイルの種類。一般的な MIME Content-Type 宣言の例を以下に示します。プレーンテキストファイル: Content-Type: text/plain; name="sample.txt"

- Microsoft Word ドキュメント: Content-Type: application/msword; name="document.docx"

- JPG イメージ: Content-Type: image/jpeg; name="photo.jpeg"

- Content-Disposition: 受取人の E メールクライアントがコンテンツをどのように処理するかを指定します。添付ファイルの場合、この値は Content-Disposition: attachmentです。

- Content-Transfer-Encoding: 添付ファイルのエンコードに使用されるスキーム。添付ファイルでは、ほとんどの場合この値は base64です。

ref: Amazon SES API を使用して raw E メールを送信する > 添付ファイル

Go での実装について

Go で実装するにはこのドキュメントで定義されているファイルを添付するための Header の定義を textproto.MIMEHeader{} の中に追加して、最後にこの Header を byte 配列に変換して扱います。

byte 配列は最終的に Amazon SES API を使用して raw E メールを送信する > MIME の使用で示されているようなサンプル文字列を []byte 配列にしたものを作ることができればOKです。

画像の添付方法について

上記 実装について で説明されている Content-Type を MIME header に追加します。ここでのポイントは Content-Transfer-Encoding ヘッダーには "base64" というデータ形式の名前が入ることです。

代表的なメール送信のリポジトリhttps://github.com/go-gomail/gomail/blob/81ebce5c23dfd25c6c67194b37d3dd3f338c98b1/writeto.go#L145 あたりを参考にしてましたがこれはデータの形式を指しているのではなく実際データを base64 に変換したものを追加するのかと思ってました。

実際にデータ本体を追加する場合は Content-Type, Content-Disposition, Content-Transfer-Encoding 追加の後一行空けて base64エンコードした画像を追加します。

Sample

--${BoundyString}
Content-Type: $ContentType; name="$FileName"
Content-Description: "$FileName"
Content-Disposition: attachment; filename="$FileName"
Content-Transfer-Encoding: base64
// 一行空ける
${Base64EncodedString}

これを Go のコードにすると以下になります。

header := textproto.MIMEHeader{}
header.Set("Content-Type", "image/png; name=sample.png")
header.Set("", "attachment; filename=sample.png")
header.Set("Content-Transfer-Encoding", "base64")

ちなみに最後の Content-Transfer-Encoding で全然うまくいかずに悩んでいるときに以下の StackOverflow のサンプルコードが役に立ちました。

stackoverflow.com

まとめ

そもそも Rawメールの構造を知らずに作っていたので添付ファイルをアタッチするときにどこにどうアタッチすれば、メールのクライアント(Gmalとかね)で読み込んでくれるのか頭を悩ませていましたが、一度作ってしまうとなるほどなーという感じでした。
たまたま仕事で使うことがあったのでドキュメント読み込みながらエンコードされたメールの生のフォーマットに触れてみることができてよかったです。

Refs

docs.aws.amazon.com

stackoverflow.com

github.com

Serverless Framework@v3 で monorepo 構成にする

Overview

Serverless Framework@v3 で monorepo 構成にするワークアラウンドについて記載します。
なお、あくまでワークアラウンドなのでいつか使えなくなるかもしれません。

Motivation

Serverless Framework を運用する上でのモノレポ構成、最新の Serverless Framework@v3 では monorepo 構成をとること(= 一つのリポジトリに対して複数のディレクトリを切ってそれぞれ serverless.yml を持つ、という構成)が非推奨になってしまいました。

ema-hiro.hatenablog.com

上記のエントリにも記載してますが、あるリポジトリに複数のサービスとして serverless.yml を持っている状態だと serverless/github-action を使って CI/CD を構築すると、Deprecation code: NESTED_CUSTOM_CONFIGURATION_PATH のエラーが発生し、デプロイができなくなります。

とはいえ、サービスとして機能させるために FaaS をスタックとして採用するケースを除き、基本的には bot の管理やちょっとしたイベントをフックして Lambda で処理を回したいときなどある FaaS ごとにリポジトリを作成したら、リポジトリ爆発を起こして、収集がつかなくなり、管理が煩雑になります。

そのため、FaaS 運用を考えるとちょっとした処理をする FaaS に関しては一つの箱の中にしまっておきたくなります。
現職ではそうした背景から、ちょっとした FaaS の運用については1つのリポジトリにまとまっていました。しかし、これが Serverless Framework の最新版になったことでこの構成をとることが非推奨構成として、基本的にはこの状態ではデプロイができなくなってしまいました。

これを受けて、FaaS を分けるのか、それともクソデカ serverless.yml を爆誕させるかで悩んでましたが、どちらも取りたくないなーと考えてたところ、調べてみると似たようなことに悩んでる人がいたらしく issue が上がっていたので改めて v3 でも monorepo 構成を取れるか調査してみようと思って調査してみたところワークアラウンドを見つけた話になります。

以下は同じようなことに悩んでる人が立てた issue。

github.com

ワークアラウンドについて

v3 でも monorepo 構成を取るためには以下の設定を CI (今回は actions をベースにしてます)に追加していきます。

actions も v3 にあげる

- uses: serverless/github-action@v2
+ uses: serverless/github-action@v3

entrypoint option を設定する

serverless/actions を使う場合は以下の issue のコメントを参考にして -c のインラインコマンドオプションと entrypoint オプションを使用します。

with:
- args: deploy --config ./${{ env.WORKING_DIR }}/serverless.yml --verbose --stage=stage
+ args: -c "cd ./$yourServiceDir && sls deploy"
+ entrypoint: /bin/sh

ref: https://github.com/serverless/github-action/issues/53#issuecomment-1059839383

SSM を使ってる場合 ~true suffix を削除する

以下のブログエントリにあるように SSM を使って Cred を読み込む場合の ~true という suffix が必要なくなります。

- ${ssm:${self:custom.environment.${self:provider.stage}.KeyName}~true}
+ ${ssm:${self:custom.environment.${self:provider.stage}.KeyName}}

With the latest version, not that this is a requirement anymore, but you have to remove it, or otherwise it will be treated as a parameter name which can lead to an error.

www.serverlessguru.com

environment で JSON を読み込む場合には raw 指定する

これは最後の最後でつまづきましたが、GCP の credential 等 json 形式で保存されてる場合に environment で指定すると string を期待してるところに JSON object が指定されてる、というエラーが出てデプロイができませんでした。

UPDATE_FAILED: $FuncName (AWS::Lambda::Function)
Properties validation failed for resource $FuncName with message:
#/Environment/Variables/$GCPCredName: expected type: String, found: JSONObject

これを解決するには raw option を指定すると JSON object の状態でも environment への指定が可能になります。

Note: you can turn off parsing by passing raw instruction into variable as: ${ssm(raw):/path/to/secureparam}, if you need to also pass custom region, put it first as: ${ssm(eu-west-1, raw):/path/to/secureparam})

ref: https://www.serverless.com/framework/docs/providers/aws/guide/variables/#resolution-of-non-plain-string-types

なので以下のように指定します。

- ${ssm:${self:custom.environment.${self:provider.stage}.KeyName}}
+ ${ssm(raw):${self:custom.environment.${self:provider.stage}.KeyName}}

まとめ

Lambda かつ SSM を使った Credentials のロード、そして actions を使ったデプロイ、など条件がありましたが、この条件において複数サービスの monorepo 構成であっても Serverless Framework@v3 でデプロイまで持っていくことができました。普通に困っていたので対処療法とはいえ、対応できてよかったです。

Vuetify プロジェクトの依存の整理でハマった話

Overview

タイトルの通りです。
業務で Vuetify を使ったプロジェクトの依存整理をしていたのでそこではまったところをメモりました。

ちなみに当初のモチベーションはようやく?そろそろ? Vuetify が Vue3 に対応しそうなのでリリース時にすぐに移行できる準備として現行のプロジェクトの依存を整理したかった、というものです。
ちなみに今現在 beta すらタスク半分くらい残ってるけど大丈夫なんだろうか...。

github.com

やったこと

TypeScript 3.9.3 -> 4.6.2 (202203時点の最新)にする。

まず最初に npm audit fix したり snyk(dependabot みたいなやつ)からサジェスチョンされていた coposition-api 系のライブラリがを上げようとしたところ、構文エラーが多発してしまって詰まったのですが、こちらはプロジェクトで使用していた TypeScript のバージョンが 3系だったからでした。

そのため、まずはTypeScript のバージョンを 3系から4系にアップデートして composition-api 周りの依存をアップデートしました。

下に記述してますが、TypeScript のアップデート時に既存の IE 向けの処理が一部 API の互換がなくなってエラー発生するようになりましたが、IE 向けの機能だったのでサポート切って実装を削除しました。

TypeScript のバージョンを上げただけで composition-api が最新になったのでさっさと上げておけばよかったなと後悔しました。

TypeScript を最新にしたら IE 対応用のメソッドが動かなくなった。

stackoverflow.com

TypeScript 4.3 -> 4.4 の間で変更があったこと知りませんでした。特にサポートする必要もなかったので IE 用の実装は削除しました。

Axios を使った API リクエストのレスポンスが unkown 型になってしまった

axios を使った API クライアントのレスポンスの例外処理において unkown 型を一部返すようになった(おそらく TS を最新にした際に諸々の依存が引きづられてアップデートされた関係)ので、API クライアントの異常系の処理において unknown の場合のハンドリングが必要になりました。

これはクラメソさんがAxiosを使った際の unknown 型のハンドリング方法をまんま公開してくれてめちゃくちゃ助かりました。

dev.classmethod.jp

try {
    // try request
} catch(e) {
    if (Axios.isAxiosError(e) && e.response) {
         // e.response.status の中を見て StatusCode ごとのエラーハンドリングをする
    }
}

vue-type-check が TS4系に対応していなかったので vue-tsc に移行

github.com

vue-tsc 採用前には、この辺でまとめられていた vue-type-check を SFC ファイルの解析のために利用してました。ただ後述しますが現時点では TypeScript の最新版に対応してません。この Qiita のエントリ自体も流石に古いのでもう参照する方も少ないのかもしれませんが。。。

qiita.com

しかし TypeScript を最新に引き上げてからどうにも vtc コマンドの実行で転けるようになり、調べると TypeScript の最新おろか 4 系にも対応してないことがわかったので一思いに外しました。 issue は上がってますけどどうやら 202203 時点では反応なく、メンテされ続けるかも怪しかったのもあります。
issue に動きがあったらまた採用を検討しようかなと思います。

github.com

vue-tsc の導入にはビザスクさんが出していたこちらのエントリを参考にさせてもらいました。

tech.visasq.com

vue-tsc で気になったところ

上記のビザスクさんのエントリでも記載されてる内容ですが、vue-tscSFC も解析対象に入れてくれるのはめちゃくちゃありがたいんですが、無名関数だと any 警告出るようになっちゃうところですね。

例えば以下のような実装だと implicit any の警告が出てしまいます。

<v-text-field
                type="string"
                label="key"
                :key="key"
                @change="v => (state = v)">
</v-text-field>

対応策としては methods に関数を追加し、@change="foo" みたいな実装にする他ないですが、v-for で loop した時の要素を追加する場合には実装状関数として切り出すのも難しい( @change="foo(v, state)" みたいな実装はできない)ので既存プロジェクトで全てを変更するのはコストだなと思い、一時的にプロジェクト全体で implicit any の option は off にしました。 Vue3 になれば SFC も TS 互換になるのでこの辺の型明示ができるはず?なのでそれを待つことにします。一旦 any については SFC は見ず、実装面ではレビューで担保しよう、と言う方針にしました。
ゆーて 2ヶ月後には Vuetify の Vue3 対応が出るはずですし...(しかし延びに延びてるのでちゃんと出るのか不安ですが...)

jest 周りの依存を整理する 

production のビルドは大体通るようになったのですが、jest でのテスト周りが転けるようになったので以下で一気に jest 周りのライブラリも全部アップデートしました。

npm i --save-dev @vue/vue2-jest@latest @types/jest@latest ts-jest@latest

まとめ

フロントエンドのプロジェクトの依存の整理は毎回迷路を解いてる気分になります。
一方でどうせしんどいのはわかっていたので、基本的には公式の提供するライブラリ以外は使わない(Vuetify が用意してくれてる便利 API 系)と言う選択をしていたのもあり、比較的短期間で上げられた(実質1dayくらい)と思います。

この手の依存の整理は何度目かなんですが、node バージョン依存だったり、TypeScript のバージョン依存だったりと、本筋と関係ないところでハマるのでライブラリのメンテナンス度等を加味しつつ、便利でもいつメンテされなくなるかわからないものを利用するよりは、大きなプロジェクトの謹製ライブラリに寄っておくのが安牌だなと再認識しました。

VSCode で Chrome のようなタブ移動をする

Overview

最近自分で登録していたキーボードショートカットが何かと競合して動かなくなってしまった際に一回リセットしてみたらタイトルにある Chrome のように Cmd(or Ctrl) + shift + [ (or ])VSCode のタブ移動をするショートカットが消失し途方に暮れていたところ、もう一回見つけたので備忘録のために記載しました。

設定方法

VSCode の Action の中に以下の二つがあるのでこれらに上記のキーボードショートカットを割り当てます。

  • View: Open Next Editor
  • View: Open Previous Editor

f:id:ema_hiro:20220305011229p:plain f:id:ema_hiro:20220305011231p:plain

JSON での設定ファイルは以下です。

[
    {
        "key": "shift+cmd+]",
        "command": "workbench.action.nextEditor"
    },
    {
        "key": "shift+cmd+[",
        "command": "workbench.action.previousEditor"
    }
]

まとめ

生産性が戻りました。これくらいデフォルトのショートカットに指定してほしいなと思います。

追記

JIS 配列がたまに US 配列と見做されて Cmd + Shift + [ を押したつもりが Cmd + Shift + ] と認識されるケースがあって完全に逆の向きで移動されて困っていたところ救われるエントリがありました。

Oh-My-Zsh -> Prezto に移行した

Overview

特に強い理由はないですが、長年愛用してきた Oh-My-Zsh から Prezto に移行しました。

github.com

強い理由はないと記載したけど、M1Pro でさえ若干ターミナルのもっさり感を感じていたのと、さすがにメンテナンスがされてなさすぎるプラグインもあって FW として微妙だなと思ったので一思いに乗り換えた、という感じです。
最初は何も入れない素の状態のターミナルを使おうかなと思いましたが、使ってて面白くないのでカスタマイズするために少なくともここ一年くらいはちゃんと動いてそうな Prezto を採用しました。

移行方法

Oh-My-Zsh の削除

手元の zshrc の中から oh-my-zsh に関する箇所だけを削除します。

export ZSH=/Users/$(whoami)/.oh-my-zsh
ZSH_THEME="$Theme"
plugins=(git)
source $ZSH/oh-my-zsh.sh

削除後に $HOME にある .oh-my-zsh を削除します。

以上です。

Prezto の導入

事前に手元の zshrc を backup しておいて削除します。

cp $HOME/.zshrc $HOME/.zshrc.back
rm .zshrc

その後 https://github.com/sorin-ionescu/prezto#installation に記載してある通りにコマンドを叩いてシェルを再起動すればすぐに使えるようになりました。

Editor の設定変更

Prezto で自動で参照される zsh の設定では Editor が nano になってしまっているので vim に変更します。

$ vim .zshprofile

export EDITOR='vim' // vim に変更
export VISUAL='vim' // vim に変更
export PAGER='less' // vim に変更

Git の情報の表示

Theme によっては Git のブランチを表示してくれる Theme がありますが、Prezto をインストールした時点では Theme にないので https://github.com/sorin-ionescu/prezto#themes の3つ目に記載されている .zpreztorc 内の preload module list に git を追加する、という作業を行う必要があります。

zstyle ':prezto:load' pmodule \
  'environment' \
  'terminal' \
  'editor' \
  'history' \
  'directory' \
  'spectrum' \
  'utility' \
  'completion' \
  'history-substring-search' \
  'prompt' \
  'git' # <- New

移行してみて

もっさり感は消えたけど以下のエントリで書かれてる内容の通りにプロファイリングしてみたら劇的に変わってる、というほどでもなかったです。

qiita.com

ただ確実に早くなったなと思ったのは git checkout コマンドで既存のブランチの一覧を保管するときに、今まではまじでびっくりするくらい遅かったんですが、これが快速になりました。

ただよくわからないのが、Prezto で history コマンドを叩くと直近10件程度しか表示されず、history の範囲を指定するような環境変数もなかったので一旦 alias hist=cat .zsh_history | peco で alias を張っています

元々の zsh の仕様って初めて知りました。今までは oh-my-zsh がうまいことやってくれていたんですね。

qiita.com

今は以下の設定を zshrc に入れて自前の history として動かしてます。

unalias history
alias history='fc -l -i 1 | fzf'

おしまい。

go-redis で custom struct を set / get するときは BinaryMarshaler を実装する必要がある

Overview

以下に記載されてる内容のことです。

github.com

go-redis を使って redis に値を set & get するときは encoding#BinaryMarshaler を実装する必要があるという話です。

Sample

type X struct {}

func Set (key string, x X) error {
    opt := redis.Options{
        // option setting
    }
    client := redis.NewClient(&opt)
    return client.Set(key, x, -1)
}

これだと Setを Call するときに redis: can't marshal X(implement encoding.BinaryMarshaler... というエラーが発生します。
カスタム struct を定義するときは encoding.BinaryMarshaler を実装することを求められるので以下のようにします。

type X struct {}

func (x X) MarshalBinary() ([]byte, error) {
    return json.Marshal(r)
}

func (x X) UnmarshalBinary(data []byte) error {
    return json.Unmarshal(data, &r)
}

func Set (key string, x X) error {
    opt := redis.Options{
        // option setting
    }
    client := redis.NewClient(&opt)
    return client.Set(key, x, -1)
}

300 記事継続の振り返りとこれから

この記事がちょうど 300 記事目です。

200記事の時の振り返りはこちら。

ema-hiro.hatenablog.com

200もの駄文をインターネット空間に投下してから2年ちょっとくらい経ちましたが、さらに 100 もの駄文を投下してしまっていたようです。

自分のドキュメント力と言語化能力の欠如を改善するために始めたこのブログも気づけば 5年弱くらい続いてて、社会人生活の半分以上はブログを書き続けてることになってて自分でも驚いています。

最近は技術的な話題に限らない話などもネタとして取り入れつつも、ブログを書く時間が減っているのもあって投稿頻度がなかなか上がってきませんでした。狂った様にエントリを量産していた頃が懐かしいです。

  • 無理をしない。
  • 続けることを目標にする。

ということをコンセプトにしてきましたが、この「意識の低さ」は次の100記事でも守りつつ、更新頻度はもう少し上げていけるように改善していきたいです。

WFH における非同期と同期の話

Overview

learn.gitlab.com

デブサミで公開されたこのスライドが非常に示唆に富んでいて考えることが多かったので、その思考の備忘録です。
※ 備忘録なので、取り止めもなく思考を吐露している文章になります。

コロナ禍以前から WFH を採用していた企業だけあって、その知見の量とここで示されてるだけの項目においても、その深さ、精度は WFH を採用する企業にとってはある意味理想と言える内容のスライドだなという感想を持ちました。

このスライドを読んだ後、Twitter ではこんな感じで感想を書き留めていました。

特に WFH か RTO か、という軸の他に非同期か同期かという軸があることに気づき、特に後者の非同期・同期の使い分けが WFH においてはプロダクト開発 -> 組織運営 -> 事業そのものに影響を及ぼしそうだなと思いました。

f:id:ema_hiro:20220224053016p:plain

前提

WFH or RTO という話

WFH or RTO (Return To Office) はポストコロナのニューノーマルな世界において、世界に名だたる GAFA ですら頭を悩ませている問題でもあり、答えは決まってないと思います。実際日本の大手企業でも働く場所の制限を無くす動きを見てるともう WFH が当たり前になってきているし、そもそも企業が当然備えてる制度の一つである、という常識が完全に定着した、という印象すらあります。特に IT 系と呼ばれる業界においてはそう感じます。

個人的にはこれは結局のところ "スタンス" の差でしかないと思っていて、それぞれにメリットデメリットがありますし、WFH を恒久的に選択肢として採用することは企業文化やそもそも仕組み上人事制度の根本から変える必要がある(それに伴い組織の構成員たる社員、特にマネジメント層のメンタルモデルを変える必要がある)ケースもあり、現場でのハレーションを考えると一朝一夕に進められるところとそうでないところははっきり分かれる事案だと思います。

なので、所属してる組織では WFH と RTO に対しては、こう考えるのでこうする、というスタンスを明示して、そのスタンスに則って組織を作っていく他ないんでしょうし、やはりそこのスタンスが合わない以上、組織から離れる選択をするというのは今後多く見られる話になると思います。

僕個人としては、WFH は一社員としてはその便益を享受しておりますが、RTO の良さも感じてて週一くらいは会社に行くようにしてます。
やはりコミュニケーションにおける UX およびレイテンシの低さ、という点では TCP/IP はリアルを超えることは未だにできていないのではないか?と思います。

非同期か同期か

これが個人的にはポイントかなと考えています。

ツイートした内容とも被りますが、WFH か RTO の話をされるときに必ず、コミュニケーションを同期的にすることによるメリットで RTO を選択してるケースを目にすることが多く、実際それは大いにあり得るんだろうなと思いつつ、スライドでも語られている内容ですが、「それ同期である必要性ある?」という観点はすっぽり抜けてる(のか、あえて議論に入れてないのか)ように感じます。

僕自身は同期的であることが当たり前すぎて、脳死で同期を選択してるケースがあると思っており、そもそも仕事とは一部の業務(エッセンシャルワーカーや小売事業など限られたユースケース) を除いて、 仕事とは本来非同期で進めていたもの だと思っており、非同期を前提としたシステムとメンタルモデルを構築することが大事なのでは?と考えています。

よく耳にする同期と非同期のハレーションは、そもそも同期を前提としてる仕組みの上で非同期的な要素を要所要所で組み込む(許容していく)という選択をしてることで生まれており、そもそも非同期を前提として、要所で必要最低限の同期要素を組み込む方が、実は本来あるべき仕組みの作り方だった、という仮説は成り立つ可能性が高いと感じます。
コロナ以前の世界でも1日の業務内容を分割してみると、作業自体は非同期なことが多かったはずです。特にこの職業ではコード書く時は、ペア作業のケースを除いて大体1人で作業進めてたと思いますのでイメージしやすいかと思います。

非同期で回すために仕組みが頓挫する(もしくは非同期要素があるだけで運用されず、形骸化する) というのは、非同期で進めることを前提にしていないからで、そうなると便利すぎる同期の魔力には勝てずに終わる、みたいな話なのかなと思いました。
リモートで組織回すなら実は全員リモートであるべき、という話もあって、これも似た話かなと思います。非同期に慣れない人が良かれと思って同期に移行してしまった結果、コミュニケーションと持ってる情報量の溝が生まれてチーム内で断絶が発生する、みたいなケースの話です。

非同期がなぜ難しいのか?

同期には強烈な魔力があるからこそ、「それ同期である必要ありますか?」という問いがなかなか出てこないのかな、という考えの他に、非同期は根本的な難しさがあると思います。

社員のスキルが高くないと〜みたい話はよくいわれると思いますし、それは前提として大きな要素かと思いますが、最終的に僕の考えは以下です。

別に出社してた時代でさえ、「わからないことがあればなんでもチャットで聞いてね」といわれてもなかなか聞けない人がいたとは思いますが、そういう時はうまいことオフラインで状況を目で察知して助け舟を出してくれる人がいたような気がします。僕もそうやって助けてもらったことがありますし、何よりチャットするより、申し訳なさを装いながら直で話しかけた方が話しかけやすい、みたいなのもあって物理で近いこと、そして同期的であることが解決してくれていた要素も実は大きかったなと今になって思います。

ただメンバー同士の距離が物理的にも離れており、こういった助け舟を出してくれる人との関係も断絶してしまい、本当に1人で作業を進めないという状況において、「わからないことを言語化できること」、そして「助けを請う」ことができること、という当たり前にできていてほしいレベルにおいて、できる人とそうでない人が分かれてしまい、後者が組織全体のスループットを低下させてしまう(律速してしまう)ことが発生するようになってしまったのと考えています。

WFH であること、そして非同期であることの難しさは、個人で適切に"同期"を選択するというスキルセットを顕在化させてしまったことにあるのではないかと思います。

結果この "個人で同期を適切に選択できない人" が多い場合に WFH を選択すると "WFH によって仕事が進まなくなった" みたいな全く異なった因果が生まれるようになるのかなと思いました。
本来であれば、そういう面も含めてオンボーディングで解決したり、採用でスクリーニングするべき話でもあるかもしれないんですが、コロナ前後で前提として求められるスキルが変わってしまったのもあって、組織全体としては低い方に合わせてサポートしていくことも必要で、チリツモでスループットに影響が出てしまう、という話です。

余談: カメラ ON について

余談に入れましたが、これを書きたかったがためにこのエントリを書いたといっても過言ではない裏テーマです。

GitLab のスライドは総じてWFH && 非同期をベースにするという点で理想的な仕組みを回しており、参考になる話しかなかったのですが、一点「カメラは原則ON」という部分だけは僕自身の考えとは違う選択をしていたので、どうしてそう感じたのかを記載しておきます。

個人の感想とスタンス

まずこの部分に関して僕個人の感想としては、ここまでうまく組織を作っているのに、カメラだけは ON にさせるんだ、という若干ネガティブよりの感想を持ちました。

とはいえ、これも原則は "スタンス" の問題かなと思っていて、GitLab では社内の原則としてカメラを ON にする、というルールの上で組織を運営してるというだけで、それ自体に是も非もないと思います。あくまでスタンスなので、僕自身はカメラ ON にしたとしても、実際のリアルフェイスを移す必要はない、というスタンスをとっています。

知られたくない権利 (プライバシーの話)の観点

WFH が当たり前の選択肢としてなっている中で表情から読み取れる情報量というのは大きく、カメラをONにするのが望ましいと意見もまっとうなものがあると思います。一方でカメラを ON にすることによる弊害というのも存在して、それはプライバシーに起因することが多いと思います。

例えば、書斎の有無があります。WFH が当たり前となってまだまだ日は浅く、仕事場としての空間を持っている人もいればそうでない人もいます。
都内で1人暮らしをしていればそういった環境を用意するような物件に住むことは一定以上のコストがかかってくることでもあり、それができない人もいることを想像すると、そうした人は生活空間の中で作業をすることで一部露呈することになるかもしれません。(なんのためのバーチャル背景か、という話はあると思いますが顔を見せるまでの身支度のコストとかもあるので、プライバシーはこれに限った話ではありません。寝癖あったりしてカメラをオンしたくない時もあるでしょう)

作業空間や身支度の話を出しましたが、PC の性能上カメラをオンにしたら途端に作業できないくらい重たくなるなど、端末が変数になりうる場合もあり、そもそもカメラオンにするのが最適でないケースもあります。

WFH にしたことで生活空間と職場が混在するようになった結果出てきた弊害ともいえますが、この問題を無視してカメラをオンにすることが是とは言い切れないと自分は考えているので、やはり知られたくないことをわざわざ知らせるルールには違和感を感じました。

アバターという選択肢

ただ、とはいえ上記にも記載しましたが、WFH においてカメラから得る情報は貴重で、話す側としてもカメラオフの相手に話しているというのはコミュニケーションとしては違和感を感じます。その違和感を否定する気はありませんが、この違和感を軽減し、かつプライバシーを守る手段として僕は「アバター」という選択肢は真面目に考えて良い選択肢だと思います。

去年現職でアドベントカレンダーを書いた際に冒頭に記載したのですが、現在のアバターは "そこに人がいる感" を感じることができるレベルでは顔の細やかな動きをトレースしてくれるようになっているので、無機質なカメラオフの画面に向かって話すよりよほど "誰か" に対して話している感覚を享受することができます。

medium.com

実際僕の関わっていたプロジェクトのメンバーから、カメラオフの状態がメインのチームのMTGから、僕のいたプロジェクトの MTG に戻ってきたらアバターがわちゃわちゃしてて全然会話しやすい、というフィードバックをもらったので、サンプルは少ないですが実際にカメラが ON であるという条件は満たしつつ、そこに人じゃないけど "人がいる" 感覚をある意味錯覚させて、コミュニケーションを円滑化するためのアバターは、WFH のご時世でこそ非常に有用だと考えます。

僕自身としてはこのアバターの体験の良さをもっと広く知って欲しいと思っているので、カメラを ON にして実際の顔を映さずとも、コミュニケーションは成り立つという仮説を検証していきたいですし、そのためにまずはアバターを簡単に被ってもらうことに力を注いでいきたいなと思っています。

ブラウザのアプリを使ってアバターを簡単に切れる方法をブログとして残しているので興味がある方は是非明日から被ってみてください。

ema-hiro.hatenablog.com

まとめ

なんとなく GitLab のスライドを読んでつらつら考えたことをまとめてみました。こういうことを考えさせてくれるという意味でいいスライドだったと思います。 ちなみに最後の方にイキッたこと書きましたけど、そもそもアバター動かすのには高スペックなマシンが必要なので M1Max を全人類に配布してほしいなと思ってます(誰に笑)

aws.EndpointResolverWithOptions はエラー時にデフォルトの Endpoint Endpoint Resolver を返す

タイトルの通りなんですが、AWS SDK Go V2 を利用して AWS の設定を初期化する実装をする際に Endpoint Resolver Option を設定したいケースがあると思います。

リージョンが異なるなどで AWS に各サービスごとに設定内容を変えたいケースなどが使いたいケースかと思いますが、特に設定しないケースにおいてはエラーを返す ( aws.EndpointNotFoundError を返す)とデフォルトの Endpoint Resolver が返されるとドキュメントに書いてありました。

EndpointResolverWithOptions is an endpoint resolver that can be used to provide or override an endpoint for the given service, region, and the service client's EndpointOptions. API clients will attempt to use the EndpointResolverWithOptions first to resolve an endpoint if available. If the EndpointResolverWithOptions returns an EndpointNotFoundError error, API clients will fallback to attempting to resolve the endpoint using its internal default endpoint resolver.

pkg.go.dev

実装としては以下のようになります。

import (
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
)

// 略

awscfg, err = config.LoadDefaultConfig(context.TODO(), config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
    if service == $awsServcieID { // cf. ses.ServiceID
        // service ごとに aws.Endpoint の中身を変更する。
    } 
    return aws.Endpoint{}, &aws.EndpointNotFoundError{}
}))

エラーを返すとデフォルトの設定が割り当てられる、というのはちょっとトリッキーな設定ですが知ってると挙動を AWS SDK に任せられるので便利だなと思いました。

Go で画像を操作する

Overview

業務において Go で画像を操作する方法を調べたので備忘録。Go の標準パッケージにある image package は必要最低限の機能を十分に備えてて便利だなと思いました。

画像の拡張子を操作する

拡張子を変換する。

同僚のブログが参考になりました。

rennnosukesann.hatenablog.com

拡張子を判別する

import (
   // blank import しないと format の判別ができないので注意
    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"
)

url := "$SomeImageURL.jpg"
response, err := http.Get(url)
if err != nil {
    panic(err)
}

defer response.Body.Close()
_, format, err := image.Decode(response.Body)
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(format) // jpg

判別も簡単にできます。コメントに書いてますが、判別したいフォーマットを blank import していないと unknown になってしまいます。

http.DetectContentType で拡張子を判定する

http package には DetectContentType というバイト配列から画像フォーマットを指定する機能があります。

DetectContentType implements the algorithm described at https://mimesniff.spec.whatwg.org/ to determine the Content-Type of the given data. It considers at most the first 512 bytes of data. DetectContentType always returns a valid MIME type: if it cannot determine a more specific one, it returns "application/octet-stream".

base64 にされた画像の拡張子判定の実装は以下のような感じです。

b, err := base64.StdEncoding.DecodeString("$base64encodedString")
if err != nil {
    panic(err)
}
format = http.DetectContentType(b) // image/png etc...

image package での encode/decode を要しないので http.DetectContentType はメモリに優しいです。
なお、画像の拡張子については実際の判別してるコードを見ると

&exactSig{[]byte("\x89PNG\x0D\x0A\x1A\x0A"), "image/png"},
&exactSig{[]byte("\xFF\xD8\xFF"), "image/jpeg"},

ref: https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/net/http/sniff.go;l=125-126;drc=refs%2Ftags%2Fgo1.17.7

という形で先頭の数バイトで一般的な画像の拡張子を判定しています。そのため実際に実装する場合には base64 エンコードされた文字列の先頭数バイトを見ればいいわけです。つまり上記のコードは以下のように変更することができます。

data := "$base64encodedString"
encLen := base64.StdEncoding.EncodedLen(8) // 先頭8バイトを読み込んで判別する。
b, err := base64.StdEncoding.DecodeString(data[:encLen])
if err != nil {
     panic(err)
}
format = http.DetectContentType(b) // image/png etc...

画像を作成する

サクッと画像を生成することもできます。
単体テストで画像を使うケースなど便利かなと思います。

buf := bytes.NewBuffer([]byte{})
img := image.NewRGBA(image.Rect(0, 0, 50, 50))
_ = png.Encode(buf, img)

// 書き出した画像を base64 文字列に変換
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
fmt.Println(enc)

Encode/Decode する

base64

Go Tour が詳しいです。

oohira.github.io