Skip to content

Commit e4c1104

Browse files
committed
prepare storages to handle traffic type validation
1 parent 18fd148 commit e4c1104

File tree

9 files changed

+368
-85
lines changed

9 files changed

+368
-85
lines changed

splitio/storage/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ def get_all_splits(self):
8383
"""
8484
pass
8585

86+
@abc.abstractmethod
87+
def is_valid_traffic_type(self, traffic_type_name):
88+
"""
89+
Return whether the traffic type exists in at least one split in cache.
90+
91+
:param traffic_type_name: Traffic type to validate.
92+
:type traffic_type_name: str
93+
94+
:return: True if the traffic type is valid. False otherwise.
95+
:rtype: bool
96+
"""
97+
pass
98+
8699
def get_segment_names(self):
87100
"""
88101
Return a set of all segments referenced by splits in storage.

splitio/storage/inmemmory.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import logging
55
import threading
6+
from collections import Counter
7+
68
from six.moves import queue
79
from splitio.models.segments import Segment
810
from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, \
@@ -18,6 +20,7 @@ def __init__(self):
1820
self._lock = threading.RLock()
1921
self._splits = {}
2022
self._change_number = -1
23+
self._traffic_types = Counter()
2124

2225
def get(self, split_name):
2326
"""
@@ -40,6 +43,7 @@ def put(self, split):
4043
"""
4144
with self._lock:
4245
self._splits[split.name] = split
46+
self._increase_traffic_type_count(split.traffic_type_name)
4347

4448
def remove(self, split_name):
4549
"""
@@ -52,13 +56,15 @@ def remove(self, split_name):
5256
:rtype: bool
5357
"""
5458
with self._lock:
55-
try:
56-
self._splits.pop(split_name)
57-
return True
58-
except KeyError:
59+
split = self._splits.get(split_name)
60+
if not split:
5961
self._logger.warning("Tried to delete nonexistant split %s. Skipping", split_name)
6062
return False
6163

64+
self._splits.pop(split_name)
65+
self._decrease_traffic_type_count(split.traffic_type_name)
66+
return True
67+
6268
def get_change_number(self):
6369
"""
6470
Retrieve latest split change number.
@@ -98,6 +104,38 @@ def get_all_splits(self):
98104
with self._lock:
99105
return list(self._splits.values())
100106

107+
def is_valid_traffic_type(self, traffic_type_name):
108+
"""
109+
Return whether the traffic type exists in at least one split in cache.
110+
111+
:param traffic_type_name: Traffic type to validate.
112+
:type traffic_type_name: str
113+
114+
:return: True if the traffic type is valid. False otherwise.
115+
:rtype: bool
116+
"""
117+
with self._lock:
118+
return traffic_type_name in self._traffic_types
119+
120+
def _increase_traffic_type_count(self, traffic_type_name):
121+
"""
122+
Increase by one the count for a specific traffic type name.
123+
124+
:param traffic_type_name: Traffic type to increase the count.
125+
:type traffic_type_name: str
126+
"""
127+
self._traffic_types.update([traffic_type_name])
128+
129+
def _decrease_traffic_type_count(self, traffic_type_name):
130+
"""
131+
Decrease by one the count for a specific traffic type name.
132+
133+
:param traffic_type_name: Traffic type to decrease the count.
134+
:type traffic_type_name: str
135+
"""
136+
self._traffic_types.subtract([traffic_type_name])
137+
self._traffic_types += Counter()
138+
101139

102140
class InMemorySegmentStorage(SegmentStorage):
103141
"""In-memory implementation of a segment storage."""

splitio/storage/redis.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class RedisSplitStorage(SplitStorage):
1616

1717
_SPLIT_KEY = 'SPLITIO.split.{split_name}'
1818
_SPLIT_TILL_KEY = 'SPLITIO.splits.till'
19+
_TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}'
1920

2021
def __init__(self, redis_client):
2122
"""
@@ -39,6 +40,18 @@ def _get_key(self, split_name):
3940
"""
4041
return self._SPLIT_KEY.format(split_name=split_name)
4142

43+
def _get_traffic_type_key(self, traffic_type_name):
44+
"""
45+
Use the provided split_name to build the appropriate redis key.
46+
47+
:param split_name: Name of the split to interact with in redis.
48+
:type split_name: str
49+
50+
:return: Redis key.
51+
:rtype: str.
52+
"""
53+
return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name)
54+
4255
def get(self, split_name):
4356
"""
4457
Retrieve a split.
@@ -57,6 +70,25 @@ def get(self, split_name):
5770
self._logger.debug('Error: ', exc_info=True)
5871
return None
5972

