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

Conversation

jiangpengcheng
Copy link
Contributor

@jiangpengcheng jiangpengcheng commented Sep 28, 2020

Description

Add action versioning feature for actions

Design

Design document: https://cwiki.apache.org/confluence/display/OPENWHISK/Action+Versioning

Newly created/updated actions will include a version in their document id, like whisk.system/hello@0.0.2, so they will not replace old versions.

And to be compatible with current actions whose id doesn't contain version info, I added a new view action-versions in CouchDB which can use action's fullyQualifiedName to get all its versions and related ids, and it will return the default version of the action when includeDocs=true, so with one query request, system can get the default version(optional) and version-ids mappings for an action:

function (doc) {
  var isAction = function (doc) { return (doc.exec !== undefined) };
  if (isAction(doc)) try {
    var value = {
      _id: doc.namespace + "/" + doc.name + "/default",
      namespace: doc.namespace,
      name: doc.name,
      version: doc.version,
      docId: doc._id
    };
    emit([doc.namespace + "/" + doc.name], value);
  } catch (e) {}
}

There is a new annotation field publish for action, action owners can set it to false for test-only purpose to make sure users can not get/invoke non-published versions, and set it to true when action is well developed

Before update/get/invoke/delete an action, this view will be queried first to get the docId of action, and then using the docId to fetch action from database:

  • create/update:
  1. if defaultVersion is passed and is a valid SemVer, system won't create a new action but just create/update the default version entry for this action with $fullyqualifiedName(false)/default as docId. if defaultVersion is not passed, goto step2
  2. system will query action-versions view, and if version count exceed max allowed value, reject request. if not exceed, goto step3
  3. get a suitable docid to update, if default version is set before, return default version's id, else, return latest version's id, goto step4; if no version exist at all, return None, goto step5.
  4. create a new action based on old action got in step3 and user's input, with version = version.upPatch
  5. create a new action based on user's input, with version=0.0.1
  • get:
  1. if showVersions is true, return versions list of the action, else, goto step2:
    case class WhiskActionVersionList(namespace: EntityPath,
                                      name: EntityName,
                                      versionMappings: Map[SemVer, DocId],
                                      defaultVersion: Option[SemVer])
  2. get docId for the given action'name and version parameter, if version is not provided, the default or latest version(when default version is not set) will be choosed, then fetch action using this docId
  • invoke:
  1. get docId for the given action'name and version parameter, if no version is specified, the default or latest version will be choosed
  2. pass the docId to ActivationMessage
  3. invoker can get action using docId passed with ActivationMessage directly, so it doesn't need to care about how to transform version to docId
  • delete:
  1. if version is provided, delete related version, else, goto step2
  2. get version list of action using design doc action-versions, goto step3
  3. if there are multiple versions exist and deleteAll=false, reject request, else goto step4
  4. delete all versions, and return the latest version of action

With this view, the old style of docId for actions is also compatible with this PR

And for performance reason, there is a cache layer for querying this view from database

Other than actions, there is also some tiny changes to trigger and rule, since in current master, trigger, rule and action can not using same name by follow same docId style, while with this PR, actions will use a different way for their docId(name + version), so triggers and rules may use same name with some actions.

So before save trigger and rule, it will also fetch the action-versions view first, and if there are some docIds returned, which means that entity name is occupied, openwhisk should return a Conflict error with message Resource by this name exists but is not in this collection.

TODO

  • support action versioning for webactions

  • support action versioning for sequence actions and conductor actions

  • support action versioning for trigger and rule

Related issue and scope

My changes affect the following components

  • API
  • Controller
  • Message Bus (e.g., Kafka)
  • Loadbalancer
  • Invoker
  • Intrinsic actions (e.g., sequences, conductors)
  • Data stores (e.g., CouchDB)
  • Tests
  • Deployment
  • CLI
  • General tooling
  • Documentation

Types of changes

  • Bug fix (generally a non-breaking change which closes an issue).
  • Enhancement or new feature (adds new functionality).
  • Breaking change (a bug fix or enhancement which changes existing behavior).

Checklist:

  • I signed an Apache CLA.
  • I reviewed the style guides and followed the recommendations (Travis CI will check :).
  • I added tests to cover my changes.
  • My changes require further changes to the documentation.
  • I updated the documentation where necessary.

