開発の進め方の反省(少し複雑なフォーム編)

概要

既存のフォームのUIなどがあまりイケてないので、少しリッチな感じのUIに改修しようというプロジェクトで進捗がよくなかったので反省メモ。

体調があまり良くなくて頭が回らなかったというのもあったけど、体調に依存せずに安定したアウトプットを出したいのと、それだけではない気がしたので。

手こずった要素

validation

validation機能を実現できる要素が多かった

js側のvalidationの仕組みをあまり理解できていなかった。

既存のコードを残しつつ進めていたので、ごっちゃになってしまっていて、どうあるべきか?などを考えられていなかった。

vue

  • 全体の構成を決める前に、動的なフォーム部分を作り始めてしまったので、データの持ち方とかの設計があやふあや感じに
  • vue自体の経験値はある程度たまってきた感があるけど、サーバーサイドと比べるとまだまだ全然だから、直感でバーっと進める感じにはできない

整理

最初に仕様のイメージをクリアにする

プロトタイピング的なもので、チーム間では仕様がふわっとしている状況でも、

自分の頭の中での仕様のイメージをクリアにしないと設計ができず、ちぐはぐな感じになってしまうのでクリアにしておくべき。

構成要素の役割を知る

スキル的な部分。

例えばvalidationでいうと、php側のフォームを扱う機能とhtml標準の機能やjs側のライブラリとか重複してしまっている要素がある場合に、

それぞれ要素ができること、役割、得手不得手をちゃんと理解しているかつ、仕様をクリアにすれば、設計時にどういう要素をどのように使うかを決められるはず。

タスクを分割して順序を守る

例えば何かを改修するときに

  • 既存のコードを把握
  • 仕様を頭の中でクリアに
  • 技術的な構成要素を理解
  • 設計 ー実装

大まかにこういう感じにタスクを分割したときに、手順を守るべき。

簡単なものだったら適当にやってもなんとかなってしまうけど、少しでも複雑な場合は、

既存のコードを把握 をしているときに、「ああこういう感じか、じゃあここはこんな感じに手を加えちゃおう」のように、

実装 をしてしまうと、改修後の仕様をクリアにする前に細かい実装を先にしてしまってしまうことになるので、

既存のコードの仕様、設計に引きづられてしまって、ちぐはぐな感じになってしまいがち。

ササッと片付けてしまいたい誘惑にまけず、手順を守ることが大事?

あとこういうフローも習慣化していないと、「タスクを分割して作業の進め方を考える」こと自体の練度も上がっていかなそう。

ノンブロッキングI/Oがよく解っていなかったので作りながら調べてみた

モチベーション

WEBエンジニアとして活動してきてますが、技術的な棚卸しとして、普段良く使っているツールがどういったものかを少し深掘りしてみようという活動の一環。

nginx軽くて早くてお世話になっていますが、どうして軽くて早いのかよく解っておらず、ノンブロッキングI/Oという言葉もふわっと流していたので、深掘りしてみようという感じです。

ざっと色々なブログなどを見てみたのですが、いまいち消化できずにいたので、せっかくなので少し手を動かしながらやるか!ということで、少し勉強中のgolangで書いてみた。

続きを読む

Deep Workを読んでみて

かなり良かったので感想をまとめてみる

日本語版もあるけど、英語の勉強も兼ねて英語版を読んでみた。

単語解らなすぎてきつい時もあったけど、後半ぐらいまで進むとコンテキストがだいぶ解ってくるので割とスラスラ読めて良かった(それでも日本語の5倍ぐらいは時間かかってそう)

技術系のドキュメントを読むのがだいぶ楽に感じたり、とりあえず読破した達成感があったりしたのは良かったところ。

本の概要としては、

最近のインターネット社会では、Deep Work(深い集中力がいる仕事、難しいけど価値があるような)の重要性が上がってきているけど、一方で Deep Work すること自体は昔に比べて難しい環境になってきてしまっている。

そういった環境自体が良いか悪いかの議論は置いておいて、こういったシチュエーションで Deep Work するスキルを身につけて、良いアウトプットを出すことができるようになれば、色々と得ができるかも?

Deep Workの価値的な話

何かの物事に深く集中すること自体に、人間は価値を見出すことができる(craftsmanship)という話をしていて、

この辺りが個人的にとても興味深かったのですが、今の時代は解りやすい信仰的なものが薄れてきて、どういった物事に価値があって輝かしいことかが解りにくくなっている世界(post Enlightment world)でニヒリズム虚無主義)になりやすい世界と表現されいて、

