Skip to content

Commit f3a2230

Browse files
authored
SG-34910 Fixup SSLEOFError exception (#346)
* Minor fixup to make sure the attempt log shows up in any cases * Add missing log * Fixup close the connection and attempt a new request in case of SSLEOFError * Remove deprecated code since the ssl module was introduced in Python 2.6 * Add a test for _make_call retry
1 parent 0323312 commit f3a2230

File tree

2 files changed

+120
-13
lines changed

2 files changed

+120
-13
lines changed

shotgun_api3/shotgun.py

+24-13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import os
4343
import re
4444
import copy
45+
import ssl
4546
import stat # used for attachment upload
4647
import sys
4748
import time
@@ -111,14 +112,6 @@ def _is_mimetypes_broken():
111112
have a self-signed internal certificate that isn't included in our certificate bundle, you may
112113
not require the added security provided by enforcing this.
113114
"""
114-
try:
115-
import ssl
116-
except ImportError as e:
117-
if "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ:
118-
raise ImportError("%s. SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable prevents "
119-
"disabling SSL certificate validation." % e)
120-
LOG.debug("ssl not found, disabling certificate validation")
121-
NO_SSL_VALIDATION = True
122115

123116
# ----------------------------------------------------------------------------
124117
# Version
@@ -3577,6 +3570,22 @@ def _make_call(self, verb, path, body, headers):
35773570
attempt += 1
35783571
try:
35793572
return self._http_request(verb, path, body, req_headers)
3573+
except ssl.SSLEOFError as e:
3574+
# SG-34910 - EOF occurred in violation of protocol (_ssl.c:2426)
3575+
# This issue seems to be related to proxy and keep alive.
3576+
# It looks like, sometimes, the proxy drops the connection on
3577+
# the TCP/TLS level despites the keep-alive. So we need to close
3578+
# the connection and make a new attempt.
3579+
LOG.debug("SSLEOFError: {}".format(e))
3580+
self._close_connection()
3581+
if attempt == max_rpc_attempts:
3582+
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
3583+
raise
3584+
# This is the exact same block as the "except Exception" bellow.
3585+
# We need to do it here because the next except will match it
3586+
# otherwise and will not re-attempt.
3587+
# When we drop support of Python 2 and we will probably drop the
3588+
# next except, we might want to remove this except too.
35803589
except ssl_error_classes as e:
35813590
# Test whether the exception is due to the fact that this is an older version of
35823591
# Python that cannot validate certificates encrypted with SHA-2. If it is, then
@@ -3608,17 +3617,19 @@ def _make_call(self, verb, path, body, headers):
36083617

36093618
self._close_connection()
36103619
if attempt == max_rpc_attempts:
3620+
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
36113621
raise
36123622
except Exception:
36133623
self._close_connection()
36143624
if attempt == max_rpc_attempts:
36153625
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
36163626
raise
3617-
LOG.debug(
3618-
"Request failed, attempt %d of %d. Retrying in %.2f seconds..." %
3619-
(attempt, max_rpc_attempts, rpc_attempt_interval)
3620-
)
3621-
time.sleep(rpc_attempt_interval)
3627+
3628+
LOG.debug(
3629+
"Request failed, attempt %d of %d. Retrying in %.2f seconds..." %
3630+
(attempt, max_rpc_attempts, rpc_attempt_interval)
3631+
)
3632+
time.sleep(rpc_attempt_interval)
36223633

36233634
def _http_request(self, verb, path, body, headers):
36243635
"""

tests/test_api.py

+96
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import datetime
1919
import sys
2020
import os
21+
from . import mock
2122
from .mock import patch, MagicMock
23+
import ssl
2224
import time
2325
import types
2426
import uuid
@@ -1900,6 +1902,100 @@ def test_status_not_200(self, mock_request):
19001902
mock_request.return_value = (response, {})
19011903
self.assertRaises(shotgun_api3.ProtocolError, self.sg.find_one, 'Shot', [])
19021904

1905+
@patch('shotgun_api3.shotgun.Http.request')
1906+
def test_make_call_retry(self, mock_request):
1907+
response = MagicMock(name="response mock", spec=dict)
1908+
response.status = 200
1909+
response.reason = 'reason'
1910+
mock_request.return_value = (response, {})
1911+
1912+
bak_rpc_attempt_interval = self.sg.config.rpc_attempt_interval
1913+
self.sg.config.rpc_attempt_interval = 0
1914+
try:
1915+
# First: make the request raise a consistent exception
1916+
mock_request.side_effect = Exception("not working")
1917+
with self.assertLogs(
1918+
'shotgun_api3', level='DEBUG'
1919+
) as cm1, self.assertRaises(
1920+
Exception
1921+
) as cm2:
1922+
self.sg.info()
1923+
1924+
self.assertEqual(cm2.exception.args[0], "not working")
1925+
log_content = "\n".join(cm1.output)
1926+
for i in [1,2]:
1927+
self.assertIn(
1928+
f"Request failed, attempt {i} of 3. Retrying",
1929+
log_content,
1930+
)
1931+
self.assertIn(
1932+
"Request failed. Giving up after 3 attempts.",
1933+
log_content,
1934+
)
1935+
1936+
# Then, make the exception happening only once and prove the
1937+
# retry works
1938+
def my_side_effect(*args, **kwargs):
1939+
try:
1940+
if my_side_effect.counter<1:
1941+
raise Exception("not working")
1942+
1943+
return mock.DEFAULT
1944+
finally:
1945+
my_side_effect.counter += 1
1946+
1947+
my_side_effect.counter = 0
1948+
mock_request.side_effect = my_side_effect
1949+
with self.assertLogs('shotgun_api3', level='DEBUG') as cm:
1950+
self.assertIsInstance(
1951+
self.sg.info(),
1952+
dict,
1953+
)
1954+
1955+
log_content = "\n".join(cm.output)
1956+
self.assertIn(
1957+
"Request failed, attempt 1 of 3. Retrying",
1958+
log_content,
1959+
)
1960+
self.assertNotIn(
1961+
"Request failed, attempt 2 of 3. Retrying",
1962+
log_content,
1963+
)
1964+
1965+
# Last: raise a SSLEOFError exception - SG-34910
1966+
def my_side_effect2(*args, **kwargs):
1967+
try:
1968+
if my_side_effect2.counter<1:
1969+
raise ssl.SSLEOFError(
1970+
"EOF occurred in violation of protocol (_ssl.c:2426)"
1971+
)
1972+
1973+
return mock.DEFAULT
1974+
finally:
1975+
my_side_effect2.counter += 1
1976+
1977+
my_side_effect2.counter = 0
1978+
mock_request.side_effect = my_side_effect2
1979+
1980+
with self.assertLogs('shotgun_api3', level='DEBUG') as cm:
1981+
self.assertIsInstance(
1982+
self.sg.info(),
1983+
dict,
1984+
)
1985+
1986+
log_content = "\n".join(cm.output)
1987+
self.assertIn("SSLEOFError", log_content)
1988+
self.assertIn(
1989+
"Request failed, attempt 1 of 3. Retrying",
1990+
log_content,
1991+
)
1992+
self.assertNotIn(
1993+
"Request failed, attempt 2 of 3. Retrying",
1994+
log_content,
1995+
)
1996+
finally:
1997+
self.sg.config.rpc_attempt_interval = bak_rpc_attempt_interval
1998+
19031999
@patch('shotgun_api3.shotgun.Http.request')
19042000
def test_sha2_error(self, mock_request):
19052001
# Simulate the exception raised with SHA-2 errors

0 commit comments

Comments
 (0)