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

Implement Action Versioning #4986

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1189caf
Implement action versioning
jiangpengcheng Sep 15, 2020
8409b85
Fix tests
jiangpengcheng Sep 28, 2020
7d72c53
Make SemVer sortable
jiangpengcheng Sep 29, 2020
595df67
Updates:
jiangpengcheng Oct 9, 2020
1b2088b
Allow to replace specify version
jiangpengcheng Oct 10, 2020
171b9dc
Fix bug
jiangpengcheng Oct 10, 2020
e69240a
Implement action-version view for memoryDB and cosmosDB
jiangpengcheng Oct 12, 2020
9f5c1d8
Fix view bug
jiangpengcheng Oct 13, 2020
5d041c4
Fix DocumentHandlerTests
jiangpengcheng Oct 13, 2020
a880b97
Ensure data consistency
jiangpengcheng Oct 29, 2020
d1b2c54
Add deleteAll parameter for action#remove
jiangpengcheng Oct 29, 2020
2b1c4e8
Add deleteOld parameter for action#create
jiangpengcheng Oct 29, 2020
e030b4b
Revert "Allow to replace specify version"
jiangpengcheng Nov 3, 2020
4d0e220
add default version feature
jiangpengcheng Nov 3, 2020
af0d7c2
Some updates:
jiangpengcheng Nov 11, 2020
5e4bc9f
Fix format style
jiangpengcheng Nov 11, 2020
4dc8147
Fix test
jiangpengcheng Nov 13, 2020
ef1711c
Remove some useless code
jiangpengcheng Nov 16, 2020
5278fdc
Fix bug
jiangpengcheng Nov 16, 2020
f2b348a
Fix rebase error
jiangpengcheng May 8, 2021
becdb02
Fix tests error
jiangpengcheng May 8, 2021
6fe1d7f
Fix tests
jiangpengcheng May 8, 2021
a3939ed
Use version mappings to get doc id from an version
jiangpengcheng May 10, 2021
fc564c9
Update some return errors
jiangpengcheng May 12, 2021
9fad6e3
Rebase master
jiangpengcheng May 12, 2021
0205d2e
Fix rebase errors
jiangpengcheng Jun 3, 2021
54cb3de
Merge branch 'master' into feature/implement_code_versioning
bdoyle0182 Feb 15, 2023
6a41968
Update Actions.scala
bdoyle0182 Feb 15, 2023
482c05e
fix compilation w/ new scheduler code
Feb 15, 2023
cc56908
fix versioned action read on fpcs invoker
Feb 16, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"triggers": {
"map": "function (doc) {\n var PATHSEP = \"/\";\n var isTrigger = function (doc) { return (doc.exec === undefined && doc.binding === undefined && doc.parameters !== undefined) };\n if (isTrigger(doc)) try {\n var ns = doc.namespace.split(PATHSEP);\n var root = ns[0];\n var value = {\n namespace: doc.namespace,\n name: doc.name,\n version: doc.version,\n publish: doc.publish,\n annotations: doc.annotations,\n updated: doc.updated\n };\n emit([doc.namespace, doc.updated], value);\n if (root !== doc.namespace) {\n emit([root, doc.updated], value);\n }\n } catch (e) {}\n}",
"reduce": "_count"
},
"action-versions": {
"map": "function (doc) {\n var isAction = function (doc) { return (doc.exec !== undefined) };\n if (isAction(doc)) try {\n var value = {\n _id: doc.namespace + \"/\" + doc.name + \"/default\",\n namespace: doc.namespace,\n name: doc.name,\n docId: doc._id,\n version: doc.version\n };\n emit([doc.namespace + \"/\" + doc.name], value);\n } catch (e) {}\n}"
}
}
}
}
1 change: 1 addition & 0 deletions common/scala/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ kamon {

whisk {
shared-packages-execute-only = false
action-maximum-versions = 10
metrics {
# Enable/disable Prometheus support. If enabled then metrics would be exposed at `/metrics` endpoint
# If Prometheus is enabled then please review `kamon.metric.tick-interval` (set to 1 sec by default above).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ object WarmUp {
revision = DocRevision.empty,
user = warmUpActionIdentity,
activationId = new ActivationIdGenerator {}.make(),
DocId(warmUpAction.asString),
rootControllerIndex = controller,
blocking = false,
content = None,
Expand All @@ -54,6 +55,7 @@ object WarmUp {
val metadata = WhiskActionMetaData(
warmUpAction.path,
warmUpAction.name,
DocId(warmUpAction.asString),
CodeExecMetaDataAsString(manifest, false, entryPoint = None))
ContainerCreationMessage(
TransactionId.warmUp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ object ConfigKeys {

val whiskConfig = "whisk.config"
val sharedPackageExecuteOnly = s"whisk.shared-packages-execute-only"
val actionVersionLimit = "whisk.action-maximum-versions"
val swaggerUi = "whisk.swagger-ui"

/* DEPRECATED: disableStoreResult is deprecated for storeBlockingResultLevel */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ case class ActivationMessage(override val transid: TransactionId,
revision: DocRevision,
user: Identity,
activationId: ActivationId,
actionId: DocId,
rootControllerIndex: ControllerInstanceId,
blocking: Boolean,
content: Option[JsValue],
Expand Down Expand Up @@ -193,7 +194,7 @@ object ActivationMessage extends DefaultJsonProtocol {
def parse(msg: String) = Try(serdes.read(msg.parseJson))

private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
implicit val serdes = jsonFormat12(ActivationMessage.apply)
implicit val serdes = jsonFormat13(ActivationMessage.apply)
}

object CombinedCompletionAndResultMessage extends DefaultJsonProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,17 @@ abstract class SimpleHandler extends DocumentHandler {
provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]] = {
//Query result from CouchDB have below object structure with actual result in `value` key
//So transform the result to confirm to that structure
val viewResult = JsObject(
"id" -> js.fields("_id"),
"key" -> createKey(ddoc, view, startKey, js),
"value" -> computeView(ddoc, view, js))
val value = computeView(ddoc, view, js)
val viewResult = JsObject("id" -> js.fields("_id"), "key" -> createKey(ddoc, view, startKey, js), "value" -> value)

val result = if (includeDocs) JsObject(viewResult.fields + ("doc" -> js)) else viewResult
Future.successful(Seq(result))
if (includeDocs) value.fields.get("_id") match {
case Some(JsString(id)) if id != js.fields("_id") =>
provider.get(DocId(id)).map { doc =>
Seq(JsObject(viewResult.fields + ("doc" -> doc.getOrElse(js))))
}
case _ =>
Future.successful(Seq(JsObject(viewResult.fields + ("doc" -> js))))
} else Future.successful(Seq(viewResult))
}

/**
Expand All @@ -115,6 +119,28 @@ abstract class SimpleHandler extends DocumentHandler {
* Key is an array which matches the view query key
*/
protected def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray

/**
* Finds and transforms annotation with matching key.
*
* @param js js object having annotations array
* @param key annotation key
* @param vtr transformer function to map annotation value
* @param default default value to use if no matching annotation found
* @return annotation value matching given key
*/
protected[database] def annotationValue[T](js: JsObject, key: String, vtr: JsValue => T, default: T): T = {
js.fields.get("annotations") match {
case Some(JsArray(e)) =>
e.view
.map(_.asJsObject.getFields("key", "value"))
.collectFirst {
case Seq(JsString(`key`), v: JsValue) => vtr(v) //match annotation with given key
}
.getOrElse(default)
case _ => default
}
}
}

object ActivationHandler extends SimpleHandler {
Expand Down Expand Up @@ -177,42 +203,23 @@ object ActivationHandler extends SimpleHandler {
}, name)
}

/**
* Finds and transforms annotation with matching key.
*
* @param js js object having annotations array
* @param key annotation key
* @param vtr transformer function to map annotation value
* @param default default value to use if no matching annotation found
* @return annotation value matching given key
*/
protected[database] def annotationValue[T](js: JsObject, key: String, vtr: JsValue => T, default: T): T = {
js.fields.get("annotations") match {
case Some(JsArray(e)) =>
e.view
.map(_.asJsObject.getFields("key", "value"))
.collectFirst {
case Seq(JsString(`key`), v: JsValue) => vtr(v) //match annotation with given key
}
.getOrElse(default)
case _ => default
}
}

private def dropNull(fields: JsField*) = JsObject(fields.filter(_._2 != JsNull): _*)
}

object WhisksHandler extends SimpleHandler {
val ROOT_NS = "rootns"
val FULL_NAME = "fullname"
private val commonFields = Set("namespace", "name", "version", "publish", "annotations", "updated")
private val actionFields = commonFields ++ Set("limits", "exec.binary")
private val actionVersionFields = commonFields ++ Set("_id", "docId")
private val packageFields = commonFields ++ Set("binding")
private val packagePublicFields = commonFields
private val ruleFields = commonFields
private val triggerFields = commonFields

protected val supportedTables = Set(
"whisks.v2.1.0/actions",
"whisks.v2.1.0/action-versions",
"whisks.v2.1.0/packages",
"whisks.v2.1.0/packages-public",
"whisks.v2.1.0/rules",
Expand All @@ -223,13 +230,20 @@ object WhisksHandler extends SimpleHandler {
case Some(JsString(namespace)) =>
val ns = namespace.split(PATHSEP)
val rootNS = if (ns.length > 1) ns(0) else namespace
JsObject((ROOT_NS, JsString(rootNS)))
js.fields.get("name") match {
case Some(JsString(name)) =>
val fullName = s"$namespace$PATHSEP$name"
JsObject((ROOT_NS, JsString(rootNS)), (FULL_NAME, JsString(fullName)))
case _ =>
JsObject((ROOT_NS, JsString(rootNS)))
}
case _ => JsObject.empty
}
}

override def fieldsRequiredForView(ddoc: String, view: String): Set[String] = view match {
case "actions" => actionFields
case "action-versions" => actionVersionFields
case "packages" => packageFields
case "packages-public" => packagePublicFields
case "rules" => ruleFields
Expand All @@ -239,6 +253,7 @@ object WhisksHandler extends SimpleHandler {

def computeView(ddoc: String, view: String, js: JsObject): JsObject = view match {
case "actions" => computeActionView(js)
case "action-versions" => computeActionVersionsView(js)
case "packages" => computePackageView(js)
case "packages-public" => computePublicPackageView(js)
case "rules" => computeRulesView(js)
Expand All @@ -256,6 +271,7 @@ object WhisksHandler extends SimpleHandler {

def getEntityTypeForDesignDoc(ddoc: String, view: String): String = view match {
case "actions" => "action"
case "action-versions" => "action"
case "rules" => "rule"
case "triggers" => "trigger"
case "packages" | "packages-public" => "package"
Expand Down Expand Up @@ -288,6 +304,12 @@ object WhisksHandler extends SimpleHandler {
val exec_binary = JsHelpers.getFieldPath(js, "exec", "binary")
JsObject(base + ("exec" -> JsObject("binary" -> exec_binary.getOrElse(JsFalse))))
}

private def computeActionVersionsView(js: JsObject): JsObject = {
val base = js.fields.filterKeys(actionVersionFields).toMap
val defaultId = js.fields("namespace") + "/" + js.fields("name") + "/default"
JsObject(base + ("_id" -> JsString(defaultId), "docId" -> js.fields("_id")))
}
}

object SubjectHandler extends DocumentHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ import org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}
import org.apache.openwhisk.core.connector.Message
import org.apache.openwhisk.core.connector.MessageFeed
import org.apache.openwhisk.core.connector.MessagingProvider
import org.apache.openwhisk.core.entity.CacheKey
import org.apache.openwhisk.core.entity.ControllerInstanceId
import org.apache.openwhisk.core.entity.WhiskAction
import org.apache.openwhisk.core.entity.WhiskActionMetaData
import org.apache.openwhisk.core.entity.WhiskPackage
import org.apache.openwhisk.core.entity.WhiskRule
import org.apache.openwhisk.core.entity.WhiskTrigger
import org.apache.openwhisk.core.entity.{
CacheKey,
ControllerInstanceId,
WhiskAction,
WhiskActionMetaData,
WhiskActionVersionList,
WhiskPackage,
WhiskRule,
WhiskTrigger
}
import org.apache.openwhisk.spi.SpiLoader
import pureconfig._

Expand Down Expand Up @@ -92,6 +95,7 @@ class RemoteCacheInvalidation(config: WhiskConfig, component: String, instance:
WhiskPackage.removeId(msg.key)
WhiskRule.removeId(msg.key)
WhiskTrigger.removeId(msg.key)
WhiskActionVersionList.removeId(msg.key)
}
}
case Failure(t) => logging.error(this, s"failed processing message: $raw with $t")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kamon.metric.MeasurementUnit
import org.apache.openwhisk.common.{LogMarkerToken, TransactionId, WhiskInstants}
import org.apache.openwhisk.core.database.ActivationHandler.NS_PATH
import org.apache.openwhisk.core.database.WhisksHandler.ROOT_NS
import org.apache.openwhisk.core.database.WhisksHandler.FULL_NAME
import org.apache.openwhisk.core.database.cosmosdb.CosmosDBConstants.{alias, computed, deleted}
import org.apache.openwhisk.core.database.{
ActivationHandler,
Expand Down Expand Up @@ -143,6 +144,7 @@ private[cosmosdb] abstract class SimpleMapper extends CosmosDBViewMapper {
private[cosmosdb] object WhisksViewMapper extends SimpleMapper {
private val NS = "namespace"
private val ROOT_NS_C = s"$computed.$ROOT_NS"
private val FULL_NAME_C = s"$computed.$FULL_NAME"
private val TYPE = "entityType"
private val UPDATED = "updated"
private val PUBLISH = "publish"
Expand All @@ -169,7 +171,8 @@ private[cosmosdb] object WhisksViewMapper extends SimpleMapper {
viewConditions(ddoc, view).map(q => (s"${q._1} AND", q._2)).getOrElse((NOTHING, Nil))

val params = ("@entityType", entityType) :: ("@namespace", namespace) :: vcParams
val baseCondition = s"$vc r.$TYPE = @entityType AND (r.$NS = @namespace OR r.$ROOT_NS_C = @namespace)"
val baseCondition =
s"$vc r.$TYPE = @entityType AND (r.$NS = @namespace OR r.$ROOT_NS_C = @namespace OR r.$FULL_NAME_C = @namespace)"

(startKey, endKey) match {
case (_ :: Nil, _ :: `TOP` :: Nil) =>
Expand All @@ -194,7 +197,8 @@ private[cosmosdb] object WhisksViewMapper extends SimpleMapper {
}

override protected def orderByField(ddoc: String, view: String): String = view match {
case "actions" | "rules" | "triggers" | "packages" | "packages-public" if ddoc.startsWith("whisks") =>
case "actions" | "action-versions" | "rules" | "triggers" | "packages" | "packages-public"
if ddoc.startsWith("whisks") =>
s"r.$UPDATED"
case _ => throw UnsupportedView(s"$ddoc/$view")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ private object ActivationViewMapper extends MemoryViewMapper {
private object WhisksViewMapper extends MemoryViewMapper {
private val NS = "namespace"
private val ROOT_NS = WhisksHandler.ROOT_NS
private val FULL_NAME = WhisksHandler.FULL_NAME
private val TYPE = "entityType"
private val UPDATED = "updated"
private val PUBLISH = "publish"
Expand All @@ -140,20 +141,23 @@ private object WhisksViewMapper extends MemoryViewMapper {
val matchTypeAndView = equal(d, TYPE, entityType) && matchViewConditions(ddoc, view, d)
val matchNS = equal(d, NS, startKey.head.asInstanceOf[String])
val matchRootNS = equal(c, ROOT_NS, startKey.head.asInstanceOf[String])
val matchFullName = equal(c, FULL_NAME, startKey.head.asInstanceOf[String])

//Here ddocs for actions, rules and triggers use
//namespace and namespace/packageName as first key

val filterResult = (startKey, endKey) match {
case (ns :: Nil, _ :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS) || (matchTypeAndView && matchRootNS)
(matchTypeAndView && matchNS) || (matchTypeAndView && matchRootNS) || (matchTypeAndView && matchFullName)

case (ns :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS && gte(d, UPDATED, since)) ||
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since))
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since)) ||
(matchTypeAndView && matchFullName && gte(d, UPDATED, since))
case (ns :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>
(matchTypeAndView && matchNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto)) ||
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto))
(matchTypeAndView && matchRootNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto)) ||
(matchTypeAndView && matchFullName && gte(d, UPDATED, since) && lte(d, UPDATED, upto))

case _ => throw UnsupportedQueryKeys(s"$ddoc/$view -> ($startKey, $endKey)")
}
Expand All @@ -177,7 +181,8 @@ private object WhisksViewMapper extends MemoryViewMapper {

override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] = {
view match {
case "actions" | "rules" | "triggers" | "packages" | "packages-public" if ddoc.startsWith("whisks") =>
case "actions" | "action-versions" | "rules" | "triggers" | "packages" | "packages-public"
if ddoc.startsWith("whisks") =>
numericSort(s, descending, UPDATED)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import spray.json.deserializationError
import spray.json.JsString
import spray.json.JsValue
import spray.json.RootJsonFormat

import scala.annotation.tailrec
import scala.util.Try

/**
Expand All @@ -34,7 +36,7 @@ import scala.util.Try
*
* @param (major, minor, patch) for the semantic version
*/
protected[core] class SemVer private (private val version: (Int, Int, Int)) extends AnyVal {
protected[core] class SemVer private (private val version: (Int, Int, Int)) extends AnyVal with Ordered[SemVer] {

protected[core] def major = version._1
protected[core] def minor = version._2
Expand All @@ -46,6 +48,24 @@ protected[core] class SemVer private (private val version: (Int, Int, Int)) exte

protected[entity] def toJson = JsString(toString)
override def toString = s"$major.$minor.$patch"

protected[core] def versionList = Seq(major, minor, patch)

override def compare(that: SemVer): Int = {
compareVersion(that)
}

@tailrec
private def compareVersion(that: SemVer, depth: Int = 0): Int = {
require(depth >= 0 && depth <= 2, "depth exceed the limit of 0 to 2")
val result = versionList(depth) - that.versionList(depth)
if (result != 0)
result
else if (depth == 2)
0
else
compareVersion(that, depth + 1)
}
}

protected[core] object SemVer {
Expand Down Expand Up @@ -78,7 +98,7 @@ protected[core] object SemVer {
* @return SemVer instance
* @thrown IllegalArgumentException if string is not a valid semantic version
*/
protected[entity] def apply(str: String): SemVer = {
protected[core] def apply(str: String): SemVer = {
try {
val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()
val major = if (parts.size >= 1) parts(0).toInt else 0
Expand Down
Loading