せらぴんブログ

サークル「せらぴん」のうのはな透です。やっぱり眼鏡っ娘が好き!!

Sinatraで肥大化したファイルを分割する

Sinatraを使ったPJ*1で、あまりにも一つのファイルに色々突っ込みすぎていたので、ファイル分割をメインとしたリファクタリングを実施した。
その際の備忘録。

ディレクトリ構成は長くなるので割愛。

やったこと

バッチファイル

  • cronで実行するバッチファイルがappディレクトリ直下に置かれまくっていたので、app/batchディレクトリを作ってそこに全部つっこむ
  • バッチファイル用の独立したライブラリファイルがあったので、新設するapp/src/modelsディレクトリに統合
  • バッチファイル内に独立したクラスが書かれていた💢ので、それもapp/src/modelsディレクトリに移行(バッチファイルは処理のみを書く)

モデルファイル

  • appディレクトリ内に2~3のモデルファイルが散逸していたため、app/src/modelsディレクトリを作ってそこに全部つっこむ
  • モデルファイルが肥大化(一番ひどいもので80KB!)していたため、1ファイル1クラス形式になるようファイル分割
  • 複数のサブクラスを抱えている肥大クラスがあったので、サブクラスをすべてファイル分割
  • それでも肥大だったため、さらに一部機能をモジュール化して別ファイルに退避

ヘルパー

  • app/src/helperディレクトリに移動
  • helper do~endを用いていたので、ヘルパークラス化

コントローラ

  • コントローラファイルが肥大化(合わせて45KB!)していたため、サブコントローラとしてファイル分割し、app/src/controllerディレクトリに移動

*1:まぁ要するに同人秘書なんだけど

続きを読む

SinatraとVueの黒魔術化

お前が○○んだよ?ジェネレータ

次なる実験場はこのジェネレータ。
ベースにSinatraを使っているので、そこに乗っける形でVueによるSPA化を施していく。

ぶっちゃけ、下記ページに必要なことは8割方書いてある。

qiita.com
qiita.com

以下、備忘録

index.htmlの返し方

上記ページでは「フロントエンドサーバがnpm、バックエンドサーバがsinatra」となっていたりして
そいつらの橋渡しをするようにしているようだが、
「別にSinatraあるんだから、そこで全部やっちゃえばよくね?」ということで今回はSinatraのみで対処した。
要するに、"/"にアクセスされたらSinatraで拾って"/index.html"を内部的に返せばよいわけである。
これは、Sinatraのreadmeにある「別ルーティングの誘発」を利用することで実現できる。

Sinatra: README (Japanese)

Ajaxどうするの?

axiosでやる。

qiita.com

axiosを使ってPOSTするとデータが渡ってないようだったので、下記ページを参照しながら直した。

qiita.com

プログラム内でページ遷移

this.$router.pushを使う。
ちなみに、動的ルーティングのパラメータはthis.$route.params、こいつはthis.$router
めちゃめちゃ紛らわしい💢

yarn

最近ではnpmの代わりにyarnを使うのが流儀らしい。

www.webprofessional.jp

早いらしいが体感的にそこまで違いを感じなかった。
実際に数値で比べてみないとわかんないものだししょうがないね。

感想

  • SinatraAPI(&index.htmlレスポンス)に徹し、フロントエンドはVueに集約することができた
  • npm(yarn)周りはデプロイした時点で役目を終え、herokuのwebプロセスではSinatraのみが走っている。美しい
  • nodeのモジュールとrubyのgemが両方Slugとして固められるため、容量としては若干不安要素がある(このアプリも大したことはしてないけどSlugが71MBになってる)

以上。

Vueいじってみた

せらぴん

jp.vuejs.org

今更ながら「Vue.jsを使いたい」という欲求を満たすため、自分のサイトをゴリゴリ改造しました。
従来mithril.js(in livescript)だったものを、Vue.js+Vue-router(in javascript)に変更してます。
以下、備忘録。

開発の流れ/詰まった点

Vue-router

最初はメインのページをindex.htmlにゴリゴリ書いていったんだが、
うちのサイトはメインページの他に本用のページがある。
この2種類のページを使い分けるために、メインページが一段落ついてからVue-routerを用いてSPA化に着手した。

Introduction · vue-router

Vue-routerはrouter-viewタグ以下にページ個別の内容を記述する。
その”ページ個別の内容”は、通常コンポーネントとして提供する。
つまり、現状からSPA化するためには、下記のようなフローを辿ることになる。

  • index.htmlは、router-viewを書くだけの質素なページにする
  • (今までindex.htmlに書いていた)メインページの部分をコンポーネント化する
  • 本用のページを新たにコンポーネントとして作成する

単一ファイルコンポーネント

コンポーネント — Vue.js
Vueのコンポーネントは気の遠くなるくらい多機能だけど、ここではごく基本的な機能のみを利用する。
問題は、コンポーネント化するためにはtemplateにJSの文字列リテラルとして持たせないといけない点。
できれば、リテラルでなく純粋なhtmlとして記述したい。
その欲求を解決してくれるのが単一ファイルコンポーネントである。
これは、例えばhome.vueというファイルを作って、そこのtemplateタグ内にコンポーネント化したいhtmlを記述するというものである。
単一ファイルコンポーネントであれば、ほぼ純粋なhtmlとしてページを記述できる。

