Building first VoltSp project¶
The simplest way to start with VoltSp is to create a Volt(SP) quickstart Maven archetype, it will seed a sample project that can be further adjusted. For those not familiar with maven archetypes, they are maven projects for generating maven projects.
Example invocation:
mvn archetype:generate \
-DarchetypeGroupId=org.voltdb \
-DarchetypeArtifactId=volt-stream-maven-quickstart \
-DarchetypeVersion=1.6.0
mvn and java to be available on your local PATH.
Best practice is to add JAVA_HOME and MAVEN_HOME to the system PATH. The latest Java version works best.
The command will ask a couple of questions, like what should be a root package of the new project's files.
âšī¸ Note
I would assume that we usecom.example.vspand name "application"
Now cd into the project directory, and list the contents (with ls command). It should show
src/
main/
java/
com/
example/
vsp/
resources/
test/
java/
com/
example/
vsp/
target/
pom.xml
README.md
pom.xml file.
Look at src/main/java/com/example/vsp it contains sample pipelines and src/test/java/com/example/vsp contains the test that verifies pipelines correctness.
You build the project with
mvn clean install
Your pipeline may need specific VoltSP plugins. The archetype already includes some dependencies. Look at pom.xml; you will find entries like
<dependencies>
<dependency>
<groupId>org.voltdb</groupId>
<artifactId>volt-stream-plugin-volt-api</artifactId>
<version>${volt.stream.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.voltdb</groupId>
<artifactId>volt-stream-plugin-kafka-api</artifactId>
<version>${volt.stream.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
provided scope is important: those dependencies are already included within the VoltSP tarball and Docker image, but they are needed for Maven to correctly compile your code.
For more information about provided plugins read the VoltSP Components Reference section.
The next chapter will walk you through how to run test for provided pipelines.
Locally testing a user-defined pipeline¶
Let's look at a sample test (you can find it after creating the sample project from the previous section):
class RandomToConsolePipelineTest {
private static final Network network = Network.newNetwork();
private VoltSpContainer simulation = null;
@AfterEach
void tearDown() {
if (simulation != null) {
simulation.shutdown();
}
}
@Test
void shouldRunAsBlackBox() {
Path licensePath = Path.of("path/to/volt-license.xml");
assertThat(licensePath.toFile())
.describedAs("Volt license file not found")
.exists();
simulation = VoltSpContainer.newVoltSp()
.withVoltLicense(licensePath)
.withParallelism(1)
.withAppJar(WorkingDirPaths.resolve("target/app.jar").hostPath())
.withPipelineClass(RandomToConsolePipeline.class)
.withConfigurationYaml("""
tps: 100
""")
.withClassesUnder(MavenPaths.mavenClasses())
.withLoggerName("simulation")
// Docker can access other docker on the same network by its container name (see simulation.getContainerName()),
// but you can specify the alias.
.withNetwork("simulation", network)
// when
.awaitStart(Duration.ofSeconds(10));
assertThat(simulation).isNotNull();
Awaitility.await("for logs to arrive")
.untilAsserted(() -> {
assertThat(simulation.getLogs()).contains("HELLO EARTH ");
});
}
}
VoltSpContainer.
The container will be run locally on your machine, so this is not the best environment to test performance, but it is great to test correctness.
First of all verify the path to a valid Volt license by editing the line where the license is set:
Path licensePath = Path.of("path/to/volt-license.xml");
Let's walk through this example.
The VoltSpContainer API requires you to provide a fully qualified Docker image like voltdb/volt-streams:1.6.0.
The API allows you to pass only a tag or nothing; then voltdb/volt-streams:latest will be used.
Once we have specified which release we want to test against, we have to specify parallelism.
Parallelism¶
.withParallelism(1)
Pipeline definition¶
.withPipelineClass(RandomToConsolePipeline.class)
com.example.vsp.RandomToConsolePipeline class. In this example the pipeline will generate random text at a default rate of 10 TPS,
then transform the text and print it to the console. Very basic.
This simple pipeline could be expressed with a YAML definition; see YamlRandomToConsolePipelineTest.
Pipeline configuration¶
.withConfigurationYaml("""
tps: 100
""")
ExecutionContext.ConfigurationContext configurator = stream.getExecutionContext().configurator();
int tps = configurator.findByPath("tps").orElse(10);
.withConfigurationYaml("""
generator:
tps: 100
""")
// and accessed like this
int tps = configurator.findByPath("generator.tps").orElse(10);
sink:
kafka:
topicName: "my-topic"
bootstrapServers: "kafka.example.com:9092"
schemaRegistry: "http://registry.example.com"
properties:
key1: value1
Defining a classpath and resources¶
Package application JAR¶
User can specify any valid JAR as the application JAR. It must contain a pipeline class and all user classes the pipeline requires. For example, let's use a Maven-packaged JAR:
.withAppJar(WorkingDirPaths.resolve("target/application-1.6.0.jar").hostPath())
Maven source and test¶
VoltSpContainer supports adding classes and files from project directories.
.withClassesUnder(MavenPaths.mavenClasses())
target/classes â will be mounted at /volt-apps/ and available at container startup.
The MavenPaths API allows filtering files to a specific folder, sourcing from the test directory. The same applies to resources.
.withClassesUnder(MavenPaths.mavenTestClasses("org.voltsp.testcontainer.app"))
/volt-apps is printed to verify all classes and resources are currently mounted.
The API also helps with files under the working directory which are not recognised by Maven.
WorkingDirPaths.resolve("data/files")
project-root/data/files directory. The working directory is defined by System.getProperty("user.dir").
3rd party JAR¶
.with3rdPartyJar("org.apache.commons", "commons-lang3", "3.17.0")
/volt-apps/ directory of the container.
Networking¶
Users can define the network that the started container will use when connecting to other local containers. By default the host that launches the container can see all running containers and access them with localhost:EXPOSED_PORT, CONTAINER_NAME:EXPOSED_PORT or ALIAS:EXPOSED_PORT. The container will start with a random name, so the alias can be handy to always refer to a container with a given name.
.withNetwork("simulation", network)
Testcontainers API¶
VoltSpContainer is built around org.testcontainers.containers.GenericContainer. Full definition under GenericContainer javadoc:
https://javadoc.io/doc/org.testcontainers/testcontainers/latest/org/testcontainers/containers/GenericContainer.html
Anything that GenericContainer can do can be configured using VoltSpContainer.
Preparing for deployment¶
The command
mvn clean install
/target directory. The test defined all dependencies for the pipeline to run: the 3rd party dependencies, and VoltSP plugin dependencies.
Some of those are already included in the VoltSP package or Docker image, but 3rd party dependencies must be delivered with the application's JAR.
There are various strategies to deliver the application to a server
Copying project dependencies to a configured directory¶
The most straightforward option is to order Maven to list and copy all non-test and non-provided dependencies.
Adding this configuration to pom.xml will copy dependencies to a target/lib directory.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-compile-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-main-jar</id>
<phase>package</phase>
<configuration>
<target>
<copy file="${project.build.directory}/${project.build.finalName}.jar"
todir="${project.build.directory}/lib" />
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
The same directory can be used to group other files needed to run the application and tar the whole directory before moving it to a server.
Shading 3rd party JARs¶
A more advanced option is repackaging the application JAR and adding 3rd party JARs inline.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
target directory. All other files still need to be delivered somehow to a server.
Creating a custom Docker image¶
Create an app.dockerfile next to pom.xml with content like
FROM registry.access.redhat.com/ubi9:9.6 AS deps
RUN dnf install -y procps-ng tar && dnf clean all
FROM voltdb/volt-streams:latest
RUN mkdir -p /volt-apps
COPY target/lib/* /volt-apps/
COPY --from=deps /usr/bin/tar /usr/bin/
This can be built with
docker buildx build \
--push \
--platform linux/amd64 \
-t "my-org/sample-app:latest" \
-f app.dockerfile \
.