patorashのブログ

方向性はまだない

CircleCIでresource_classをsmall、parallelismを増やして高速化・節約

副題:CircleCIでdocker-composeを使うのをやめた。

1年前くらいには、CircleCIでdocker-composeを動かす方法についての記事を書いてた。

patorash.hatenablog.com

しかし、これが遅い。まぁ自分のやり方が悪いというのはあったのだけれど。ライブラリのキャッシュを1コンテナでやってから、次のジョブで複数コンテナに配布するようにしたほうが無駄がなくていいかなと思って、そうしていたのだけれど、docker-volume内にあるからキャッシュするためには一旦取り出さなければならないし、キャッシュを反映するにはdocker-volume内にインポートしなければならず、これがすごく遅かった。

最近ではresource_classがmedium、parallelismが4で、parallel_testsを使っても40分くらいかかっていた。遅すぎる。

長いことプロダクトバックログに積んでいたのだけれど、いい加減耐えられなくなってきたのと、チーム内でもテストの費用が上がっていて、下げる施策を探らなければならないという状態だったので、手をつけた。うちのプロジェクトは実験台にするにはちょうどいいのだ。

何をしたか?

やりたかったのは、スタディストさんのところのブログにあったやつ。

このブログにあった通りで、docker-composeを使う限りは速くならない(金をかければ多少速くなるけれど、コストと見合わなさそう)。

executorをdockerに変更

CircleCIのexecutorをmachineからdockerに変えるところからやった。

開発ではdocker-composeを使い続けたいが、CIではやめたいので、ENV['CI']があればという条件を加えていく。 例えば、Elasticsearchの接続条件をいじる場合はこんな感じ。

Elasticsearch::Model.client = case
                              when Rails.env.development?
                                Elasticsearch::Client.new(host: 'elasticsearch:9200/', log: true)
                              when Rails.env.test?
                                if ENV['CI']
                                  Elasticsearch::Client.new(host: 'localhost:9200/')
                                else
                                  Elasticsearch::Client.new(host: 'elasticsearch:9200/')
                                end
                              else
                                raise 'SEARCHBOX_URL not found.' unless ENV['SEARCHBOX_URL']
                                Elasticsearch::Client.new(host: ENV['SEARCHBOX_URL'], http: { port: 443, scheme: 'https' })
                              end

似たような感じで、どんどんif ENC['CI']を付けていったら、テストは動くようにはなったが、Elasticsearchが落ちるようになった。

落ちているElasticsearchを動かす

Elasticsearchはエラーコード137を出した落ちてた。つまりはOut Of Memoryなので、ES_JAVA_OPTS: -Xms256m -Xmx256mとか付けてみたり、増やしてみたりもしたが、どうにも不安定。machineの頃はCPU 2つ、メモリ7.5GBだったが、dockerになるとCPU 2つ、メモリ4GBになってるのを思い出した(デフォルトのmediumの場合)。

Configuring CircleCI - CircleCI

parallel_testsの並列数を4にしているのがマズいのかも…と思い、2に減らしたところ、Elasticsearchは安定して動くようになった。この時点でまだ落ちるテストはあったものの、24分程度に終わるようになった。

assets:precompileの結果をキャッシュ

webpackerを使っていると、env RAILS_ENV=test bin/rails webpacker:compileをしてからでないとテストが実行できなかったのだが、こいつが遅い。2分くらいかかる。事前準備のジョブの時点で、assets:precompileを行うようにして、./public/pack-testディレクトリをキャッシュするようにした。しかしJSファイルやライブラリの更新などがあった場合はキャッシュを破棄したいので、JS系ファイルのハッシュ値を集めたテキストファイルのハッシュ値を使うようにした。

やり方に関しては、この記事を参考にさせてもらった。

md5sum でディレクトリ単位のチェックサム計算等 - clock-up-blog

これをCircleCIのコマンドにしたら、こんな感じ。

commands:
  restore_packs_test:
    steps:
      - run:
          name: JavaScript Checksum
          command: |
            find app/javascript -type f -exec md5sum {} \; | sort -k 2 > javascript_checksums.txt
            find config/webpack -type f -exec md5sum {} \; | sort -k 2 >> javascript_checksums.txt
            md5sum package.json >> javascript_checksums.txt
            md5sum yarn.lock >> javascript_checksums.txt
            md5sum postcss.config.js >> javascript_checksums.txt
            md5sum babel.config.js >> javascript_checksums.txt
            md5sum .browserslistrc >> javascript_checksums.txt
            md5sum config/webpacker.yml >> javascript_checksums.txt
      - run:
          name: cat javascript_checksums.txt
          command: |
            cat javascript_checksums.txt
      - restore_cache:
          name: Restore ./public/packs-test
          key: packs_test-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "javascript_checksums.txt" }}

  save_packs_test:
    steps:
      - save_cache:
          key: packs_test-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "javascript_checksums.txt" }}
          paths:
            - ./public/packs-test
          when: always