単一ファイルコンポーネント — Vue.js
vue.jsのcomponentをwebpackで.vueにして単一ファイルコンポーネントにする - Qiita

ここでの注意点は2つ。
templateタグ直下の要素は必ず1個のみにするという点*1と、
単一ファイルコンポーネントを使うためには、.vueファイルのコンパイルが必要であるという点だ。

Webpack

vueファイルはそのままでは使えない。コンパイルしてJS(VDOM)化しないといけない。
モジュールバンドラが必要になる。
私はモジュールバンドラといえばbrowserifyしか知らなかったのだが、
Webで調べたところ、WebpackというものがVueとよく併用されているようなので、そちらを利用してみることにした。

webpack
webpack 入門 (v3系 対応) - Qiita

vueファイルのコンパイルには、通常の設定に加えて、vue-loaderを使うよう設定する必要がある。
vue-loaderはnpmでインストールする。

Vue.jsを使った大規模開発に必要なもの - Qiita
Introduction · vue-loader

また、vue-template-complierもあわせてインストールする必要がある。

vue-template-compiler

このあたりは、vue-cliをインストールしている場合は一緒にインストールされている……と思う*2

ページが切り替わらない(1)

本用のページは一つ前のページと一つ後ろのページにそれぞれ遷移できる。
最初、ページを切り替えようとすると白いページに遷移してしまうことがあった。

理由は単純で、aタグを使っていたから。
Vue-routerでの遷移はrouter-linkタグを使う必要がある。
遷移先もhrefでなくtoで指定する必要があるため注意。

以下蛇足

ちなみにaタグで単純に遷移できない理由は、SPAではルートが"/"でなく"/#/"や"/?/"などになり、ルートの変換が必要になるためである。
aタグで"/"を直接変更してしまったら、シングルページでなくなってしまうのでSPAでなくなってしまう。よくよく考えてみると自明の理であった。

ページが切り替わらない(2)

上記問題が解決したあとも、ページを切り替えようとするとURLは変更されるのにページの内容は変更されないことがあった。
これについては、下記ページが詳しい。

[vue-router] パラメーター違いのページリンクをクリックしても更新されない時は ? - atuweb : つながりを作るWebプログラマ

以下蛇足

公式のページにも「$routeをwatchしろ」と書いてはいるのだが、
watchで何をすればいいかはいまいちピンときてなかった。
上記ページのようにwatchメソッド内でto.params.idを引数としてdataの各変数を更新してしまうのが直感的でわかりやすい。
その場合、更新用メソッドが必要になる。初期読み込み時に更新用メソッドを読み込むためには、createdプロパティを利用する。

gulpとwebpack

今までgulpを使っていたので、引き続きメインのタスクランナーにはgulpを利用しwebpackはモジュールバンドラに徹してもらうことにする。
gulpとwebpackの連携については、下記ページが詳しい。

Gulpで始めるwebpack 3入門 - Qiita

以下蛇足

webpackを導入した際、webpackの実行でエラーが発生していた。
"=>"*3を認識していなかったようなので、決断的npmバージョンアップを実行。
(io.js 2.0.2→v8.9.4にバージョンアップ。かなり浦島太郎状態である……)
ところがバージョンアップ後、gulpの途中でよく止まるようになってしまった。

Installing node.js get error - after npm install - cannot find module internal/util/types (Windows 10 64bit) · Issue #19032 · npm/npm · GitHub

issueのページを見てもピンとこなかったので、古いnodeモジュールをすべて削除。
gulpfileを頼りに、必要最低限のモジュールを入れ直す羽目になった……。

デプロイ

とりあえず諸々整理して、npm startを実行すれば静的サーバが立ち上がるように設定し終わったので、ここでherokuにデプロイ。
しかしデプロイ後、アプリケーションエラーが発生するようになってしまった。

原因は、npm startで利用している一部のモジュールをdevDependenciesとしてインストールしていたため。
静的ファイルはすべてnpm startで生成していたため、heroku上でも同じモジュールが使えないといけないのだが、どうやらherokuではdepencenciesはインストールしてもdevDependenciesはインストールしてくれないようだ。
お行儀がいいとはいえないが、すべてdepencenciesに突っ込むことで解決した。

Vueの感想

  • VDOMをhtmlとして書けるのが嬉しい。それでいてコンパイルするから最終的にはVDOMとして動作しているのが面白い
  • 最初の動機は「vue.js+vue-loader.jsだけ使えばnpm非依存で作れるのでは……?」だったが、終わってみればnpmからは逃れられそうになかった
  • vueファイル内ではpugやstylusも使えるようなので、そっちも使ってみたい
  • 「『htmlとして書けるのが嬉しい』のにpug使うの?」って言われるかもしれないけど、JSのリテラルでない点が重要なのでhtml or pugはそこまで重要ではない。むしろvueファイルごとに使い分けられるので、そういう楽しみもある(と思う)
  • テンプレートは奥が深そう(親子関係とか)なので、もう少し勉強が必要そう
  • アニメーションさせたい
  • 既存のruby製サイト(Sinatraなど)に組み込めれば強そうなので、研究したい

