A Technical Odyssey

[
[
[

]
]
]

In the last article we saw how to connect to ZooKeeper, elect a client, read it’s configuration and react to a change in the number of agents subscribed to the elected client. All this was required to implement the first state in our application typology (figure 1). We have now a distributed, autoconfigurable application ready to work.

Figure 1 : Application topology

Our first use case mentions that users will have two options : either have the market data delivered to a database hosted on a platform of their choice, eg. amazon RDS, or have it hosted on our platform.

Today we are going to add this functionality to our project and dynamically configure a datasource. As usual, the code for this article is available in a dedicated branch on github


The project relies on Spring Data JPA and Flyway DB. Flyway is an open-source database migration tool. Spring Boot integration for Flyway DB automatically check if the database structure is at the expected level. When it detects that the schema requires an update, it automatically applies the provided migration scripts before running the application.

Finally, we’ll use PostgreSQL. Support for other databases will be added later.

Let’s start by the required dependencies (figure 2)

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
    </dependency>
    <dependency>
      <groupId>org.flywaydb</groupId>
      <artifactId>flyway-core</artifactId>
    </dependency>

Figure 2 : maven dependencies

As we don’t know the database configuration before electing a client , we need to deactivate spring boot Datasource automatic configuration. The annotations in figure 3. will take care of that.

@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
FlywayAutoConfiguration.class})
public class PheidippidesApplication {

Figure 3 : deactivation of spring boot auto-configuration


Routing requests

The datasource routing will be managed by RoutingDataSource. This class relies on Spring’s AbstractRoutingDatasourceto that provides a usefull squeleton (figure 4).

public class RoutingDataSource extends AbstractRoutingDataSource {
private static final String REMOTE = "remote";
private static final String DEFAULT = "default";
private final Map<String, DataSource> dataSources = new HashMap<>();
public RoutingDataSource(DataSource defaultDatasource) {
dataSources.put(DEFAULT, defaultDatasource);
}
@Override
protected Object determineCurrentLookupKey() {
if (dataSources.containsKey(REMOTE))
return REMOTE;
return DEFAULT;
}
@Override
protected DataSource determineTargetDataSource() {
String lookupKey = (String) determineCurrentLookupKey();
return (DataSource) dataSources.get(lookupKey);
}
public void createRemoteDatabase(String url, String user, String password) {
if (!dataSources.containsKey(REMOTE)) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
...
dataSources.putIfAbsent(REMOTE, dataSource);
}
else
throw new IllegalStateException("Remote database has already been created");
}

Figure 4 : RoutingDataSource implementation

The class maintain a map with all configured datasources. Once a client has been elected and it’s database information has been retrieved, a new datasource is created and added to the datasource map.

Creation is done in the createDataSource method. This method creates and configures the new datasource and initiates Flyway migration if required.

DetermineCurrentLookupKey selects the key to access the target datasource in the map. As long as no « remote » database exists, the program will use the default datasource. Once a datasource is added to the map the program uses it.

Please note that RoutingDataSourceBean is instantiated as a singleton.

  public DataSource createDefaultSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(defaulDBPDriver);
    dataSource.setUrl(defaulDBUrl);
    dataSource.setUsername(defaulDBUsername);
    dataSource.setPassword(defaulDBPassword);
    return dataSource;
  }
  @Bean
  @Scope("singleton")
  public RoutingDataSource routingDatasource() {
    return new RoutingDataSource(createDefaultSource());
  }

Figure 5 : RoutingDataSource instantiation


FlyWay DB

Once a new datasource has been created, the Flyway migration is initiated, upgrading the schema if required (figure 6). The scripts are located in the db/migration directory.

  public void createRemoteDatabase(String url, String user, String password) {
    if (!dataSources.containsKey(REMOTE)) {
      ....
      Flyway flyway = Flyway.configure()
        .dataSource(url, user, password)
        .locations("db/migration")
        .load();
      flyway.migrate();
      dataSources.putIfAbsent(REMOTE, dataSource);
    }
    else
      throw new IllegalStateException("Remote database has already been created");
  }

Figure 6 : Flyway migration

The files in the migration directory are written in psql. See figure 7 for an example.

drop table IF EXISTS CONFIG CASCADE;
CREATE TABLE CONFIG
(
ID SERIAL PRIMARY KEY,
CODE varchar(50) NOT NULL,
TYPE varchar(50) NOT NULL,
VALUE text NOT NULL
);

Figure 7 : flywax migration script


EntityManagerFactory and EntityManager

Finally, two more beans need to be configured : first, entityManagerFactory. This factory is responsible to build the entitymanager associated with a persistence context.

The set of entities managed by our entity manager are defined by a persistence unit. A persistence unit « defines the set of all classes that are related or grouped by the application, and which must be colocated in their mapping to a single database« .

The factory requires a datasource, a base package to scan for entities, and a JpaVendorAdapter (figure 8).

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory(RoutingDataSource routingDatasource) {
    LocalContainerEntityManagerFactoryBean em
      = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(routingDatasource);
    em.setPackagesToScan(new String[]{"ch.nblotti.Pheidippides"});
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    em.setJpaProperties(additionalProperties());
    return em;
  }

Figure 8 : the entity manager factory

The second bean is a transaction manager (figure 9) to control transactions. The transaction manager manages the transaction boundary for our entity manager. As we are not going to access multiple transactional resources within the same transaction, we will use the JpaTransactionManager implementation.

 @Bean
  public PlatformTransactionManager transactionManager(RoutingDataSource routingDatasource) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory(routingDatasource).getObject());
    return transactionManager;
  }

Figure 9 : the transaction manager

Everything is ready. Let’s add some monitoring capacity to our project


Monitoring

We want to use Prometheus and Grafana to monitor our application (see example in figure 10)

Figure 10 : a Grafana dashboard

Prometheus is an open-source monitoring framework. It provides out-of-the-box monitoring capabilities for the Kubernetes container orchestration platform. It will collect all information. Promteheus installation is described here and here.

Grafana is an open-source lightweight dashboard tool. It can be integrated with many data sources like Prometheus, AWS cloud watch, Stackdriver, etc. Grafana installation is described here

Finally, our application uses spring boot actuator and micrometer (figure 11) to export metrics.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Figure 11 : Actuator dependencies

Available endpoints need to be configured in application.properties (figure 12).

management.endpoints.web.exposure.include=* 
management.endpoints.web.exposure.exclude=

Figure 12 : Endpoint configuration

K8s services need to be adapted (figure 13) to let prometheus know metrics should be collected for this service.

apiVersion: v1
kind: Service
metadata:
name: nblotti-pheidippides
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '8080'
prometheus.io/path: "/actuator/prometheus"
prometheus.io/scheme: "http"
spec:
selector:
app: nblotti_pheidippides
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080

Figure 13 : k8s service configuration

Et voilà ! Next time we will use Kafa, Kafka Streams, Kafka connect, Debezium and Apicurio registry to stream data to our new database.

Une réponse

  1. […] have already covered the first three states of our application topology (figure 1) in the previous articles and now we are ready to start streaming […]

Laisser un commentaireAnnuler la réponse.

En savoir plus sur 1974

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture

Quitter la version mobile