jobs:
  generate_cache:
    executor: default
    parallelism: 1
    steps:
      # 色々あるけど省略
      - restore_packs_test
      - run:
          name: assets:precompile
          command: |
            if [ ! -d ./public/packs-test ]; then
              bin/rails assets:precompile
            fi
      - save_packs_test
      # 続く

これにより、JS系の変更がない場合はテストが2分近く短縮されるようになった。この時点で22分(とはいえ、テストはまだ落ちてたので本来のスピードではない)

resource_classをsmallに変更

落ちる原因が掴みきれずにいたのだが、ふと思い出して対応できた。

patorash.hatenablog.com

docker-composeの頃はdocker imageの時点で対応済みだったが、今はそうではないので、対応する処理を追加したらテストも通るようになった。

最初にこれの変更の時間を測っておけばよかったのだが、なかなか気づかずにとりあえずresource_classをsmallにするのをやりたかったので先にやってしまっていた。

resource_classをsmallにすると、CPUが1つ、メモリが2GBになる。しかし、使用クレジットは5になる(mediumは10)。 最初のほうで参考にしたスタディストのブログにもあったけれど、Railsのテストは大体I/Oが遅いので、マシンパワーが貧弱でも台数が多い方が速度が上がりそうだなと思っていた。

そこで、resource_classをsmallにして、parallelismを4から倍の8に増やした。これで、1分あたりのクレジット使用量は変わらない。

Elasticsearchが落ちる

するとまたElasticsearchが落ち始めた…。メモリが2GBになったせいか…。parallel_testsの並列数を1にしてみたら、安定した。しかし、もうそれはparallelではない!でも一応parallel_tests経由でテストを実行したところ、全部通った(他にもちょこちょこ直してはいたが)。これで、22分だった。全部通るようにはなったけれど、速度はあんまり変わらず。

あと、色々とelasticsearchのイメージの環境変数を設定していたので、それを晒しておく。不要な物もあるかもしれない…。

- image: patorash/elasticsearch-kuromoji:7.9.1
  environment:
    node.name: es01
    cluster.name: es-docker-cluster
# メモリがカツカツなのでスワップを有効にしたいのでコメントアウト
#     bootstrap.memory_lock: true
    bootstrap.system_call_filter: false
    ES_JAVA_OPTS: -Xms256m -Xmx256m
    TZ: /usr/share/zoneinfo/Asia/Tokyo
    transport.host: localhost
    network.host: 127.0.0.1
    http.port: 9200
    xpack.security.enabled: false
    discovery.type: single-node
    mem_limit: 256m
    memswap_limit: 1g

parallel_testsをやめる

parallel_testsの並列数を1にしてしまったので、もう並列じゃないし、外そうと思って、直接knapsack_pro経由でrspecを呼び出すように修正した。 parallel_testsを起動するオーバーヘッドがなくなる分、多少は速くなるだろうけれど、まぁ誤差の範囲だろうなぁとタカを括っていたら、かなり速くなった。17分!🚀

knapsack_proをやめてCircleCIのsplit-by=timingに戻したら、knapsack_proの代金を浮かせることができるから、久々にtimingに戻してみるかーと思って実験してみたけれど、案の定、22分〜24分かかるようになってしまったので、knapsack_proを使うように戻した。knapsack_proよくできてんな…😇😇😇

knapsack_proをご存知ない方はこちらの過去記事をどうぞ。

patorash.hatenablog.com

まとめ

executorでmachineを使ってのdocker-composeを使ったテストはあまり速度が出なかったのだが、dockerに戻したらかなり速くなった。

また、resource_classをsmallにしてparallelismを2倍にしたほうがトータルでは高速化できた。前処理時間が全部のコンテナにかかるので、どこかで頭打ちになるとは思うが、大量にメモリを消費するような処理がない場合は、デフォルトのmediumからsmallに変えてparallelismを倍にするだけで高速化出来そう。

そして、CIにおいて高速化=節約に繋がる。40分かかっていたのが17分になったので、半分以上の高速化で、その分コストカット💵できた。CircleCIを使っているRailsプロジェクトであれば、resource_classを下げて並列数を増やすのが得策と思われる。早速社内でも横展開していく!

WSLでLocalhostForwardingが効かない場合は高速スタートアップをオフにしよう

タイトルで全てを語ってしまいましたが、これです。

WSL2でRailsアプリの動作確認をしようとbin/rails sを実行後、 http://localhost:3000 にアクセスしたのですが、何故か表示されず…。pumaも起動していますが、ポートフォワーディングに失敗しているようです。.wslconfigには、LocalhostForwarding=Trueを設定してあるのに…。

