OSSチャレンジPart4(敗北)

目的

学んだことを忘れないようにメモ!

間違えたことを書いている可能性あるので、注意してください!

あと、全部教えてもらいながらやったことを書いてるだけなので、自分の実力ではないです。

やってること

Apache Arrow の C++に、Unicode正規化の機能を追加したい。

github.com

頂いたレビューに関してメモ

outputにそのまま正規化後の文字列を挿入するべき

This is definitely non-performant as it allocates a heap area for every input string. Ideally, we should write directly into the output buffer output.

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

utf8proc_map は、内部で以下のようにメモリを確保してる

utf8proc_int32_t *buffer;
buffer = (utf8proc_int32_t *) malloc(((utf8proc_size_t)result) * sizeof(utf8proc_int32_t) + 1);

そのため、このutf8proc_mapを使っちゃうと、arrowの方でoutputの領域を確保してるのに、それに追加でbufferの領域を確保するという無駄なことをしている。

なので、utf8proc_mapを使うのでなく、utf8proc_decompose_customutf8proc_reencode を使うのが良さそう。

こんな感じになるとのことだった。

const auto n_chars = utf8proc_decompose(input, input_string_ncodeunits, reinterpret_cast<utf8proc_int32_t*>(output), output_string_ncodeunits, option);
if (n_chars < 0) return output_string_ncodeunits;

const auto n_codepoints = utf8proc_normalize_utf32(reinterpret_cast<utf8proc_int32_t*>(output), n_chars, option);
if (n_codepoints < 0) return output_string_ncodeunits;

auto codepoints = reinterpret_cast<utf8proc_int32_t*>(output);
for (utf8proc_ssize_t i = 0; i < n_codepoints; ++i) {
  auto codepoint = codepoints[i];
  output = arrow::util::UTF8Encode(output, codepoint);
}

utf8proc_reencodeを使わない

utf8proc_reencode() has a warning message regarding its size and IIUC it creates a null-terminating string which you are writing into the output. AFAIK, Arrow stores strings contiguously without null-terminations.

Cの文字列は、最後に\0を置いて、区切りを表現するそうで、ヌル終端文字列と呼ばれる。

utf8proc_reencode は、下のように文字列の最後に 0 を入れてる。

utf8proc_ssize_t rpos, wpos = 0;
for (rpos = 0; rpos < length; rpos++) {
     uc = buffer[rpos];
     wpos += utf8proc_encode_char(uc, ((utf8proc_uint8_t *)buffer) + wpos);
}
((utf8proc_uint8_t *)buffer)[wpos] = 0;

しかし、Arrowで使う文字列は、ヌル終端文字列を扱う想定でないので、このutf8proc_reencodeを使ってはならない。

utf8proc_reencodeは、コードポイントからutf-8文字に変更しているので、arrow::util::UTF8Encodeを利用できるとのこと。

最初のメモリ割り当ての最適化

we're calling the normalization function twice: once here to compute the output size, once in Transform to actually output the data. It seems wasteful (especially as I don't think normalization is especially fast).

scalar_string.ccでの文字列の処理は、主にMaxCodeunitsTransformで行われる。

MaxCodeunitsは、あらかじめ確保が必要なバイト数を計算する関数で、Transformは実際に与えられた文字列を処理後のものに変換する処理を行う。

今回、自分は最初MaxCodeunits時に、どのくらいのbyte数の確保を必要かをutf8proc_decomposeを一度実行して計算しておき、十分な領域を確保してから、Transformで実際にutf8proc_decomposeutf8proc_normalize_utf32をもう一度実行し、次は実際に文字列処理を行なうようにしていた。

この方法だと、確実に2回utf8proc_decomposeを実行する必要がある。

だが、実際に多くの文字列はUnicode正規化を行なっても、必要なサイズが変わるわけでない。だから、与えられた文字数が分かれば、処理に必要な領域は大体分かる。

一方で、アラビア文字などは、正規化すると、「ﷺ」が 「صلى الله عليه وسلم」になるように、文字数が大幅に変わる。

