Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

public class Operations {
private final OperationsDeps client;
Expand Down Expand Up @@ -53,94 +52,4 @@ public void delete(String name) throws IOException, InterruptedException, Libsod
String method = "DELETE";
this.client.fetch(path, method, null);
}

public <T> Operation<T> wait(Operation<T> op) throws IOException, InterruptedException, LibsodiumException {
return wait(op, WaitOptions.builder().build(), System.currentTimeMillis());
}

public <T> Operation<T> wait(Operation<T> op, WaitOptions options) throws IOException, InterruptedException, LibsodiumException {
return wait(op, options, System.currentTimeMillis());
}

public <T> Operation<T> wait(Operation<T> op, WaitOptions options, long startingTime) throws IOException, InterruptedException, LibsodiumException {
int minSleep = options.getMinSleep();
int maxSleep = options.getMaxSleep();
int increaseFactor = options.getIncreaseFactor();

if (op.getMetadata() != null && op.getMetadata().getDepends() != null && !op.getMetadata().getDepends().isDone()) {
wait(op.getMetadata().getDepends(), options, startingTime);
}

if (op.isDone()) {
return op;
}

int retries = 0;

while (true) {
String opName = op.getName();
op = this.<T>get(opName).orElseThrow(() -> new IllegalArgumentException("Operation not found: " + opName));

int delay = Math.max(minSleep, Math.min(maxSleep, (int) Math.pow(2, retries) * increaseFactor));
retries++;

if (op.isDone()) {
return op;
}
Thread.sleep(delay);

if (options.getAbortSignal().getTimeout() != null) {
long currentTime = System.currentTimeMillis();
if (currentTime - startingTime > options.getAbortSignal().getTimeout()) {
options.getAbortSignal().abort("Timeout");
}
}

options.getAbortSignal().throwIfAborted();
}
}

@Builder
@Getter
@Setter
public static class WaitOptions {

@Builder.Default
private Integer minSleep = 10;
@Builder.Default
private Integer maxSleep = 10000;
@Builder.Default
private Integer increaseFactor = 50;

@Builder.Default
private AbortSignal abortSignal = new AbortSignal();
}

@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class AbortSignal {
private final AtomicBoolean aborted = new AtomicBoolean(false);
private Object reason;
private Long timeout;

public boolean isAborted() {
return aborted.get();
}

public void abort(Object reason) {
if (!isAborted()) {
this.reason = reason;
aborted.set(true);
}
}

public void throwIfAborted() throws InterruptedException {
if (isAborted()) {
throw new InterruptedException("Operation aborted: " + reason.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.cardanofoundation.signify.app;

import org.cardanofoundation.signify.app.coring.Operation;
import org.cardanofoundation.signify.app.coring.Operations;
import org.cardanofoundation.signify.app.coring.deps.OperationsDeps;
import org.cardanofoundation.signify.cesr.exceptions.LibsodiumException;
import org.cardanofoundation.signify.cesr.util.Utils;
import org.cardanofoundation.signify.e2e.utils.OperationWaiter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.*;

import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class OperationWaiterTest {

@Mock
private OperationsDeps client;

private Operations operations;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
operations = new Operations(client);
}

@Test
@DisplayName("Does not wait for operation that is already done")
void doesNotWaitForOperationThatIsAlreadyDone() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(true, true);

var result = OperationWaiter.wait(operations, operation);
verify(client, never()).fetch(anyString(), anyString(), isNull());
assertEquals(operation, result);
}

@Test
@DisplayName("Returns when operation is done after first call")
void returnsWhenOperationIsDoneAfterFirstCall() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(true, true);

HttpResponse<String> mockResponse = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse.body()).thenReturn(Utils.jsonStringify(operation));
Mockito.when(mockResponse.statusCode()).thenReturn(200);
when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse);

operation.setDone(false);
OperationWaiter.wait(operations, operation);
verify(client, times(1)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Returns when operation is done after second call")
void returnsWhenOperationIsDoneAfterSecondCall() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation1 = buildOperation(false, false);
Operation<String> operation2 = buildOperation(true, true);

HttpResponse<String> mockResponse1 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse1.body()).thenReturn(Utils.jsonStringify(operation1));
Mockito.when(mockResponse1.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse2 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse2.body()).thenReturn(Utils.jsonStringify(operation2));
Mockito.when(mockResponse2.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse1)
.thenReturn(mockResponse2);

OperationWaiter.WaitOptions options = OperationWaiter.WaitOptions.builder()
.maxSleep(10)
.build();
OperationWaiter.wait(operations, operation1, options);
verify(client, times(3)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Returns when child operation is also done")
void returnsWhenChildOperationIsAlsoDone() throws IOException, InterruptedException, LibsodiumException {
HttpResponse<String> mockResponse1 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse1.body()).thenReturn(Utils.jsonStringify(buildOperation(false, false)));
Mockito.when(mockResponse1.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse2 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse2.body()).thenReturn(Utils.jsonStringify(buildOperation(false, true)));
Mockito.when(mockResponse2.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse3 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse3.body()).thenReturn(Utils.jsonStringify(buildOperation(true, true)));
Mockito.when(mockResponse3.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse1)
.thenReturn(mockResponse2)
.thenReturn(mockResponse3);

OperationWaiter.WaitOptions options = OperationWaiter.WaitOptions.builder()
.maxSleep(10)
.build();
OperationWaiter.wait(operations, buildOperation(false, false), options);
verify(client, times(4)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Throw if aborting operation")
void throwIfAbortingOperation() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(false, false);

HttpResponse<String> mockResponse = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse.body()).thenReturn(Utils.jsonStringify(operation));
Mockito.when(mockResponse.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse);

OperationWaiter.WaitOptions options = OperationWaiter.WaitOptions.builder()
.maxSleep(10)
.abortSignal(OperationWaiter.AbortSignal.builder().timeout(5000L).build())
.build();

Exception exception = assertThrows(InterruptedException.class, () -> OperationWaiter.wait(operations, operation, options));
assertEquals("Operation aborted: Timeout", exception.getMessage());
}

Operation<String> buildOperation(boolean done, boolean dependsDone) {
Operation<String> operation = Operation.<String>builder()
.name(UUID.randomUUID().toString())
.response("response")
.done(done)
.build();

Operation<String> depends = Operation.<String>builder()
.name(UUID.randomUUID().toString())
.response("depend")
.done(dependsDone)
.build();

Operation.Metadata<String> metadata = Operation.Metadata.<String>builder()
.depends(depends)
.properties(Map.of("key", "value"))
.build();
operation.setMetadata(metadata);
return operation;
}
}
102 changes: 0 additions & 102 deletions src/test/java/org/cardanofoundation/signify/app/OperationsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,106 +104,6 @@ void canDeleteOperationByName() throws IOException, InterruptedException, Libsod
assertEquals("DELETE", methodCaptor.getValue());
}

