2018-10-01

Promise.reject()で処理は止まるがそのタイミングは「期待」通りとは限らない

まとめ

止まることは止まるが、思ったタイミングで止まるかどうかは分からないので、それ前提で作る必要がある。

例えば以下は思い違い。

  • 最初の一つが rejected になったら、その一つめで必ず止まる

実際には

  • 最初の rejected が Promise.all に伝わる段階ですでにいくつも並列で処理が走っている可能性があるので、rejected 以降にエラーがバンバン発生することはあり得る

もっと言うと rejected が伝わった段階で中の複数の Promise の結果は fulfilled と rejected が混ざっている可能性もあるので、全体の rejected を retry すると fulfiled になった Promise をもう一度実行する場合もある。

ということは例えば何かの POST 処理を Promise.all で回しつつ retry を考慮している場合、すでに成功しているデータが複数回 POST される可能性がある。

もっと言うと、

そこで何かの拍子に unique 制約に引っかかって逆にエラーがあとから起きたりもする。

ということ。

非同期並列面倒くせぇ…。

何で悩んでいたのか

Promise.all(Array.map(e => Promise))

なコードがあった。これを呼び出す function foo() があった。

function foo(data) {
  Promise.all(data.map(e => Promise))
}

この foo() を直接読んでテストしている時は最初の一つが reject になった段階で止まっていた。で、

一つめが rejected になったら止まると思い込んでしまった

しかし、次に

function bar() {
  ..
  ..
  foo()
  .then(() => {
    ..
  })
  .catch((err) => {
    ..
  })
  ..
}

の bar() でテストすると、

大量にエラーが起きてしまったので、「止まらないじゃん!」と、また勘違い

「何かコードの書き方を間違えているから止まらないのでは…」とありもしない原因を探る旅に出てしまったのでした…。

正解は、Promise.all([Promise, ..]) の実行に至るまでに払っている実行コストの間で中の Promise が並列に実行を開始してしまい、結果、

呼び出し元が rejected でストップしたタイミング以降にいくつもエラーが起きて当然の状況になる

つまり

一つめの rejected で止まるのも、まったく止まっていないように見えるのも、どちらも正しい!!!

非同期並列難しい!

何が難しいって同期的な処理を期待しているシステムと繋げるのが難しい!

愚痴まじりで言うと Node.js をこれまで避けてたのはやっぱり正しかったなぁと思った。それは動作が安定しているかどうかとか、高速かどうかとか、そういうことじゃなくて動作の考え方そのものが難しいので、カジュアルな変更を前提にしたコードには向いていないなぁという意味。記法だけを見れば callback 地獄は Promise によって解決されたかのように見えるが、動作については非同期並列という特徴はそのままなので、伝統的な、同期的で排他ロック可能なコードを扱う脳との切り替えおよび整合性の確保が難しい。整合性の確保は複数のサービスの連携が前提となるマイクロサービスにおいては必須の要件になってくる。

今後 FaaS, サーバレス化を進めるに当たってはここはキモになってくるし、

  • スケール、非同期前提のインフラに載せつつ変更の少ない部分に使っていく
  • 同期的な仕組みの側も非同期並列の仕組みから繋げる際のエラーの起き方を知っている必要がある
  • モニタリングの意味も同期的な仕組みとちょっと変わってくる

かなぁということを思っている。

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm