patorashのブログ

方向性はまだない

Elasticsearchへの保存を非同期ではなく同期で行う方法

環境情報

DBに保存したらElasticsearchも更新されたい

Railsプロジェクトで、データベースへの保存を行ったら同時にElasticsearchへの反映も行いたい、というケースが出てきました。

Elasticsearchの利用には、elasticsearch-railsを使っているのですが、データの反映は簡単でした。

# Elasticsearchへの保存が失敗してもロールバックしたいので…
class Foo < ApplicationRecord
  def apply!
    ApplicationRecord.transaction do
      # なんやかんかして
      self.applied = true
      self.save!
      self.__elasticsearch__.index_document # => これでElasticsearchにも反映される
    end
  end
end

画面側で確認しても問題なし👍Elasticsearchを使う検索フォームで検索してもOK。

問題発生(テストが落ちる)

しかし、RSpecでテストを書いたら、落ちる…😥

require 'rails_helper'

RSpec.describe Foo, type: :model do
  describe '#apply!' do
    subject { Foo.create!(applied: false) }
    it '反映されること' do
      expect {
        subject.apply!
      }.to change { subject.applied? }.from(false).to(true)
      definition = Elasticsearch::DSL::Search.search
      definition.query do
        must do
          term applied: true
        end
      end
      result = Foo.__elasticsearch__.search(definition).records
      expect(result).to be_present # => ❌存在しないと言われる👻
    end
  end
end

調査

なんとなく、もしかして非同期更新か?と思い、雑に2秒待つようにしたら、テストが通るように!

require 'rails_helper'

RSpec.describe Foo, type: :model do
  describe '#apply!' do
    subject { Foo.create!(applied: false) }
    it '反映されること' do
      expect {
        subject.apply!
      }.to change { subject.applied? }.from(false).to(true)
      sleep(2) # => 2秒待つ…
      definition = Elasticsearch::DSL::Search.search
      definition.query do
        must do
          term applied: true
        end
      end
      result = Foo.__elasticsearch__.search(definition).records
      expect(result).to be_present # => 🟢通った👍
    end
  end
end

とりあえずテストは通るようになったけれど、テストにsleepとか入れたくありません。どうにか同期的にデータ更新できる方法はないものか…とコードを読んでいきます。

https://github.com/elastic/elasticsearch-rails/blob/main/elasticsearch-model/lib/elasticsearch/model/indexing.rb

Yardを読むと、@param options [Hash] Optional arguments for passing to the clientと書いてあるけれど、いやそのHashにどう設定すればいいかがわからん!😠

ググったら、zennの記事がヒットしました。

zenn.dev

コードの感じからすると、Kotlinかなんかだろうか…?とりあえずrefreshオプションがあることがわかりました。

www.elastic.co

解決方法

というわけで、該当箇所を修正。

# apply!メソッドの一部
self.__elasticsearch__.index_document(refresh: true) # => Elasticsearchに同期的に反映

これで、sleepなしでもRSpecが通るようになりました👍

テストの時だけ同期をとるようにしたい

しかし、なぜ非同期でElasticsearch側に反映されるかというと、パフォーマンス向上のためですね。つまり、毎回同期をとるように更新していたら、遅くなります。ループでFoo#apply!メソッドを呼ぶこともあるので、それは避けたい…。 そもそも、普段は即時同期が取れていなければならないほどシビアな条件でもありません。単に、テストの時だけ同期が取れていればいいわけです。

そこで、該当箇所を以下のように修正。

# apply!メソッドの一部
self.__elasticsearch__.index_document(refresh: Rails.env.test?) # => テストの時だけElasticsearchに同期的に反映

これで、テストの時だけElasticsearchへの反映が同期的になりました👏👏👏

「現場で役立つシステム設計の原則」読書会 vol.10

第10回の感想です。前回の感想はこちら。

patorash.hatenablog.com

会社のブログのレポートはこちら。

tech.rhizome-e.com

最後なのでみんな参加できるようにしようということで日程調整してから行ったので少しずれ込みました。というても5月前半だったので、私がブログ書くのが遅かっただけですね、はい。色々バタバタしてまして。もう次の読書会が始まってますし(汗)