ググったところ、close済みのissueにたどり着きました。

github.com

このコメントの途中に、「高速スタートアップをオフにしろ」と書いてありました。やり方は以下のサイトにありますが、英語です。

https://www.tenforums.com/tutorials/4189-turn-off-fast-startup-windows-10-a.html

高速スタートアップをオフにする方法

まず、コントロールパネルを開きます。検索から「コントロールパネル」と書けば出てきますからクリックしましょう。因みに私はタスクバーを上に持ってくる派です。(Macと同じようにしたい)

f:id:patorash:20210115022209p:plain

コントロールパネルを開いたら、システムとセキュリティをクリックします。

f:id:patorash:20210115022400p:plain

電源オプションの、電源ボタンの動作の変更をクリックします。

f:id:patorash:20210115022514p:plain

シャットダウン設定の、「高速スタートアップを有効にする(推奨)」にチェックが入っていますが、このままだと変更できないので、「現在利用可能ではない設定を変更します」をクリックします。

f:id:patorash:20210115022644p:plain

「高速スタートアップを有効にする(推奨)」のチェックを外して、変更の保存をクリックします。

f:id:patorash:20210115022917p:plain

これで完了です。PCを再起動してから、WSL2のUbuntuを起動し、rails sをしたところ、ちゃんと表示されました🎉

f:id:patorash:20210115023359p:plain

よかったよかった。

AWS SDK for Rubyでminio上のバケットを削除するときのTips

自分のための備忘録です。

seed-fuを使ってデータ投入していたのだが、データを作り直そうと思ってseed-fuを再び実行したところ、minioのバケットを作るところでコケた。原因は、既にバケットがあったからだった。

require 'aws-sdk-s3'

storage_yml = YAML.safe_load(ERB.new(File.read(Rails.root.join('config', 'storage.yml'))).result).fetch('minio')
bucket_name = storage_yml.delete('bucket')

# 既にバケットがある場合はNGだった…
Aws::S3::Client.new(
  access_key_id: storage_yml['access_key_id'],
  secret_access_key: storage_yml['secret_access_key'],
  region: storage_yml['region'],
  endpoint: storage_yml['endpoint'],
  force_path_style: storage_yml['force_path_style']
).tap { |s3_client| s3_client.create_bucket(bucket: bucket_name) }

なので、AWS SDK for Rubyを使ってバケットを削除しようとしたのだが、またコケた…。バケットにファイルが残っていたらバケットは消せないらしい。ファイルごと消せるかと思いきや、現時点ではそんなオプションはなかった。

# 省略
).tap do |s3_client|
  # バケットにファイルがある場合は削除できず…
  if s3_client.list_buckets.buckets.any? { |bucket| bucket.name == bucket_name }
    s3_client.delete_bucket(bucket: bucket_name)
  end
  s3_client.create_bucket(bucket: bucket_name)
end

なので!バケットにあるファイルを全部削除してからバケットを削除しようとしたのだが、またコケた!

# 省略
).tap do |s3_client|
  if s3_client.list_buckets.buckets.any? { |bucket| bucket.name == bucket_name }
    # バケット内のファイルを削除
    s3_client.list_objects(bucket: bucket_name).contents.each do |object|
      s3_client.delete_object(bucket: bucket_name, key: object.key)
    end
    # 何故かまだバケットが削除できない!?
    s3_client.delete_bucket(bucket: bucket_name)
  end
  s3_client.create_bucket(bucket: bucket_name)
end

削除できない原因

原因はシンプルで、まだバケットにファイルが残っていたからだった。list_objectsメソッドで取得できるオブジェクトの数の上限が1,000だったので、1,000個以上ファイルがバケットにある場合は消しきれない。ということは、ファイルを全部消すまでループするようにしなければならない。

削除できるように直す

最初の実装

シンプルにこうした。

# 省略
).tap do |s3_client|
  if s3_client.list_buckets.buckets.any? { |bucket| bucket.name == bucket_name }
    # コンテンツが有る限り削除し続ける
    while s3_client.list_objects(bucket: bucket_name).contents.present?
      s3_client.list_objects(bucket: bucket_name).contents.each do |object|
        s3_client.delete_object(bucket: bucket_name, key: object.key)
      end
    end
    s3_client.delete_bucket(bucket: bucket_name)
  end
  s3_client.create_bucket(bucket: bucket_name)
end

しかし、これだとファイルが大量にある場合はminioに都度ファイルの有無を確認する通信が発生するので、ちょっとダサいなと感じた。

リファクタリング

begin .. end whileを使って書き直した。これならば、minioに通信する回数は最小限となって効率がよい。

