CORD-1171 Cache authentication results for 10 seconds

Change-Id: I0b3cd617784aa91f6b9a485f20a1c746965819e4
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index f79701f..981610c 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -18,6 +18,7 @@
 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
 
 def translate_exceptions(function):
+    """ this decorator translates XOS exceptions to grpc status codes """
     def wrapper(*args, **kwargs):
         try:
             return function(*args, **kwargs)
@@ -36,6 +37,60 @@
             raise
     return wrapper
 
+
+bench_tStart = time.time()
+bench_ops = 0
+def benchmark(function):
+    """ this decorator will report gRPC benchmark statistics every 10 seconds """
+    def wrapper(*args, **kwargs):
+        global bench_tStart
+        global bench_ops
+        result = function(*args, **kwargs)
+        bench_ops = bench_ops+1
+        elap = time.time() - bench_tStart
+        if (elap >= 10):
+            print "performance %d" % (bench_ops/elap)
+            bench_ops=0
+            bench_tStart = time.time()
+        return result
+    return wrapper
+
+class CachedAuthenticator(object):
+    """ Django Authentication is very slow (~ 10 ops/second), so cache
+        authentication results and reuse them.
+    """
+
+    def __init__(self):
+        self.cached_creds = {}
+        self.timeout = 10          # keep cache entries around for 10s
+
+    def authenticate(self, username, password):
+        self.trim()
+
+        key = "%s:%s" % (username, password)
+        cred = self.cached_creds.get(key, None)
+        if cred:
+            user = User.objects.filter(id=cred["user_id"])
+            if user:
+               user = user[0]
+               #print "cached authenticated %s:%s as %s" % (username, password, user)
+               return user
+
+        user = django_authenticate(username=username, password=password)
+        if user:
+            #print "django authenticated %s:%s as %s" % (username, password, user)
+            self.cached_creds[key] = {"timeout": time.time() + self.timeout, "user_id": user.id}
+
+        return user
+
+    def trim(self):
+        """ Delete all cache entries that have expired """
+        for (k, v) in list(self.cached_creds.items()):
+            if time.time() > v["timeout"]:
+                del self.cached_creds[k]
+
+cached_authenticator = CachedAuthenticator()
+
 class XOSAPIHelperMixin(object):
     def __init__(self):
         import django.apps
@@ -284,10 +339,9 @@
                 if (method.lower() == "basic"):
                     auth = base64.b64decode(auth)
                     (username, password) = auth.split(":")
-                    user = django_authenticate(username=username, password=password)
+                    user = cached_authenticator.authenticate(username=username, password=password)
                     if not user:
                         raise XOSPermissionDenied("failed to authenticate %s:%s" % (username, password))
-                    print "authenticated %s:%s as %s" % (username, password, user)
                     return user
             elif (k.lower()=="x-xossession"):
                  s = SessionStore(session_key=v)
diff --git a/xos/coreapi/protos/utility.proto b/xos/coreapi/protos/utility.proto
index d899e41..88dd8f1 100644
--- a/xos/coreapi/protos/utility.proto
+++ b/xos/coreapi/protos/utility.proto
@@ -79,6 +79,13 @@
         };
   }
 
+  rpc AuthenticatedNoOp(google.protobuf.Empty) returns (google.protobuf.Empty) {
+        option (google.api.http) = {
+            post: "/xosapi/v1/utility/auth_noop"
+            body: "*"
+        };
+  }
+
   rpc SetDirtyModels(ModelFilter) returns (ModelList) {
         option (google.api.http) = {
             post: "/xosapi/v1/utility/dirty_models"
diff --git a/xos/coreapi/xos_utility_api.py b/xos/coreapi/xos_utility_api.py
index d9c3a92..bce9bf0 100644
--- a/xos/coreapi/xos_utility_api.py
+++ b/xos/coreapi/xos_utility_api.py
@@ -41,6 +41,7 @@
 class UtilityService(utility_pb2.utilityServicer, XOSAPIHelperMixin):
     def __init__(self, thread_pool):
         self.thread_pool = thread_pool
+        XOSAPIHelperMixin.__init__(self)
 
     def stop(self):
         pass
@@ -129,6 +130,11 @@
         return Empty()
 
     @translate_exceptions
+    def AuthenticatedNoOp(self, request, context):
+        self.authenticate(context, required=True)
+        return Empty()
+
+    @translate_exceptions
     def ListDirtyModels(self, request, context):
         dirty_models = utility_pb2.ModelList()
 
diff --git a/xos/xos_client/tests/nopper.py b/xos/xos_client/tests/nopper.py
new file mode 100644
index 0000000..564c84b
--- /dev/null
+++ b/xos/xos_client/tests/nopper.py
@@ -0,0 +1,34 @@
+""" nopper
+
+    Sends NoOp operations to Core API Server at maximum rate and reports
+    performance.
+"""
+
+import sys
+import time
+sys.path.append("..")
+
+from xosapi import xos_grpc_client
+
+def test_callback():
+    print "TEST: nop"
+
+    c = xos_grpc_client.coreclient
+
+    while True:
+        tStart = time.time()
+        count = 0
+        while True:
+            if type(xos_grpc_client.coreclient) == xos_grpc_client.SecureClient:
+               c.utility.AuthenticatedNoOp(xos_grpc_client.Empty())
+            else:
+               c.utility.NoOp(xos_grpc_client.Empty())
+            count = count + 1
+            elap = time.time()-tStart
+            if (elap >= 10):
+                print "nops/second = %d" % int(count/elap)
+                tStart = time.time()
+                count = 0
+
+xos_grpc_client.start_api_parseargs(test_callback)
+