New Redis structure for services

This commit is contained in:
Mario de Frutos 2015-11-17 18:02:21 +01:00
parent 928e33b489
commit 9e30bf2223
9 changed files with 207 additions and 108 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
*.DS_Store
.DS_Store
*.pyc

View File

@ -25,7 +25,7 @@ RETURNS cdb_geocoder_server._redis_conf_params AS $$
$$ LANGUAGE plpythonu;
-- Get the connection to redis from cache or create a new one
CREATE OR REPLACE FUNCTION cdb_geocoder_server._connect_to_redis(user_id name)
CREATE OR REPLACE FUNCTION cdb_geocoder_server._connect_to_redis(user_id text)
RETURNS boolean AS $$
if user_id in GD and 'redis_connection' in GD[user_id]:
return False

View File

@ -1,8 +1,7 @@
-- Interface of the server extension
CREATE OR REPLACE FUNCTION cdb_geocoder_server.geocode_admin0_polygon(user_id name, tx_id bigint, country_name text)
CREATE OR REPLACE FUNCTION cdb_geocoder_server.geocode_admin0_polygon(user_id name, user_config_data JSON, geocoder_config_data JSON, country_name text)
RETURNS Geometry AS $$
from cartodb_geocoder import quota_service
plpy.debug('Entering geocode_admin0_polygons')
plpy.debug('user_id = %s' % user_id)
@ -12,23 +11,14 @@ RETURNS Geometry AS $$
plpy.error('The api_key must be provided')
#--TODO: rate limiting check
#--This will create and cache a redis connection, if needed, in the GD object for the current user
redis_conn_plan = plpy.prepare("SELECT cdb_geocoder_server._connect_to_redis($1)", ["name"])
redis_conn_result = plpy.execute(redis_conn_plan, [user_id], 1)
qs = quota_service.QuotaService(user_id, tx_id, GD[user_id]['redis_connection'])
if not qs.check_user_quota():
plpy.error("Not enough quota for this user")
#--TODO: quota check
#-- Copied from the doc, see http://www.postgresql.org/docs/9.4/static/plpython-database.html
plan = plpy.prepare("SELECT cdb_geocoder_server._geocode_admin0_polygon($1) AS mypolygon", ["text"])
result = plpy.execute(plan, [country_name], 1)
if result.status() == 5 and result.nrows() == 1:
qs.increment_geocoder_use()
plpy.debug('Returning from geocode_admin0_polygons')
return result[0]["mypolygon"]
else:
plpy.error('Something wrong with the georefence operation')
rv = plpy.execute(plan, [country_name], 1)
plpy.debug('Returning from Returning from geocode_admin0_polygons')
return rv[0]["mypolygon"]
$$ LANGUAGE plpythonu;

View File

