Skip to content

Commit 3f43357

Browse files
committed
Truncate (to a configurable limit) very large lock errors. Fixes #511.
1 parent c05aea0 commit 3f43357

File tree

6 files changed

+64
-28
lines changed

6 files changed

+64
-28
lines changed

CHANGES.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
4.1.1 (unreleased)
66
==================
77

8-
- Nothing changed yet.
8+
- Make certain lock-related errors in very large transactions no
9+
longer produce huge (unusable) error messages when logged or
10+
printed. Now such messages are truncated. Previously, they were
11+
allowed to grow without bounds. See :issue:`511`
912

1013

1114
4.1.0 (2024-10-11)

src/relstorage/adapters/locker.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,10 @@ def _modify_commit_lock_kind(self, kind, exc): # pylint:disable=unused-argument
254254
return kind
255255

256256
def reraise_commit_lock_error(self, cursor, lock_stmt, kind):
257-
v = sys.exc_info()[1]
258-
kind = self._modify_commit_lock_kind(kind, v)
259-
if self.driver.exception_is_deadlock(v):
257+
_current_exc_type, current_exc, current_tb = sys.exc_info()
258+
259+
kind = self._modify_commit_lock_kind(kind, current_exc)
260+
if self.driver.exception_is_deadlock(current_exc):
260261
kind = getattr(kind, 'DEADLOCK_VARIANT', UnableToLockRowsDeadlockError)
261262

262263
try:
@@ -270,13 +271,15 @@ def reraise_commit_lock_error(self, cursor, lock_stmt, kind):
270271
logger.debug("Failed to acquire commit lock:\n%s", debug_info)
271272

272273
message = "Acquiring a lock during commit failed: %s%s" % (
273-
sys.exc_info()[1],
274+
current_exc,
274275
'\n' + debug_info if debug_info else '(No debug info.)'
275276
)
276277
val = kind(message)
277-
val.__relstorage_cause__ = v
278-
del v
279-
raise val.with_traceback(sys.exc_info()[2])
278+
val.__relstorage_cause__ = current_exc
279+
try:
280+
raise val.with_traceback(current_tb) from current_exc
281+
finally:
282+
del current_exc, current_tb
280283

281284
# MySQL allows aggregates in the top level to use FOR UPDATE,
282285
# but PostgreSQL does not, so we have to use the second form.
@@ -342,6 +345,8 @@ def _get_commit_lock_debug_info(self, cursor, was_failure=False):
342345
that will be added to the exception message when a commit lock cannot
343346
be acquired. For example, it might list other connections that
344347
have conflicting locks.
348+
349+
:rtype: str
345350
"""
346351
return ''
347352

src/relstorage/storage/tpc/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ def local_client(self):
133133
def store_connection(self):
134134
conn = self._storage._store_connection_pool.borrow()
135135
# Report on the connection we will use.
136-
# https://github.com/zodb/relstorage/issues/460
137-
logger.info("Using store connection %s", conn)
136+
# https://github.com/zodb/relstorage/issues/460
137+
logger.debug("Using store connection %s", conn)
138138
return conn
139139

140140
@store_connection.aborter

src/relstorage/storage/tpc/temporary_storage.py

+4
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ def __repr__(self):
155155
)
156156

157157
def __str__(self):
158+
"""
159+
The string of this object can get very, very long, if the transaction
160+
modifies a lot of objects.
161+
"""
158162
base = repr(self)
159163
if not self:
160164
return base

src/relstorage/storage/tpc/tests/test_vote.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
Tests for vote.py.
44
55
"""
6-
from __future__ import absolute_import
7-
from __future__ import division
8-
from __future__ import print_function
9-
106

117
import unittest
8+
from unittest.mock import patch as Patch
129

1310
from hamcrest import assert_that
1411
from nti.testing.matchers import verifiably_provides
@@ -227,7 +224,7 @@ def _callFUT(self, ex, state, required_tids):
227224
add_details_to_lock_error(ex, state, required_tids)
228225
return ex
229226

230-
def _check(self, kind):
227+
def _check(self, kind, truncated=False):
231228
ex = kind('MESSAGE')
232229
storage = MockStorage()
233230
storage.keep_history = False # pylint:disable=attribute-defined-outside-init
@@ -237,9 +234,9 @@ def _check(self, kind):
237234
self._callFUT(ex, state, {1: 1})
238235
s = str(ex)
239236
self.assertIn('readCurrent {oid: tid}', s)
240-
self.assertIn('{1: 1}', s)
241-
self.assertIn('Previous TID', s)
242-
self.assertIn('42', s)
237+
self.assertIn('{1: 1}' if not truncated else '{...', s)
238+
self.assertIn('Previous TID' if not truncated else '<...', s)
239+
self.assertIn('42' if not truncated else '', s)
243240
self.assertIn('MESSAGE', s)
244241