10章 オブジェクト指向設計の学び方と教え方

  • オブジェクト指向の説明は意味不明というところ…。まぁそこまで意味不明とは思っていなかったんだけれど、ドメインオブジェクトを設計していくという考え方にはあまりなっていなかった…というか値オブジェクトを作っていくというところがあんまりなかった。受託開発の場合、リファクタリングをする機会などほぼ与えられないため、嬉しさはよくわかっていなかった。
  • オブジェクト思考の学び方、リファクタリグ。こっちのほうがいいと思う。
  • 極端なコーディング規則を作ると、1人ならいいのかもしれないけれど、チームでやるとイライラしそう。
  • 本の紹介があった。リファクタリング Rubyエディションは会社に置いてあるはず…。ただ、情報が古いので現在だと微妙かも。過去に有志で読書会やったけれど(当時の参加者は自分以外は転職してるが)、なんでやねんってよく言ってた。しかし今見返すと「なるほど」となるかもしれない…。
  • オブジェクト指向の考え方を理解する。のところ。この本に書かれていることで十分な気もするんだけれど。エヴァンスのドメイン駆動設計はよく紹介されている。Kindle積読してある…。書かれたのが2000年代初頭なので、それを念頭において読まなければならないと、texta.fmでt-wadaさんが言ってた。本に書いてある通りの実装方法が現代においては冗長だったりするらしい。難しい本なので、この本こそ、読書会で取り組んだほうがいいかもしれないと思った。

私はPodcastGoogle podcastで聴いてるので、リンク貼っておきます。

podcasts.google.com

雑感

エリック・エヴァンスのドメイン駆動設計は他の先輩社員にもオススメされていたという話が他チームの参加者からあった。まだちゃんと読んだことないので、いつか読んでみたいとは思っている。これも読書会とかでやらないと多分読み切れない気がする…。 どうやってオブジェクト指向を学ぶか?というテーマでは、「やっぱりリファクタリングしながらうまく構造化できたりして身についていくもんじゃないですかねー?」という感じだった。 とあるメンバーが、「実際に動いている業務コードがあるのだから、それを触れていくのが一番手っ取り早い。なので整地日(リファクタリングデー)を活用してやっていけたらいいと思うんですけど、整地日にみんなが取り組むのがドキュメントの整備とかが多くて、コードの整地がほとんど進んでない」と話してたと思う。まぁドキュメントの整備も重要なので、いいとは思うんだけれど、ずっとドキュメントの整備をしてたら進まないというか、ドキュメントの整備は整地日じゃなくて普通の業務の日に取り組むべきじゃないか?と思った。まぁそれはそう伝えたような…(ちょっと前なので記憶が曖昧)

この本を読んでる最中にリファクタリングしましたか?という質問には、あまりいい返事がなかったので、やや悲しい。私は得られたことがかなりあって、ガシガシとリファクタリングしていってたので、みんなも取り組んでるかなと思ってたので、返答が意外だった。しかし、後日、同じチームの後輩氏と一緒に取り組んだリファクタリングデーでは、後輩氏のコードはちゃんと本の内容を意識したコードになってるなと思った。あとやっぱりコードレビューのときの指摘の説明が簡単で済むのが助かる。開発もまた、ドメイン知識の共有なのだと思う。

読んですぐ手を動かす瞬発力を身につけさせるにはどうしたらいいのだろうか?

読書会全体を通して

今回から、事前に読んできてもらって、感想や疑問・他の人の感想に対する疑問・アドバイス、そこから派生して業務の雑談(自然とテーマは本の内容に近くなる)などを1時間~1時間半くらいでやってました。そして、テックブログも始まったので、せっかくだからこういうことやってるよとテックブログで発信していこうと話して、交代しながらアウトプットもしました。

なんとなく感じているモヤモヤを相談する場にもなっていたので、やった甲斐はあったかなと思います。解釈の違いに気づけたりもしました。それはやっぱり1人で読んでいるとわからない点ですね。

twitterでは、テックブログの記事について、著者の @masuda220 さんにRTしていただいたりしたので、そのときはアクセスが増えてました👍ありがとうございます。

オブジェクト指向言語でどう開発していけばいいかを網羅した素晴らしい書籍だと思います。新人教育など、ある程度経ってから、また読書会を開いていけたらいいなと思いました。

Herokuへのデプロイが成功しているのにgitのエラーが発生する件に対応した

git pushでHerokuにデプロイしたら、デプロイは成功しているのに、最後にエラーメッセージが出てしまう現象に遭遇しました。

$ git push heroku release-yyyymmdd:master
# なんやかんやあって

remote: Waiting for release.... done.
fatal: protocol error: bad line length character: fata
error: error in sideband demultiplexer
error: failed to push some refs to 'https://git.heroku.com/xxxxxxxxxx.git'

いや、release..... done.ってなってるやないか。なんで最後エラーに…。このせいでデプロイの自動処理がここで止まってしまう…。

