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で証明書チェックをスキップする
できたと思ってChromeでhttps://localhost:3000
を開くと証明書チェックにひっかかって進めない。Development modeのときはAdvanced以下に開くリンクが出るんだけど、こっちでは出ないのは何の違いによるものなのか。
なんでだよと思って調べてみたら、このページ上で(どこかをクリックしてから?) thisisunsafe
とタイプすると開けるとのこと。マジかよと思ってやってみたら、できた。マジかよ。
まとめ
できた。やれやれ。
@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 />
コンポーネントの呼び出しにonLoad
とonUnmount
ってフックがあったので、引数に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重に引かれてることになる。
どちらの呼び出しもsetState
でpolyline
にnew
したオブジェクトを保存しているので、1回目の呼び出しでstateに保存されたPolylineオブジェクトは上書きされて消えてしまい、setMap(null);
が呼ばれることがなくなってしまう、ということっぽい。
で、これはdevelopment mode的なやつでだけ起きてるんじゃないかなとnpm run build
したものを手元で動かしてみた*3らonLoad
フックが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
の使いかたがやっとちゃんとわかった気がする。
余談
RubyKaigi Takeout 2021でしゃべった
RubyKaigi Takeout 2021の途中ではあるけど、とりあえず自分の発表終わったのでメモ。
Ractorをワーカーとして使うアプリケーションサーバを作ったよ! という話なんですが、まあこれは話の入りというかそういうもので、実際の内容の中心はRactorではどういうコードが動かないのか、どうすればよいのか、みたいな話です。 で、Ractorで動かないコードは全体的には直していきたいよねということになると思いますが、そうはいっても動かしてみないと確認もできないじゃんということで、主な問題であるところのWebアプリケーションを実際にRactorの上で動かして確認できるようにしておきましたよ、というのが今回作ったright_speedです。
本当はRactorの数を増やしていくごとにどれくらいのリクエストを処理できるようになるのかとか、MJITを有効にしたときその傾向はどうなるのかとか試したかったんですが、それよりだいぶ手前のところでSEGVを踏んでしまって、セッション録画の提出日までにはちょっとどうにもできませんでした。残念。
副産物
新しい機能なもんで作業してる途中であれこれあって、明確なやつはbugsにいくつか報告してます。
- Bug #18024: Ractor crashes when connections are closed in multiple Ractors - Ruby master - Ruby Issue Tracking System
- Bug #18130: Showing exceptions from Ractor on console causes errors - Ruby master - Ruby Issue Tracking System
- Feature #18137: A new method to check Proc is isolated or not - Ruby master - Ruby Issue Tracking System
- Feature #18139: Add a method to stop/kill a Ractor from outside - Ruby master - Ruby Issue Tracking System
あとそのへんについてのパッチとか。
- Make ErrorHighlight.formatter Ractor-ready by tagomoris · Pull Request #4769 · ruby/ruby · GitHub
- Update the error message to make users understand the Proc is unshareable by tagomoris · Pull Request #4771 · ruby/ruby · GitHub
- Add Proc#isolated? by tagomoris · Pull Request #4785 · ruby/ruby · GitHub
SEGVってるやつは見てみてはいるんだけど明確にこれかなというのはまだ分かってない*1んで、どうにかなるかなあ。あとRactorまわりの話はいじってると割と簡単にプロセスがビジー状態になったりSEGVったりするのが厳しい。
フィードバック
定数に値を入れるときにデフォルトでshareableだとマークする方法が欲しい、マジックコメントかな? という議論がスライド中であるんだけど、発表中にコメント欄で@ko1さんに教えてもらった。
# shareable_constant_value: true class Foo VALUE = {key: "value"} end
Ractorのドキュメントにも書いてあったので完全に自分の見落としでした。すいません。
(追記) …… と思ったら今日直されていたのでたぶんドキュメント読んだときはmagic commentだと思わずそのままスルーして忘れてたんだな。無実だったことにしよう。
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).
なんだよもー。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をよろしくお願いします!
Gradle経由でのテスト実行時、コンソールに失敗したテストケースの情報を出力する
`./gradlew test`とか`./gradlew build`とかしたときに失敗したテストの情報はこの`index.html`を見てね! っていうのがめんどくさくて、なんでデフォルトで失敗したテストの情報を出してくれないんだっけ、と思っている人、主に俺の問題を解決する。
ていうかデフォルトでそうしてくれればCIサービスの設定時にテスト結果の保存とかを個別にやらないと何が失敗したかもわかんなくなってて困る、みたいな状態にならなくてすむのにね。
調べてたら「とにかくコンソールに全部出したい!」みたいなのがいっぱいひっかかるけどそうじゃないんだよなー、そういうのはtoo muchなんだよ。
で、これ。
のうち一番下の回答(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
これでうまく動くようになった。めんどくさいですね。