Skip to content

Commit e546efb

Browse files
feat: improve reliability of certificate refresh (#599)
1 parent cdd7fc9 commit e546efb

File tree

3 files changed

+57
-29
lines changed

3 files changed

+57
-29
lines changed

google/cloud/sql/connector/instance.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,8 @@ async def _refresh_task(self: Instance, delay: int) -> InstanceMetadata:
407407
raise
408408
# if valid refresh, replace current with valid metadata and schedule next refresh
409409
self._current = refresh_task
410-
# Ephemeral certificate expires in 1 hour, so we schedule a refresh to happen in 55 minutes.
411-
delay = _seconds_until_refresh(
412-
refresh_data.expiration, self._enable_iam_auth
413-
)
410+
# calculate refresh delay based on certificate expiration
411+
delay = _seconds_until_refresh(refresh_data.expiration)
414412
self._next = self._schedule_refresh(delay)
415413

416414
return refresh_data

google/cloud/sql/connector/refresh_utils.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,9 @@
2929

3030
_sql_api_version: str = "v1beta4"
3131

32-
# default_refresh_buffer is the amount of time before a refresh's result expires
32+
# _refresh_buffer is the amount of time before a refresh's result expires
3333
# that a new refresh operation begins.
34-
_default_refresh_buffer: int = 5 * 60 # 5 minutes
35-
36-
# _iam_auth_refresh_buffer is the amount of time before a refresh's result expires
37-
# that a new refresh operation begins when IAM DB AuthN is enabled. Because token
38-
# sources may be cached until ~60 seconds before expiration, this value must be smaller
39-
# than default_refresh_buffer.
40-
_iam_auth_refresh_buffer: int = 55 # seconds
34+
_refresh_buffer: int = 4 * 60 # 4 minutes
4135

4236

4337
async def _get_metadata(
@@ -194,29 +188,28 @@ async def _get_ephemeral(
194188

195189
def _seconds_until_refresh(
196190
expiration: datetime.datetime,
197-
enable_iam_auth: bool,
198191
) -> int:
199192
"""
200-
Helper function to get time in seconds before performing next refresh.
193+
Calculates the duration to wait before starting the next refresh.
194+
195+
Usually the duration will be half of the time until certificate
196+
expiration.
201197
202198
:rtype: int
203199
:returns: Time in seconds to wait before performing next refresh.
204200
"""
205-
if enable_iam_auth:
206-
refresh_buffer = _iam_auth_refresh_buffer
207-
else:
208-
refresh_buffer = _default_refresh_buffer
209201

210-
delay = (expiration - datetime.datetime.now()) - datetime.timedelta(
211-
seconds=refresh_buffer
212-
)
202+
duration = int((expiration - datetime.datetime.now()).total_seconds())
213203

214-
if delay.total_seconds() < 0:
215-
# If the time until the certificate expires is less than the buffer,
216-
# schedule the refresh closer to the expiration time
217-
delay = (expiration - datetime.datetime.now()) - datetime.timedelta(seconds=5)
204+
# if certificate duration is less than 1 hour
205+
if duration < 3600:
206+
# something is wrong with certificate, refresh now
207+
if duration < _refresh_buffer:
208+
return 0
209+
# otherwise wait until 4 minutes before expiration for next refresh
210+
return duration - _refresh_buffer
218211

219-
return int(delay.total_seconds())
212+
return duration // 2
220213

221214

222215
async def _is_valid(task: asyncio.Task) -> bool:

tests/unit/test_refresh_utils.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""""
1+
"""
22
Copyright 2021 Google LLC
33
44
Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
"""
1616
from typing import Any, no_type_check
17-
17+
from datetime import datetime, timedelta
1818
import aiohttp
1919
from google.auth.credentials import Credentials
2020
import google.oauth2.credentials
@@ -28,6 +28,7 @@
2828
_get_metadata,
2929
_is_valid,
3030
_downscope_credentials,
31+
_seconds_until_refresh,
3132
)
3233
from google.cloud.sql.connector.utils import generate_keys
3334

@@ -37,7 +38,7 @@
3738
instance_metadata_expired,
3839
FakeCSQLInstance,
3940
)
40-
from tests.conftest import SCOPES # type: ignore
41+
from conftest import SCOPES # type: ignore
4142

4243

4344
@pytest.fixture
@@ -265,3 +266,39 @@ def test_downscope_credentials_user() -> None:
265266
# verify downscoped credentials have new scope
266267
assert credentials.scopes == ["https://www.googleapis.com/auth/sqlservice.login"]
267268
assert credentials != creds
269+
270+
271+
def test_seconds_until_refresh_over_1_hour() -> None:
272+
"""
273+
Test _seconds_until_refresh returns proper time in seconds.
274+
275+
If expiration is over 1 hour, should return duration/2.
276+
"""
277+
# using pytest.approx since sometimes can be off by a second
278+
assert (
279+
pytest.approx(_seconds_until_refresh(datetime.now() + timedelta(minutes=62)), 1)
280+
== 31 * 60
281+
)
282+
283+
284+
def test_seconds_until_refresh_under_1_hour_over_4_mins() -> None:
285+
"""
286+
Test _seconds_until_refresh returns proper time in seconds.
287+
288+
If expiration is under 1 hour and over 4 minutes,
289+
should return duration-refresh_buffer (refresh_buffer = 4 minutes).
290+
"""
291+
# using pytest.approx since sometimes can be off by a second
292+
assert (
293+
pytest.approx(_seconds_until_refresh(datetime.now() + timedelta(minutes=5)), 1)
294+
== 60
295+
)
296+
297+
298+
def test_seconds_until_refresh_under_4_mins() -> None:
299+
"""
300+
Test _seconds_until_refresh returns proper time in seconds.
301+
302+
If expiration is under 4 minutes, should return 0.
303+
"""
304+
assert _seconds_until_refresh(datetime.now() + timedelta(minutes=3)) == 0

0 commit comments

Comments
 (0)