これに対応するためには、以下の方法をとるのが良さそう。

  1. MaxCodeunitsでは、与えられた文字数から、ざっくり必要そうな値を計算しとく。

  2. Transformで、一度utf8proc_decomposeを実行する。

  3. utf8proc_decomposeは、あらかじめ確保した領域が足りないと、If the number of written codepoints would be bigger than bufsize, the required buffer size is returned, while the buffer will be overwritten with undefined data.必要な領域を返してくれるので、足りないだけメモリ領域を再確保する。

  4. 確保した後に、もう一度utf8proc_decomposeを実行する。

こうすれば、必要な時だけutf8proc_decomposeが実行されるので、効率が良い。

ちなみに、1でざっくり必要な領域を計算する際は、多めに出した方が良いことが多いらしい。メモリの再確保は、コストがかかるらしく、再確保が走るぐらいなら、多めにメモリを確保した方が良いことが多いとのこと。

答えの実装

今回のPRについて最後まで実装出来なかった。

巻き取ろうかと、commiterの方にコメント頂いたので、お願いした。実力不足が否めない...

答えはこちら! github.com

大きくは、須藤さんに教えてもらった実装方針と一緒だったが、既存のExec系の処理を継承するのでなく、新しく作成していた。

感想

開発に関して重要なことを学べた。

まず、答えが誰もわかってないこと。本音を言うと、コミッターの人はほとんど答えをわかってるんだと思ってた。

けど、実際は、どうやったらメモリ効率が良いかとか、パフォーマンスが良いかとかを、コミッター同士で議論しながら、何が一番最適かを話し合うフローを経て、リリースまで持っていってるんだと分かった。

そしてその議論に入るためには、自分の知識は全く足りていないことも分かった。

今の自分は完全に下のブログでいう、使う人になっているが、今回の経験で少し作る人の世界が見えた気がして、すごくいい経験だった。

作る人と使う人 - kawasin73のブログ

メモリアロケーションに対する罪悪感 - kawasin73のブログ

OSSチャレンジPart3

目的

学んだことを忘れないようにメモ!

間違えたことを書いている可能性あるので、注意してください!

あと、全部教えてもらいながらやったことを書いてるだけなので、自分の実力ではないです。

やってること

Apache Arrow の C++に、Unicode正規化の機能を追加したい。

github.com

レビューを頂いた部分の修正の際に調べたことをメモ

修正項目

新たにヒープ領域を割り当てないようにした方が良いのではないか。

ARROW-14205: [C++] Add unicode normalization to scalar string by okadakk · Pull Request #11298 · apache/arrow · GitHub

前提知識

ヒープ領域: 動的に確保と解放を繰り返せるメモリ領域のこと

ヒープメモリ確保には3種類の関数と、解放する1種類の関数がある。

#include <stdlib.h>

// 確保
void * malloc(size_t size);
void * calloc(size_t num, size_t size);
void * realloc(void * mem, size_t size);

// 解放
void free(void * mem);
  • malloc
    • 確保したいメモリサイズを引数として受け取り、確保されたヒープメモリのポインタが返される
    • メモリ確保に失敗したらNullが返ってくる
  • calloc
    • 確保したい個数と、一個あたりのメモリサイズを引数とする以外は、mallocと一緒。
    • 1点だけ異なり、確保されたヒープ領域の値が「0」の値にされている。(0初期化というらしい)
  • realloc
    • mallocなどで確保したメモリ領域をサイズを変えて再確保できる。
    • メモリ確保に失敗したらNullが返るので、memory = malloc(memory, 500)は避けること。後でメモリ領域を解放できなくなる。
    • 再割り当てというより、そのメモリ領域にあるデータをコピーして、新しいメモリ領域を確保する感じなので、注意!

元のjuliaのコードを読む

utf8proc_map_custom

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

  1. utf8proc_decompose_custom を実行して、与えられた文字を正規化した時の文字数を取得する。
  2. その文字数の数だけint32のメモリ領域を確保して、その領域のポインタを取得する。
  3. utf8proc_decompose_custom を実行して、確保した領域に正規化した文字を保存する。
  4. utf8proc_reencode を実行して、
  5. reencodeの結果、変更された文字数に応じて、メモリ領域をサイズを変えて再確保する。
  6. この関数の第3引数に渡されたポインタが指すメモリ領域に、再確保したメモリ領域の値をコピーする。
  7. 正規化された文字数を返り値として処理終了!!

utf8proc_decompose_custom

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

ざっくりいうと正規分解及びに互換分解をしている