# 省略
).tap do |s3_client|
  if s3_client.list_buckets.buckets.any? { |bucket| bucket.name == bucket_name }
    contents = nil
    begin
      contents&.each do |object|
        s3_client.delete_object(bucket: bucket_name, key: object.key)
      end
      contents = s3_client.list_objects(bucket: bucket_name).contents
    end while contents.present?
    s3_client.delete_bucket(bucket: bucket_name)
  end
  s3_client.create_bucket(bucket: bucket_name)
end

まとめ

  • バケットを消すにはファイルを全削除しなければならない
  • list_objectsメソッドは1,000件しか取れないので注意
  • AWS CLIだったらファイルごとバケットを削除可能らしい(同僚に教えてもらった)

バケットを削除するか空にする - Amazon Simple Storage Service

ちなみにminioでAWS CLIを使う時の情報はこちら

MinIO | AWS CLI with MinIO - Cookbook/Recipe

PowerAutomateで毎月第2水曜日にTeamsで通知するやつを作った。

まだちゃんと動くか検証できてないですけど、これをやったので、その実装の話を書いておこうと思う。まぁメモです。

PowerAutomateの繰り返しは、しょぼい

Teamsの会議の予定は、繰り返しでカスタムを選択すると、毎月第3水曜日に会議を予約することができる。いいですね。

f:id:patorash:20210106132254p:plain
Teamsの予定表は第3水曜日の指定が可能

しかし、PowerAutomateの繰り返しは、それができない…。

f:id:patorash:20210106132437p:plain
PowerAutomateでは、第3水曜日とか指定できない

毎週水曜日とか、隔週水曜日とか3週間毎とかはできるんだけれど、第3水曜日とかそういう指定ができない。

f:id:patorash:20210106132611p:plain
できても、何週間毎とか…。

しかし、やりたい。

ロジックを考える

やりたいことは、「第3水曜日に勉強会を行うので、その1週間前に勉強会の開催の告知をしたい」なのだが、どうすればいいかわからなかったので、ググったら、良いヒントが。

[Power Automate] 月末(の最終営業日)にリマインダーを送るmatkjin8.wordpress.com

この記事の中で

[遅延]アクションを使うと、指定した期間フローを待機(一時停止)させることができる

というのがあり、これだ!!と思い、計算に入ります。

第2水曜日になりうる日付を抽出する

月初の1日がどの曜日になるかで決まります。

  • 日曜日だった場合、11日
  • 月曜日だった場合、10日
  • 火曜日だった場合、9日
  • 水曜日だった場合、8日
  • 木曜日だった場合、14日
  • 金曜日だった場合、13日
  • 土曜日だった場合、12日

という感じで考えると、8〜14日になります。そのため、PowerAutomateの起動日は毎月8日に設定します。

f:id:patorash:20210106140520p:plain
毎月8日の12:00に起動。

次に、何日待つかを扱う変数を初期化します。

f:id:patorash:20210106140811p:plain

次に、月初を取得して変数startDayofMonthに格納します。値には、式にして startOfMonth(convertFromUtc(utcNow(), 'Tokyo Standard Time')) を入れます。 f:id:patorash:20210106140854p:plain

次に、月初の曜日を取得して変数weekdayに格納します。値には、式にして、 dayOfWeek(variables('startDayOfMonth')) を入れます。

f:id:patorash:20210106141040p:plain

変数weekdayには、日曜日ならば0、月曜日ならば1という感じで、最大6までの整数が入ってます。では、何日待てばいいでしょうか?

月初の曜日 weekday 第2水曜日 何日待つか
日曜日 0 11日 3日
月曜日 1 10日 2日
火曜日 2 9日 1日
水曜日 3 8日 0日
木曜日 4 14日 6日
金曜日 5 13日 5日
土曜日 6 12日 4日

単純計算で出せるかと思ったけれど、なんかいいのが思いつかなくて、

  • 日〜水曜日までならば、 3 - weekday = 待つ日数
  • 木〜土曜日までならば、10 - weekday = 待つ日数

でいいかなと思ったので、条件を追加。

f:id:patorash:20210106142600p:plain

はいの場合は、3 - weekdayにするので、sub関数を使って変数daysToWaitに値を設定。式には、 sub(3, variables('weekday')) を入れます。

f:id:patorash:20210106142723p:plain

いいえの場合は、10 - weekdayにするので、sub関数を使って変数daysToWaitに値を設定。式には、 sub(10, variables('weekday')) を入れます。

f:id:patorash:20210106142829p:plain

これで何日待つかは決まったので、処理を遅延させます。変数daysToWaitの日数だけ遅延させます。

f:id:patorash:20210106142959p:plain

