Skip to content

Commit 4abe8d0

Browse files
author
Matias Melograno
committed
added cdn bypass segments
1 parent 2049b14 commit 4abe8d0

File tree

9 files changed

+237
-109
lines changed

9 files changed

+237
-109
lines changed

splitio/api/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Split API module."""
22

33

4+
_CACHE_CONTROL = 'Cache-Control'
5+
_CACHE_CONTROL_NO_CACHE = 'no-cache'
6+
7+
48
class APIException(Exception):
59
"""Exception to raise when an API call fails."""
610

@@ -76,3 +80,30 @@ def __eq__(self, other):
7680
if self._change_number != other._change_number:
7781
return False
7882
return True
83+
84+
85+
def build_fetch(change_number, fetch_options, metadata):
86+
"""
87+
Build fetch with new flags if that is the case.
88+
89+
:param change_number: Last known timestamp of definition.
90+
:type change_number: int
91+
92+
:param fetch_options: Fetch options for getting definitions.
93+
:type fetch_options: splitio.api.FetchOptions
94+
95+
:param metadata: Metadata Headers.
96+
:type metadata: dict
97+
98+
:return: Objects for fetch
99+
:rtype: dict, dict
100+
"""
101+
query = {'since': change_number}
102+
extra_headers = metadata
103+
if fetch_options is None:
104+
return query, extra_headers
105+
if fetch_options.cache_control_headers:
106+
extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE
107+
if fetch_options.change_number is not None:
108+
query['till'] = fetch_options.change_number
109+
return query, extra_headers

splitio/api/segments.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import logging
55

6-
from splitio.api import APIException, headers_from_metadata
6+
from splitio.api import APIException, headers_from_metadata, build_fetch
77
from splitio.api.client import HttpClientException
88

99

@@ -29,25 +29,30 @@ def __init__(self, http_client, apikey, sdk_metadata):
2929
self._apikey = apikey
3030
self._metadata = headers_from_metadata(sdk_metadata)
3131

32-
def fetch_segment(self, segment_name, change_number):
32+
def fetch_segment(self, segment_name, change_number, fetch_options):
3333
"""
3434
Fetch splits from backend.
3535
3636
:param segment_name: Name of the segment to fetch changes for.
3737
:type segment_name: str
38-
:param change_number: Last known timestamp of a split modification.
38+
39+
:param change_number: Last known timestamp of a segment modification.
3940
:type change_number: int
4041
42+
:param fetch_options: Fetch options for getting segment definitions.
43+
:type fetch_options: splitio.api.FetchOptions
44+
4145
:return: Json representation of a segmentChange response.
4246
:rtype: dict
4347
"""
4448
try:
49+
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata)
4550
response = self._client.get(
4651
'sdk',
4752
'/segmentChanges/{segment_name}'.format(segment_name=segment_name),
4853
self._apikey,
49-
extra_headers=self._metadata,
50-
query={'since': change_number}
54+
extra_headers=extra_headers,
55+
query=query,
5156
)
5257

5358
if 200 <= response.status_code < 300:

splitio/api/splits.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@
33
import logging
44
import json
55

6-
from splitio.api import APIException, headers_from_metadata
6+
from splitio.api import APIException, headers_from_metadata, build_fetch
77
from splitio.api.client import HttpClientException
88

99

1010
_LOGGER = logging.getLogger(__name__)
1111

1212

13-
_CACHE_CONTROL = 'Cache-Control'
14-
_CACHE_CONTROL_NO_CACHE = 'no-cache'
15-
16-
1713
class SplitsAPI(object): # pylint: disable=too-few-public-methods
1814
"""Class that uses an httpClient to communicate with the splits API."""
1915

@@ -32,29 +28,6 @@ def __init__(self, client, apikey, sdk_metadata):
3228
self._apikey = apikey
3329
self._metadata = headers_from_metadata(sdk_metadata)
3430

35-
def _build_fetch(self, change_number, fetch_options):
36-
"""
37-
Build fetch with new flags if that is the case.
38-
39-
:param change_number: Last known timestamp of a split modification.
40-
:type change_number: int
41-
42-
:param fetch_options: Fetch options for getting split definitions.
43-
:type fetch_options: splitio.api.FetchOptions
44-
45-
:return: Objects for fetch
46-
:rtype: dict, dict
47-
"""
48-
query = {'since': change_number}
49-
extra_headers = self._metadata
50-
if fetch_options is None:
51-
return query, extra_headers
52-
if fetch_options.cache_control_headers:
53-
extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE
54-
if fetch_options.change_number is not None:
55-
query['till'] = fetch_options.change_number
56-
return query, extra_headers
57-
5831
def fetch_splits(self, change_number, fetch_options):
5932
"""
6033
Fetch splits from backend.
@@ -69,7 +42,7 @@ def fetch_splits(self, change_number, fetch_options):
6942
:rtype: dict
7043
"""
7144
try:
72-
query, extra_headers = self._build_fetch(change_number, fetch_options)
45+
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata)
7346
response = self._client.get(
7447
'sdk',
7548
'/splitChanges',

splitio/client/input_validator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
import math
66

7-
from splitio.api import APIException
7+
from splitio.api import APIException, FetchOptions
88
from splitio.client.key import Key
99
from splitio.engine.evaluator import CONTROL
1010

@@ -458,7 +458,7 @@ def validate_apikey_type(segment_api):
458458
_logger = logging.getLogger('splitio.api.segments')
459459
try:
460460
_logger.addFilter(api_messages_filter) # pylint: disable=protected-access
461-
segment_api.fetch_segment('__SOME_INVALID_SEGMENT__', -1)
461+
segment_api.fetch_segment('__SOME_INVALID_SEGMENT__', -1, FetchOptions())
462462
except APIException as exc:
463463
if exc.status_code == 403:
464464
_LOGGER.error('factory instantiation: you passed a browser type '

splitio/sync/segment.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import logging
2+
import time
23

3-
from splitio.api import APIException
4+
from splitio.api import APIException, FetchOptions
45
from splitio.tasks.util import workerpool
56
from splitio.models import segments
7+
from splitio.util.backoff import Backoff
68

79

810
_LOGGER = logging.getLogger(__name__)
911

1012

13+
_ON_DEMAND_FETCH_BACKOFF_BASE = 10 # backoff base starting at 10 seconds
14+
_ON_DEMAND_FETCH_BACKOFF_MAX_WAIT = 60 # don't sleep for more than 1 minute
15+
_ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10
16+
17+
1118
class SegmentSynchronizer(object):
1219
def __init__(self, segment_api, split_storage, segment_storage):
1320
"""
@@ -28,6 +35,9 @@ def __init__(self, segment_api, split_storage, segment_storage):
2835
self._segment_storage = segment_storage
2936
self._worker_pool = workerpool.WorkerPool(10, self.synchronize_segment)
3037
self._worker_pool.start()
38+
self._backoff = Backoff(
39+
_ON_DEMAND_FETCH_BACKOFF_BASE,
40+
_ON_DEMAND_FETCH_BACKOFF_MAX_WAIT)
3141