utf8proc_reencode

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

  1. utf8proc_normalize_utf32 を実行して、正規合成を行い代入して、文字数を取得する。
  2. 文字数分ループを回し、その位置にある文字を取得して、1文字づつ utf8proc_encode_char を実行し、UTF-8にencodeした文字を代入&文字数を取得する。
  3. bufferの最後に、null終端ということで、0を入れて、合計の文字数を返す。

やりたいこと

分解と合成のロジックはjuliaのライブラリを使って良さそう。

  1. 与えられたメモリアドレスが示す場所に入っている文字列に対して、utf8proc_decompose_customして、UTF-32でencodeした場合の文字数を取得し、メモリ領域を再確保する。
  2. 確保したメモリ領域に対して、与えられた文字列をutf8proc_decompose_customにして、今度は結果をメモリ領域に代入する。
  3. 分解した文字列を、utf8proc_reencode を行い、合成(合成だから、文字数が増えることはないので、メモリ領域が増えないはず)して、UTF-8に変換してメモリ領域に代入して、その文字数を返す。
  4. メモリ領域をutf-8でencodeした文字 * 文字数分だけ、再確保する。
  5. UTF-8でencodeした時の文字数を返す。(メモリ領域は渡したものをそのまま利用してるので、返す必要はない)

OSSチャレンジPart2

目的

学んだことを忘れないようにメモ!

Part1はこっち ちょっとだけOSSデビューした - okadak1990’s blog

やったこと

Apache Arrow の C++に、Unicode正規化の機能を追加した。

github.com

こちらのコードに、正規化のロジックを追加する。

arrow/scalar_string.cc at master · apache/arrow · GitHub

Apache Arrow C++UTF-8文字列の処理にJulia由来のutf8procというライブラリーを使ってるので、こちらを利用する。

utf8proc/utf8proc.h at master · JuliaStrings/utf8proc · GitHub

Unicode正規化って?

Unicode文字って同じ文字でも、違うコードを持ってることがある。

例えば、「が」という文字は、「が(U+304C)」 or 「か(U+304B) と ゛(U+3099)」という2つの表し方がある。

これらがバラバラだと、文字比較できなくなるので、正規化して比較しようって時に使う。

4種類の比較方法がある。

NFD/NFKDとNFC/NFKCは、例えば「が」の場合、NFD/NFKDだと「か(U+304B) と ゛(U+3099)」になって、NFC/NFKCだと「が(U+304C)」になる。

NFC/NFD と NFKC/NFKDの違いは、例えば「ガ」の場合、NFC/NFDだと見た目上「ガ」で、 NFKC/NFKDだと見た目上「ガ」になる。

詳しくは下のリンクを見てください。

詰まったこと

開発したい場所のテストが実行できない

Building Arrow C++ — Apache Arrow v5.0.0

このURL通りに実行したら、テスト動くところまではすぐだったけど、今回直したい、「scalar_string_test」が実行されなかった。

原因は、上の方にある手順はあくまで最小限のbuildで必要なもので、optionが足りなかった。

-DARROW_COMPUTE=ON を追加すればできた。

Building Arrow C++ — Apache Arrow v5.0.0

今回は、scalar_string_test だけ実行したかったので、開発中は、テストコマンドに-R arrow-compute-scalar-test を追加すると、labelで絞り込みしてテストができた。

繰り返しで一文字ずつ入れようとしてみた

最初は下のコードのように、一文字づつunicode正規化を行って、その結果を返すようにしてみた。

struct Utf8NfkcTransform : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {

    for (const uint8_t* c = input; c < input + input_string_ncodeunits; ++c) {
      uint8_t* o = utf8proc_NFKC(c);
      *output++ = *o;
    }

    return input_string_ncodeunits;
  }
};

このコードが間違えてる理由は次の通り

  • c には、1byteが入ってるけど、utf-8文字は1文字(コードポイントというらしい)が1byteとは限らない。だから、「①」(のポインター)がutf8proc_NFKCに渡されるのでなく、「①」を表す文字の最初の1byteのポインターが渡される。
  • 正規化した文字が、2文字になる可能性があるので、このようにoutputに1文字づつ追加するという考えは間違えている。(そもそも文字とbyte数の概念から意識できてない)
  • utf8proc_NFKCは、NULL終端文字列を受け取るようになっている。最初の一文字のpointerを受け取ると、そこからNULL終端文字があるまでの文字列を変換してくれる。今回は、変換したい文字列の区切りにNULL終端文字を使ってないので、今回のケースでは使えない。

