Skip to content

Commit 41636c4

Browse files
authored
Merge pull request #345 from bjester/buffer-serialization-perf
Improve buffer serialization performance by performing bulk counter lookup
2 parents 3ab3267 + 3548306 commit 41636c4

5 files changed

Lines changed: 54 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
List of the most important changes for each release.
44

5+
## 0.8.15
6+
- Improves buffer serialization performance by performing bulk counter lookup
7+
58
## 0.8.14
69
- Adds utility for addressing immediate FK constraints caused by Django upgrade, automatically performed for morango models in a Django migration.
710
- Adds retry behavior for low-level connection issues not handled by `urllib3` retries

morango/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.8.14"
1+
__version__ = "0.8.15"

morango/api/serializers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import defaultdict
2+
13
from rest_framework import exceptions
24
from rest_framework import serializers
35
from rest_framework.fields import ReadOnlyField
@@ -158,11 +160,38 @@ class Meta:
158160
read_only_fields = fields
159161

160162

163+
class BufferListSerializer(serializers.ListSerializer):
164+
def to_representation(self, data):
165+
buffers = list(data)
166+
if buffers:
167+
rmcb_map = defaultdict(list)
168+
transfer_session_ids = {b.transfer_session_id for b in buffers}
169+
# Morango implementation will only ever call this for one transfer session ID at a time,
170+
# but a loop makes the code straightforward regardless and limits the quantity of
171+
# SQLite variables in use
172+
for transfer_session_id in transfer_session_ids:
173+
# bulk-fetch all RMCB records needed for this batch of buffers in a
174+
# single query, then cache the relevant subset on each buffer, instead
175+
# of letting each buffer's nested rmcb_list serializer issue its own query
176+
rmcb_queryset = RecordMaxCounterBuffer.objects.filter(
177+
transfer_session_id=transfer_session_id,
178+
model_uuid__in={b.model_uuid for b in buffers if b.transfer_session_id == transfer_session_id},
179+
)
180+
for rmcb in rmcb_queryset:
181+
rmcb_map[(transfer_session_id, rmcb.model_uuid)].append(rmcb)
182+
for buffer in buffers:
183+
buffer._rmcb_list = rmcb_map[
184+
(buffer.transfer_session_id, buffer.model_uuid)
185+
]
186+
return super(BufferListSerializer, self).to_representation(buffers)
187+
188+
161189
class BufferSerializer(serializers.ModelSerializer):
162190
rmcb_list = RecordMaxCounterBufferSerializer(many=True)
163191

164192
class Meta:
165193
model = Buffer
194+
list_serializer_class = BufferListSerializer
166195
fields = (
167196
"serialized",
168197
"deleted",

morango/models/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ class Meta:
553553
unique_together = ("transfer_session", "model_uuid")
554554

555555
def rmcb_list(self):
556+
# allow callers (e.g. BufferListSerializer) to batch-fetch RMCB records
557+
# for many buffers at once and cache them here, to avoid an N+1 query
558+
# pattern when serializing a large number of buffers
559+
if hasattr(self, "_rmcb_list"):
560+
return self._rmcb_list
556561
return RecordMaxCounterBuffer.objects.filter(
557562
model_uuid=self.model_uuid, transfer_session_id=self.transfer_session_id
558563
)

tests/testapp/tests/test_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,22 @@ def test_buffer_serializer_makes_no_transfer_session_query(self):
10731073
for q in ctx.captured_queries:
10741074
self.assertFalse('morango_transfersession' in q['sql'])
10751075

1076+
def test_buffer_serializer_batches_rmcb_queries_for_many(self):
1077+
transfer_session_id = self.create_records_for_pulling(count=10)
1078+
buffers = Buffer.objects.filter(
1079+
transfer_session_id=transfer_session_id
1080+
).order_by("pk")
1081+
1082+
with CaptureQueriesContext(connection) as ctx:
1083+
BufferSerializer(buffers, many=True).data
1084+
1085+
rmcb_queries = [
1086+
q
1087+
for q in ctx.captured_queries
1088+
if "recordmaxcounterbuffer" in q["sql"].lower()
1089+
]
1090+
self.assertEqual(len(rmcb_queries), 1)
1091+
10761092
def test_pull_valid_buffer_list(self):
10771093

10781094
transfer_session_id = self.create_records_for_pulling()

0 commit comments

Comments
 (0)