API Stability¶
Rationale¶
In this context, the UCloud API refers to anything in the api
sub-module of every micro-service. This means RPC
calls, and their associated request/response/error models.
Maturity Levels and Deprecation Policies¶
In this issue we will be introducing three maturity levels of the UCloud API.
@UCloudApiInternal
: Reserved for internal APIs, provides no guarantees wrt. stability@UCloudApiExperimental
: Used for APIs which are maturing into a stable state, provides some guarantees wrt. stability@UCloudApiStable
: Used for mature and stable APIs, provides strong guarantees wrt. stability
The maturity levels will be implemented via a Kotlin annotation, which can be used by tooling to efficiently convey this information.
We will also be using the following deprecation markers:
@Deprecated
: Uses the one in Kotlin stdlib, indicates that a call/model has been deprecated. It will likely still work but should be replaced with an alternative.
Example¶
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.TYPEALIAS)
annotation class UCloudApiInternal(val level: Level) {
enum class Level {
BETA,
STABLE
}
}
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.TYPEALIAS)
annotation class UCloudApiExperimental(val level: Level) {
enum class Level {
ALPHA,
BETA
}
}
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.TYPEALIAS)
annotation class UCloudApiStable
@UCloudApiInternal(UcloudApiInternal.Level.BETA)
class InternalCalls : CallDescriptionContainer("internal_calls") {
val call = call<CallRequest, CallResponse, CommonErrorMessage>("call") {}
@Deprecated("This call has been deprecated in favor of 'call'", ReplaceWith("call"))
val deprecatedCall = call<Unit, Unit, CommonErrorMessage>("deprecatedCall") {}
}
@UCloudApiStable
class StableCalls : CallDescriptionContainer("stable_calls") {
// This call is considered stable (including request, response and error types)
val c1 = call<Unit, Unit, CommonErrorMessage>("c1") {}
// Individual calls of a stable container can have different classifications
@UCloudApiExperimental(UCloudApiExperimental.Level.BETA)
val c2 = call<Unit, Unit, CommonErrorMessage>("c2") {}
// Individual calls of a stable container can have different classifications
@UCloudApiInternal(UCloudApiInternal.Level.BETA)
val c3 = call<Unit, Unit, CommonErrorMessage>("c3") {}
}
@UCloudApiStable
data class CallRequest(
val foo: Int,
// We can also introduce experimental parts of a data-model
@UCloudApiExperimental(UCloudApiExperimental.Level.ALPHA)
var unsureAboutThis: String?
)
@UCloudApiStable
data class CallResponse(val foo: Int)
Internal¶
Internal APIs and models are indicated using the @UCloudApiInternal
annotation.
External clients should avoid these calls as they may change with no notice. We will not provide any guidance or migration path for this type of API.
There are several levels of internal APIs:
Beta:
Breaking changes can be made quickly (with no real deprecation cycle)
Stable:
Breaking changes should be made with a deprecation cycle lasting two versions
Note: The deprecation cycle is only there to allow for rolling upgrades
The two versions participating in the deprecation cycle could be rolled out very quickly (less than one day is perfectly okay)
Experimental¶
Experimental API are indicated using the @UCloudApiExperimental
annotation.
APIs of this type are expected to be consumed by external clients but has not yet reached a mature level. This means we will still be changing this API rather frequently as needed.
There are several levels of experimental APIs:
Alpha:
Feature complete according to initial design
Breaking changes are made when needed
No migration path or deprecation cycle
Beta:
Feature complete
User feedback is heavily encouraged during this phase
Breaking changes are generally only done if feedback suggests that this is needed
Short deprecation period if deemed necessary
External clients are encouraged to use beta-level APIs and to try out alpha-level APIs.
Stable¶
Stable APIs are indicated using the @UCloudApiStable
annotation.
A stable API is considered done and will require a deprecation cycle for any breaking change to be made. The only exception to this rule is if the change is required for security reasons. See below for a definition of a breaking change. Stable APIs must be documented.
The deprecation cycle is as follows:
Mark the API with
@Deprecated
Implement a replacement (if relevant)
Develop a migration path and announce removal date
Note: This is a a rough time-frame, will not be removed earlier than announced
Note: Migration path and removal date will be released at least 6 months before removal of old API
No earlier than announced removal date, remove the functionality
Note: Security patches do not follow the deprecation cycle. Instead, security patches are released as quickly as possible. Migration paths, if needed, will be released later.
Definition of a breaking change¶
Breaking changes can be considered in different contexts, these are as follows:
Source code compatibility (Kotlin
api
module)Binary code compatibility (Kotlin
api
module)Network-level compatibility
For the time being we will only consider network-level compatibility. In the future we might consider source code compatibility and binary code compatibility.
The contract of an API is defined by its documentation.
A UCloud API is backwards compatible at the network-level if:
A valid request payload of a previous version is still supported in the new version
Fields which were mandatory in a previous version are still mandatory and returned. In other words, the API must return a super-set of the previous response payload.
The API follows the same contract as the previous version
This means that the following is not considered a breaking change:
Adding an optional field anywhere in the response payload
Developer note: be careful with types which are used as part of request and response
Adding an optional field anywhere in the request payload
Marking an optional field anywhere in the response payload as mandatory
Any change which changes the implementation while still conforming to the contract
Clarifying the API contract
The following is considered a breaking change:
Marking a mandatory field in the response payload as optional
Marking an optional field anywhere in the request payload as mandatory
Changing the API contract