このロジックは、「1234」みたいな1byte文字だと想定通りの挙動を示すけど、それ以外だと上手く行かない。

正規化後の文字のbyte数がわからない

utf8proc_NFKCでなく、utf8proc_mapを使うようにとアドバイスをもらったので書き換えてみた。

utf8proc/utf8proc.c at master · JuliaStrings/utf8proc · GitHub

struct Utf8NfkcTransform : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {

    uint8_t* output_start = output;

    // ここの型が上手く解決できず、static_castというやり方を教えてもらってできた...
    utf8proc_option_t options = static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_COMPAT);
    utf8proc_map(input, input_string_ncodeunits, &output, options);

    return output - output_start;
  }
};

このコードが間違えてる理由は次の通り

  • utf8proc_mapを通すと、outputのアドレスが変わっていた。そのため、output - output_start というponterのアドレスの引き算では、byte数が上手く取得できなかった。(「1435477920」みたいな全然違う値が返ってきた。)
  • 最初のoutputが持つアドレスの場所に変換後の文字を入れなければいけないのに、utf8proc_mapを通すことで、outputという変数が指し示すアドレスの位置が変わってしまい、想定したアドレスの場所に値を入れられなかった。

結局正解を教えてもらった...

struct Utf8NfTransformBase : public StringTransformBase {
  int64_t Transform(const uint8_t* input, int64_t input_string_ncodeunits,
                    uint8_t* output) {
    utf8proc_uint8_t* transformed = nullptr;
 auto options = static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_COMPAT);
    auto output_string_ncodeunits =
        utf8proc_map(input, input_string_ncodeunits, &transformed, options);
    std::memcpy(output, transformed, output_string_ncodeunits);
    free(transformed);

    return output_string_ncodeunits;
  }
};

学んだこと

  • そもそもutf8_proc_map はsize_t(文字列のbyte数)を返してくれるので、それをそのまま返り値にすればよかった...。output - output_start をしてみたが、0が返ってきた。多分最初は、++output = *oみたいにpointerの指し示す位置を文字数分進めてたから、上手く動いたのだと思われる。
  • 一時的な変数に正規化の結果を入れて、それをoutputにcopyする処理が必要。こうすることで、outputが指し示す元のアドレスに対して、値を格納できる。

感想

今までponterとかふわっと理解したつもりだったけど、全然理解してなかったんだなと思った。

ほんとに学びが多い。すごく楽しい。

ちょっとだけOSSデビューした

目的

OSS活動に興味があったが、全然できてなかった。

ちょうど、会社でイベントがあったので、参加してみた!

OSS Gateミートアップ for Red Data Tools by Speee - connpass

学んだことをメモする。

kou さんに感謝...

やったこと

Red dataset にデータセット追加

github.com

他の修正見ながらコピペでやってみた。いかに自分が、変数名とかコメントとか、英語とかそこまで意識してないんだなーというのが分かった...

そしてpushするときの緊張感が違った...

通常業務でも甘えずに、人様に見せても恥ずかしくないコードを書かないといけない。

red-arrow の slice が、hashを受け取るようにする

github.com

table.slice(1..8)

みたいなIDでの絞り込みは既にできてたけど、

table.slice(count: 1..8)

のように、カラムを指定して、絞り込みはできてなかったのでそれを追加した。

ハマったこと(学んだこと)

解決できなくて、kou さんに教えてもらった

begin less range を使おうとすると、エラーが起きる

こんな感じに書けるとあったんだが、自分の環境ではできなかった。

{ count: ..8 }

最終的に、ruby2.7から入った機能だと知り、なんなくできた...

Railsとか、Nuxtとかのフレームワークの修正はある程度追ってたけど、ruby の version up を全然追ってなくてダメだなと気づけた...

Rangeの最後の値を確認する方法がわからない

{ count: 8.. }

としたときに、この値がendlessかどうかを判断したかった。

Range.lastを使うと、cannot get the last element of endless range こういうエラーが起きて詰まった。

結局、Range.endを使えば良いだけであった。

Range#end (Ruby 3.0.0 リファレンスマニュアル)

