Skip to content

Commit 9938ba5

Browse files
committed
stored_dict: add support for write batches
Add unit test of atomicity. Use a write batch for DB upgrades.
1 parent d3bb453 commit 9938ba5

5 files changed

Lines changed: 49 additions & 11 deletions

File tree

electrum/json_db.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import copy
2727
import json
2828
from typing import TYPE_CHECKING, Optional, Sequence, List, Union, Dict, Any
29+
from contextlib import contextmanager
2930

3031
import jsonpatch
3132
import jsonpointer
@@ -94,6 +95,7 @@ def __init__(
9495
self._is_closed = True
9596
self.lock = threading.RLock()
9697
self.pending_changes = [] # type: List[str]
98+
self._write_batch = False
9799
self._modified = False
98100
if self.path:
99101
self.storage = FileStorage(path, allow_partial_writes=allow_partial_writes)
@@ -318,8 +320,20 @@ def dump(self, *, human_readable: bool = True) -> str:
318320
sort_keys=bool(human_readable),
319321
)
320322

323+
@contextmanager
324+
def write_batch(self):
325+
# FIXME: if an exception is raised during the context, changes will not
326+
# be written to disk, but they will be added to the in-memory dict.
327+
assert self._write_batch is False
328+
self._write_batch = True
329+
yield
330+
self._write_batch = False
331+
if self.storage:
332+
self.write()
333+
321334
@locked
322335
def write(self):
336+
assert self._write_batch is False
323337
if not self.storage:
324338
return
325339
if self.storage.should_do_full_write_next():
@@ -328,7 +342,7 @@ def write(self):
328342
self._append_pending_changes()
329343

330344
def close(self):
331-
# do not call write
345+
# do not call write, because we may need to close the DB after an exception was raised during a batch write
332346
self._is_closed = True
333347

334348
def is_closed(self):

electrum/stored_dict.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ def __init__(self, db: BaseDB, key: _FLEX_KEY, parent):
290290
def should_convert(self):
291291
return self._db._should_convert
292292

293+
def write_batch(self):
294+
return self._db.write_batch()
295+
293296
def dump(self) -> dict:
294297
data = {}
295298
for k, v in self.items():

electrum/wallet.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4510,8 +4510,6 @@ def restore_wallet_from_text(
45104510
if gap_limit_for_change is not None:
45114511
db.put('gap_limit_for_change', gap_limit_for_change)
45124512
wallet = wallet_factory(db, config=config)
4513-
if storage:
4514-
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
45154513
wallet.synchronize()
45164514
msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
45174515
"Start a daemon and use load_wallet to sync its history.")

electrum/wallet_db.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,14 +1562,15 @@ def upgrade_wallet_db(data: 'StoredDict', do_upgrade: bool) -> Tuple[dict, bool]
15621562
)
15631563

15641564
data._db._should_convert = False
1565-
dbu = WalletDBUpgrader(data)
1566-
if dbu.requires_split():
1567-
raise WalletRequiresSplit(dbu.get_split_accounts())
1568-
if dbu.requires_upgrade() and do_upgrade:
1569-
dbu.upgrade()
1570-
was_upgraded = True
1571-
if dbu.requires_upgrade():
1572-
raise WalletRequiresUpgrade()
1565+
with data.write_batch():
1566+
dbu = WalletDBUpgrader(data)
1567+
if dbu.requires_split():
1568+
raise WalletRequiresSplit(dbu.get_split_accounts())
1569+
if dbu.requires_upgrade() and do_upgrade:
1570+
dbu.upgrade()
1571+
was_upgraded = True
1572+
if dbu.requires_upgrade():
1573+
raise WalletRequiresUpgrade()
15731574
data._db._should_convert = True
15741575
return was_upgraded
15751576

tests/test_stored_dict.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ def test_db_iterators(self):
5858
for i, v in enumerate(sl):
5959
self.assertEqual(i, v)
6060

61+
def test_write_batch(self):
62+
# test that batches are written atomically
63+
sd = DictStorage(self.path)
64+
with sd.write_batch():
65+
sd['a'] = 0
66+
self.assertEqual(len(sd), 1)
67+
with sd.write_batch():
68+
sd['a'] = 1
69+
self.assertEqual(len(sd), 1)
70+
try:
71+
with sd.write_batch():
72+
sd['b'] = 1
73+
raise Exception('blah')
74+
except Exception as e:
75+
pass
76+
# at this point, the StoredDict length is 2
77+
self.assertEqual(len(sd), 2)
78+
sd.close()
79+
# check that changes have not been written to disk
80+
sd = DictStorage(self.path)
81+
self.assertEqual(len(sd), 1)
82+
6183
async def test_dangling_dict(self):
6284
storage = DictStorage(self.path)
6385
storage['a'] = {'b': {'c': 0}}

0 commit comments

Comments
 (0)