Skip to content

Commit 7ca7887

Browse files
erikjohnstonH-Shay
authored andcommitted
Fix slipped logging context when media rejected (element-hq#17239)
When a module rejects a piece of media we end up trying to close the same logging context twice. Instead of fixing the existing code we refactor to use an async context manager, which is easier to write correctly.
1 parent 7834ed2 commit 7ca7887

File tree

6 files changed

+55
-91
lines changed

6 files changed

+55
-91
lines changed

changelog.d/17239.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix errors in logs about closing incorrect logging contexts when media gets rejected by a module.

synapse/media/media_repository.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ async def _download_remote_file(
650650

651651
file_info = FileInfo(server_name=server_name, file_id=file_id)
652652

653-
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
653+
async with self.media_storage.store_into_file(file_info) as (f, fname):
654654
try:
655655
length, headers = await self.client.download_media(
656656
server_name,
@@ -693,8 +693,6 @@ async def _download_remote_file(
693693
)
694694
raise SynapseError(502, "Failed to fetch remote media")
695695

696-
await finish()
697-
698696
if b"Content-Type" in headers:
699697
media_type = headers[b"Content-Type"][0].decode("ascii")
700698
else:
@@ -1045,14 +1043,9 @@ async def _generate_thumbnails(
10451043
),
10461044
)
10471045

1048-
with self.media_storage.store_into_file(file_info) as (
1049-
f,
1050-
fname,
1051-
finish,
1052-
):
1046+
async with self.media_storage.store_into_file(file_info) as (f, fname):
10531047
try:
10541048
await self.media_storage.write_to_file(t_byte_source, f)
1055-
await finish()
10561049
finally:
10571050
t_byte_source.close()
10581051

synapse/media/media_storage.py

Lines changed: 37 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@
2727
IO,
2828
TYPE_CHECKING,
2929
Any,
30-
Awaitable,
30+
AsyncIterator,
3131
BinaryIO,
3232
Callable,
33-
Generator,
3433
Optional,
3534
Sequence,
3635
Tuple,
@@ -97,11 +96,9 @@ async def store_file(self, source: IO, file_info: FileInfo) -> str:
9796
the file path written to in the primary media store
9897
"""
9998

100-
with self.store_into_file(file_info) as (f, fname, finish_cb):
99+
async with self.store_into_file(file_info) as (f, fname):
101100
# Write to the main media repository
102101
await self.write_to_file(source, f)
103-
# Write to the other storage providers
104-
await finish_cb()
105102

106103
return fname
107104

@@ -111,32 +108,27 @@ async def write_to_file(self, source: IO, output: IO) -> None:
111108
await defer_to_thread(self.reactor, _write_file_synchronously, source, output)
112109

113110
@trace_with_opname("MediaStorage.store_into_file")
114-
@contextlib.contextmanager
115-
def store_into_file(
111+
@contextlib.asynccontextmanager
112+
async def store_into_file(
116113
self, file_info: FileInfo
117-
) -> Generator[Tuple[BinaryIO, str, Callable[[], Awaitable[None]]], None, None]:
118-
"""Context manager used to get a file like object to write into, as
114+
) -> AsyncIterator[Tuple[BinaryIO, str]]:
115+
"""Async Context manager used to get a file like object to write into, as
119116
described by file_info.
120117
121-
Actually yields a 3-tuple (file, fname, finish_cb), where file is a file
122-
like object that can be written to, fname is the absolute path of file
123-
on disk, and finish_cb is a function that returns an awaitable.
118+
Actually yields a 2-tuple (file, fname,), where file is a file
119+
like object that can be written to and fname is the absolute path of file
120+
on disk.
124121
125122
fname can be used to read the contents from after upload, e.g. to
126123
generate thumbnails.
127124
128-
finish_cb must be called and waited on after the file has been successfully been
129-
written to. Should not be called if there was an error. Checks for spam and
130-
stores the file into the configured storage providers.
131-
132125
Args:
133126
file_info: Info about the file to store
134127
135128
Example:
136129
137-
with media_storage.store_into_file(info) as (f, fname, finish_cb):
130+
async with media_storage.store_into_file(info) as (f, fname,):
138131
# .. write into f ...
139-
await finish_cb()
140132
"""
141133

142134
path = self._file_info_to_path(file_info)
@@ -145,62 +137,42 @@ def store_into_file(
145137
dirname = os.path.dirname(fname)
146138
os.makedirs(dirname, exist_ok=True)
147139

148-
finished_called = [False]
149-
150140
main_media_repo_write_trace_scope = start_active_span(
151141
"writing to main media repo"
152142
)
153143
main_media_repo_write_trace_scope.__enter__()
154144

155-
try:
156-
with open(fname, "wb") as f:
157-
158-
async def finish() -> None:
159-
# When someone calls finish, we assume they are done writing to the main media repo
160-
main_media_repo_write_trace_scope.__exit__(None, None, None)
161-
162-
with start_active_span("writing to other storage providers"):
163-
# Ensure that all writes have been flushed and close the
164-
# file.
165-
f.flush()
166-
f.close()
167-
168-
spam_check = await self._spam_checker_module_callbacks.check_media_file_for_spam(
169-
ReadableFileWrapper(self.clock, fname), file_info
170-
)
171-
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
172-
logger.info("Blocking media due to spam checker")
173-
# Note that we'll delete the stored media, due to the
174-
# try/except below. The media also won't be stored in
175-
# the DB.
176-
# We currently ignore any additional field returned by
177-
# the spam-check API.
178-
raise SpamMediaException(errcode=spam_check[0])
179-
180-
for provider in self.storage_providers:
181-
with start_active_span(str(provider)):
182-
await provider.store_file(path, file_info)
183-
184-
finished_called[0] = True
185-
186-
yield f, fname, finish
187-
except Exception as e:
145+
with main_media_repo_write_trace_scope:
188146
try:
189-
main_media_repo_write_trace_scope.__exit__(
190-
type(e), None, e.__traceback__
191-
)
192-
os.remove(fname)
193-
except Exception:
194-
pass
147+
with open(fname, "wb") as f:
148+
yield f, fname
195149

196-
raise e from None
150+
except Exception as e:
151+
try:
152+
os.remove(fname)
153+
except Exception:
154+
pass
197155

198-
if not finished_called:
199-
exc = Exception("Finished callback not called")
200-
main_media_repo_write_trace_scope.__exit__(
201-
type(exc), None, exc.__traceback__
156+
raise e from None
157+
158+
with start_active_span("writing to other storage providers"):
159+
spam_check = (
160+
await self._spam_checker_module_callbacks.check_media_file_for_spam(
161+
ReadableFileWrapper(self.clock, fname), file_info
162+
)
202163
)
203-
raise exc
164+
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
165+
logger.info("Blocking media due to spam checker")
166+
# Note that we'll delete the stored media, due to the
167+
# try/except below. The media also won't be stored in
168+
# the DB.
169+
# We currently ignore any additional field returned by
170+
# the spam-check API.
171+
raise SpamMediaException(errcode=spam_check[0])
172+
173+
for provider in self.storage_providers:
174+
with start_active_span(str(provider)):
175+
await provider.store_file(path, file_info)
204176

205177
async def fetch_media(self, file_info: FileInfo) -> Optional[Responder]:
206178
"""Attempts to fetch media described by file_info from the local cache

synapse/media/url_previewer.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ async def _handle_url(
592592

593593
file_info = FileInfo(server_name=None, file_id=file_id, url_cache=True)
594594

595-
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
595+
async with self.media_storage.store_into_file(file_info) as (f, fname):
596596
if url.startswith("data:"):
597597
if not allow_data_urls:
598598
raise SynapseError(
@@ -603,8 +603,6 @@ async def _handle_url(
603603
else:
604604
download_result = await self._download_url(url, f)
605605

606-
await finish()
607-
608606
try:
609607
time_now_ms = self.clock.time_msec()
610608

tests/rest/client/test_media.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
9393
# from a regular 404.
9494
file_id = "abcdefg12345"
9595
file_info = FileInfo(server_name=self.remote_server_name, file_id=file_id)
96-
with hs.get_media_repository().media_storage.store_into_file(file_info) as (
97-
f,
98-
fname,
99-
finish,
100-
):
101-
f.write(SMALL_PNG)
102-
self.get_success(finish())
96+
97+
media_storage = hs.get_media_repository().media_storage
98+
99+
ctx = media_storage.store_into_file(file_info)
100+
(f, fname) = self.get_success(ctx.__aenter__())
101+
f.write(SMALL_PNG)
102+
self.get_success(ctx.__aexit__(None, None, None))
103103

104104
self.get_success(
105105
self.store.store_cached_remote_media(

tests/rest/media/test_domain_blocking.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
4444
# from a regular 404.
4545
file_id = "abcdefg12345"
4646
file_info = FileInfo(server_name=self.remote_server_name, file_id=file_id)
47-
with hs.get_media_repository().media_storage.store_into_file(file_info) as (
48-
f,
49-
fname,
50-
finish,
51-
):
52-
f.write(SMALL_PNG)
53-
self.get_success(finish())
47+
48+
media_storage = hs.get_media_repository().media_storage
49+
50+
ctx = media_storage.store_into_file(file_info)
51+
(f, fname) = self.get_success(ctx.__aenter__())
52+
f.write(SMALL_PNG)
53+
self.get_success(ctx.__aexit__(None, None, None))
5454

5555
self.get_success(
5656
self.store.store_cached_remote_media(

0 commit comments

Comments
 (0)