このドキュメントを見た感じ、endとlastが同じように見えたから、phpでよくある、名前が違うだけで中身は一緒かなと思ったけど、そんなことはなかった

C言語読めないけど、雰囲気的にこっちがendで、

ruby/range.c at master · ruby/ruby · GitHub

こっちがlast(引数がない時はendと同じ挙動だけど、引数渡すと挙動が変わる)

ruby/range.c at master · ruby/ruby · GitHub

ドキュメントにちゃんと違うものとして、書いてあるから、何故そう思い込んだのか自分が不思議...

複数条件での絞り込み方法がわからない

ここのメソッドを修正するのが今回の目的だった。

arrow/table.rb at master · okadakk/arrow · GitHub

もともと、以下のように書けるようになっていたので、この機能を使えばいいことは分かった。

@table.slice do |slicer|
  slicer.count >= 8
end

このメソッドは、 こういう動きをしているから、これをそのまま書くようにしたら、一応動いた。

slicers = []
slicer = Slicer.new(table)
slicers << (slicer[count] >= 8)

# 省略: slicersを用いて、filterする

コード的には、ここで、「[]」メソッドを実行して、(知識では知ってたけど、こういうコードをしっかり読んだの初めてで楽しかった)

arrow/slicer.rb at master · okadakk/arrow · GitHub

ここに到達して、Table Classの「[]」メソッドを実行して、

arrow/column-containable.rb at master · okadakk/arrow · GitHub

Columnインスタンスが生成されて、これがColumnConditionに渡される。

arrow/column.rb at master · okadakk/arrow · GitHub

そして、そこで「>=」メソッドが実行されて、GreaterEqualCondition が返ってくる。

arrow/slicer.rb at master · okadakk/arrow · GitHub

このGreaterEqualCondition Classは、Condition Classを継承していて、「&」メソッドを実行すると、二つのCondition Classを同時にfilterで利用できる。

arrow/slicer.rb at master · okadakk/arrow · GitHub

よって、複数条件での絞り込みができるって形だった。下のように書くとできる。

slicers = []
conditions = []
slicer = Slicer.new(table)
conditions << (slicer[count] >= 8)
conditions << (slicer[name] == 'okadak')
slicers << conditions.inject(:&)

# 省略: slicersを用いて、filterする

こんな感じのコードを一から自分で書けるようになりたい!

おまけ: そもそも実行環境が作れない

$ cd ruby/red-arrow
$ bundle install
$ bundle exec rake test

で、実行できるはずなんだけど、できなかった。

下のようにすると解決できた。

# homebrewで、標準でインストールできる依存ライブラリのVersionが古いので、最新をとってくる
brew install apache-arrow --head
brew install apache-arrow-glib --head

# packageのpath指定が間違えていた?? ちょっとよく分かってないからまた調べたい。
export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@1.1/lib/pkgconfig"

ネットで調べたら、みんなばんばんエラーメッセージを質問してて、それにkouさんが答えてて、そのIssueを読んで解決ができた。

こういう風に詰まったところを、Issueにあげてもらえるのは助かるなーと思った。自分もそういう風に慣れていきたい。

感想

綺麗なコードとは何かみたいなのが少しづつ分かってる感覚がある。

あと、他のライブラリがどうやってできてるかみたいな想像が少しできるようになった。

まだまだ能力不足であり、学びが多い。もっと勉強する。

技術選定的なものについて現状思ってることメモ

目的

現時点でどういった形の開発環境が良いと思っているかを一旦メモする。

いろいろ考えたけど、自分でも突っ込みどころあるので、もっといい技術選定できるように勉強しないといけない。

toC向けの自社開発(もちろんそれに伴いtoB向け機能とかも作ったりしましたが)の経験しかないのでそのイメージで書いてます。

1人

フロント

React & TS。 Vueも好きだが、今はReactの方がTSとの親和性の点と情報量の多さで優位だと思う。

もし、自分一人だったら、SSRやSSG不要の時でも、Nextjsを入れて、ドキュメントコストを減らしたい。

デプロイ先は、Firebase Hosting、認証もFirebase Authnication、測定は、GA4やFirebaseを入れてBigQuery連携させておく。

記事サイトの場合はindexまでの速度が大事になってくると思うので、ISRがすぐ使えるVercel で最初は楽するのが良さそう。

