Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit ff6fd52

Browse files
authored
checks for generators in database functions (#11564)
A couple of safety-checks to hopefully stop people doing what I just did, and create a storage function which only works the first time it is called (and not when it is re-run due to a database concurrency error or similar).
1 parent eb39da6 commit ff6fd52

File tree

3 files changed

+49
-7
lines changed

3 files changed

+49
-7
lines changed

changelog.d/11564.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add some safety checks that storage functions are used correctly.

synapse/storage/database.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16+
import inspect
1617
import logging
1718
import time
19+
import types
1820
from collections import defaultdict
1921
from sys import intern
2022
from time import monotonic as monotonic_time
@@ -526,6 +528,12 @@ def new_transaction(
526528
the function will correctly handle being aborted and retried half way
527529
through its execution.
528530
531+
Similarly, the arguments to `func` (`args`, `kwargs`) should not be generators,
532+
since they could be evaluated multiple times (which would produce an empty
533+
result on the second or subsequent evaluation). Likewise, the closure of `func`
534+
must not reference any generators. This method attempts to detect such usage
535+
and will log an error.
536+
529537
Args:
530538
conn
531539
desc
@@ -536,6 +544,39 @@ def new_transaction(
536544
**kwargs
537545
"""
538546

547+
# Robustness check: ensure that none of the arguments are generators, since that
548+
# will fail if we have to repeat the transaction.
549+
# For now, we just log an error, and hope that it works on the first attempt.
550+
# TODO: raise an exception.
551+
for i, arg in enumerate(args):
552+
if inspect.isgenerator(arg):
553+
logger.error(
554+
"Programming error: generator passed to new_transaction as "
555+
"argument %i to function %s",
556+
i,
557+
func,
558+
)
559+
for name, val in kwargs.items():
560+
if inspect.isgenerator(val):
561+
logger.error(
562+
"Programming error: generator passed to new_transaction as "
563+
"argument %s to function %s",
564+
name,
565+
func,
566+
)
567+
# also check variables referenced in func's closure
568+
if inspect.isfunction(func):
569+
f = cast(types.FunctionType, func)
570+
if f.__closure__:
571+
for i, cell in enumerate(f.__closure__):
572+
if inspect.isgenerator(cell.cell_contents):
573+
logger.error(
574+
"Programming error: function %s references generator %s "
575+
"via its closure",
576+
f,
577+
f.__code__.co_freevars[i],
578+
)
579+
539580
start = monotonic_time()
540581
txn_id = self._TXN_ID
541582

@@ -1226,9 +1267,9 @@ async def simple_upsert_many(
12261267
self,
12271268
table: str,
12281269
key_names: Collection[str],
1229-
key_values: Collection[Iterable[Any]],
1270+
key_values: Collection[Collection[Any]],
12301271
value_names: Collection[str],
1231-
value_values: Iterable[Iterable[Any]],
1272+
value_values: Collection[Collection[Any]],
12321273
desc: str,
12331274
) -> None:
12341275
"""
@@ -1920,7 +1961,7 @@ async def simple_delete_many(
19201961
self,
19211962
table: str,
19221963
column: str,
1923-
iterable: Iterable[Any],
1964+
iterable: Collection[Any],
19241965
keyvalues: Dict[str, Any],
19251966
desc: str,
19261967
) -> int:
@@ -1931,7 +1972,8 @@ async def simple_delete_many(
19311972
Args:
19321973
table: string giving the table name
19331974
column: column name to test for inclusion against `iterable`
1934-
iterable: list
1975+
iterable: list of values to match against `column`. NB cannot be a generator
1976+
as it may be evaluated multiple times.
19351977
keyvalues: dict of column names and values to select the rows with
19361978
desc: description of the transaction, for logging and metrics
19371979

synapse/storage/databases/main/presence.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ async def add_users_to_send_full_presence_to(self, user_ids: Iterable[str]):
269269
"""
270270
# Add user entries to the table, updating the presence_stream_id column if the user already
271271
# exists in the table.
272+
presence_stream_id = self._presence_id_gen.get_current_token()
272273
await self.db_pool.simple_upsert_many(
273274
table="users_to_send_full_presence_to",
274275
key_names=("user_id",),
@@ -279,9 +280,7 @@ async def add_users_to_send_full_presence_to(self, user_ids: Iterable[str]):
279280
# devices at different times, each device will receive full presence once - when
280281
# the presence stream ID in their sync token is less than the one in the table
281282
# for their user ID.
282-
value_values=(
283-
(self._presence_id_gen.get_current_token(),) for _ in user_ids
284-
),
283+
value_values=[(presence_stream_id,) for _ in user_ids],
285284
desc="add_users_to_send_full_presence_to",
286285
)
287286

0 commit comments

Comments
 (0)