あとは、Teamsでメッセージを投稿するようにしました。

まだ動いてないのでなんとも言えませんが、今月でいえば13日になって通知が来たら成功です。

まとめ

遅延っていうやつは便利そう。

2020年を振り返る

はい、というわけで、今年も振り返っていこうと思います。去年のはこれ。

patorash.hatenablog.com

なんか、来年の目標は来年考えるとか書いてたけれど、それをブログにしてなかった。

2020年序盤

たしか、OpenID Connectの技術検証をしていたので、それをそのまま第22回Ruby, Ruby on Rails勉強会で発表したのだが、発表後に公開した資料についてOpenID Connectに詳しい方々から資料が間違っているというご指摘をいただき、直したりしていた。大変勉強になったのでやはり発表駆動勉強はしんどいけどよかった。

patorash.hatenablog.com

あと確か中国地方DB勉強会でLT2本やったかな…。まぁまぁ受けてた気がする。

その中でも仕事では後輩氏に1人でアプリを設計してもらって実装してもらうっていうのをやってて、そのコードレビューとかやっていた。4月頃には、OIDCのアプリは一旦検証完了となった。

この頃のプライベートでは、夫婦で4月から長男の通うところをどうするか?を話し合っていて、2019年の間に見学に行っていた療育施設に通わせるために、療育手帳をもらうための検査したり申請したり、特別児童扶養手当をもらうための手続きをしたりとかしていたと思う。この時点で長男は週3で2つの療育施設に通っていた。そこは2つとも送迎をしてくださるところだったので、その点は非常に助かっていた。しかし、結構家から遠かったり、小さい子だけを預かっているところではなかったので小学校等が長期休暇になったら大きな子たちが来るので、身の安全を確保できないので休んでほしい、と向こうからお願いされたりもしていた。

そのため、4月から妻が調べてくれた近隣で週5~6で行ける同世代のみのところに変更することにした。ただし、送迎はない…。そのため、次男がまだ幼いので妻には次男を見ていてほしかったので、私が送迎することにした。会社に相談して、変則的な勤務になることを許可してもらって、なんとか今のところはうまくいっている。会社には圧倒的感謝。ということで、4月からの私の勤務体系のデフォルトは、こう(現在も)。

時刻 行動
09:00 長男を園に送る
09:30 勤務開始
14:30 会社を出て迎えに行く
15:00 勤務再開
19:00 勤務終了

とはいえ、当然ながらこんなにうまくいくことはないので、時々間に合わなかったりしていた。

あと、長男氏が私の髪の毛をめちゃくちゃ引っ張って痛いので、物理的に対策するために坊主にした(今も坊主頭は継続中)。

5~8月あたり

在宅勤務はしつつも週に何日かは後輩氏と日程を合わせて出社するべしということになっていたので、週2くらいで出社して、あとは在宅ワークだったかな?後輩氏とペアプロをして教えたりしていたと思う。後輩氏と私は4月から特命を受けていて、私が監修のもと、後輩氏が実装するという形で静的サイトを作っていた。静的サイトなのでRailsではなく、Middlemanで作っていたのだが、そのセットアップは私がやった。webpackを使えるようにするのが結構大変だったと思う。RailsはWebpackerがあるから楽だ。

うちの会社は期末が8月で、来期の方針として配置転換が行われて私の担当プロジェクトから1人外れて後輩氏が入ることになったので、後輩氏が配属になるまでに面倒そうなところは片付けておこうと思い、Rails6へのバージョンアップと脱CoffeeScriptをしておいた。その頃の記事。

patorash.hatenablog.com

これで、CoffeeScriptを教えなければならないという手間を防ぐことができた。そしてその後、SprocketsもやめてWebpackerに移行した。これはかなり頑張った。これに関しては id:luccafort に公開してほしいと言われたので記事を書いた。

patorash.hatenablog.com

その頃に後輩氏と特命を受けていたサイトがリリース。弊社の30周年記念事業として、バッティングセンターが広島県福山市にオープンしたのですが、そのサイトを作ってました。

www.bpark.jp

オープンが決まっていたので、そこまでに必要なコンテンツのみ実装して、リリース後も実装という形で結局全部やるのに9月末くらいまでかかったような…。とはいえ、このサイトの実装のおかげで後輩氏のBootstrap力は、そこそこ上がりました。

あとは期末までに試験を受けようと思っていたので、OSS-DB Gold v2を受けたけれど、敢えなく不合格…。そのあたりはまとめた。まだ再試験受けていないので、どこかで受け直したい。

patorash.hatenablog.com