3242
def recreate(self):
3343
"""
@@ -44,27 +54,33 @@ def shutdown(self):
4454
"""
4555
self._worker_pool.stop()
4656

47-
def synchronize_segment(self, segment_name, till=None):
57+
def _fetch_until(self, segment_name, fetch_options, till=None):
4858
"""
49-
Update a segment from queue
59+
Hit endpoint, update storage and return when since==till.
5060
5161
:param segment_name: Name of the segment to update.
5262
:type segment_name: str
5363
54-
:param till: ChangeNumber received.
64+
:param fetch_options Fetch options for getting segment definitions.
65+
:type fetch_options splitio.api.FetchOptions
66+
67+
:param till: Passed till from Streaming.
5568
:type till: int
5669
70+
:return: last change number
71+
:rtype: int
5772
"""
58-
while True:
73+
while True: # Fetch until since==till
5974
change_number = self._segment_storage.get_change_number(segment_name)
6075
if change_number is None:
6176
change_number = -1
6277
if till is not None and till < change_number:
6378
# the passed till is less than change_number, no need to perform updates
64-
return
79+
return change_number
6580

6681
try:
67-
segment_changes = self._api.fetch_segment(segment_name, change_number)
82+
segment_changes = self._api.fetch_segment(segment_name, change_number,
83+
fetch_options)
6884
except APIException as exc:
6985
_LOGGER.error('Exception raised while fetching segment %s', segment_name)
7086
_LOGGER.debug('Exception information: ', exc_info=True)
@@ -82,7 +98,63 @@ def synchronize_segment(self, segment_name, till=None):
8298
)
8399

