1
1
import logging
2
+ from collections .abc import Callable
2
3
from dataclasses import dataclass
3
4
from typing import Literal
4
5
from urllib .parse import urlparse
7
8
from django .http import HttpResponse
8
9
from rest_framework .exceptions import UnsupportedMediaType
9
10
11
+ import aiohttp
10
12
import django_redis
11
13
import requests
12
14
import sentry_sdk
15
+ from asgiref .sync import sync_to_async
13
16
from sentry_sdk import push_scope , set_context
14
17
18
+ from api .utils .aiohttp import get_aiohttp_session
19
+ from api .utils .asyncio import fire_and_forget
15
20
from api .utils .image_proxy .exception import UpstreamThumbnailException
16
21
from api .utils .image_proxy .extension import get_image_extension
17
22
from api .utils .image_proxy .photon import get_photon_request_params
@@ -75,11 +80,40 @@ def get_request_params_for_extension(
75
80
)
76
81
77
82
78
- def get (
83
+ @sync_to_async
84
+ def _tally_response (
85
+ tallies ,
86
+ media_info : MediaInfo ,
87
+ month : str ,
88
+ domain : str ,
89
+ response : aiohttp .ClientResponse ,
90
+ ):
91
+ """
92
+ Tally image proxy response without waiting for Redis to respond.
93
+
94
+ Pulled into a separate function to help reduce overload when skimming
95
+ the `get` function, which is complex enough as is.
96
+ """
97
+ tallies .incr (f"thumbnail_response_code:{ month } :{ response .status } " ),
98
+ tallies .incr (
99
+ f"thumbnail_response_code_by_domain:{ domain } :" f"{ month } :{ response .status } "
100
+ )
101
+ tallies .incr (
102
+ f"thumbnail_response_code_by_provider:{ media_info .media_provider } :"
103
+ f"{ month } :{ response .status } "
104
+ )
105
+
106
+
107
+ _UPSTREAM_TIMEOUT = aiohttp .ClientTimeout (15 )
108
+
109
+
110
+ async def get (
79
111
media_info : MediaInfo ,
80
112
request_config : RequestConfig = RequestConfig (),
81
113
) -> HttpResponse :
82
114
"""
115
+ Retrieve proxied image from site accelerator.
116
+
83
117
Proxy an image through Photon if its file type is supported, else return the
84
118
original image if the file type is SVG. Otherwise, raise an exception.
85
119
"""
@@ -88,9 +122,10 @@ def get(
88
122
89
123
logger = parent_logger .getChild ("get" )
90
124
tallies = django_redis .get_redis_connection ("tallies" )
125
+ tallies_incr = sync_to_async (tallies .incr )
91
126
month = get_monthly_timestamp ()
92
127
93
- image_extension = get_image_extension (image_url , media_identifier )
128
+ image_extension = await get_image_extension (image_url , media_identifier )
94
129
95
130
headers = {"Accept" : request_config .accept_header } | HEADERS
96
131
@@ -106,26 +141,36 @@ def get(
106
141
)
107
142
108
143
try :
109
- upstream_response = requests .get (
144
+ # todo: refactor to use aiohttp shared session
145
+ session = await get_aiohttp_session ()
146
+
147
+ upstream_response = await session .get (
110
148
upstream_url ,
111
- timeout = 15 ,
149
+ timeout = _UPSTREAM_TIMEOUT ,
112
150
params = params ,
113
151
headers = headers ,
114
152
)
115
- tallies .incr (f"thumbnail_response_code:{ month } :{ upstream_response .status_code } " )
116
- tallies .incr (
117
- f"thumbnail_response_code_by_domain:{ domain } :"
118
- f"{ month } :{ upstream_response .status_code } "
119
- )
120
- tallies .incr (
121
- f"thumbnail_response_code_by_provider:{ media_info .media_provider } :"
122
- f"{ month } :{ upstream_response .status_code } "
153
+ fire_and_forget (
154
+ _tally_response (tallies , media_info , month , domain , upstream_response )
123
155
)
124
156
upstream_response .raise_for_status ()
157
+ status_code = upstream_response .status
158
+ content_type = upstream_response .headers .get ("Content-Type" )
159
+ logger .debug (
160
+ "Image proxy response status: %s, content-type: %s" ,
161
+ status_code ,
162
+ content_type ,
163
+ )
164
+ content = await upstream_response .content .read ()
165
+ return HttpResponse (
166
+ content ,
167
+ status = status_code ,
168
+ content_type = content_type ,
169
+ )
125
170
except Exception as exc :
126
171
exception_name = f"{ exc .__class__ .__module__ } .{ exc .__class__ .__name__ } "
127
172
key = f"thumbnail_error:{ exception_name } :{ domain } :{ month } "
128
- count = tallies . incr (key )
173
+ count = await tallies_incr (key )
129
174
if count <= settings .THUMBNAIL_ERROR_INITIAL_ALERT_THRESHOLD or (
130
175
count % settings .THUMBNAIL_ERROR_REPEATED_ALERT_FREQUENCY == 0
131
176
):
@@ -144,24 +189,14 @@ def get(
144
189
sentry_sdk .capture_exception (exc )
145
190
if isinstance (exc , requests .exceptions .HTTPError ):
146
191
code = exc .response .status_code
147
- tallies .incr (
148
- f"thumbnail_http_error:{ domain } :{ month } :{ code } :{ exc .response .text } "
192
+ fire_and_forget (
193
+ tallies_incr (
194
+ f"thumbnail_http_error:{ domain } :{ month } :{ code } :{ exc .response .text } "
195
+ )
149
196
)
150
197
logger .warning (
151
198
f"Failed to render thumbnail "
152
199
f"{ upstream_url = } { code = } "
153
200
f"{ media_info .media_provider = } "
154
201
)
155
202
raise UpstreamThumbnailException (f"Failed to render thumbnail. { exc } " )
156
-
157
- res_status = upstream_response .status_code
158
- content_type = upstream_response .headers .get ("Content-Type" )
159
- logger .debug (
160
- f"Image proxy response status: { res_status } , content-type: { content_type } "
161
- )
162
-
163
- return HttpResponse (
164
- upstream_response .content ,
165
- status = res_status ,
166
- content_type = content_type ,
167
- )
0 commit comments