自分としてもこういった言葉は全然知らなかったんですが、感覚的にはかなり理解できるなあという感じで、

価値を生み出すというよりも、既にそこにある価値を見出すスキルを耕していくことが、ニヒリズム的な価値観から解放されるやりかななのでは、 -> Deep Work につながる。

Deep Work する難しさについて。

人間は常にちょっとした衝動、欲求を感じている(何か食べたい、寝たい、ネットみたい、テレビ見たいなど)

こういった衝動は過小評価されがちで、思っているよりも強いもので、これらは Deep Work の敵。

また、日常的にこういった衝動に侵されている状態では、深い集中をすること自体が無理になってしまっている。(ほんのちょっとした空き時間などですぐスマホを見てしまったりするのが日常にってしまっている状態)

これはマルチタスクは難しいよね、という話と同じで、人間の脳のタスクの切り替えは簡単ではなく、前にやっていた作業などに注意が奪われてしまい、1つのことに集中することができなくなってしまう。

また、こういった性質を理解した上で、 Deep Work の重要性を理解していれば大丈夫でしょ?という訳でもなく、

こういった衝動に争う意思力というのは有限で、消費されていってしまうものらしい(筋肉が疲れるように)

じゃあどうするか。

習慣や儀式化することで、意思力に頼らずに Deep Work しよう。色々な偉人はこういったルーティーンを取り入れていた。

色々な習慣化、儀式化の方法があるよ。


Deep Work の対象としては、多くのことに取り組むより、少ない実質的な利益が解りやすいことの方が、定期的な熱量が生まれやすい


Deep Work の取り組みの評価としては、行っていること自体の評価は結構後になってしまうので、日々の取り組みに影響しない。

例えば 1週間で Deep Work できた時間を測ることで、日々の取り組みの評価ができ、どう改善していくかを考える指標とすることができる。


思ったこと

自分の場合だとSNSより会社のslackにだいぶ注意が奪われていたなと。これは何とかしないと。

あと人間の意思や脳はそんなに都合よくできていないので、色々と工夫、改善していく余地がかなりある。

(わりと自然に身を任せていけばいいのでは、という楽観的な感じだった)

Deep Work の中身にはそれほど触れられていないので、工夫していかないと(考え方自体のスキーマ、テンプレなど)

あと Deep Work することができても、個人でどれくらいの価値を生み出すことができるんだろうという弱気な気持ちも。

本で紹介されている例が偉人ばかりなので。

まあ今までできていなかったこと、意識できていなかったことがかなりあるので、トライしていきたいという感じ。

ActiveRecordへのattributeの代入時に何が起こっているか

ActiveRecordを継承したモデルへのattributeの代入時の挙動について。

通常の attribute

activerecord/attribute_methods/write.rb 内で #{attribute_name}= メソッドを動的に定義している。

その中で @attributes<ActiveRecord::AttributeSet> (ハッシュ的なもの)に対して attribute_nameのキーに値を設定している。

has manyとかの _ids への代入

active_record/associations/builder/collection_association.rb 内で、 #{attribute_name}_ids= メソッドを動的に定義していて、

