データ永続化で実現する React Native 製 POS レジのオフライン対策
ダイニーの whatasoda です。
モバイルオーダー一体型 POS レジで飲食店を支えるダイニーの障害対策、連載第二回目です。
今回はデータ永続化で実現する React Native 製レジのオフライン対策と題して、システムに障害が発生しても最低限のオペレーションをレジ単体で実現するガチャレジ機能についてご紹介します。
ガチャレジとは、レジとして最低限の機能であるお金の管理に対応しているレジのことを指す俗称です。ダイニーがサービスを提供する飲食店ではお金の管理だけの機能では満足な営業を行うには不十分なため、この連載の中での「ガチャレジ」という表現は最低限の注文管理と会計機能を備えたレジまたはそのような振る舞いをする機能のことを指します。
なぜお金の管理だけでは営業できないのか、どうしてガチャレジ対応が必要なのかについての説明は第一回の記事をご覧ください。
ガチャレジ機能が想定する状況
開発にあたり、ガチャレジ機能が発動するときはどのような状況かを考える必要があります。基本的にはダイニーのシステムやネットワークに何らかの障害が発生していることが想定されますが、それにもグラデーションがあります。障害点ごとに障害が起きているパターンと起きていないパターン両方を想定して、それらがレジ目線でどのように見えているのかを考え、全てのパターンで考慮漏れが起きないような設計・実装が求められるのです。
ここでは主要な障害点とそのバリエーションを紹介します。
- ダイニーのシステムが正常に機能していないかもしれないし、機能しているかもしれない
- インターネットに繋がっていないかもしれないし、繋がっているかもしれない
- 営業の最初から障害が起きているかもしれないし、途中から障害が起きているかもしれない
これらに加えて、店舗スタッフによる操作で Wi-Fi の切り替えやレジアプリの再起動などが行われる可能性も考慮している必要があります。
ガチャレジ機能の要件
機能要件を端的に表すと「レジ単体で最低限の注文管理と会計が行えて、後からデータベースに記録できること」です。注文にせよ会計にせよ、まずはその操作を行うために必要なマスターデータを覚えていることが求められます。
- 店舗の基本情報(店舗名やレジの振る舞いの設定など)
- 店舗内のテーブルの構成
- メニュー情報
- 対応可能な支払い方法
これらのマスターデータは変更されることは少なく、正常に動作しているときのものを記録しておけば要件を満たせます。これで要件の前半部分は満たすことができますが、注文や会計の操作によって発行されたデータについても記録しておくことが求められます。でなければ後からデータベースに記録することができないからです。
- どのテーブルをアクティブにしたか
- どのテーブルでどのメニューをいくつ注文したか
- どのテーブルでどの注文を会計したか
加えて、これらの記録されたデータをリアルタイムに UI に反映させたり、一部の機能を制限することも求められます。
今回はデータの保存に関する内容を重点的に取り上げます。説明を簡単にするため、マスターデータなどの変更が少ないデータのことを静的なデータ、注文や会計の操作で発行されたデータのことを動的なデータと呼ぶこととします。
エラーを考えうる限界まで切り分ける
動的なデータを保存するといっても全てのデータを保存すればいいわけではありません。
サーバーへのリクエストが失敗する要因は様々で、そもそもサーバーまで到達できていないケースや、到達できていても処理にものすごく時間がかかっているケース、サーバーに到達していて問題なく処理できているがリクエストに不備があって失敗しているケースなど、挙げだすとキリがありません。
保存したデータは後からデータベースに記録する必要がありますが、その際データに不備があれば弾く必要があるため、不備があることがわかっているデータを保存してしまうと通常の操作では消すことができないデータが生まれてしまいます。
そのため、エラーの種類が膨大であっても、できる限り切り分けて適切な処理を行うように実装する必要があるのです。
アプリを閉じられても消されても、この世のどこかにはデータがあるようにする
静的なデータも動的なデータも、どこに保存するかは慎重に決めていく必要があります。
まず静的なデータですが、これは言い換えればデータベース上のデータのローカルバックアップに過ぎません。アプリが閉じられた程度で失われてしまっては実用上困りますが、アプリのアンインストールがされて端末内のデータが全て削除されてもまたデータベースから取得することができるため、そこまでケアする必要はないでしょう。つまり、静的なデータはメモリ上の保存では不十分だが端末内部のファイルに保存できていれば実用に足るということです。
動的なデータはどうでしょうか。これはレジの内部で生み出されたもので、そのデータが消えてしまうとこの世界から完全に失われてしまいます。アプリが閉じられて失われては話になりませんし、予期せぬ事態によりアプリをアンインストールせざるを得ない状況になったとしてもどこかでそのデータを記録できていないと重要な注文や会計のデータがこの世界から完全に失われてしまうことになってしまいます。そのため、動的なデータは端末内部のファイル以外のどこか違う場所にも保存しておくことが理想です。
ダイニーでは現在、静的なデータの保存については Apollo Client のキャッシュの永続化で行い、動的なデータの保存については端末内部への保存に加えて Firestore とデータを同期してバックアップとして利用することでそれぞれの求められるデータ保持のレベルを満たしています。
ライブラリに頼り切った永続化に潜むリスク
ガチャレジ機能を実装した当初、 Apollo Client のキャッシュの永続化には apollo3-cache-persist を利用し、 Firestore とのデータの同期には Firestore SDK が備えている Firestore Persistency という機能を利用して端末内部へのデータ保存についてもそこに任せていました。しかし現在は Firestore については内製した仕組みでデータ保存も同期も置き換えており、 Apollo Client のキャッシュについても永続化部分のロジックの内製化を予定しています。
注意していただきたいのはライブラリに頼ること全て否定するわけではないということです。むしろ、乗れる巨人の肩には乗っておきたいものです。しかし、巨人が自分の思うように動いてくれるとは限らないということは常に意識しておくべきです。
全てが1つの領域に保存されるため不具合の影響範囲がコントロールしにくい
レジが取り扱うデータの全てで高い品質のデータ永続化が求められるわけではありません。システムが正常なときにだけ利用できればよい発展的な機能で使うデータは永続化する必要がなかったり、大量に発行される統計用のログのデータなどはむしろストレージを圧迫してしまう原因になるため永続化されてほしくない場合もあります。
ライブラリによって実現されているデータ永続化の多くはデータ取得・発行の仕組みに組み込まれていたりそれを拡張する形で作られていることが多く、その仕組みを使って取得・発行されたデータ全てに永続化が適用されます。永続化したいデータは確実に永続化しておきたいですが、永続化が不要なデータに対する処理によって本当に永続化が必要なデータに対する処理が阻害されてしまったり、そもそものレジアプリのパフォーマンスに影響を及ぼしてしまうことも考えられます。
また、データの保存領域も1つのファイルにまとめられてしまいます。仮に分割されていても分割の基準は自分たちが望むものではないことがほとんどでしょう。これは保存されたファイルが破損したときの影響範囲を自分たちでコントロールできないことを意味します。
実装がブラックボックスであるため不具合の調査や対処が困難
データ永続化の仕組みは複雑なものが多いです。特に Firestore Persistency は Firestore のクラウド上のデータとの同期についてのロジックも含むため、特別に複雑です。
プログラムの不具合を修正するためにはその原因を知る必要がありますが、根が深かったりインパクトの大きい問題ほど実装の深部に関係していることが多いです。そのためデータ永続化の仕組みでなにか問題が発生したときの対応難度は高く、仮にできたとしても時間がかかってしまいます。
不具合に対応するコストやリードタイムが大きいと、サービスが提供しているビジネス的な価値に影響を及ぼします。例えば飲食店のオペレーション効率が悪化したり、データの正確性が損なわれてしまったりするのです。ダイニーのサービスの場合それらの影響は最終的に飲食店の人的リソースを余計に消費してしまうことに繋がることが多いです。
ダイニーとしても不具合対応のためにエンジニアや顧客サポートチームのリソースを使うことになるため、新しい価値への投資が滞ることにも繋がってしまいます。現実的なコストで不具合に対応できるような技術構成であることは、飲食業界がより発展するためにも必要なことなのです。
React Native アプリで利用されることがあまり想定されていないことがある
これは Firestore Persistency に限った話になりますが、 Web ブラウザ上で動くことを前提として作られたライブラリであっても利用しなくてはならないことがあります。その場合は polyfill などと組み合わせて利用することになりますが、ただでさえ複雑な構造のライブラリを利用しているのにそこに polyfill が追加されるとその複雑度は更に高くなってしまいます。
また、ネイティブで実装されていないものを利用するため、安定性やパフォーマンスへの影響についても検討する必要があるでしょう。
内製化による信頼性の向上
このような課題を解決するためにダイニーでは永続化の仕組みの内製化を進めています。
今回挙げた Apollo Client のキャッシュや Firestore Persistency の他にもデータ永続化を必要とする箇所はあるため、全ての箇所で共通して利用できる基本的なデータ永続化の仕組み(ここでは Persistency Core と呼ぶこととします)を実装しています。汎用的に再利用可能な形で Persistency Core を実装したうえで Apollo Client とのつなぎ込みや Firestore との同期の実装を被せていくことで理想的なデータ永続化の仕組みを実現していく作戦です。
すべてのデータ永続化で保存したデータの肥大化のリスクを抑えたり、データ破損による影響をコントロールできるように、 Persistency Core は以下の要件を満たすように実装しています。
- データの種類毎に別のファイルに記録できる
- データをパーティショニングしてファイルを分割できる
- この仕組み自体に問題があってもデータが完全に失われないようにログに流すことができる
また、 Persistency Core 自体のインターフェイスは in-memory の store のようなものにしていて、個別のデータ永続化の実装の際には永続化のロジックをあまり意識することなく実現できるようにしています。
今回のまとめ
今回は障害対策の要とも言えるガチャレジ機能について、データの永続化に焦点を当てて説明しました。この機能によって飲食店が全く営業できなくなるという状況からはギリギリ脱することができます。
しかしながら、この機能だけでは通常時の店内オペレーションを実現することはできません。次回の記事ではレジがローカルネットワークを利用してキッチンプリンターやハンディと連携する機能について説明します。(次は連載外の記事を出すかもしれません!)
なお、次回記事の内容の一部は先日開催された TSKaigi Mashup #1 にて発表させていただきました。
We’re hiring!
ダイニーで一緒に働きませんか?
Discussion