IndexedDb:database.close() 挂起

IndexedDb: database.close() is hanging

我正在为我使用 IndexedDb 的一些数据存储编写集成测试。这涉及通过创建数据库来设置每个测试,执行一些操作(运行 测试),然后通过调用 database.close() 拆除每个测试,然后通过调用 window.indexedDB.deleteDatabase(DB_NAME) 删除数据库。

documentation for IDBDatabase.close()指出”IDBDatabase接口的close()方法returns立即关闭并在一个单独的线程中关闭连接。Close不会不接受在数据库实际关闭后触发的回调,因此无法确定连接是否已关闭。

我的初始测试在尝试使用 window.indexedDB.deleteDatabase(DB_NAME) 删除数据库时超时。测试所做的只是打开数据库,除此之外没有执行任何操作。我能够通过在调用 database.close() 后添加一个小的超时来解决这个问题。

添加另一个向数据库添加数据的测试后,删除数据库的调用再次挂起,即使超时也是如此。数据 does 成功添加并且事务回调完成,所以我不确定为什么对 database.close() 的调用会挂起。任何见解将不胜感激。

编辑

我创建了一个项目来说明这个问题。代码可以在这里找到:https://github.com/bgourlie/idb-hang-repro

需要注意的几件事 -- 重现是用 dart 编写的,因为这就是我看到这个问题的地方。 Chrome 和 Dartium(嵌入了 dart VM 的 Chromium 的特殊版本)都重现了该行为。对于那些没有使用过 Dart 但仍然想解决这个问题的人,请按照以下步骤操作:

这将启动 pub 开发服务器,很可能在 http://localhost:8080。我已经在测试运行器中重现了这个问题,可以在 http://localhost:8080/tests.html 访问它。测试超时和显示任何输出都需要很短的时间。还有重要的打印消息将显示在开发者控制台上。

如您所见,无法知道 database.close() 何时完成,并且 window.indexedDB.deleteDatabase 在尝试删除仍处于打开状态的数据库时表现得有点奇怪。但这些都不是无法克服的问题。看看what actually happens when you try to delete a database。我读到的是如果你试图删除一个仍然打开的数据库,它会在每个打开的数据库连接上触发一个 versionchange 事件。然后,如果数据库仍然打开,它会为你的 deleteDatabase 触发一个 blocked 事件而不是 success,但它仍然会继续并以任何一种方式删除数据库。

因此,我做了这样的事情:

var request = indexedDB.deleteDatabase("whatever");
request.onsuccess = function () {
    success();
};
request.onfailure = function (event) {
    fail(event);
};
request.onblocked = function () {
    success();
};

更好的解决方案可能是以某种方式侦听上述 versionchange 事件并在那里关闭数据库,但我不知道该怎么做。只有当您在删除数据库时关心数据库是否打开时,这才重要,而您可能不关心。

因为我一直在为我的 idb_shim 项目做大量的索引数据库实验,所以我可以分享我能够在新数据库上编写单元测试,只要我确保

  • 最后一个事务在关闭数据库前完成
  • 数据库在删除前关闭

据此,我能够修复您的测试项目(感谢分享),并进行以下更改:

  • 在你的第一个测试中,确保你 return 来自 tx.completed 的未来,这样 tearDown 在事务完成之前不会被调用(不需要添加完成者,人们往往会忘记你可以在 test/setUp/tearDown 中安全地 return 未来并且单元测试框架将为他们 'wait'):
return tx.completed.then((_) {
    print('transaction complete.');
},...
  • 在您的拆解中,在删除之前调用 db.close。

附带说明一下,我通常更喜欢在设置功能中删除数据库,这样如果之前的测试失败且数据库未清理,它也能正常工作。所以你现有代码的解决方案是:

setUp(() {
  return dom.window.indexedDB.deleteDatabase(DB_NAME, onBlocked: (e) {
    print('delete db blocked, but completing future anyway');
  }).then((_) {
    print('db successfully deleted!');
    return dom.window.indexedDB.open(DB_NAME, version: 1, onUpgradeNeeded: (VersionChangeEvent e) {
      print('db upgrade called (${e.oldVersion} -> ${e.newVersion})');
      final db = (e.target as Request).result;
      db.createObjectStore('foo', autoIncrement: true);
    }, onBlocked: (e) => print('open blocked.')).then((_db_) {
      print('db opened.');
      db = _db_;
    });
  });
});

tearDown(() {
  // note the 'close' here
  db.close();
});

group('indexed DB delete hang repro', () {
  test('second test which will add data', () {
    print('adding data in second test...');
    final tx = db.transaction('foo', 'readwrite');
    final objectStore = tx.objectStore('foo');
    objectStore.add({
      'bar': 1,
      'baz': 2
    }).then((addedKey) {
      print('object added to store with key=$addedKey');
    }, onError: (e) => print('error adding object!'));

    // note the 'return' here
    return tx.completed.then((_) {
      print('transaction complete.');
    }, onError: (e) => print('transaction errored!'));
  });

  test('call setup and teardown', () {
    print('just setup and teardown being called in first test.');
  });
});