経験は何よりも饒舌

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

axiosの内部構造を理解してadapterの仕組みを解明する


axiosのadapterを使う機会があり、仕組みが気になったのでメモしておく。
github.com

adapterで検索をかけると、lib/core/dispatchRequest.jsの51行目の記述が目についた。
axios/dispatchRequest.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
...

dispatchRequest.jsは名前からも、lib/core/Axios.jsで使われていることからも、実際にリクエストを送る部分であろう。

まずはdefaults.adapterがなんなのかを調べると、lib/defaults.jsに以下の記述があった。
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
...
var defaults = {
    adapter: getDefaultAdapter(),
    ....

typeof XMLHttpRequestchromeのコンソールで調べると以下のようになったので、adapter = require('./adapters/xhr');の元を見にいった。

typeof XMLHttpRequest
"function"
typeof XMLHttpRequest = "function"

lib/adapters/xhr.jsでは、XMLHttpRequestを使ってリクエストが送られていた。

axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

var request = new XMLHttpRequest();
...
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
...
// Send the request
request.send(requestData);

つまり、axiosをadapterオプションを指定せずに使うと、内部的にはXMLHttpRequestを使った通信が行われ、adapterオプションを指定すると、通信そのものをラップできることが分かる。

オプションの反映はlib/core/mergeConfig.jsを使って行われていることが分かったが、深追いはせずテストコードを眺めるだけに留めた。
axios/mergeConfig.spec.js at main · axios/axios · GitHub

実際にadapterオプションを書いて使ってみた。
この場合リクエストは発生せず、実装したPromiseが返っている。

const axios = require("axios");

const sampleAdapter = () => {
  return new Promise((resolve, reject) => {
    resolve({ data: "sample data" });
  });
};

axios({
  url: "https://example.com/",
  adapter: sampleAdapter,
}).then((res) => console.log(res.data));
$ node index.js
sample data

リクエストを送る部分はlib/core配下にあるものだと思っていたので、adapterオプションを指定しない場合にlib/adapters配下のコードが使われているのが意外だった。

追記:
axiosにPR投げてみたらlib/adapters配下のコードが使われているという理解はあっていた。
github.com

AWA株式会社でのインターンでインフラに強くなりました

株式会社サイバーエージェント全職種向け 実践就業型インターン【オンライン参加可】に3月2日~3月31日の1ヶ月間参加し、インフラに強くなりました(過去の自分比)。

参加するまで

サイバーエージェントは魅力的な会社ですし、人生で1回は行っておきたいという気持ちがまずありました。
また、大規模サービスに触れる機会はもともとあったのですが、インフラがほぼブラックボックスの状態で開発をしていたので、インフラに詳しくなりたいという目的がありました。
個人ではJS/TSを重点的にやることが多く、また、インフラの実務経験がほぼない状態でインフラを志望するのは勇気が要りましたが、春は挑戦の季節ということで応募してみました。
とは言っても、エントリー締め切りがインターンシップ開始月の3ヶ月前の月末だったので、11月あたりにエントリーした記憶があります。
1次面接(人事)でABEMAかAWAに行きたいと伝え、2次面接(エンジニア)でインフラをやりたいと伝えたところ、希望が通ってAWAでインフラをさせてもらうことになりました。
面接とはいっても雑談のような感じで、エントリーから参加するまでは長かったですが、合否の結果は早かったのでストレスなく暮らせました。
(EC2とECSという単語が出てくると少し混乱するというようなレベル感だったので、ゴリゴリの技術面接ではなくてよかったです。)
一応オフラインを希望したのですが、フルリモートでの勤務となりました。
出社できなかったのは残念ですが、メンターさんや人事の方とのコミュニケーションに苦労はしませんでした。
働いてみて、AWAは少数精鋭という印象を受けました。
僕が所属したサーバーチームは自分を含めて4名で、Androidをやりながらサーバーもやるといったようなマルチな方の集団といった感じでした。

やったこと

ここからの内容は以下のスライドにも載せてあります。

speakerdeck.com

  • Terrafromのバージョンアップ(v0.12 -> v0.13)

まずはレポジトリ全体の構成をしっかり把握してから始めました。
ただCIを回してバージョンをあげればいいというわけではなく、運用上、HCLの記述と実際の設定に差分が生じるので、それを解消する必要がありました。

  • 本番LOUNGEサーバーのリソース増強(Terraform/Datadog)

ECSの設定をいじって、タスクが切り替わる様子やDatadogのグラフが遷移する様子をみました。

  • LOUNGE検索の追加(MongoDB/Mongo Connector/Elasticsearch/Jenkins/Ansible)

登場してくるツールやレポジトリが多くなったので、自分がしたことがどこにどう反映されるのかを理解しながら進めることを意識しました。
初めての概念が多かったのでドキュメントをしっかり読みました。
github.com

  • ドキュメントの作成(DocBase)

検索データの同期周りは、1年前のドキュメントしかなかったので、新たに作成しました。
ドキュメントとかテストとかしっかりしたいタイプなのでこういう仕事もよかったです。

  • LOUNGEの検索処理

gRPCのproto定義から始めてGoを書いていきました。
クリーンアーキテクチャが採用されているレポジトリに項目を足す作業だったので、難しい部分はありましたが、見通しは立てやすかったです。
これでつまったりしていました。
github.com

感想

  • 夕会すごい

毎日30分、金曜1時間の夕会に参加させていただきました。
エンジニアの方だけではなく、40名くらいの方々が集まり、念密に施策のすり合わせをしていらっしゃいました。
AWAは毎日聴いているものの、今回はインフラでの参加だったので、開発者としてサービスに対する理解が薄くなるのではないかという心配をしていましたが、異なる職種の方が何をしているのかまで知れました。
また、メンターさんにも毎日1on1で30分以上時間をいただき、質問の対応や進捗の管理をしていただきました。

  • Go読みやすい

普段はGoでの開発はしていなかったので不安だったのですが、インターフェイスから辿っていったり、VSCodeの機能を使ったりして初見でも自力で読める部分は多かったです。
Goでの運用を知れたのもよかったです。

  • インフラ周り楽しい

普段インフラはあまり触っておらず、何をしているか分からなくて怖い印象を持っていたのですが、1つの作業でアプリケーションを大きく動かすことができることに特に楽しみを感じました。

  • 触るレポジトリ多かった

マイクロサービスで構成されているため、自分が関連するところだけでもTerraformのレポジトリを合わせると8つありました。
普段はいわゆるモノレポしか基本触らないので、最初は頭が混乱しましたが、慣れるとやりやすさを感じました。

  • サービスの歴史の理解が重要

どのサービスにも少なからずクセのようなものがあり、なんでこのようなコードになっているのか、どうしてこのツールを用いているのか、に対する理解が働く上で重要だと感じました。

  • 意外と自走できる部分はあった

Goもインフラもほぼ未経験の状態で飛び込んだので、1ヶ月で自分ができることはかなり少ないと思っていましたが、ひとつひとつキャッチアップしていくことでなんとか1エンジニアとして働くことができました。
gomockを使ったりしながらテストまで一貫して書けたり、コード書いてる途中で自分が生んだN+1問題に気づけて修正できたりしたのもよかったです。
どこでもインフラのタスクに手をあげていくぞというやっていきや、独学でももっとインフラの知識を深めていきたいという感情が芽生えたのも大きな成功体験でした。

AWAはこれからも使い続けるサービスですし、今回得た経験もこれから活き続けるので、とても濃い1ヶ月でした。
課題としては、iOS/Android の知識がもっとあれば、得るものもより多かったと思います。
どんな質問にも答えてくださったメンターさん、進捗や目標を管理してくださった人事の方、インターンという場に関わる方々に感謝です、ありがとうございました!

DefinitelyTyped で Ace の型を大幅に改善した


前のこの記事では、Definitely Typedから型をインストールして適用する際に、実際にレポジトリまで見に行かないといけなかった、ということを書いた。

wafuwafu13.hatenadiary.com

今回は、PRを出してマージされたので、そのことを書く。

github.com

PRタイトルにある通り、この型は全て ajaxorg/ace/ace.d.ts から取ってきた。
コミットログを見ると、ace.d.tsファーストコミットが2018年3月DefinitelyTyped/types/ace/index.d.tsファーストコミットが2017年3月 となっており、DefinitelyTypedの方が先に作成されているが、以下のコメントに示す通り、

[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

ace.d.tsの型の方が正確であり、以下の差分に示す通り、

[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

DefinitelyTypedの型にはanyが多く、その役割をきちんと果たしてはいなかった。
DefinitelyTypedのレポジトリは巨大で、cloneするにも検索するにも時間がかかって大変だったが、PRを出した。
ace.d.tsを参考にしてるとはいえ、変更がかなり多いので、レビュワーが大変だなーと思っていたが、2週間経ってもレビュワーは反応しなかった。
botが反応し、違う人にレビュー依頼を出してからは、すぐにマージされてリリースされた。
[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

anyだらけで長い間放置されていたことも、なかなかレビューがされなかったことも、違う人にレビューが回ると一瞬でマージされたことにも、DefinitelyTypedに対する今までの信頼性(主観)が少し損なわれたが、改善できたのはよかった。

参考にしていた、ace.d.tsにも、まだ足りない型定義があったので少し追加しておいた。
github.com

こちらの反応は早かった。

Jest のモック関数を整理する


npqという、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxiosnode-fetchで代替するというissueがあったのでやってみた。

github.com
github.com

置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
jest.mockmockImplementationjest.spyOnに対する理解が曖昧だったので、整理したいと思う。
コードはこのレポジトリにまとめた。
github.com


まずは、単純な関数のモックから整理していきたい。
下のように、第2引数とランダムな値を足した値を返す関数があるとする。

function random(a, b, c) {
  return b + Math.random();
}

module.exports = random;

この関数のテストでは、便宜上、ランダムな値は無視したい。
なぜなら、無視しないと返り値が予測できず、テストが通らないからだ。

const random = require("./random");

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // Expected: 2
  // Received: 2.119340184508286
});

モックする1つの方法として、jest.fn を用い、randomに新しい、未使用の mock functionを代入してしまう方法がある。
しかし、この方法では、後続のテストが壊れてしまう恐れがある。

let random = require("./random");

random = jest.fn(() => 2)

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // pass
  expect(random(1, 3, 5)).toBe(3);
  // Expected: 3
  // Received: 2
});