そうでなければ、SPAで最初は良いと思われる。search consoleでindex数に悪影響があったりしたらISRやSSRを検討するにした方が楽。

ただ、ある程度スケールしたら、GCPに行った方が管理が楽そう。

Next.jsアプリをVercelからGCPに移行した話

SSGをしたことあるけど、ページが増えれば増えるほど、レスポンスタイムが増えるし、新規ページ追加する時もいろいろ考えないといけなくて、めちゃくちゃ苦しんだ過去があるので、あまり頑張る価値を感じない。将来的にも、CDNでキャッシュすれば変わらないですし。(セキュリティ上のリスクはついて回りますが...)

アプリ開発は、現時点だとFlutterが良さそう。試した所、細かい所はハマったが、大枠問題なくリリースまで持って行けた。

Flutter webに関しては、WEBとアプリで提供する体験って違うと考えているし、まずWEBでいろいろ試してみてから、ユーザーインタビューとかの結果を元に、アプリ用のUXを考慮してリリースっていう方が初期はよいと思うので、あまり使うイメージが湧かない。

API

Auroraか、CloudSQLをデータストアとして使いたい。

Firestoreも簡単でとても良いが、途中で上手く設計できず、RDSに利用に変更を2回ぐらいしたので、今のスキルでスピード優先なら最初からRDSだと思う。

APIに関しては、Next js のAPI routeに書くのが良いのかもしれないが、やっぱり以下の点から、最初はRailsが個人的には好き。

  • いろいろなGemがあって、めちゃくちゃ楽。
  • Rails Way に乗っ取ればいいから、めちゃくちゃ楽。
  • Railsはやったことあります! って人が市場に多いから、コストを下げて採用しやすい。

デプロイ先は、ECS(GCPだとGKE??)が設定慣れてるので好み。

アーキテクチャ

Rails Way

APIの連携方法

RPCでいいのではないかと思う。Validationや型付けは頑張るしかないが...

あるいは、Nodeでしか使ったことないが、GraphQLも良いと思うけど、型付けを手で頑張るのとそこまで変わらない感覚だったのと、学習コストがかかるので、内部のAPIには積極的に使わなくても良いかと思った。

監視

CloudWatch logs(GCPだとなんだろう?) とSentry のアラートをSlack通知するレベルで十分だと思う。

開発環境

DBはクラウド上に1つ用意。 - Local開発用に、自分でDB準備。 - 本番用に、本番用DB作成。

テストは本番DBに繋いでしたりする。 この時点だと、同期的にビジネスサイドとの確認がしやすいはずだから、ばんばん確認してリリースしていく。

CI, CD

最近は Github Actions にはまってる。 ブランチにpushしたら、lint & Deploy が走るようにする。

テスト

複雑なビジネスロジック部分は開発する時に楽なので書くけど、積極的には書く必要はないと思う。 テストの保守が大変なので...

レポジトリ構成

基本はレポジトリ分けて開発したい。

web1、flutter1、API1 といった感じ。

データ収集

BigQuery にデータをずっと入れておく。

となると、RDSの置き場所は、GCPがやはり分析に便利...

その他

AmplifyもAnalytics機能があるけど、どうなんだろう??

2人 ~

開発環境整備や、E2Eテストの整備が必要になってきそう。

開発環境

DBはクラウド上に2つ用意。

  • Local開発用には、DockerFileで自動で開発用DBが構築されるように準備。
  • 本番 & Staging環境用に、本番用DB作成
  • 社内テスト用に、確認用DB作成

ブランチは、mainブランチ、stagingブランチ、qaブランチを作成し、それぞれのブランチ毎に、フロントとAPIサーバーを作成する。

確認用DBの中身は、とりあえず手で同期するようにしておく。

自動テスト

フロント側に関しては、staging、QAブランチにアクセス時に、E2Eテストが走るようにしておくと、気が楽。

アプリはテストをしたことないのでわからないです。

API側の単体テストは、開発時には便利なので書いてますし、ビジネスロジックと呼ばれてる部分は書きたい。が、コントローラーのテストとかは変更が多くて辛いので、開発時に書いたとしても、後でテスト対象から外してる。

社内用ツール

社内の人がRDSにCRUDできるためのツールが求められてくる。