プライベートは、長男が園に通い始めて、色々できるようになってきた。靴を1人で履いたりとか。送迎の時に色々と園での様子を聞いたり、家での様子を伝えたりという感じ。通園前に比べると、以前の通っていたところより同世代の子が増えたので、色々な刺激を受けているみたいだった。音程だけの歌を歌い始めるようになったりした。次男もこの頃には語彙が急に増え始めた頃だったかと思う。こういってはなんだが、次男は喋り始めたのですごく安心した。

9~12月

プライベートなことから書くと、9月~10月は大変だった。長男が重度の虫歯になっていた。時々急に不機嫌になったり泣いたり、周囲の人に噛みついたりしていた。私も何度も噛まれた。発達障害児は過敏な子が多くて偏食だったり、歯磨きを猛烈に嫌がったりというのはあるのだけれど、うちの子もまさにそれで、全然奥歯を磨かせてくれてなかった。近所の歯科の検診が10月だったので、そこで診てもらおうかと思っていたのだが、情緒の不安定さが尋常ではないので、発達障害児を専門に診ている歯医者さんのチラシを園で見かけたのでそこに行ってみたら、かなり虫歯が進行していることが発覚した。

「治療が必要だけれど、長男くんは大暴れして普通の歯医者では無理だから、全身麻酔で歯の治療をするところを紹介します」という話になった。しかし、県内の病院は半年待ち、県外の病院でも1か月待ちということだった…。もう世間は新型コロナで県をまたいだ移動は控えるようにと言われまくっていたのだけれど、もう選択の余地はなく、隣県で治療することとした。治療自体は1ヶ月待ちだが、初診は早めに行っていきたいということで、9月頭くらいに病院に行った。

9月下旬に治療できるという話になったので、まぁ1ヶ月の辛抱じゃ、頑張れ長男!と思っていて、園でも「もうすぐ歯の治療ですね~」と言われてたら、予定日1週間前くらいになって、病院から「麻酔科医の都合が治療予定日にどうしても都合がつかなくなったので予定日から3週間延期させてほしい」と言われてしまった。一番大変なのは長男だけれど、私もかなりショックだった。とはいえ、麻酔科医がいないのでは治療できないので受け入れるしかなく、虫歯が悪化しないことをただただ祈るのみ…。甘いものを極力取らせない生活が続いたので、すごく不機嫌にもなるし、偏食もあるので栄養的に心配でもあった。

10月中旬にようやく予定日を迎え、全身麻酔で治療に成功。治療後も問題なかった。情緒が不安定になる頻度は格段に減り、噛み癖もだいぶ収まったので本当によかった(でも油断してたら噛まれたことがある)。その後は、歯医者に慣れることから始めようということで、2週間毎に通院して歯医者で歯磨きと歯のチェックをしている(つまりこの通院も私のタスクとして追加されたということだ…)

後は、園で予定されていた行事が新型コロナの影響でなくなり、代わりのイベントが準備されてて移動動物園が園に来てくれて、それに保護者として私が参加して長男と一緒に色んな動物とのふれあいや、園が準備してくれたアトラクションに参加したりなどをした。

そして発達障害児の育児書を数冊読んで、公園に行く回数を増やしたり、高タンパク質な食生活にできないか工夫したり、テレビ・スマホタブレット・電子玩具類を禁止するようにした。これは禁止する決断をさせてくれた本の話。

patorash.hatenablog.com

テレビは見なくなってもうちょっとで2か月になるだろうか。次男はアンパンマンのアニメが大好きだったので、「アンパンマン見たい」と最初はよく言われたけれど、「テレビが壊れちゃった」と言ってます(実際に調子は悪いんだけど…)。今は一緒に遊ぶ回数を増やしたり、本を読んだりするのを増やしたりとかしてます。テレビ消してから、次男の語彙力がどんどん良くなっているので、効果はありそうだけれど、長男はまだ喋らず…。でも昔より確実に人に興味を持つようになってきてる…ような…。

あとISUCONに初挑戦したんだった。

patorash.hatenablog.com

他の人のブログとかチェックまだできてないので年末年始にチェックしておこうかな。

あー、そして、思い出した。DBスペシャリストを受けましたが、落ちました。OSS-DB Gold v2も落ちたし、全然データベースに詳しくない!もっと詳しくなりたい!!

仕事の面では、スクラムっぽいことをやっていて、後輩氏に機能開発を一任して、私は環境整備や調整やサブシステムの機能改善などを行っていた。特にサブシステムに関しては、長いこと機能改善に取り組めておらず、手がけた開発者も別のプロジェクトに配置転換になってしまったため、私が改修した。要望対応と、パフォーマンス改善が目下の課題だったが、要望対応はもちろんのこと、パフォーマンスはかなり良くなった。メモリ不足のアラートがしょっちゅう上がっていたが、全く来なくなった。このサブシステムではVue.jsを使っていたので、多少Vue.jsが分かるようになった。 あとElasticesearchのバージョンアップやった。こういうのに取り組めるようになったのは、機能開発は後輩氏に任せる!と決めていたからこそ。

