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

Update retrying client documentation #352

Merged
merged 2 commits into from
Feb 9, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 14 additions & 35 deletions docs/RetryingTarantoolClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,17 @@ By default, failed requests will be repeated only for some known network problem
Some retry policies are available in the `TarantoolRequestRetryPolicies` class, but you may use your own implementations.
If you want to use proxy calls or retry settings only for a number of requests, you may use configureClient(client)
in `TarantoolClientFactory` for making a new configured client instance. Note, that the new instance will share the same
connection pool and basic client settings, and only augment the behavior of the client.
See an example below:

```java

TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> setupClient() {
return TarantoolClientFactory.createClient()
.withCredentials("admin", "secret-cluster-cookie")
.withAddress(container.getRouterHost(), container.getRouterPort())
.withProxyMethodMapping()
.build();
}

TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> retrying(
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> client, int retries, long delay) {
return TarantoolClientFactory.configureClient(client)
.withRetryingByNumberOfAttempts(
retries,
// you can use default predicates from TarantoolRequestRetryPolicies for checking errors
TarantoolRequestRetryPolicies.retryNetworkErrors()
// also you can use your own predicates and combine them with each other or with defaults
.or(e -> e.getMessage().contains("Unsuccessful attempt"))
.or(TarantoolRequestRetryPolicies.retryTarantoolNoSuchProcedureErrors()),
policy -> policy.withDelay(delay))
.build();
}

...

TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> client = setupClient();
String result = retrying(client, 4, 500).callForSingleResult("retrying_function", String.class).get();
assertEquals("Success", result);
result = retrying(client, 2, 1000).callForSingleResult("retrying_function", String.class).get();
assertEquals("Success", result);
```
connection pool and basic client settings, and only augment the behavior of the client.

In this example I use custom delete function.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"I use" -> "we use" looks better I believe

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or "let's consider a custom delete function"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"I use" -> "we use" looks better I believe

I don't think so

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or "let's consider a custom delete function"

This looks better. I'll increase the formality of language in documentation soon #356

https://github.com/tarantool/cartridge-java/blob/3d30c6dcec6f88cabfdcdea01e9eed02614f3067/src/test/resources/cartridge/app/roles/api_router.lua#L165-L176
You can set up any client. In this case I use CRUD client.
https://github.com/tarantool/cartridge-java/blob/3d30c6dcec6f88cabfdcdea01e9eed02614f3067/src/test/java/io/tarantool/driver/integration/ReconnectIT.java#L143-L159
And reuse it then I need retrying client.
https://github.com/tarantool/cartridge-java/blob/3d30c6dcec6f88cabfdcdea01e9eed02614f3067/src/test/java/io/tarantool/driver/integration/ReconnectIT.java#L190-L215
You don't have to set up basic client if you need retying client only.
All methods of client builder with prefix `withRetrying` can be used with `createClient`.

In this code I call `delete_with_error_if_not_found` (custom delete function) before the record was inserted to the
database. So client recalls delete and removes the record after it was inserted.
https://github.com/tarantool/cartridge-java/blob/3d30c6dcec6f88cabfdcdea01e9eed02614f3067/src/test/java/io/tarantool/driver/integration/ReconnectIT.java#L85-L105
21 changes: 21 additions & 0 deletions src/test/java/io/tarantool/driver/TarantoolUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,25 @@ public Integer getMinor() {
return minor;
}
}

public static Integer DEFAULT_RETRYING_ATTEMPTS = 5;
public static Integer DEFAULT_RETRYING_DELAY = 100;

public static void retry(Runnable fn) throws InterruptedException {
retry(DEFAULT_RETRYING_ATTEMPTS, DEFAULT_RETRYING_DELAY, fn);
}

