Skip to content

Commit edf1ad7

Browse files
authored
Merge pull request #402 from splitio/async-memory-segment-storage
added memory segment storage async class
2 parents 6ce391f + 50f6492 commit edf1ad7

2 files changed

Lines changed: 194 additions & 2 deletions

File tree

splitio/storage/inmemmory.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,134 @@ def get_segments_keys_count(self):
576576
return total_count
577577

578578

579+
class InMemorySegmentStorageAsync(SegmentStorage):
580+
"""In-memory implementation of a segment async storage."""
581+
582+
def __init__(self):
583+
"""Constructor."""
584+
self._segments = {}
585+
self._change_numbers = {}
586+
self._lock = asyncio.Lock()
587+
588+
async def get(self, segment_name):
589+
"""
590+
Retrieve a segment.
591+
592+
:param segment_name: Name of the segment to fetch.
593+
:type segment_name: str
594+
595+
:rtype: str
596+
"""
597+
async with self._lock:
598+
fetched = self._segments.get(segment_name)
599+
if fetched is None:
600+
_LOGGER.debug(
601+
"Tried to retrieve nonexistant segment %s. Skipping",
602+
segment_name
603+
)
604+
return fetched
605+
606+
async def put(self, segment):
607+
"""
608+
Store a segment.
609+
610+
:param segment: Segment to store.
611+
:type segment: splitio.models.segment.Segment
612+
"""
613+
async with self._lock:
614+
self._segments[segment.name] = segment
615+
616+
async def update(self, segment_name, to_add, to_remove, change_number=None):
617+
"""
618+
Update a split. Create it if it doesn't exist.
619+
620+
:param segment_name: Name of the segment to update.
621+
:type segment_name: str
622+
:param to_add: Set of members to add to the segment.
623+
:type to_add: set
624+
:param to_remove: List of members to remove from the segment.
625+
:type to_remove: Set
626+
"""
627+
async with self._lock:
628+
if segment_name not in self._segments:
629+
self._segments[segment_name] = Segment(segment_name, to_add, change_number)
630+
return
631+
632+
self._segments[segment_name].update(to_add, to_remove)
633+
if change_number is not None:
634+
self._segments[segment_name].change_number = change_number
635+
636+
async def get_change_number(self, segment_name):
637+
"""
638+
Retrieve latest change number for a segment.
639+
640+
:param segment_name: Name of the segment.
641+
:type segment_name: str
642+
643+
:rtype: int
644+
"""
645+
async with self._lock:
646+
if segment_name not in self._segments:
647+
return None
648+
return self._segments[segment_name].change_number
649+
650+
async def set_change_number(self, segment_name, new_change_number):
651+
"""
652+
Set the latest change number.
653+
654+
:param segment_name: Name of the segment.
655+
:type segment_name: str
656+
:param new_change_number: New change number.
657+
:type new_change_number: int
658+
"""
659+
async with self._lock:
660+
if segment_name not in self._segments:
661+
return
662+
self._segments[segment_name].change_number = new_change_number
663+
664+
async def segment_contains(self, segment_name, key):
665+
"""
666+
Check whether a specific key belongs to a segment in storage.
667+
668+
:param segment_name: Name of the segment to search in.
669+
:type segment_name: str
670+
:param key: Key to search for.
671+
:type key: str
672+
673+
:return: True if the segment contains the key. False otherwise.
674+
:rtype: bool
675+
"""
676+
async with self._lock:
677+
if segment_name not in self._segments:
678+
_LOGGER.warning(
679+
"Tried to query members for nonexistant segment %s. Returning False",
680+
segment_name
681+
)
682+
return False
683+
return self._segments[segment_name].contains(key)
684+
685+
async def get_segments_count(self):
686+
"""
687+
Retrieve segments count.
688+
689+
:rtype: int
690+
"""
691+
async with self._lock:
692+
return len(self._segments)
693+
694+
async def get_segments_keys_count(self):
695+
"""
696+
Retrieve segments keys count.
697+
698+
:rtype: int
699+
"""
700+
total_count = 0
701+
async with self._lock:
702+
for segment in self._segments:
703+
total_count += len(self._segments[segment]._keys)
704+
return total_count
705+
706+
579707
class InMemoryImpressionStorage(ImpressionStorage):
580708
"""In memory implementation of an impressions storage."""
581709