patorash.hatenablog.com

ただこの頃はもうほぼフルリモートになっていたので、後輩氏の進捗というか状態がどうなのか確認しづらくなっていた。朝と夕方に確認を取るようにしたり、詰まっているところがないかを確認するようにしたりなどするようにはしたが、なかなか大変だった。それまでは画面共有でコード見たりもしていたのだけれど、12月からはvscodeのliveshareを使ってリモートペアプロをするようにして指導した。だんだん慣れてきてくれたのか、最近は詰まったら早めに相談してくれるようになったので、それはよかったと思う。あと、私自身が単一障害点となっていたあたりについて、引継ぎというか共有ができたので、安心感ができた。

そしてこの1ヶ月くらいはまだ明かせないけれど、やりたかった社内イベントのために準備を進めている。あと1ヶ月くらいはかかるかもしれないけれど、公開していい状態になったらまた情報解禁したいと思う。

総括

総括すると、家族ファーストで色々と取り組んでいて、大変な一年であった。特に送り迎えが発生するので、迎えの時間帯では会議できないので関係者に調整してもらったりと、色々と迷惑をかけつつも協力してもらえて助かったなと思います。 良かった点は、フルコミットの開発メンバーを増やせたこと。まだまだ教育途中ではあるけれど、1年前に比べるとだいぶ成長したなぁと思う。自分がずっとやっていた定期メンテナンス作業を私以外の人ができるようになったのは、普通に嬉しい。

悪かった点は、やはり長男氏の虫歯にもっと早く気づけていれば…という申し訳なさ。あとはマネジメント的なこともやらないといかんのに、ちょっとそちらが薄いかもしれないなぁ…というところ。そのあたりの手法については id:tech-kazuhisa に相談していきたいと考えている。あとはオンライン勉強会へのアンテナの低さ…。インプットの少なさ…。

来年の目標は、

…来年考えます。

stimulus 2.0.0の進化が凄い件(サンプルコードあり)

仕事で、とあるRailsアプリを作っているのですが、そこでstimulusを採用していました。そうしたらちょうど少し前にstimulusのバージョン2.0.0がリリースされていました。このバージョンアップによって、かなり書きやすくなりました。

今までと何が違うのか?

今までは、コントローラーを指定してから、そのコントローラーのターゲットを指定するのが面倒でした。

before

<div data-controller="vote">
  <button type="button" data-target="vote.button">投票</button>
</div>

after

属性名だけでどのコントローラーのターゲットか分かるので読みやすくなりました。

<div data-controller="vote">
  <button type="button" data-vote-target="button">投票</button>
</div>

新しく追加された機能

大きく変わる機能が、Value APICSS Class APIです。

Value API

Value APIは、そのまんまですが、値を持つことができます。 stimulusの公式サイトのリファレンスから引用しますと、以下のような感じです。

html

<div data-controller="loader"
     data-loader-url-value="/messages">
</div>

JavaScript

import { Controller } from "stimulus"

export default class extends Controller {
  static values = { url: String }

  connect() {
    fetch(this.urlValue).then(/* … */)
  }
}

この例だと、fetchする先のURLをValue APIで変えることができる、という感じです。valueの指定は、Hashのキーにvalueの名前を、値に型を書きます。そうすると、読み込んだ際に型に沿った形にキャストしてくれます。

これだけだと、値を渡せるようになっただけ?という感じですが、それだけではありません!Value APIは、値が変わった時にコールバック関数が呼ばれます。 CodePenで、Value APIと、そのコールバックを使って、送信ボタンの状態制御をしてみました。

See the Pen stimulus v.2.0.0 sample(Value API) by patorash (@patorash) on CodePen.

stimulusのコントローラーでinitialize, connect, disconnectの3つのメソッドが自動でコールバックされます。今回はこれらも使って、

  1. connectのタイミングでコメント入力エリアにinputのイベントリスナを定義する。
  2. 入力されたら、handleEventメソッドでイベントを拾い、入力エリアに1文字以上あったら、enableValueをtrueに変更。0文字ならfalseに変更。
  3. enableValueの値が変わったらenableValueChangedメソッドがコールバックで呼ばれ、submitボタンのdisabledの状態が変わる!
  4. disconnectのタイミングでイベントリスナを解除する。

という仕組みになっています。valueはhtmlの要素の属性として値がある状態なので、戻るボタンなどで戻った時は再びconnectが呼ばれてちゃんと動く、というわけです。

私的には非常にわかりやすくていいなぁ!と思いました。

