たごもりすメモ

コードとかその他の話とか。

React appを手元でProduction modeで動かす

react-scripts startで使えるDevelopment modeだとなんか変なことがちょいちょい起きるので、動作確認をProduction modeでやりたい。

ところでこのアプリからはCORSリクエストを送りまくるのでHTTPSのサイトとしてlocalhostにアクセスしたい。Development modeについては、これはpackage.jsonに以下のように書いておいてnpm startすることで実現できる。

...
  "scripts": {
    "start": "HTTPS=true react-scripts start",
...

React appをProduction modeで動かす

単に動かすならドキュメントにあるようにnpm run buildでビルドしたあと、それを配信するサーバを実行すればいい。

$ npm run build
$ npm install -g serve
$ serve -s build

serveコマンドをHTTPSを有効にして実行する

ところがこれだとHTTPなので、HTTPSにしたいときには困る。見てみたらserveコマンドには--ssl-cert--ssl-keyオプションがあって、このふたつを指定してあればHTTPSで動いてくれるっぽい。ちょいちょいと鍵と証明書を作って実行する。

$ openssl genrsa -out key.pem
$ openssl req -new -key key.pem -out csr.pem
(色々聞かれるが、全部Enter連打でいい)
$ openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
$ serve -s build --ssl-cert cert.pem --ssl-key key.pem 

   ┌─────────────────────────────────────────────────────┐
   │                                                     │
   │   Serving!                                          │
   │                                                     │
   │   - Local:            https://localhost:3000        │
   │   - On Your Network:  https://192.168.68.103:3000   │
   │                                                     │
   │   Copied local address to clipboard!                │
   │                                                     │
   └─────────────────────────────────────────────────────┘

できた。

Chromeで証明書チェックをスキップする

できたと思ってChromehttps://localhost:3000を開くと証明書チェックにひっかかって進めない。Development modeのときはAdvanced以下に開くリンクが出るんだけど、こっちでは出ないのは何の違いによるものなのか。

なんでだよと思って調べてみたら、このページ上で(どこかをクリックしてから?) thisisunsafe とタイプすると開けるとのこと。マジかよと思ってやってみたら、できた。マジかよ。

yuki.world

まとめ

できた。やれやれ。

@react-google-maps/apiでの描画地図にPolylineで線を描くと消えなくなる

という問題が起きてあれこれやってた。React難しいの巻。たぶんnpm startで起動できるDevelopment modeでだけ起きる問題。

問題

@react-google-maps/apiでReactアプリ上にGoogle Mapsを表示*1し、そこに好き勝手にマーカーとか線を描きたい。以下のような感じ。

// MyMapComponent
import { LoadScript, GoogleMap, Marker, Polyline } from "@react-google-maps/api";

// ...中略
// in function MyMapComponent
return (
  <LoadScript googleMapsApiKey={myApiKey}>
    <GoogleMaps
      id="myMap"
      mapContainerStyle={{height: "80%", width: "100%}}
      zoom={calculatedZoom}
      center={{lat: calculatedCenterLat, lng: calculatedCenterLng}}
      mapOptions={{disabledDefaultUI: true, zoomControl: true}}
    >
      <Marker key={"map-marker-start-" + start.uuid} position={{lat: start.lat, lng: start.lng}} />
      <Marker key={"map-marker-end-" + end.uuid} position={{lat: end.lat, lng: end.lng}} />
      <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
      />
    </GoogleMaps>
  </LoadScript>
);

startとかendあるいはpathFromStartToEndなんかはpropsで親から受け取る。これでまあうまく動く、ように見える。 なんだけど、外部から与えてるpropsの中身を変えてマーカーや線を再描画するとマーカーや線が残ることがあって、なんでなんだこれってだいぶ苦労した。

調査

いろいろ調べてると、Google Maps JavaScript APIのドキュメントにこんなのを見掛けた。

Removing Polylines  |  Maps JavaScript API  |  Google Developers

なんかこれがうまく呼べてないんだろうなってことで@react-google-maps/apiの実装を調べてみると、このコードを読む限りではthis.state.polyline.setMap(null);してるように見える。おっかしーな。 で、調査してみようと以下のように<Polyline />コンポーネントの呼び出しにonLoadonUnmountってフックがあったので、引数にstateに格納しているpolylineオブジェクトを受け取れる。これを以下のように指定して動かしてみた。

const onLoadHook = (line) => {
  console.log({message:"onLoad", line});
};
const onUnmountHook = (line) => {
  console.log({message:"onUnmount", line});
};

return (
  // ... 中略
        <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
        onLoad={onLoadHook}
        onUnmount={onUnmountHook}
      />
  // ... 中略
);

そしたらonLoadは2回呼ばれてるのにonUnmountは一回しか呼ばれてないことがわかった。えー。Polylineコンポーネントが作り直されて1回ずつ呼ばれたのか、Polylineコンポーネントは1回だけ作られて2回mountされたのかはよくわかってないんだけど *2 。でもたぶん後者かなという気がする。以下の推論がうまくハマるから。

ひとつのコンポーネントが2回マウントされているとすると、Polylineオブジェクトが作られたあとcomponentDidMountが2回呼ばれてるってことで、それぞれ呼び出しの中でnew google.maps.Polyline({...})が行われてるから、実際の地図上には同じ線が2重に引かれてることになる。

どちらの呼び出しもsetStatepolylinenewしたオブジェクトを保存しているので、1回目の呼び出しでstateに保存されたPolylineオブジェクトは上書きされて消えてしまい、setMap(null);が呼ばれることがなくなってしまう、ということっぽい。

で、これはdevelopment mode的なやつでだけ起きてるんじゃないかなとnpm run buildしたものを手元で動かしてみた*3onLoadフックが1回しか呼ばれなかったので、Production buildすればこの症状は起きない。

が、まあちょっと手元の開発でこれ起きてるの無視するのはねえ……。

解決策

しょうがないので自分でpolyline.setMap(null);を確実に呼ぶようにする。以下のようにonLoadフック経由で対象オブジェクトを受け取り、再レンダリング前のクリーンナップ時に過去描画されたものに対してsetMap(null);を呼ぶ。

import { useEffect } from `react`;

const lines = [];
const onLoadHook = (line) => {
  lines.push(line);
};

useEffect(() => {
  return () => {
    lines.forEach((line) => {
      line.setMap(null);
    });
  };
});

return (
  // ... 中略
        <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
        onLoad={onLoadHook}
        onUnmount={onUnmountHook}
      />
  // ... 中略
);

これでうまくいった。useEffectの使いかたがやっとちゃんとわかった気がする。

余談

はてなブログMarkdown書式、コードハイライトの形式にjsx指定しても無効なの悲しいね。

*1:最初はGoogleのオフィシャルのReact Wrapper使おうと思ったんだけど、細かいところどうやるかのドキュメントが何もないのとロードがうまく動かないのと、あれこれあって諦めて使うライブラリをスイッチしたら一発でできた。なんだよ。

*2:JavaScriptのObjectにもobject_idがあったら便利なのに……。

*3:これをHTTPS有効にしてやるのにまたひとハマりした、ああもう

RubyKaigi Takeout 2021でしゃべった

RubyKaigi Takeout 2021の途中ではあるけど、とりあえず自分の発表終わったのでメモ。

Ractorをワーカーとして使うアプリケーションサーバを作ったよ! という話なんですが、まあこれは話の入りというかそういうもので、実際の内容の中心はRactorではどういうコードが動かないのか、どうすればよいのか、みたいな話です。 で、Ractorで動かないコードは全体的には直していきたいよねということになると思いますが、そうはいっても動かしてみないと確認もできないじゃんということで、主な問題であるところのWebアプリケーションを実際にRactorの上で動かして確認できるようにしておきましたよ、というのが今回作ったright_speedです。

github.com

本当はRactorの数を増やしていくごとにどれくらいのリクエストを処理できるようになるのかとか、MJITを有効にしたときその傾向はどうなるのかとか試したかったんですが、それよりだいぶ手前のところでSEGVを踏んでしまって、セッション録画の提出日までにはちょっとどうにもできませんでした。残念。

副産物

新しい機能なもんで作業してる途中であれこれあって、明確なやつはbugsにいくつか報告してます。

あとそのへんについてのパッチとか。

SEGVってるやつは見てみてはいるんだけど明確にこれかなというのはまだ分かってない*1んで、どうにかなるかなあ。あとRactorまわりの話はいじってると割と簡単にプロセスがビジー状態になったりSEGVったりするのが厳しい。

フィードバック

定数に値を入れるときにデフォルトでshareableだとマークする方法が欲しい、マジックコメントかな? という議論がスライド中であるんだけど、発表中にコメント欄で@ko1さんに教えてもらった。

# shareable_constant_value: true

class Foo
  VALUE = {key: "value"}
end

Ractorのドキュメントにも書いてあったので完全に自分の見落としでした。すいません。

(追記) …… と思ったら今日直されていたのでたぶんドキュメント読んだときはmagic commentだと思わずそのままスルーして忘れてたんだな。無実だったことにしよう。

*1:デバッグ用のカウンタまわりなんじゃないかなと思ってはいるんだけど

Amazon SESのメール受信は特定のリージョンでしか使えない

メモ。ap-northeast-1のコンソールでSES開いてても"Email Receiving"の"Rule Sets"が灰色になった(gray outした)ままで選択できなくて、ドメインのverifyとかIAMとかで何かうまくいってない? と調べてまわってた。

結論としては特定のリージョンでしかサポートされてない。

Email Receiving Endpoints

Amazon SES doesn't support email receiving in the following Regions: US East (Ohio), US West (N. California) Asia Pacific (Mumbai), Asia Pacific (Seoul), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), Canada (Central), Europe (Frankfurt), Europe (London), Europe (Paris), Europe (Stockholm), Middle East (Bahrain), South America (São Paulo), and AWS GovCloud (US).

docs.aws.amazon.com

なんだよもー。us-east-1かus-west-2かなあ。
なおリージョンが変われば "Verify a New Domain" もやりなおしみたい。

退職します2021

TL;DR

  • 現職のTreasure Dataを本日を最終出社として退職します
  • しばらくは休みをとりつつ次に何をやるかを考えるつもり
  • 次は自分でビジネスを立ち上げるか、それともエンジニアリングチームを作るところにフォーカスするか、これから考える
  • 技術顧問業もはじめます、が、メインにはしないつもり
  • その他これからの活動にご期待ください

現職について

就職時にこのエントリを書いてから6年3ヶ月、当初思っていたより長く働いたなあという感じです。入ったときはUSと日本で合計40人もいなかったくらいだったと思うけど、今では世界中に同僚がいて規模は約10倍くらいになりました。途中Armによる買収もあって、スタートアップから中規模企業までのビジネスと会社の成長を見てきました。自分もそれなりに貢献できてたんじゃないかなと思います。 いま見直すと就職エントリに書いていた3点、「技術ベンチャーであること」「ベンチャー企業として大きな成功を狙っていること、またそれが有望に見えること」「優秀なプログラマが同僚に多いこと」はすべて見事に満たされていて、データ処理に関する高い技術をベースにビジネスを組み立て、それにより成功したexitを経験できました。優秀なプログラマ大勢と働けていたのは言うまでもありません。

いやー、いい6年余りでした。

特に初期の頃は営業やSE*1、果てはBizDev*2担当のVPなんかとも一緒に仕事をすることがあったりして、顧客を訪問しにベイエリアやシアトル、スコットランドなんかに行って直接話したりもしました。

ここ3〜4年くらいは自分のマネージャや同じチームの同僚が基本的に北米の人ばっかりという状態で、おかげで英会話の機会には困りませんでした。入社時は英会話はうーん、がんばる! くらいだったんですけど、今はもうペラペラ……というと語弊があるけど、日常会話から業務上の議論や交渉、練習なしのプレゼンテーションまでだいたい困らなくなりました。英語圏のIT系企業ならたぶんどこでも働けるくらいにはなれたんじゃないかなと思います。会社から英会話サービスの補助とかも出てたけど、結局まったく使わずに終わってしまった*3

他はもちろん、お金は大事で、もう非常によい目を見させてもらいました。Treasure Dataの創業者たちがインタビュー等でよく開発者への待遇や見返りについて話していますが、正直彼らのやったことはインタビューで言ってる内容でもまだだいぶ過少に言ってるなという感じで、考えられる限りによい報酬プログラムにしてもらったなと思います。本当に感謝しています。

じゃあなんで辞めるの

単純に次に何をやるか考える時間が欲しくなった、という感じです。Armによる買収の後から「次になにやるの?」という質問が脳内をぐるぐるしていて、けれど普段の業務をやっているとそういうことを考える時間や余裕がなかなか取れないので、いったん休んでそういうこと考えてもいいかという気分になりました。ありがたいことに、金銭的には多少の余裕はあるし。

まあ他にもいち開発者として働いている今のポジション/ロールを変えたくなったこと、会社の規模が大きくなってきたことにより自分がTreasure Dataという組織に求めているものと実態との間のギャップが大きくなってきたこと、なんかもあります。特に自分の役割というか仕事としてやることの内容については、前までは一生いち開発者でいいと思っていたんですが、この6年経験したことを考えると、もうちょっとやりたいことも色々あるなあというか。このへんはまだはっきりしたことが考えられてないので、これからですが。

次どうするの

とりあえず数ヶ月はゆっくり休みをとりつつ考えようかと思っています。現状、次のフルタイム仕事についてはどことも何も話をしていません*4

が、ちょうどよいタイミングで来た話がありまして、6月からはあるスタートアップの技術顧問を1件引き受けることにしました。技術顧問業を生業にしようとはあまり思っていないのですが、それでも自分の知識と経験を役立てられそうなこと、色々な会社やサービスの実態を見せてもらうのは今の自分の考え事にとっても非常に大きいプラスになりそうなことから、とりあえずやってみようと思っています。 ということで、他にももしお困りのところに自分がお役に立てそうなところがあればご連絡ください。もう数件くらいは引き受けられるかもしれません。

次のメインの仕事はどうするかこれからなんですけど、どちらかというと開発者としての仕事よりは、チームを作る・ビジネスを作る、といった方向かなあとぼんやり考えています。明確なイメージはまだないんですが、どうしたものか。先達の方々に話を聞きに行きたいなあと思っているので、そのうち皆様にお願いに上がるかもしれません。その際はよろしくお願いします。

あとは最近読めてない本を読みまくったり、あまり触ってない方面の言語をやってみたりしつつ、ちょっとは遊んだりします。他にボランティアで参加する活動なんかもありそう。コロナが無ければ世界中旅行して回ったりしたいところなんですけど、それはまた今度の機会かな、しょうがない。飲みにいったりもできると良かったんだけど、コロナ明けまで持ち越しですね。

ということで

今後ともtagomorisをよろしくお願いします!

*1:Sales Engineer

*2:Business Development、日本語で言うの難しいなこれ

*3:というか大学を出て以来、英会話教室的なものには一度も通ったことがない。義務教育と受験英語はなかなかよくできている

*4:実は最近どうかと思える件もあったんだけど、詳しい内容を聞くとこちらの期待との間にギャップがあったのでそのお話は無かったことに

Gradle経由でのテスト実行時、コンソールに失敗したテストケースの情報を出力する

`./gradlew test`とか`./gradlew build`とかしたときに失敗したテストの情報はこの`index.html`を見てね! っていうのがめんどくさくて、なんでデフォルトで失敗したテストの情報を出してくれないんだっけ、と思っている人、主に俺の問題を解決する。
ていうかデフォルトでそうしてくれればCIサービスの設定時にテスト結果の保存とかを個別にやらないと何が失敗したかもわかんなくなってて困る、みたいな状態にならなくてすむのにね。

調べてたら「とにかくコンソールに全部出したい!」みたいなのがいっぱいひっかかるけどそうじゃないんだよなー、そういうのはtoo muchなんだよ。
で、これ。

stackoverflow.com

のうち一番下の回答(TestLoggingを使う)が簡単に調整できそうだったので、手元プロジェクトで以下のようにした。Kotlin DSL使ってるんで`build.gradle.kts`の変更。

import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent

// ...

tasks {
    "test"(Test::class) {
        // ...
        testLogging {
            events(TestLogEvent.FAILED, TestLogEvent.SKIPPED)
            exceptionFormat = TestExceptionFormat.FULL
            showCauses = true
            showExceptions = true
            showStackTraces = true
        }
    }
}

標準出力とか標準エラー出力とかの表示を抑制して失敗したテストケースの原因とスタックトレースを出す。やってみたらこんな表示になって自分の期待したものはこれだったんだ!!!!!!!!!!!! という感じ。

MBA:my-project tagomoris$ ./gradlew build

> Task :test

com.treasuredata.myproject.FooBarTest > should fail FAILED
    java.lang.AssertionError: expected:<1> but was:<0>
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:120)
        at kotlin.test.junit.JUnitAsserter.assertEquals(JUnitSupport.kt:32)
        at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:57)
        at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
        at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:56)
        at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
        at com.treasuredata.myproject.FooBarTest.should fail(FooBarTest.kt:72)

