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

[JBTM-3294] adding HTTP version headers for coordinator #1783

Merged
merged 2 commits into from Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Expand Up @@ -497,6 +497,7 @@
<version.org.eclipse.microprofile.openapi>1.1.2</version.org.eclipse.microprofile.openapi>
<version.smallrye-converter-api>1.0.10</version.smallrye-converter-api>
<version.io.mashona>1.0.0.Beta1</version.io.mashona>
<version.hamcrest>2.2</version.hamcrest>

<!-- Maven plugin versions -->
<version.org.codehaus.mojo.jboss-maven-plugin>1.5.0</version.org.codehaus.mojo.jboss-maven-plugin>
Expand Down
68 changes: 68 additions & 0 deletions rts/lra/API.adoc
@@ -0,0 +1,68 @@
= Versioning of Narayna LRA REST API

The goal of this document is summarize the approach to REST API versioning
in Narayana LRA coordinator service.
This document is written for developer of Narayana LRA services.

The version format is `major.minor` and both components are required.

Client may demand behaviour based on particular API version
by providing HTTP header link:./service-base/src/main/java/io/narayana/lra/LRAConstants.java[`Narayana-LRA-API-version`] on the call.
Copy link
Contributor

Choose a reason for hiding this comment

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

You make the assumption that the value in the version header must exactly match the supported version, ie white space is significant. I guess that requirement is implicit?

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. But we can state it in the document. Or if you think the behaviour should be "less" strict.
Some text suggestion would be great in such case.


If the caller is able to accept more than one version then the caller should include the header for each one.

== Listing of API versions

.Support status of Narayana LRA API versions
[options="header"]
|===

| Version | Support status

| `1.0` | Supported

|===

== API definition as Open API document

The Narayana LRA API is documented with Open API annotations at the java
classes. The Open API definition needs to be published at the http://narayana.io
Copy link
Contributor

Choose a reason for hiding this comment

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

I will reference it in my PR for JBTM-2929 (Create LRA documentation)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't mention API versioning in my community docs update, sorry

page.

== Changes in LRA REST API

The Narayana LRA REST API is expected to support for at least two previous
`major` versions (ie. support is expected in parallel at least
of versions 1.x, 2.x and 3.x until the 4.0 is released).
Still versions other than the last three may be supported.

Changes which do not make a trouble for backward compatibility
from client perspective (i.e., addition of features or enhancing the API
with return types or similar) are considered to bump a `minor` version.
Incompatible changes needs to bump the `major` version.

== Unsupported version errors

