Revision #1 on Apr 8th, 2020: Fixed Sidecar application name in accompanying source code, configuration and logs.

Spring Cloud Series

Subscribe to my newsletter to receive updates when content like this is published.

  1. Developing Microservices using Spring Boot, Jersey, Swagger and Docker
  2. Integration Testing using Spring Boot, Postgres and Docker
  3. Services registration and discovery using Spring Cloud Netflix Eureka Server and client-side load-balancing using Ribbon and Feign
  4. Centralized and versioned configuration using Spring Cloud Config Server and Git
  5. Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server
  6. Microservices Sidecar pattern implementation using Postgres, Spring Cloud Netflix and Docker (you are here)
  7. Implementing Circuit Breaker using Hystrix, Dashboard using Spring Cloud Turbine Server (work in progress)

1. OVERVIEW

What’s a Sidecar? A Sidecar is a companion application of the main service, typically non-JVM, either developed in-house or a 3rd party service (eg Elastic Search, Apache Solr, etc.) where it’s desirable for them to take advantage of other infrastructure services such as service registration and discovery, routing, dynamic configuration, monitoring, etc..

Implementing the Sidecar pattern using Spring Cloud Netflix, Postgres, Docker

This post covers implementing a Sidecar Java application attached to a Postgres database bundled in a Docker image and a Demo client application connecting to the database after retrieving the Postgres metadata (eg host, port) from a Eureka registry.

2. REQUIREMENTS

  • Java 7+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.
  • A Eureka server instance for the Spring Cloud Netflix Sidecar application to register the bundled Postgres server with.
  • Docker, local or remote host.

3. THE SIDECAR APPLICATION

can be created like any other Spring Cloud app, from your preferred IDE, http://start.spring.io or from command line:

curl "https://start.spring.io/starter.tgz"
 -d bootVersion=1.5.9.RELEASE
 -d dependencies=actuator,cloud-eureka
 -d language=java
 -d type=maven-project
 -d baseDir=Sidecar
 -d groupId=com.asimio.cloud
 -d artifactId=sidecar
 -d version=0-SNAPSHOT
 | tar -xzvf -

This command will create a Maven project in a folder named Sidecar with most of the dependencies used in the accompanying source code for this post.

The Sidecar relevant files are discussed next:

pom.xml:

...
<properties>
  ...
  <spring-cloud.version>Dalston.SR4</spring-cloud.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-sidecar</artifactId>
  </dependency>
...
</dependencies>

spring-cloud-starter-eureka includes Eureka client support for applications to register and / or discovery services metadata with / from a remote Eureka server.

spring-cloud-netflix-sidecar provides beans Spring autoconfiguration for a companion application to take care of the service registration and / or discovery of 3rd party services (JVM or not).

SidecarApplication.java:

package com.asimio.cloud.sidecar;
...
@SpringBootApplication
@EnableSidecar
public class SidecarApplication {

  public static void main(String[] args) {
    SpringApplication.run(SidecarApplication.class, args);
  }
}

@EnableSidecar annotation might be all needed for this Java application to behave as a companion Sidecar to a 3rd party service (JVM or not) running in the same runtime unit (eg host, VM, Docker container) whose metadata will be registered with a Eureka server.

In the case of non-JVM in-house services, all is needed from them is to provide a Spring Boot health-like endpoint returning something like:

{
  "status":"UP"
}

In the case of external services such as Postgres, Elastic Search, Kafka, etc., which likely won’t provide a health check as previously described, the Sideacar app itself could be used to implement such requirement:

SidecarHealthIndicator.java:

package com.asimio.cloud.sidecar.healthcheck;
...
public interface SidecarHealthIndicator extends HealthIndicator {

}

AppConfig.java:

package com.asimio.cloud.sidecar.config;
...
@Configuration
public class AppConfig {

  @ConditionalOnProperty(name = "sidecar.postgres.enabled", havingValue = "true", matchIfMissing = false)
  @Bean
  public SidecarHealthIndicator postgresHealthCheck() {
    return new PostgresHealthCheck();
  }
}

Assumming a Sidecar instance is going to serve as a companion to only one service, it translates to a single SidecarHealthIndicator bean needed per Sidecar application, a PostgresHealthCheck instance in this demo.