4時を過ぎたので、今日はこの辺りまでにして、続きのCSS Class APIはこの記事に追記していきます。

続きを書いていきます!

CSS Class API

CSS Class APIは、stimulusで使うCSSに名前をつけることができる機能です。 stimulusの公式サイトのリファレンスから引用しますと、以下のような感じです。

JavaScript

// controllers/search_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static classes = [ "loading" ]

  loadResults() {
    this.element.classList.add(this.loadingClass)

    fetch(/* … */)
  }
}

html

<form data-controller="search"
      data-search-loading-class="search--busy">
  <input data-action="search#loadResults">
</form>

アクションのloadResultsメソッドを呼び出したら、searchコントローラーの紐づいたformのcss.search--busyを追加します。

以前までは、クラス名をJavaScriptにハードコーディングしなければいけませんでした。

// 以前のstimulus
// controllers/search_controller.js
import { Controller } from "stimulus"

export default class extends Controller {

  loadResults() {
    this.element.classList.add('search--busy') // => ハードコーディング

    fetch(/* … */)
  }
}

これだと、CSSのクラス名が変わった場合にstimulusのコードに修正が必要になっていました。しかし、CSS Class APIの登場により、HTML側の修正だけでよくなりました。

CSS Class APIが活用できる具体的なシーン

例えばBootstrapのバージョンが上がったときを例にしましょう。

未だにBootstrap3を使い続けている環境があるとします(私の担当プロジェクトですが…)。そこでは、要素を隠すためのCSS Class名は、.hiddenです。

Bootstrap3を使っているフォーム

<div data-controller="comment"
    data-comment-close-class="hidden">
  <button type="button"
      data-action="comment#openForm">コメントする場合は押してください</button>
  <form class="hidden" onSubmit="return false"
      data-comment-target="form">
    <input type="text" name="content">
    <input type="submit" value="送信">
    <button type="button"
        data-action="comment#closeForm">キャンセル</button>
  </form>
</div>

JavaScript

// controllers/comment_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "form" ]
  static classes = [ "close" ]

  openForm() {
    this.formTarget.classList.remove(this.closeClass)
  }

  closeForm() {
    this.formTarget.classList.add(this.closeClass)
  }
}

これをBootstrap4にバージョンアップします。 data-comment-close-class="d-none"と、<form class="d-none">に修正します。

<div data-controller="comment"
    data-comment-close-class="d-none">
  <button type="button"
      data-action="comment#openForm">コメントする場合は押してください</button>
  <form class="d-none" onSubmit="return false"
      data-comment-target="form">
    <input type="text" name="content">
    <input type="submit" value="送信">
    <button type="button"
        data-action="comment#closeForm">キャンセル</button>
  </form>
</div>

stimulus側のコードは、変更することは何もありません!

このように、CSS Frameworkに依存しない形でJavaScriptのコーディングが可能になりました。

まとめ

stimulusが登場した時点でも、すごい!とは思っていたのですが、バージョン2になって、さらに使いやすくなりました!Value APIの登場で、他のMVVMライブラリのcomputedみたいなことがやりやすくなりましたし、CSS Class APIの登場で、CSS Frameworkに依存しないコーディングができるようになりました。

最近のフロントエンドとバックエンドを疎結合にしていく流れも、もちろんいいとは思いますが、少々(かなり?)複雑です。Railsで簡単なものを簡単に実装しやすいという点では、stimulusは最高のパートナーになってくれると思います。

simple_formを使ってActiveStorageのダイレクトアップロードを行う

引き続き、ActiveStorageネタです。 simple_formを使ってActiveStorageのdirect_uploadの設定を書くと、うまくいきません…。

= simple_form_for(@post) do |f|
  / 略
  .form-inputs
    / 動くけれどダイレクトアップロードにはならない…
    = f.input :images, as: :file, direct_upload: true, input_html: { multiple: true, accept: 'image/*' }

どうもdirect_upload: trueの部分が関係ないものとして無視されてしまうようです。 しかしもう既にActiveStorageが出てから随分経っているので、なんかいい方法あるだろうとググったら、stack overflowにありました。

stackoverflow.com

simple_formのカスタムインプットクラスを作る方法です。app/inputs/direct_upload_file_input.rbを作って、以下のコードを貼ります。

# frozen_string_literal: true

class DirectUploadFileInput < SimpleForm::Inputs::FileInput
  def input_html_options
    super.merge({ direct_upload: true })
  end
end

そして、ビューを修正します。

= simple_form_for(@post) do |f|
  / 略
  .form-inputs
    / ダイレクトアップロードになった!
    = f.input :images, as: :direct_upload_file, input_html: { multiple: true, accept: 'image/*' }

エラーメッセージも綺麗に出るようになったので、こっちのほうがいいですね。