rabbah closed pull request #2730: Reduce memory consumption for invocations of
cached actions
URL: https://github.com/apache/incubator-openwhisk/pull/2730
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
diff --git
a/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
b/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
index 2080d47aea..e8cac180ac 100644
---
a/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
+++
b/common/scala/src/main/scala/whisk/core/database/ArtifactStoreProvider.scala
@@ -23,6 +23,7 @@ import spray.json.RootJsonFormat
import whisk.common.Logging
import whisk.core.WhiskConfig
import whisk.spi.Spi
+import whisk.core.entity.DocumentReader
/**
* An Spi for providing ArtifactStore implementations
@@ -32,6 +33,7 @@ trait ArtifactStoreProvider extends Spi {
name: WhiskConfig => String,
useBatching: Boolean = false)(
implicit jsonFormat: RootJsonFormat[D],
+ docReader: DocumentReader,
actorSystem: ActorSystem,
logging: Logging,
materializer: ActorMaterializer): ArtifactStore[D]
diff --git
a/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
b/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
index 0e66aee32b..298cf09dcd 100644
--- a/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CouchDbRestStore.scala
@@ -36,6 +36,7 @@ import whisk.core.entity.DocInfo
import whisk.core.entity.DocRevision
import whisk.core.entity.WhiskDocument
import whisk.http.Messages
+import whisk.core.entity.DocumentReader
/**
* Basic client to put and delete artifacts in a data store.
@@ -58,7 +59,8 @@ class CouchDbRestStore[DocumentAbstraction <:
DocumentSerializer](dbProtocol: St
implicit system: ActorSystem,
val logging: Logging,
jsonFormat: RootJsonFormat[DocumentAbstraction],
- materializer: ActorMaterializer)
+ materializer: ActorMaterializer,
+ docReader: DocumentReader)
extends ArtifactStore[DocumentAbstraction]
with DefaultJsonProtocol {
@@ -223,7 +225,13 @@ class CouchDbRestStore[DocumentAbstraction <:
DocumentSerializer](dbProtocol: St
e match {
case Right(response) =>
transid.finished(this, start, s"[GET] '$dbName' completed: found
document '$doc'")
- val asFormat = jsonFormat.read(response)
+
+ val asFormat = try {
+ docReader.read(ma, response)
+ } catch {
+ case e: Exception => jsonFormat.read(response)
+ }
+
if (asFormat.getClass != ma.runtimeClass) {
throw DocumentTypeMismatchException(
s"document type ${asFormat.getClass} did not match expected type
${ma.runtimeClass}.")
diff --git
a/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
b/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
index d2c08dd27f..08b915e4f9 100644
--- a/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
+++ b/common/scala/src/main/scala/whisk/core/database/CouchDbStoreProvider.scala
@@ -22,11 +22,13 @@ import akka.stream.ActorMaterializer
import spray.json.RootJsonFormat
import whisk.common.Logging
import whisk.core.WhiskConfig
+import whisk.core.entity.DocumentReader
object CouchDbStoreProvider extends ArtifactStoreProvider {
def makeStore[D <: DocumentSerializer](config: WhiskConfig, name:
WhiskConfig => String, useBatching: Boolean)(
implicit jsonFormat: RootJsonFormat[D],
+ docReader: DocumentReader,
actorSystem: ActorSystem,
logging: Logging,
materializer: ActorMaterializer): ArtifactStore[D] = {
diff --git
a/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
b/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
index 426b602a5a..b615708e4f 100644
---
a/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
+++
b/common/scala/src/main/scala/whisk/core/database/RemoteCacheInvalidation.scala
@@ -36,6 +36,7 @@ import whisk.core.connector.MessagingProvider
import whisk.core.entity.CacheKey
import whisk.core.entity.InstanceId
import whisk.core.entity.WhiskAction
+import whisk.core.entity.WhiskActionMetaData
import whisk.core.entity.WhiskPackage
import whisk.core.entity.WhiskRule
import whisk.core.entity.WhiskTrigger
@@ -76,12 +77,16 @@ class RemoteCacheInvalidation(config: WhiskConfig,
component: String, instance:
removeFromLocalCache)
})
+ def invalidateWhiskActionMetaData(key: CacheKey) =
+ WhiskActionMetaData.removeId(key)
+
private def removeFromLocalCache(bytes: Array[Byte]): Future[Unit] = Future {
val raw = new String(bytes, StandardCharsets.UTF_8)
CacheInvalidationMessage.parse(raw) match {
case Success(msg: CacheInvalidationMessage) => {
if (msg.instanceId != instanceId) {
+ WhiskActionMetaData.removeId(msg.key)
WhiskAction.removeId(msg.key)
WhiskPackage.removeId(msg.key)
WhiskRule.removeId(msg.key)
diff --git a/common/scala/src/main/scala/whisk/core/entity/DocumentReader.scala
b/common/scala/src/main/scala/whisk/core/entity/DocumentReader.scala
new file mode 100644
index 0000000000..ec53cf783f
--- /dev/null
+++ b/common/scala/src/main/scala/whisk/core/entity/DocumentReader.scala
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package whisk.core.entity
+
+import spray.json._
+
+protected[core] abstract class DocumentReader {
+ def read[A](ma: Manifest[A], value: JsValue): WhiskDocument
+}
diff --git a/common/scala/src/main/scala/whisk/core/entity/Exec.scala
b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
index 49ede1dd73..3ea1ec57f5 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Exec.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
@@ -45,7 +45,7 @@ import whisk.core.entity.size.SizeString
* main : name of the entry point function, when using a non-default value
(for Java, the name of the main class)" }
*/
sealed abstract class Exec extends ByteSizeable {
- override def toString = Exec.serdes.write(this).compactPrint
+ override def toString: String = Exec.serdes.write(this).compactPrint
/** A type descriptor. */
val kind: String
@@ -54,6 +54,10 @@ sealed abstract class Exec extends ByteSizeable {
val deprecated: Boolean
}
+sealed abstract class ExecMetaDataBase extends Exec {
+ override def toString: String =
ExecMetaDataBase.serdes.write(this).compactPrint
+}
+
/**
* A common super class for all action exec types that contain their executable
* code explicitly (i.e., any action other than a sequence).
@@ -89,6 +93,14 @@ sealed abstract class CodeExec[+T <% SizeConversion] extends
Exec {
override def size = code.sizeInBytes +
entryPoint.map(_.sizeInBytes).getOrElse(0.B)
}
+sealed abstract class ExecMetaData extends ExecMetaDataBase {
+
+ /** Indicates if a container image is required from the registry to execute
the action. */
+ val pull: Boolean
+
+ override def size = 0.B
+}
+
protected[core] case class CodeExecAsString(manifest: RuntimeManifest,
override val code: String,
override val entryPoint:
Option[String])
@@ -102,6 +114,12 @@ protected[core] case class CodeExecAsString(manifest:
RuntimeManifest,
override def codeAsJson = JsString(code)
}
+protected[core] case class CodeExecMetaDataAsString(manifest: RuntimeManifest)
extends ExecMetaData {
+ override val kind = manifest.kind
+ override val deprecated = manifest.deprecated.getOrElse(false)
+ override val pull = false
+}
+
protected[core] case class CodeExecAsAttachment(manifest: RuntimeManifest,
override val code:
Attachment[String],
override val entryPoint:
Option[String])
@@ -126,6 +144,12 @@ protected[core] case class CodeExecAsAttachment(manifest:
RuntimeManifest,
}
}
+protected[core] case class CodeExecMetaDataAsAttachment(manifest:
RuntimeManifest) extends ExecMetaData {
+ override val kind = manifest.kind
+ override val deprecated = manifest.deprecated.getOrElse(false)
+ override val pull = false
+}
+
/**
* @param image the image name
* @param code an optional script or zip archive (as base64 encoded) string
@@ -144,12 +168,24 @@ protected[core] case class BlackBoxExec(override val
image: ImageName,
override def size = super.size + image.publicImageName.sizeInBytes
}
+protected[core] case class BlackBoxExecMetaData(val native: Boolean) extends
ExecMetaData {
+ override val kind = ExecMetaDataBase.BLACKBOX
+ override val deprecated = false
+ override val pull = !native
+}
+
protected[core] case class SequenceExec(components:
Vector[FullyQualifiedEntityName]) extends Exec {
override val kind = Exec.SEQUENCE
override val deprecated = false
override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
}
+protected[core] case class SequenceExecMetaData(components:
Vector[FullyQualifiedEntityName]) extends ExecMetaDataBase {
+ override val kind = ExecMetaDataBase.SEQUENCE
+ override val deprecated = false
+ override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
+}
+
protected[core] object Exec extends ArgNormalizer[Exec] with
DefaultJsonProtocol {
val sizeLimit = 48 MB
@@ -187,6 +223,7 @@ protected[core] object Exec extends ArgNormalizer[Exec]
with DefaultJsonProtocol
val code = b.code.filter(_.trim.nonEmpty).map("code" -> JsString(_))
val main = b.entryPoint.map("main" -> JsString(_))
JsObject(base ++ code ++ main)
+ case _ => JsObject()
}
override def read(v: JsValue) = {
@@ -278,3 +315,119 @@ protected[core] object Exec extends ArgNormalizer[Exec]
with DefaultJsonProtocol
} else false
}
}
+
+protected[core] object ExecMetaDataBase extends
ArgNormalizer[ExecMetaDataBase] with DefaultJsonProtocol {
+
+ val sizeLimit = 48 MB
+
+ // The possible values of the JSON 'kind' field for certain runtimes:
+ // - Sequence because it is an intrinsic
+ // - Black Box because it is a type marker
+ protected[core] val SEQUENCE = "sequence"
+ protected[core] val BLACKBOX = "blackbox"
+
+ private def execManifests = ExecManifest.runtimesManifest
+
+ override protected[core] implicit lazy val serdes = new
RootJsonFormat[ExecMetaDataBase] {
+ private def attFmt[T: JsonFormat] = Attachments.serdes[T]
+ private lazy val runtimes: Set[String] =
execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)
+
+ override def write(e: ExecMetaDataBase) = e match {
+ case c: CodeExecMetaDataAsString =>
+ val base = Map("kind" -> JsString(c.kind))
+ JsObject(base)
+
+ case a: CodeExecMetaDataAsAttachment =>
+ val base =
+ Map("kind" -> JsString(a.kind))
+ JsObject(base)
+
+ case s @ SequenceExecMetaData(comp) =>
+ JsObject("kind" -> JsString(s.kind), "components" ->
comp.map(_.qualifiedNameWithLeadingSlash).toJson)
+
+ case b: BlackBoxExecMetaData =>
+ val base =
+ Map("kind" -> JsString(b.kind))
+ JsObject(base)
+ }
+
+ override def read(v: JsValue) = {
+ require(v != null)
+
+ val obj = v.asJsObject
+
+ val kind = obj.fields.get("kind") match {
+ case Some(JsString(k)) => k.trim.toLowerCase
+ case _ => throw new DeserializationException("'kind'
must be a string defined in 'exec'")
+ }
+
+ lazy val optMainField: Option[String] = obj.fields.get("main") match {
+ case Some(JsString(m)) => Some(m)
+ case Some(_) =>
+ throw new DeserializationException(s"if defined, 'main' be a string
in 'exec' for '$kind' actions")
+ case None => None
+ }
+
+ kind match {
+ case ExecMetaDataBase.SEQUENCE =>
+ val comp: Vector[FullyQualifiedEntityName] =
obj.fields.get("components") match {
+ case Some(JsArray(components)) => components map
(FullyQualifiedEntityName.serdes.read(_))
+ case Some(_) => throw new
DeserializationException(s"'components' must be an array")
+ case None => throw new
DeserializationException(s"'components' must be defined for sequence kind")
+ }
+ SequenceExecMetaData(comp)
+
+ case ExecMetaDataBase.BLACKBOX =>
+ val image: ImageName = obj.fields.get("image") match {
+ case Some(JsString(i)) => ImageName.fromString(i).get // throws
deserialization exception on failure
+ case _ =>
+ throw new DeserializationException(
+ s"'image' must be a string defined in 'exec' for
'${Exec.BLACKBOX}' actions")
+ }
+ val code: Option[String] = obj.fields.get("code") match {
+ case Some(JsString(i)) => if (i.trim.nonEmpty) Some(i) else None
+ case Some(_) =>
+ throw new DeserializationException(
+ s"if defined, 'code' must a string defined in 'exec' for
'${Exec.BLACKBOX}' actions")
+ case None => None
+ }
+ val native = execManifests.blackboxImages.contains(image)
+ BlackBoxExecMetaData(native)
+
+ case _ =>
+ // map "default" virtual runtime versions to the currently blessed
actual runtime version
+ val manifest = execManifests.resolveDefaultRuntime(kind) match {
+ case Some(k) => k
+ case None => throw new DeserializationException(s"kind '$kind'
not in $runtimes")
+ }
+
+ manifest.attached
+ .map { a =>
+ val jar: Attachment[String] = {
+ // java actions once stored the attachment in "jar" instead of
"code"
+ obj.fields.get("code").orElse(obj.fields.get("jar"))
+ } map {
+ attFmt[String].read(_)
+ } getOrElse {
+ throw new DeserializationException(
+ s"'code' must be a valid base64 string in 'exec' for '$kind'
actions")
+ }
+ val main = optMainField.orElse {
+ if (manifest.requireMain.exists(identity)) {
+ throw new DeserializationException(s"'main' must be a string
defined in 'exec' for '$kind' actions")
+ } else None
+ }
+ CodeExecMetaDataAsAttachment(manifest)
+ }
+ .getOrElse {
+ val code: String = obj.fields.get("code") match {
+ case Some(JsString(c)) => c
+ case _ =>
+ throw new DeserializationException(s"'code' must be a string
defined in 'exec' for '$kind' actions")
+ }
+ CodeExecMetaDataAsString(manifest)
+ }
+ }
+ }
+ }
+}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
index c90367e570..41af1aa1da 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
@@ -100,6 +100,10 @@ abstract class WhiskActionLike(override val name:
EntityName) extends WhiskEntit
"annotations" -> annotations.toJson)
}
+abstract class WhiskActionLikeMetaData(override val name: EntityName) extends
WhiskActionLike(name) {
+ override def exec: ExecMetaDataBase
+}
+
/**
* A WhiskAction provides an abstraction of the meta-data
* for a whisk action.
@@ -161,6 +165,51 @@ case class WhiskAction(namespace: EntityPath,
}
}
+@throws[IllegalArgumentException]
+case class WhiskActionMetaData(namespace: EntityPath,
+ override val name: EntityName,
+ exec: ExecMetaDataBase,
+ parameters: Parameters = Parameters(),
+ limits: ActionLimits = ActionLimits(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters = Parameters())
+ extends WhiskActionLikeMetaData(name) {
+
+ require(exec != null, "exec undefined")
+ require(limits != null, "limits undefined")
+
+ /**
+ * Merges parameters (usually from package) with existing action parameters.
+ * Existing parameters supersede those in p.
+ */
+ def inherit(p: Parameters) = copy(parameters = p ++
parameters).revision[WhiskActionMetaData](rev)
+
+ /**
+ * Resolves sequence components if they contain default namespace.
+ */
+ protected[core] def resolve(userNamespace: EntityName): WhiskActionMetaData
= {
+ exec match {
+ case SequenceExecMetaData(components) =>
+ val newExec = SequenceExecMetaData(components map { c =>
+ FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace),
c.name)
+ })
+ copy(exec = newExec).revision[WhiskActionMetaData](rev)
+ case _ => this
+ }
+ }
+
+ def toExecutableWhiskAction = exec match {
+ case execMetaData: ExecMetaData =>
+ Some(
+ ExecutableWhiskActionMetaData(namespace, name, execMetaData,
parameters, limits, version, publish, annotations)
+ .revision[ExecutableWhiskActionMetaData](rev))
+ case _ =>
+ None
+ }
+
+}
+
/**
* Variant of WhiskAction which only includes information necessary to be
* executed by an Invoker.
@@ -206,6 +255,25 @@ case class ExecutableWhiskAction(namespace: EntityPath,
WhiskAction(namespace, name, exec, parameters, limits, version, publish,
annotations).revision[WhiskAction](rev)
}
+@throws[IllegalArgumentException]
+case class ExecutableWhiskActionMetaData(namespace: EntityPath,
+ override val name: EntityName,
+ exec: ExecMetaData,
+ parameters: Parameters = Parameters(),
+ limits: ActionLimits = ActionLimits(),
+ version: SemVer = SemVer(),
+ publish: Boolean = false,
+ annotations: Parameters =
Parameters())
+ extends WhiskActionLikeMetaData(name) {
+
+ require(exec != null, "exec undefined")
+ require(limits != null, "limits undefined")
+
+ def toWhiskAction =
+ WhiskActionMetaData(namespace, name, exec, parameters, limits, version,
publish, annotations)
+ .revision[WhiskActionMetaData](rev)
+}
+
object WhiskAction extends DocumentFactory[WhiskAction] with
WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
val execFieldName = "exec"
@@ -342,6 +410,85 @@ object WhiskAction extends DocumentFactory[WhiskAction]
with WhiskEntityQueries[
}
}
+object WhiskActionMetaData
+ extends DocumentFactory[WhiskActionMetaData]
+ with WhiskEntityQueries[WhiskActionMetaData]
+ with DefaultJsonProtocol {
+
+ val execFieldName = "exec"
+ val finalParamsAnnotationName = "final"
+
+ override val collectionName = "actions"
+
+ override implicit val serdes = jsonFormat(
+ WhiskActionMetaData.apply,
+ "namespace",
+ "name",
+ "exec",
+ "parameters",
+ "limits",
+ "version",
+ "publish",
+ "annotations")
+
+ override val cacheEnabled = true
+
+ /**
+ * Resolves an action name if it is contained in a package.
+ * Look up the package to determine if it is a binding or the actual package.
+ * If it's a binding, rewrite the fully qualified name of the action using
the actual package path name.
+ * If it's the actual package, use its name directly as the package path
name.
+ */
+ def resolveAction(db: EntityStore, fullyQualifiedActionName:
FullyQualifiedEntityName)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[FullyQualifiedEntityName] = {
+ // first check that there is a package to be resolved
+ val entityPath = fullyQualifiedActionName.path
+ if (entityPath.defaultPackage) {
+ // this is the default package, nothing to resolve
+ Future.successful(fullyQualifiedActionName)
+ } else {
+ // there is a package to be resolved
+ val pkgDocId = fullyQualifiedActionName.path.toDocId
+ val actionName = fullyQualifiedActionName.name
+ WhiskPackage.resolveBinding(db, pkgDocId) map {
+ _.fullyQualifiedName(withVersion = false).add(actionName)
+ }
+ }
+ }
+
+ /**
+ * Resolves an action name if it is contained in a package.
+ * Look up the package to determine if it is a binding or the actual package.
+ * If it's a binding, rewrite the fully qualified name of the action using
the actual package path name.
+ * If it's the actual package, use its name directly as the package path
name.
+ * While traversing the package bindings, merge the parameters.
+ */
+ def resolveActionAndMergeParameters(entityStore: EntityStore,
fullyQualifiedName: FullyQualifiedEntityName)(
+ implicit ec: ExecutionContext,
+ transid: TransactionId): Future[WhiskActionMetaData] = {
+ // first check that there is a package to be resolved
+ val entityPath = fullyQualifiedName.path
+ if (entityPath.defaultPackage) {
+ // this is the default package, nothing to resolve
+ WhiskActionMetaData.get(entityStore, fullyQualifiedName.toDocId)
+ } else {
+ // there is a package to be resolved
+ val pkgDocid = fullyQualifiedName.path.toDocId
+ val actionName = fullyQualifiedName.name
+ val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid,
mergeParameters = true)
+ wp flatMap { resolvedPkg =>
+ // fully resolved name for the action
+ val fqnAction = resolvedPkg.fullyQualifiedName(withVersion =
false).add(actionName)
+ // get the whisk action associate with it and inherit the parameters
from the package/binding
+ WhiskActionMetaData.get(entityStore, fqnAction.toDocId) map {
+ _.inherit(resolvedPkg.parameters)
+ }
+ }
+ }
+ }
+}
+
object ActionLimitsOption extends DefaultJsonProtocol {
implicit val serdes = jsonFormat3(ActionLimitsOption.apply)
}
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
b/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
index 166b4c8c53..eafb7d56b6 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskEntity.scala
@@ -106,6 +106,20 @@ object WhiskEntity {
}
}
+object WhiskDocumentReader extends DocumentReader {
+ override def read[A](ma: Manifest[A], value: JsValue) = {
+ ma.runtimeClass match {
+ case x if x == classOf[WhiskAction] =>
WhiskAction.serdes.read(value)
+ case x if x == classOf[WhiskActionMetaData] =>
WhiskActionMetaData.serdes.read(value)
+ case x if x == classOf[WhiskPackage] =>
WhiskPackage.serdes.read(value)
+ case x if x == classOf[WhiskActivation] =>
WhiskActivation.serdes.read(value)
+ case x if x == classOf[WhiskTrigger] =>
WhiskTrigger.serdes.read(value)
+ case x if x == classOf[WhiskRule] =>
WhiskRule.serdes.read(value)
+ case _ => throw
DocumentUnreadable(Messages.corruptedEntity)
+ }
+ }
+}
+
/**
* Dispatches to appropriate serdes. This object is not itself implicit so as
to
* avoid multiple implicit alternatives when working with one of the subtypes.
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
b/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
index df8d1fe3dd..838b8ee112 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskStore.scala
@@ -88,6 +88,8 @@ protected[core] trait WhiskDocument extends
DocumentSerializer with DocumentRevi
}
object WhiskAuthStore {
+ implicit val docReader = WhiskDocumentReader
+
def requiredProperties =
Map(
dbProvider -> null,
@@ -116,11 +118,16 @@ object WhiskEntityStore {
def datastore(config: WhiskConfig)(implicit system: ActorSystem, logging:
Logging, materializer: ActorMaterializer) =
SpiLoader
.get[ArtifactStoreProvider]
- .makeStore[WhiskEntity](config, _.dbWhisk)(WhiskEntityJsonFormat,
system, logging, materializer)
-
+ .makeStore[WhiskEntity](config, _.dbWhisk)(
+ WhiskEntityJsonFormat,
+ WhiskDocumentReader,
+ system,
+ logging,
+ materializer)
}
object WhiskActivationStore {
+ implicit val docReader = WhiskDocumentReader
def requiredProperties =
Map(
dbProvider -> null,
diff --git a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
index 09c3bfe74f..da60dcb6f7 100644
--- a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
+++ b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
@@ -35,6 +35,7 @@ import whisk.common.TransactionId
import whisk.core.entity.SizeError
import whisk.core.entity.ByteSize
import whisk.core.entity.Exec
+import whisk.core.entity.ExecMetaDataBase
import whisk.core.entity.ActivationId
object Messages {
@@ -55,6 +56,12 @@ object Messages {
def runtimeDeprecated(e: Exec) =
s"The '${e.kind}' runtime is no longer supported. You may read and delete
but not update or invoke this action."
+ /**
+ * Standard message for reporting deprecated runtimes.
+ */
+ def runtimeDeprecated(e: ExecMetaDataBase) =
+ s"The '${e.kind}' runtime is no longer supported. You may read and delete
but not update or invoke this action."
+
/** Standard message for resource not found. */
val resourceDoesNotExist = "The requested resource does not exist."
diff --git a/core/controller/src/main/scala/whisk/core/controller/Actions.scala
b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
index fe1ebe6161..fcdea992d8 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Actions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
@@ -222,11 +222,11 @@ trait WhiskActionsApi extends WhiskCollectionAPI with
PostActionActivation with
'result ? false,
'timeout.as[FiniteDuration] ?
WhiskActionsApi.maxWaitForBlockingActivation) { (blocking, result,
waitOverride) =>
entity(as[Option[JsObject]]) { payload =>
- getEntity(WhiskAction, entityStore, entityName.toDocId, Some {
- act: WhiskAction =>
+ getEntity(WhiskActionMetaData, entityStore, entityName.toDocId, Some {
+ act: WhiskActionMetaData =>
// resolve the action --- special case for sequences that may
contain components with '_' as default package
val action = act.resolve(user.namespace)
- onComplete(entitleReferencedEntities(user, Privilege.ACTIVATE,
Some(action.exec))) {
+ onComplete(entitleReferencedEntitiesMetaData(user,
Privilege.ACTIVATE, Some(action.exec))) {
case Success(_) =>
val actionWithMergedParams = env.map(action.inherit(_))
getOrElse action
val waitForResponse = if (blocking) Some(waitOverride) else
None
@@ -386,6 +386,16 @@ trait WhiskActionsApi extends WhiskCollectionAPI with
PostActionActivation with
}
}
+ private def entitleReferencedEntitiesMetaData(user: Identity, right:
Privilege, exec: Option[ExecMetaDataBase])(
+ implicit transid: TransactionId) = {
+ exec match {
+ case Some(seq: SequenceExecMetaData) =>
+ logging.info(this, "checking if sequence components are accessible")
+ entitlementProvider.check(user, right, referencedEntities(seq))
+ case _ => Future.successful(true)
+ }
+ }
+
/** Creates a WhiskAction from PUT content, generating default values where
necessary. */
private def make(user: Identity, entityName: FullyQualifiedEntityName,
content: WhiskActionPut)(
implicit transid: TransactionId) = {
diff --git
a/core/controller/src/main/scala/whisk/core/controller/Controller.scala
b/core/controller/src/main/scala/whisk/core/controller/Controller.scala
index bb723bc627..cc140b1a3d 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Controller.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Controller.scala
@@ -110,7 +110,10 @@ class Controller(val instance: InstanceId,
private implicit val activationStore =
WhiskActivationStore.datastore(whiskConfig)
private implicit val cacheChangeNotification = Some(new
CacheChangeNotification {
val remoteCacheInvalidaton = new RemoteCacheInvalidation(whiskConfig,
"controller", instance)
- override def apply(k: CacheKey) =
remoteCacheInvalidaton.notifyOtherInstancesAboutInvalidation(k)
+ override def apply(k: CacheKey) = {
+ remoteCacheInvalidaton.invalidateWhiskActionMetaData(k)
+ remoteCacheInvalidaton.notifyOtherInstancesAboutInvalidation(k)
+ }
})
// initialize backend services
diff --git
a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
index 0a12036726..8cfb1e935e 100644
--- a/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/WebActions.scala
@@ -449,8 +449,8 @@ trait WhiskWebActionsApi extends Directives with
ValidateRequestSize with PostAc
* This method is factored out to allow mock testing.
*/
protected def getAction(actionName: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskAction] = {
- WhiskAction.get(entityStore, actionName.toDocId)
+ implicit transid: TransactionId): Future[WhiskActionMetaData] = {
+ WhiskActionMetaData.get(entityStore, actionName.toDocId)
}
/**
@@ -549,7 +549,7 @@ trait WhiskWebActionsApi extends Directives with
ValidateRequestSize with PostAc
}
private def extractEntityAndProcessRequest(actionOwnerIdentity: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
extension: MediaExtension,
onBehalfOf: Option[Identity],
context: Context,
@@ -597,7 +597,7 @@ trait WhiskWebActionsApi extends Directives with
ValidateRequestSize with PostAc
}
private def processRequest(actionOwnerIdentity: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
responseType: MediaExtension,
onBehalfOf: Option[Identity],
context: Context,
@@ -694,7 +694,7 @@ trait WhiskWebActionsApi extends Directives with
ValidateRequestSize with PostAc
* @return future action document or NotFound rejection
*/
private def actionLookup(actionName: FullyQualifiedEntityName)(
- implicit transid: TransactionId): Future[WhiskAction] = {
+ implicit transid: TransactionId): Future[WhiskActionMetaData] = {
getAction(actionName) recoverWith {
case _: ArtifactStoreException | DeserializationException(_, _, _) =>
Future.failed(RejectRequest(NotFound))
@@ -717,8 +717,8 @@ trait WhiskWebActionsApi extends Directives with
ValidateRequestSize with PostAc
/**
* Checks if an action is exported (i.e., carries the required annotation).
*/
- private def confirmExportedAction(actionLookup: Future[WhiskAction],
authenticated: Boolean)(
- implicit transid: TransactionId): Future[WhiskAction] = {
+ private def confirmExportedAction(actionLookup: Future[WhiskActionMetaData],
authenticated: Boolean)(
+ implicit transid: TransactionId): Future[WhiskActionMetaData] = {
actionLookup flatMap { action =>
val requiresAuthenticatedUser =
action.annotations.asBool("require-whisk-auth").exists(identity)
val isExported = action.annotations.asBool("web-export").exists(identity)
diff --git
a/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
b/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
index 34b3bb2361..5548229ca8 100644
---
a/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
+++
b/core/controller/src/main/scala/whisk/core/controller/actions/PostActionActivation.scala
@@ -46,14 +46,14 @@ protected[core] trait PostActionActivation extends
PrimitiveActions with Sequenc
*/
protected[controller] def invokeAction(
user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
payload: Option[JsObject],
waitForResponse: Option[FiniteDuration],
cause: Option[ActivationId])(implicit transid: TransactionId):
Future[Either[ActivationId, WhiskActivation]] = {
action.toExecutableWhiskAction match {
// this is a topmost sequence
case None =>
- val SequenceExec(components) = action.exec
+ val SequenceExecMetaData(components) = action.exec
invokeSequence(user, action, components, payload, waitForResponse,
cause, topmost = true, 0).map(r => r._1)
// a non-deprecated ExecutableWhiskAction
case Some(executable) if !executable.exec.deprecated =>
diff --git
a/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
b/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
index 2a9fa1f613..f604e63b33 100644
---
a/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
+++
b/core/controller/src/main/scala/whisk/core/controller/actions/PrimitiveActions.scala
@@ -92,7 +92,7 @@ protected[actions] trait PrimitiveActions {
*/
protected[actions] def invokeSingleAction(
user: Identity,
- action: ExecutableWhiskAction,
+ action: ExecutableWhiskActionMetaData,
payload: Option[JsObject],
waitForResponse: Option[FiniteDuration],
cause: Option[ActivationId])(implicit transid: TransactionId):
Future[Either[ActivationId, WhiskActivation]] = {
diff --git
a/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
b/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
index 92b2b80273..8df2033ad5 100644
---
a/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
+++
b/core/controller/src/main/scala/whisk/core/controller/actions/SequenceActions.scala
@@ -63,7 +63,7 @@ protected[actions] trait SequenceActions {
/** A method that knows how to invoke a single primitive action. */
protected[actions] def invokeAction(
user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
payload: Option[JsObject],
waitForResponse: Option[FiniteDuration],
cause: Option[ActivationId])(implicit transid: TransactionId):
Future[Either[ActivationId, WhiskActivation]]
@@ -84,7 +84,7 @@ protected[actions] trait SequenceActions {
*/
protected[actions] def invokeSequence(
user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
components: Vector[FullyQualifiedEntityName],
payload: Option[JsObject],
waitForOutermostResponse: Option[FiniteDuration],
@@ -146,7 +146,7 @@ protected[actions] trait SequenceActions {
private def completeSequenceActivation(seqActivationId: ActivationId,
futureSeqResult:
Future[SequenceAccounting],
user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
topmost: Boolean,
start: Instant,
cause: Option[ActivationId])(
@@ -188,7 +188,7 @@ protected[actions] trait SequenceActions {
* Creates an activation for a sequence.
*/
private def makeSequenceActivation(user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
activationId: ActivationId,
accounting: SequenceAccounting,
topmost: Boolean,
@@ -248,7 +248,7 @@ protected[actions] trait SequenceActions {
*/
private def invokeSequenceComponents(
user: Identity,
- seqAction: WhiskAction,
+ seqAction: WhiskActionMetaData,
seqActivationId: ActivationId,
inputPayload: Option[JsObject],
components: Vector[FullyQualifiedEntityName],
@@ -264,7 +264,7 @@ protected[actions] trait SequenceActions {
// This action/parameter resolution is done in futures; the execution
starts as soon as the first component
// is resolved.
val resolvedFutureActions = resolveDefaultNamespace(components, user) map
{ c =>
- WhiskAction.resolveActionAndMergeParameters(entityStore, c)
+ WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, c)
}
// this holds the initial value of the accounting structure, including the
input boxed as an ActivationResponse
@@ -314,7 +314,7 @@ protected[actions] trait SequenceActions {
*/
private def invokeNextAction(
user: Identity,
- futureAction: Future[WhiskAction],
+ futureAction: Future[WhiskActionMetaData],
accounting: SequenceAccounting,
cause: Option[ActivationId])(implicit transid: TransactionId):
Future[SequenceAccounting] = {
futureAction.flatMap { action =>
@@ -327,7 +327,7 @@ protected[actions] trait SequenceActions {
// invoke the action by calling the right method depending on whether
it's an atomic action or a sequence
val futureWhiskActivationTuple = action.toExecutableWhiskAction match {
case None =>
- val SequenceExec(components) = action.exec
+ val SequenceExecMetaData(components) = action.exec
logging.info(this, s"sequence invoking an enclosed sequence $action")
// call invokeSequence to invoke the inner sequence; this is a
blocking activation by definition
invokeSequence(
diff --git
a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
index a2fc2136f6..9afa83aa78 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
@@ -325,6 +325,10 @@ trait ReferencedEntities {
e.components.map { c =>
Resource(c.path, Collection(Collection.ACTIONS),
Some(c.name.asString))
}.toSet
+ case e: SequenceExecMetaData =>
+ e.components.map { c =>
+ Resource(c.path, Collection(Collection.ACTIONS),
Some(c.name.asString))
+ }.toSet
case _ => Set()
}
}
diff --git
a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
index 0b5a06d385..4237ed61f2 100644
---
a/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
+++
b/core/controller/src/main/scala/whisk/core/loadBalancer/LoadBalancerService.scala
@@ -46,7 +46,7 @@ import whisk.core.connector.MessagingProvider
import whisk.core.database.NoDocumentException
import whisk.core.entity.{ActivationId, WhiskActivation}
import whisk.core.entity.EntityName
-import whisk.core.entity.ExecutableWhiskAction
+import whisk.core.entity.ExecutableWhiskActionMetaData
import whisk.core.entity.Identity
import whisk.core.entity.InstanceId
import whisk.core.entity.UUID
@@ -76,7 +76,7 @@ trait LoadBalancer {
* The future is guaranteed to complete within the declared action
time limit
* plus a grace period (see activeAckTimeoutGrace).
*/
- def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
+ def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(
implicit transid: TransactionId): Future[Future[Either[ActivationId,
WhiskActivation]]]
}
@@ -110,7 +110,7 @@ class LoadBalancerService(config: WhiskConfig, instance:
InstanceId, entityStore
override def totalActiveActivations = loadBalancerData.totalActivationCount
- override def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
+ override def publish(action: ExecutableWhiskActionMetaData, msg:
ActivationMessage)(
implicit transid: TransactionId): Future[Future[Either[ActivationId,
WhiskActivation]]] = {
chooseInvoker(msg.user, action).flatMap { invokerName =>
val entry = setupActivation(action, msg.activationId, msg.user.uuid,
invokerName, transid)
@@ -173,7 +173,7 @@ class LoadBalancerService(config: WhiskConfig, instance:
InstanceId, entityStore
/**
* Creates an activation entry and insert into various maps.
*/
- private def setupActivation(action: ExecutableWhiskAction,
+ private def setupActivation(action: ExecutableWhiskActionMetaData,
activationId: ActivationId,
namespaceId: UUID,
invokerName: InstanceId,
@@ -312,7 +312,7 @@ class LoadBalancerService(config: WhiskConfig, instance:
InstanceId, entityStore
}
/** Determine which invoker this activation should go to. Due to dynamic
conditions, it may return no invoker. */
- private def chooseInvoker(user: Identity, action: ExecutableWhiskAction):
Future[InstanceId] = {
+ private def chooseInvoker(user: Identity, action:
ExecutableWhiskActionMetaData): Future[InstanceId] = {
val hash = generateHash(user.namespace, action)
loadBalancerData.activationCountPerInvoker.flatMap { currentActivations =>
@@ -334,7 +334,7 @@ class LoadBalancerService(config: WhiskConfig, instance:
InstanceId, entityStore
}
/** Generates a hash based on the string representation of namespace and
action */
- private def generateHash(namespace: EntityName, action:
ExecutableWhiskAction): Int = {
+ private def generateHash(namespace: EntityName, action:
ExecutableWhiskActionMetaData): Int = {
(namespace.asString.hashCode() ^
action.fullyQualifiedName(false).asString.hashCode()).abs
}
}
diff --git
a/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
b/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
index 6e65a8091d..aa60a25072 100644
--- a/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
@@ -812,6 +812,19 @@ class ActionsApiTests extends ControllerTestCommon with
WhiskActionsApi {
}
}
+ it should "ensure WhiskActionMetadata is used to invoke an action" in {
+ implicit val tid = transid()
+ val action = WhiskAction(namespace, aname(), jsDefault("??"))
+ put(entityStore, action)
+ Post(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~>
check {
+ status should be(Accepted)
+ val response = responseAs[JsObject]
+ response.fields("activationId") should not be None
+ }
+ stream.toString should include(s"[WhiskActionMetaData] [GET] serving from
datastore: ${CacheKey(action)}")
+ stream.reset()
+ }
+
it should "report proper error when record is corrupted on delete" in {
implicit val tid = transid()
val entity = BadEntity(namespace, aname())
diff --git
a/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala
b/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala
index 548c612f14..d316dace29 100644
--- a/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala
@@ -448,7 +448,12 @@ class ActivationsApiTests extends ControllerTestCommon
with WhiskActivationsApi
implicit val materializer = ActorMaterializer()
val activationStore = SpiLoader
.get[ArtifactStoreProvider]
- .makeStore[WhiskEntity](whiskConfig,
_.dbActivations)(WhiskEntityJsonFormat, system, logging, materializer)
+ .makeStore[WhiskEntity](whiskConfig, _.dbActivations)(
+ WhiskEntityJsonFormat,
+ WhiskDocumentReader,
+ system,
+ logging,
+ materializer)
implicit val tid = transid()
val entity = BadEntity(namespace, EntityName(ActivationId().toString))
put(activationStore, entity)
diff --git
a/tests/src/test/scala/whisk/core/controller/test/ControllerTestCommon.scala
b/tests/src/test/scala/whisk/core/controller/test/ControllerTestCommon.scala
index 1daa2a4dbb..e7af616a91 100644
--- a/tests/src/test/scala/whisk/core/controller/test/ControllerTestCommon.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/ControllerTestCommon.scala
@@ -184,7 +184,7 @@ class DegenerateLoadBalancerService(config:
WhiskConfig)(implicit ec: ExecutionC
override def totalActiveActivations = Future.successful(0)
override def activeActivationsFor(namespace: UUID) = Future.successful(0)
- override def publish(action: ExecutableWhiskAction, msg: ActivationMessage)(
+ override def publish(action: ExecutableWhiskActionMetaData, msg:
ActivationMessage)(
implicit transid: TransactionId): Future[Future[Either[ActivationId,
WhiskActivation]]] =
Future.successful {
whiskActivationStub map {
diff --git
a/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
b/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
index 3a55d9bfdb..9f5acaded5 100644
--- a/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/WebActionsApiTests.scala
@@ -185,12 +185,12 @@ trait WebActionsApiBaseTests extends ControllerTestCommon
with BeforeAndAfterEac
override protected def getAction(actionName:
FullyQualifiedEntityName)(implicit transid: TransactionId) = {
if (!failActionLookup) {
def theAction = {
- val annotations = Parameters(WhiskAction.finalParamsAnnotationName,
JsBoolean(true))
+ val annotations =
Parameters(WhiskActionMetaData.finalParamsAnnotationName, JsBoolean(true))
- WhiskAction(
+ WhiskActionMetaData(
actionName.path,
actionName.name,
- jsDefault("??"),
+ jsDefaultMetaData("??"),
defaultActionParameters,
annotations = {
if (actionName.name.asString.startsWith("export_")) {
@@ -242,7 +242,7 @@ trait WebActionsApiBaseTests extends ControllerTestCommon
with BeforeAndAfterEac
override protected[controller] def invokeAction(
user: Identity,
- action: WhiskAction,
+ action: WhiskActionMetaData,
payload: Option[JsObject],
waitForResponse: Option[FiniteDuration],
cause: Option[ActivationId])(implicit transid: TransactionId):
Future[Either[ActivationId, WhiskActivation]] = {
diff --git a/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
b/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
index a3cbde46cc..3d0be03e20 100644
--- a/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
+++ b/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
@@ -56,6 +56,15 @@ trait ExecHelpers extends Matchers with WskActorSystem with
StreamLogging {
js6(code, main)
}
+ protected def js6MetaData(code: String, main: Option[String] = None) = {
+ CodeExecMetaDataAsString(
+ RuntimeManifest(NODEJS6, imagename(NODEJS6), default = Some(true),
deprecated = Some(false)))
+ }
+
+ protected def jsDefaultMetaData(code: String, main: Option[String] = None) =
{
+ js6MetaData(code, main)
+ }
+
protected def swift(code: String, main: Option[String] = None) = {
CodeExecAsString(RuntimeManifest(SWIFT, imagename(SWIFT), deprecated =
Some(true)), trim(code), main.map(_.trim))
}
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
With regards,
Apache Git Services