Eureka!

Lucee is an application server for the CFML (ColdFusion Markup Language) programming language, which I used for many years, initially with Railo and then with Lucee. Even though I now work exclusively with Java, I enjoy writing CFML code from time to time. Recently, I noticed that the concept of a service registry is not widely used in the CFML world, even though it offers numerous advantages. In this article, we will explore what a service registry is, focusing on Eureka Server, and how we can use it to register Lucee Instances with Eureka.

What is a Service Registry?

A service registry is an architectural pattern used to maintain a list of available services within a distributed system. It acts as a centralised directory where services can register and discover other services. This pattern is crucial in microservices architectures, where the number of services can be large, and the instances of each service can vary dynamically.

Benefits of a Service Registry

Scalability: Allows dynamic scaling of services without the need to manually configure each instance.

Resilience: Improves system resilience as services can automatically detect the failure of other instances and reroute requests accordingly.

Ease of Configuration: Reduces the complexity of configuring connections between services by centralising information about available services.

Monitoring and Management: Provides a central point for monitoring the status of service instances.

Eureka Server

Eureka Server is a discovery service developed by Netflix. It is one of the core components of the Netflix OSS project and is used to implement the service registry pattern. Eureka is designed to be highly available and scalable, offering advanced features such as replication between multiple Eureka instances to ensure data availability even in case of failures.

How Does Work Eureka

Service Registration: When a service starts, it sends a registration request to the Eureka Server. The request includes metadata about the service, such as the access URL, service name, and other relevant information.

Heartbeat: Service instances periodically send a “heartbeat” signal to Eureka to confirm they are still active. If Eureka does not receive a heartbeat within a certain time interval, it considers the instance unavailable and removes it from the registry.

Service Discovery: Client services can query Eureka to get the list of available instances for a particular service. This allows clients to dynamically find services without needing static configurations.

Problems Solved by Eureka

Managing Dynamic IP Addresses: In microservices architectures, service instances can be created and destroyed dynamically, and their IP addresses can change frequently. Eureka solves this problem by centralising information about service instances, allowing clients to discover active instances without needing to know their IP addresses in advance.

Load Balancing: Eureka facilitates load balancing between service instances. Clients can use the information provided by Eureka to distribute requests among available instances, improving load distribution and system scalability.

Automatic Failover: In case of a service instance failure, Eureka enables clients to quickly detect the unavailable instance and reroute requests to other active instances. This increases system resilience and reduces downtime.

Centralised Configuration: Eureka centralise the configuration of service information, reducing the complexity of managing connections between services. This is particularly useful in environments with many services that change frequently.

How to register a Lucee Instances with Eureka

application.yml

spring:
  application:
    name: eureka-server

server:
  port: 8761

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

Maven.xml

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

SpringBoot Application

@EnableEurekaServer
@SpringBootApplication
public class DemoEurekaApplication {

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

}
2024-07-11T16:31:09.826+02:00  INFO 3580 --- [eureka-server] [on(2)-127.0.0.1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms
2024-07-11T16:32:09.320+02:00  INFO 3580 --- [eureka-server] [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry  : Running the evict task with compensationTime 0ms

Integrate the Eureka REST Operations API

https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

component displayname="EurekaClient" hint="Client for Eureka Service Registry" {

    // Configure the default Eureka server details
    this.eurekaServerUrl = "http://localhost:8761/eureka";
    this.appName = "MyCFMLApp";
    this.instanceId = createUUID();
    this.instancePort = cgi.server_port; // Use the server port from CGI

    // Constructor to initialize the Eureka client with custom settings
    public EurekaClient function init(struct config={}) {
        if (structKeyExists(config, "eurekaServerUrl")) {
            this.eurekaServerUrl = config.eurekaServerUrl;
        }
        if (structKeyExists(config, "appName")) {
            this.appName = config.appName;
        }
        if (structKeyExists(config, "instanceId")) {
            this.instanceId = config.instanceId;
        }
        if (structKeyExists(config, "instancePort")) {
            this.instancePort = config.instancePort;
        }
        return this;
    }

    // Function to register the application with Eureka
    public void function register() {
        var registrationData = {
            "instance": {
                "instanceId": this.instanceId,
                "hostName": cgi.server_name,
                "app": this.appName,
                "ipAddr": cgi.remote_addr,
                "status": "UP",
                "port": {
                    "$": this.instancePort,
                    "@enabled": "true"
                },
                "dataCenterInfo": {
                    "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
                    "name": "MyOwn"
                }
            }
        };

        var httpResponse = httpRequest(
                method="POST",
                endpoint="#this.eurekaServerUrl#/apps/#this.appName#",
                headers={
                    "Content-Type" = "application/json"
                },
                body=serializeJSON(registrationData)
            );

        if (httpResponse.statusCode != 204) {
            throw(type="Application", message="Failed to register with Eureka. Status code: #httpResponse.statusCode#");
        }
    }

    // Function to send a heartbeat to Eureka
    public void function sendHeartbeat() {
        var httpResponse = httpRequest(
            method="PUT",
            endpoint="#this.eurekaServerUrl#/apps/#this.appName#/#this.instanceId#"
            );


        writeLog( text=httpResponse.statusCode, file="Heartbeat" );

        if (httpResponse.statusCode != 200) {
            throw(type="Application", message="Failed to send heartbeat to Eureka. Status code: #httpResponse.statusCode#");
        }
    }

    // Function to get all registered instances
    public struct function getRegisteredInstances() {
        var httpResponse = httpRequest(
            method="GET",
            endpoint="#this.eurekaServerUrl#/apps",
            headers={
                "Accept": "application/json"
            }
                );

        if (httpResponse.statusCode != 200) {
            throw(type="Application", message="Failed to retrieve instances from Eureka. Status code: #httpResponse.statusCode#");
        }

        return deserializeJSON(httpResponse.fileContent);
    }

    // Function to get instances of a specific application
    public struct function getInstancesByAppName(string appName) {
        var httpResponse = httpRequest(
            method="GET",
            endpoint="#this.eurekaServerUrl#/apps/#appName#",
            headers={
                "Accept": "application/json"
            }
                );

        if (httpResponse.statusCode != 200) {
            throw(type="Application", message="Failed to retrieve instances from Eureka for app: #appName#. Status code: #httpResponse.statusCode#");
        }

        return deserializeJSON(httpResponse.fileContent);
    }

    // Helper function to send HTTP requests
    private struct function httpRequest(string method, string endpoint, struct headers={}, string body="") {

        var httpService = new Http();

        httpService.setMethod(method);
        httpService.setUrl(endpoint);
        httpService.setCharset( "utf-8" );

        for (var headerName in headers) {
            httpService.addParam( type="header", name=headerName, value=headers[headerName] );
        }

        if (body != "") {
            httpService.addParam( type="body", value=body );
        }

        return httpService.send().getPrefix();
    }

}

config = {instanceid = "18C8F3FF-B0C3-4DBC-836915E8DBA55431"} ;

// registerLucee.cfm
// Register Lucee Server

eurekaClient = new EurekaClient( );
eurekaClient.register();

// heartbeat.cfm
// Heartbeat 

eurekaClient = new EurekaClient( config );
eurekaClient.sendHeartbeat();

// eureka.cfm

eurekaClient = new EurekaClient( config );
instances = eurekaClient.getRegisteredInstances();

writeDump( instances );

http://localhost:8761/

http://localhost:8888/eureka.cfm

Comments are closed