@Test
@DisplayName("Does not wait for operation that is already done")
void doesNotWaitForOperationThatIsAlreadyDone() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(true, true);

var result = operations.wait(operation);
verify(client, never()).fetch(anyString(), anyString(), isNull(), isNull());
assertEquals(operation, result);
}

@Test
@DisplayName("Returns when operation is done after first call")
void returnsWhenOperationIsDoneAfterFirstCall() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(true, true);

HttpResponse<String> mockResponse = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse.body()).thenReturn(Utils.jsonStringify(operation));
Mockito.when(mockResponse.statusCode()).thenReturn(200);
when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse);

operation.setDone(false);
operations.wait(operation);
verify(client, times(1)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Returns when operation is done after second call")
void returnsWhenOperationIsDoneAfterSecondCall() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation1 = buildOperation(false, false);
Operation<String> operation2 = buildOperation(true, true);

HttpResponse<String> mockResponse1 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse1.body()).thenReturn(Utils.jsonStringify(operation1));
Mockito.when(mockResponse1.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse2 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse2.body()).thenReturn(Utils.jsonStringify(operation2));
Mockito.when(mockResponse2.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse1)
.thenReturn(mockResponse2);

Operations.WaitOptions options = Operations.WaitOptions.builder()
.maxSleep(10)
.build();
operations.wait(operation1, options);
verify(client, times(3)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Returns when child operation is also done")
void returnsWhenChildOperationIsAlsoDone() throws IOException, InterruptedException, LibsodiumException {
HttpResponse<String> mockResponse1 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse1.body()).thenReturn(Utils.jsonStringify(buildOperation(false, false)));
Mockito.when(mockResponse1.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse2 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse2.body()).thenReturn(Utils.jsonStringify(buildOperation(false, true)));
Mockito.when(mockResponse2.statusCode()).thenReturn(200);

HttpResponse<String> mockResponse3 = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse3.body()).thenReturn(Utils.jsonStringify(buildOperation(true, true)));
Mockito.when(mockResponse3.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse1)
.thenReturn(mockResponse2)
.thenReturn(mockResponse3);

Operations.WaitOptions options = Operations.WaitOptions.builder()
.maxSleep(10)
.build();
operations.wait(buildOperation(false, false), options);
verify(client, times(4)).fetch(anyString(), anyString(), isNull());
}

@Test
@DisplayName("Throw if aborting operation")
void throwIfAbortingOperation() throws IOException, InterruptedException, LibsodiumException {
Operation<String> operation = buildOperation(false, false);

HttpResponse<String> mockResponse = Mockito.mock(HttpResponse.class);
Mockito.when(mockResponse.body()).thenReturn(Utils.jsonStringify(operation));
Mockito.when(mockResponse.statusCode()).thenReturn(200);

when(client.fetch(anyString(), anyString(), isNull()))
.thenReturn(mockResponse);

Operations.WaitOptions options = Operations.WaitOptions.builder()
.maxSleep(10)
.abortSignal(Operations.AbortSignal.builder().timeout(5000L).build())
.build();

Exception exception = assertThrows(InterruptedException.class, () -> operations.wait(operation, options));
assertEquals("Operation aborted: Timeout", exception.getMessage());
}


Operation<String> buildOperation(boolean done, boolean dependsDone) {
Operation<String> operation = Operation.<String>builder()
.name(UUID.randomUUID().toString())
Expand All @@ -224,6 +124,4 @@ Operation<String> buildOperation(boolean done, boolean dependsDone) {
operation.setMetadata(metadata);
return operation;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.cardanofoundation.signify.core.Manager;
import org.cardanofoundation.signify.core.States;
import org.cardanofoundation.signify.e2e.utils.MultisigUtils;
import org.cardanofoundation.signify.e2e.utils.OperationWaiter;
import org.cardanofoundation.signify.e2e.utils.ResolveEnv;
import org.cardanofoundation.signify.e2e.utils.TestUtils;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -980,7 +981,7 @@ public static <T> Operation<T> waitOperations(
Object op) throws IOException, InterruptedException, LibsodiumException {
Operation operation = Operation.fromObject(op);
String name = operation.getName();
operation = client.operations().wait(operation);
operation = OperationWaiter.wait(client.operations(), operation);
TestUtils.deleteOperations(client, operation);
TestUtils.deleteOperation(client, name);
return operation;
Expand Down
Loading