fatal: protocol error: bad line length characterググると、.bashrcでechoしてないか?とか、色々出てくるんですが、確認したところ、そういうこともしていませんでした。

エラーメッセージと、herokuで検索したら、それっぽいのをstackoverflowで発見!

stackoverflow.com

これのコメントにある通りに対応したら、直りました👍

$ heroku plugins:install heroku-repo
$ heroku repo:reset -a xxxxxxxxxx
$ git commit --allow-empty -m "Reset repo"
$ git push heroku release-yyyymmdd:master

Herokuのリポジトリをリセットしたら直ったってことは、なんか壊れていたってことなんかなぁ〜🤔と思いつつ、これで終わりとしたいと思います。

「現場で役立つシステム設計の原則」読書会 vol.9

読書会自体はGW前にやっていたのですが、諸々あって書くのが遅くなってしまった…。

第9回の感想です。前回の感想はこちら。

patorash.hatenablog.com

会社のブログのレポートはこちら。

tech.rhizome-e.com

9章 オブジェクト指向開発のプロセス

  • 開発の基本はV字モデルではあるが、そのサイクルが異なる。今はアジャイルで開発することが主流なので、同じチームで全部を担当することが普通になっている。大体うちでもそういうプロセスでやってる。
  • しかし、ここに書いてある通り、分析・設計にほとんど時間をかけずにとにかくプログラミングするという流れはある。特に、Railsでは顕著ではないかと思う。ちょっと規模が大きくなるとコードの見通しが急速に悪化するというのは、見に覚えがある。
  • ドメインモデルを中心にしたソフトウェアの考え方。従来の開発の仕方は、今いるメンバーは殆ど経験したことがないんじゃなかろうか?オブジェクト指向らしい開発の進め方について。設計した人が開発もするのでドキュメントの確認も減るし大変効率が良い。
  • 口頭でのやりとりをラフスケッチとしてホワイトボードに起こしていくというのは、写真を撮るだけでいいしよくやっていた。うちの会社だとスマホで写真を撮って共有、がやりにくいが…。リモートワークが主流になった現在だと、そういうツールを使いこなせばいいのかもしれないが、あんまりやってない。Miroとか?一応MS Whiteboardはある。ペン付きのPCならいいんだけれど、Macだとそのあたり厳しい。
  • ドキュメントに関して。データベースのテーブル名/カラム名とコメントは書くようにしている。I18nのデータを元にコメントにしていく仕組みを準備している。gemにしてもいいかもしれない。
  • 全体を俯瞰するドキュメント。これはインセプションデッキと役割が似ている。時々見返したり、見直したりする必要がある。
  • 技術方式のドキュメントもソースコードで表現できるというところ。Infrastructure as Codeの話。現代においては、この辺りはもう全てコード化しておくべきもの。学ばなければならないものが多い反面、自動化できることの恩恵は大きい。
  • 分析と設計が一体になった開発のやり方をマネジメントする
  • 受託開発の行うときに発注する側、受注する側で気を付けるべき点等が書かれているのはよい。準委任契約のほうがいいという点は、そうだろうなぁと思う。
  • 進捗の判断…チケット単位でドメインオブジェクトを作るように組んでいけば、この通りに進捗管理できるんだろうか?ドメインオブジェクトの組み合わせて機能を作っていけるので、例え機能が未実装でも、ドメインオブジェクト作成の進捗があれば、進んでいるとみなせるということか。その発想はなかった。
  • 品質保証について。ドメインオブジェクト単位で独立しているから、テストしやすく、ミスがあったとしても簡単に修正できる。
  • 他の章にも書いてあったが、プログラミングスキルとドメイン知識の両方を備えていかないと、優秀なエンジニアとは言えないと思う。業務知識の勉強会を行うことも大事と思われるので、そういうことも計画していったほうがよいかもしれない。

雑感

読書会のメンバーは自社製品の開発をしているので、受託開発する際の契約に関するところは、「うーん?」という感じであった。設計と、設計レビューにもっと時間を割いて、熟練者の知見を吸収していったほうがいいんでないか?ということを話したように思う。大して設計せずにすぐ作り込んでから、実装に対するレビューをしてしまうとレビューの視点もブレるし、実装者もあんまり変更したくないもんだから設計がおかしいまま実現しようとしてしまいがちだと思う。わかったふりをせずに、貪欲に質問したほうがいい。みんな答えてくれるから。

「どうやって業務知識をつけていったんですか?」という質問があった。「扱っている製品が違うので、一概には言えないんだけれど、定例会のタイミングとかで疑問をぶつけたり、お客さんからの反応を聞き出したり、おすすめされた記事を読んだり等はしていたよ」という話をしたり等。先輩社員にドメイン知識をどうやってつけたのか?を聞いてみたら、ヒントがあるのではないか?と話したら「そういえばあんまり聞いたことがなかった」ということだったので、聞いてみよう!ということに。