そこで、randomモジュールをモックし、random関数が第2引数を返すようにモックすれば、うまくいく。

const random = require("./random");

jest.mock('./random')
random.mockImplementation((a, b, c) => b);

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // pass
  expect(random(1, 3, 5)).toBe(3);
  // pass
});

これをnode-fetchに対するモックでも応用してみる。
下のように、Promiseを返す関数があるとする。

const fetch = require("node-fetch");

function getByFetch() {
  const data = fetch("http://example.com/");
  return data;
}

module.exports = getByFetch;

これに対するテストは、node-fetchモジュールをモックし、fetch関数がfooを返すようにモックすれば、うまくいく。

const fetch = require("node-fetch");
const getByFetch = require("./fetch");

jest.mock("node-fetch");
fetch.mockImplementation(() => {
  return "foo";
});

it("fetch", () => {
  const data = getByFetch();
  expect(data).toBe("foo");
  // pass
});

次に、オブジェクト内の関数のモックについて整理していく。
このようなオブジェクトがあるとする。

const obj = {
  random: function () {
    return Math.random();
  },
  randomPlusOne: function () {
    return Math.random() + 1;
  },
   foo: function () {
    return "foo";
  },
};

module.exports = obj;

これに対するテストでモックする1つの方法としては、objモジュールごとモックしてしまう方法がある。

