経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

インフラに関してやってきたこと/やりたいこと(2021年7月)

読んだ本

  • キタミ式基本情報技術者
  • WEB+DB 大規模サービス技術入門
  • WEB+DB 24時間365日 サーバ/インフラを支える技術
  • WEB+DB nginx実践入門
  • Real World HTTP 歴史とコードに学ぶインターネットをウェブ技術
  • Linuxで動かしながら学ぶ TCP/IP ネットワーク入門
  • DNSがよくわかる教科書
  • GCPの教科書
  • Amazon Web Service 基礎からのネットワーク&サーバー構築
  • Kubernetes on AWS
  • Docker/Kubernetes実践コンテナ開発入門
  • SREサイトリライアビリティエンジニアリング
  • サイトリライアビリティワークブック
  • Infrastructure as Code
  • 入門監視
  • クラウドインフラとAPIの仕組み
  • Mackerelサーバ監視実践入門
  • 実践Terraform
  • ISUCONのススメ

経験

NTT Performance Tuning Contest

alpとかpt-query-digestとかの勉強をして対策をした。
wafuwafu13.hatenadiary.com



株式会社はてな

NginxやVarnish周りのコードを読んだり書いたりしているが、ssh権限はどのサーバーにもない。
この辺りの話は大体理解している。
developer.hatenastaff.com
障害対応を行なったことはないが、大規模サービスにおいてどのような障害が起こりうるのか、どのような対策をするのかは大体理解している。
Mackerelをどのように使用しているのかをなんとなく理解している。
Mackerelのソースコードを少し理解して簡単なPRを出したことがある。
wafuwafu13.hatenadiary.com
インターンでコンテナ等に関する講義を受けてGo/gRPC/k8sでブログを作った。
wafuwafu13.hatenadiary.com
hatenacorp.jp



AWA株式会社

wafuwafu13.hatenadiary.com
Terraformのバージョンアップをした。
Go/MongoDB/Mongo Connector/Elasticsearch/gRPC/ECS/Terraform/Ansible/Jenkins を触って検索処理の追加をした。
技術の概要を理解して使うことはできるが、長期間の運用をしたことはない。

やりたいこと

  • とにかくssh接続できたらなんでもやりたい
  • AWS/GCP/k8s/Docker/Terraform あたりの技術を本番環境で長期的に触れてみたい
  • アプリケーションから見えるインフラ、見えないインフラの両方に携わりたい

graphql-validation-complexity から学ぶGraphQLのAST走査

オライリー『初めてのGraphQL』の「7.3.4 クエリの複雑さ制限」で、graphql-validation-complexity というライブラリが紹介されていて、面白そうだったのでのぞいてみた。

github.com

graphql-validation-complexityは、クエリに対して複雑度を計算し、その複雑度に制限を設けることができる。
デフォルトでそれぞれのフィールドに値を設定し、リストが入れ子になるたびに値を10倍して、複雑度を計算している。
テストコードで例を示すと、以下のように複雑度が計算される。

graphql-validation-complexity/ComplexityVisitor.test.js at master · 4Catalyzer/graphql-validation-complexity · GitHub

query {
  item {
    name # 複雑度1
    item {
      name # 複雑度10
    }
  }
  list {
    item {
      name # 複雑度10
    }
    list {
      name # 複雑度100
    }
  }
  nonNullItem {
    name # 複雑度1
  }
  nonNullList {
    name # 複雑度1
  }
} # 総複雑度123

この複雑度の計算の解釈が正しいことは、以下のテストが通ったことからも保証できる。

describe('simple queries', () => {
    it('should calculate the correct cost', () => {
      const cost = checkCost(`
        query {
          list {
            item {
              name
            }
            list {
              name
            }
          }
        }
      `);

      expect(cost).toBe(110);
    });
});

実際にどう使うかというと、例えばApolloを使う場合は、以下のようにvalidationRulesフィールドに設定する。

const apolloServer = new ApolloServer({
  schema,
  validationRules: [createComplexityLimitRule(1000)],
});

では、このcreateComplexityLimitRuleの中身を中身をみていく。

createComplexityLimitRule関数は、src/index.js##L14-L17でexportされており、引数に設定値をmaxCostとしてとっている。
graphql-validation-complexity/index.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

