Skip to content
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

Cache empty auth results to reduce db load #4104

Merged
merged 3 commits into from Nov 15, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,18 +17,16 @@

package org.apache.openwhisk.core.database

import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.{ConcurrentMap, TimeUnit}

import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.Failure
import scala.util.Success
import com.github.benmanes.caffeine.cache.Caffeine
import org.apache.openwhisk.common.Logging
import org.apache.openwhisk.common.LoggingMarkers
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}
import org.apache.openwhisk.core.entity.CacheKey

import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success}

/**
* A cache that allows multiple readers, but only a single writer, at
* a time. It will make a best effort attempt to coalesce reads, but
Expand Down Expand Up @@ -90,12 +88,13 @@ case object AccessTime extends EvictionPolicy
case object WriteTime extends EvictionPolicy

trait MultipleReadersSingleWriterCache[W, Winfo] {
import MultipleReadersSingleWriterCache._
import MultipleReadersSingleWriterCache.State._
import MultipleReadersSingleWriterCache._

/** Subclasses: Toggle this to enable/disable caching for your entity type. */
protected val cacheEnabled = true
protected val evictionPolicy: EvictionPolicy = AccessTime
protected val fixedCacheSize = 0

private object Entry {
def apply(transid: TransactionId, state: State, value: Option[Future[W]]): Entry = {
Expand Down Expand Up @@ -445,25 +444,19 @@ trait MultipleReadersSingleWriterCache[W, Winfo] {
}

/** This is the backing store. */
private lazy val cache: ConcurrentMapBackedCache[Entry] = evictionPolicy match {
case AccessTime =>
new ConcurrentMapBackedCache(
Caffeine
.newBuilder()
.asInstanceOf[Caffeine[Any, Future[Entry]]]
.expireAfterAccess(5, TimeUnit.MINUTES)
.softValues()
.build()
.asMap())

case _ =>
new ConcurrentMapBackedCache(
Caffeine
.newBuilder()
.asInstanceOf[Caffeine[Any, Future[Entry]]]
.expireAfterWrite(5, TimeUnit.MINUTES)
.softValues()
.build()
.asMap())
private lazy val cache: ConcurrentMapBackedCache[Entry] = createCache()

private def createCache() = {
val b = Caffeine
.newBuilder()
.softValues()

evictionPolicy match {
case AccessTime => b.expireAfterAccess(5, TimeUnit.MINUTES)
case _ => b.expireAfterWrite(5, TimeUnit.MINUTES)
}

if (fixedCacheSize > 0) b.maximumSize(fixedCacheSize)
new ConcurrentMapBackedCache(b.build().asMap().asInstanceOf[ConcurrentMap[Any, Future[Entry]]])
}
}
Expand Up @@ -17,17 +17,19 @@

package org.apache.openwhisk.core.entity

import org.apache.openwhisk.common.{Logging, TransactionId}
import org.apache.openwhisk.core.database.{
MultipleReadersSingleWriterCache,
NoDocumentException,
StaleParameter,
WriteTime
}
import org.apache.openwhisk.core.entitlement.Privilege
import org.apache.openwhisk.core.entity.types.AuthStore
import spray.json._

import scala.concurrent.Future
import scala.util.Try
import spray.json._
import types.AuthStore
import org.apache.openwhisk.common.Logging
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.database.MultipleReadersSingleWriterCache
import org.apache.openwhisk.core.database.NoDocumentException
import org.apache.openwhisk.core.database.StaleParameter
import org.apache.openwhisk.core.database.WriteTime
import org.apache.openwhisk.core.entitlement.Privilege

case class UserLimits(invocationsPerMinute: Option[Int] = None,
concurrentInvocations: Option[Int] = None,
Expand All @@ -50,12 +52,15 @@ protected[core] case class Identity(subject: Subject,
rights: Set[Privilege],
limits: UserLimits = UserLimits())

object Identity extends MultipleReadersSingleWriterCache[Identity, DocInfo] with DefaultJsonProtocol {
object Identity extends MultipleReadersSingleWriterCache[Option[Identity], DocInfo] with DefaultJsonProtocol {

private val viewName = "subjects/identities"

override val cacheEnabled = true
override val evictionPolicy = WriteTime
// upper bound for the auth cache to prevent memory pollution by sending
// malicious namespace patterns
override val fixedCacheSize = 100000

implicit val serdes = jsonFormat5(Identity.apply)

Expand All @@ -75,16 +80,16 @@ object Identity extends MultipleReadersSingleWriterCache[Identity, DocInfo] with
list(datastore, List(ns), limit = 1) map { list =>
list.length match {
case 1 =>
rowToIdentity(list.head, ns)
Some(rowToIdentity(list.head, ns))
case 0 =>
logger.info(this, s"$viewName[$namespace] does not exist")
throw new NoDocumentException("namespace does not exist")
None
case _ =>
logger.error(this, s"$viewName[$namespace] is not unique")
throw new IllegalStateException("namespace is not unique")
}
}
})
}).map(_.getOrElse(throw new NoDocumentException("namespace does not exist")))
}

def get(datastore: AuthStore, authkey: BasicAuthenticationAuthKey)(
Expand All @@ -97,16 +102,16 @@ object Identity extends MultipleReadersSingleWriterCache[Identity, DocInfo] with
list(datastore, List(authkey.uuid.asString, authkey.key.asString)) map { list =>
list.length match {
case 1 =>
rowToIdentity(list.head, authkey.uuid.asString)
Some(rowToIdentity(list.head, authkey.uuid.asString))
case 0 =>
logger.info(this, s"$viewName[${authkey.uuid}] does not exist")
throw new NoDocumentException("uuid does not exist")
None
case _ =>
logger.error(this, s"$viewName[${authkey.uuid}] is not unique")
throw new IllegalStateException("uuid is not unique")
}
}
})
}).map(_.getOrElse(throw new NoDocumentException("namespace does not exist")))
}

def list(datastore: AuthStore, key: List[Any], limit: Int = 2)(
Expand Down
Expand Up @@ -82,6 +82,22 @@ trait ArtifactStoreSubjectQueryBehaviors extends ArtifactStoreBehaviorBase {
Identity.get(authStore, ak1).failed.futureValue shouldBe a[NoDocumentException]
}

it should "should throw NoDocumentException for non existing namespaces" in {
implicit val tid: TransactionId = transid()
val nonExistingNamesSpace = "nonExistingNamesSpace"
Identity.get(authStore, EntityName(nonExistingNamesSpace)).failed.futureValue shouldBe a[NoDocumentException]
}

it should "should throw NoDocumentException for non existing authKeys" in {
implicit val tid: TransactionId = transid()
val nonExistingUUID = "nonExistingUUID"
val nonExistingSecret = "nonExistingSecret"
Identity
.get(authStore, BasicAuthenticationAuthKey(UUID(nonExistingUUID), Secret()))
.failed
.futureValue shouldBe a[NoDocumentException]
}

it should "find subject having multiple namespaces" in {
implicit val tid: TransactionId = transid()
val uuid1 = UUID()
Expand Down