その中で association(:#{name}).ids_writer(ids) を呼び出している

association ??

関連先のモデル自体ではなく、関連元、関連先、連携の設定の情報をもった、連携用の機能を色々もったオブジェクト(ActiveRecord::Associations::Association)

associationオブジェクトは、関連の種類(belongs_to, has_many ...)によって機能が拡張されている。


ids_writer内の処理では、idsからインスタンスリストを生成して、削除分の差分が delete_all され、追加分の差分が insert される

devise_for

Deviseを使うとき、 routes.rbに書く devise_for が何をやっているか。

devse_for?

devise/rails/routes.rb の devise_for のコメント欄のところ

# Includes devise_for method for routes. This method is responsible to
# generate all needed routes for devise, based on what modules you have
# defined in your model.

モデル上でDeviseのモジュール設定をベースとして、routesの設定をする。

また、

devise/mapping.rb の先頭のコメント。

# Responsible for handling devise mappings and routes configuration. Each
 # resource configured by devise_for in routes is actually creating a mapping
 # object. You can refer to devise_for in routes for usage options.

devise_for の定義は、実質はDevise::Mapping のインスタンスを作成している。

mappingオブジェクト作った後に、 devise_scope を呼び出して、 devise_#{module name} を呼び出すことで、routesを定義している。

# devise_for メソッドの最下部で、devise_scope を呼び出している箇所。
devise_scope mapping.name do
  with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
    routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
  end
end

例えば、passwordモジュールのroutesは下記のように定義されてる

def devise_password(mapping, controllers) #:nodoc:
  resource :password, only: [:new, :create, :edit, :update],
    path: mapping.path_names[:password], controller: controllers[:passwords]
end

mappingオブジェクトの設定を元に resource の設定をしてる。

devise_scope?

devise_scope :resource_name &block で、ブロックにroutesを定義する処理を指定できる。

devise_for でモジュール毎の基本的な設定と各種オプションでの調整はできる。

devise_scope はもっと自由にroutesをカスタマイズするときに使うとのこと。

constraints (配下のネストされたルートに制約をかける処理)で request.env["devise.mapping"] に Devise.mappings[scope]を設定して、

scope :resource_name に、上記で定義したroute情報を渡している

constraints の処理は、initializeのときではなく、実際に対象のrouteにアクセスがあったときに走るので、

deviseのroutesのリクエスト毎に request.env["devise.mapping"] にMapping情報を設定している(目的は謎。)

=以下2020/11/20に追記=

mapping情報はdeviseが複数のresource(UserやAdminのような)に対応するために必要なものだった。 mappingにはuserのようなresource名の情報と、registerableのような設定されているモジュールの情報が格納されている。

DeviseController自体はresourceごとに生成されないので、1つしかない。 なので、mapping情報がないと、今アクセスされている対象のresourceが、例えばuserなのか、adminなのか解らない。

devise.mappingはDeviseControllerに対して、対象のresourceやmoduleの設定を知らせるためのメタ的な情報だった。

まとめ

Railsのroutes周りの仕組みをハックして、deviseの各モジュールで必要なroutesの設定を、簡単で、柔軟な形で行えるようにしているイメージ。

Sidekiqの仕組み

Railsのジョブキューのgemで有名なSidekiq。 どのように動いているかよくわかっていないので調べてみた。

エンキュー

ActibeJob.perform_later

キューに入れたい処理はActiveJob::Baseを拡張したクラスのperformメソッドに実装し、

perform_laterを呼び出すと、エンキューの処理が走る。

Class SampleJob < ActiveJob::Base
  def perform
    p "Hello!"
  end
end

...

SampleJob.perform_later

queue_adapter.enqueue

perform_laterは ActiveJob.queue_adapter に設定されているオブジェクトの enqueue にjobインスタンスを投げます。 queue_adapterの設定はデフォルトでは、 :async(AsyncAdapter) で、configで以下のように指定すると、SidekiqAdapterが設定されます。

config.active_job.queue_adapter = :sidekiq

他にどんなアダプターがあるかは、activjob/queue_adapters.rb のコメントに結構細かく書かれています。

rails/queue_adapters.rb at master · rails/rails · GitHub

SidekiqAdapter.enqueue

def enqueue(job) #:nodoc:
  #Sidekiq::Client does not support symbols as keys
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ]
end

こんな感じ。 ジョブのクラス名や引数を Sidekiq::Client に投げている。

class として指定されている。JobWrapper の実装はかなり薄くて、

class JobWrapper #:nodoc:
  include Sidekiq::Worker

  def perform(job_data)
    Base.execute job_data.merge('provider_job_id' => jid)
  end
end

これだけです。 Base.executeは受け取ったjob_dataをdeserializeして、performを実行します。

Sidekiq.client

Sidekiq.clientのpush処理では、受け取ったジョブ情報をRedis上に保存できる状態に成形し、ジョブIDの情報を追加して、 Redisの queues:#{queue_name} キーに追加します。

サンプルで追加してみた中身はこんな感じのjsonです。

{
    "class": "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
    "wrapped": "SampleJob",
    "queue": "default",
    "args": [
        {
            "job_class": "SampleJob",
            "job_id": "ae214f55-3d84-4037-8086-be111661ec6a",
            "queue_name": "default",
            "priority": null,
            "arguments": [],
            "locale": "ja"
        }
    ],
    "retry": true,
    "jid": "d1ecd3aa4bf5340b5c2a52f6",
    "created_at": 1506733202.404303,
    "enqueued_at": 1506733202.405045
}

ジョブの取得と実行