245242
def test_add_details_to_UnableToAcquireCommitLockError(self):
@@ -250,6 +247,12 @@ def test_add_details_to_UnableToLockRowsToModifyError(self):
250247
from relstorage.adapters.interfaces import UnableToLockRowsToModifyError
251248
self._check(UnableToLockRowsToModifyError)
252249

250+
def test_add_truncated_details_to_UnableToLockRowsToModifyError(self):
251+
from relstorage.adapters.interfaces import UnableToLockRowsToModifyError
252+
from .. import vote
253+
with Patch.object(vote, 'DETAIL_TRUNCATION_LEN', new=1):
254+
self._check(UnableToLockRowsToModifyError, truncated=True)
255+
253256
def test_add_details_to_UnableToLockRowsToReadCurrentError(self):
254257
from relstorage.adapters.interfaces import UnableToLockRowsToReadCurrentError
255258
self._check(UnableToLockRowsToReadCurrentError)

src/relstorage/storage/tpc/vote.py

+31-10
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@
1818
temporary objects to the database, and moving them to their final locations,
1919
live here.
2020
"""
21-
from __future__ import absolute_import
22-
from __future__ import print_function
2321

2422
import time
25-
from logging import DEBUG
23+
import logging
2624

2725
from zope.interface import implementer
2826

@@ -37,6 +35,7 @@
3735
from relstorage._util import do_log_duration_info
3836
from relstorage._util import TRACE
3937
from relstorage._util import METRIC_SAMPLE_RATE
38+
from relstorage._util import get_positive_integer_from_environ
4039
from relstorage.options import COMMIT_EXIT_CRITICAL_SECTION_EARLY
4140
from relstorage.adapters.interfaces import UnableToAcquireLockError
4241
from ..interfaces import VoteReadConflictError
@@ -46,9 +45,9 @@
4645
from . import AbstractTPCStateDatabaseAvailable
4746
from .finish import Finish
4847

49-
LOG_LEVEL_TID = DEBUG
48+
LOG_LEVEL_TID = logging.DEBUG
5049

51-
logger = __import__('logging').getLogger(__name__)
50+
logger = logging.getLogger(__name__)
5251
perf_logger = logger.getChild('timing')
5352

5453
class DatabaseLockedForTid(object):
@@ -663,14 +662,36 @@ def loadSerial(self, oid, serial):
663662
assert bytes8_to_int64(serial) == tid
664663
return state
665664

665+
#: The maximum number of characters to allow in each portion
666+
#: of lock detail errors.
667+
DETAIL_TRUNCATION_LEN = get_positive_integer_from_environ(
668+
'RS_LOCK_ERROR_MAX_LEN',
669+
# 2k seems a reasonable default. If you are actually getting
670+
# truncated, then chances are the transaction is much larger
671+
# than that
672+
2048,
673+
logger=logger
674+
)
675+
666676
def add_details_to_lock_error(ex, shared_state, required_tids):
667677
# type: (Exception, SharedState, required_tids)
678+
679+
obj_msg = str(shared_state.temp_storage) if shared_state.has_temp_data() else 'None'
680+
tid_msg = str(dict(required_tids)) # May be a BTree, which has no useful str/repr
681+
682+
obj_msg = (obj_msg[:DETAIL_TRUNCATION_LEN] + '...'
683+
if len(obj_msg) > DETAIL_TRUNCATION_LEN
684+
else obj_msg)
685+
tid_msg = (tid_msg[:DETAIL_TRUNCATION_LEN] + '...'
686+
if len(tid_msg) > DETAIL_TRUNCATION_LEN
687+
else tid_msg)
688+
668689
message = '\n'.join((
669-
'Stored Objects',
670-
str(shared_state.temp_storage) if shared_state.has_temp_data() else 'None',
671-
'readCurrent {oid: tid}',
672-
str(dict(required_tids)) # May be a BTree, which has no
673-
))
690+
'Stored Objects',
691+
obj_msg,
692+
'readCurrent {oid: tid}',
693+
tid_msg,
694+
))
674695

675696
if hasattr(ex, 'message'):
676697
# A ConflictError subclass *or* we're on Python 2.

0 commit comments

Comments
 (0)