Let’s backup a little bit, how can it be verified if a Postgres DB is acceping connections? It turns out pg_isready, a Postgres command accomplishes it:

pg_isready -U postgres -h localhost -p 5432
localhost:5432 - rejecting connections

And this is what PostgresHealthCheck.java does:

package com.asimio.cloud.sidecar.healthcheck.postgres;
...
public class PostgresHealthCheck implements SidecarHealthIndicator {
...
  // pg_isready U <user> -h localhost -p <sidecarPort>
  private static final String COMMAND_PATTERN = "pg_isready -U %s -h localhost -p %s";

  @Value("${sidecar.port}")
  private int sidecarPort;

  @Override
  public Health health() {
    Health.Builder result = null;
    try {
      String output = this.runCommand();
      LOGGER.info(output);
      if (output.indexOf("accepting connections") != -1) {
        result = Health.up();
      } else if (output.indexOf("rejecting connections") != -1 || output.indexOf("no response") != -1) {
        result = Health.down().withDetail("reason", output);
      }
    } catch (IOException e) {
      LOGGER.warn("Failed to execute command.", e);
      result = Health.down().withException(e);
    }
    return result.build();
  }
...
}

The health() method will return:

{
  "status":"UP"
}

or

{
  "status":"DOWN"
  ...
}

depending on the output of the OS command.

The last piece of code needed in this case, where the service desired to register with a Eureka server is outside of our control, is to expose its Health information via an endpoint the Eureka server can send requests to.

LocalStatusDelegatorController.java:

package com.asimio.cloud.sidecar.web;
...
@RestController
public class LocalStatusDelegatorController {

  @Autowired
  private SidecarHealthIndicator healthIndicator;

  @RequestMapping("/delegating-status")
  public Health sidecarHealthStatus() {
    return this.healthIndicator.health();
  }
}

The Sidecar application exposes the endpoint /delegating-status which uses a health check to run a specific OS command to verify if the 3rd party service is usable.

Let’s look at the Sidecar configuration files:

bootstrap.yml:

# Generic app name.
# Should be passed from command line (eg java -jar ... --spring.application.name=POSTGRES-DB_DVDRENTAL ...)
spring:
  application:
    name: generic-sidecar

The spring.application.name property value is what’s going to be used for registration and discovery purposes.

application.yml:

...
sidecar:
  hostname: localhost
  # port should be passed from command line (eg java -jar ... --sidecar.port=5432 ...)
  port: 5432
  # health-uri the service uri returning health data in the form of { "status": "UP" } or
  # http://localhost:${sidecar.port}/${health-uri:health.json} if the service provides such endpoint.
  health-uri: http://localhost:${server.port}/delegating-status
  # Sidecar controller
  home-page-uri: http://${sidecar.hostname}:${server.port}/
  postgres:
    enabled: true
...
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
  instance:
    appname: ${spring.application.name}
    hostname: ${sidecar.hostname}
    statusPageUrlPath: ${management.context-path}/info
    healthCheckUrlPath: ${sidecar.health-uri}
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${sidecar.port}
...

Notice the sidecar.hostname property (used in Eureka client configuration) is hard-coded to localhost because the Sidecar companion application is supposed to run in the same host / VM / Docker container / … as the 3rd party service intended to be discovered.

Other relevant settings are the sidecar.port property set to the port the 3rd party service listens on. The sidecar.health-uri property pointing to the /delegating-status endpoint, used by the Eureka server to get information about the availability of the service. And sidecar.postgres.enabled, which causes the application to act as a Sidecar for Postgres.

It still need to be built so that the resulting artifact could be used in a Docker image along with Postgres.

mvn clean package

which should create the application target/sidecar.jar.

4. SETUP POSTGRES DVD RENTAL DATABASE AND SIDECAR APPLICATION IN A DOCKER IMAGE

asimio/db_dvdrental Docker image, used in Integration Testing using Spring Boot, Postgres and Docker would be the starting point to bundle a Postgres DB with the Sidecar companion application.

cp target/sidecar.jar /<path to>/postgres/db_dvdrental-sidecar/scripts/

