解构赋值踩坑:神秘失踪的数据
话说在我们 PLCT ArchRV 组,基础设施都是我们自己搭出来的,分工、打标记、追踪状态全部靠 gh: XieJiSS/plct-archrv-pkg-bot
、gh: cubercsl/archrv-pkg-notification-bot
和 gh: Ast-x64/plct-archrv-status-worker
实现。近日我们在维护标记和追踪状态时,发现 plct-archrv-pkg-bot
(以下简称 bot)在重启时会丢失几条记录。
最开始,怀疑是重启的时候还有数据没写入到磁盘,导致数据丢失。但是经过排查,发现并不太可能,理论上所有的数据都是在修改时当场写入磁盘的。保险起见,增加了使用同步 IO 的 storePackageMarksSync
,通过 npm: async-exit-hook
在进程退出时强制保存。然而还是丢数据。
看 log 可以确认,在退出时把内存里的数据全部写入到磁盘了,那丢数据是怎么回事呢?
又开始考虑是不是 race condition,于是通过 npm: lockfile
实现了进程的保护锁,确保同时只有一个 bot 进程在运行。除了没效果以外,效果非常好。看来也不是多进程并行写入的问题。
最后经过排查,发现问题的根源在 export
和 import
上。
最小可复现样例如下:
1 | // a.js |
1 | // b.js |
可以看到,在执行 cleanUp
之后的数据全部丢失了。这是因为在 a.js
里对 A
重新赋值过后,b.js
解构赋值得到的 A
和 a.js
里的 A
不再指向同一个对象。解构赋值出来的 identifier A
指向的是 module.exports
的 A
属性在解构赋值那一时刻所指向的内存,随后执行的 A = A.filter
只是在 a.js
内部修改了 A
的指向,外部解构赋值出的 A
并不会随之更新。我们可以用 cpp 来翻译一下第一种写法:
1 | const auto *A = a.A; |
概括一下:如果把 JS 里面解构赋值拿到的东西当做 &ref
来理解(这是常有的事)就会导致这种 inconsistency 的出现。
如果想修复这个问题,需要在 a.js
里面把 A
声明为 const
,随后将 Array#filter
替换为自己实现的 in-place filter
,详见这个 commit。当然你也可以在 b.js
里始终使用 a.A
的方式来访问 A
。