const obj = require("./object");

jest.mock("./object", () => {
  return {
    random: () => 1,
    randomPlusOne: () => 2,
    foo: () => 'foo',
  };
});

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

これは、次のようにも書ける。

const obj = require("./object");

jest.mock("./object");
obj.random.mockImplementation(() => 1);
obj.randomPlusOne.mockImplementation(() => 2);
obj.foo.mockImplementation(() => 'foo');

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

しかしこれらは、objモジュールごとモックしているため、例えばfooをモックし忘れたら、存在していないことになってしまう。

const obj = require("./object");

jest.mock("./object");
obj.random.mockImplementation(() => 1);
obj.randomPlusOne.mockImplementation(() => 2);

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  //  Expected: "foo"
  //  Received: undefined
});

jest.spyOnを用いれば、その心配はなくなる。

const obj = require("./object");

jest.spyOn(obj, 'random').mockImplementation(() => 1);
jest.spyOn(obj, 'randomPlusOne').mockImplementation(() => 2);

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

これをaxiosに対するモックでも応用してみる。
下のように、Promiseを返す関数があるとする。

const axios = require("axios");

function getByAxios() {
  const data = axios.get("http://example.com/");
  return data;
}

module.exports = getByAxios;

これに対するテストは、以下のように書けばうまくいく。

const axios = require("axios");
const getByAxios = require("./axios");

jest.spyOn(axios, 'get').mockImplementation(() => Promise.resolve('foo'));

it("axios", async () => {
  const data = await getByAxios();
  expect(data).toBe("foo");
});

理解が少し難しいjest.mock/mockImplementation/jest.spyOn あたりを整理した。

Go の Short variable declarations と Named return values


go-mp4という、mp4ファイルをパースしてくれるGoで書かれたライブラリがあった。
Goに慣れるため、golintカバレッジを上げるPRを出してみた。

github.com

自分の書いたコードで、:=ではno new variables on left side of :=というエラーが出たけれど、代わりに=を使えばうまくいく、という事象があった。
具体的には、以下の部分でエラーが出ていた。

func UnmarshalAny(r io.ReadSeeker, boxType BoxType, payloadSize uint64, ctx Context) (box IBox, n uint64, err error) {
	dst, err := boxType.New(ctx)
	if err != nil {
		return nil, 0, err
	}
	n, err := Unmarshal(r, payloadSize, dst, ctx) // no new variables on left side of :=
	return dst, n, err
}

Goの言語仕様のShort_variable_declarationsによると、同じブロック内で同じ型として宣言された変数に対して、宣言する変数のうち少なくともひとつはブランク変数でなければ、再宣言が可能ということだった。