Dockerfile:

FROM asimio/db_dvdrental:latest
MAINTAINER Orlando L Otero ootero@asimio.net, https://bitbucket.org/asimio/postgres
# Manually build using command: docker build -t asimio/db_dvdrental-sidecar:latest .

# Install JDK
RUN \
  mkdir -p /usr/lib && \
  wget --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u162-b12/0da788060d494f5095bf8624735fa2f1/jdk-8u162-linux-x64.tar.gz  && \
  tar -zxf jdk-8u162-linux-x64.tar.gz -C /usr/lib && \
  ln -s /usr/lib/jdk1.8.0_162 /usr/lib/jdk && \
  chown -R root:root /usr/lib/jdk && \
  rm jdk-8u162-linux-x64.tar.gz  

ENV JAVA_HOME="/usr/lib/jdk"
ENV PATH="$JAVA_HOME/bin:$PATH"
ENV JAVA_TOOL_OPTIONS="-Xms256M -Xmx256M -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"
ENV SPRING_APPLICATION_NAME="POSTGRES-DB_DVDRENTAL"

COPY scripts/sidecar.jar /opt/asimio-cloud/sidecar.jar
ADD scripts/sidecar.sh /docker-entrypoint-initdb.d/
RUN chmod 755 /docker-entrypoint-initdb.d/sidecar.sh

This Dockerfile bundles a Postgres DB, the Sidecar application and a shell file to start the Sidecar app in the background when a Docker container is started.

sidecar.sh:

#!/bin/bash

echo "Starting sidecar application: ${SPRING_APPLICATION_NAME}"
java -jar -Dspring.application.name=${SPRING_APPLICATION_NAME} /opt/asimio-cloud/sidecar.jar &

The image can be built running:

cd /<path to>/postgres/db_dvdrental-sidecar
docker build -t asimio/db_dvdrental-sidecar:latest .
...
Successfully built 3955eae0bf98
Successfully tagged asimio/db_dvdrental-sidecar:latest

5. STARTING THE EUREKA SERVER

These Demo was run using Docker where comunication between containers is needed, so I’ll first create a Docker network for them to run on:

docker network create -d bridge --subnet 172.25.0.0/16 sidecarpostgresdemo_default
4c68647c5a20c8b010f33d2fc26d3a4e751cfb4ec7571984cdcec4002313c3a6

docker run -idt -p 8000:8000 --network=sidecarpostgresdemo_default -e spring.profiles.active=standalone -e server.port=8000 -e hostName=$HOSTNAME asimio/discovery-server:1.0.73
8726b16b6abeca71dca7d882b5c30edad3c6c450959ac70b9de396bd444d8490

docker logs 87
...
2018-02-13 04:10:21.156  INFO 1 --- [      Thread-10] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
2018-02-13 04:10:21.201  INFO 1 --- [           main] b.c.e.u.UndertowEmbeddedServletContainer : Undertow started on port(s) 8000 (http)
2018-02-13 04:10:21.202  INFO 1 --- [           main] c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 8000
2018-02-13 04:10:21.207  INFO 1 --- [           main] c.a.c.eureka.EurekaServerApplication     : Started EurekaServerApplication in 6.493 seconds (JVM running for 7.121)

curl http://localhost:8000/eureka/apps
<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode></apps__hashcode>
</applications>

Logs and a request to Eureka server confirm it started successfully and no application has been registered yet.

6. STARTING THE POSTGRES DVD RENTAL DATABASE AND REGISTERING IT WITH EUREKA VIA THE SIDECAR APP

Similarly to starting the Eureka server, a container including Postgres and Sidecar apps is started specifying the same network for containers to reach each other:

docker run -d -p 5432:5432 -p 8080:8080 --network=sidecarpostgresdemo_default -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e DB_NAME=db_dvdrental -e DB_USER=user_dvdrental -e DB_PASSWD=changeit -e sidecar.port=5432 -e eureka.client.serviceUrl.defaultZone=http://172.25.0.2:8000/eureka/ asimio/db_dvdrental-sidecar:latest
252c8fc4703dde2208344e761eaa69ef6ad01b8cf5481fcb4210017848bb41bf

