Haxeのマクロで外部プロセスに処理を移譲する場合の知見
HaxeでVue.jsの開発をするために、.vueファイル(Single File Component)コンパイラをNode.jsで実装して、マクロから呼び出してコード生成するということをやっていた。このエントリーを書いている時点では、コンパイラ自体はまだ「とりあえずコードが生成できている」というレベルなのだが、マクロから外部プロセスを起動する部分については知見が溜まってきたのでメモを残す。
なお、この記事で使用しているHaxeはv3.4.3である。Windows 10でしか動作確認していないが、おそらく他のOSでも挙動は同じになると思われる。
プロセスの起動方法
次のコードが基本形となる。
ポイントとしては、
sys.io.Process
を使う。- プロセスの起動確認にstdoutを読み込んでいる。これはstdoutが読めるまでブロッキングされる。
- 外部プロセス側で「起動が完了したことが分かる何らかの値」をstdoutに出力しなければならない。
- プロセスの起動が失敗するなどの要因で、プロセス側からstdoutがクローズされ、終端を読み込むとEofがthrowされてくる。
この方法だと、外部プロセス側で処理が完了したら、successfulでもなんでもいいので、なんらかの起動完了を表す文字列をstdoutに出力しなければならない。これがないと、readLine()
で処理がブロッキングされてしまう。
上記以外に起動確認をする方法としては、リターンコードの確認(process.exitCode()
)をする方法もある。これもリターンコードが返されるまでブロッキングされる。ただ、後述する外部プロセスを常駐化することを考えると、この方法は利用できない。
また、補足として、process.stdout
の読み込みも当然ながらブロッキングされるため、こちらを監視して正常起動を確認するという方式はとることができない。
その他注意点
- プロセス起動は比較的オーバーヘッドが大きいので、マクロ内で何度もプロセス起動をすべきではない。
- stdoutに改行なしで大きなデータ出力される場合、応答がなくなる?
- 最初は都度プロセスを起動して、処理結果をstdoutに出力してデータのやり取りをする素朴な実装をしていたのだが、
process.stdout.readAll().toString()
という実装ではマクロの応答がなくなってしまう問題が発生した。readLine()
の場合はデータサイズが大きくても問題は発生していない。 - 原因未調査だが、プロセスが停止されている(=stdoutが閉じられている)状況でもデータのサイズが大きいと応答がなくなってしまうため、Haxeの標準ライブラリ側に問題がある可能性もあり。
- 最初は都度プロセスを起動して、処理結果をstdoutに出力してデータのやり取りをする素朴な実装をしていたのだが、
- stdoutでテキストデータのやり取りをする場合、Windows環境では文字エンコーディングの問題が生じる。
上記のような問題に遭遇し、色々試したのだが、マクロ起動時にコンパイラをサービスプロセスとして起動して、HTTP RPCで都度の処理を移譲する返す形に落ち着いた。
マクロ起動時に処理を実行するには、Haxeコンパイラパラメータで--macro
https://haxe.org/manual/macro-initialization.html を指定する。また、Haxelib化している場合は、extraParams https://haxe.org/manual/haxelib-extraParams.html と併用すれば、ライブラリを参照するだけで自動で --macro
が付与されるようにできる。
また、sys.io.Process
は明示的にclose()
もしくはkill()
しなくても、マクロ終了時にプロセスを停止してくれるのだが、Haxeコンパイラの入力補完機能を利用している場合は事情が異なってくる。
この場合は、Haxeコンパイラプロセスが常駐状態になるので、入力補完が走るたびにプロセス起動処理が呼び出され、明示的に外部プロセスをクローズしない限りは起動しっぱなしになる。そのため、マクロ側かプロセス側のどちらかで(できれば両側で)多重起動を防止するコードが必要となる。マクロ側で対応する場合は、単純にsingletonとして処理すれば十分である。
追記:Haxeコンパイラの入力補完サービスの場合、Haxeコンパイラプロセスが停止しても起動した外部プロセスは停止されないようだ。これは明示的にハンドリングする方法がないようなので、ある程度お行儀をよくするには、外部プロセス側で一定期間利用されていなければ自動終了させるような仕組みを入れるしかないと思われる。
思い付きでHaxe/JavaScript extern用のabstract型を作ったら幸福度が上がった
HaxeでJavaScript用のexternを書く際、EtherType(Union Typeを表現する型)とRest(可変長引数を表現する型)が存在する。
ただ、「EtherTypeは型を2つしか指定できないからなー」という感じの思考から、思い付きでちょっとabstract型を定義してみたら思いのほかexternを書くのが楽になるものができた。見たらわかるレベルのものなので、解説省略。
Kotlin/JavaScriptを試してみた
Kotolinとは
ググって
KotlinでJavaScriptにトランスコンパイル
スタンドアロンコンパイラを使う方法とGradle等のJVMビルドツールを使う方法がある。実際に開発すると仮定すると、インクリメンタルビルドが可能なGradleを使うことになると思われる。
手順としては、
- JREをインストール(JVMやAndroid向けの開発も行う場合はJDKが必要だが、JavaScript開発の場合はJREで良い)
- Gradleをインストール(当然、Gradle Wrapperを使ってもよい)
- プロジェクトのスケルトンを作る(build.gradle)
という感じになる。
Hello World
ブラウザでHello Worldが動くところまでを試してみた。IntelliJ IDEA等のIDEを使えばプロジェクトテンプレートがあるかもしれないが、IDEを使わずにテキストエディタのみでやってみた。
ここに記載する手順は、次の環境で確認した。
まずビルドファイルを作る。任意の空ディレクトリ内で次のように build.gradle
ファイルを作成する。
ext.kotlin_version
の部分は、使いたいKotlinのバージョンを指定する。
group 'org.example' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.1.4' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'kotlin2js' repositories { mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" } task assembleWeb(type: Sync) { configurations.compile.each { File file -> from(zipTree(file.absolutePath), { includeEmptyDirs = false include { fileTreeElement -> def path = fileTreeElement.path path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/")) } }) } from compileKotlin2Js.destinationDir into "${projectDir}/web" dependsOn classes } assemble.dependsOn assembleWeb
次にMainとなるコードを記述する。
fun main(args: Array<String>) { println("Hello World") }
ビルドは gradle build
コマンドを実行する。ビルドに成功すると、 web
ディレクトリ以下にJavaScriptが生成される。
ブラウザで実行するために、HTMLファイルを作成する。ここでは作業ディレクトリ直下にHTMLファイルを作成する(webディレクトリ配下にHTMLファイルを配置してしまうと、ビルド時に消えてしまう)。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <script type="text/javascript" src="web/kotlin.js"></script> <script type="text/javascript" src="web/kotlin_main.js"></script> </body> </html>
作成したHTMLファイルをブラウザで読み込み、開発者ツールのコンソールを確認すると、 Hello World
と表示されているはずである。
試してみた感想
実用レベルだが、自分で積極的に使うことはないかなぁというのが正直な感想。
実用となる根拠としては、
- DOM操作が可能
- js()関数で生JavaScriptコードの埋め込みが可能
- Dynamic型でJavaScript側のオブジェクトが操作可能
- CommonJS, AMD形式のモジュールとしてビルド可能
- SourceMapが出力可能
- dead code elimination (DCE) が使える
というあたり。
一方で積極的に使うことはないなぁという根拠としては、
- 実行時にKotlin Runtime (kotlin.js) に依存しなければならず、非圧縮状態で1.3MB程度ある
というところが個人的に最大のネックになる。kotlin.jsはminifyすれば数百KB程度になると思われるが、どうせRuntimeに依存しないといけないのであれば、私ならScala.jsを選択する。
Scala.jsの場合、
- 最初からビルドツールチェーンでminifyが考慮されている(fullOptJS指定時、Google Closure Compilerが使われる)
- Scala/JDKのクラスライブラリが一部JavaScriptコードに変換される https://www.scala-js.org/libraries/libs.html
- 単純にScala.jsの方がユーザーが多い(と思われる)
というメリットがあるため、Kotlinの現状では、チームメンバーがKotlinに精通しまくっているということでもない限りは選ぶことはない。
(蛇足として、そもそもHaxeおじさんなのでScala.jsも積極的には選択しないのだけど、自社の人間はScala.js経験値を積み上げているという…
エンジニアは業務時間外に勉強すべきかの話
他社社長が盛り上がってるみたいなんですが、そこの言説だけが広がっていってもアレだなぁと思ったので、単に自分がやってきた経験値とかを書いてみた。銀の弾丸欲しい。
お前誰よ
- 零細ITシステム会社経営
- 従業員5人、エンジニア数だと6人(私自身が含まれる)
- 会社は設立して4年弱
- 自社サービスを作っているが、今のところの収益構造は受託・SESが100%
- 10年ぐらい名古屋でコミュニティ活動に関わってきている(ただし、ここ2年ぐらいは忙しすぎて、ほぼ勉強会に行けてない)
色々やってきて至った基本的な考え方
会社という組織を前提とするのであれば「雇用契約」による利害関係で考えるのがシンプル。
- 会社は利益を上げたい
- 利益を上げる手段としては、良いエンジニアが必要(それだけではないが、この話題の本筋ではないので割愛)
- 良いエンジニアを育むには学習が必要
目的は利益であって、エンジニアの勉強は手段。エンジニアが勉強すべきかみたいな話をしたら、そりゃあ、「エンジニアである以上は生涯学習すべき。ただし、プライベートに勉強を強制させるの(無償労働)はあかん」という一般論にしか終着しない。
「かくあるべき」という話にせず、人事評価(賃金)の話にするのが分かりやすくなる。
- 業務を遂行する上で必要最低限の教育を、賃金を支払ったうえで行わなくてはならない
- 賃金が発生する以上、学習成果が上がっているかを人事評価に反映させることは正当となる
- エンジニア個人の学習成果が収益に直結するのであれば賃金に反映し、新たな事業展開に繋がるような場合はプロジェクトメンバーに抜擢するとか、人事的な面で評価する
これを前提に、会社としてはこういう技術が欲しいとか、こういう人材が不足しているということを伝えて、賃金に反映させる。賃金に繋がるとなれば、学習意欲に繋がって資本主義ヤッターとなる。
当然ならない場合もあるが、賃金に反映されない理由としてはわかりやすい。賃金を払うための収益が改善されないだから、割増する理由がない(これも単純化しすぎたらあかんところではあるけど、割愛)。
強制される勉強の成果は上がらない
サラリーマンプログラマーの時代には、こういうことをやっていたがうまくいかなかった。
会社を作ってから、ある程度の選別をして採用を行ってきているが、すべてのケースにおいて合格点となることはない。そのため、社員教育はやるしかないという意識で取り組んできている。ただ、正直なところ、期待する成果は上がっていない。
振り返ってみて、
- 本人に興味・素質がある領域や教育を行いやすい領域は伸びるが、そうではないところはほぼ伸びない
- 期待値が高すぎた
という、当たり前だよねという思いに至っている。
「強制される勉強」というのは「本人に興味がない」とか「学習成果が上がってるように感じない」というところから来るものかと思う。
「本人に興味がない」ケースで、人事評価で釣れないのであれば、これ以上は会社からインセンティブを与える手段はない。ただ、本人に聞いても「やる気はあります!」みたいな答えしか当然出てこないので、外部から「やる気」を評価することはできないように思う。
「学習成果が上がってるように感じない」については、学習カリキュラムやゲーミフィケーションなど、改善余地はあると思う。自社に対してだけ考えると、正直私の力不足(時間不足)感が強いので、やり方を見直さないといけないなぁとは考えている領域にはなっている。 ただ、学習成果の面において、「マニュアル化」「自動化」については、成果が見えやすいと思っている。マニュアルには即効性のある内容が書かれている。自動化は厳密には学習ではないのだけど、そもそも覚えなくてもよい状況にすれば、成果はあがる。
まとめる
割と殺伐としたまとめになるが、雇用契約に基づいて、システマティック・資本主義的に考えるのが素直だと思っている。
端的に「エンジニアは業務時間外でも勉強すべきか」という問いに対して、経営者としての立場の回答は「強制は逆効果になるからNG、インセンティブを明示して社の要望を伝えることは必要」となる。
- 期待しすぎない
- ある程度の誘導はできるにしても他人が自分の期待通りに動くわけがないし、銀の弾丸もない
- 高すぎる期待値は精神衛生上良くない結果になりやすい
- マニュアル化、自動化等の属人性が排除できる部分を対処していくことで底上げを図る
- プライベートでの勉強の有無に関係なく、会社の利益につながる人間(端的に業務遂行能力の高い人間)を人事的に評価する
社員のやる気を上げる手段は色々提供することは可能だけど、最終的には、やる気とかいうのは運要素だと考えた方が良い。そういう人が採用できるとも限らないし、興味分野はコロコロ変わるし、ゲームやりたくなったりするし、結婚したりとかで状況は常に変わる、人間だもの。
追記: 「期待しすぎない」は「地に足をつけろ」「着実にやれることをやる」みたいなニュアンス。
おまけ
うちは就業規則をGitHubに公開しているので、エンジニアの勉強に関連する制度の現時点の評価を書く。
EmployeeHandbook/005_福利厚生規程.md at master · DenkiYagi/EmployeeHandbook · GitHub
- 書籍購入補助
- セミナー補助(勉強会参加補助)
- 年に数回程度しか使われていない。これは社員が近場の勉強会に行く頻度が高いので、単に費用が発生しない等の理由と思われる。
- 有償セミナー・トレーニング等については今のところ実績がない。行きたい・行かせたいという具体的なところまで決まりきってないだけ。
その他、社員から機械学習ぶん回したいからサーバーと空調をどうにかしてくれみたいな要望が上がってきたが、マニー・設置スペースの両面で都合がつかなかったので、直近は見送りになっている。
Spring Bootで実装した認証付きWeb APIで異なるAPIのレスポンスが返される問題
発生していた問題
- 未ログイン状態で、認証が必要なWebAPI-1にリクエスト
- HTTP 401が返される
- ログイン
- HTTP 200 ログイン成功
- レスポンスボディなし
- WebAPI-2(1とは異なるAPI)を呼び出す
Spring Bootのバージョン
- Spring Boot 1.4.2
- Spring Security 4.1.3
原因
問題が発生するパターンのSpring Securityの設定は概ねこんな感じ。
public class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String URL_PATTERN = "xxx"; @Override protected void configure(final HttpSecurity http) throws Exception { http.regexMatcher(URL_PATTERN) .formLogin() .successHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK)) .failureHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED)) .permitAll(); http.regexMatcher(URL_PATTERN) .exceptionHandling() .authenticationEntryPoint(new Http401AuthenticationEntryPoint("Bearer error=\"invalid_request\"")); http.regexMatcher(URL_PATTERN).csrf().disable(); } }
また、APIは下記のような実装を行っていた。
- 単一URLにPOST
- HTTP Headerによって処理をルーティング
この環境下で、前述の再現手順を行うと、以下のようなことが発生していた。
対応策
Spring Securityの設定でリクエストキャッシュを無効にする。
public class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String URL_PATTERN = "xxx"; @Override protected void configure(final HttpSecurity http) throws Exception { http.regexMatcher(URL_PATTERN) .requestCache().requestCache(new NullRequestCache()); } }
参考URL
http://www.sedooe.com/2016/04/rest-authentication-using-spring-security-and-spring-session/
分散協調サービスのメモ
以前は分散システムの設計をやってたんですが、最近ちょっと離れているので、2017年7月末時点の分散協調サービスについて確認。
- Apache ZooKeeper https://zookeeper.apache.org/
- 相変わらず使われている
- APIが低レベルすぎるので、Apache Curator http://curator.apache.org/ との組み合わせで使う(これも相変わらず)
- Consul https://www.consul.io/
- HashiCorp製
- etcd https://github.com/coreos/etcd
- Goで書かれてるやつ
自分で使うとしたらこの辺りになるかなぁ。あとは用途要件に合わせて、RabbitMQ + Zabbixみたいな組み合わせと比較していく感じ。
DynamoDBがスケールしねーぞの話に関するメモ
You probably shouldn’t use DynamoDB https://t.co/Y8cOEDqgeq 10GB毎にノードが勝手に増えるので分散writeで速度激落ちのDynamoDB,実は全然スケールしないという.巨大データは素直にBigTableを使えと. pic.twitter.com/zh8CRImbz6
— Yuta Kashino (@yutakashino) 2017年7月8日
こういう話が流れてきたので元記事を読んでみたのだけど、そもそもDynamoDBの仕組みを把握してなかったので理解ができなかった。ということで、ざっと調べた結果を、自分(Cassandraをがっつり触ってた経験あり)が後から読んでわかればいいや程度のメモに。
DynamoDBとは
- オートスケールするDB
- データはパーティションに分割されていて、パーティションはパーティションキーを元に特定する
- パーティションとデータ分散 - Amazon DynamoDB
- ストレージ容量ではなくスループットで課金される
- Read Capacity Unit (RCU):1RCUで、1秒あたり最大2回のRead
- Consistent Readオプションを使う場合、2倍のユニットを消費する
- Write Capacity Unit (WCU):1WCUで、1秒あたり最大1回のWrite
- Read Capacity Unit (RCU):1RCUで、1秒あたり最大2回のRead
- パーティションはデータサイズとスループット設定により自動で分割される
- 1パーティションが10GBを超えるか、1パーティションに対し3000RCU/1000WCUを超えるスループット設定がなされた場合に分割
- テーブルのベストプラクティス - Amazon DynamoDB
DynamoDBのスループット設定の考え方の注意点
このスライドを一回読むとよさそう。
端的には、
100,000RCUを持っていたとしても、パーティションが50個あれば、1パーティションあたりのRCUは2,000となる。
で、パーティションキーを元にアクセスするパーティションを特定する性質上、以下ようなアクセスの偏りが生じるケースがある。
元記事の指摘ポイント
言いたいことはわかるけど、DynamoDBの仕組み(課金の仕組み)を考えると、なかなか難しい問題。分散DBは難しい。