Unlike regular variable declarations, a short variable declaration may redeclare variables provided they were originally declared earlier in the same block (or the parameter lists if the block is the function body) with the same type, and at least one of the non-blank variables is new.

ただし今回は、dst, err := boxType.New(ctx)に対する再宣言が原因でエラーが出ているわけではない。
以下のコードがコンパイルエラーを起こさないことがそれを証明している。
(本来dstIBoxという型だが、ここではstringにしている。)

func main() {
	dst, err := "foo", fmt.Errorf("error1")
	n, err := 1, fmt.Errorf("error2")
	
	fmt.Println(dst,n,err) // foo 1 error2
}

試しに以下のように、関数内で使われているnという変数名をn1に変えてみると、no new variables on left side of :=というエラーは起こらなかった。

func UnmarshalAny(r io.ReadSeeker, boxType BoxType, payloadSize uint64, ctx Context) (box IBox, n uint64, err error) {
	dst, err := boxType.New(ctx)
	if err != nil {
		return nil, 0, err
	}
        // n1に変えた
	n1, err := Unmarshal(r, payloadSize, dst, ctx)
	return dst, n1, err
}

また、戻り値の変数の名前(named return value)をnからn1に変えてみても、no new variables on left side of :=というエラーは起こらなかった。

func UnmarshalAny(r io.ReadSeeker, boxType BoxType, payloadSize uint64, ctx Context) (box IBox, n1 uint64, err error) { // n1に変えた
	dst, err := boxType.New(ctx)
	if err != nil {
		return nil, 0, err
	}
	n, err := Unmarshal(r, payloadSize, dst, ctx)
	return dst, n, err
}

A Tour of Go の Named return values によると、nerrorは関数の最初で定義した変数名として扱われるということだった。

Goでの戻り値となる変数に名前をつける( named return value )ことができます。戻り値に名前をつけると、関数の最初で定義した変数名として扱われます。
この戻り値の名前は、戻り値の意味を示す名前とすることで、関数のドキュメントとして表現するようにしましょう。

つまり、以下のコードが通らず、

func main() {
        var n uint64
        var err error
	
	n, err := 1, fmt.Errorf("error") // no new variables on left side of :=
}

以下のコードが通る、ということが今回起きていたのだった。

func main() {
        var n uint64
        var err error
	
	n, err = 1, fmt.Errorf("error")
}


Goの言語仕様でnamed return valueの仕様は、さっとみた限りヒットしなかったが、Defer statementsの欄に、

function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned.

と書いてあった。

エラーを報告する際に意識していること


基本的には質問をせずに粘ってしまうタイプなのだが、環境構築の段階でのエラーや、人生で初見のエラーなどは経験上、粘っても何も得られないので、即質問するようにしている。

そこで意識していることは、

  • 何が原因で何ができなかったのかを記す
  • 自分が打ったコマンドのログを全て記す
  • なぜそのコマンドを打ったのかの情報源を記す
  • その結果のログを記す
  • 自分なりの解決案があれば記す

具体例
github.com

qrcode.react とスナップショットテストの相性が最高すぎた


qrcode.reactは、その名の通りQRコードを生成してくれるReactコンポーネントだ。
github.com

ファイル構成はsrc/index.jsでゴリゴリ計算というか文字列を算出し、コンポーネントを返す構成だった。
テストがなかったので、テストを追加したかったのだが、何に対してテストを追加すればよいのかわからず、しばらく放置していた。
convertStr関数やgeneratePath関数の入力と出力に対する単体テストを追加しようと思ったけれど、あまり恩恵はないように思えた。
qrcode.react/index.js at 6aeb42abc26ffecc868b630b6ad8f507d2125813 · zpao/qrcode.react · GitHub
qrcode.react/index.js at 6aeb42abc26ffecc868b630b6ad8f507d2125813 · zpao/qrcode.react · GitHub


そんなとき、以下の記事を目にした。
www.mizdra.net

試しにqrcode.reactで試してみると、とてもよい結果が得られた。

github.com

何がよかったかと思うと、convertStr関数やgeneratePath関数の入力と出力に対する単体テストをせずとも、それらがうまく機能していることを保証でき、なにより、pathコンポーネントdの値の複雑性がスナップショットの出力により明らかになり、スナップショットの結果が1つの情報としても機能しているところだ。

  <path
    d="M0,0 h29v29H0z"
    fill="#ffffff"
  />
  <path
    d="M0 0h7v1H0zM8 0h1v1H8zM10 0h5v1H10zM17 0h2v1H17zM22,0 h7v1H22zM0 1h1v1H0zM6 1h1v1H6zM9 1h2v1H9zM13 1h1v1H13zM19 ....
    fill="#000000"
  />

マージされなくてもブログを書けば供養できると思って書いた。