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

Add sample examples to test redis.io build #3051

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
40 changes: 40 additions & 0 deletions .github/workflows/doctests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Documentation Tests

on:
push:
tags-ignore:
- '*'
branches:
- 'main'
pull_request:
workflow_dispatch:

jobs:
doctests:
runs-on: ubuntu-latest
services:
redis-stack:
image: redis/redis-stack-server:latest
options: >-
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 6379:6379

steps:
- name: Checkout project
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'temurin'
- name: Cache local Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Run doctests
run: |
mvn -Pdoctests test
19 changes: 19 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,25 @@
</build>
</profile>

<profile>
<id>doctests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/examples/reactive/*Example.java</include>
<include>**/examples/async/*Example.java</include>
</includes>
<failIfNoTests>true</failIfNoTests>
</configuration>
</plugin>
</plugins>
</build>
</profile>

</profiles>

</project>
127 changes: 127 additions & 0 deletions src/test/java/io/redis/examples/async/StringExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// EXAMPLE: set_tutorial
package io.redis.examples.async;
Copy link
Collaborator

Choose a reason for hiding this comment

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

btw is this package a requirement?

we already have some examples in the io.lettuce.examples package inside src/test/java


import io.lettuce.core.*;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.StatefulRedisConnection;

// REMOVE_START
import org.junit.jupiter.api.Test;
// REMOVE_END

import java.util.*;
import java.util.concurrent.CompletableFuture;

// REMOVE_START
import static org.assertj.core.api.Assertions.assertThat;
// REMOVE_END

public class StringExample {

// REMOVE_START
@Test
// REMOVE_END
public void run() {
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
Copy link
Collaborator

Choose a reason for hiding this comment

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

FWIW I would encourage our users to use the RedisUri pattern instead of providing the URL and port inside a string. Not critical, but I think for educational purposes it would be nice to have them done this way.


try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
RedisAsyncCommands<String, String> asyncCommands = connection.async();

// STEP_START set_get
CompletableFuture<Void> setAndGet = asyncCommands.set("bike:1", "Deimos").thenCompose(v -> {
System.out.println(v); // OK
// REMOVE_START
assertThat(v).isEqualTo("OK");
// REMOVE_END
return asyncCommands.get("bike:1");
})
// REMOVE_START
.thenApply(res -> {
assertThat(res).isEqualTo("Deimos");
return res;
})
// REMOVE_END
.thenAccept(System.out::println) // Deimos
.toCompletableFuture();
// STEP_END

// STEP_START setnx_xx
CompletableFuture<Void> setnx = asyncCommands.setnx("bike:1", "bike").thenCompose(v -> {
System.out.println(v); // false (because key already exists)
// REMOVE_START
assertThat(v).isFalse();
// REMOVE_END
return asyncCommands.get("bike:1");
})
// REMOVE_START
.thenApply(res -> {
assertThat(res).isEqualTo("Deimos");
return res;
})
// REMOVE_END
.thenAccept(System.out::println) // Deimos (value is unchanged)
.toCompletableFuture();

// set the value to "bike" if it already exists
CompletableFuture<Void> setxx = asyncCommands.set("bike:1", "bike", SetArgs.Builder.xx())
// REMOVE_START
.thenApply(res -> {
assertThat(res).isEqualTo("OK");
return res;
})
// REMOVE_END
.thenAccept(System.out::println) // OK
.toCompletableFuture();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not necessarily bad, but you are not calling any of the methods that would guarantee that the main thread would not end before the executor threads.

As far as my understanding of how CompletableFuture works the code inside the stages / futures is executed by some thread pool (if no executor is provided then that would be the ForkJoinPool#commonPool) and without calling get() or join() for example theoretically the main thread might end before the thread that executes the future does. IMHO it would be good to implement some sort of synchronization here, simplest would be to call get(), but you can also do some sort of awat-all for all the futures in the end of the main method

// STEP_END

// STEP_START mset
Map<String, String> bikeMap = new HashMap<>();
bikeMap.put("bike:1", "Deimos");
bikeMap.put("bike:2", "Ares");
bikeMap.put("bike:3", "Vanth");

CompletableFuture<Void> mset = asyncCommands.mset(bikeMap).thenCompose(v -> {
System.out.println(v); // OK
return asyncCommands.mget("bike:1", "bike:2", "bike:3");
})
// REMOVE_START
.thenApply(res -> {
List<KeyValue<String, String>> expected = new ArrayList<>(
Arrays.asList(KeyValue.just("bike:1", "Deimos"), KeyValue.just("bike:2", "Ares"),
KeyValue.just("bike:3", "Vanth")));
assertThat(res).isEqualTo(expected);
return res;
})
// REMOVE_END
.thenAccept(System.out::println) // [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3,
// Vanth]]
.toCompletableFuture();
// STEP_END

// STEP_START incr
CompletableFuture<Void> incrby = asyncCommands.set("total_crashes", "0")
.thenCompose(v -> asyncCommands.incr("total_crashes")).thenCompose(v -> {
System.out.println(v); // 1
// REMOVE_START
assertThat(v).isEqualTo(1L);
// REMOVE_END
return asyncCommands.incrby("total_crashes", 10);
})
// REMOVE_START
.thenApply(res -> {
assertThat(res).isEqualTo(11L);
return res;
})
// REMOVE_END
.thenAccept(System.out::println) // 11
.toCompletableFuture();
// STEP_END

CompletableFuture.allOf(setAndGet, setnx, setxx, mset, incrby).join();

} finally {
redisClient.shutdown();
}
}

}
104 changes: 104 additions & 0 deletions src/test/java/io/redis/examples/reactive/StringExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// EXAMPLE: set_tutorial
package io.redis.examples.reactive;

import io.lettuce.core.*;
import io.lettuce.core.api.reactive.RedisReactiveCommands;
import io.lettuce.core.api.StatefulRedisConnection;
// REMOVE_START
import org.junit.jupiter.api.Test;
// REMOVE_END
import reactor.core.publisher.Mono;

import java.util.*;

// REMOVE_START
import static org.assertj.core.api.Assertions.assertThat;
// REMOVE_END

public class StringExample {

// REMOVE_START
@Test
// REMOVE_END
public void run() {
RedisClient redisClient = RedisClient.create("redis://localhost:6379");

try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
RedisReactiveCommands<String, String> reactiveCommands = connection.reactive();

// STEP_START set_get
Mono<Void> setAndGet = reactiveCommands.set("bike:1", "Deimos").doOnNext(v -> {
System.out.println(v); // OK
// REMOVE_START
assertThat(v).isEqualTo("OK");
// REMOVE_END
}).flatMap(v -> reactiveCommands.get("bike:1")).doOnNext(res -> {
// REMOVE_START
assertThat(res).isEqualTo("Deimos");
// REMOVE_END
System.out.println(res); // Deimos
}).then();
Copy link
Contributor

@ggivo ggivo Nov 26, 2024

Choose a reason for hiding this comment

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

Since we are demonstrating set and then get, removing the then() and returning the actual result from the get operation is better. This should be closer to the actual use.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above I would expect that we call block() here.

@ggivo is that what you mean by "return the actual result"?

Copy link
Contributor

@ggivo ggivo Nov 29, 2024

Choose a reason for hiding this comment

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

What I meant is that then() will discard the result from get() and will get as result Mono<Void> instead Mono<String>
I would expect that if anyone is following, a reactive example will be aware of .block() or ".subscribe()'
and hopefully, he will be searching for a way to integrate it within existing reactive code and know how to use the resulting Mono.
So the example code should be something like

            Mono<String> setAndGet = reactiveCommands.set("bike:1", "Deimos")
	                    .doOnNext(res-> System.out.println(res)) // OK
	                    .flatMap(v -> reactiveCommands.get("bike:1"))
	                    .doOnNext(res-> System.out.println(res)); // "Deimos"

// STEP_END
Comment on lines +29 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// STEP_START set_get
Mono<Void> setAndGet = reactiveCommands.set("bike:1", "Deimos").doOnNext(v -> {
System.out.println(v); // OK
// REMOVE_START
assertThat(v).isEqualTo("OK");
// REMOVE_END
}).flatMap(v -> reactiveCommands.get("bike:1")).doOnNext(res -> {
// REMOVE_START
assertThat(res).isEqualTo("Deimos");
// REMOVE_END
System.out.println(res); // Deimos
}).then();
// STEP_END
// STEP_START set_get
// Deimos
Mono<String> setAndGet = reactiveCommands.set("bike:1", "Deimos")
.doOnNext(res-> System.out.println(res)) // OK
.flatMap(v -> reactiveCommands.get("bike:1"))
.doOnNext(res-> System.out.println(res)); // Deimos
// REMOVE_START
String[] output = SysOutCaptor.CaptureSystemOut(() ->
StepVerifier.create(setAndGet)
.assertNext(v -> assertThat(v).isEqualTo("Deimos"))
.expectComplete()
.verify()
);
assertThat(output).containsSequence("OK", "Deimos");
// REMOVE_END
// STEP_END

I suggest extracting checks for code examples, as a separate step for readability and ease of maintenance


// STEP_START setnx_xx
Mono<Void> setnx = reactiveCommands.setnx("bike:1", "bike").doOnNext(v -> {
System.out.println(v); // false (because key already exists)
// REMOVE_START
assertThat(v).isFalse();
// REMOVE_END
}).flatMap(v -> reactiveCommands.get("bike:1")).doOnNext(res -> {
// REMOVE_START
assertThat(res).isEqualTo("Deimos");
// REMOVE_END
System.out.println(res); // Deimos (value is unchanged)
}).then();

Mono<Void> setxx = reactiveCommands.set("bike:1", "bike", SetArgs.Builder.xx()).doOnNext(res -> {
// REMOVE_START
assertThat(res).isEqualTo("OK");
// REMOVE_END
System.out.println(res); // OK
}).then();
Copy link
Contributor

Choose a reason for hiding this comment

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

same here. we can remove .then()

// STEP_END

// STEP_START mset
Map<String, String> bikeMap = new HashMap<>();
bikeMap.put("bike:1", "Deimos");
bikeMap.put("bike:2", "Ares");
bikeMap.put("bike:3", "Vanth");

Mono<Void> mset = reactiveCommands.mset(bikeMap).doOnNext(System.out::println) // OK
.flatMap(v -> reactiveCommands.mget("bike:1", "bike:2", "bike:3").collectList()).doOnNext(res -> {
List<KeyValue<String, String>> expected = new ArrayList<>(
Arrays.asList(KeyValue.just("bike:1", "Deimos"), KeyValue.just("bike:2", "Ares"),
KeyValue.just("bike:3", "Vanth")));
// REMOVE_START
assertThat(res).isEqualTo(expected);
// REMOVE_END
System.out.println(res); // [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3, Vanth]]
}).then();
// STEP_END

// STEP_START incr
Mono<Void> incrby = reactiveCommands.set("total_crashes", "0").flatMap(v -> reactiveCommands.incr("total_crashes"))
.doOnNext(v -> {
System.out.println(v); // 1
// REMOVE_START
assertThat(v).isEqualTo(1L);
// REMOVE_END
}).flatMap(v -> reactiveCommands.incrby("total_crashes", 10)).doOnNext(res -> {
// REMOVE_START
assertThat(res).isEqualTo(11L);
// REMOVE_END
System.out.println(res); // 11
}).then();
// STEP_END

Mono.when(setAndGet, setnx, setxx, mset, incrby).block();

} finally {
redisClient.shutdown();
}
}

}
Loading