@ -7,11 +7,12 @@ class UserConfig:
USER_CONFIG_KEYS = ['is_organization', 'entity_name']
def __init__(self, user_config_json):
def __init__(self, user_config_json, db_user_id = None):
config = json.loads(user_config_json)
filtered_config = { key: config[key] for key in self.USER_CONFIG_KEYS if key in config.keys() }
self.__check_config(filtered_config)
self.__parse_config(filtered_config)
self._user_id = self.__extract_uuid(db_user_id)
def __check_config(self, filtered_config):
if len(filtered_config.keys()) != len(self.USER_CONFIG_KEYS):
@ -19,10 +20,6 @@ class UserConfig:
return True
def __parse_config(self, filtered_config):
self._is_organization = filtered_config['is_organization']
self._entity_name = filtered_config['entity_name']
@property
def is_organization(self):
return self._is_organization
@ -31,6 +28,18 @@ class UserConfig:
def entity_name(self):
return self._entity_name
@property
def user_id(self):
return self._user_id
def __parse_config(self, filtered_config):
self._is_organization = filtered_config['is_organization']
self._entity_name = filtered_config['entity_name']
def __extract_uuid(self, db_user_id):
# Format: development_cartodb_user_<UUID>
return db_user_id.split('_')[-1]
class GeocoderConfig:
GEOCODER_CONFIG_KEYS = ['street_geocoder_provider', 'google_maps_private_key',
@ -70,6 +79,10 @@ class GeocoderConfig:
self._nokia_monthly_quota = filtered_config[self.NOKIA_QUOTA_KEY]
self._nokia_soft_geocoder_limit = filtered_config[self.NOKIA_SOFT_LIMIT_KEY]
@property
def service_type(self):
return self._geocoder_type
@property
def nokia_geocoder(self):
return self._geocoder_type == self.NOKIA_GEOCODER

View File

@ -1,27 +1,31 @@
import user_service
import config_helper
from datetime import date
class QuotaService:
""" Class to manage all the quota operation for the Geocoder SQL API Extension """
def __init__(self, user_id, transaction_id, redis_connection):
self._user_service = user_service.UserService(user_id, redis_connection)
self.transaction_id = transaction_id
def __init__(self, user_config, geocoder_config, redis_connection):
self._user_config = user_config
self._geocoder_config = geocoder_config
self._user_service = user_service.UserService(self._user_config,
self._geocoder_config.service_type, redis_connection)
def check_user_quota(self):
""" Check if the current user quota surpasses the current quota """
# TODO We need to add the hard/soft limit flag for the geocoder
user_quota = self.user_service.user_quota()
today = date.today()
current_used = self.user_service.used_quota_month(today.year, today.month)
soft_geocoder_limit = self.user_service.soft_geocoder_limit()
# We don't have quota check for google geocoder
if self._geocoder_config.google_geocoder:
return True
return True if soft_geocoder_limit or (current_used + 1) < user_quota else False
user_quota = self._geocoder_config.nokia_monthly_quota
today = date.today()
service_type = self._geocoder_config.service_type
current_used = self._user_service.used_quota(service_type, today.year, today.month)
soft_geocoder_limit = self._geocoder_config.nokia_soft_limit
print "User quota: {0} --- current_used: {1} --- limit: {2}".format(user_quota, current_used, soft_geocoder_limit)
return True if soft_geocoder_limit or current_used <= user_quota else False
def increment_geocoder_use(self, amount=1):
today = date.today()
self.user_service.increment_geocoder_use(today.year, today.month, self.transaction_id)
@property
def user_service(self):
return self._user_service
self._user_service.increment_service_use(self._geocoder_config.service_type)

View File

@ -12,39 +12,53 @@ class UserService:
REDIS_CONNECTION_PORT = "redis_port"
REDIS_CONNECTION_DB = "redis_db"
def __init__(self, user_id, redis_connection):
self.user_id = user_id
def __init__(self, user_config, service_type, redis_connection):
self.user_config = user_config
self.service_type = service_type
self._redis_connection = redis_connection
def user_quota(self):
# Check for exceptions or redis timeout
user_quota = self._redis_connection.hget(self.__get_user_redis_key(), self.GEOCODING_QUOTA_KEY)
return int(user_quota) if user_quota and int(user_quota) >= 0 else 0
def soft_geocoder_limit(self):
""" Check what kind of limit the user has """
soft_limit = self._redis_connection.hget(self.__get_user_redis_key(), self.GEOCODING_SOFT_LIMIT_KEY)
return True if soft_limit == '1' else False
def used_quota_month(self, year, month):
def used_quota(self, service_type, year, month, day=None):
""" Recover the used quota for the user in the current month """
# Check for exceptions or redis timeout
current_used = 0
for _, value in self._redis_connection.hscan_iter(self.__get_month_redis_key(year,month)):
current_used += int(value)
return current_used
redis_key_data = self.__get_redis_key(service_type, year, month, day)
current_use = self._redis_connection.hget(redis_key_data['redis_name'], redis_key_data['redis_key'])
return int(current_use) if current_use else 0
def increment_geocoder_use(self, year, month, key, amount=1):
# TODO Manage exceptions or timeout
self._redis_connection.hincrby(self.__get_month_redis_key(year, month),key,amount)
def increment_service_use(self, service_type, date=date.today(), amount=1):
""" Increment the services uses in monthly and daily basis"""
self.__increment_monthly_uses(date, service_type, amount)
self.__increment_daily_uses(date, service_type, amount)
@property
def redis_connection(self):
return self._redis_connection
# Private functions
def __get_month_redis_key(self, year, month):
today = date.today()
return "geocoder:{0}:{1}{2}".format(self.user_id, year, month)
def __increment_monthly_uses(self, date, service_type, amount):
redis_key_data = self.__get_redis_key(service_type, date.year, date.month)
self._redis_connection.hincrby(redis_key_data['redis_name'],redis_key_data['redis_key'],amount)
def __get_user_redis_key(self):
return "geocoder:{0}".format(self.user_id)
def __increment_daily_uses(self, date, service_type, amount):
redis_key_data = self.__get_redis_key(service_type, date.year, date.month, date.day)
self._redis_connection.hincrby(redis_key_data['redis_name'],redis_key_data['redis_key'],amount)
def __get_redis_key(self, service_type, year, month, day=None):
redis_name = self.__parse_redis_name(service_type,day)
redis_key = self.__parse_redis_key(year,month,day)
return {'redis_name': redis_name, 'redis_key': redis_key}
def __parse_redis_name(self,service_type, day=None):
prefix = "org" if self.user_config.is_organization else "user"
dated_key = "used_quota_day" if day else "used_quota_month"
redis_name = "{0}:{1}:{2}:{3}".format(
prefix, self.user_config.entity_name, service_type, dated_key
)
if self.user_config.is_organization and day:
redis_name = "{0}:{1}".format(redis_name, self.user_config.user_id)
return redis_name
def __parse_redis_key(self,year,month,day=None):
if day:
redis_key = "{0}_{1}_{2}".format(year,month,day)
else:
redis_key = "{0}_{1}".format(year,month)
return redis_key

View File

@ -7,9 +7,10 @@ class TestConfigHelper(TestCase):
def test_should_return_list_of_user_config_if_its_ok(self):
user_config_json = '{"is_organization": false, "entity_name": "test_user"}'
user_config = config_helper.UserConfig(user_config_json)
user_config = config_helper.UserConfig(user_config_json, 'development_cartodb_user_UUID')
assert user_config.is_organization == False
assert user_config.entity_name == 'test_user'
assert user_config.user_id == 'UUID'
def test_should_return_raise_config_exception_if_not_ok(self):
user_config_json = '{"is_organization": "false"}'

View File

@ -1,34 +1,85 @@
from mockredis import MockRedis
from cartodb_geocoder import quota_service
from cartodb_geocoder import config_helper
from unittest import TestCase
from nose.tools import assert_raises
from datetime import datetime
class TestQuotaService(TestCase):
# single user
# user:<username>:<service>:used_quota_month:year_month
# user:<username>:<service>:used_quota_day:year_month_day
# organization user
# org:<orgname>:<service>:used_quota_month:year_month
# org:<orgname>:<service>:<uuid>:used_quota_day:year_month_day
def setUp(self):
self.fake_redis_connection = MockRedis()
self.fake_redis_connection.hset('geocoder:user_id','geocoding_quota', 100)
self.fake_redis_connection.hset('geocoder:user_id','soft_geocoder_limit', 0)
self.qs = quota_service.QuotaService('user_id', 'tx_id', redis_connection = self.fake_redis_connection)
def test_should_return_true_if_quota_with_no_use(self):
assert self.qs.check_user_quota() == True
def test_should_return_true_if_user_quota_with_no_use(self):
qs = self.__build_quota_service()
assert qs.check_user_quota() == True
def test_should_return_true_if_quota_is_not_completely_used(self):
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id', 10)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id_2', 10)
assert self.qs.check_user_quota() == True
def test_should_return_true_if_org_quota_with_no_use(self):
qs = self.__build_quota_service(organization=True)
assert qs.check_user_quota() == True
def test_should_return_false_if_quota_is_surpassed(self):
self.fake_redis_connection.hset('geocoder:user_id','geocoding_quota', 1)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id', 10)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id_2', 10)
assert self.qs.check_user_quota() == False
def test_should_return_true_if_user_quota_is_not_completely_used(self):
qs = self.__build_quota_service()
self.__increment_geocoder_uses('test_user', '20151111')
assert qs.check_user_quota() == True
def test_should_return_true_if_quota_is_surpassed_but_soft_limit_is_enabled(self):
self.fake_redis_connection.hset('geocoder:user_id','geocoding_quota', 1)
self.fake_redis_connection.hset('geocoder:user_id','soft_geocoder_limit', 1)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id', 10)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id_2', 10)
assert self.qs.check_user_quota() == True
def test_should_return_true_if_org_quota_is_not_completely_used(self):
qs = self.__build_quota_service(organization=True)
self.__increment_geocoder_uses('test_user', '20151111', org=True)
assert qs.check_user_quota() == True
def test_should_return_false_if_user_quota_is_surpassed(self):
qs = self.__build_quota_service(quota = 1, soft_limit=False)
self.__increment_geocoder_uses('test_user', '20151111')
assert qs.check_user_quota() == False
def test_should_return_false_if_org_quota_is_surpassed(self):
qs = self.__build_quota_service(organization=True, quota=1)
self.__increment_geocoder_uses('test_user', '20151111', org=True)
assert qs.check_user_quota() == False
def test_should_return_true_if_user_quota_is_surpassed_but_soft_limit_is_enabled(self):
qs = self.__build_quota_service(quota=1, soft_limit=True)
self.__increment_geocoder_uses('test_user', '20151111')
assert qs.check_user_quota() == True
def test_should_return_true_if_org_quota_is_surpassed_but_soft_limit_is_enabled(self):
qs = self.__build_quota_service(organization=True, quota=1, soft_limit=True)
self.__increment_geocoder_uses('test_user', '20151111', org=True)
assert qs.check_user_quota() == True
def test_should_check_user_increment_and_quota_check_correctly(self):
qs = self.__build_quota_service(quota=2, soft_limit=False)
qs.increment_geocoder_use()
assert qs.check_user_quota() == True
def test_should_check_org_increment_and_quota_check_correctly(self):
qs = self.__build_quota_service(organization=True, quota=2, soft_limit=False)
qs.increment_geocoder_use()
assert qs.check_user_quota() == True
def __build_quota_service(self, quota=100, service='nokia', organization=False, soft_limit=False):
is_organization = 'true' if organization else 'false'
has_soft_limit = 'true' if soft_limit else 'false'
user_config_json = '{{"is_organization": {0}, "entity_name": "test_user"}}'.format(is_organization)
geocoder_config_json = """{{"street_geocoder_provider": "{0}","nokia_monthly_quota": {1},
"nokia_soft_geocoder_limit": {2}}}""".format(service, quota, has_soft_limit)
user_config = config_helper.UserConfig(user_config_json, 'user_id')
geocoder_config = config_helper.GeocoderConfig(geocoder_config_json)
return quota_service.QuotaService(user_config, geocoder_config, redis_connection = self.fake_redis_connection)
def __increment_geocoder_uses(self, entity_name, date_string, service='nokia', amount=20, org=False):
prefix = 'org' if org else 'user'
date = datetime.strptime(date_string, "%Y%m%d")
redis_name = "{0}:{1}:{2}:used_quota_month".format(prefix, entity_name, service)
redis_key_month = "{0}_{1}".format(date.year, date.month)
self.fake_redis_connection.hset(redis_name, redis_key_month, amount)