RSpecでブロック引数をmockで置き換える

RSpecで、ブロック引数に渡されたオブジェクトが、とあるメソッドを実装しているかどうかによって処理を変える件のテストをしたかったのだけれど、どうやったらいいかわからなかったので調べました。

テストしたい処理

class Hoge < ApplicationRecord
  def foo
    # 略
  end
end

class Fuga

  # @param [Class] model_class ApplicationRecordを継承したクラス
  def initialize(model_class)
    @model_class = model_class
  end

  def execute!
    @model_class.find_each do |record|
      # fooメソッドが実装されていたらfooを実行したい
      value = if record.respond_to?(:foo)
                record.foo
              else
                record.bar
              end
      # 略
    end
  end
end

これで、model_class#fooがあれば、それが実行されていることを確認したかった。

やったこと

【NG】stubでnewの戻り値をmockに置き換える

当初はHoge#newメソッドを置き換えておいたらええんちゃうか?と思ったけれど、ダメでした。binding.pryfind_each内のループに仕込んでも到達しなかったので、やめました。

RSpec.describe Fuga do
  context 'fooメソッドがある場合' do
    let(:record) { instance_double(Hoge) }

    before do
      allow(Hoge).to receive(:new).and_return(record) # NG
      # record.respond_to?(:foo) の戻り値をtrueにstubしておく
      allow(record).to receive(:respond_to).with(:foo).and_return(true)
    end

    it 'Hoge#fooが呼ばれること' do
      expect(record).to receive(:foo)
      described_class.new(Hoge).execute!
    end
  end
end

【OK】stubでfind_eachのブロック引数をmockに置き換える

and_yieldを使えば、ブロック引数を置き換えることができることを知りました。

RSpec.describe Fuga do
  context 'ApplicationRecord#fooメソッドがある場合' do
    let(:record) { instance_double(Hoge) }

    before do
      allow(Hoge).to receive(:find_each).and_yield(record) # OK
      # record.respond_to?(:foo) の戻り値をtrueにstubしておく
      allow(record).to receive(:respond_to).with(:foo).and_return(true)
    end

    it 'fooメソッドが呼ばれること' do
      expect(record).to receive(:foo)
      described_class.new(Hoge).execute!
    end
  end
end

これで無事にHoge#fooが呼ばれることを確認することに成功!✌️

参考にしたページ

Rspecで環境変数をstubしていたのが動かなくなった件(解決済)

小ネタです。rubocopのバージョンアップを行ったら、新しいルールで怒られが発生しました。

 C: [Correctable] Style/FetchEnvVar: Use ENV.fetch('FOO') or ENV.fetch('FOO', nil) instead of ENV['FOO'].

Correctableなので、bundle exec rubocop -Aで自動修正を行ったところ、ENV.fetch('FOO', nil)になりました👍

これでテストを実行したところ、思わぬエラーが発生😵

環境変数をstubしているテストが落ちる

環境変数によって、とある項目をチェックするかどうかを切り替えている処理がありまして、そのテストのために以下のようにしていました。

context '環境変数BARに0が設定されている場合' do
  before do
    allow(ENV).to receive(:[]).and_call_original
    allow(ENV).to receive(:[]).with('BAR').and_return('0')
  end

  it 'チェックされないこと' do
    # テストケース
  end
end

※この書き方は、以下の記事を参考にしています。

qiita.com

該当箇所のコードは、元々はこんな感じ。

if (ENV['BAR'] || '1') != '0'
  # チェックする
end

これが、rubocopのAutoCorrectによって、こうなりました。(カッコが多かったため、手でも修正しています)

if ENV.fetch('BAR', '1') != '0'
  # チェックする
end

これが全くstubされないように…😢

原因

これはstubの定義がENV['BAR']の場合にしか対応していないためです。この変更に対応するには、ENV#fetchの結果をstubしなければなりません。

解決方法

環境変数の参照の仕方のstubをfetch経由に変更します。

context '環境変数BARに0が設定されている場合' do
  before do
    allow(ENV).to receive(:fetch).and_call_original
    allow(ENV).to receive(:fetch).with('BAR', '1').and_return('0')
  end

  # 略
end

肝は、with('BAR', '1')のところで、with('BAR')だとstubされませんでした。きちんとデフォルト値まで渡さなければなりません。

まとめ