84100
if segment_changes['till'] == segment_changes['since']:
85-
return
101+
return segment_changes['till']
102+
103+
def _attempt_segment_sync(self, segment_name, fetch_options, till=None):
104+
"""
105+
Hit endpoint, update storage and return True if sync is complete.
106+
107+
:param segment_name: Name of the segment to update.
108+
:type segment_name: str
109+
110+
:param fetch_options Fetch options for getting split definitions.
111+
:type fetch_options splitio.api.FetchOptions
112+
113+
:param till: Passed till from Streaming.
114+
:type till: int
115+
116+
:return: Flags to check if it should perform bypass or operation ended
117+
:rtype: bool, int, int
118+
"""
119+
self._backoff.reset()
120+
remaining_attempts = _ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES
121+
while True:
122+
remaining_attempts -= 1
123+
change_number = self._fetch_until(segment_name, fetch_options, till)
124+
if till is None or till <= change_number:
125+
return True, remaining_attempts, change_number
126+
elif remaining_attempts <= 0:
127+
return False, remaining_attempts, change_number
128+
how_long = self._backoff.get()
129+
time.sleep(how_long)
130+
131+
def synchronize_segment(self, segment_name, till=None):
132+
"""
133+
Update a segment from queue
134+
135+
:param segment_name: Name of the segment to update.
136+
:type segment_name: str
137+
138+
:param till: ChangeNumber received.
139+
:type till: int
140+
141+
"""
142+
fetch_options = FetchOptions(True) # Set Cache-Control to no-cache
143+
successful_sync, remaining_attempts, change_number = self._attempt_segment_sync(segment_name, fetch_options, till)
144+
attempts = _ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - remaining_attempts
145+
if successful_sync: # succedeed sync
146+
_LOGGER.debug('Refresh completed in %s attempts.', attempts)
147+
return
148+
with_cdn_bypass = FetchOptions(True, change_number) # Set flag for bypassing CDN
149+
without_cdn_successful_sync, remaining_attempts, change_number = self._attempt_segment_sync(segment_name, with_cdn_bypass, till)
150+
without_cdn_attempts = _ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - remaining_attempts
151+
if without_cdn_successful_sync:
152+
_LOGGER.debug('Refresh completed bypassing the CDN in %s attempts.',
153+
without_cdn_attempts)
154+
return
155+
else:
156+
_LOGGER.debug('No changes fetched after %s attempts with CDN bypassed.',
157+
without_cdn_attempts)
86158

87159
def synchronize_segments(self):
88160
"""

tests/api/test_segments_api.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Segment API tests module."""
22

33
import pytest
4-
from splitio.api import segments, client, APIException
4+
from splitio.api import segments, client, APIException, FetchOptions
55
from splitio.client.util import SdkMetadata
66

77

@@ -13,8 +13,8 @@ def test_fetch_segment_changes(self, mocker):
1313
httpclient = mocker.Mock(spec=client.HttpClient)
1414
httpclient.get.return_value = client.HttpResponse(200, '{"prop1": "value1"}')
1515
segment_api = segments.SegmentsAPI(httpclient, 'some_api_key', SdkMetadata('1.0', 'some', '1.2.3.4'))
16-
response = segment_api.fetch_segment('some_segment', 123)
1716

17+
response = segment_api.fetch_segment('some_segment', 123, FetchOptions())
1818
assert response['prop1'] == 'value1'
1919
assert httpclient.get.mock_calls == [mocker.call('sdk', '/segmentChanges/some_segment', 'some_api_key',
2020
extra_headers={
@@ -24,11 +24,35 @@ def test_fetch_segment_changes(self, mocker):
2424
},
2525
query={'since': 123})]
2626

27+
httpclient.reset_mock()
28+
response = segment_api.fetch_segment('some_segment', 123, FetchOptions(True))
29+
assert response['prop1'] == 'value1'
30+
assert httpclient.get.mock_calls == [mocker.call('sdk', '/segmentChanges/some_segment', 'some_api_key',
31+
extra_headers={
32+
'SplitSDKVersion': '1.0',
33+
'SplitSDKMachineIP': '1.2.3.4',
34+
'SplitSDKMachineName': 'some',
35+
'Cache-Control': 'no-cache'
36+
},
37+
query={'since': 123})]
38+
39+
httpclient.reset_mock()
40+
response = segment_api.fetch_segment('some_segment', 123, FetchOptions(True, 123))
41+
assert response['prop1'] == 'value1'
42+
assert httpclient.get.mock_calls == [mocker.call('sdk', '/segmentChanges/some_segment', 'some_api_key',
43+
extra_headers={
44+
'SplitSDKVersion': '1.0',
45+
'SplitSDKMachineIP': '1.2.3.4',
46+
'SplitSDKMachineName': 'some',
47+
'Cache-Control': 'no-cache'
48+
},
49+
query={'since': 123, 'till': 123})]
50+
2751
httpclient.reset_mock()
2852
def raise_exception(*args, **kwargs):
2953
raise client.HttpClientException('some_message')
3054
httpclient.get.side_effect = raise_exception
3155
with pytest.raises(APIException) as exc_info:
32-
response = segment_api.fetch_segment('some_segment', 123)
56+
response = segment_api.fetch_segment('some_segment', 123, FetchOptions())
3357
assert exc_info.type == APIException
3458
assert exc_info.value.message == 'some_message'

0 commit comments

Comments
 (0)