View File

@ -1,37 +1,63 @@
from mockredis import MockRedis
from cartodb_geocoder import user_service
from cartodb_geocoder import config_helper
from datetime import datetime
from unittest import TestCase
from nose.tools import assert_raises
class TestUserService(TestCase):
NOKIA_GEOCODER = 'nokia'
def setUp(self):
self.fake_redis_connection = MockRedis()
self.us = user_service.UserService('user_id', redis_connection = self.fake_redis_connection)
def test_user_quota_should_be_10(self):
self.fake_redis_connection.hset('geocoder:user_id','geocoding_quota', 10)
assert self.us.user_quota() == 10
def test_should_return_0_if_negative_quota(self):
self.fake_redis_connection.hset('geocoder:user_id','geocoding_quota', -10)
assert self.us.user_quota() == 0
def test_should_return_0_if_not_user(self):
assert self.us.user_quota() == 0
def test_user_used_quota_for_a_month(self):
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id', 10)
self.fake_redis_connection.hset('geocoder:user_id:201511','tx_id_2', 10)
assert self.us.used_quota_month(2015, 11) == 20
us = self.__build_user_service()
self.__increment_month_geocoder_uses('test_user', '20151111')
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 20
def test_org_used_quota_for_a_month(self):
us = self.__build_user_service(organization=True)
self.__increment_month_geocoder_uses('test_user', '20151111', org=True)
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 20
def test_user_not_amount_in_used_quota_for_month_should_be_0(self):
assert self.us.used_quota_month(2015, 11) == 0
us = self.__build_user_service()
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 0
def test_increment_used_quota(self):
self.us.increment_geocoder_use(2015, 11, 'tx_id', 1)
assert self.us.used_quota_month(2015, 11) == 1
def test_org_not_amount_in_used_quota_for_month_should_be_0(self):
us = self.__build_user_service(organization=True)
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 0
def test_should_increment_user_used_quota(self):
us = self.__build_user_service()
date = datetime.strptime("20151111", "%Y%m%d")
us.increment_service_use(self.NOKIA_GEOCODER, date=date, amount=1)
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 1
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11, 11) == 1
def test_should_increment_org_used_quota(self):
us = self.__build_user_service(organization=True)
date = datetime.strptime("20151111", "%Y%m%d")
us.increment_service_use(self.NOKIA_GEOCODER, date=date, amount=1)
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11) == 1
assert us.used_quota(self.NOKIA_GEOCODER, 2015, 11, 11) == 1
def test_exception_if_not_redis_config(self):
assert_raises(Exception, user_service.UserService, 'user_id')
assert_raises(Exception, user_service.UserService, 'user_id')
def __build_user_service(self, organization=False, service='nokia'):
is_organization = 'true' if organization else 'false'
user_config_json = '{{"is_organization": {0}, "entity_name": "test_user"}}'.format(is_organization)
user_config = config_helper.UserConfig(user_config_json, 'user_id')
return user_service.UserService(user_config, service, redis_connection = self.fake_redis_connection)
def __increment_month_geocoder_uses(self, entity_name, date_string, service='nokia', amount=20, org=False):
parent_tag = 'org' if org else 'user'
date = datetime.strptime(date_string, "%Y%m%d")
redis_name = "{0}:{1}:{2}:used_quota_month".format(parent_tag, entity_name, service)
redis_key_month = "{0}_{1}".format(date.year, date.month)
self.fake_redis_connection.hset(redis_name, redis_key_month, amount)