docker logs 25
...
/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/db-init.sh
Verifying DB db_dvdrental presence ...
db_dvdrental DB does not exist, creating it ...
Verifying role user_dvdrental presence ...
user_dvdrental role does not exist, creating it ...
CREATE ROLE
user_dvdrental role successfully created
CREATE DATABASE
GRANT
db_dvdrental DB successfully created

/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/db-restore.sh
Importing data into DB db_dvdrental
db_dvdrental DB restored from backup
Granting permissions in DB 'db_dvdrental' to role 'user_dvdrental'.
GRANT
GRANT
Permissions granted

/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/sidecar.sh
Starting sidecar application: POSTGRES-DB_DVDRENTAL
...
2018-02-13 04:40:06.683  INFO 136 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_POSTGRES-DB_DVDRENTAL/252c8fc4703d:POSTGRES-DB_DVDRENTAL:8080: registering service...
...
2018-02-13 04:40:06.858  INFO 136 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-02-13 04:40:06.861  INFO 136 --- [           main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080
2018-02-13 04:40:06.867  INFO 136 --- [           main] c.a.cloud.sidecar.SidecarApplication     : Started SidecarApplication in 7.395 seconds (JVM running for 7.924)
2018-02-13 04:40:06.982  INFO 136 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_POSTGRES-DB_DVDRENTAL/252c8fc4703d:POSTGRES-DB_DVDRENTAL:8080 - registration status: 204
2018-02-13 04:40:07.087  INFO 136 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-02-13 04:40:07.087  INFO 136 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2018-02-13 04:40:07.108  INFO 136 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 20 ms
2018-02-13 04:40:07.126  WARN 136 --- [nio-8080-exec-2] o.s.c.n.zuul.web.ZuulHandlerMapping      : No routes found from RouteLocator
2018-02-13 04:40:07.204  INFO 136 --- [nio-8080-exec-2] c.a.c.s.h.postgres.PostgresHealthCheck   : localhost:5432 - accepting connections

See how a Postgres DB is setup first, then the Sidecar companion app successfully started and ... PostgresHealthCheck : localhost:5432 - accepting connections log indicates the Sidecar is able to connect to the Postgres server.

Sending a request to the Eureka server now results in a POSTGRES-DB_DVDRENTAL service metadata stored in the registry, with [host=172.25.0.3, port=5432].

curl http://localhost:8000/eureka/apps
<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode>UP_1_</apps__hashcode>
  <application>
    <name>POSTGRES-DB_DVDRENTAL</name>
    <instance>
      <instanceId>30117a3b2bbb:POSTGRES-DB_DVDRENTAL:8080</instanceId>
      <hostName>172.25.0.3</hostName>
      <app>POSTGRES-DB_DVDRENTAL</app>
      <ipAddr>172.25.0.3</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">5432</port>
      <securePort enabled="false">443</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1518584036042</registrationTimestamp>
        <lastRenewalTimestamp>1518584036042</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1518584036058</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <instanceId>POSTGRES-DB_DVDRENTAL:5432</instanceId>
      </metadata>
      <homePageUrl>http://localhost:5432/</homePageUrl>
      <statusPageUrl>http://localhost:8080/info</statusPageUrl>
      <healthCheckUrl>http://localhost:8080/health</healthCheckUrl>
      <vipAddress>POSTGRES-DB_DVDRENTAL</vipAddress>
      <secureVipAddress>POSTGRES-DB_DVDRENTAL</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1518584036058</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1518584036033</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
</applications>

7. THE POSTGRES CLIENT DEMO APPLICATION

Setting up a Demo client application could be done following a similar approach as when creating the sidecar application.

pom.xml:

...
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
</dependency>
...

These are the main dependencies needed for the Demo Postgres client application to locate and connect to a Postgres DB.

SidecarPostgresDemoApplication.java:

package com.asimio.demo;
...
@SpringBootApplication
@EnableEurekaClient
public class SidecarPostgresDemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(SidecarPostgresDemoApplication.class, args);
  }
}

@EnableEurekaClient annotation along with configuration properties allow this application to register and / or discover metadata from a Eureka server.

AppConfig.java:

