Using the Cassandra Client
Apache Cassandra® is a free and open-source, distributed, wide column store, NoSQL database management system designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure.
In this guide, we will see how you can get your REST services to use a Cassandra database.
This extension is developed by a third party and is part of the Quarkus Platform. |
准备
要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.6
-
Optionally the Quarkus CLI if you want to use it
-
Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
-
A running Apache Cassandra, DataStax Enterprise (DSE) or DataStax Astra database; or alternatively, a fresh Docker installation.
架构
This quickstart guide shows how to build a REST application using the Cassandra Quarkus extension, which allows you to connect to an Apache Cassandra, DataStax Enterprise (DSE) or DataStax Astra database, using the DataStax Java driver.
This guide will also use the DataStax Object Mapper – a powerful Java-to-CQL mapping framework that greatly simplifies your application’s data access layer code by sparing you the hassle of writing your CQL queries by hand.
The application built in this quickstart guide is quite simple: the user can add elements in a list using a form, and the items list is updated. All the information between the browser and the server is formatted as JSON, and the elements are stored in the Cassandra database.
完整源码
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
The solution is located in the quickstart directory of the Cassandra Quarkus extension GitHub repository.
Creating a Blank Maven Project
First, create a new Maven project and copy the pom.xml
file that is
present in the quickstart
directory.
The pom.xml
is importing all the Quarkus extensions and dependencies you
need.
Creating the Data Model and Data Access Objects
In this example, we will create an application to manage a list of fruits.
First, let’s create our data model – represented by the Fruit
class – as
follows:
@Entity
@PropertyStrategy(mutable = false)
public class Fruit {
@PartitionKey
private final String name;
private final String description;
public Fruit(String name, String description) {
this.name = name;
this.description = description;
}
// getters, hashCode, equals, toString methods omitted for brevity
}
As stated above, we are using the DataStax Object Mapper. In other words, we are not going to write our CQL queries manually; instead, we will annotate our data model with a few annotations, and the mapper will generate proper CQL queries underneath.
This is why the Fruit
class is annotated with @Entity
: this annotation
marks it as an entity class that is mapped to a Cassandra table. Its
instances are meant to be automatically persisted into, and retrieved from,
the Cassandra database. Here, the table name will be inferred from the class
name: fruit
.
Also, the name
field represents a Cassandra partition key, and so we are
annotating it with @PartitionKey
– another annotation from the Object
Mapper library.
Entity classes are normally required to have a default no-arg constructor,
unless they are annotated with @PropertyStrategy(mutable = false) , which
is the case here.
|
The next step is to create a DAO (Data Access Object) interface that will
manage instances of Fruit
entities:
@Dao
public interface FruitDao {
@Update
void update(Fruit fruit);
@Select
PagingIterable<Fruit> findAll();
}
This interface exposes operations that will be used in our REST
service. Again, the annotation @Dao
comes from the DataStax Object Mapper,
which will also automatically generate an implementation of this interface
for you.
Note also the special return type of the findAll
method,
PagingIterable
:
it’s the base type of result sets returned by the driver.
Finally, let’s create a Mapper interface:
@Mapper
public interface FruitMapper {
@DaoFactory
FruitDao fruitDao();
}
The @Mapper
annotation is yet another annotation recognized by the
DataStax Object Mapper. A mapper is responsible for constructing DAO
instances – in this case, out mapper is constructing an instance of our only
DAO, FruitDao
.
Think of the mapper interface as a factory for DAO beans. If you intend to
construct and inject a specific DAO bean in your own code, then you first
must add a @DaoFactory
method for it in a @Mapper
interface.
@DaoFactory method names are irrelevant.
|
@DaoFactory
methods should return beans of the following types:
-
Any
@Dao
-annotated interface, e.g.FruitDao
; -
A
CompletionStage
of any@Dao
-annotated interface, e.g.CompletionStage<FruitDao>
. -
A
Uni
of any@Dao
-annotated interface, e.g.Uni<FruitDao>
.
Uni is a type from the Mutiny library, which is the reactive programming
library used by Quarkus. This will be explained in more detail in the
"Reactive Programming" section below.
|
Generating the DAO and mapper implementations
As you probably guessed already, we are not going to implement the interfaces above. Instead, the Object Mapper will generate such implementations for us.
The Object Mapper is composed of 2 pieces:
-
A (compile-time) annotation processor that scans the classpath for classes annotated with
@Mapper
,@Dao
or@Entity
, and generates code and CQL queries for them; and -
A runtime module that contains the logic to execute the generated queries.
Therefore, enabling the Object Mapper requires two steps:
-
Declare the
cassandra-quarkus-mapper-processor
annotation processor. With Maven, this is done by modifying the compiler plugin configuration in the project’spom.xml
file as follows:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>com.datastax.oss.quarkus</groupId>
<artifactId>cassandra-quarkus-mapper-processor</artifactId>
<version>${cassandra-quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
With Gradle, this is done by adding the following line to the build.gradle
file:
annotationProcessor "com.datastax.oss.quarkus:cassandra-quarkus-mapper-processor:${cassandra-quarkus.version}"
Verify that you are enabling the right annotation processor! The Cassandra
driver ships with its Object Mapper annotation processor, called
java-driver-mapper-processor . But the Cassandra Quarkus extension also
ships with its own annotation processor:
cassandra-quarkus-mapper-processor , which has more capabilities than the
driver’s. This annotation processor is the only one suitable for use in a
Quarkus application, so check that this is the one in use. Also, never use
both annotation processors together.
|
-
Declare the
java-driver-mapper-runtime
dependency in compile scope in the project’spom.xml
file as follows:
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-mapper-runtime</artifactId>
</dependency>
Although this module is called "runtime", it must be declared in compile scope. |
If your project is correctly set up, you should now be able to compile it
without errors, and you should see the generated code in the
target/generated-sources/annotations
directory (if you are using
Maven). It’s not required to get familiar with the generated code though, as
it is mostly internal machinery to interact with the database.
Creating a service & JSON REST endpoint
Now let’s create a FruitService
that will be the business layer of our
application and store/load the fruits from the Cassandra database.
@ApplicationScoped
public class FruitService {
@Inject FruitDao dao;
public void save(Fruit fruit) {
dao.update(fruit);
}
public List<Fruit> getAll() {
return dao.findAll().all();
}
}
Note how the service is being injected a FruitDao
instance. This DAO
instance is injected automatically, thanks to the generated implementations.
The Cassandra Quarkus extension allows you to inject any of the following beans in your own components:
-
All
@Mapper
-annotated interfaces in your project. -
You can also inject a
CompletionStage
orUni
of any@Mapper
-annotated interface. -
Any bean returned by a
@DaoFactory
method (see above for possible bean types). -
The
QuarkusCqlSession
bean: this application-scoped, singleton bean is your main entry point to the Cassandra client; it is a specialized Cassandra driver session instance with a few methods tailored especially for Quarkus. Read its javadocs carefully! -
You can also inject
CompletionStage<QuarkusCqlSession>
orUni<QuarkusCqlSession>
.
In our example, both FruitMapper
and FruitDao
could be injected
anywhere. We chose to inject FruitDao
in FruitService
.
The last missing piece is the REST API that will expose GET and POST methods:
@Path("/fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FruitResource {
@Inject FruitService fruitService;
@GET
public List<FruitDto> getAll() {
return fruitService.getAll().stream().map(this::convertToDto).collect(Collectors.toList());
}
@POST
public void add(FruitDto fruit) {
fruitService.save(convertFromDto(fruit));
}
private FruitDto convertToDto(Fruit fruit) {
return new FruitDto(fruit.getName(), fruit.getDescription());
}
private Fruit convertFromDto(FruitDto fruitDto) {
return new Fruit(fruitDto.getName(), fruitDto.getDescription());
}
}
Notice how FruitResource
is being injected a FruitService
instance
automatically.
It is generally not recommended using the same entity object between the
REST API and the data access layer. These layers should indeed be decoupled
and use distinct APIs in order to allow each API to evolve independently of
the other. This is the reason why our REST API is using a different object:
the FruitDto
class – the word DTO stands for "Data Transfer Object". This
DTO object will be automatically converted to and from JSON in HTTP
messages:
public class FruitDto {
private String name;
private String description;
public FruitDto() {}
public FruitDto(String name, String description) {
this.name = name;
this.description = description;
}
// getters and setters omitted for brevity
}
The translation to and from JSON is done automatically by the Quarkus RESTEasy Reactive extension, which is included in this guide’s pom.xml file. If you want to add it manually to your application, add the below snippet to your application’s ppm.xml file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
DTO classes used by the JSON serialization layer are required to have a default no-arg constructor. |
The conversion from DTO to JSON is handled automatically for us, but we
still must convert from Fruit
to FruitDto
and vice versa. This must be
done manually, which is why we have two conversion methods declared in
FruitResource
: convertToDto
and convertFromDto
.
In our example, Fruit and FruitDto are very similar, so you might wonder
why not use Fruit everywhere. In real life cases though, it’s not uncommon
to see DTOs and entities having very different structures.
|
Connecting to the Cassandra Database
Connecting to Apache Cassandra or DataStax Enterprise (DSE)
The main properties to configure are: contact-points
, to access the
Cassandra database; local-datacenter
, which is required by the driver; and
– optionally – the keyspace to bind to.
A sample configuration should look like this:
quarkus.cassandra.contact-points={cassandra_ip}:9042
quarkus.cassandra.local-datacenter={dc_name}
quarkus.cassandra.keyspace={keyspace}
In this example, we are using a single instance running on localhost, and
the keyspace containing our data is k1
:
quarkus.cassandra.contact-points=127.0.0.1:9042
quarkus.cassandra.local-datacenter=datacenter1
quarkus.cassandra.keyspace=k1
If your cluster requires plain text authentication, you must also provide
two more settings: username
and password
.
quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t
Connecting to a DataStax Astra Cloud Database
When connecting to DataStax Astra, instead of providing a contact point and a datacenter, you should provide a so-called secure connect bundle, which should point to a valid path to an Astra secure connect bundle file. You can download your secure connect bundle from the Astra web console.
You will also need to provide a username and password, since authentication is always required on Astra clusters.
A sample configuration for DataStax Astra should look like this:
quarkus.cassandra.cloud.secure-connect-bundle=/path/to/secure-connect-bundle.zip
quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t
quarkus.cassandra.keyspace=k1
Advanced Driver Configuration
You can configure other Java driver settings using application.conf
or
application.json
files. They need to be located in the classpath of your
application. All settings will be passed automatically to the underlying
driver configuration mechanism. Settings defined in application.properties
with the quarkus.cassandra
prefix will have priority over settings defined
in application.conf
or application.json
.
To see the full list of settings, please refer to the driver settings reference.
Running a Local Cassandra Database
By default, the Cassandra client is configured to access a local Cassandra database on port 9042 (the default Cassandra port).
Make sure that the setting quarkus.cassandra.local-datacenter matches the
datacenter of your Cassandra cluster.
|
If you don’t know the name of your local datacenter, this value can be found
by running the following CQL query: SELECT data_center FROM system.local .
|
If you want to use Docker to run a Cassandra database, you can use the following command to launch one in the background:
docker run --name local-cassandra-instance -p 9042:9042 -d cassandra
Next you need to create the keyspace and table that will be used by your application. If you are using Docker, run the following commands:
docker exec -it local-cassandra-instance cqlsh -e "CREATE KEYSPACE IF NOT EXISTS k1 WITH replication = {'class':'SimpleStrategy', 'replication_factor':1}"
docker exec -it local-cassandra-instance cqlsh -e "CREATE TABLE IF NOT EXISTS k1.fruit(name text PRIMARY KEY, description text)"
You can also use the CQLSH utility to interactively interrogate your database:
docker exec -it local-cassandra-instance cqlsh
Testing the REST API
In the project root directory:
-
Run
mvn clean package
and thenjava -jar ./target/cassandra-quarkus-quickstart-*-runner.jar
to start the application; -
Or better yet, run the application in dev mode:
mvn clean quarkus:dev
.
Now you can use curl commands to interact with the underlying REST API.
To create a fruit:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"apple","description":"red and tasty"}' \
http://localhost:8080/fruits
To retrieve fruits:
curl -X GET http://localhost:8080/fruits
Creating a Frontend
Now let’s add a simple web page to interact with our FruitResource
.
Quarkus automatically serves static resources located under the
META-INF/resources
directory. In the
src/main/resources/META-INF/resources
directory, add a fruits.html
file
with the contents from
this file in it.
You can now interact with your REST service:
-
If you haven’t done yet, start your application with
mvn clean quarkus:dev
; -
Point your browser to
http://localhost:8080/fruits.html
; -
Add new fruits to the list via the form.
Reactive Programming with the Cassandra Client
The
QuarkusCqlSession
interface gives you access to a series of reactive methods that integrate
seamlessly with Quarkus and its reactive framework, Mutiny.
If you are not familiar with Mutiny, please check Mutiny - an intuitive reactive programming library. |
Let’s rewrite our application using reactive programming with Mutiny.
First, let’s declare another DAO interface that works in a reactive way:
@Dao
public interface ReactiveFruitDao {
@Update
Uni<Void> updateAsync(Fruit fruit);
@Select
MutinyMappedReactiveResultSet<Fruit> findAll();
}
Note the usage of MutinyMappedReactiveResultSet
- it is a specialized
Mutiny
type converted from the original Publisher
returned by the
driver, which also exposes a few extra methods, e.g. to obtain the query
execution info. If you don’t need anything in that interface, you can also
simply declare your method to return Multi
: Multi<Fruit> findAll()
,
Similarly, the method updateAsync
returns a Uni
- it is automatically
converted from the original result set returned by the driver.
The Cassandra driver uses the Reactive Streams Publisher API for reactive
calls. The Quarkus framework however uses Mutiny. Because of that, the
CqlQuarkusSession interface transparently converts the Publisher
instances returned by the driver into the reactive type Multi .
CqlQuarkusSession is also capable of converting a Publisher into a Uni
– in this case, the publisher is expected to emit at most one row, then
complete. This is suitable for write queries (they return no rows), or for
read queries guaranteed to return one row at most (count queries, for
example).
|
Next, we need to adapt the FruitMapper
to construct a ReactiveFruitDao
instance:
@Mapper
public interface FruitMapper {
// the existing method omitted
@DaoFactory
ReactiveFruitDao reactiveFruitDao();
}
Now, we can create a ReactiveFruitService
that leverages our reactive DAO:
@ApplicationScoped
public class ReactiveFruitService {
@Inject ReactiveFruitDao fruitDao;
public Uni<Void> add(Fruit fruit) {
return fruitDao.update(fruit);
}
public Multi<Fruit> getAll() {
return fruitDao.findAll();
}
}
Finally, we can create a ReactiveFruitResource
:
@Path("/reactive-fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveFruitResource {
@Inject ReactiveFruitService service;
@GET
public Multi<FruitDto> getAll() {
return service.getAll().map(this::convertToDto);
}
@POST
public Uni<Void> add(FruitDto fruitDto) {
return service.add(convertFromDto(fruitDto));
}
private FruitDto convertToDto(Fruit fruit) {
return new FruitDto(fruit.getName(), fruit.getDescription());
}
private Fruit convertFromDto(FruitDto fruitDto) {
return new Fruit(fruitDto.getName(), fruitDto.getDescription());
}
}
The above resource is exposing a new endpoint, reactive-fruits
. Its
capabilities are identical to the ones that we created before with
FruitResource
, but everything is handled in a reactive fashion, without
any blocking operation.
The getAll() method above returns Multi , and the add() method returns
Uni . These types are the same Mutiny types that we met before; they are
automatically recognized by the Quarkus reactive REST API, so we don’t need
to convert them into JSON ourselves.
|
RESTEasy Reactive natively supports the Mutiny reactive types e.g. Uni
and
Multi
.
This dependency is already included in this guide’s pom.xml, but if you are starting a new project from scratch, make sure to include it.
Testing the Reactive REST API
Run the application in dev mode as explained above, then you can use curl commands to interact with the underlying REST API.
To create a fruit using the reactive REST endpoint:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"banana","description":"yellow and sweet"}' \
http://localhost:8080/reactive-fruits
To retrieve fruits with the reactive REST endpoint:
curl -X GET http://localhost:8080/reactive-fruits
Creating a Reactive Frontend
Now let’s add a simple web page to interact with our
ReactiveFruitResource
. In the src/main/resources/META-INF/resources
directory, add a reactive-fruits.html
file with the contents from
this file
in it.
You can now interact with your reactive REST service:
-
If you haven’t done yet, start your application with
mvn clean quarkus:dev
; -
Point your browser to
http://localhost:8080/reactive-fruits.html
; -
Add new fruits to the list via the form.
Health Checks
If you are using the Quarkus SmallRye Health extension, then the Cassandra client will automatically add a readiness health check to validate the connection to the Cassandra cluster. This extension is already included in this guide’s pom.xml, but if you need to include it manually in your application, add the following:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
When health checks are available, you can access the /health/ready
endpoint of your application and have information about the connection
validation status.
Running in dev mode with mvn clean quarkus:dev
, if you point your browser
to http://localhost:8080/health/ready you should see an output similar to
the following one:
{
"status": "UP",
"checks": [
{
"name": "DataStax Apache Cassandra Driver health check",
"status": "UP",
"data": {
"cqlVersion": "3.4.4",
"releaseVersion": "3.11.7",
"clusterName": "Test Cluster",
"datacenter": "datacenter1",
"numberOfNodes": 1
}
}
]
}
If you need health checks globally enabled in your application, but don’t
want to activate Cassandra health checks, you can disable Cassandra health
checks by setting the quarkus.cassandra.health.enabled property to false
in your application.properties .
|
Metrics
The Cassandra Quarkus client can provide metrics about the Cassandra session and about individual Cassandra nodes. It supports both Micrometer and MicroProfile.
The first step to enable metrics is to add a few additional dependencies depending on the metrics framework you plan to use.
Enabling Metrics with Micrometer
Micrometer is the recommended metrics framework in Quarkus applications.
To enable Micrometer metrics in your application, you need to add the following to your pom.xml.
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-metrics-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
This guide uses Micrometer, so the above dependencies are already included in this guide’s pom.xml.
Enabling Metrics with MicroProfile Metrics
Remove any dependency to Micrometer from your pom.xml, then add the following ones instead:
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-metrics-microprofile</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
Enabling Cassandra Metrics
Even when metrics are enabled in your application, the Cassandra client will
not report any metrics, unless you opt in for this feature. So your next
step is to enable Cassandra metrics in your application.properties
file.
quarkus.cassandra.metrics.enabled=true
That’s it!
The final (and optional) step is to customize which specific Cassandra metrics you would like the Cassandra client to track. Several metrics can be tracked; if you skip this step, a default set of useful metrics will be automatically tracked.
For the full list of available metric names, please refer to the
driver
settings reference page; search for the advanced.metrics section. Also,
Cassandra driver metrics are covered in detail in the
driver
manual.
|
If you do wish to customize which metrics to track, you should use the following properties:
-
quarkus.cassandra.metrics.session.enabled
should contain the session-level metrics to enable (metrics that are global to the session). -
quarkus.cassandra.metrics.node.enabled
should contain the node-level metrics to enable (metrics for which each node contacted by the Cassandra client gets its own metric value).
Both properties accept a comma-separated list of valid metric names.
For example, let’s assume that you wish to enable the following three Cassandra metrics:
-
Session-level:
session.connected-nodes
andsession.bytes-sent
; -
Node-level:
node.pool.open-connections
.
Then you should add the following settings to your application.properties
:
quarkus.cassandra.metrics.enabled=true
quarkus.cassandra.metrics.session.enabled=connected-nodes,bytes-sent
quarkus.cassandra.metrics.node.enabled=pool.open-connections
This guide’s application.properties
file has already many metrics enabled;
you can use its metrics list as a good starting point for exposing useful
Cassandra metrics in your application.
When metrics are properly enabled, metric reports for all enabled metrics
are available at the /metrics
REST endpoint of your application.
Running in dev mode with mvn clean quarkus:dev
, if you point your browser
to http://localhost:8080/metrics
you should see a list of metrics; search
for metrics whose names contain cassandra
.
For Cassandra metrics to show up, the Cassandra client needs to be initialized and connected; if you are using lazy initialization (see below), you won’t see any Cassandra metrics until your application actually connects and hits the database for the first time. |
Running in native mode
If you installed GraalVM, you can build a native image using:
mvn clean package -Dnative
Beware that native compilation can take a significant amount of time! Once the compilation is done, you can run the native executable as follows:
./target/cassandra-quarkus-quickstart-*-runner
You can then point your browser to http://localhost:8080/fruits.html
and
use your application.
Choosing between eager and lazy initialization
As explained above, this extension allows you to inject many types of beans:
-
A simple bean like
QuarkusCqlSession
orFruitDao
; -
The asynchronous version of that bean, for example
CompletionStage<QuarkusCqlSession>
or `CompletionStage<FruitDao>; -
The reactive version of that bean, for example
Uni<QuarkusCqlSession>
orUni<FruitDao>
.
The most straightforward approach is obviously to inject the bean
directly. This should work just fine for most applications. However, the
QuarkusCqlSession
bean, and all DAO beans that depend on it, might take
some time to initialize before they can be used for the first time, and this
process is blocking.
Fortunately, it is possible to control when the initialization should
happen: the quarkus.cassandra.init.eager-init
parameter determines if the
QuarkusCqlSession
bean should be initialized on its first access (lazy) or
when the application is starting (eager). The default value of this
parameter is false
, meaning the init process is lazy: the
QuarkusCqlSession
bean will be initialized lazily on its first access –
for example, when there is a first REST request that needs to interact with
the Cassandra database.
Using lazy initialization speeds up your application startup time, and
avoids startup failures if the Cassandra database is not available. However,
it could also prove dangerous if your code is fully non-blocking, for
example if it uses reactive routes. Indeed, the
lazy initialization could accidentally happen on a thread that is not
allowed to block, such as a Vert.x event loop thread. Therefore, setting
quarkus.cassandra.init.eager-init
to false
and injecting
QuarkusCqlSession
should be avoided in these contexts.
If you want to use Vert.x (or any other non-blocking framework) and keep the
lazy initialization behavior, you should instead inject only a
CompletionStage
or a Uni
of the desired bean. When injecting these
beans, the initialization process will be triggered lazily, but it will
happen in the background, in a non-blocking way, leveraging the Vert.x event
loop. This way you don’t risk blocking the Vert.x thread.
Alternatively, you can set quarkus.cassandra.init.eager-init
to true: in
this case the session bean and all DAO beans will be initialized eagerly
during application startup, on the Quarkus main thread. This would eliminate
any risk of blocking a Vert.x thread, at the cost of making your startup
time (much) longer.