tests/storage/test_inmemory_storage.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
from splitio.engine.telemetry import TelemetryStorageProducer
1212

1313
from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \
14-
InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, InMemorySplitStorageAsync
15-
14+
InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, InMemorySegmentStorageAsync, InMemorySplitStorageAsync
1615

1716
class InMemorySplitStorageTests(object):
1817
"""In memory split storage test cases."""
@@ -455,6 +454,71 @@ def test_segment_update(self):
455454
assert storage.get_change_number('some_segment') == 456
456455

457456

457+
class InMemorySegmentStorageAsyncTests(object):
458+
"""In memory segment storage tests."""
459+
460+
@pytest.mark.asyncio
461+
async def test_segment_storage_retrieval(self, mocker):
462+
"""Test storing and retrieving segments."""
463+
storage = InMemorySegmentStorageAsync()
464+
segment = mocker.Mock(spec=Segment)
465+
name_property = mocker.PropertyMock()
466+
name_property.return_value = 'some_segment'
467+
type(segment).name = name_property
468+
469+
await storage.put(segment)
470+
assert await storage.get('some_segment') == segment
471+
assert await storage.get('nonexistant-segment') is None
472+
473+
@pytest.mark.asyncio
474+
async def test_change_number(self, mocker):
475+
"""Test storing and retrieving segment changeNumber."""
476+
storage = InMemorySegmentStorageAsync()
477+
await storage.set_change_number('some_segment', 123)
478+
# Change number is not updated if segment doesn't exist
479+
assert await storage.get_change_number('some_segment') is None
480+
assert await storage.get_change_number('nonexistant-segment') is None
481+
482+
# Change number is updated if segment does exist.
483+
storage = InMemorySegmentStorageAsync()
484+
segment = mocker.Mock(spec=Segment)
485+
name_property = mocker.PropertyMock()
486+
name_property.return_value = 'some_segment'
487+
type(segment).name = name_property
488+
await storage.put(segment)
489+
await storage.set_change_number('some_segment', 123)
490+
assert await storage.get_change_number('some_segment') == 123
491+
492+
@pytest.mark.asyncio
493+
async def test_segment_contains(self, mocker):
494+
"""Test using storage to determine whether a key belongs to a segment."""
495+
storage = InMemorySegmentStorageAsync()
496+
segment = mocker.Mock(spec=Segment)
497+
name_property = mocker.PropertyMock()
498+
name_property.return_value = 'some_segment'
499+
type(segment).name = name_property
500+
await storage.put(segment)
501+
502+
await storage.segment_contains('some_segment', 'abc')
503+
assert segment.contains.mock_calls[0] == mocker.call('abc')
504+
505+
@pytest.mark.asyncio
506+
async def test_segment_update(self):
507+
"""Test updating a segment."""
508+
storage = InMemorySegmentStorageAsync()
509+
segment = Segment('some_segment', ['key1', 'key2', 'key3'], 123)
510+
await storage.put(segment)
511+
assert await storage.get('some_segment') == segment
512+
513+
await storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456)
514+
assert await storage.segment_contains('some_segment', 'key1')
515+
assert await storage.segment_contains('some_segment', 'key4')
516+
assert await storage.segment_contains('some_segment', 'key5')
517+
assert not await storage.segment_contains('some_segment', 'key2')
518+
assert not await storage.segment_contains('some_segment', 'key3')
519+
assert await storage.get_change_number('some_segment') == 456
520+
521+
458522
class InMemoryImpressionsStorageTests(object):
459523
"""InMemory impressions storage test cases."""
460524

0 commit comments

Comments
 (0)