33import json
44import logging
55import os
6+ import datetime
7+ import threading
68from concurrent .futures import ThreadPoolExecutor , TimeoutError as FuturesTimeoutError
79
810from pyramid .view import view_config
2325_USAGE_YEARLY_TIMEOUT_SECONDS = float (
2426 os .environ .get ("USAGE_YEARLY_TIMEOUT_SECONDS" , str (_USAGE_REPORT_TIMEOUT_SECONDS ))
2527)
26- _AFFILIATIONS_EXECUTOR = ThreadPoolExecutor (
28+ _USAGE_TIMEOUT_POOL_SIZE = int (os .environ .get ("USAGE_TIMEOUT_POOL_SIZE" , "8" ))
29+ _USAGE_MAX_INFLIGHT = int (os .environ .get ("USAGE_TIMEOUT_MAX_INFLIGHT" , str (_USAGE_TIMEOUT_POOL_SIZE )))
30+ _PUBLICATION_MAX_INFLIGHT = int (
31+ os .environ .get ("PUBLICATION_AFFILIATIONS_MAX_INFLIGHT" , str (_AFFILIATIONS_TIMEOUT_POOL_SIZE ))
32+ )
33+
34+ _USAGE_EXECUTOR = ThreadPoolExecutor (
35+ max_workers = _USAGE_TIMEOUT_POOL_SIZE ,
36+ thread_name_prefix = "usage-timeout" ,
37+ )
38+ _PUBLICATION_EXECUTOR = ThreadPoolExecutor (
2739 max_workers = _AFFILIATIONS_TIMEOUT_POOL_SIZE ,
28- thread_name_prefix = "affiliations -timeout" ,
40+ thread_name_prefix = "publication -timeout" ,
2941)
42+ _USAGE_INFLIGHT = threading .BoundedSemaphore (value = _USAGE_MAX_INFLIGHT )
43+ _PUBLICATION_INFLIGHT = threading .BoundedSemaphore (value = _PUBLICATION_MAX_INFLIGHT )
3044
3145
3246def _usage_cache_key (prefix , payload ):
@@ -36,11 +50,39 @@ def _usage_cache_key(prefix, payload):
3650 )
3751
3852
39- def _run_with_timeout (callable_obj , fallback_data , timeout_seconds , operation_name ):
40- future = _AFFILIATIONS_EXECUTOR .submit (callable_obj )
53+ def _submit_guarded (executor , inflight_guard , callable_obj , operation_name ):
54+ if not inflight_guard .acquire (blocking = False ):
55+ logger .warning (
56+ "Inflight guard reached for %s; returning fallback without backend call" ,
57+ operation_name
58+ )
59+ return None
60+
61+ try :
62+ future = executor .submit (callable_obj )
63+ except Exception :
64+ inflight_guard .release ()
65+ raise
66+
67+ def _release_guard (_ ):
68+ try :
69+ inflight_guard .release ()
70+ except ValueError :
71+ logger .warning ("Inflight guard release imbalance for %s" , operation_name )
72+
73+ future .add_done_callback (_release_guard )
74+ return future
75+
76+
77+ def _run_with_timeout (executor , inflight_guard , callable_obj , fallback_data , timeout_seconds , operation_name ):
78+ future = _submit_guarded (executor , inflight_guard , callable_obj , operation_name )
79+ if future is None :
80+ return fallback_data
81+
4182 try :
4283 return future .result (timeout = timeout_seconds )
4384 except FuturesTimeoutError :
85+ future .cancel ()
4486 logger .warning (
4587 "Timeout in %s after %.2fs; returning fallback" ,
4688 operation_name ,
@@ -133,13 +175,27 @@ def _usage_selected_values(request):
133175 return selected_code , selected_collection_code , selected_document_code
134176
135177
178+ def _usage_date_range (request ):
179+ today = datetime .datetime .now ().isoformat ()[0 :10 ]
180+ range_start = (
181+ request .GET .get ('range_start' )
182+ or request .session .get ('range_start' )
183+ or '1998-01-01'
184+ )
185+ range_end = (
186+ request .GET .get ('range_end' )
187+ or request .session .get ('range_end' )
188+ or today
189+ )
190+ return range_start , range_end
191+
192+
136193@view_config (route_name = 'usage_report_chart' , request_method = 'GET' , renderer = 'jsonp' )
137194@check_session
138195def usage_report_chart (request ):
139196
140197 api_version = request .GET .get ('api_version' , 'v2' )
141- range_start = request .GET .get ('range_start' , None )
142- range_end = request .GET .get ('range_end' , None )
198+ range_start , range_end = _usage_date_range (request )
143199 report_code = request .GET .get ('report_code' , 'tr_j1' )
144200 granularity = request .GET .get ('granularity' , 'monthly' )
145201 selected_code , selected_collection_code , selected_document_code = _usage_selected_values (request )
@@ -161,6 +217,8 @@ def usage_report_chart(request):
161217 fallback_data = {'series' : []}
162218
163219 data_chart = _run_with_timeout (
220+ _USAGE_EXECUTOR ,
221+ _USAGE_INFLIGHT ,
164222 lambda : cache_region .get_or_create (
165223 cache_key ,
166224 lambda : request .stats .usage .get_usage_report (
@@ -191,8 +249,7 @@ def usage_report_chart(request):
191249def usage_report_yearly_chart (request ):
192250
193251 api_version = request .GET .get ('api_version' , 'v2' )
194- range_start = request .GET .get ('range_start' , None )
195- range_end = request .GET .get ('range_end' , None )
252+ range_start , range_end = _usage_date_range (request )
196253 report_code = request .GET .get ('report_code' , 'cr_j1' )
197254 metric_type = request .GET .get ('metric_type' , 'Total_Item_Requests' )
198255 selected_code , selected_collection_code , selected_document_code = _usage_selected_values (request )
@@ -241,6 +298,8 @@ def _compute_yearly_chart():
241298 return request .stats .usage ._title_report_to_yearly_chart_data (data_raw , metric_type = metric_type )
242299
243300 data_chart = _run_with_timeout (
301+ _USAGE_EXECUTOR ,
302+ _USAGE_INFLIGHT ,
244303 lambda : cache_region .get_or_create (cache_key , _compute_yearly_chart ),
245304 fallback_data = {'series' : [], 'categories' : []},
246305 timeout_seconds = _USAGE_YEARLY_TIMEOUT_SECONDS ,
@@ -289,6 +348,8 @@ def publication_article_affiliations_map(request):
289348 cache_key = _usage_cache_key ("publication_article_affiliations_map" , cache_payload )
290349
291350 chart_data = _run_with_timeout (
351+ _PUBLICATION_EXECUTOR ,
352+ _PUBLICATION_INFLIGHT ,
292353 lambda : cache_region .get_or_create (
293354 cache_key ,
294355 lambda : request .stats .publication .general (
@@ -326,6 +387,8 @@ def publication_article_affiliations(request):
326387 cache_key = _usage_cache_key ("publication_article_affiliations" , cache_payload )
327388
328389 chart_data = _run_with_timeout (
390+ _PUBLICATION_EXECUTOR ,
391+ _PUBLICATION_INFLIGHT ,
329392 lambda : cache_region .get_or_create (
330393 cache_key ,
331394 lambda : request .stats .publication .general (
@@ -364,6 +427,8 @@ def publication_article_affiliations_publication_year(request):
364427 cache_key = _usage_cache_key ("publication_article_affiliations_publication_year" , cache_payload )
365428
366429 chart_data = _run_with_timeout (
430+ _PUBLICATION_EXECUTOR ,
431+ _PUBLICATION_INFLIGHT ,
367432 lambda : cache_region .get_or_create (
368433 cache_key ,
369434 lambda : request .stats .publication .affiliations_by_publication_year (
0 commit comments