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

Commit 5949ab8

Browse files
authored
Fix potential thumbnail memory leaks. (#12932)
1 parent 2e8763e commit 5949ab8

File tree

3 files changed

+202
-135
lines changed

3 files changed

+202
-135
lines changed

changelog.d/12932.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix potential memory leak when generating thumbnails.

synapse/rest/media/v1/media_repository.py

+141-124
Original file line numberDiff line numberDiff line change
@@ -587,15 +587,16 @@ async def generate_local_exact_thumbnail(
587587
)
588588
return None
589589

590-
t_byte_source = await defer_to_thread(
591-
self.hs.get_reactor(),
592-
self._generate_thumbnail,
593-
thumbnailer,
594-
t_width,
595-
t_height,
596-
t_method,
597-
t_type,
598-
)
590+
with thumbnailer:
591+
t_byte_source = await defer_to_thread(
592+
self.hs.get_reactor(),
593+
self._generate_thumbnail,
594+
thumbnailer,
595+
t_width,
596+
t_height,
597+
t_method,
598+
t_type,
599+
)
599600

600601
if t_byte_source:
601602
try:
@@ -657,15 +658,16 @@ async def generate_remote_exact_thumbnail(
657658
)
658659
return None
659660

660-
t_byte_source = await defer_to_thread(
661-
self.hs.get_reactor(),
662-
self._generate_thumbnail,
663-
thumbnailer,
664-
t_width,
665-
t_height,
666-
t_method,
667-
t_type,
668-
)
661+
with thumbnailer:
662+
t_byte_source = await defer_to_thread(
663+
self.hs.get_reactor(),
664+
self._generate_thumbnail,
665+
thumbnailer,
666+
t_width,
667+
t_height,
668+
t_method,
669+
t_type,
670+
)
669671

670672
if t_byte_source:
671673
try:
@@ -749,119 +751,134 @@ async def _generate_thumbnails(
749751
)
750752
return None
751753

752-
m_width = thumbnailer.width
753-
m_height = thumbnailer.height
754-
755-
if m_width * m_height >= self.max_image_pixels:
756-
logger.info(
757-
"Image too large to thumbnail %r x %r > %r",
758-
m_width,
759-
m_height,
760-
self.max_image_pixels,
761-
)
762-
return None
763-
764-
if thumbnailer.transpose_method is not None:
765-
m_width, m_height = await defer_to_thread(
766-
self.hs.get_reactor(), thumbnailer.transpose
767-
)
754+
with thumbnailer:
755+
m_width = thumbnailer.width
756+
m_height = thumbnailer.height
768757

769-
# We deduplicate the thumbnail sizes by ignoring the cropped versions if
770-
# they have the same dimensions of a scaled one.
771-
thumbnails: Dict[Tuple[int, int, str], str] = {}
772-
for requirement in requirements:
773-
if requirement.method == "crop":
774-
thumbnails.setdefault(
775-
(requirement.width, requirement.height, requirement.media_type),
776-
requirement.method,
777-
)
778-
elif requirement.method == "scale":
779-
t_width, t_height = thumbnailer.aspect(
780-
requirement.width, requirement.height
758+
if m_width * m_height >= self.max_image_pixels:
759+
logger.info(
760+
"Image too large to thumbnail %r x %r > %r",
761+
m_width,
762+
m_height,
763+
self.max_image_pixels,
781764
)
782-
t_width = min(m_width, t_width)
783-
t_height = min(m_height, t_height)
784-
thumbnails[
785-
(t_width, t_height, requirement.media_type)
786-
] = requirement.method
787-
788-
# Now we generate the thumbnails for each dimension, store it
789-
for (t_width, t_height, t_type), t_method in thumbnails.items():
790-
# Generate the thumbnail
791-
if t_method == "crop":
792-
t_byte_source = await defer_to_thread(
793-
self.hs.get_reactor(), thumbnailer.crop, t_width, t_height, t_type
765+
return None
766+
767+
if thumbnailer.transpose_method is not None:
768+
m_width, m_height = await defer_to_thread(
769+
self.hs.get_reactor(), thumbnailer.transpose
794770
)
795-
elif t_method == "scale":
796-
t_byte_source = await defer_to_thread(
797-
self.hs.get_reactor(), thumbnailer.scale, t_width, t_height, t_type
771+
772+
# We deduplicate the thumbnail sizes by ignoring the cropped versions if
773+
# they have the same dimensions of a scaled one.
774+
thumbnails: Dict[Tuple[int, int, str], str] = {}
775+
for requirement in requirements:
776+
if requirement.method == "crop":
777+
thumbnails.setdefault(
778+
(requirement.width, requirement.height, requirement.media_type),
779+
requirement.method,
780+
)
781+
elif requirement.method == "scale":
782+
t_width, t_height = thumbnailer.aspect(
783+
requirement.width, requirement.height
784+
)
785+
t_width = min(m_width, t_width)
786+
t_height = min(m_height, t_height)
787+
thumbnails[
788+
(t_width, t_height, requirement.media_type)
789+
] = requirement.method
790+
791+
# Now we generate the thumbnails for each dimension, store it
792+
for (t_width, t_height, t_type), t_method in thumbnails.items():
793+
# Generate the thumbnail
794+
if t_method == "crop":
795+
t_byte_source = await defer_to_thread(
796+
self.hs.get_reactor(),
797+
thumbnailer.crop,
798+
t_width,
799+
t_height,
800+
t_type,
801+
)
802+
elif t_method == "scale":
803+
t_byte_source = await defer_to_thread(
804+
self.hs.get_reactor(),
805+
thumbnailer.scale,
806+
t_width,
807+
t_height,
808+
t_type,
809+
)
810+
else:
811+
logger.error("Unrecognized method: %r", t_method)
812+
continue
813+
814+
if not t_byte_source:
815+
continue
816+
817+
file_info = FileInfo(
818+
server_name=server_name,
819+
file_id=file_id,
820+
url_cache=url_cache,
821+
thumbnail=ThumbnailInfo(
822+
width=t_width,
823+
height=t_height,
824+
method=t_method,
825+
type=t_type,
826+
),
798827
)
799-
else:
800-
logger.error("Unrecognized method: %r", t_method)
801-
continue
802-
803-
if not t_byte_source:
804-
continue
805-
806-
file_info = FileInfo(
807-
server_name=server_name,
808-
file_id=file_id,
809-
url_cache=url_cache,
810-
thumbnail=ThumbnailInfo(
811-
width=t_width,
812-
height=t_height,
813-
method=t_method,
814-
type=t_type,
815-
),
816-
)
817828

818-
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
819-
try:
820-
await self.media_storage.write_to_file(t_byte_source, f)
821-
await finish()
822-
finally:
823-
t_byte_source.close()
824-
825-
t_len = os.path.getsize(fname)
826-
827-
# Write to database
828-
if server_name:
829-
# Multiple remote media download requests can race (when
830-
# using multiple media repos), so this may throw a violation
831-
# constraint exception. If it does we'll delete the newly
832-
# generated thumbnail from disk (as we're in the ctx
833-
# manager).
834-
#
835-
# However: we've already called `finish()` so we may have
836-
# also written to the storage providers. This is preferable
837-
# to the alternative where we call `finish()` *after* this,
838-
# where we could end up having an entry in the DB but fail
839-
# to write the files to the storage providers.
829+
with self.media_storage.store_into_file(file_info) as (
830+
f,
831+
fname,
832+
finish,
833+
):
840834
try:
841-
await self.store.store_remote_media_thumbnail(
842-
server_name,
843-
media_id,
844-
file_id,
845-
t_width,
846-
t_height,
847-
t_type,
848-
t_method,
849-
t_len,
850-
)
851-
except Exception as e:
852-
thumbnail_exists = await self.store.get_remote_media_thumbnail(
853-
server_name,
854-
media_id,
855-
t_width,
856-
t_height,
857-
t_type,
835+
await self.media_storage.write_to_file(t_byte_source, f)
836+
await finish()
837+
finally:
838+
t_byte_source.close()
839+
840+
t_len = os.path.getsize(fname)
841+
842+
# Write to database
843+
if server_name:
844+
# Multiple remote media download requests can race (when
845+
# using multiple media repos), so this may throw a violation
846+
# constraint exception. If it does we'll delete the newly
847+
# generated thumbnail from disk (as we're in the ctx
848+
# manager).
849+
#
850+
# However: we've already called `finish()` so we may have
851+
# also written to the storage providers. This is preferable
852+
# to the alternative where we call `finish()` *after* this,
853+
# where we could end up having an entry in the DB but fail
854+
# to write the files to the storage providers.
855+
try:
856+
await self.store.store_remote_media_thumbnail(
857+
server_name,
858+
media_id,
859+
file_id,
860+
t_width,
861+
t_height,
862+
t_type,
863+
t_method,
864+
t_len,
865+
)
866+
except Exception as e:
867+
thumbnail_exists = (
868+
await self.store.get_remote_media_thumbnail(
869+
server_name,
870+
media_id,
871+
t_width,
872+
t_height,
873+
t_type,
874+
)
875+
)
876+
if not thumbnail_exists:
877+
raise e
878+
else:
879+
await self.store.store_local_thumbnail(
880+
media_id, t_width, t_height, t_type, t_method, t_len
858881
)
859-
if not thumbnail_exists:
860-
raise e
861-
else:
862-
await self.store.store_local_thumbnail(
863-
media_id, t_width, t_height, t_type, t_method, t_len
864-
)
865882

866883
return {"width": m_width, "height": m_height}
867884

0 commit comments

Comments
 (0)