73+
def is_valid_traffic_type(self, traffic_type_name):
74+
"""
75+
Return whether the traffic type exists in at least one split in cache.
76+
77+
:param traffic_type_name: Traffic type to validate.
78+
:type traffic_type_name: str
79+
80+
:return: True if the traffic type is valid. False otherwise.
81+
:rtype: bool
82+
"""
83+
try:
84+
raw = self._redis.get(self._get_traffic_type_key(traffic_type_name))
85+
count = json.loads(raw) if raw else 0
86+
return count > 0
87+
except RedisAdapterException:
88+
self._logger.error('Error fetching split from storage')
89+
self._logger.debug('Error: ', exc_info=True)
90+
return False
91+
6092
def put(self, split):
6193
"""
6294
Store a split.

splitio/storage/uwsgi.py

Lines changed: 126 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ class UWSGISplitStorage(SplitStorage):
1919

2020
_KEY_TEMPLATE = 'split.{suffix}'
2121
_KEY_TILL = 'splits.till'
22-
_KEY_FEATURE_LIST_LOCK = 'splits.list.lock'
2322
_KEY_FEATURE_LIST = 'splits.list'
23+
_KEY_FEATURE_LIST_LOCK = 'splits.list.lock'
24+
_KEY_TRAFFIC_TYPES = 'splits.traffic_types'
25+
_KEY_TRAFFIC_TYPES_LOCK = 'splits.traffic_types.lock'
2426
_OVERWRITE_LOCK_SECONDS = 5
2527

2628
def __init__(self, uwsgi_entrypoint):
@@ -64,21 +66,8 @@ def put(self, split):
6466
0,
6567
_SPLITIO_SPLITS_CACHE_NAMESPACE
6668
)
67-
68-
with UWSGILock(self._uwsgi, self._KEY_FEATURE_LIST_LOCK):
69-
try:
70-
current = set(json.loads(
71-
self._uwsgi.cache_get(self._KEY_FEATURE_LIST, _SPLITIO_MISC_NAMESPACE)
72-
))
73-
except TypeError:
74-
current = set()
75-
current.add(split.name)
76-
self._uwsgi.cache_update(
77-
self._KEY_FEATURE_LIST,
78-
json.dumps(list(current)),
79-
0,
80-
_SPLITIO_MISC_NAMESPACE
81-
)
69+
self._add_split_to_list(split.name)
70+
self._increase_traffic_type_count(split.traffic_type_name)
8271

8372
def remove(self, split_name):
8473
"""
@@ -90,31 +79,23 @@ def remove(self, split_name):
9079
:return: True if the split was found and removed. False otherwise.
9180
:rtype: bool
9281
"""
93-
with UWSGILock(self._uwsgi, self._KEY_FEATURE_LIST_LOCK):
94-
try:
95-
current = set(json.loads(
96-
self._uwsgi.cache_get(self._KEY_FEATURE_LIST, _SPLITIO_MISC_NAMESPACE)
97-
))
98-
current.remove(split_name)
99-
self._uwsgi.cache_update(
100-
self._KEY_FEATURE_LIST,
101-
json.dumps(list(current)),
102-
0,
103-
_SPLITIO_MISC_NAMESPACE
104-
)
105-
except TypeError:
106-
# Split list not found, no need to delete anything
107-
pass
108-
except KeyError:
109-
# Split not found in list. nothing to do.
110-
pass
82+
# We need to fetch the split to get the traffic type name prior to deleting.
83+
fetched = self.get(split_name)
84+
if fetched is None:
85+
self._logger.warning(
86+
"Tried to remove feature \"%s\" not present in cache. Ignoring.", split_name
87+
)
88+
return
11189

11290
result = self._uwsgi.cache_del(
11391
self._KEY_TEMPLATE.format(suffix=split_name),
11492
_SPLITIO_SPLITS_CACHE_NAMESPACE
11593
)
11694
if not result is False:
117-
self._logger.warning("Trying to retrieve nonexistant split %s. Ignoring.", split_name)
95+
self._logger.warning("Trying to delete nonexistant split %s. Ignoring.", split_name)
96+
97+
self._remove_split_from_list(split_name)
98+
self._decrease_traffic_type_count(fetched.traffic_type_name)
11899
return result
119100

120101
def get_change_number(self):
@@ -162,6 +143,116 @@ def get_all_splits(self):
162143
"""
163144
return [self.get(split_name) for split_name in self.get_split_names()]
164145

