Skip to content

Commit 278dc76

Browse files
authored
Merge pull request #37 from configcat/offline
LayzLoad TTL, AutoPoll Interval should be stored in cache
2 parents bf20ac6 + 5f172c9 commit 278dc76

12 files changed

+301
-116
lines changed

configcatclient/autopollingcachepolicy.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import logging
22
import sys
3-
import datetime
43
import time
54
from threading import Thread, Event
65
from requests import HTTPError
76
from requests import Timeout
7+
from datetime import datetime
8+
from datetime import timedelta
89

10+
from . import utils
11+
from .configfetcher import FetchResponse
912
from .readwritelock import ReadWriteLock
1013
from .interfaces import CachePolicy
1114

@@ -26,11 +29,11 @@ def __init__(self, config_fetcher, config_cache, cache_key,
2629
self._config_cache = config_cache
2730
self._cache_key = cache_key
2831
self._poll_interval_seconds = poll_interval_seconds
29-
self._max_init_wait_time_seconds = datetime.timedelta(seconds=max_init_wait_time_seconds)
32+
self._max_init_wait_time_seconds = timedelta(seconds=max_init_wait_time_seconds)
3033
self._on_configuration_changed_callback = on_configuration_changed_callback
3134
self._initialized = False
3235
self._is_running = False
33-
self._start_time = datetime.datetime.utcnow()
36+
self._start_time = utils.get_utc_now()
3437
self._lock = ReadWriteLock()
3538

3639
self.thread = Thread(target=self._run, args=[])
@@ -50,32 +53,40 @@ def _run(self):
5053

5154
def get(self):
5255
while not self._initialized \
53-
and datetime.datetime.utcnow() < self._start_time + self._max_init_wait_time_seconds:
56+
and utils.get_utc_now() < self._start_time + self._max_init_wait_time_seconds:
5457
time.sleep(.500)
5558

5659
try:
5760
self._lock.acquire_read()
58-
return self._config_cache.get(self._cache_key)
61+
configuration = self._config_cache.get(self._cache_key)
62+
return configuration.get(FetchResponse.CONFIG) if configuration else None
5963
finally:
6064
self._lock.release_read()
6165

6266
def force_refresh(self):
6367
try:
6468
old_configuration = None
65-
force_fetch = False
69+
etag = ''
6670

6771
try:
6872
self._lock.acquire_read()
6973
old_configuration = self._config_cache.get(self._cache_key)
70-
force_fetch = not bool(old_configuration)
74+
if bool(old_configuration):
75+
etag = old_configuration.get(FetchResponse.ETAG, '')
76+
# Cache isn't expired
77+
utc_now = utils.get_utc_now()
78+
if datetime.utcfromtimestamp(old_configuration.get(FetchResponse.FETCH_TIME, 0) + self._poll_interval_seconds) > utc_now:
79+
self._initialized = True
80+
return
7181
finally:
7282
self._lock.release_read()
7383

74-
configuration_response = self._config_fetcher.get_configuration_json(force_fetch)
84+
configuration_response = self._config_fetcher.get_configuration_json(etag)
7585

7686
if configuration_response.is_fetched():
7787
configuration = configuration_response.json()
78-
if configuration != old_configuration:
88+
if configuration is None or old_configuration is None or \
89+
configuration.get(FetchResponse.CONFIG) != old_configuration.get(FetchResponse.CONFIG):
7990
try:
8091
self._lock.acquire_write()
8192
self._config_cache.set(self._cache_key, configuration)

configcatclient/configfetcher.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import logging
33
import sys
44
from enum import IntEnum
5+
from platform import python_version
56

67
from .datagovernance import DataGovernance
8+
from .utils import get_utc_now_seconds_since_epoch
79
from .version import CONFIGCATCLIENT_VERSION
810
from .constants import *
911

@@ -25,14 +27,23 @@ class RedirectMode(IntEnum):
2527

2628

2729
class FetchResponse(object):
28-
def __init__(self, response):
30+
ETAG = 'etag'
31+
FETCH_TIME = 'fetch_time'
32+
CONFIG = 'config'
33+
34+
def __init__(self, response, etag='', fetch_time=None):
2935
self._response = response
36+
self._etag = etag
37+
self._fetch_time = fetch_time if fetch_time is not None else get_utc_now_seconds_since_epoch()
3038

3139
def json(self):
3240
"""Returns the json-encoded content of a response, if any.
3341
:raises ValueError: If the response body does not contain valid json.
3442
"""
35-
return self._response.json()
43+
json = self._response.json()
44+
return {FetchResponse.ETAG: self._etag,
45+
FetchResponse.FETCH_TIME: self._fetch_time,
46+
FetchResponse.CONFIG: json}
3647

3748
def is_fetched(self):
3849
"""Gets whether a new configuration value was fetched or not.
@@ -55,9 +66,9 @@ def __init__(self, sdk_key, mode, base_url=None, proxies=None, proxy_auth=None,
5566
self._proxy_auth = proxy_auth
5667
self._connect_timeout = connect_timeout
5768
self._read_timeout = read_timeout
58-
self._etag = ''
5969
self._headers = {'User-Agent': 'ConfigCat-Python/' + mode + '-' + CONFIGCATCLIENT_VERSION,
6070
'X-ConfigCat-UserAgent': 'ConfigCat-Python/' + mode + '-' + CONFIGCATCLIENT_VERSION,
71+
'X-ConfigCat-PythonVersion': python_version(),
6172
'Content-Type': "application/json"}
6273
if base_url is not None:
6374
self._base_url_overridden = True
@@ -75,31 +86,31 @@ def get_connect_timeout(self):
7586
def get_read_timeout(self):
7687
return self._read_timeout
7788

78-
def get_configuration_json(self, force_fetch=False, retries=0):
89+
def get_configuration_json(self, etag='', retries=0):
7990
"""
8091
:return: Returns the FetchResponse object contains configuration json Dictionary
8192
"""
8293
uri = self._base_url + '/' + BASE_PATH + self._sdk_key + BASE_EXTENSION
8394
headers = self._headers
84-
if self._etag and not force_fetch:
85-
headers['If-None-Match'] = self._etag
95+
if etag:
96+
headers['If-None-Match'] = etag
8697
else:
8798
headers['If-None-Match'] = None
8899

89100
response = requests.get(uri, headers=headers, timeout=(self._connect_timeout, self._read_timeout),
90101
proxies=self._proxies, auth=self._proxy_auth)
91102
response.raise_for_status()
92-
etag = response.headers.get('Etag')
93-
if etag:
94-
self._etag = etag
103+
response_etag = response.headers.get('Etag')
104+
if response_etag is None:
105+
response_etag = ''
95106

96-
fetch_response = FetchResponse(response)
107+
fetch_response = FetchResponse(response, response_etag)
97108

98109
# If there wasn't a config change, we return the response.
99110
if not fetch_response.is_fetched():
100111
return fetch_response
101112

102-
preferences = fetch_response.json().get(PREFERENCES, None)
113+
preferences = fetch_response.json()[FetchResponse.CONFIG].get(PREFERENCES, None)
103114
if preferences is None:
104115
return fetch_response
105116

@@ -137,4 +148,4 @@ def get_configuration_json(self, force_fetch=False, retries=0):
137148
return fetch_response
138149

139150
# Retry the config download with the new base_url
140-
return self.get_configuration_json(force_fetch, retries + 1)
151+
return self.get_configuration_json(etag, retries + 1)

configcatclient/interfaces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def set(self, key, value):
2222

2323
class CachePolicy(object):
2424
"""
25-
Config cache interface
25+
Cache policy interface
2626
"""
2727
__metaclass__ = ABCMeta
2828

configcatclient/lazyloadingcachepolicy.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import logging
22
import sys
3-
import datetime
3+
from datetime import datetime
4+
from datetime import timedelta
5+
46
from requests import HTTPError
57

8+
from . import utils
9+
from .configfetcher import FetchResponse
610
from .readwritelock import ReadWriteLock
711
from .interfaces import CachePolicy
812

@@ -16,22 +20,23 @@ def __init__(self, config_fetcher, config_cache, cache_key, cache_time_to_live_s
1620
self._config_fetcher = config_fetcher
1721
self._config_cache = config_cache
1822
self._cache_key = cache_key
19-
self._cache_time_to_live = datetime.timedelta(seconds=cache_time_to_live_seconds)
23+
self._cache_time_to_live = timedelta(seconds=cache_time_to_live_seconds)
2024
self._lock = ReadWriteLock()
21-
self._last_updated = None
2225

2326
def get(self):
24-
config = None
27+
configuration = None
28+
etag = ''
2529

2630
try:
2731
self._lock.acquire_read()
2832

29-
config = self._config_cache.get(self._cache_key)
33+
configuration = self._config_cache.get(self._cache_key)
34+
35+
utc_now = utils.get_utc_now()
3036

31-
utc_now = datetime.datetime.utcnow()
32-
if self._last_updated is not None and self._last_updated + self._cache_time_to_live > utc_now:
33-
if config is not None:
34-
return config
37+
if configuration is not None:
38+
if datetime.utcfromtimestamp(configuration.get(FetchResponse.FETCH_TIME, 0)) + self._cache_time_to_live > utc_now:
39+
return configuration.get(FetchResponse.CONFIG)
3540
finally:
3641
self._lock.release_read()
3742

@@ -40,43 +45,43 @@ def get(self):
4045
# If while waiting to acquire the write lock another
4146
# thread has updated the content, then don't bother requesting
4247
# to the server to minimise time.
43-
if config is None or self._last_updated is None or self._last_updated + self._cache_time_to_live <= datetime.datetime.utcnow():
44-
force_fetch = not bool(config)
45-
self._force_refresh(force_fetch)
48+
utc_now = utils.get_utc_now()
49+
if configuration is None or datetime.utcfromtimestamp(configuration.get(FetchResponse.FETCH_TIME, 0)) + self._cache_time_to_live <= utc_now:
50+
if bool(configuration):
51+
etag = configuration.get(FetchResponse.ETAG, '')
52+
self._force_refresh(etag)
4653
finally:
4754
self._lock.release_write()
4855

4956
try:
5057
self._lock.acquire_read()
51-
config = self._config_cache.get(self._cache_key)
52-
return config
58+
configuration = self._config_cache.get(self._cache_key)
59+
return configuration.get(FetchResponse.CONFIG) if configuration else None
5360
finally:
5461
self._lock.release_read()
5562

56-
def force_refresh(self):
57-
force_fetch = False
58-
63+
def force_refresh(self, etag=''):
5964
try:
6065
self._lock.acquire_read()
61-
config = self._config_cache.get(self._cache_key)
62-
force_fetch = not bool(config)
66+
configuration = self._config_cache.get(self._cache_key)
67+
if bool(configuration):
68+
etag = configuration.get(FetchResponse.ETAG, '')
6369
finally:
6470
self._lock.release_read()
6571

6672
try:
6773
self._lock.acquire_write()
68-
self._force_refresh(force_fetch)
74+
self._force_refresh(etag)
6975
finally:
7076
self._lock.release_write()
7177

72-
def _force_refresh(self, force_fetch):
78+
def _force_refresh(self, etag):
7379
try:
74-
configuration_response = self._config_fetcher.get_configuration_json(force_fetch)
75-
# set _last_updated regardless of whether the cache is updated
80+
configuration_response = self._config_fetcher.get_configuration_json(etag)
81+
# set _config_cache regardless of whether the cache is updated
7682
# or whether a 304 not modified has been sent back as the content
7783
# we have hasn't been updated on the server so not need
7884
# for subsequent requests to retry this within the cache time to live
79-
self._last_updated = datetime.datetime.utcnow()
8085
if configuration_response.is_fetched():
8186
configuration = configuration_response.json()
8287
self._config_cache.set(self._cache_key, configuration)

configcatclient/manualpollingcachepolicy.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33
from requests import HTTPError
44

5+
from .configfetcher import FetchResponse
56
from .readwritelock import ReadWriteLock
67
from .interfaces import CachePolicy
78

@@ -19,23 +20,24 @@ def get(self):
1920
try:
2021
self._lock.acquire_read()
2122

22-
config = self._config_cache.get(self._cache_key)
23-
return config
23+
configuration = self._config_cache.get(self._cache_key)
24+
return configuration.get(FetchResponse.CONFIG) if configuration else None
2425
finally:
2526
self._lock.release_read()
2627

2728
def force_refresh(self):
28-
force_fetch = False
29+
etag = ''
2930

3031
try:
3132
self._lock.acquire_read()
32-
config = self._config_cache.get(self._cache_key)
33-
force_fetch = not bool(config)
33+
old_configuration = self._config_cache.get(self._cache_key)
34+
if bool(old_configuration):
35+
etag = old_configuration.get(FetchResponse.ETAG, '')
3436
finally:
3537
self._lock.release_read()
3638

3739
try:
38-
configuration_response = self._config_fetcher.get_configuration_json(force_fetch)
40+
configuration_response = self._config_fetcher.get_configuration_json(etag)
3941
if configuration_response.is_fetched():
4042
configuration = configuration_response.json()
4143
try:

configcatclient/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import sys
22
import inspect
33
from qualname import qualname
4+
from datetime import datetime
5+
6+
epoch_time = datetime(1970, 1, 1)
47

58

69
def get_class_from_method(method):
@@ -49,3 +52,15 @@ def method_is_called_from(method, level=1):
4952
if calling_class == expected_class:
5053
return True
5154
return False
55+
56+
57+
def get_utc_now():
58+
return datetime.utcnow()
59+
60+
61+
def get_seconds_since_epoch(date_time):
62+
return (date_time - epoch_time).total_seconds()
63+
64+
65+
def get_utc_now_seconds_since_epoch():
66+
return get_seconds_since_epoch(get_utc_now())

0 commit comments

Comments
 (0)