public static void retry(Integer attempts, Integer delay, Runnable fn) throws InterruptedException {
while (attempts > 0) {
try {
fn.run();
return;
} catch (AssertionError ignored) {
}

--attempts;
Thread.sleep(delay);
}
fn.run();
}
}
100 changes: 78 additions & 22 deletions src/test/java/io/tarantool/driver/integration/ReconnectIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.tarantool.driver.api.TarantoolClusterAddressProvider;
import io.tarantool.driver.api.TarantoolResult;
import io.tarantool.driver.api.TarantoolServerAddress;
import io.tarantool.driver.api.conditions.Conditions;
import io.tarantool.driver.api.space.TarantoolSpaceOperations;
import io.tarantool.driver.api.tuple.DefaultTarantoolTupleFactory;
import io.tarantool.driver.api.tuple.TarantoolTuple;
import io.tarantool.driver.api.tuple.TarantoolTupleFactory;
Expand Down Expand Up @@ -33,26 +35,38 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static io.tarantool.driver.TarantoolUtils.retry;
import static io.tarantool.driver.api.retry.TarantoolRequestRetryPolicies.retryNetworkErrors;
import static io.tarantool.driver.api.retry.TarantoolRequestRetryPolicies.retryTarantoolNoSuchProcedureErrors;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* @author Oleg Kuznetsov
* @author Ivan Dneprov
* <p>
* WARNING: If you updated the code in this file, don't forget to update the MultiInstanceConnecting.md and
* docs/RetryingTarantoolClient.md permalinks!
*/
@Testcontainers
public class ReconnectIT extends SharedCartridgeContainer {

private static final Logger logger = LoggerFactory.getLogger(ReconnectIT.class);

private static String USER_NAME;
private static String PASSWORD;
private static final String SPACE_NAME = "test__profile";
private static final String PK_FIELD_NAME = "profile_id";
private static AtomicInteger retryingCounter;

private static final DefaultMessagePackMapperFactory mapperFactory = DefaultMessagePackMapperFactory.getInstance();
private static final TarantoolTupleFactory tupleFactory =
Expand All @@ -66,21 +80,47 @@ public static void setUp() throws TimeoutException {
PASSWORD = container.getPassword();
}

@Test
public void test_should_retryDelete_ifRecordsNotFound() throws InterruptedException {
// Creating multiple clients
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> crudClient = getCrudClient();
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> retryingClient =
getRetryingTarantoolClient(crudClient);

TarantoolSpaceOperations<TarantoolTuple, TarantoolResult<TarantoolTuple>> space =
crudClient.space(SPACE_NAME);
space.truncate().join();

// Use TarantoolTupleFactory for instantiating new tuples
TarantoolTupleFactory tupleFactory = new DefaultTarantoolTupleFactory(
crudClient.getConfig().getMessagePackMapper());

// Call delete_with_error_if_not_found before record was inserted
Conditions conditions = Conditions.equals(PK_FIELD_NAME, 1);
CompletableFuture deleteFuture = retryingClient.space(SPACE_NAME).delete(conditions);
retry(() -> assertEquals(1, retryingCounter.get()));
space.insert(tupleFactory.create(1, null, "FIO", 50, 100)).join();
deleteFuture.join();
TarantoolResult<TarantoolTuple> selectResult = space.select(conditions).join();
assertEquals(0, selectResult.size());
}

/**
* Checking if this test is valid is here
* {@link TarantoolErrorsIT#test_should_throwTarantoolNoSuchProcedureException_ifProcedureIsNil}
*/
@Test
public void test_should_reconnect_ifCrudProcedureIsNotDefined() {
//when
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> clusterClient = getClusterClient();
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> retryingClient = getRetryingTarantoolClient();
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> crudClient = getCrudClient();
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> retryingClient =
getRetryingTarantoolClient(crudClient);

try {
//save procedure to tmp variable set it to nil and call
clusterClient.eval("rawset(_G, 'tmp_test_no_such_procedure', test_no_such_procedure)")
.thenAccept(c -> clusterClient.eval("rawset(_G, 'test_no_such_procedure', nil)"))
.thenApply(c -> clusterClient.call("test_no_such_procedure"))
crudClient.eval("rawset(_G, 'tmp_test_no_such_procedure', test_no_such_procedure)")
.thenAccept(c -> crudClient.eval("rawset(_G, 'test_no_such_procedure', nil)"))
.thenApply(c -> crudClient.call("test_no_such_procedure"))
.join();
} catch (CompletionException exception) {
assertTrue(exception.getCause() instanceof TarantoolNoSuchProcedureException);
Expand All @@ -93,30 +133,36 @@ public void test_should_reconnect_ifCrudProcedureIsNotDefined() {
} catch (InterruptedException e) {
e.printStackTrace();
}
clusterClient.eval("rawset(_G, 'test_no_such_procedure', tmp_test_no_such_procedure)").join();
crudClient.eval("rawset(_G, 'test_no_such_procedure', tmp_test_no_such_procedure)").join();
}).start();

assertDoesNotThrow(() -> retryingClient.call("test_no_such_procedure").join());
assertEquals("test_no_such_procedure", retryingClient.call("test_no_such_procedure").join().get(0));
}

private TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> getClusterClient() {
return TarantoolClientFactory
.createClient()
private TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> getCrudClient() {
return TarantoolClientFactory.createClient()
// You can connect to multiple routers
.withAddresses(
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3301)),
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3302)),
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3303))
)
// For connecting to a Cartridge application,
// use the value of cluster_cookie parameter in the init.lua file
.withCredentials(USER_NAME, PASSWORD)
// Number of connections per Tarantool instance
.withConnections(10)
// Specify using the default CRUD proxy operations mapping configuration
.withProxyMethodMapping()
.build();
}

@Test
public void test_should_reconnect_ifReconnectIsInvoked() throws Exception {
//when
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> client = getRetryingTarantoolClient();
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> client =
getRetryingTarantoolClient(getCrudClient());

// getting all routers uuids
final Set<String> routerUuids = getInstancesUuids(client);
Expand All @@ -141,18 +187,28 @@ public void test_should_reconnect_ifReconnectIsInvoked() throws Exception {
assertEquals(routerUuids.size(), uuidsAfterReconnect.size());
}

private TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> getRetryingTarantoolClient() {
return TarantoolClientFactory.createClient()
.withAddresses(
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3301)),
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3302)),
new TarantoolServerAddress(container.getRouterHost(), container.getMappedPort(3303))
)
.withCredentials(USER_NAME, PASSWORD)
.withConnections(10)
.withProxyMethodMapping()
private TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> getRetryingTarantoolClient(
TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> client) {

retryingCounter = new AtomicInteger();

return TarantoolClientFactory.configureClient(client)
// Configure a custom delete function
.withProxyMethodMapping(builder -> builder.withDeleteFunctionName("delete_with_error_if_not_found"))
// Set retrying policy
// First parameter is number of attempts
.withRetryingByNumberOfAttempts(5,
retryNetworkErrors().or(retryTarantoolNoSuchProcedureErrors()),
// You can use default predicates from TarantoolRequestRetryPolicies for checking errors
retryNetworkErrors()
// Also you can use your own predicates and combine them with each other
.or(e -> e.getMessage().contains("Unsuccessful attempt"))
.or(e -> {
retryingCounter.getAndIncrement();
return e.getMessage().contains("Records not found");
})
// Or with defaults
.or(retryTarantoolNoSuchProcedureErrors()),
// Also you can set delay in millisecond between attempts
factory -> factory.withDelay(300)
)
.build();
Expand Down
13 changes: 13 additions & 0 deletions src/test/resources/cartridge/app/roles/api_router.lua
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ local function box_error_non_network_error()
return nil, box.error.new(box.error.WAL_IO):unpack()
end

function delete_with_error_if_not_found(space_name, key, opts)
local result, err = crud.delete(space_name, key, opts)
if err then
return nil, err
end

if #result.rows == 0 then
return nil, "Records not found"
end

return result
end

local function test_no_such_procedure()
return 'test_no_such_procedure'
end
Expand Down