Google アカウントを発行しておけば、認証はGmailで完結するのでそれを使うと楽そう。

社内ツール作成は他チームと関わることができるし、目に見えて感謝されるので、新しく入った方で経験が浅い方にチャレンジしてもらえると、すごく勉強になるんじゃないかなと思うので、その人の技術レベルで使用言語を決めるといいのかなと思う。

インフラ

早めにコード管理しておきたい。

10人 ~

フロントもサーバーもインフラも全部みんなでやっていくのが厳しくなる時期だと思う。

自社開発系でこの規模だと、「サービスが好きな人」が採用しやすいと思ってて、その人たちがいかにボトルネックなく、そして不具合リスクを減らしつつ、サービス開発を進めていけるかが鍵だと考える。

一方で、技術力の高いエンジニアを、データの整合性担保や、難易度の高い業務ロジックに集中してもらうことで、業務量を集中させ、高い効率を発揮してもらえると思ってる。

そのため、以下のようなのが良いのかなと今思っている。

API

BFF的なAPI層と、BackendAPI層の二つに分ける。

BFF「的な」と書いたのは、一般的に行わないDBアクセスまで、そのAPI層に行わせるべきだと思うため。

BFF的なAPI層の責務
  • BackendAPI層の呼び出し
  • WEB、ネイティブアプリと分けて、それぞれが必要なレスポンスだけを返すようにする。
  • APIの認証
  • RDSの読み込み処理

レポジトリは二つに分けて、片方はWEBフロントエンジニア、もう一方はアプリエンジニアが中心で開発するのが良さそう。

自分としては TS & prisma & express で開発するイメージ。

BackendAPI層の責務
  • BFF的なAPI層の責務じゃないもの全部
  • RDSへの書き込み処理
  • 会計処理などの複雑な業務ロジック
  • バッチ処理

こちらは引き続きRailsで作ったAPIのまま使っていく。

BFF的なAPI層から、BackendAPI層を呼び出すライブラリの作成もする必要がある。

この辺りを、技術力の高いエンジニア集団で保守していく。この時に、業務ロジックを極力ドメイン毎に分けていくことで、今後のマイクロサービス化に備える。

大きくは、一つのデータストアに対して、一つのAPIを作るイメージでいる。

APIの連携方法

引き続きrpc的な方法のままで良いかと思う。

BFF的なAPIの言語をTSに絞って、型付きのライブラリを提供すると開発しやすかった。

開発環境

DEV DBの自動同期が必要になりそう。

こんなイメージ

開発環境のデータをできるだけ本番に近づける - クックパッド開発者ブログ

20人 ~

未知の領域

ここに関して知見が皆無。

フロント

サービスが複数個出来上がっていることが多いが、その場合も特に技術スタックを変更する必要もなさそう。

API

BackendAPI層の肥大化が生まれてると思う。

そのため、いくつかの主要なビジネスロジックは、マイクロサービス化していく必要がありそう。

ドメインに分けて、データストアを分離をしつつ、読み込み、書き込み共にマイクロサービス側から提供する。

トランザクションの保障や、ドキュメント化、k8sの検討などいろいろ考えることがありそう。

APIの連携方法

新規のマイクロサービスでは、gRPCを利用するのが良さそう。言語としては、Go言語を使ってる所が多そうと感じて、親和性が高そう。

一方で、BFF層を一般的な責務に抑えて、データ取得を比較的自由にできるAPIを提供したいのならば、GraphQLが良さそうだが、この規模ではまだ必要なさそう。

終わり

20人以上の規模の会社になると、どうなるんだろう...

想像で書いたけど全然違ってそう...

ActiveRecordのAfter〇〇ではまったこと

やりたかったこと

hogehoge.update!

after_save do
  // 外部サービスをAPIアクセス
end

みたいな特定のテーブル更新時に、別サービスを呼び出すみたいなことをやってみたかった。

何が問題だったか

テーブル更新したはずなのに、外部サービスで SELECT しても更新前のデータしか取得できなかった...

原因

BEGIN

before_save
update ...
after_save

COMMIT

という流れでafter_save段階では、まだcommitされてない状態になっていた...

commit の後には、after_commit を使うべきらしい。

結局、model内で他のサービス呼ぶのは積極的には使いたくないなと思って、hookで行うのは辞めたが、はまったのでメモ。

