Failover 時の Prisma の問題
DB に問題が発生した際に Writer instance と Reader instance がパチっと切り替わる。
切り替わった後、書き込めないエラーが発生した。
PrismaClientUnknownRequestError:
Error occurred during query execution:
ConnectorError(ConnectorError {
user_facing_error: None,
kind: QueryError(Server(ServerError {
code: 1792,
message "Cannot execute statement in a READ ONLY transaction.",
state: "25006"
}))
})
こんなようなエラーが出る。
API などの基本コネクションを貼り続けるようなアプリケーションでは、DB 側で Failover が行われると、コネクションは、元 Writer instance・新 Reader instance に繋がりっぱなしで、書き込みエラーが発生する。Prisma は自動でコネクションを貼り直してくれたりしない模様。
Failover を検知し、disconnect して上げる必要がある。
ローカルで問題を再現する
ちょっとエラーは違うんですが、read_only または innodb_read_only で MySQL を立ち上げれば、PrismaClientUnknownRequestError を発生させることができる。Docker の場合は、例えばこんな感じ。
version: '3.8'
services:
db:
image: mysql:5.7
ports:
- '3306:3306'
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASS}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
TZ: 'Asia/Tokyo'
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./mysql/sql:/docker-entrypoint-initdb.d
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --innodb_read_only
container_name: ${CONTAINER_NAME_DB}
Prisma のエラーを判別する
Prisma のエラーは、5種類ある。
- PrismaClientKnownRequestError
- PrismaClientUnknownRequestError
- PrismaClientRustPanicError
- PrismaClientInitializationError
- PrismaClientValidationError
全てのエラーがエラーコードを返してくれれば話しは単純だが、エラーコードを返してくれるのは、PrismaClientKnownRequestError
, PrismaClientInitializationError
だけだ。
Prisma で発生したエラーの Error instance のプロパティで、PrismaClientUnknownRequestError
が取得できればそれが良いが、出来ない。
という訳で、以下のように判別する
if (err instanceof Prisma.PrismaClientUnknownRequestError) {
prisma.$disconnect();
}
他のエラーも同様に認識できる。
5つのエラーのうち、PrismaClientRustPanicError
は、コネクションを貼り直してもダメそうだが、それで復旧できるならラッキーくらいでこれも díconnect しておく。PrismaClientUnknownRequestError
は、エラーメッセージから、MySQL のエラーコード等で絞り込んであげた方が、本当は良いが、UnknownError が何通りあるか分からないし、今回は disconnect するだけなので、ちょっと乱暴だが、全部 disconnect する。エラーメッセージは一見 object のようで string なので、扱いやすくはないし。
PrismaClientKnownRequestError
, PrismaClientInitializationError
のうち、P1XXX
, P5XXX
のコードのエラーは、そもそもコネクションできていなさそうなので、disconnect する必要があるのか分からないが、念の為。
なお、PrismaClientInitializationError
は、MySQL を立ち上げた状態で、API を起動し、MySQL を落とすことで再現できる。
再現してみると、errorCode: undefined
…… エラーコードなし……………
そんなような事に留意して、処理を書けば良い。
都度切断すれば、この問題は起きないが、API のように都度切断する必要がないものは、パフォーマンスが低下するので、このような処理を追加する必要がある。
落とし穴
この処理を先に実装してしまうと、発見が遅れそうな問題が実はひとつある。
Amazon Aurora Global Database
などの、Secondary cluster で、write forwarding を使っているような場合。
Secondary cluster には、Read instance しかないので、エラーとしては、前述の UnknownError になるはずだが、disconnect しても意味がない。
この場合は、MySQL の変数として、aurora_replica_read_consistency
を設定してやる必要があるが、Prisma の設定で、
DATABASE_URL=mysql://${user}:${password}@${host}:3306/${dbname}?aurora_replica_read_consistency=session
などとして、設定することができない。
どこでどう設定するのが正解なのか分からないが、どこかで、
await prisma.$executeRaw`set aurora_replica_read_consistency = 'session';`
.catch((err) => handlePrismaError(err));
こんなようなことをしてあげる必要がある。もっとスマートな方法があったら、誰か教えてほしい。
おわり。