以上。

*1:ただしv-if/v-elseを用いて”結果的に要素1個のみになる状況”にしても、許容される

*2:私はvue.jsをダウンロードして利用していたので、vue-cliはインストールしていない

*3:アロー関数( https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions

遅延処理に使えそうなgem

業務で遅延処理が必要だったので、Rubyで下記のような感じで実装しました。

  • 遅延処理の情報(実行メソッド、実行予定時刻、パラメータ)をDBのワーカーテーブルに書き込む
  • ワーカースレッドがワーカーテーブルを定期的に読み込んで処理を実行する

業務が終わって調べたんですが、それっぽいgemが当然のようにありました(震え声)


github.com
qiita.com


delayed_job自体は非同期処理をするためのgemっぽいんだけど、
:run_atを指定することで、その時間に処理を発火させることができるみたい(未検証)。

また車輪の再発明をしてしまったらしい……。

処理速度を向上させようとして失敗した話

気付けばもう2018年です。
去年は結局ブログのエントリを1本しか出しませんでした。
ブログのエントリは、twitterよりはまとまった話がしやすいですし大概追いやすいので、少し去年の四方山話をまとめておこうと思います。


閑話休題
去年の12月、実装していたプログラムの納期が迫っていた頃の失敗談です。
そのプログラムは、複数のラジコンを制御するようなプログラムで、
MQTT*1を受信した際の処理とリアルタイム処理の二本柱で制御していました。

このリアルタイム処理が曲者で、
「0.2秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Aを行う」スレッドAと
「0.5秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Bを行う」スレッドBが併存していました*2
この「全レコードを最新の状態に更新」は処理A、処理Bとも”直前”*3に行う必要があるため、まとめることができなかったわけです。

レコード件数は高々10件程度ですが、
プログラムの性質上、当該更新処理にかかる数十ミリ秒は馬鹿にできない時間でした。
かと言って、スレッドでの処理間隔を不用意に開けることはできません。
なぜなら処理間隔が開きすぎるとラジコンの制御が間に合わず、ラジコン同士が衝突したりコースアウトしたりするリスクが増大するためです。

なんとかできないか……。

そこで着目したのは「MQTT受信処理」の方でした。
下記のように処理を修正すれば全レコードを更新するタイミングが減ると考え、実際にプログラムを修正して様子を見ました。

  1. 特定のMQTTを受信した際にスレッドA,Bで行っている処理を行うべきタイミングかチェックする
  2. 処理を行うべきタイミングでなければ、行うべきタイミングになるまで待機し、再度チェックを掛ける*4

結論から言うと、これは大失敗でした。
修正後のプログラムを走らせると、10分くらい経過した時点でDBエラーが発生するようになりました。
ログを検証すると「全レコードを更新している途中のスレッドが殺された」ために起きていた現象のようでした。
プログラムを修正した際に一部のスレッドは「そのスレッドが請負う処理を行う必要がなくなった場合*5、外部から当該スレッドを殺す」ようにしていました。

「全レコードを更新している途中でスレッドを殺したらそりゃダメよなぁ」ということで、「レコード更新中はフラグを立てて、外部からスレッドを殺せないようにする」よう修正を掛けましたが、これは何故か失敗。
「面倒だし、スレッドを殺すこと自体を止めよう」と再度修正を掛けましたが、今度はスレッドが無尽蔵に作成され、処理がパンクする事態に。
ここに至ってこの修正部分にこれ以上時間を費やすことは不毛であると判断し、結局、以前通りのリアルタイム処理に巻き戻しました。


この件の教訓としては、概ね以下のような感じです。

  • スレッドを外部から殺すような処理を設ける場合、DBなどの外部リソースを変更している最中にスレッドを殺さないようにする
  • そもそも一度立てたスレッドはできるだけ外部から殺さない。スレッド内の適切なタイミングで終了チェック処理などを設ける
  • 上記に関連して)スレッドなどの非同期処理は基本Fire&Forget(撃ちっぱなし)の精神を厳守する
  • 常駐スレッドが無尽蔵に立つような設計は厳に慎む(例えあるスレッドの生存期間が短くても、そのスレッドが後続のスレッドを延々と立て続けるなら、それは常駐と変わらない)
  • エンバグなどで困った時のために、修正部分をいつでも切り捨てられるようにする(修正用ブランチを切ってそこで処理するなど)
  • 処理速度が致命的に遅いわけでないなら、わざわざ処理速度を上げるための修正をしない。プログラムの修正には常にエンバグのリスクが伴う

*1:HTTPの簡易版みたいなもの

*2:実際には他に1~2本ほどテーブルの全レコードの更新を必要とするスレッドが走っていました

*3:10ミリ秒以上はずらしたくない

*4:一見再チェックは必要ないように見えますが、待機中に状況が変わることもあるためチェックは必須でした

*5:上述の"待機中に状況が変わった"場合、と言い換えてもいいです