環境変数をちゃんとstubしているのに何故!?と5分くらい考えてしまいましたが、当然っちゃあ当然です😅 環境変数をfetch経由で受け取る修正をした際は、テストのstubも見直しましょう。

「現場で役立つシステム設計の原則」読書会 vol.8

第8回の感想です。前回の感想はこちら。

patorash.hatenablog.com

会社のブログのレポートはこちら。

tech.rhizome-e.com

8章 アプリケーション間の連携

  • 1つのサイトであっても、様々なシステムが連携しあっている。
  • アプリケーション間の連携方式。
    • ファイル転送
    • データベース共有
    • WebAPI
    • メッセージング
  • うちでよくやる方式なのはファイル転送だろう…。オンプレの環境との連携とかがあるので仕方ない面はある。WebAPI連携は多少やっている。自分の担当製品とその派生製品で、API連携している。WebAPIはGraphQLで作っていっているが、ちょっとわかりにくい。自分の実装が中途半端だからだと思う。知見が足りない。
  • 非同期メッセージングは、RailsでいえばActiveJobのようなものだろう。Redisなどを介して処理をキューイングして、取り出して実行するような形だ。JSのMVVMの仕組みも非同期メッセージングと言えるんじゃないだろうか?Pub/Subのようなやつ。
  • WebAPIの仕組みのところは、RESTful APIの話が中心。更新や削除もPOSTで行うのはRESTに反するので、ちょっとどうかなぁとは思った。
  • このURL設計やレスポンスの設計も、ドメインオブジェクトを軸として考えるとスッキリしたものが作れそう。ただ、ここまで細かくドメインオブジェクト単位で取得しようとすると、WebAPIへのリクエストが増えていって大変そうな気もする。ただ、よいAPIとして書かれていた「多様性を維持しつつ、組み立ての負担が増えすぎない適切な大きさの部品を用意すること」というのはわかる。
  • SwaggerUIの話も出てきた。前にチームの仲間と話したことがあるやつ。スキーマ駆動開発をするにしても、そこでドメインオブジェクトの設計が重要になりそう。
  • WebAPIが出力するJSONなりXMLも、Viewだから、関心事とドメインオブジェクトの不一致は起きそう。Railsだと、そこをjbuilderを使って関心事にフォーカスさせるのかなと思う。そうでなければ、to_jsonだけで成立するからだ。
  • 複雑な連携に取り組む。のところ。WebAPIを作るところをずっと課題にしているにも関わらず、要望対応やリファクタリングをしているので、なかなか進めていないのだが、ここにヒントがあった。コア機能と拡張APIと個別対応APIを分けて考えて、それぞれ完成させていけばいいかなと思った。
  • マイクロサービスの話もあった。マイクロサービスについての知見をあまり持っていないので、一概には言えないけれど、この本に書いてある通り、試行錯誤がし辛そうだなぁと思った。サービス単位の分離を上手にやらないといけない。対象業務への理解が不十分な場合はモノリスで作っておき、後々分けていくのがよさそう。マイクロサービスについての勉強もしていかなければならない。サーバーレス&マイクロサービスが今後の主流になっていくことは間違いないとは思うが、それはある程度サービスが枯れてからかなぁと思う。
  • 非同期メッセージングをアプリケーション間で使ったことはない気がする…。dRubyを使えば、もしかしたらできるのか?

雑感

今回参加していたメンバーが自分以外、Web APIを作ったことがない(作ったと思っていない?)、かつ、利用したことがないようだった。世界には様々なPublicなWeb APIがあるよってことをWeb APIまとめサイトを紹介しながら教えた。

Heroku CLIAWS CLIにしても、HerokuやAWSのWeb APIにリクエストを投げてるんだよって話をしたら、なるほどって理解してくれた感じに思えた。

Web APIを自前で作るにしても、外部に公開する場合には、SDKとか作って利用しやすくしたようがいいよねって話したら、「SDKって何ですか?」っていうメンバーもいたので、その辺りから説明した。見たこともないっていってたけれど、「いや、君のプロジェクトのGemfileにaws-sdkってあるやろ!?」と言ったら、「あ、ほんとだ」という感じで、ほとんど意識したことがなかった模様。まぁ実際に使う機会がないとわからないもんだろう。そういうのも知ってもらえるいい機会になったんじゃないかなぁと思う。

Web API設計のここ悩むよね~的な話はほぼできず(だってみんな作ったことがないし…)、今回はひたすら説明要員の役割だった。まぁみんながこんなWeb APIがあるのか~とか、マッシュアップアワードとかあるのか~って知ってもらえたと思うので、それで良しとする!!