97 tests completed, 1 failed

> Task :test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/tagomoris/td/my-project/build/reports/tests/test/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 12s
17 actionable tasks: 1 executed, 16 up-to-date

macOS Big Sur なM1 MacのEmacsでDocuments以下のファイル(など)を開く

M1 mac (macOS Big Sur)買ってからまだちゃんと開発とかに使えてなくてちょっとずつセットアップ進めてるんだけど、Emacsで ~/Documents 下とかが読めないことに気付いた。以前のバージョンで指摘されてた解法であるところの以下の方法もダメ:

  • M-x ns-open-file-using-panel で一度Documents下のファイルを開けばいける → ダメ
  • EmacsにFull Disk Access権限を与える → ダメ
  • 実は裏技で/usr/bin/rubyにFull Disk Access権限を与える → なんかファイル開くダイアログで/usr/binとかが開けなくなってない?(ダメ)

というわけで調べてたらこんなのが見付かった。

braveam.com
Can not do dired on ~/Documents when it is in iCloud on Catalina · Issue #84 · caldwell/build-emacs · GitHub

このままやるとx86_64なバイナリを起動しようとしてしまうので以下のように。
1. システム環境設定 → システムとセキュリティ → プライバシー → フルディスクアクセスをEmacs.appに与える
2. Terminalで以下のコマンドを実行、arm64なバイナリにリンクするよう注意

$ cd /Applications/Emacs.app/Contents/MacOS
$ mv Emacs Emacs.old
$ ln -s Emacs-arm64-11_2 Emacs

これでうまく動くようになった。めんどくさいですね。