export function createComplexityLimitRule(
  maxCost,
  { onCost, createError, formatErrorMessage, ...options } = {},
) {

costが設定値のmaxCostを超えていたら、エラーが吐かれるようになっている。

graphql-validation-complexity/index.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

if (cost > maxCost) {
        context.reportError(
         createError
            ? createError(cost, node)
            : new GraphQLError(formatErrorMessage(cost), [node]),
        );
}

では、costはどのように求められているのかを調べると、以下のコードが目についた。

Document: {
   enter(node) {
      visit(node, visitWithTypeInfo(typeInfo, visitor));
    },
    leave(node) {
      const cost = visitor.getCost();

ここのvisit関数はfrom 'graphql';でインポートされているため、GraphQLのドキュメントを見てみると、以下のような説明があった。

graphql.org

visit() will walk through an AST using a depth first traversal, calling the visitor's enter function at each node in the traversal, and calling the leave function after visiting that node and all of its child nodes.

By returning different values from the enter and leave functions, the behavior of the visitor can be altered, including skipping over a sub-tree of the AST (by returning false), editing the AST by returning a value or null to remove the value, or to stop the whole traversal by returning BREAK.

When using visit() to edit an AST, the original AST will not be modified, and a new version of the AST with the changes applied will be returned from the visit function.

visit()は、深さ優先のトラバーサルを用いてASTを走査し、トラバーサル内の各ノードでビジターのenter関数を呼び出し、そのノードとその子ノードすべてを訪れた後にleave関数を呼び出します。

enter関数とleave関数から異なる値を返すことで、ビジターの動作を変更することができます。例えば、falseを返すことでASTのサブツリーをスキップしたり、値を返すことでASTを編集したり、nullを返すことで値を削除したり、BREAKを返すことでトラバーサル全体を停止したりすることができます。

visit()を使ってASTを編集する場合、元のASTは変更されず、変更が適用された新しいバージョンのASTがvisit関数から返されます。

このenterにあるvisit()によりAST走査がはじまるらしい。
テストコードを見てみると、const ast = parse(query);で明示的にqueryがパースされていたので、enter(node)nodeはASTであることがわかる。

graphql-validation-complexity/ComplexityVisitor.test.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

describe('ComplexityVisitor', () => {
  const typeInfo = new TypeInfo(schema);

  function checkCost(query) {
    const ast = parse(query);
    const context = new ValidationContext(schema, ast, typeInfo);
    const visitor = new ComplexityVisitor(context, {});

    deepFreeze(ast); // ensure we aren't mutating accidentally

    visit(ast, visitWithTypeInfo(typeInfo, visitor));

    return visitor.getCost();
  }

  describe('simple queries', () => {
    it('should calculate the correct cost', () => {
      const cost = checkCost(`
        query {
          item {
            name
            item {
              name
            }
           ...
      `);

      expect(cost).toBe(123);
    });
  });

astvisitWithTypeInfo(typeInfo, visitor)デバッグすると以下のようであった。

console.log
    {
      kind: 'Document',
      definitions: [
        {
          kind: 'OperationDefinition',
          operation: 'query',
          name: undefined,
          variableDefinitions: [],
          directives: [],
          selectionSet: [Object],
          loc: [Object]
        }
      ],
      loc: { start: 0, end: 370 }
    }

console.log
    { enter: [Function: enter], leave: [Function: leave] }

astをさらに詳しくデバッグすると、queryのフィールドはSelectionSetで分割されていることがわかった。
console.log(ast.definitions[0].selectionSet);

console.log
    {
      kind: 'SelectionSet',
      selections: [
        {
          kind: 'Field',
          alias: undefined,
          name: [Object],
          arguments: [],
          directives: [],
          selectionSet: [Object],
          loc: [Object]
        },
        {
          kind: 'Field',
          alias: undefined,
          name: [Object],
          arguments: [],
          directives: [],
          selectionSet: [Object],
          loc: [Object]
        },
        {
          kind: 'Field',
          alias: undefined,
          name: [Object],
          arguments: [],
          directives: [],
          selectionSet: [Object],
          loc: [Object]
        },
        {
          kind: 'Field',
          alias: undefined,
          name: [Object],
          arguments: [],
          directives: [],
          selectionSet: [Object],
          loc: [Object]
        }
      ],
      loc: { start: 15, end: 363 }
    }

では、実際に複雑度を計算している箇所をみていく。

const cost = visitor.getCost();getCost関数はsrc/ComplexityVisitor.js#L221-L223で定義されていて、

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

  getCost() {
    return this.cost;
  }

119行目から127行目あたりで実際にcostの計算がされていた。

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

 enterField() {
    this.costFactor *= this.getFieldCostFactor();

    this.cost += this.costFactor * this.getFieldCost();
  }

  leaveField() {
    this.costFactor /= this.getFieldCostFactor();
  }

astselectionSetがいじられている箇所があったことから、selectionSetを使って複雑度に重みを与えていることが想像できる。

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

    this.SelectionSet = this.flattenFragmentSpreads;
  }

  flattenFragmentSpreads(selectionSet) {
    const nextSelections = selectionSet.selections.flatMap((node) => {
  ....

複雑度が計算されて値に応じてエラーが返る流れはなんとなく掴めた。
が、graphql-validation-complexityとGraphqlのvisit()がどのように噛み合ってるのかがいまいちよくわからない。
ComplexityVisitorクラスのコンストラクタにあるthis.Fieldenter/leaveが発火しているっぽいが、そもそもthis.Fieldが何を指しているのかわかっていない。

    this.Field = {
      enter: this.enterField,
      leave: this.leaveField,
    };

さっきみたように、visitWithTypeInfoによってenter/leaveが作られているから、visitWithTypeInfoを調べればわかるかもしれないが、ドキュメントを調べても出てこないから諦める。

const visitor = new ComplexityVisitor(context, options);
const typeInfo = context._typeInfo || new TypeInfo(context.getSchema());

console.log(visitWithTypeInfo(typeInfo, visitor))
// { enter: [Function: enter], leave: [Function: leave] }

にしてもいろんなところでASTが使われている。

Go の main goroutine が exit する場所

ascii.jp
を進めているメモ。

package main
import "fmt"

func main() {
    fmt.Println("Hello World!")
}

VSCodeデバッグして最後にexitするのはここ。

go/src/runtime/proc.go#L277
go/proc.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub

exit(0)


あと、連載の時からwrite()メソッドが書き換わっていた。

ここから先は、環境によって固有のコードに飛びます。 Unix系OSmacOSLinuxなど)では、write() メソッドは次のようになっていると思います。 for ループに囲まれて、送信が終わるまで何度も syscall.Write を呼んでいることがわかります。 この syscall.Write がシステムコールです。

func (f *File) write(b []byte) (n int, err error) {
    for {
        bcap := b
        if needsMaxRW && len(bcap) > maxRW {
            bcap = bcap[:maxRW]
        }
        m, err := fixCount(syscall.Write(f.fd, bcap))
        n += m
        :
    }
}

実際には以下のようになっていた。

go/file_posix.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub

// write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
func (f *File) write(b []byte) (n int, err error) {
	n, err = f.pfd.Write(b)
	runtime.KeepAlive(f)
	return n, err
}

f.pfd.Writefd_unix.goで呼び出されているので、「環境によって固有のコードに飛びます」はこのことを言ってそう。
syscall.Writeもここで呼び出されている。

go/fd_unix.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
	if err := fd.writeLock(); err != nil {
		return 0, err
	}
	defer fd.writeUnlock()
	if err := fd.pd.prepareWrite(fd.isFile); err != nil {
		return 0, err
	}
	var nn int
	for {
		max := len(p)
		if fd.IsStream && max-nn > maxRW {
			max = nn + maxRW
		}
		n, err := ignoringEINTR(func() (int, error) { return syscall.Write(fd.Sysfd, p[nn:max]) })
		if n > 0 {
			nn += n
		}
		if nn == len(p) {
			return nn, err
		}
		if err == syscall.EAGAIN && fd.pd.pollable() {
			if err = fd.pd.waitWrite(fd.isFile); err == nil {
				continue
			}
		}
		if err != nil {
			return nn, err
		}
		if n == 0 {
			return nn, io.ErrUnexpectedEOF
		}
	}
}

大体以下のような流れでfmt.Println("Hello World!")がされていることがわかった。

func Println(a ...interface{}) (n int, err error) =>
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) =>
func (f *File) Write(b []byte) (n int, err error) =>
func (f *File) write(b []byte) (n int, err error) =>
func (fd *FD) Write(p []byte) (int, error) =>
syscall.Write(fd.Sysfd, p[nn:max]) =>
exit(0)

Goのコードはよくわからないからこんな感じで外観を掴むだけでよさそう。
go/src/runtime/proc.goのコメントを和訳してみたけどよくわからない。

ゴルーチン・スケジューラ
スケジューラの仕事は、すぐに実行可能なgoroutineをワーカースレッドに分配することです。

主な概念は次のとおりです。
G - ゴルーチン。
M - ワーカースレッド、またはマシン。
P - プロセッサ。Goコードを実行するために必要なリソースです。
Mは、Goコードを実行するために、関連するPを持っていなければなりません。
ブロックされたり、関連付けられたPを持たないシステムコールの中にいたりします。

https://golang.org/s/go11sched のデザインドキュメントをご覧ください。

ワーカースレッドのパーキング/アンパーキング。
利用可能なハードウェアの並列性を利用するために十分な数のワーカースレッドを走らせておくことと、ハードウェアの並列性を利用するために十分な数のワーカースレッドを確保することとを駐留させて、CPUリソースと電力を節約することが必要です。
これは2つの理由から簡単ではありません。
(1) スケジューラの状態は(特に、P単位の作業キュー)が意図的に分散されているため、高速な経路でグローバルな述語を計算することはできません。
(2) 最適なスレッド管理のためには、未来を知る必要がある(近い将来、新しいゴルーチンが準備されるときにワーカスレッドをパークしない)。

悪い方向に働くであろう3つの拒絶されたアプローチ
1. スケジューラの状態をすべて集中管理する(スケーラビリティが阻害される)。
2. ゴルーチンの直接ハンドオフ。つまり、新しいゴルーチンの準備ができて、予備のPがあるときにスレッドをアンパークし、スレッドとゴルーチンをハンドオフします。
これでは、ゴルーチンを準備したスレッドがスレッドの状態を崩してしまうことになります。
ゴルーチンを準備したスレッドは、次の瞬間には仕事をしていない可能性があるので、それをパークする必要があります。
また、計算の局所性も失われてしまいます。
また、同一スレッド上に依存するゴルーチンを保持したいため、計算の局所性が失われ、さらなるレイテンシーが発生します。
3. ゴルーチンの準備ができてアイドルPがあるときは、追加のスレッドをアンパークします。
アイドル状態のPがあるときに追加のスレッドをアンパークするが、ハンドオフは行わない。これは、追加スレッドのパーキング/アンパーキングが過剰になります。
スレッドを過剰にパーキング/アンパーキングすることになります。
する作業を発見することなく、即座にパークしてしまうからです。

現在のアプローチは(1) アイドル状態の P があり、「回転している」ワーカースレッドがない場合、ゴルーチンの準備時に追加のスレッドをアンパークします。
アイドルPが存在し、「回転している」ワーカースレッドが存在しない場合。ワーカースレッドが回転していると考えられるのは
紡いでいるとみなされるのは、ローカルな仕事がなく、グローバルなランキュー/ネットポラーで仕事を見つけられなかった場合です。
スピニング状態は m.spinning および sched.nmspinning で示されます。
このようにしてパークされていないスレッドもスピンしているとみなされますが、私たちはゴルーチンハンドオフを行わないので、そのようなスレッドは最初は仕事がありません。
スピンするスレッドは、パーキングする前に、Pごとの実行キューで仕事を探すためにいくつかのスピンを行います。
もしスピニングスレッドは仕事を見つけると、自分自身をスピニング状態から解放し、実行に進みます。
仕事を見つけられなかった場合は、スピニング状態から抜け出してパークします。
少なくとも1つの回転中のスレッドがある場合(sched.nmspinning>1)、ゴルーチンの準備中に新しいスレッドをアンパークすることはありません。
ゴルーチンを準備する際に新しいスレッドをアンパークしません。
これを補うために、最後に回転していたスレッドが仕事を見つけて回転を止めた場合は新しい回転中のスレッドをアンパークしなければなりません。
この方法により、スレッドのアンパークが不当に急増することがなくなります。

主な実装上の問題点は、スピンしているスレッドとスピンしていないスレッドの間の紡績->非紡績のスレッド遷移の際に非常に注意する必要があることです。
この移行は、新しいゴルーチンの投入と競合する可能性があります。
両方がアンパークに失敗すると、半永久的にCPUの使用率が低下することになります。
ゴルーチンの準備の一般的なパターンは、ゴルーチンをローカルワークキューに投入し、#StoreLoadスタイルのメモリバリアを行い、sched.nmspinningをチェックします。
spinning->non-spinningの遷移の一般的なパターンは、nmspinningをデクリメントすることです。
Nmspinningをデクリメントし、#StoreLoad形式のメモリバリアで、すべてのper-Pのワークキューに新しい仕事がないかチェックする。
これらの複雑さは、グローバルランキューには適用されないことに注意してください。
グローバルキューへの投入時にスレッドのアンパークを怠らないからです。また、コメントを参照してください。

Class::Enumemon の中身をちょっとのぞいてみる

使う機会があってRailsのActive Recordっぽくて便利だと思ったけど、中身があまり想像できなかったのでちょっとだけ調べてみた。

metacpan.org

package IdolType;
use Class::Enumemon (
    values => 1,
    getter => 1,
    indexer => {
        by_id     => 'id',
        from_type => 'type',
    },
    {
        id   => 1,
        type => 'cute',
    },
    {
        id   => 2,
        type => 'cool',
    },
    {
        id   => 3,
        type => 'passion',
    },
);
 
1;
 
package My::Pkg;
use IdolType;
 
# `values`: defines a method for getting all values
IdolType->values; #=> [ bless({ id => 1, type => 'cute' }, 'IdolType'), ... ]

これはcpanに載っていたサンプルコードで、とりあえず IdolType->valuesでなぜ [ bless({ id => 1, type => 'cute' }, 'IdolType'), ... ]が返ってくるのかを調べてみる。

コードを見ると、100行程度でそこまで長くはなかった。
p5-Class-Enumemon/Enumemon.pm at master · pokutuna/p5-Class-Enumemon · GitHub


valuesに注目すると、36行目で作られていた。
p5-Class-Enumemon/Enumemon.pm at 0b7f39e89379c4be478fb88a607aca4db9480eeb · pokutuna/p5-Class-Enumemon · GitHub

$data->{values} = [ map { bless $_, $pkg } @$defs ];


blessが何をしているのかパッと説明できなかったので
Hatena-Textbook/foundation-of-programming-perl.md at master · hatena/Hatena-Textbook · GitHub
あたりを見直す必要があると痛感。

IdolType->valuesで呼び出される処理は52行目から書かれている。

p5-Class-Enumemon/Enumemon.pm at 0b7f39e89379c4be478fb88a607aca4db9480eeb · pokutuna/p5-Class-Enumemon · GitHub

sub _mk_values {
    my ($pkg, $data) = @_;
    no strict 'refs';
    *{"$pkg\::values"} = sub {
        my $values = $data->{values};
        wantarray ? @$values : [ @$values ];
    };
}

*{"$pkg\::values"}が、「IdolType->valuesでなぜ [ bless({ id => 1, type => 'cute' }, 'IdolType'), ... ]が返ってくるのか」の核となっていそう。
「*」について『初めてのPerl』で引いてみても正規表現についてしか出てこない。
『続・初めてのPerl』で調べると、p91に以下の文言があった。

ここでは型グロブの説明はしませんが、*プリフィックスを使って、リファレンスを見て文字列内でどの変数型を使うべきかを判断せよとData::Dumperに指示するという意味になっています。

型グロブについて調べてみると、以下のブログに次のような説明があった。

blog.livedoor.jp

型グロブでシンボルテーブルにアクセス出来ます。シンボルテーブルでは、各データ型ごとに1つのスロットがあって、各スロットは各データ型のリファレンスを参照しています。

とりあえず型グロブっていう概念を使ってやっているんだなー、という感じで今日は終わっておく。
Perlむずかしい。

mackerel-plugin-accesslogを中心にMackerelのコードを読んだ


グラフのラベル名をいい感じにする簡単なPRを投げた時のメモ。

[plugin-aws-cloudfront]Replace label name of graph by wafuwafu13 · Pull Request #766 · mackerelio/mackerel-agent-plugins · GitHub


公式ブログを読めばなんとなく機能的な部分はわかった。
mackerel.io
mackerel.io

mackerel-plugin-accesslog/main.gompaccesslog.Do()呼び出されているmp.NewMackerelPlugin(...).Run()は、go-mackerel-plugin/mackerel-plugin.goに定義されている。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

mp.NewMackerelPlugin(&AccesslogPlugin{
	prefix:    *optPrefix,
	file:      flag.Args()[0],
	posFile:   *optPosFile,
	noPosFile: *optNoPosFile,
	parser:    parser,
}).Run()

go-mackerel-plugin/mackerel-plugin.go at 42f2323dad976a17b36639d1ddbefa85ad8e771e · mackerelio/go-mackerel-plugin · GitHub

// Run the plugin
func (mp *MackerelPlugin) Run() {
	if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" {
		mp.OutputDefinitions()
	} else {
		mp.OutputValues()
	}
}

このgo-mackerel-pluginmp.OutputValues()mackerel-plugin-accesslogFetchMetricsが呼び出されている。

go-mackerel-plugin/mackerel-plugin.go at 42f2323dad976a17b36639d1ddbefa85ad8e771e · mackerelio/go-mackerel-plugin · GitHub

// OutputValues output the metrics
func (mp *MackerelPlugin) OutputValues() {
	now := time.Now()
	stat, err := mp.FetchMetrics()


mackerel-plugin-accesslogFetchMetricsでは、まずgetReadCloser()p.fileつまり flag.Args()[0]で指定されたアクセスログのファイルが開かれる。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

// FetchMetrics interface for mackerelplugin
func (p *AccesslogPlugin) FetchMetrics() (map[string]float64, error) {
	rc, takeMetrics, err := p.getReadCloser()

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

func (p *AccesslogPlugin) getReadCloser() (io.ReadCloser, bool, error) {
	if p.noPosFile {
		rc, err := os.Open(p.file)
		return rc, true, err
	}

それをbufio.NewReader, Reader.Readlineで読み込んでいくという流れ。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

r := bufio.NewReader(rc)
...
buf, isPrefix, err := r.ReadLine()
...

ちなみにアクセスログのパーサーはoptFormat = flag.String("format", "", "Access Log format ('ltsv' or 'apache')")ltsvapacheが指定されてパースされている。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

switch *optFormat {
case "":
	parser = nil // guess format by log (default)
case "ltsv":
	parser = parsers.LTSV
case "apache":
	parser = parsers.Apache
if p.parser == nil {
	p.parser, l, err = parsers.GuessParser(line)
} else {
	l, err = p.parser.Parse(line)
}

パーサーの実態はGitHub - Songmu/axslogparser: Fairly accurate access Log Parserで、ltsvapacheも指定されていなかった場合は推測してくれるらしい。

あとはret := make(map[string]float64)のマップに結果を集計していってそれを返しておしまい。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

ret[string(fmt.Sprintf("%d", l.Status)[0])+"xx_count"]++
ret["total_count"]++
...
ret[v+"_percentage"] = ret[v+"_count"] * 100 / ret["total_count"]

はじめてOSSに新機能追加した


owという、Lots of built-in validationsOSSに、BigIntのバリデーションを追加した。

github.com
github.com

owに関しては2月頃に3回、型整備のPRを出してマージされていたので、コードには少し馴染みがあった。
https://wafuwafu13.hateblo.jp/#sindresorhusow

OSSの型整備より一歩踏み込んだことがしたいと思い、issueを消化することを目標に最近やっていた。
実際、null, undefinedに起因するバグがあったのでそれを消化できた。

Fix `ow.object` to return `ArgumentError` when `null` or `undefined` is passed by wafuwafu13 · Pull Request #211 · sindresorhus/ow · GitHub

そして今回、BigIntのサポートをするという、約3年前のissueがあったので拾ってやってみた。

Support BigInt validator · Issue #54 · sindresorhus/ow · GitHub

owの構造と、何をどう追加したのかを雑にメモしておく。

まず、README.mdにも載っている以下のコードでArgumentErrorがどのようにして起こるのかをみていく。

import ow from 'ow';

const unicorn = input => {
	ow(input, ow.string.minLength(5));

	// …
};

unicorn('yo');
//=> ArgumentError: Expected string `input` to have a minimum length of `5`, got `yo`


source/predicate/string.tsに、ow.stringに関するコードが定義されており、minLengthの定義は以下のようになっている。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

minLength(length: number): this {
    return this.addValidator({
	message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value}\``,
	validator: value => value.length >= length,
	negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value}\``
    });
}

このvalue => value.length >= lengthのバリデーターは、source/predicates/predicate.tsで発火される。
messageの発火も同じ部分である。

ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

...
result = validator(value);
...
const errorMessage = message(value, label_, result);

このvalidatorの実行結果がtrueでなければ、最終的に独自定義のArgumentErrorが投げられる。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

throw new ArgumentError(message, main, errors);

ArgumentErrorが投げられるのはこれ以外にも、ow(input, ow.type.foo)においてinputtypeの型が合っていない場合がある。
例えばtest/string.tsでテストされているような場合がある。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

t.throws(() => {
	ow(12 as any, ow.string);
}, 'Expected argument to be of type `string` but received type `number`');

このバリデーションは、Predicateクラスのコンストラクタで定義されている。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

this.addValidator({
	message: (value, label) => {
		// We do not include type in this label as we do for other messages, because it would be redundant.
		const label_ = label?.slice(this.type.length + 1);

		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
		return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${is(value)}\``;
	},
	validator: value => (is as any)[typeString](value)
});


ここのバリデーションで使われているisは、owと同じ作者が作ったライブラリである。
github.com

今回やったissueに立ち返ってみると、isBigInt detectionがサポートされたので、それをowにも取り込もう、というものだった。
Support BigInt detection · Issue #39 · sindresorhus/is · GitHub

最初は、source/predicates/number.tsbigintメソッドを足して、ow(BigInt(9007199254740991), ow.number.bigint)で判定させようと思ったが、numberbigintで型が違うため、コンストラクタのバリデーションで意図せず弾かれてしまった。
新たにBigIntPredicateクラスを定義し、意図してコンストラクタのバリデーションで弾かれる方針で実装を進めた。

NodeのBigIntサポートが10.4.0からなのでテストが思うように動かなかったりいろいろしたが、無事マージされた。
このOSSの作者はFull-Time Open-Sourcererでレスポンスもレビューも手早くて時差があるものの開発体験がとても良かった。
ソースコードの核となる部分の7割くらいを理解できれば、issueのバグを潰したり新機能開発ができるという肌感がある。

技術書積読消化購読メモ

4月5月
読んだ

  1. Docker Kubernetes実践コンテナ開発入門(Kubernetesから)
  2. Amazon Web Services基礎からのネットワーク&サーバー構築
  3. Kubernetes on AWS
  4. TCP/IPネットワーク入門
  5. Real World HTTP
  6. 入門監視
  7. Mackerelサーバ監視入門
  8. DNSがよくわかる教科書
  9. マイクロサービスアーキテクチャ
  10. オブジェクト指向でなぜ作るのか
  11. ドメイン駆動設計入門
  12. SREサイトリライアビリティエンジニアリング
  13. サイトリライアビリティワークブック

6月中
手動かす

  1. Webブラウザセキュリティ
  2. Go言語でつくるインタプリタ
  3. セキュリティコンテストチャレンジブック

読む

  1. Infrastructure as Code
  2. nginx実践入門
  3. 大規模サービス技術入門
  4. Go言語による並行処理
  5. コンピュータシステムの理論と実装
  6. 情報処理概論
  7. Linuxのしくみ
  8. SQLアンチパターン
  9. Elasticsearch実践ガイド
  10. Webフロントエンドハイパフォーマンスチューニング
  11. DMM.comを支えるデータ駆動戦略
  12. 関数型プログラミングの基礎 JavaScriptを使って学ぶ

7月以降
買って手動かす

  1. GraphQLどれか
  2. 実践 パケット解析 第3版
  3. 現代暗号への招待
  4. 言語論どれか
  5. Goならわかるシステムプログラミング
  6. Rust本どれか
  7. ガベージコレクション 自動的メモリ管理を構成する理論と実装
  8. 関数プログラミング実践入門
  9. 型システム入門 プログラミング言語と型の理論

8月までマストでやらないといけないっぽい
理論系はインターンの波が去ったらでいいか

  1. UdemyのSwift
  2. UdemyのSwift UI
  3. iOSアプリ設計パターン入門
  4. iOSテスト全書
  5. Rust本どれか
  6. WebAssemblyのチュートリアルどれか
  7. GraphQLどれか