Redisにジョブを追加するだけではジョブは実行されません。 ジョブ情報を取得して、実行する必要があります。

取得

bundle exec sidekiq を実行することで、ジョブを取得、実行するプロセスを起動できます。

プロセスが起動すると、Sidekiq::Launcher.run が実行されます。

sidekiq/launcher.rb at master · mperham/sidekiq · GitHub

まず、Sidekiq::Managerがworkerを作成し、起動します。(デフォルトの設定では25個作成されます。)

workerはそれぞれのスレッドを起動し、ジョブ情報をRedisから取得しに行きます。

wokerの中身は Sidekiq::Processor で、Processor.get_one から下記の Sidekiq::BasicFetch.retrieve_work が呼び出され、Redisからジョブ情報を取得しています。

sidekiq/fetch.rb at master · mperham/sidekiq · GitHub

実行

Redisから取得したジョブ情報を元に、ジョブのクラスと引数情報を deserialize して、worker上のスレッドで実行されます

sidekiq/processor.rb at master · mperham/sidekiq · GitHub

performが呼び出されてます。

Railsのloadは元のプロセス起動時に行われています。

sidekiq/cli.rb at master · mperham/sidekiq · GitHub

scheduled

下記のように waitwait_untill を設定した場合はどうなるか?

SampleJob.set(wait: 5.minutes).perform_later

エンキュー

指定された値から、実行する時刻を計算し、 queue_adapterの enqueue_at にジョブ情報と一緒に渡されます。

#SidekiqAdapter.enqueue_at
def enqueue_at(job, timestamp) #:nodoc:
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ],
    'at'      => timestamp
end

Sidekiq::Client側の処理

Sidekiq::Client側の処理では、 at が指定されていると sorted set に timestampでソートする形でジョブ情報が登録されます。 (上記の通常(at指定なし)のジョブ情報リストとは違うキーに保持されます)

取得、実行

Sidekiqのプロセスは上記で説明した プロセスを実行する woker を管理する manager の他に、 at 指定で登録されているジョブを ジョブキューに登録する poller が動いています。

pollerは5秒ごとに、 sorted set から 現在時刻より前 の timestamp のジョブ情報がないかを見に行き、 対象のジョブがあった場合は Sidekiq::Client::push を呼び出し、 sorted set から ジョブキューへジョブ情報を移します。

この処理により、 at で指定したジョブも worker達が処理する対象になってくれます。

retry

プロセスでエラーが起きた時、

retry が有効な場合で、retry数がまだMAX retry数内(デフォルトは25)の場合は、ジョブ情報が retry queue に追加されます。

retry queue に保持される情報は、通常のジョブ情報に、エラー情報とリトライ回数などの情報が加えらえれたものになります。

上記で話した scheduledのキューを処理する poller は、retryのキュー情報も見るようになっているので、retryに入れられた情報は、

自然とジョブキューへ追加されます。

リトライ実行のディレイはリトライ数が多くなるごとに指数関数的に多くなるようになっています。

ディレイ時間の計算処理 sidekiq/job_retry.rb at master · mperham/sidekiq · GitHub

リトライ設定がfalseの場合と、リトライ数が上限を超えたジョブ情報は dead というキーの sorted set に 失敗時刻が sort 対象の項目として、登録されます。

dead データセットは、オプションの dead_max_jobs, dead_timeout_in_seconds の設定を元に、

失敗したジョブ情報を保持しています。

デフォルトの設定は下記となっています

deed_max_jobs: 1万
dead_timeout_in_seconds: 6ヶ月

となっています。

sidekiq-cron

Sidekiqをcron形式の設定でスケジューリングするgemが sidekiq-cron です。

github.com

設定ファイルの読み込み

YAMLの設定ファイルを読み込む場合は、 config/initializers などに

Sidekiq::Cron::Job.load_from_hash YAML.load_file(#{filepath})

のように指定することで、Redis上に、cronの設定情報が保存されます。

poller

sidekiq-cronでプロセスを持つことはなく、sidekiqのlauncher を上書きすることで実現しています。

# Sidekiq::Lancher

# remember old run
alias_method :old_run, :run

# execute normal run of launcher and run cron poller
def run
  old_run
  cron_poller.start
end

sidekiqの manager, poller とは別に、 cron_poller の処理を start しているのが解ります。

cron_poller の 処理は、上記でRedisに取り込んだ cron の設定を定期的に見に行って、ジョブキューへ追加する処理をやっています。