package com.asimio.demo.config;
...
@Configuration
@EnableConfigurationProperties({ DataSourceProperties.class })
public class AppConfig {
  ...
  @Autowired
  private DataSourceProperties dsProperties;

  @Autowired
  private DiscoveryClient discoveryClient;

  @Value("${sidecar.appName:POSTGRES-DB_DVDRENTAL}")
  private String dbServiceName;

  @Bean
  public DataSource dataSource() {
    ...
    ServiceInstance instance = this.discoveryClient.getInstances(this.dbServiceName).iterator().next();
    ...
    return this.createDataSource(instance.getHost(), instance.getPort());
  }

  private DataSource createDataSource(String host, int port) {
    String jdbcUrl = String.format(this.dsProperties.getUrl(), host, port);
    ...
    DataSourceBuilder factory = DataSourceBuilder
        .create()
        .url(jdbcUrl)
        .username(this.dsProperties.getUsername())
        .password(this.dsProperties.getPassword())
        .driverClassName(this.dsProperties.getDriverClassName());
    return factory.build();
  }
}

A DataSource bean is instantiated instead of relying in DataSourceAutoConfiguration because the hostname and port the Postgres server listens on is unknown until Eureka responds back with metadata.

application.yml:

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  datasource:
    name: db_dvdrental
    # Placeholder to be replaced at runtime with metadata retrieved from Registration server
    url: jdbc:postgresql://%s:%s/db_dvdrental
    username: user_dvdrental
    password: changeit
    driverClassName: org.postgresql.Driver
    
  jpa:
    database: POSTGRESQL
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    generate-ddl: false
    hibernate:
      ddl-auto: none
...
sidecar.appName: POSTGRES-DB_DVDRENTAL

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
...

Notice the JDBC url includes placeholders to be replaced with values coming from Eureka.

mvn clean package -DskipTests
...
mvn docker:build
...

I’ll skip runnning the integration tests for now because this demo application needs a Eureka and Postgres servers to connect to while loading the Spring context. In Integration Testing using Spring Boot, Postgres and Docker I covered how to start dependant services before running each test.

8. STARTING THE POSTGRES CLIENT DEMO

Running this demo in a Docker container requires the usage of the same network the other two containers use:

docker run -idt -p 8090:8090 --network=sidecarpostgresdemo_default -e server.port=8090 -e eureka.client.serviceUrl.defaultZone=http://172.25.0.2:8000/eureka/ asimio/sidecar-postgres-demo:latest
671a60617aab2134e2a037519b56e349beb01b5c88787d6280d043e21e85ef5b

docker logs 67
...
2018-02-13 05:03:48.248  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http)
2018-02-13 05:03:48.249  INFO 1 --- [           main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8090
2018-02-13 05:03:48.255  INFO 1 --- [           main] c.a.demo.SidecarPostgresDemoApplication  : Started SidecarPostgresDemoApplication in 9.586 seconds (JVM running for 10.059)

Now that it has started, lets send a couple of requests to an endpoint which should successfully execute a DB query:

curl http://localhost:8090/actors/1
[actor: Penelope Guiness]

curl http://localhost:8090/actors/2
[actor: Nick Wahlberg]

A relevant note before finishing showing how to implement the Sidecar pattern using Postgres, Spring Cloud Netflix and Docker.

You probably noticed I have started the Eureka server Docker container first, then a Postgres container bundled with a Sidecar Java application and lastly the Demo Restful service container which connects to a Postgres DB once it finds its metadata from Eureka. I actually included a Docker compose file in an attempt to simplify and automate this process but not without its challenges.

Connection-related problems arose when starting the Demo API application but the Eureka server hasn’t started yet or when the Postgres host metadata is not still available in the Eureka server while instantiating the Datasource bean.

I believe it would be a good practice for microservices to recover themselves, to self heal from a situation like this, where services startup order is ideal but not required, where an application would keep trying to connect to dependant services for a given time and / or a number of attempts. Would this be a concern of the application or a concern of some kind of platform orchestrator? Stay tuned, I might follow-up with an implementation of this approach in another post.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

9. SOURCE CODE

Accompanying source code for this blog post can be found at:

10. REFERENCES