SpringBoot: Flow Polling

This commit is contained in:
djmil 2023-09-01 23:06:34 +02:00
parent 5035a16930
commit a0c91ee9ce
3 changed files with 127 additions and 28 deletions

View File

@ -3,6 +3,8 @@ package djmil.cordacheckers.cordaclient;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -12,25 +14,27 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; import djmil.cordacheckers.cordaclient.dao.flow.RequestBody;
import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
@Service @Service
public class CordaClient { public class CordaClient {
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final ObjectMapper jsonMapper;
public CordaClient(RestTemplate restTemplate) { public CordaClient(RestTemplate restTemplate, ObjectMapper jsonMapper) {
//System.out.println("Creating REST Service"); //System.out.println("Creating REST Service");
// this.restTemplate = restTemplateBuilder // this.restTemplate = restTemplateBuilder
// .basicAuthentication("admin", "admin") // .basicAuthentication("admin", "admin")
// .build(); // .build();
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
this.jsonMapper = jsonMapper;
} }
public List<VirtualNode> getVirtualNodeList() { public List<VirtualNode> getVirtualNodeList() {
@ -59,41 +63,121 @@ public class CordaClient {
* Obtain list of unconsumed (active) GameProposals * Obtain list of unconsumed (active) GameProposals
* @param holdingIdentity * @param holdingIdentity
* @return GameProposals list in JSON form * @return GameProposals list in JSON form
* @throws JsonProcessingException
*/ */
public String listGameProposals(HoldingIdentity holdingIdentity) throws JsonProcessingException { public String listGameProposals(HoldingIdentity holdingIdentity) {
// Request authorization header
HttpHeaders headers = basicAuthorizationHeader();
final RequestBody requestBody = new RequestBody(
"list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ListFlow",
new Empty()
);
final String gameProposalsJsonString = cordaFlowExecute(
holdingIdentity,
requestBody
);
RequestBody body = new RequestBody("list-2", "djmil.cordacheckers.gameproposal.ListFlow", new Empty()); return gameProposalsJsonString;
}
ObjectMapper mapper = new ObjectMapper(); private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) {
String json = mapper.writeValueAsString(body); try {
final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody);
if (json != null) final ResponseBody startedFlow = cordaFlowPost(
return json; holdingIdentity,
// new String("{\n" + // requestBodyJson
// " \"clientRequestId\": \"list-1\",\n" + // );
// " \"flowClassName\": \"djmil.cordacheckers.gameproposal.ListFlow\",\n" + //
// " \"requestBody\": {}\n" + //
// "}");
System.out.println("HiH "+holdingIdentity.shortHash()+"\nRequst JSON = "+json); final String flowExecutionResult = cordaFlowPoll(startedFlow);
// NOTE:
// At this point, real production code, probably should convert data between CordaFlow
// abstarction into ReactApp abstraction. Instead, to limit boring json shuffling, all
// family of Corda.List flows were deliberatly designed to return frontend frendly JSONs.
// At the same time, all other Corda flows, simply return plain text string with
// operation result.
return flowExecutionResult;
}
catch (Exception e) {
throw new RuntimeException("Unable to perform "+requestBody.flowClassName()
+". Reason "+e.getMessage());
}
}
// Request private ResponseBody cordaFlowPost(HoldingIdentity holdingIdentity, String requestBodyJson) {
final HttpEntity<String> request = new HttpEntity<>(json, headers); final HttpHeaders requestHeaders = basicAuthorizationHeader();
ResponseEntity<String> resp = this.restTemplate.exchange( final HttpEntity<String> request = new HttpEntity<>(requestBodyJson, requestHeaders);
"https://localhost:8888/api/v1/flow/"+holdingIdentity.shortHash(),
final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange(
"https://localhost:8888/api/v1/flow/" + holdingIdentity.shortHash(),
HttpMethod.POST, HttpMethod.POST,
request, request,
String.class); ResponseBody.class
);
return resp.getBody(); if (!responce.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("CordaClient.startCordaFlow: unexpected request status "
+responce.getStatusCode()) ;
}
final ResponseBody responseBody = requireNonNull(
responce.getBody(),
"CordaClient.startCordaFlow: empty getBody()"
);
if (!responseBody.isFlowStarted()) {
throw new RuntimeException("CordaClient.startCordaFlow: Unexpected status: "
+responseBody.flowStatus() + ": "
+responseBody.flowError());
}
return responseBody;
}
private String cordaFlowPoll(ResponseBody startedFlow) throws InterruptedException {
final HttpHeaders requestHeaders = basicAuthorizationHeader();
final HttpEntity<String> request = new HttpEntity<>(requestHeaders);
for (int retry = 0; retry < 6; retry++) {
// Give Corda cluster some time to process our request
TimeUnit.SECONDS.sleep(retry*retry +1); // 1 2 5 8 17 33 sec
final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange(
"https://localhost:8888/api/v1/flow/"
+ startedFlow.holdingIdentityShortHash()+"/"
+ startedFlow.clientRequestId(),
HttpMethod.GET,
request,
ResponseBody.class
);
if (responce.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("CordaClient.cordaFlowPoll: unexpected request status "
+responce.getStatusCode()) ;
}
final ResponseBody responseBody = requireNonNull(
responce.getBody(),
"CordaClient.cordaFlowPoll: empty getBody()"
);
if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) {
System.out.println("Completed "+responseBody.flowResult());
return responseBody.flowResult();
} else
if (responseBody.flowError() != null) {
return "Flow execution error: " +responseBody.flowError();
} else
if (!responseBody.isFlowRunning()) {
return "Unexpect ResponseBody status: " +responseBody.flowStatus();
}
}
return "CordaClient.cordaFlowPoll: retry limit";
} }
private HttpHeaders basicAuthorizationHeader() { private HttpHeaders basicAuthorizationHeader() {

View File

@ -8,4 +8,18 @@ public record ResponseBody(
String flowResult, String flowResult,
String flowError, String flowError,
String timestamp String timestamp
) { } ) {
public boolean isFlowStarted() {
return this.flowStatus.equals("START_REQUESTED");
}
public boolean isFlowRunning() {
return this.flowStatus.equals("RUNNING");
}
public boolean isFlowCompleted() {
return this.flowStatus.equals("COMPLETED");
}
}

View File

@ -32,7 +32,8 @@ public class CordaClientTest {
@Test @Test
void testListGameProposals() throws JsonProcessingException { void testListGameProposals() throws JsonProcessingException {
String resp = cordaClient.listGameProposals(holdingIdentityResolver.getByCommonName("alice")); String resp = cordaClient.listGameProposals(
holdingIdentityResolver.getByCommonName("alice"));
System.out.println("testListGameProposals "+ resp); System.out.println("testListGameProposals "+ resp);
} }