When client demands unsupported version from the REST API endpoint
the code returns HTTP status
link:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.18[`417` EXPECTATION_FAILED`].

On returning the `417` error the API is expected to return the header
`Narayana-LRA-API-version` with the highest supported
API version.

For an unsupported version is considered any request to the REST API endpoint
which demands (via HTTP header `Narayana-LRA-API-version`) version
higher than the current API version (i.e., the highest version that the API
is created for).

Multiple versions of the API may be supported in parallel.
The current version can be discovered by:

* Either looking at <<_listing_of_api_versions, the listing above>>.
* And/or by sending a request without the version header.
The response will include the version header containing
the latest supported version

Typically the last 3 versions are supported
but older version may also be maintained
14 changes: 10 additions & 4 deletions rts/lra/client/pom.xml
Expand Up @@ -30,14 +30,20 @@
<version>${version.microprofile.lra.api}</version>
</dependency>
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-service-base</artifactId>
<version>${project.version}</version>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-service-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.json</groupId>
<artifactId>jboss-json-api_1.0_spec</artifactId>
<version>1.0.0.Final</version>
<version>${version.jboss.json-api}</version>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>${version.cdi-api}</version>
<scope>provided</scope>
</dependency>
</dependencies>

Expand Down
Expand Up @@ -60,7 +60,6 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
Expand All @@ -79,6 +78,7 @@
import static io.narayana.lra.LRAConstants.COMPLETE;
import static io.narayana.lra.LRAConstants.FORGET;
import static io.narayana.lra.LRAConstants.LEAVE;
import static io.narayana.lra.LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME;
import static io.narayana.lra.LRAConstants.PARENT_LRA_PARAM_NAME;
import static io.narayana.lra.LRAConstants.STATUS;
import static io.narayana.lra.LRAConstants.TIMELIMIT_PARAM_NAME;
Expand Down Expand Up @@ -139,7 +139,7 @@ public class NarayanaLRAClient implements Closeable {
* If not defined as default value is taken {@code http://localhost:8080/lra-coordinator}.
* The LRA recovery coordinator will be searched at the sub-path {@value LRAConstants#RECOVERY_COORDINATOR_PATH_NAME}.
*
* @throws IllegalStateException thrown when the URL taken from the system property value is not an URI format
* @throws IllegalStateException thrown when the URL taken from the system property value is not a URL format
*/
public NarayanaLRAClient() {
this(System.getProperty(NarayanaLRAClient.LRA_COORDINATOR_URL_KEY,
Expand Down Expand Up @@ -176,14 +176,14 @@ public NarayanaLRAClient(URI coordinatorUrl) {
* The LRA recovery coordinator will be searched at the sub-path {@value LRAConstants#RECOVERY_COORDINATOR_PATH_NAME}.
*
* @param coordinatorUrl url of the LRA coordinator
* @throws IllegalStateException thrown when the provided URL String is not an URI format
* @throws IllegalStateException thrown when the provided URL String is not a URL format
*/
public NarayanaLRAClient(String coordinatorUrl) {
try {
this.coordinatorUrl = new URI(coordinatorUrl);
} catch (URISyntaxException use) {
throw new IllegalStateException("Cannot convert the provided coordinator url String "
+ coordinatorUrl + " to URI format", use);
+ coordinatorUrl + " to URL format", use);
}
}

Expand All @@ -208,10 +208,11 @@ public List<LRAData> getAllLRAs() {
try {
client = getClient();
Response response = client.target(coordinatorUrl)
.request()
.async()
.get()
.get(QUERY_TIMEOUT, TimeUnit.SECONDS);
.request()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.async()
.get()
.get(QUERY_TIMEOUT, TimeUnit.SECONDS);

if (response.getStatus() != OK.getStatusCode()) {
LRALogger.logger.debugf("Error getting all LRAs from the coordinator, response status: %d", response.getStatus());
Expand Down Expand Up @@ -315,6 +316,7 @@ public URI startLRA(URI parentLRA, String clientID, Long timeout, ChronoUnit uni
.queryParam(TIMELIMIT_PARAM_NAME, Duration.of(timeout, unit).toMillis())
.queryParam(PARENT_LRA_PARAM_NAME, encodedParentLRA)
.request()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.async()
.post(null)
.get(START_TIMEOUT, TimeUnit.SECONDS);
Expand Down Expand Up @@ -344,7 +346,7 @@ public URI startLRA(URI parentLRA, String clientID, Long timeout, ChronoUnit uni
}
throwGenericLRAException(null, INTERNAL_SERVER_ERROR.getStatusCode(),
"Cannot connect to the LRA coordinator: " + coordinatorUrl + " as provided parent LRA URL '" + parentLRA +
"' is not in URI format (" + uee.getClass().getName() + ":" + uee.getCause().getMessage() + ")", uee);
"' is not in URL format (" + uee.getClass().getName() + ":" + uee.getCause().getMessage() + ")", uee);
return null;
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new WebApplicationException("start LRA client request timed out, try again later", e,
Expand Down Expand Up @@ -417,9 +419,10 @@ public void leaveLRA(URI lraId, String body) throws WebApplicationException {
response = client.target(coordinatorUrl)
.path(String.format(LEAVE_PATH, LRAConstants.getLRAUid(lraId)))
.request()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.async()
.put(body == null ? Entity.text("") : Entity.text(body))
.get(LEAVE_TIMEOUT, TimeUnit.SECONDS);
.get(LEAVE_TIMEOUT, TimeUnit.SECONDS);

if (OK.getStatusCode() != response.getStatus()) {
LRALogger.i18NLogger.error_lraLeaveUnexpectedStatus(response.getStatus(),
Expand Down Expand Up @@ -593,9 +596,10 @@ public LRAStatus getStatus(URI uri) throws WebApplicationException {
response = client.target(coordinatorUrl)
.path(String.format(STATUS_PATH, LRAConstants.getLRAUid(uri)))
.request()
.async()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.async()
.get()
.get(QUERY_TIMEOUT, TimeUnit.SECONDS);
.get(QUERY_TIMEOUT, TimeUnit.SECONDS);

if (response.getStatus() == NOT_FOUND.getStatusCode()) {
String responseEntity = response.hasEntity() ? response.readEntity(String.class) : "";
Expand Down Expand Up @@ -722,10 +726,11 @@ private URI enlistCompensator(URI uri, Long timelimit, String linkHeader, String
.path(LRAConstants.getLRAUid(uri))
.queryParam(TIMELIMIT_PARAM_NAME, timelimit)
.request()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.header("Link", linkHeader)
.async()
.async()
.put(Entity.text(compensatorData == null ? linkHeader : compensatorData))
.get(JOIN_TIMEOUT, TimeUnit.SECONDS);
.get(JOIN_TIMEOUT, TimeUnit.SECONDS);

String responseEntity = response.hasEntity() ? response.readEntity(String.class) : "";
if (response.getStatus() == Response.Status.PRECONDITION_FAILED.getStatusCode()) {
Expand All @@ -746,9 +751,8 @@ private URI enlistCompensator(URI uri, Long timelimit, String linkHeader, String
String recoveryUrl = null;
try {
recoveryUrl = response.getHeaderString(LRA_HTTP_RECOVERY_HEADER);
String url = URLDecoder.decode(recoveryUrl, StandardCharsets.UTF_8.name());
return new URI(url);
} catch (URISyntaxException | UnsupportedEncodingException e) {
return new URI(recoveryUrl);
} catch (URISyntaxException e) {
LRALogger.logger.infof(e,"join %s returned an invalid recovery URI '%s': %s", lraId, recoveryUrl, responseEntity);
throwGenericLRAException(null, Response.Status.SERVICE_UNAVAILABLE.getStatusCode(),
"join " + lraId + " returned an invalid recovery URI '" + recoveryUrl + "' : " + responseEntity, e);
Expand Down Expand Up @@ -778,11 +782,12 @@ private void endLRA(URI lra, boolean confirm) throws WebApplicationException {
String lraUid = LRAConstants.getLRAUid(lra);
try {
response = client.target(coordinatorUrl)
.path(confirm ? String.format(CLOSE_PATH, lraUid) : String.format(CANCEL_PATH, lraUid))
.request()
.async()
.put(Entity.text(""))
.get(END_TIMEOUT, TimeUnit.SECONDS);
.path(confirm ? String.format(CLOSE_PATH, lraUid) : String.format(CANCEL_PATH, lraUid))
.request()
.header(NARAYANA_LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING)
.async()
.put(Entity.text(""))
.get(END_TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new WebApplicationException("end LRA client request timed out, try again later",
Response.Status.SERVICE_UNAVAILABLE.getStatusCode());
Expand Down
11 changes: 11 additions & 0 deletions rts/lra/coordinator/pom.xml
Expand Up @@ -68,6 +68,17 @@
<version>${version.org.jboss.resteasy}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>${version.cdi-api}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
<version>${version.org.jboss.servlet.api}</version>
</dependency>
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-client</artifactId>
Expand Down