Skip to content

Commit

Permalink
[JBTM-3294] adding version HTTP headers version handling to coordinat…
Browse files Browse the repository at this point in the history
…or API
  • Loading branch information
ochaloup committed Feb 16, 2021
1 parent 1eb0b05 commit 99b8804
Show file tree
Hide file tree
Showing 21 changed files with 1,671 additions and 288 deletions.
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
49 changes: 49 additions & 0 deletions rts/lra/API.adoc
@@ -0,0 +1,49 @@
= 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`[`-preRelease`].
The `major` and `minor` parts are required and `-preRelease` is used
during development and is optional.

NOTE: Any final `minor.major` release is considered higher than
the same version with `-preRelease` part.

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.

== 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
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).

Changes which do not make a trouble for backward compatibility
from client perspective (ie. 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 (ie. the highest version that the API
is created for).
The unsupported version could be one that is already deprecated
and not supported anymore.
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.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 @@ -109,6 +109,11 @@ public class NarayanaLRAClient implements Closeable {
*/
public static final String LRA_COORDINATOR_URL_KEY = "lra.coordinator.url";

/**
* Version of Narayana LRA API that client is capable to work with.
*/
private static final String CLIENT_API_VERSION = LRAConstants.NARAYANA_LRA_API_VERSION_1_0;

// LRA Coordinator API
private static final String START_PATH = "/start";
private static final String LEAVE_PATH = "/%s/remove";
Expand Down Expand Up @@ -139,7 +144,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 +181,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 +213,11 @@ public List<LRAData> getAllLRAs() {
try {
client = getClient();
Response response = client.target(coordinatorUrl)
.request()
.async()
.get()
.get(QUERY_TIMEOUT, TimeUnit.SECONDS);
.request()
.header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.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,9 +321,10 @@ 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(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.async()
.post(null)
.get(START_TIMEOUT, TimeUnit.SECONDS);
.get(START_TIMEOUT, TimeUnit.SECONDS);

// validate the HTTP status code says an LRA resource was created
if (isUnexpectedResponseStatus(response, Response.Status.CREATED)) {
Expand All @@ -344,7 +351,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,6 +424,7 @@ public void leaveLRA(URI lraId, String body) throws WebApplicationException {
response = client.target(coordinatorUrl)
.path(String.format(LEAVE_PATH, LRAConstants.getLRAUid(lraId)))
.request()
.header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.async()
.put(body == null ? Entity.text("") : Entity.text(body))
.get(LEAVE_TIMEOUT, TimeUnit.SECONDS);
Expand Down Expand Up @@ -593,9 +601,10 @@ public LRAStatus getStatus(URI uri) throws WebApplicationException {
response = client.target(coordinatorUrl)
.path(String.format(STATUS_PATH, LRAConstants.getLRAUid(uri)))
.request()
.async()
.header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.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,8 +731,9 @@ private URI enlistCompensator(URI uri, Long timelimit, String linkHeader, String
.path(LRAConstants.getLRAUid(uri))
.queryParam(TIMELIMIT_PARAM_NAME, timelimit)
.request()
.header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.header("Link", linkHeader)
.async()
.async()
.put(Entity.text(compensatorData == null ? linkHeader : compensatorData))
.get(JOIN_TIMEOUT, TimeUnit.SECONDS);

Expand All @@ -746,9 +756,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 +787,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(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION)
.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

0 comments on commit 99b8804

Please sign in to comment.