Skip to content

Uwsgi shared memory #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Feb 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
2.1.x
2.2.0
- Added uwsgi cache support
- Fixed HTTP status code exceptions
2.1.0
- Added enabled labels
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
tests_require=tests_require,
extras_require={
'test': tests_require,
'redis': ['redis>=2.10.5', 'jsonpickle>=0.9.3']
'redis': ['redis>=2.10.5', 'jsonpickle>=0.9.3'],
'uwsgi': ['uwsgi>=2.0.0', 'jsonpickle>=0.9.3']
},
setup_requires=['nose'],
classifiers=[
Expand Down
219 changes: 165 additions & 54 deletions splitio/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from splitio.config import DEFAULT_CONFIG, MAX_INTERVAL, parse_config_file
from splitio.treatments import CONTROL

from splitio.uwsgi import (UWSGISplitCache, UWSGIImpressionsCache, UWSGIMetricsCache, get_uwsgi)


class Key(object):
def __init__(self, matching_key, bucketing_key):
Expand Down Expand Up @@ -195,6 +197,59 @@ def random_interval():

return random_interval

class JSONFileClient(Client):
def __init__(self, segment_changes_file_name, split_changes_file_name):
"""
A Client implementation that uses responses from the segmentChanges and splitChanges
resources to provide access to splits. It is intended to be used on integration
tests only.

:param segment_changes_file_name: The name of the file with the segmentChanges response
:type segment_changes_file_name: str
:param split_changes_file_name: The name of the file with the splitChanges response
:type split_changes_file_name: str
"""
super(JSONFileClient, self).__init__()
self._segment_changes_file_name = segment_changes_file_name
self._split_changes_file_name = split_changes_file_name
self._split_fetcher = self._build_split_fetcher()
self._treatment_log = TreatmentLog()
self._metrics = Metrics()

def _build_split_fetcher(self):
"""
Build the json backed split fetcher
:return: The json backed split fetcher
:rtype: SelfRefreshingSplitFetcher
"""
segment_fetcher = JSONFileSegmentFetcher(self._segment_changes_file_name)
split_parser = SplitParser(segment_fetcher)
split_fetcher = JSONFileSplitFetcher(self._split_changes_file_name, split_parser)

return split_fetcher

def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""Get the treatment log implementation.
:return: The treatment log implementation.
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""Get the metrics implementation.
:return: The metrics implementation.
:rtype: Metrics
"""
return self._metrics


class SelfRefreshingClient(Client):
def __init__(self, api_key, config=None, sdk_api_base_url=None, events_api_base_url=None):
Expand Down Expand Up @@ -350,60 +405,6 @@ def get_metrics(self):
return self._metrics


class JSONFileClient(Client):
def __init__(self, segment_changes_file_name, split_changes_file_name):
"""
A Client implementation that uses responses from the segmentChanges and splitChanges
resources to provide access to splits. It is intended to be used on integration
tests only.

:param segment_changes_file_name: The name of the file with the segmentChanges response
:type segment_changes_file_name: str
:param split_changes_file_name: The name of the file with the splitChanges response
:type split_changes_file_name: str
"""
super(JSONFileClient, self).__init__()
self._segment_changes_file_name = segment_changes_file_name
self._split_changes_file_name = split_changes_file_name
self._split_fetcher = self._build_split_fetcher()
self._treatment_log = TreatmentLog()
self._metrics = Metrics()

def _build_split_fetcher(self):
"""
Build the json backed split fetcher
:return: The json backed split fetcher
:rtype: SelfRefreshingSplitFetcher
"""
segment_fetcher = JSONFileSegmentFetcher(self._segment_changes_file_name)
split_parser = SplitParser(segment_fetcher)
split_fetcher = JSONFileSplitFetcher(self._split_changes_file_name, split_parser)

return split_fetcher

def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""Get the treatment log implementation.
:return: The treatment log implementation.
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""Get the metrics implementation.
:return: The metrics implementation.
:rtype: Metrics
"""
return self._metrics


class LocalhostEnvironmentClient(Client):
_COMMENT_LINE_RE = compile('^#.*$')
_DEFINITION_LINE_RE = compile('^(?<![^#])(?P<feature>[\w_]+)\s+(?P<treatment>[\w_]+)$')
Expand Down Expand Up @@ -541,6 +542,62 @@ def get_metrics(self):
"""
return self._metrics

class UWSGIClient(Client):
def __init__(self, uwsgi, config=None):
"""
A Client implementation that consumes data from uwsgi cache framework. The config parameter
is a dictionary that allows you to control the behaviour of the client.

:param config: The configuration dictionary
:type config: dict
"""
labels_enabled = True
if config is not None and 'labelsEnabled' in config:
labels_enabled = config['labelsEnabled']

super(UWSGIClient, self).__init__(labels_enabled)

split_cache = UWSGISplitCache(uwsgi)
split_fetcher = CacheBasedSplitFetcher(split_cache)

impressions_cache = UWSGIImpressionsCache(uwsgi)
delegate_treatment_log = CacheBasedTreatmentLog(impressions_cache)
treatment_log = AsyncTreatmentLog(delegate_treatment_log)

metrics_cache = UWSGIMetricsCache(uwsgi)
delegate_metrics = CacheBasedMetrics(metrics_cache)
metrics = AsyncMetrics(delegate_metrics)

self._split_fetcher = split_fetcher
self._treatment_log = treatment_log
self._metrics = metrics


def get_split_fetcher(self):
"""
Get the split fetcher implementation for the client.
:return: The split fetcher
:rtype: SplitFetcher
"""
return self._split_fetcher

def get_treatment_log(self):
"""
Get the treatment log implementation for the client.
:return: The treatment log
:rtype: TreatmentLog
"""
return self._treatment_log

def get_metrics(self):
"""
Get the metrics implementation for the client.
:return: The metrics
:rtype: Metrics
"""
return self._metrics



def _init_config(api_key, **kwargs):
config = kwargs.pop('config', dict())
Expand Down Expand Up @@ -698,4 +755,58 @@ def get_redis_client(api_key, **kwargs):

return redis_client

def get_uwsgi_client(api_key, **kwargs):
"""
Builds a Split Client that that gets its information from a uWSGI cache instance. It also writes
impressions and metrics to the same instance.

In order for this work properly, you need to periodically call the spooler uwsgi_update_splits and
uwsgi_update_segments scripts. You also need to run the uwsgi_report_impressions and uwsgi_report_metrics scripts in
order to push the impressions and metrics onto the Split.io backend-

The config_file parameter is the name of a file that contains the client configuration. Here's
an example of a config file:

{
"apiKey": "some-api-key",
"sdkApiBaseUrl": "https://sdk.split.io/api",
"eventsApiBaseUrl": "https://events.split.io/api",
"featuresRefreshRate": 30,
"segmentsRefreshRate": 60,
"metricsRefreshRate": 60,
"impressionsRefreshRate": 60
}

If the api_key argument is 'localhost' a localhost environment client is built based on the
contents of a .split file in the user's home directory. The definition file has the following
syntax:

file: (comment | split_line)+
comment : '#' string*\n
split_line : feature_name ' ' treatment\n
feature_name : string
treatment : string

It is possible to change the location of the split file by using the split_definition_file_name
argument.

:param api_key: The API key provided by Split.io
:type api_key: str
:param config_file: Filename of the config file
:type config_file: str
:param sdk_api_base_url: An override for the default API base URL.
:type sdk_api_base_url: str
:param events_api_base_url: An override for the default events base URL.
:type events_api_base_url: str
:param split_definition_file_name: Name of the definition file (Optional)
:type split_definition_file_name: str
"""
api_key, config, _, _ = _init_config(api_key, **kwargs)

if api_key == 'localhost':
return LocalhostEnvironmentClient(**kwargs)

uwsgi = get_uwsgi()
uwsgi_client = UWSGIClient(uwsgi, config)

return uwsgi_client
13 changes: 9 additions & 4 deletions splitio/factories.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""A module for Split.io Factories"""
from __future__ import absolute_import, division, print_function, unicode_literals

from splitio.clients import get_client, get_redis_client
from splitio.managers import (RedisSplitManager, SelfRefreshingSplitManager, LocalhostSplitManager)
from splitio.clients import get_client, get_redis_client, get_uwsgi_client
from splitio.managers import (RedisSplitManager, SelfRefreshingSplitManager, LocalhostSplitManager, UWSGISplitManager)
from splitio.redis_support import get_redis
from splitio.uwsgi import get_uwsgi

import logging

Expand Down Expand Up @@ -41,8 +42,12 @@ def __init__(self, api_key, **kwargs):
redis = get_redis(config)
self._manager = RedisSplitManager(redis)
else:
self._client = get_client(api_key, **kwargs)
self._manager = SelfRefreshingSplitManager(self._client.get_split_fetcher())
if 'uwsgiClient' in config and config['uwsgiClient'] :
self._client = get_uwsgi_client(api_key, **kwargs)
self._manager = UWSGISplitManager(get_uwsgi())
else:
self._client = get_client(api_key, **kwargs)
self._manager = SelfRefreshingSplitManager(self._client.get_split_fetcher())



Expand Down
66 changes: 66 additions & 0 deletions splitio/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import logging

from splitio.uwsgi import UWSGISplitCache
from splitio.redis_support import RedisSplitCache
from splitio.splits import (CacheBasedSplitFetcher, SplitView)
from splitio.utils import bytes_to_string
Expand Down Expand Up @@ -104,6 +105,71 @@ def split(self, feature_name): # pragma: no cover
return split_view


class UWSGISplitManager(SplitManager):
def __init__(self, uwsgi):
"""A SplitManager implementation that uses uWSGI as its backend.
:param uwsgi: A uwsgi module
:type uwsgi: module"""
super(UWSGISplitManager, self).__init__()

split_cache = UWSGISplitCache(uwsgi)
split_fetcher = CacheBasedSplitFetcher(split_cache)

self._split_cache = split_cache
self._split_fetcher = split_fetcher

def split_names(self):
"""Get the name of fetched splits.
:return: A list of str
:rtype: list
"""
splits = self._split_cache.get_splits_keys()
split_names = []
for split_name in splits:
split_name = bytes_to_string(split_name)
split_names.append(split_name)

return split_names

def splits(self): # pragma: no cover
"""Get the fetched splits. Subclasses need to override this method.
:return: A List of SplitView.
:rtype: list
"""
splits = self._split_fetcher.fetch_all()

split_views = []

for split in splits:
treatments = []
if hasattr(split, 'conditions'):
for condition in split.conditions:
for partition in condition.partitions:
treatments.append(partition.treatment)
split_views.append(SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=split.change_number))

return split_views

def split(self, feature_name): # pragma: no cover
"""Get the splitView of feature_name. Subclasses need to override this method.
:return: The SplitView instance.
:rtype: SplitView
"""
split = self._split_fetcher.fetch(feature_name)

if split is None:
return None

treatments = []

for condition in split.conditions:
for partition in condition.partitions:
treatments.append(partition.treatment)

#Using sets on treatments to avoid duplicate entries
split_view = SplitView(name=split.name, traffic_type=split.traffic_type_name, killed=split.killed, treatments=list(set(treatments)), change_number=split.change_number)
return split_view

class SelfRefreshingSplitManager(SplitManager):

def __init__(self, split_fetcher):
Expand Down
Loading