@@ -143,6 +149,8 @@ case class WhiskAction(namespace: EntityPath,
require(exec != null, "exec undefined")
require(limits != null, "limits undefined")

override def docid = DocId(fullyQualifiedName(true).asString)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions will add @version to their docIds

@@ -106,8 +110,10 @@ abstract class WhiskActionLike(override val name: EntityName) extends WhiskEntit
"annotations" -> annotations.toJson)
}

abstract class WhiskActionLikeMetaData(override val name: EntityName) extends WhiskActionLike(name) {
abstract class WhiskActionLikeMetaData(override val name: EntityName, val docId: DocId) extends WhiskActionLike(name) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActionMetaData will get docId from database instead of generating it in code for compatible reason

}

// delete cache
def deleteCache(action: FullyQualifiedEntityName)(implicit transId: TransactionId,
Copy link
Contributor Author

@jiangpengcheng jiangpengcheng Sep 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete cache when any action is created/updated/deleted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any possibility that this call fails which results in inconsistent data in the cache with what's in the db?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for late response, there were other things seized me

the failure can happen at remote cache invalidation step, since it's using kafka, this is kind of critical, I need to think more

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I override the evictionPolicy: EvictionPolicy = WriteTime, so event there is an error in kafka while delete cache, versions data will eventually consistent

@@ -392,7 +392,7 @@ trait WriteOps extends Directives {
onComplete(factory.get(datastore, docid) flatMap { entity =>
confirm(entity) flatMap {
case _ =>
factory.del(datastore, entity.docinfo) map { _ =>
factory.del(datastore, docid.asDocInfo(entity.rev)) map { _ =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to compatible with old docId style

Copy link
Member

@style95 style95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be also great to share this PR to the dev list especially in terms of motivation, design, and the current approach in this PR.

@@ -548,8 +632,12 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
wp flatMap { resolvedPkg =>
// fully resolved name for the action
val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
val action = WhiskActionVersionList.get(fqnAction, entityStore).flatMap { result =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still unsure about having two separate calls to fetch a certain version of the action.
I haven't tried this but there are some tricks to sort data like https://stackoverflow.com/a/41959344

If we can sort the data using the views we may have something like this with one list query:

actionA@0.0.3
actionA@0.0.2
actionA@0.0.1

Then we might be able to get the latest action metadata with the limit=1 parameter.

actionA@0.0.3

Also, if a DocId includes the version information, we can directly fetch a certain version even if it is old.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still important to provide a feature in a backward-compatible way, if this is designed in this way for backward compatibility, I am inclined not to let the design be changed because of the backward compatibility. Instead, we can provide other options such as a feature flag with a way to easily migrate existing data.

I believe the semantics or behavior of the feature should not be affected because of the backward compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be hard to sort since you'll have to follow semver versus just being able to do a lexicographical sort. It should be able to be done though in the view definition I think?

But yea keep in mind:
actionA@0.0.21
actionA@0.0.9

might not get sorted correctly if you don't have semver parsing in your sort

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, maybe we can try use view key like [ "whisk.system/hello", 0, 0, 1 ]? I will check it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that it cannot reuse many current codes if do this way

@@ -548,8 +632,12 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
wp flatMap { resolvedPkg =>
// fully resolved name for the action
val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
val action = WhiskActionVersionList.get(fqnAction, entityStore).flatMap { result =>
val docId = result.matchedDocId(version).getOrElse(fqnAction.toDocId)
WhiskAction.get(entityStore, docId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case no version is specified, how does it fetch the latest version?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about doing a default version rather than defaulting to latest version? That would be more work though because you have to add maintaining a default version so I'm fine with it defaulting to latest and that can be an added feature later. So long as as you just asked, old actions are backwards compatible and get picked up as latest version if there are no versions for the action.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdoyle0182 That sounds good to me.
Currently, it is too easy to update the action and it is immediately exposed.
In the production system, it is prone to do a mistake even if a proper CI is settled.

So I imagined having multiple versions with multiple routing just like blue-green or canary deployment and I believe the feature in this PR would be the baseline for it.
But at the same time, we can also let users specify and fix a certain version to be exposed, and make them change the exposed version for the safety.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly, having a client specified default version allows them to follow a safe release process. They can deploy their function, run tests on it, and then update the default version to start accepting traffic when they're ready. And then can also easily rollback by changing the default version back.

of course if all of your invoke requests can include the version you can do all of that without needing to maintain a default version, but not every client use case may have that level of control over their incoming traffic to be able to update the version on all invoke requests.

As I said though I think it's a great follow up feature since it's additional work to store. Just defaulting to a latest version is already much better for deployment management than what exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then we need to store that default version for each action and fetch it before invoke/get an action

how about adding a new field or an annotation field to action named publish which default to true if not exist, when user invoke/get an action without an version parameter, the latest and published action will be used, if version parameter is provided, then ignore this publish field, so users can test(invoke/get) with no-published versions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would provide the functionality needed I think

@bdoyle0182
Copy link
Contributor

so many exciting PR's at once :)

@@ -216,11 +224,23 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
case _ => entitlementProvider.check(user, content.exec)
}

val latestDocId = WhiskActionVersionList.get(entityName, entityStore).map { result =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be reused everywhere. Can we make this a future function we can just call to get the latest doc id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I will update

@bdoyle0182
Copy link
Contributor

Can we add creating a max version limit to todo? This is especially important if not supplying a version creates a new version on every action post, users will end up creating a lot of versions. Should be easy to have the check if we already have the action version view.

@jiangpengcheng
Copy link
Contributor Author

Can we add creating a max version limit to todo? This is especially important if not supplying a version creates a new version on every action post, users will end up creating a lot of versions. Should be easy to have the check if we already have the action version view.

I will implement it in this PR

@jiangpengcheng jiangpengcheng force-pushed the feature/implement_code_versioning branch from f02111f to e5c6af0 Compare October 9, 2020 09:08
@jiangpengcheng jiangpengcheng changed the title [WIP]Implement action versioning Implement action versioning Oct 14, 2020
make(user, entityName, request)
},
postProcess = Some { action: WhiskAction =>
// delete oldest version when created successfully
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have an operator level configuration where if you are at the max limit of versions, you either do this delete the oldest version or reject the create request and require the user to delete an old version before creating a new one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, I will update

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a deleteOld parameter to create method

WhiskAction.del(entityStore, entity.docinfo).map(_ => entity)
})
else
results.versions.values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a little scary to delete all versions if a version accidentally isn't supplied. Should there be a delete all parameter flag otherwise return an error?

Also is it possible that you have a list of versions and then from backwards compatibility still have an action with no version? Does that result in the action with no version to get left over after the delete?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

since it will fetch a version-id mappings at first, so even for action with no version, we can get the id of it and delete action by id

@bdoyle0182
Copy link
Contributor

"if there is no docId, just created a new action based on user's input and save it"

Does this imply that if an entirely new doc id / action is created without a version supplied, that it will be created without a version? Or is it created as 0.0.1 so that all new actions post update will be created with a version?

}

object WhiskActionVersionList extends MultipleReadersSingleWriterCache[WhiskActionVersionList, DocInfo] {
lazy val viewName = WhiskQueries.entitiesView(collection = "action-versions").name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should use a constant for "action-versions"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@bdoyle0182
Copy link
Contributor

LGTM after my comments. I don't know what the approval process for such a substantial feature like this should be, but I will approve once one or two more people have gone through this. I'm impressed you were able to implement a feature like this and maintain backwards compatibility without the code getting out of control. This is a feature we've wanted for years.

One thing to add about backwards compatibility, could we create a migration script that adds version @0.0.1 to the db and version view for every non-versioned action. Once upgraded, we would like to do something like that so that we don't have a mixture of the two types of data to deal with. And if such a script is provided, we can remove the backwards compatibility support in a subsequent release to keep the codebase clean and direct operators to use the script once upgraded past this commit.

@jiangpengcheng
Copy link
Contributor Author

"if there is no docId, just created a new action based on user's input and save it"

Does this imply that if an entirely new doc id / action is created without a version supplied, that it will be created without a version? Or is it created as 0.0.1 so that all new actions post update will be created with a version?

a 0.0.1 will be used

@jiangpengcheng jiangpengcheng force-pushed the feature/implement_code_versioning branch from 34938f8 to f284a6c Compare October 29, 2020 08:44
// there may be a chance that database is updated while cache is not, we need to invalidate cache and try again
if (docId.isEmpty && tryAgain) {
WhiskActionVersionList.removeId(cacheKey(action))
getMatchedDocId(action, version, datastore, false)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete cache in memory first and try again to avoid the inconformity between database and cache

@jiangpengcheng
Copy link
Contributor Author

jiangpengcheng commented Oct 29, 2020

code looks kind of ugly to make it compatible with old style, I will write a script to copy current docs to new docs

@jiangpengcheng jiangpengcheng force-pushed the feature/implement_code_versioning branch from f6d3b65 to e3c44da Compare November 13, 2020 10:08
@jiangpengcheng
Copy link
Contributor Author

I wrote a wiki in https://cwiki.apache.org/confluence/display/OPENWHISK/Action+Versioning to help understanding this PR

@ningyougang
Copy link
Contributor

ningyougang commented Apr 30, 2021

  • will provide version list query feature with cli tool in future? e.g. wsk actionVersion $action list or wsk action version $action list
  • it is better to update the document
    • Change the create action flow's withDefaultVerstion -to withVersionParameter
    • Add the publish logic to the document

@upgle
Copy link
Member

upgle commented May 6, 2021

@jiangpengcheng please resolve conflicts

@Deklin
Copy link

Deklin commented May 6, 2021

Just to add some comments to this.
It would be beneficial to be able to run an action with a @Version tag as specified

e.g. /helloworld@1.0.0

However it is also important that /helloworld and perhaps helloworld/@latest always run the latest published version. I believe other FAAS services like fission do versioning this way and it makes contractual binding from consumers much easier.

I can either point to a specific version if I am in a strictly controlled environment, or can always point to @latest to get the latest published version of the code.

--edit

It does look like the design document handles this scenario in the invoker :) Thank you

@jiangpengcheng jiangpengcheng force-pushed the feature/implement_code_versioning branch from ad99088 to 4df5178 Compare May 8, 2021 06:18
@jiangpengcheng jiangpengcheng force-pushed the feature/implement_code_versioning branch from acdde25 to 0205d2e Compare June 3, 2021 03:01
@bdoyle0182
Copy link
Contributor

bdoyle0182 commented Jun 23, 2021

Are there any open concerns with this PR? Since this is backwards compatible, I don't have any concerns with finally merging this to master once built. The only thing I can think of would be to wait until there's support for all action types, but I'm not sure if that's worth waiting for. This is a very important feature to keep up with other FaaS offerings.

@jiangpengcheng
Copy link
Contributor Author

Are there any open concerns with this PR? Since this is backwards compatible, I don't have any concerns with finally merging this to master once built. The only thing I can think of would be to wait until there's support for all action types, but I'm not sure if that's worth waiting for. This is a very important feature to keep up with other FaaS offerings.

thanks for review, I'm waiting for all new scheduler prs get merged, and add versioning support for them, after then I will send an email to mail list for a discussion

@bdoyle0182
Copy link
Contributor

Bumping this pr since all pr's for the new scheduler have been merged in. It would be great to this feature in now finally. It's a big deal to have

@jiangpengcheng
Copy link
Contributor Author

@bdoyle0182 thanks for reminding, I will handle this after Lunar New Year vacation

@bdoyle0182
Copy link
Contributor

It would be great to finally get this feature in now that the scheduler is open sourced.

One idea I had was how can we handle dev / snapshotting versions? Since right now it seems like it will only support strict semantic versioning of /d*/./d*/./d. Or if it's something we really need?

@bdoyle0182
Copy link
Contributor

resolved conflicts

@bdoyle0182
Copy link
Contributor

One Additional ToDo on top of what's listed in the description, add view mapper for actions-versions view to the MongoDb view mappers

@bdoyle0182 bdoyle0182 changed the title Implement action versioning Implement Action Versioning Feb 16, 2023
@style95 style95 added the stale old issue which needs to validate label May 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale old issue which needs to validate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants