1- import os
1+ import copy
22import json
3+ import os
4+ import time
35
46from requests import Session
7+ from requests .adapters import HTTPAdapter
8+ from requests .packages .urllib3 .util import Retry
59
6- from .helpers import urlify , CustomJSONEncoder , AlgoliaException
10+ from .helpers import AlgoliaException , CustomJSONEncoder , rotate , urlify
711
812try :
913 from urllib import urlencode
1216
1317APPENGINE = 'APPENGINE_RUNTIME' in os .environ
1418SSL_CERTIFICATE_DOMAIN = 'algolia.net'
19+ DNS_TIMER_DELAY = 5 * 60 # 5 minutes
1520
1621if APPENGINE :
1722 from google .appengine .api import urlfetch
2328 }
2429
2530
26- class Transport ():
31+ # urllib ultimately uses `/etc/resolv.conf` on linux to get its DNS resolution
32+ # timeout, this is the settings allowing to change it.
33+ if 'RES_OPTIONS' not in os .environ :
34+ os .environ ['RES_OPTIONS' ] = 'timeout:2 attempts:1'
35+
36+
37+ class Transport (object ):
2738 def __init__ (self ):
2839 self .headers = {}
2940 self .read_hosts = []
3041 self .write_hosts = []
3142 self .timeout = (2 , 30 )
3243 self .search_timeout = (2 , 5 )
44+ self .dns_timer = time .time ()
3345
3446 self .session = Session ()
47+ # Ask urllib not to make retries on its own.
48+ self .session .mount ('https://' , HTTPAdapter (max_retries = Retry (connect = 0 )))
49+
3550 self .session .verify = os .path .join (os .path .dirname (__file__ ),
3651 'resources/ca-bundle.crt' )
3752
53+ @property
54+ def read_hosts (self ):
55+ return self ._read_hosts
56+
57+ @read_hosts .setter
58+ def read_hosts (self , value ):
59+ self ._read_hosts = value
60+ self ._original_read_hosts = value
61+
62+ @property
63+ def write_hosts (self ):
64+ return self ._write_hosts
65+
66+ @write_hosts .setter
67+ def write_hosts (self , value ):
68+ self ._write_hosts = value
69+ self ._original_write_hosts = value
70+
3871 def _app_req (self , host , path , meth , timeout , params , data ):
3972 """
4073 Perform an HTTPS request with AppEngine's urlfetch. SSL certificate
@@ -91,6 +124,25 @@ def _session_req(self, host, path, meth, timeout, params, data):
91124
92125 res .raise_for_status ()
93126
127+ def _rotate_hosts (self , is_search ):
128+ if is_search :
129+ self ._read_hosts = rotate (self ._read_hosts )
130+ else :
131+ self ._write_hosts = rotate (self ._write_hosts )
132+
133+ def _get_hosts (self , is_search ):
134+ secs_since_rotate = time .time () - self .dns_timer
135+ if is_search :
136+ if secs_since_rotate < DNS_TIMER_DELAY :
137+ return self .read_hosts
138+ else :
139+ return self ._original_read_hosts
140+ else :
141+ if secs_since_rotate < DNS_TIMER_DELAY :
142+ return self .write_hosts
143+ else :
144+ return self ._original_write_hosts
145+
94146 def req (self , is_search , path , meth , params = None , data = None ):
95147 """Perform an HTTPS request with retry logic."""
96148 if params is not None :
@@ -99,12 +151,8 @@ def req(self, is_search, path, meth, params=None, data=None):
99151 if data is not None :
100152 data = json .dumps (data , cls = CustomJSONEncoder )
101153
102- if is_search :
103- hosts = self .read_hosts
104- timeout = self .search_timeout
105- else :
106- hosts = self .write_hosts
107- timeout = self .timeout
154+ hosts = self ._get_hosts (is_search )
155+ timeout = self .search_timeout if is_search else self .timeout
108156
109157 exceptions = {}
110158 for i , host in enumerate (hosts ):
@@ -120,6 +168,8 @@ def req(self, is_search, path, meth, params=None, data=None):
120168 except AlgoliaException as e :
121169 raise e
122170 except Exception as e :
171+ self ._rotate_hosts (is_search )
172+ self .dns_timer = time .time ()
123173 exceptions [host ] = "%s: %s" % (e .__class__ .__name__ , str (e ))
124174
125175 raise AlgoliaException ('Unreachable hosts: %s' % exceptions )
0 commit comments