ちゃんとドキュメントには書いてある

Active Record コールバック - Railsガイド

その他

before_save とかは、changed? で今まで行けたけど、after_save では、saved_change_to_attribute? としないと変更を取れなかった

多重トランザクション

この前、Railsの多重トランザクション調べた時に知った、update! はトランザクションを生成するってことを覚えておけば、すぐ気づけた...

【翻訳】ActiveRecordにおける、ネストしたトランザクションの落とし穴 - Qiita

エンジニアのマネジメントについて調べたことと思ってること

目的

いろんな本とかサイトとかで勉強したことをまとめる

そして、自分がどういう風にするべきと今思ってるかもまとめる

本文

1on1

何よりも1on1というか対話が大事であり、ちゃんと時間を取って話すことが大事。

オープンドアポリシーというのがあるが、これだと仲良い人はもっと仲良くなって、もともと疎遠な人はもっとそうなる。

目的は、関係性の構築というのもあるが、コアバリューを伝え続けることも大事。

何を会社がしたいと思っていて、このような方向で進みたい。その時に、本人がやりたいことやできることと、どのようにマッチさせて、やる気をもって事業に貢献してもらうかが、スキルの必要なところらしい。

あと、良いフィードバックはとても大事で、日頃からするのが良いらしい。みんなの前で褒めるのも大事そう。(だが、自分はみんなの前で褒められるの茶番に思えて、すごく苦手でいたたまれなくなるので、そこは人によりそう。)

逆に悪いフィードバックは1対1で行うのが良いとのこと。

メモもプライベートのことを除いて、ちゃんと残しておくのが良い

情報の透明性

マネジメントをすると、確実にマネージャー側が情報を多く持ち、情報格差が生まれる。

これはチームにとって健全ではないし、そういったチームは上手く回らない模様。

かといって、MTGで全てを共有しようとしたり、議事録を共有するとかも、結局人によってはみなくて、上手くいかない模様。

スライドでみたのは、マネージャーが毎日、日報を書くのがいいらしい。もちろんチームメンバーもしてくれるとなお良い。

どう考えたかとか、どういう話をしたかとかちゃんと書くことで、情報格差が薄まるとのこと。

役割の明確化

JobDescription を明確化した方がいい

採用の時に役立つし、チーム外からの理解に繋がる。

よく誰に頼んだらいいか分からないみたいなことを、エンジニア以外から言われるが、役割を明確化して発信したら明らかに減った。けど、時間がたつと何故か特定の人に偏りがちになっちゃうから、ちゃんと運用は必要。

あと、新しい人(でコミュニケーションが嫌いではない人)の場合は、社内ツールの改善をお願いすると、社内の人とのコミュニケーションが行われ、業務知識獲得と社内の人に感謝されることによる、モチベーション向上や一体感が狙えて、とてもいい仕事だなと感じた。

朝会

チームのコミュニケーション量は大事。特にコロナでそれは切に感じる。

ただ、プロジェクトチームで行うべきか、職能別チームで行うべきかが、いまだによくわかってない。

組織体制

ここは勉強が不足してるし、経験も不足してる。

会社によっても適切なものは違うとのことなので、いろんな実例を学んでいきたい。

少なくとも自分は、仕事上の上下関係は無駄だと思う。

なので、階層構造は効率の良い情報伝達のために維持しながらも、開発者もマネージャーもCTOも全うするべき役割が違うだけで、偉いみたいなよく分からない概念はないという組織を目指したい。やはり情報格差をいかに減らすかが鍵だと思っている。

採用

大きな問題で難しい...もっと勉強と実践しないと...

技術力

評価をする立場にもなるわけだから、メンバーに信頼されるように、技術についていけるようにしないといけない。 かといって、開発を引き続きして、タスクが滞るのもダメなのでバランスが大事。

思ってること

やっぱりチームメンバーが成長すれば、自分の知りたいことも増えるし、今までより使える手段が増えるし、一度しっかり経験したら次に自分をマネジメントしてくれる人のやりやすいように動けそうだし、ますます本腰を入れてやっていきたいと思った。

参考

エンジニアのためのマネジメントキャリアパス エンジニア組織論への招待 エンジニアとしてワクワクし続けるためのエンジニアリングマネージャという役割分担 - BASEプロダクトチームブログ