Skip to content

Commit 88e66c9

Browse files
committed
stored_dict: add support for write batches
Add unit test of atomicity. Use a write batch for DB upgrades.
1 parent 3c5770d commit 88e66c9

5 files changed

Lines changed: 52 additions & 11 deletions

File tree

electrum/json_db.py

Lines changed: 17 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)
@@ -309,8 +311,22 @@ def dump(self, *, human_readable: bool = True) -> str:
309311
sort_keys=bool(human_readable),
310312
)
311313

314+
@contextmanager
315+
def write_batch(self):
316+
assert self._write_batch is False
317+
self._write_batch = True
318+
try:
319+
yield
320+
finally:
321+
# FIXME: if an exception is raised here, changes will not be
322+
# written to disk, but they will be added to the in-memory dict.
323+
self._write_batch = False
324+
if self.storage:
325+
self.write()
326+
312327
@locked
313328
def write(self):
329+
assert self._write_batch is False
314330
if not self.storage:
315331
return
316332
if self.storage.should_do_full_write_next():
@@ -319,7 +335,7 @@ def write(self):
319335
self._append_pending_changes()
320336

321337
def close(self):
322-
# do not call write
338+
# do not call write, because we may need to close the DB after an exception was raised during a batch write
323339
self._is_closed = True
324340

325341
def is_closed(self):

electrum/stored_dict.py

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

299+
def write_batch(self):
300+
return self._db.write_batch()
301+
299302
def dump(self) -> dict:
300303
data = {}
301304
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
@@ -1565,14 +1565,15 @@ def upgrade_wallet_db(data: 'StoredDict', do_upgrade: bool) -> Tuple[dict, bool]
15651565
)
15661566

15671567
data._db._should_convert = False
1568-
dbu = WalletDBUpgrader(data)
1569-
if dbu.requires_split():
1570-
raise WalletRequiresSplit(dbu.get_split_accounts())
1571-
if dbu.requires_upgrade() and do_upgrade:
1572-
dbu.upgrade()
1573-
was_upgraded = True
1574-
if dbu.requires_upgrade():
1575-
raise WalletRequiresUpgrade()
1568+
with data.write_batch():
1569+
dbu = WalletDBUpgrader(data)
1570+
if dbu.requires_split():
1571+
raise WalletRequiresSplit(dbu.get_split_accounts())
1572+
if dbu.requires_upgrade() and do_upgrade:
1573+
dbu.upgrade()
1574+
was_upgraded = True
1575+
if dbu.requires_upgrade():
1576+
raise WalletRequiresUpgrade()
15761577
data._db._should_convert = True
15771578
return was_upgraded
15781579

tests/test_stored_dict.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ 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+
self.assertEqual(sd._db._write_batch, False)
77+
# at this point, the StoredDict length is 2
78+
self.assertEqual(len(sd), 2)
79+
sd.close()
80+
# check that changes have not been written to disk
81+
sd = DictStorage(self.path)
82+
self.assertEqual(len(sd), 1)
83+
6184
async def test_dangling_dict(self):
6285
storage = DictStorage(self.path)
6386
storage['a'] = {'b': {'c': 0}}

0 commit comments

Comments
 (0)