146+
def is_valid_traffic_type(self, traffic_type_name):
147+
"""
148+
Return whether the traffic type exists in at least one split in cache.
149+
150+
:param traffic_type_name: Traffic type to validate.
151+
:type traffic_type_name: str
152+
153+
:return: True if the traffic type is valid. False otherwise.
154+
:rtype: bool
155+
"""
156+
try:
157+
tts = json.loads(
158+
self._uwsgi.cache_get(self._KEY_TRAFFIC_TYPES, _SPLITIO_MISC_NAMESPACE)
159+
)
160+
return traffic_type_name in tts
161+
except TypeError:
162+
return False
163+
164+
def _add_split_to_list(self, split_name):
165+
"""
166+
Add a specific split to the list we keep track of.
167+
168+
:param split_name: Name of the split to add.
169+
:type split_name: str
170+
"""
171+
with UWSGILock(self._uwsgi, self._KEY_FEATURE_LIST_LOCK):
172+
try:
173+
current = set(json.loads(
174+
self._uwsgi.cache_get(self._KEY_FEATURE_LIST, _SPLITIO_MISC_NAMESPACE)
175+
))
176+
except TypeError:
177+
current = set()
178+
current.add(split_name)
179+
self._uwsgi.cache_update(
180+
self._KEY_FEATURE_LIST,
181+
json.dumps(list(current)),
182+
0,
183+
_SPLITIO_MISC_NAMESPACE
184+
)
185+
186+
def _remove_split_from_list(self, split_name):
187+
"""
188+
Remove a specific split from the list we keep track of.
189+
190+
:param split_name: Name of the split to remove.
191+
:type split_name: str
192+
"""
193+
with UWSGILock(self._uwsgi, self._KEY_FEATURE_LIST_LOCK):
194+
try:
195+
current = set(json.loads(
196+
self._uwsgi.cache_get(self._KEY_FEATURE_LIST, _SPLITIO_MISC_NAMESPACE)
197+
))
198+
current.remove(split_name)
199+
self._uwsgi.cache_update(
200+
self._KEY_FEATURE_LIST,
201+
json.dumps(list(current)),
202+
0,
203+
_SPLITIO_MISC_NAMESPACE
204+
)
205+
except TypeError:
206+
# Split list not found, no need to delete anything
207+
pass
208+
except KeyError:
209+
# Split not found in list. nothing to do.
210+
pass
211+
212+
def _increase_traffic_type_count(self, traffic_type_name):
213+
"""
214+
Increase by 1 the count for a specific traffic type.
215+
216+
:param traffic_type_name: Traffic type name to increase count.
217+
:type traffic_type_name: str
218+
"""
219+
with UWSGILock(self._uwsgi, self._KEY_TRAFFIC_TYPES_LOCK):
220+
try:
221+
tts = json.loads(
222+
self._uwsgi.cache_get(self._KEY_TRAFFIC_TYPES, _SPLITIO_MISC_NAMESPACE)
223+
)
224+
tts[traffic_type_name] = tts.get(traffic_type_name, 0) + 1
225+
226+
except TypeError:
227+
tts = {traffic_type_name: 1}
228+
229+
self._uwsgi.cache_update(
230+
self._KEY_TRAFFIC_TYPES, json.dumps(tts), 0, _SPLITIO_MISC_NAMESPACE
231+
)
232+
233+
def _decrease_traffic_type_count(self, traffic_type_name):
234+
"""
235+
Decreaase by 1 the count for a specific traffic type.
236+
237+
:param traffic_type_name: Traffic type name to decrease count.
238+
:type traffic_type_name: str
239+
"""
240+
with UWSGILock(self._uwsgi, self._KEY_TRAFFIC_TYPES_LOCK):
241+
try:
242+
tts = json.loads(
243+
self._uwsgi.cache_get(self._KEY_TRAFFIC_TYPES, _SPLITIO_MISC_NAMESPACE)
244+
)
245+
tts[traffic_type_name] = tts.get(traffic_type_name, 0) - 1
246+
if tts[traffic_type_name] <= 0:
247+
del tts[traffic_type_name]
248+
except TypeError:
249+
# Traffic type list not present. nothing to do here.
250+
return
251+
252+
self._uwsgi.cache_update(
253+
self._KEY_TRAFFIC_TYPES, json.dumps(tts), 0, _SPLITIO_MISC_NAMESPACE
254+
)
255+
165256

166257
class UWSGISegmentStorage(SegmentStorage):
167258
"""UWSGI-Cache based implementation of a split storage."""

0 commit comments

Comments
 (0)