-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
base: master
Are you sure you want to change the base?
Implement Action Versioning #4986
Conversation
@@ -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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { _ => |
There was a problem hiding this comment.
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
There was a problem hiding this 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 => |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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 => |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I will update
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 |
f02111f
to
e5c6af0
Compare
make(user, entityName, request) | ||
}, | ||
postProcess = Some { action: WhiskAction => | ||
// delete oldest version when created successfully |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
"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 |
} | ||
|
||
object WhiskActionVersionList extends MultipleReadersSingleWriterCache[WhiskActionVersionList, DocInfo] { | ||
lazy val viewName = WhiskQueries.entitiesView(collection = "action-versions").name |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
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 |
a |
34938f8
to
f284a6c
Compare
// 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) |
There was a problem hiding this comment.
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
code looks kind of ugly to make it compatible with old style, I will write a script to copy current docs to new docs |
f6d3b65
to
e3c44da
Compare
I wrote a wiki in https://cwiki.apache.org/confluence/display/OPENWHISK/Action+Versioning to help understanding this PR |
|
@jiangpengcheng please resolve conflicts |
Just to add some comments to this. 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 |
ad99088
to
4df5178
Compare
This reverts commit 49396bf.
1. provide a migrate script to copy old actions to new docs with new id 2. use a separate entity to save the default version for action
acdde25
to
0205d2e
Compare
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 |
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 |
@bdoyle0182 thanks for reminding, I will handle this after Lunar New Year vacation |
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? |
resolved conflicts |
fix bad conflict resolution
One Additional ToDo on top of what's listed in the description, add view mapper for actions-versions view to the MongoDb view mappers |
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, likewhisk.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 viewaction-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 whenincludeDocs=true
, so with one query request, system can get the default version(optional) and version-ids mappings for an action:There is a new annotation fieldpublish
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 developedBefore 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:
defaultVersion
is passed and is a validSemVer
, system won't create a new action but just create/update the default version entry for this action with$fullyqualifiedName(false)/default
as docId. ifdefaultVersion
is not passed, goto step2action-versions
view, and if version count exceed max allowed value, reject request. if not exceed, goto step3version = version.upPatch
version=0.0.1
showVersions
is true, return versions list of the action, else, goto step2:version
is not provided, the default or latest version(when default version is not set) will be choosed, then fetch action using this docIdversion
is specified, the default or latest version will be choosedActivationMessage
ActivationMessage
directly, so it doesn't need to care about how to transform version to docIdversion
is provided, delete related version, else, goto step2action-versions
, goto step3deleteAll=false
, reject request, else goto step4With 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
andrule
, since in current master,trigger
,rule
andaction
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
andrule
, it will also fetch theaction-versions
view first, and if there are some docIds returned, which means that entity name is occupied, openwhisk should return aConflict
error with messageResource 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
Types of changes
Checklist: